Skip to content

Commit

Permalink
Merge pull request #13880 from ckipp01/coverage
Browse files Browse the repository at this point in the history
Add in initial support for code coverage
  • Loading branch information
smarter committed Apr 13, 2022
2 parents f4822ac + 2fc33a3 commit 93fc41f
Show file tree
Hide file tree
Showing 57 changed files with 5,015 additions and 38 deletions.
5 changes: 5 additions & 0 deletions NOTICE.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ major authors were omitted by oversight.
docs/js/. Please refer to the license header of the concerned files for
details.

* dotty.tools.dotc.coverage: Coverage instrumentation utilities have been
adapted from the scoverage plugin for scala 2 [5], which is under the
Apache 2.0 license.

* The Dotty codebase contains parts which are derived from
the ScalaPB protobuf library [4], which is under the Apache 2.0 license.

Expand All @@ -96,3 +100,4 @@ major authors were omitted by oversight.
[2] https://github.com/adriaanm/scala/tree/sbt-api-consolidate/src/compiler/scala/tools/sbt
[3] https://github.com/sbt/sbt/tree/0.13/compile/interface/src/main/scala/xsbt
[4] https://github.com/lampepfl/dotty/pull/5783/files
[5] https://github.com/scoverage/scalac-scoverage-plugin
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/Compiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class Compiler {

/** Phases dealing with the transformation from pickled trees to backend trees */
protected def transformPhases: List[List[Phase]] =
List(new InstrumentCoverage) :: // Perform instrumentation for code coverage (if -coverage-out is set)
List(new FirstTransform, // Some transformations to put trees into a canonical form
new CheckReentrant, // Internal use only: Check that compiled program has no data races involving global vars
new ElimPackagePrefixes, // Eliminate references to package prefixes in Select nodes
Expand Down
3 changes: 2 additions & 1 deletion compiler/src/dotty/tools/dotc/ast/Desugar.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1437,6 +1437,7 @@ object desugar {
ValDef(param.name, param.tpt, selector(idx))
.withSpan(param.span)
.withAttachment(UntupledParam, ())
.withFlags(Synthetic)
}
Function(param :: Nil, Block(vdefs, body))
}
Expand Down Expand Up @@ -1693,7 +1694,7 @@ object desugar {
case (p, n) => makeSyntheticParameter(n + 1, p).withAddedFlags(mods.flags)
}
RefinedTypeTree(polyFunctionTpt, List(
DefDef(nme.apply, applyTParams :: applyVParams :: Nil, res, EmptyTree)
DefDef(nme.apply, applyTParams :: applyVParams :: Nil, res, EmptyTree).withFlags(Synthetic)
))
}
else {
Expand Down
6 changes: 4 additions & 2 deletions compiler/src/dotty/tools/dotc/ast/MainProxies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,14 @@ object MainProxies {
.filterNot(_.matches(defn.MainAnnot))
.map(annot => insertTypeSplices.transform(annot.tree))
val mainMeth = DefDef(nme.main, (mainArg :: Nil) :: Nil, TypeTree(defn.UnitType), body)
.withFlags(JavaStatic)
.withFlags(JavaStatic | Synthetic)
.withAnnotations(annots)
val mainTempl = Template(emptyConstructor, Nil, Nil, EmptyValDef, mainMeth :: Nil)
val mainCls = TypeDef(mainFun.name.toTypeName, mainTempl)
.withFlags(Final | Invisible)
if (!ctx.reporter.hasErrors) result = mainCls.withSpan(mainAnnotSpan.toSynthetic) :: Nil

if (!ctx.reporter.hasErrors)
result = mainCls.withSpan(mainAnnotSpan.toSynthetic) :: Nil
}
result
}
Expand Down
54 changes: 27 additions & 27 deletions compiler/src/dotty/tools/dotc/ast/Trees.scala
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,7 @@ object Trees {
override def toString = s"InlineMatch($selector, $cases)"
}

/** case pat if guard => body; only appears as child of a Match */
/** case pat if guard => body */
case class CaseDef[-T >: Untyped] private[ast] (pat: Tree[T], guard: Tree[T], body: Tree[T])(implicit @constructorOnly src: SourceFile)
extends Tree[T] {
type ThisTree[-T >: Untyped] = CaseDef[T]
Expand Down Expand Up @@ -1367,13 +1367,26 @@ object Trees {
/** The context to use when mapping or accumulating over a tree */
def localCtx(tree: Tree)(using Context): Context

/** The context to use when transforming a tree.
* It ensures that the source is correct, and that the local context is used if
* that's necessary for transforming the whole tree.
* TODO: ensure transform is always called with the correct context as argument
* @see https://github.com/lampepfl/dotty/pull/13880#discussion_r836395977
*/
def transformCtx(tree: Tree)(using Context): Context =
val sourced =
if tree.source.exists && tree.source != ctx.source
then ctx.withSource(tree.source)
else ctx
tree match
case t: (MemberDef | PackageDef | LambdaTypeTree | TermLambdaTypeTree) =>
localCtx(t)(using sourced)
case _ =>
sourced

abstract class TreeMap(val cpy: TreeCopier = inst.cpy) { self =>
def transform(tree: Tree)(using Context): Tree = {
inContext(
if tree.source != ctx.source && tree.source.exists
then ctx.withSource(tree.source)
else ctx
){
inContext(transformCtx(tree)) {
Stats.record(s"TreeMap.transform/$getClass")
if (skipTransform(tree)) tree
else tree match {
Expand Down Expand Up @@ -1430,13 +1443,9 @@ object Trees {
case AppliedTypeTree(tpt, args) =>
cpy.AppliedTypeTree(tree)(transform(tpt), transform(args))
case LambdaTypeTree(tparams, body) =>
inContext(localCtx(tree)) {
cpy.LambdaTypeTree(tree)(transformSub(tparams), transform(body))
}
cpy.LambdaTypeTree(tree)(transformSub(tparams), transform(body))
case TermLambdaTypeTree(params, body) =>
inContext(localCtx(tree)) {
cpy.TermLambdaTypeTree(tree)(transformSub(params), transform(body))
}
cpy.TermLambdaTypeTree(tree)(transformSub(params), transform(body))
case MatchTypeTree(bound, selector, cases) =>
cpy.MatchTypeTree(tree)(transform(bound), transform(selector), transformSub(cases))
case ByNameTypeTree(result) =>
Expand All @@ -1452,30 +1461,21 @@ object Trees {
case EmptyValDef =>
tree
case tree @ ValDef(name, tpt, _) =>
inContext(localCtx(tree)) {
val tpt1 = transform(tpt)
val rhs1 = transform(tree.rhs)
cpy.ValDef(tree)(name, tpt1, rhs1)
}
val tpt1 = transform(tpt)
val rhs1 = transform(tree.rhs)
cpy.ValDef(tree)(name, tpt1, rhs1)
case tree @ DefDef(name, paramss, tpt, _) =>
inContext(localCtx(tree)) {
cpy.DefDef(tree)(name, transformParamss(paramss), transform(tpt), transform(tree.rhs))
}
cpy.DefDef(tree)(name, transformParamss(paramss), transform(tpt), transform(tree.rhs))
case tree @ TypeDef(name, rhs) =>
inContext(localCtx(tree)) {
cpy.TypeDef(tree)(name, transform(rhs))
}
cpy.TypeDef(tree)(name, transform(rhs))
case tree @ Template(constr, parents, self, _) if tree.derived.isEmpty =>
cpy.Template(tree)(transformSub(constr), transform(tree.parents), Nil, transformSub(self), transformStats(tree.body, tree.symbol))
case Import(expr, selectors) =>
cpy.Import(tree)(transform(expr), selectors)
case Export(expr, selectors) =>
cpy.Export(tree)(transform(expr), selectors)
case PackageDef(pid, stats) =>
val pid1 = transformSub(pid)
inContext(localCtx(tree)) {
cpy.PackageDef(tree)(pid1, transformStats(stats, ctx.owner))
}
cpy.PackageDef(tree)(transformSub(pid), transformStats(stats, ctx.owner))
case Annotated(arg, annot) =>
cpy.Annotated(tree)(transform(arg), transform(annot))
case Thicket(trees) =>
Expand Down
3 changes: 3 additions & 0 deletions compiler/src/dotty/tools/dotc/config/ScalaSettings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ trait CommonScalaSettings:
val unchecked: Setting[Boolean] = BooleanSetting("-unchecked", "Enable additional warnings where generated code depends on assumptions.", initialValue = true, aliases = List("--unchecked"))
val language: Setting[List[String]] = MultiStringSetting("-language", "feature", "Enable one or more language features.", aliases = List("--language"))

/* Coverage settings */
val coverageOutputDir = PathSetting("-coverage-out", "Destination for coverage classfiles and instrumentation data.", "", aliases = List("--coverage-out"))

/* Other settings */
val encoding: Setting[String] = StringSetting("-encoding", "encoding", "Specify character encoding used by source files.", Properties.sourceEncoding, aliases = List("--encoding"))
val usejavacp: Setting[Boolean] = BooleanSetting("-usejavacp", "Utilize the java.class.path in classpath resolution.", aliases = List("--use-java-class-path"))
Expand Down
4 changes: 4 additions & 0 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import typer.ImportInfo.RootRef
import Comments.CommentsContext
import Comments.Comment
import util.Spans.NoSpan
import Symbols.requiredModuleRef

import scala.annotation.tailrec

Expand Down Expand Up @@ -460,6 +461,9 @@ class Definitions {
}
def NullType: TypeRef = NullClass.typeRef

@tu lazy val InvokerModule = requiredModule("scala.runtime.coverage.Invoker")
@tu lazy val InvokedMethodRef = InvokerModule.requiredMethodRef("invoked")

@tu lazy val ImplicitScrutineeTypeSym =
newPermanentSymbol(ScalaPackageClass, tpnme.IMPLICITkw, EmptyFlags, TypeBounds.empty).entered
def ImplicitScrutineeTypeRef: TypeRef = ImplicitScrutineeTypeSym.typeRef
Expand Down
27 changes: 27 additions & 0 deletions compiler/src/dotty/tools/dotc/coverage/Coverage.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package dotty.tools.dotc
package coverage

import scala.collection.mutable

/** Holds a list of statements to include in the coverage reports. */
class Coverage:
private val statementsById = new mutable.LongMap[Statement](256)

def statements: Iterable[Statement] = statementsById.values

def addStatement(stmt: Statement): Unit = statementsById(stmt.id) = stmt

/** A statement that can be invoked, and thus counted as "covered" by code coverage tools. */
case class Statement(
source: String,
location: Location,
id: Int,
start: Int,
end: Int,
line: Int,
desc: String,
symbolName: String,
treeName: String,
branch: Boolean,
ignored: Boolean = false
)
47 changes: 47 additions & 0 deletions compiler/src/dotty/tools/dotc/coverage/Location.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package dotty.tools.dotc
package coverage

import ast.tpd._
import dotty.tools.dotc.core.Contexts.Context
import dotty.tools.dotc.core.Flags.*
import java.nio.file.Path

/** Information about the location of a coverable piece of code.
*
* @param packageName name of the enclosing package
* @param className name of the closest enclosing class
* @param fullClassName fully qualified name of the closest enclosing class
* @param classType "type" of the closest enclosing class: Class, Trait or Object
* @param method name of the closest enclosing method
* @param sourcePath absolute path of the source file
*/
final case class Location(
packageName: String,
className: String,
fullClassName: String,
classType: String,
method: String,
sourcePath: Path
)

object Location:
/** Extracts the location info of a Tree. */
def apply(tree: Tree)(using ctx: Context): Location =

val enclosingClass = ctx.owner.denot.enclosingClass
val packageName = ctx.owner.denot.enclosingPackageClass.name.toSimpleName.toString
val className = enclosingClass.name.toSimpleName.toString

val classType: String =
if enclosingClass.is(Trait) then "Trait"
else if enclosingClass.is(ModuleClass) then "Object"
else "Class"

Location(
packageName,
className,
s"$packageName.$className",
classType,
ctx.owner.denot.enclosingMethod.name.toSimpleName.toString(),
ctx.source.file.absolute.jpath
)
84 changes: 84 additions & 0 deletions compiler/src/dotty/tools/dotc/coverage/Serializer.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package dotty.tools.dotc
package coverage

import java.nio.file.{Path, Paths, Files}
import java.io.Writer
import scala.language.unsafeNulls

/**
* Serializes scoverage data.
* @see https://github.com/scoverage/scalac-scoverage-plugin/blob/main/scalac-scoverage-plugin/src/main/scala/scoverage/Serializer.scala
*/
object Serializer:

private val CoverageFileName = "scoverage.coverage"
private val CoverageDataFormatVersion = "3.0"

/** Write out coverage data to the given data directory, using the default coverage filename */
def serialize(coverage: Coverage, dataDir: String, sourceRoot: String): Unit =
serialize(coverage, Paths.get(dataDir, CoverageFileName).toAbsolutePath, Paths.get(sourceRoot).toAbsolutePath)

/** Write out coverage data to a file. */
def serialize(coverage: Coverage, file: Path, sourceRoot: Path): Unit =
val writer = Files.newBufferedWriter(file)
try
serialize(coverage, writer, sourceRoot)
finally
writer.close()

/** Write out coverage data (info about each statement that can be covered) to a writer.
*/
def serialize(coverage: Coverage, writer: Writer, sourceRoot: Path): Unit =

def getRelativePath(filePath: Path): String =
val relPath = sourceRoot.relativize(filePath)
relPath.toString

def writeHeader(writer: Writer): Unit =
writer.write(s"""# Coverage data, format version: $CoverageDataFormatVersion
|# Statement data:
|# - id
|# - source path
|# - package name
|# - class name
|# - class type (Class, Object or Trait)
|# - full class name
|# - method name
|# - start offset
|# - end offset
|# - line number
|# - symbol name
|# - tree name
|# - is branch
|# - invocations count
|# - is ignored
|# - description (can be multi-line)
|# '\f' sign
|# ------------------------------------------
|""".stripMargin)

def writeStatement(stmt: Statement, writer: Writer): Unit =
// Note: we write 0 for the count because we have not measured the actual coverage at this point
writer.write(s"""${stmt.id}
|${getRelativePath(stmt.location.sourcePath)}
|${stmt.location.packageName}
|${stmt.location.className}
|${stmt.location.classType}
|${stmt.location.fullClassName}
|${stmt.location.method}
|${stmt.start}
|${stmt.end}
|${stmt.line}
|${stmt.symbolName}
|${stmt.treeName}
|${stmt.branch}
|0
|${stmt.ignored}
|${stmt.desc}
|\f
|""".stripMargin)

writeHeader(writer)
coverage.statements.toSeq
.sortBy(_.id)
.foreach(stmt => writeStatement(stmt, writer))
Loading

0 comments on commit 93fc41f

Please sign in to comment.