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

scoverage.ScoveragePlugin.scala Maven / Gradle / Ivy

The newest version!
package scoverage

import java.io.File
import java.util.concurrent.atomic.AtomicInteger

import scala.reflect.internal.ModifierFlags
import scala.reflect.internal.util.SourceFile
import scala.tools.nsc.Global
import scala.tools.nsc.plugins.Plugin
import scala.tools.nsc.plugins.PluginComponent
import scala.tools.nsc.transform.Transform
import scala.tools.nsc.transform.TypingTransformers
import scala.util.control.NonFatal

import scoverage.domain.Coverage
import scoverage.domain.Statement
import scoverage.serialize.Serializer

/** @author Stephen Samuel */
class ScoveragePlugin(val global: Global) extends Plugin {

  override val name: String = ScoveragePlugin.name
  override val description: String = ScoveragePlugin.description

  // I'm not 100% sure why, but historically these have been parsed out first.
  // One thing to play around with in the future would be to not do this here
  // and rather do it later when we utilize setOpts and instead just initialize
  // then in the instrumentationCompoent. This will save us iterating over
  // these options twice.
  private val (extraAfterPhase, extraBeforePhase) =
    ScoverageOptions.processPhaseOptions(
      pluginOptions
    )

  val instrumentationComponent = new ScoverageInstrumentationComponent(
    global,
    extraAfterPhase,
    extraBeforePhase
  )

  override val components: List[PluginComponent] = List(
    instrumentationComponent
  )

  override def init(opts: List[String], error: String => Unit): Boolean = {

    val options =
      ScoverageOptions.parse(opts, error, ScoverageOptions.default())

    if (options.dataDir.isEmpty())
      throw new RuntimeException(
        "Cannot invoke plugin without specifying "
      )

    if (options.sourceRoot.isEmpty())
      throw new RuntimeException(
        "Cannot invoke plugin without specifying "
      )

    instrumentationComponent.setOptions(options)
    true
  }

  override val optionsHelp: Option[String] = ScoverageOptions.help

  private def pluginOptions: List[String] = {
    // Process plugin options of form plugin:option
    def namec = name + ":"
    global.settings.pluginOptions.value
      .filter(_.startsWith(namec))
      .map(_.stripPrefix(namec))
  }
}

class ScoverageInstrumentationComponent(
    val global: Global,
    extraAfterPhase: Option[String],
    extraBeforePhase: Option[String]
) extends PluginComponent
    with TypingTransformers
    with Transform {

  import global._

  val statementIds = new AtomicInteger(0)
  val coverage = new Coverage

  override val phaseName: String = ScoveragePlugin.phaseName
  override val runsAfter: List[String] =
    List("typer") ::: extraAfterPhase.toList
  override val runsBefore: List[String] =
    List("patmat") ::: extraBeforePhase.toList

  /** Our options are not provided at construction time, but shortly after,
    * so they start as None.
    * You must call "setOptions" before running any commands that rely on
    * the options.
    */
  private var options: ScoverageOptions = ScoverageOptions.default()
  private var coverageFilter: CoverageFilter = AllCoverageFilter

  private lazy val isScalaJsEnabled: Boolean = {
    try {
      rootMirror.getClassIfDefined("scala.scalajs.js.Any") != NoSymbol
    } catch {
      case NonFatal(_) => false
    }
  }

  def setOptions(options: ScoverageOptions): Unit = {
    this.options = options
    coverageFilter = new RegexCoverageFilter(
      options.excludedPackages,
      options.excludedFiles,
      options.excludedSymbols,
      reporter
    )
    new File(options.dataDir).mkdirs() // ensure data directory is created
  }

  override def newPhase(prev: scala.tools.nsc.Phase): Phase = new Phase(prev) {

    override def run(): Unit = {
      reporter.echo(s"Cleaning datadir [${options.dataDir}]")
      // we clean the data directory, because if the code has changed, then the number / order of
      // statements has changed by definition. So the old data would reference statements incorrectly
      // and thus skew the results.
      Serializer.clean(options.dataDir)

      reporter.echo("Beginning coverage instrumentation")
      super.run()
      reporter.echo(
        s"Instrumentation completed [${coverage.statements.size} statements]"
      )

      Serializer.serialize(
        coverage,
        Serializer.coverageFile(options.dataDir),
        new File(options.sourceRoot)
      )
      reporter.echo(
        s"Wrote instrumentation file [${Serializer.coverageFile(options.dataDir)}]"
      )
      reporter.echo(s"Will write measurement data to [${options.dataDir}]")
    }
  }

  protected def newTransformer(unit: CompilationUnit): Transformer =
    new Transformer(unit)

  class Transformer(unit: global.CompilationUnit)
      extends TypingTransformer(unit) {

    import global._

    // contains the location of the last node
    var location: domain.Location = _

    /** The 'start' of the position, if it is available, else -1
      * We cannot use 'isDefined' to test whether pos.start will work, as some
      * classes (e.g. scala.reflect.internal.util.OffsetPosition have
      * isDefined true, but throw on `start`
      */
    def safeStart(tree: Tree): Int =
      scala.util.Try(tree.pos.start).getOrElse(-1)
    def safeEnd(tree: Tree): Int = scala.util.Try(tree.pos.end).getOrElse(-1)
    def safeLine(tree: Tree): Int =
      if (tree.pos.isDefined) tree.pos.line else -1
    def safeSource(tree: Tree): Option[SourceFile] =
      if (tree.pos.isDefined) Some(tree.pos.source) else None

    def invokeCall(id: Int): Tree = {
      Apply(
        Select(
          Select(
            Ident("scoverage"),
            newTermName("Invoker")
          ),
          newTermName("invoked")
        ),
        Literal(
          Constant(id)
        ) ::
          Literal(
            Constant(options.dataDir)
          ) ::
          (if (options.reportTestName)
             List(
               Literal(
                 Constant(true)
               )
             )
           else Nil)
      )
    }

    override def transform(tree: Tree) = process(tree)

    def transformStatements(trees: List[Tree]): List[Tree] = trees.map(process)

    def transformForCases(cases: List[CaseDef]): List[CaseDef] = {
      // we don't instrument the synthetic case _ => false clause
      cases
        .dropRight(1)
        .map(c => {
          treeCopy.CaseDef(
            // in a for-loop we don't care about instrumenting the guards, as they are synthetically generated
            c,
            c.pat,
            process(c.guard),
            process(c.body)
          )
        }) ++ cases.takeRight(1)
    }

    def transformCases(
        cases: List[CaseDef],
        branch: Boolean = false
    ): List[CaseDef] = {
      cases.map(c => {
        treeCopy.CaseDef(
          c,
          c.pat,
          process(c.guard),
          if (branch) instrument(process(c.body), c.body, branch = true)
          else process(c.body)
        )
      })
    }

    def instrument(
        tree: Tree,
        original: Tree,
        branch: Boolean = false
    ): Tree = {
      safeSource(tree) match {
        case None =>
          reporter.warning(
            NoPosition,
            s"Could not instrument [${tree.getClass.getSimpleName}/${tree.symbol}]."
          )
          tree
        case Some(source) =>
          val id = statementIds.incrementAndGet
          val statement = Statement(
            location,
            id,
            safeStart(tree),
            safeEnd(tree),
            safeLine(tree),
            original.toString,
            Option(original.symbol).fold("")(_.fullNameString),
            tree.getClass.getSimpleName,
            branch
          )
          if (tree.pos.isDefined && !isStatementIncluded(tree.pos)) {
            coverage.add(statement.copy(ignored = true))
            tree
          } else if (isUndefinedParameterInScalaJs(tree.symbol)) {
            coverage.add(statement.copy(ignored = true))
            statementIds.decrementAndGet()
            tree
          } else {
            coverage.add(statement)

            val apply = invokeCall(id)
            val block = Block(List(apply), tree)
            localTyper.typed(atPos(tree.pos)(block))
          }
      }
    }

    // Copied from
    // https://github.com/scala-js/scala-js/blob/4619d906baef7feb5d0b6d555d5b33044669434e/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala#L2696-L2721
    private def isJSDefaultParam(sym: Symbol): Boolean = {
      if (isCtorDefaultParam(sym)) {
        isJSCtorDefaultParam(sym)
      } else {
        sym.hasFlag(reflect.internal.Flags.DEFAULTPARAM) &&
        isJSType(sym.owner) && {
          /* If this is a default parameter accessor on a
           * non-native JS class, we need to know if the method for which we
           * are the default parameter is exposed or not.
           * We do this by removing the $default suffix from the method name,
           * and looking up a member with that name in the owner.
           * Note that this does not work for local methods. But local methods
           * are never exposed.
           * Further note that overloads are easy, because either all or none
           * of them are exposed.
           */
          def isAttachedMethodExposed = {
            val methodName = nme.defaultGetterToMethod(sym.name)
            val ownerMethod = sym.owner.info.decl(methodName)
            ownerMethod.filter(isExposed).exists
          }

          !isNonNativeJSClass(sym.owner) || isAttachedMethodExposed
        }
      }
    }

    private lazy val JSTypeAnnot =
      rootMirror.getRequiredClass("scala.scalajs.js.annotation.internal.JSType")
    private lazy val ExposedJSMemberAnnot = rootMirror.getRequiredClass(
      "scala.scalajs.js.annotation.internal.ExposedJSMember"
    )
    private lazy val JSNativeAnnotation =
      rootMirror.getRequiredClass("scala.scalajs.js.native")

    private def isJSType(sym: Symbol): Boolean =
      sym.hasAnnotation(JSTypeAnnot)

    def isNonNativeJSClass(sym: Symbol): Boolean =
      !sym.isTrait && isJSType(sym) && !sym.hasAnnotation(JSNativeAnnotation)

    private def isExposed(sym: Symbol): Boolean = {
      !sym.isBridge && {
        if (sym.isLazy)
          sym.isAccessor && sym.accessed.hasAnnotation(ExposedJSMemberAnnot)
        else
          sym.hasAnnotation(ExposedJSMemberAnnot)
      }
    }

    private def isJSCtorDefaultParam(sym: Symbol) = {
      isCtorDefaultParam(sym) &&
      isJSType(patchedLinkedClassOfClass(sym.owner))
    }

    private def patchedLinkedClassOfClass(sym: Symbol): Symbol = {
      /* Work around a bug of scalac with linkedClassOfClass where package
       * objects are involved (the companion class would somehow exist twice
       * in the scope, making an assertion fail in Symbol.suchThat).
       * Basically this inlines linkedClassOfClass up to companionClass,
       * then replaces the `suchThat` by a `filter` and `head`.
       */
      val flatOwnerInfo = {
        // inline Symbol.flatOwnerInfo because it is protected
        if (sym.needsFlatClasses)
          sym.info
        sym.owner.rawInfo
      }
      val result = flatOwnerInfo.decl(sym.name).filter(_ isCoDefinedWith sym)
      if (!result.isOverloaded) result
      else result.alternatives.head
    }

    private def isCtorDefaultParam(sym: Symbol) = {
      sym.hasFlag(reflect.internal.Flags.DEFAULTPARAM) &&
      sym.owner.isModuleClass &&
      nme.defaultGetterToMethod(sym.name) == nme.CONSTRUCTOR
    }

    def isUndefinedParameterInScalaJs(sym: Symbol): Boolean = {
      isScalaJsEnabled && sym != null && isJSDefaultParam(sym)
    }
    def isClassIncluded(symbol: Symbol): Boolean =
      coverageFilter.isClassIncluded(symbol.fullNameString)
    def isFileIncluded(source: SourceFile): Boolean =
      coverageFilter.isFileIncluded(source)
    def isStatementIncluded(pos: Position): Boolean =
      coverageFilter.isLineIncluded(pos)
    def isSymbolIncluded(symbol: Symbol): Boolean =
      coverageFilter.isSymbolIncluded(symbol.fullNameString)

    def updateLocation(t: Tree): Unit = {
      Location.fromGlobal(global)(t) match {
        case Some(loc) => this.location = loc
        case _ =>
          reporter.warning(t.pos, s"Cannot update location for $t")
      }
    }

    def transformPartial(c: ClassDef): ClassDef = {
      treeCopy.ClassDef(
        c,
        c.mods,
        c.name,
        c.tparams,
        treeCopy.Template(
          c.impl,
          c.impl.parents,
          c.impl.self,
          c.impl.body.map {
            case d: DefDef if d.name.toString == "applyOrElse" =>
              d.rhs match {
                case Match(selector, cases) =>
                  treeCopy.DefDef(
                    d,
                    d.mods,
                    d.name,
                    d.tparams,
                    d.vparamss,
                    d.tpt,
                    treeCopy.Match(
                      // note: do not transform last case as that is the default handling
                      d.rhs,
                      selector,
                      transformCases(cases.init, branch = true) :+ cases.last
                    )
                  )
                case _ =>
                  reporter.error(
                    c.pos,
                    "Cannot instrument partial function apply. Please file bug report"
                  )
                  d
              }
            case other => other
          }
        )
      )
    }

    def debug(t: Tree): Unit = {
      import scala.reflect.runtime.{universe => u}
      reporter.echo(
        t.getClass.getSimpleName + ": LINE " + safeLine(t) + ": " + u.showRaw(t)
      )
    }

    def traverseApplication(t: Tree): Tree = {
      t match {
        case a: ApplyToImplicitArgs =>
          treeCopy.Apply(
            a,
            traverseApplication(a.fun),
            transformStatements(a.args)
          )
        case Apply(Select(_, name), List(fun @ Function(params, body)))
            if name.toString == "withFilter" && fun.symbol.isSynthetic && fun.toString
              .contains("check$ifrefutable$1") =>
          t
        case a: Apply =>
          treeCopy.Apply(
            a,
            traverseApplication(a.fun),
            transformStatements(a.args)
          )
        case a: TypeApply =>
          treeCopy.TypeApply(
            a,
            traverseApplication(a.fun),
            transformStatements(a.args)
          )
        case s: Select =>
          treeCopy.Select(s, traverseApplication(s.qualifier), s.name)
        case i: Ident => i
        case t: This  => t
        case other    => process(other)
      }
    }

    private def isSynthetic(t: Tree): Boolean =
      Option(t.symbol).fold(false)(_.isSynthetic)
    private def isNonSynthetic(t: Tree): Boolean = !isSynthetic(t)
    private def containsNonSynthetic(t: Tree): Boolean =
      isNonSynthetic(t) || t.children.exists(containsNonSynthetic)

    def allConstArgs(args: List[Tree]) =
      args.forall(arg => arg.isInstanceOf[Literal] || arg.isInstanceOf[Ident])

    def process(tree: Tree): Tree = {
      tree match {

        //        // non ranged inside ranged will break validation after typer, which only kicks in for yrangepos.
        //        case t if !t.pos.isRange => super.transform(t)

        // ignore macro expanded code, do not send to super as we don't want any children to be instrumented
        case t
            if t.attachments.all
              .toString()
              .contains("MacroExpansionAttachment") =>
          t

        //        /**
        //         * Object creation from new.
        //         * Ignoring creation calls to anon functions
        //         */
        //        case a: GenericApply if a.symbol.isConstructor && a.symbol.enclClass.isAnonymousFunction => tree
        //        case a: GenericApply if a.symbol.isConstructor => instrument(a)

        /** When an apply has no parameters, or is an application of purely literals or idents
          * then we can simply instrument the outer call. Ie, we can treat it all as one single statement
          * for the purposes of code coverage.
          * This will include calls to case apply.
          */
        case a: GenericApply if allConstArgs(a.args) => instrument(a, a)

        /** Applications of methods with non trivial args means the args themselves
          * must also be instrumented
          */
        // todo remove once scala merges into Apply proper
        case a: ApplyToImplicitArgs =>
          instrument(
            treeCopy.Apply(
              a,
              traverseApplication(a.fun),
              transformStatements(a.args)
            ),
            a
          )

        // handle 'new' keywords, instrumenting parameter lists
        case a @ Apply(s @ Select(New(tpt), name), args) =>
          instrument(treeCopy.Apply(a, s, transformStatements(args)), a)
        case a: Apply =>
          instrument(
            treeCopy.Apply(
              a,
              traverseApplication(a.fun),
              transformStatements(a.args)
            ),
            a
          )
        case a: TypeApply =>
          instrument(
            treeCopy.TypeApply(
              a,
              traverseApplication(a.fun),
              transformStatements(a.args)
            ),
            a
          )

        /** pattern match with syntax `Assign(lhs, rhs)`.
          * This AST node corresponds to the following Scala code:
          * lhs = rhs
          */
        case assign: Assign =>
          treeCopy.Assign(assign, assign.lhs, process(assign.rhs))

        /** pattern match with syntax `Block(stats, expr)`.
          * This AST node corresponds to the following Scala code:
          * { stats; expr }
          * If the block is empty, the `expr` is set to `Literal(Constant(()))`.
          */
        case b: Block =>
          treeCopy.Block(b, transformStatements(b.stats), transform(b.expr))

        // special support to handle partial functions
        case c: ClassDef
            if c.symbol.isAnonymousFunction &&
              c.symbol.enclClass.superClass.nameString.contains(
                "AbstractPartialFunction"
              ) =>
          if (isClassIncluded(c.symbol)) {
            transformPartial(c)
          } else {
            c
          }

        // scalac generated classes, we just instrument the enclosed methods/statements
        // the location would stay as the source class
        case c: ClassDef
            if c.symbol.isAnonymousClass || c.symbol.isAnonymousFunction =>
          if (isFileIncluded(c.pos.source) && isClassIncluded(c.symbol))
            super.transform(tree)
          else {
            c
          }

        case c: ClassDef =>
          if (isFileIncluded(c.pos.source) && isClassIncluded(c.symbol)) {
            updateLocation(c)
            super.transform(tree)
          } else {
            c
          }

        // ignore macro definitions in 2.11
        case DefDef(mods, _, _, _, _, _) if mods.isMacro => tree

        // this will catch methods defined as macros, eg def test = macro testImpl
        // it will not catch macro implementations
        case d: DefDef
            if d.symbol != null
              && d.symbol.annotations.nonEmpty
              && d.symbol.annotations.toString() == "macroImpl" =>
          tree

        // will catch macro implementations, as they must end with Expr, however will catch
        // any method that ends in Expr. // todo add way of allowing methods that return Expr
        case d: DefDef if d.symbol != null && !isSymbolIncluded(d.tpt.symbol) =>
          tree

        // we can ignore primary constructors because they are just empty at this stage, the body is added later.
        case d: DefDef if d.symbol.isPrimaryConstructor => tree

        /** Case class accessors for vals
          * EG for case class CreditReject(req: MarketOrderRequest, client: ActorRef)
          *     def req: com.sksamuel.scoverage.samples.MarketOrderRequest
          *     def client: akka.actor.ActorRef
          */
        case d: DefDef if d.symbol.isCaseAccessor => tree

        // Compiler generated case apply and unapply. Ignore these
        case d: DefDef if d.symbol.isCaseApplyOrUnapply => tree

        /** Lazy stable DefDefs are generated as the impl for lazy vals.
          */
        case d: DefDef
            if d.symbol.isStable && d.symbol.isGetter && d.symbol.isLazy =>
          updateLocation(d)
          treeCopy.DefDef(
            d,
            d.mods,
            d.name,
            d.tparams,
            d.vparamss,
            d.tpt,
            process(d.rhs)
          )

        /** Stable getters are methods generated for access to a top level val.
          * Should be ignored as this is compiler generated code.
          *
          * Eg
          *   def MaxCredit: scala.math.BigDecimal = CreditEngine.this.MaxCredit
          *   def alwaysTrue: String = InstrumentLoader.this.alwaysTrue
          */
        case d: DefDef if d.symbol.isStable && d.symbol.isGetter => tree

        /** Accessors are auto generated setters and getters.
          * Eg
          *  private def _clientName: String =
          *  def cancellable: akka.actor.Cancellable = PriceEngine.this.cancellable
          *  def cancellable_=(x$1: akka.actor.Cancellable): Unit = PriceEngine.this.cancellable = x$1
          */
        case d: DefDef if d.symbol.isAccessor => tree

        // was `abstract' for members | trait is virtual
        case d: DefDef if tree.symbol.isDeferred => tree

        /** eg
          * override  def hashCode(): Int
          *  def copy$default$1: com.sksamuel.scoverage.samples.MarketOrderRequest
          *  def $default$3: Option[org.joda.time.LocalDate] @scala.annotation.unchecked.uncheckedVariance = scala.None
          */
        case d: DefDef if d.symbol.isSynthetic => tree

        /** Match all remaining def definitions
          *
          * If the return type is not specified explicitly (i.e. is meant to be inferred),
          * this is expressed by having `tpt` set to `TypeTree()` (but not to an `EmptyTree`!).
          */
        case d: DefDef =>
          updateLocation(d)
          treeCopy.DefDef(
            d,
            d.mods,
            d.name,
            d.tparams,
            d.vparamss,
            d.tpt,
            process(d.rhs)
          )

        case EmptyTree => tree

        // handle function bodies. This AST node corresponds to the following Scala code: vparams => body
        case f: Function =>
          f.body match {
            case b: Match =>
              // anonymous function bodies with pattern matching needs to account for branches
              treeCopy.Function(
                tree,
                f.vparams,
                treeCopy.Match(
                  b,
                  b.selector,
                  transformCases(b.cases, branch = true)
                )
              )
            case _ => treeCopy.Function(tree, f.vparams, process(f.body))
          }

        case _: Ident => tree

        // the If statement itself doesn't need to be instrumented, because instrumenting the condition is
        // enough to determine if the If statement was executed.
        // The two procedures (then and else) are instrumented separately to determine if we entered
        // both branches.
        case i: If =>
          treeCopy.If(
            i,
            process(i.cond),
            instrument(process(i.thenp), i.thenp, branch = true),
            instrument(process(i.elsep), i.elsep, branch = true)
          )

        case _: Import => tree

        // labeldefs are never written natively in scala
        case l: LabelDef =>
          treeCopy.LabelDef(tree, l.name, l.params, transform(l.rhs))

        // profile access to a literal for function args todo do we need to do this?
        case l: Literal => instrument(l, l)

        // pattern match clauses will be instrumented per case
        case m @ Match(selector: Tree, cases: List[CaseDef]) =>
          // we can be fairly sure this was generated as part of a for loop
          if (
            selector.toString.contains("check$")
            && selector.tpe.annotations.mkString == "unchecked"
            && m.cases.last.toString == "case _ => false"
          ) {
            treeCopy.Match(tree, process(selector), transformForCases(cases))
          } else {
            // if the selector was added by compiler, we don't want to instrument it....
            // that usually means some construct is being transformed into a match
            if (Option(selector.symbol).exists(_.isSynthetic))
              treeCopy.Match(tree, selector, transformCases(cases))
            else
              // .. but we will if it was a user match
              treeCopy.Match(
                tree,
                process(selector),
                transformCases(cases, branch = true)
              )
          }

        // a synthetic object is a generated object, such as case class companion
        case m: ModuleDef if m.symbol.isSynthetic =>
          updateLocation(m)
          super.transform(tree)

        // user defined objects
        case m: ModuleDef =>
          if (isFileIncluded(m.pos.source) && isClassIncluded(m.symbol)) {
            updateLocation(m)
            super.transform(tree)
          } else {
            m
          }

        /** match with syntax `New(tpt)`.
          * This AST node corresponds to the following Scala code:
          *
          * `new` T
          *
          * This node always occurs in the following context:
          *
          * (`new` tpt).[targs](args)
          *
          * For example, an AST representation of:
          *
          * new Example[Int](2)(3)
          *
          * is the following code:
          *
          * Apply(
          * Apply(
          * TypeApply(
          * Select(New(TypeTree(typeOf[Example])), nme.CONSTRUCTOR)
          * TypeTree(typeOf[Int])),
          * List(Literal(Constant(2)))),
          * List(Literal(Constant(3))))
          */
        case n: New => n

        case s @ Select(n @ New(tpt), name) =>
          instrument(treeCopy.Select(s, n, name), s)

        case p: PackageDef =>
          if (isClassIncluded(p.symbol))
            treeCopy.PackageDef(p, p.pid, transformStatements(p.stats))
          else p

        // This AST node corresponds to the following Scala code:  `return` expr
        case r: Return =>
          treeCopy.Return(r, transform(r.expr))

        /** pattern match with syntax `Select(qual, name)`.
          * This AST node corresponds to the following Scala code:
          *
          * qualifier.selector
          *
          * Should only be used with `qualifier` nodes which are terms, i.e. which have `isTerm` returning `true`.
          * Otherwise `SelectFromTypeTree` should be used instead.
          *
          * foo.Bar // represented as Select(Ident(), )
          * Foo#Bar // represented as SelectFromTypeTree(Ident(), )
          */
        case s: Select if location == null => tree

        /** I think lazy selects are the LHS of a lazy assign.
          * todo confirm we can ignore
          */
        case s: Select if s.symbol.isLazy => tree

        // Generated by compiler for lazy definitions involving
        // by-name implicit parameters. More on that here:
        // https://docs.scala-lang.org/sips/byname-implicits.html
        //
        // final  val lazyDefns$1: LazyDefns$1 = new LazyDefns$1();
        // lazyDefns$1.rec$1()
        case s: Select if s.symbol.isSynthetic => tree

        case s: Select =>
          instrument(
            treeCopy.Select(s, traverseApplication(s.qualifier), s.name),
            s
          )

        case s: Super => tree

        // This AST node corresponds to the following Scala code:    qual.this
        case t: This => super.transform(tree)

        // This AST node corresponds to the following Scala code:    `throw` expr
        case t: Throw => instrument(tree, tree)

        // This AST node corresponds to the following Scala code: expr: tpt
        case t: Typed => super.transform(tree)

        // instrument trys, catches and finally as separate blocks
        case Try(t: Tree, cases: List[CaseDef], f: Tree) =>
          treeCopy.Try(
            tree,
            instrument(process(t), t, branch = true),
            transformCases(cases, branch = true),
            if (f.isEmpty) f else instrument(process(f), f, branch = true)
          )

        // type aliases, type parameters, abstract types
        case t: TypeDef => super.transform(tree)

        case t: Template =>
          updateLocation(t)
          treeCopy.Template(
            tree,
            t.parents,
            t.self,
            transformStatements(t.body)
          )

        case _: TypeTree => super.transform(tree)

        /** We can ignore lazy val defs as they are implemented by a generated defdef
          */
        case v: ValDef if v.symbol.isLazy => tree

        /**  val default: A1 => B1 =
          *  val x1: Any = _
          */
        case v: ValDef if v.symbol.isSynthetic => tree

        /** Vals declared in case constructors
          */
        case v: ValDef if v.symbol.isParamAccessor && v.symbol.isCaseAccessor =>
          tree

        // we need to remove the final mod so that we keep the code in order to check its invoked
        case v: ValDef if v.mods.isFinal =>
          updateLocation(v)
          treeCopy.ValDef(
            v,
            v.mods.&~(ModifierFlags.FINAL),
            v.name,
            v.tpt,
            process(v.rhs)
          )

        /** This AST node corresponds to any of the following Scala code:
          *
          * mods `val` name: tpt = rhs
          * mods `var` name: tpt = rhs
          * mods name: tpt = rhs        // in signatures of function and method definitions
          * self: Bar =>                // self-types
          *
          * For user defined value statements, we will instrument the RHS.
          *
          * This includes top level non-lazy vals. Lazy vals are generated as stable defs.
          */
        case v: ValDef =>
          updateLocation(v)
          treeCopy.ValDef(tree, v.mods, v.name, v.tpt, process(v.rhs))

        case _ =>
          reporter.warning(
            tree.pos,
            "BUG: Unexpected construct: " + tree.getClass + " " + tree.symbol
          )
          super.transform(tree)
      }
    }
  }
}

object ScoveragePlugin {
  val name: String = "scoverage"
  val description: String = "scoverage code coverage compiler plugin"
  val phaseName: String = "scoverage-instrumentation"
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy