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

scoverage.plugin.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.{PluginComponent, Plugin}
import scala.tools.nsc.transform.{Transform, TypingTransformers}

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

  override val name: String = "scoverage"
  override val description: String = "scoverage code coverage compiler plugin"
  private val (extraAfterPhase, extraBeforePhase) = processPhaseOptions(pluginOptions)
  val instrumentationComponent = new ScoverageInstrumentationComponent(global, extraAfterPhase, extraBeforePhase)
  override val components: List[PluginComponent] = List(instrumentationComponent)

  private def parseExclusionEntry(entryName: String, inOption: String): Seq[String] =
    inOption.substring(entryName.length).split(";").map(_.trim).toIndexedSeq.filterNot(_.isEmpty)

  override def processOptions(opts: List[String], error: String => Unit): Unit = {
    val options = new ScoverageOptions

    for (opt <- opts) {
      if (opt.startsWith("excludedPackages:")) {
        options.excludedPackages = parseExclusionEntry("excludedPackages", opt)
      } else if (opt.startsWith("excludedFiles:")) {
        options.excludedFiles = parseExclusionEntry("excludedFiles", opt)
      } else if (opt.startsWith("excludedSymbols:")) {
        options.excludedSymbols = parseExclusionEntry("excludedSymbols", opt)
      } else if (opt.startsWith("dataDir:")) {
        options.dataDir = opt.substring("dataDir:".length)
      } else if (opt.startsWith("extraAfterPhase:") || opt.startsWith("extraBeforePhase:")) {
        // skip here, these flags are processed elsewhere
      } else {
        error("Unknown option: " + opt)
      }
    }
    if (!opts.exists(_.startsWith("dataDir:")))
      throw new RuntimeException("Cannot invoke plugin without specifying ")
    instrumentationComponent.setOptions(options)
  }

  override val optionsHelp: Option[String] = Some(Seq(
    "-P:scoverage:dataDir:                  where the coverage files should be written\n",
    "-P:scoverage:excludedPackages:;         semicolon separated list of regexs for packages to exclude",
    "-P:scoverage:excludedFiles:;            semicolon separated list of regexs for paths to exclude",
    "-P:scoverage:excludedSymbols:;          semicolon separated list of regexs for symbols to exclude",
    "-P:scoverage:extraAfterPhase:              phase after which scoverage phase runs (must be after typer phase)",
    "-P:scoverage:extraBeforePhase:             phase before which scoverage phase runs (must be before patmat phase)",
    "                                                      Any classes whose fully qualified name matches the regex will",
    "                                                      be excluded from coverage."
  ).mkString("\n"))

  // copied from scala 2.11
  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)
  }

  private def processPhaseOptions(opts: List[String]): (Option[String], Option[String]) = {
    var afterPhase: Option[String] = None
    var beforePhase: Option[String] = None
    for (opt <- opts) {
      if (opt.startsWith("extraAfterPhase:")) {
        afterPhase = Some(opt.substring("extraAfterPhase:".length))
      }
      if (opt.startsWith("extraBeforePhase:")) {
        beforePhase = Some(opt.substring("extraBeforePhase:".length))
      }
    }
    (afterPhase, beforePhase)
  }
}

class ScoverageOptions {
  var excludedPackages: Seq[String] = Nil
  var excludedFiles: Seq[String] = Nil
  var excludedSymbols: Seq[String] = Seq("scala.reflect.api.Exprs.Expr", "scala.reflect.api.Trees.Tree", "scala.reflect.macros.Universe.Tree")
  var dataDir: String = IOUtils.getTempPath
}

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 = "scoverage-instrumentation"
  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 = new ScoverageOptions()
  private var coverageFilter: CoverageFilter = AllCoverageFilter

  def setOptions(options: ScoverageOptions): Unit = {
    this.options = options
    coverageFilter = new RegexCoverageFilter(options.excludedPackages, options.excludedFiles, options.excludedSymbols)
    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"[info] 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.
      IOUtils.clean(options.dataDir)

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

      Serializer.serialize(coverage, Serializer.coverageFile(options.dataDir))
      reporter.echo(s"[info] Wrote instrumentation file [${Serializer.coverageFile(options.dataDir)}]")
      reporter.echo(s"[info] 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: 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")
        ),
        List(
          Literal(
            Constant(id)
          ),
          Literal(
            Constant(options.dataDir)
          )
        )
      )
    }

    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]): List[CaseDef] = {
      cases.map(c => {
        treeCopy.CaseDef(
          c, c.pat, process(c.guard), process(c.body)
        )
      })
    }

    def instrument(tree: Tree, original: Tree, branch: Boolean = false): Tree = {
      safeSource(tree) match {
        case None =>
          reporter.echo(s"[warn] Could not instrument [${tree.getClass.getSimpleName}/${tree.symbol}]. No pos.")
          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 {
            coverage.add(statement)

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

    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(global)(t) match {
        case Some(loc) => this.location = loc
        case _ => reporter.warning(t.pos, s"[warn] 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) :+ 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 =>
          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))
          }

        // 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

        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),
            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)
      }
    }
  }
}





© 2015 - 2024 Weber Informatics LLC | Privacy Policy