All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.gatling.internal.quicklens.QuicklensMacros.scala Maven / Gradle / Ivy

/*
 * Copyright 2011-2024 GatlingCorp (https://gatling.io)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// the original sources from https://github.com/softwaremill/quicklens are lacking file headers.
// we'll add them as soon as they are added upstream.
package io.gatling.internal.quicklens

import scala.annotation.tailrec
import scala.reflect.macros.blackbox

@SuppressWarnings(Array("org.wartremover.warts.Recursion"))
object QuicklensMacros {
  private val ShapeInfo = "Path must have shape: _.field1.field2.each.field3.(...)"

  /**
   * modify(a)(_.b.c) => new PathMod(a, (A, F) => A.copy(b = A.b.copy(c = F(A.b.c))))
   */
  def modify_impl[T: c.WeakTypeTag, U: c.WeakTypeTag](c: blackbox.Context)(obj: c.Expr[T])(
      path: c.Expr[T => U]
  ): c.Tree = modifyUnwrapped(c)(obj, modificationForPath(c)(path))

  /**
   * modifyAll(a)(_.b.c, _.d.e) => new PathMod(a, << chained modifications >>)
   */
  def modifyAll_impl[T: c.WeakTypeTag, U: c.WeakTypeTag](c: blackbox.Context)(obj: c.Expr[T])(
      path1: c.Expr[T => U],
      paths: c.Expr[T => U]*
  ): c.Tree = modifyUnwrapped(c)(obj, modificationsForPaths(c)(path1, paths))

  /**
   * A helper method for modify_impl and modifyAll_impl.
   */
  private def modifyUnwrapped[T: c.WeakTypeTag, U: c.WeakTypeTag](c: blackbox.Context)(
      obj: c.Expr[T],
      modifications: c.Tree
  ): c.Tree = {
    import c.universe._
    q"_root_.io.gatling.internal.quicklens.PathModify($obj, $modifications)"
  }

  /**
   * modify[A](_.b.c) => a => new PathMod(a, (A, F) => A.copy(b = A.b.copy(c = F(A.b.c))))
   */
  def modifyLazy_impl[T: c.WeakTypeTag, U: c.WeakTypeTag](c: blackbox.Context)(
      path: c.Expr[T => U]
  ): c.Tree = modifyLazyUnwrapped(c)(modificationForPath(c)(path))

  /**
   * modifyAll[A](_.b.c, _.d.e) => a => new PathMod(a, << chained modifications >>)
   */
  def modifyLazyAll_impl[T: c.WeakTypeTag, U: c.WeakTypeTag](c: blackbox.Context)(
      path1: c.Expr[T => U],
      paths: c.Expr[T => U]*
  ): c.Tree = modifyLazyUnwrapped(c)(modificationsForPaths(c)(path1, paths))

  /**
   * A helper method for modify_impl and modifyAll_impl.
   */
  private def modifyLazyUnwrapped[T: c.WeakTypeTag, U: c.WeakTypeTag](c: blackbox.Context)(
      modifications: c.Tree
  ): c.Tree = {
    import c.universe._
    q"_root_.io.gatling.internal.quicklens.PathLazyModify($modifications)"
  }

  /**
   * a.modify(_.b.c) => new PathMod(a, (A, F) => A.copy(b = A.b.copy(c = F(A.b.c))))
   */
  def modifyPimp_impl[T: c.WeakTypeTag, U: c.WeakTypeTag](c: blackbox.Context)(
      path: c.Expr[T => U]
  ): c.Tree = modifyWrapped(c)(modificationForPath(c)(path))

  /**
   * a.modify(_.b.c, _.d.e) => new PathMod(a, << chained modifications >>)
   */
  def modifyAllPimp_impl[T: c.WeakTypeTag, U: c.WeakTypeTag](c: blackbox.Context)(
      path1: c.Expr[T => U],
      paths: c.Expr[T => U]*
  ): c.Tree = modifyWrapped(c)(modificationsForPaths(c)(path1, paths))

  /**
   * A helper method for modifyPimp_impl and modifyAllPimp_impl.
   */
  def modifyWrapped[T: c.WeakTypeTag, U: c.WeakTypeTag](c: blackbox.Context)(
      modifications: c.Tree
  ): c.Tree = {
    import c.universe._

    val wrappedValue = c.macroApplication match {
      case Apply(TypeApply(Select(Apply(_, List(w)), _), _), _) => w
      case _                                                    => c.abort(c.enclosingPosition, s"Unknown usage of ModifyPimp. Please file a bug.")
    }

    val valueAlias = TermName(c.freshName())

    q"""{
      val $valueAlias = $wrappedValue;
      _root_.io.gatling.internal.quicklens.PathModify(${Ident(valueAlias)}, $modifications)
     }"""
  }

  /**
   * Compose modifications generated for each path, from left to right.
   */
  private def modificationsForPaths[T: c.WeakTypeTag, U: c.WeakTypeTag](c: blackbox.Context)(
      path1: c.Expr[T => U],
      paths: Seq[c.Expr[T => U]]
  ): c.Tree = {
    import c.universe._

    val valueName = TermName(c.freshName())
    val modifierName = TermName(c.freshName())

    val modification1 = q"${modificationForPath(c)(path1)}($valueName, $modifierName)"
    val chained = paths.foldLeft(modification1) { case (tree, path) =>
      val modification = modificationForPath(c)(path)
      q"$modification($tree, $modifierName)"
    }

    val valueArg = q"val $valueName: ${weakTypeOf[T]}"
    val modifierArg = q"val $modifierName: (${weakTypeOf[U]} => ${weakTypeOf[U]})"
    q"($valueArg, $modifierArg) => $chained"
  }

  /**
   * Produce a modification for a single path.
   */
  private def modificationForPath[T: c.WeakTypeTag, U: c.WeakTypeTag](c: blackbox.Context)(
      path: c.Expr[T => U]
  ): c.Tree = {
    import c.universe._

    sealed trait PathAccess
    case object DirectPathAccess extends PathAccess
    final case class SealedPathAccess(types: Set[Symbol]) extends PathAccess

    sealed trait PathElement
    final case class TermPathElement(term: c.TermName, access: PathAccess, xargs: c.Tree*) extends PathElement
    final case class SubtypePathElement(subtype: c.Type) extends PathElement
    final case class FunctorPathElement(functor: c.Tree, method: c.TermName, xargs: c.Tree*) extends PathElement

    /**
     * Determine if the `.copy` method should be applied directly or through a match across all subclasses (for sealed traits).
     */
    def determinePathAccess(typeSymbol: Symbol): PathAccess = {
      def ifEmpty[A](set: Set[A], empty: => Set[A]) =
        if (set.isEmpty) empty else set

      def knownDirectSubclasses(sealedSymbol: ClassSymbol) = ifEmpty(
        sealedSymbol.knownDirectSubclasses,
        c.abort(
          c.enclosingPosition,
          s"""Could not find subclasses of sealed trait $sealedSymbol.
             |You might need to ensure that it gets compiled before this invocation.
             |See also: .""".stripMargin
        )
      )

      def expand(symbol: Symbol): Set[Symbol] =
        Set(symbol)
          .filter(_.isClass)
          .map(_.asClass)
          .map { s =>
            s.typeSignature; s
          } // see 
          .filter(_.isSealed)
          .flatMap(s => knownDirectSubclasses(s))
          .flatMap(s => ifEmpty(expand(s), Set(s)))

      val subclasses = expand(typeSymbol)
      if (subclasses.isEmpty) DirectPathAccess else SealedPathAccess(subclasses)
    }

    /**
     * _.a.b.each.c => List(TPE(a), TPE(b), FPE(functor, each/at/eachWhere, xargs), TPE(c))
     */
    @tailrec
    def collectPathElements(tree: c.Tree, acc: List[PathElement]): List[PathElement] = {
      def methodSupported(method: TermName) =
        Seq("at", "eachWhere", "atOrElse", "index").contains(method.toString)
      def typeSupported(quicklensType: c.Tree) =
        Seq("QuicklensEach", "QuicklensAt", "QuicklensMapAt", "QuicklensWhen", "QuicklensEither", "QuicklensSingleAt")
          .exists(quicklensType.toString.endsWith)
      tree match {
        case q"$parent.$child" =>
          val access = determinePathAccess(parent.tpe.typeSymbol)
          collectPathElements(parent, TermPathElement(child, access) :: acc)
        case q"$tpname[..$_]($parent).when[$tp]" if typeSupported(tpname) =>
          collectPathElements(parent, SubtypePathElement(tp.tpe) :: acc)
        case q"$parent.$method(..$xargs)" if methodSupported(method) =>
          collectPathElements(parent, TermPathElement(method, DirectPathAccess, xargs: _*) :: acc)
        case q"$tpname[..$_]($t)($f)" if typeSupported(tpname) =>
          val newAcc = (acc: @unchecked) match {
            // replace the term controlled by quicklens
            case TermPathElement(term, _, xargs @ _*) :: rest => FunctorPathElement(f, term, xargs: _*) :: rest
            case pathEl :: _ =>
              c.abort(c.enclosingPosition, s"Invalid use of path element $pathEl. $ShapeInfo, got: ${path.tree}")
          }
          collectPathElements(t, newAcc)
        case t: Ident => acc
        case _        => c.abort(c.enclosingPosition, s"Unsupported path element. $ShapeInfo, got: $tree")
      }
    }

    /**
     * (x, List(TPE(c), TPE(b), FPE(functor, method, xargs), TPE(a))) => x.b.c
     */
    def generateSelects(rootPathEl: c.TermName, reversePathEls: List[PathElement]): c.Tree = {
      @tailrec
      def terms(els: List[PathElement], result: List[c.TermName]): List[c.TermName] =
        (els: @unchecked) match {
          case Nil                               => result
          case TermPathElement(term, _) :: tail  => terms(tail, term :: result)
          case SubtypePathElement(_) :: _        => result
          case FunctorPathElement(_, _, _*) :: _ => result
        }

      @tailrec
      def go(els: List[c.TermName], result: c.Tree): c.Tree =
        els match {
          case Nil => result
          case pathEl :: tail =>
            val select = q"$result.$pathEl"
            go(tail, select)
        }

      go(terms(reversePathEls, Nil), Ident(rootPathEl))
    }

    /**
     * (tree, DirectPathAccess) => f(tree)
     *
     * (tree, SealedPathAccess(Set(T1, T2, ...)) => tree match { case x1: T1 => f(x1) case x2: T2 => f(x2) ... }
     */
    def generateAccess(tree: c.Tree, access: PathAccess)(f: c.Tree => c.Tree) = access match {
      case DirectPathAccess => f(tree)
      case SealedPathAccess(types) =>
        val cases = types map { tp =>
          val pat = TermName(c.freshName())
          cq"$pat: $tp => ${f(Ident(pat))}"
        }
        q"$tree match { case ..$cases }"
    }

    /**
     * (a, List(TPE(d), TPE(c), FPE(functor, method, xargs), TPE(b)), k) => (aa, aa.copy(b = functor.method(aa.b, xargs)(a => a.copy(c = a.c.copy(d = k)))
     */
    def generateCopies(
        rootPathEl: c.TermName,
        reversePathEls: List[PathElement],
        newVal: c.Tree
    ): (c.TermName, c.Tree) =
      (reversePathEls: @unchecked) match {
        case Nil => (rootPathEl, newVal)
        case TermPathElement(pathEl, access) :: tail =>
          val selectCurrVal = generateSelects(rootPathEl, tail)
          val copy = generateAccess(selectCurrVal, access) { currVal =>
            q"$currVal.copy($pathEl = $newVal)"
          }
          generateCopies(rootPathEl, tail, copy)
        case SubtypePathElement(subtype) :: tail =>
          val newRootPathEl = TermName(c.freshName())
          val intactPathEl = TermName(c.freshName())
          val selectCurrVal = generateSelects(newRootPathEl, tail)
          val cases = Seq(
            cq"$rootPathEl: $subtype => $newVal",
            cq"$intactPathEl => ${Ident(intactPathEl)}"
          )
          val modifySubtype = q"$selectCurrVal match { case ..$cases }"
          generateCopies(newRootPathEl, tail, modifySubtype)
        case FunctorPathElement(functor, method, xargs @ _*) :: tail =>
          val newRootPathEl = TermName(c.freshName())
          // combine the selected path with variable args
          val args = generateSelects(newRootPathEl, tail) :: xargs.toList.map(c.untypecheck(_))
          val rootPathElParamTree = ValDef(Modifiers(), rootPathEl, TypeTree(), EmptyTree)
          val functorMap = q"$functor.$method(..$args)(($rootPathElParamTree) => $newVal)"
          generateCopies(newRootPathEl, tail, functorMap)
      }

    //

    val pathEls = path.tree match {
      case q"($arg) => $pathBody" => collectPathElements(pathBody, Nil)
      case _                      => c.abort(c.enclosingPosition, s"$ShapeInfo, got: ${path.tree}")
    }

    // the initial root object (the end-root object can be different if there are .each's on the way)
    val initialRootPathEl = TermName(c.freshName())
    val fn = TermName(c.freshName()) // the function that modifies the last path element

    val reversePathEls = pathEls.reverse

    // new value of the last path element is an invocation of $fn on the current last path element value
    val select = generateSelects(initialRootPathEl, reversePathEls)
    val mod = q"$fn($select)"

    val (rootPathEl, copies) = generateCopies(initialRootPathEl, reversePathEls, mod)

    val rootPathElParamTree = q"val $rootPathEl: ${weakTypeOf[T]}"
    val fnParamTree = q"val $fn: (${weakTypeOf[U]} => ${weakTypeOf[U]})"

    q"($rootPathElParamTree, $fnParamTree) => $copies"
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy