Skip to content

Commit 93fc41f

Browse files
authored
Merge pull request #13880 from ckipp01/coverage
Add in initial support for code coverage
2 parents f4822ac + 2fc33a3 commit 93fc41f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+5015
-38
lines changed

NOTICE.md

+5
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ major authors were omitted by oversight.
8888
docs/js/. Please refer to the license header of the concerned files for
8989
details.
9090

91+
* dotty.tools.dotc.coverage: Coverage instrumentation utilities have been
92+
adapted from the scoverage plugin for scala 2 [5], which is under the
93+
Apache 2.0 license.
94+
9195
* The Dotty codebase contains parts which are derived from
9296
the ScalaPB protobuf library [4], which is under the Apache 2.0 license.
9397

@@ -96,3 +100,4 @@ major authors were omitted by oversight.
96100
[2] https://github.com/adriaanm/scala/tree/sbt-api-consolidate/src/compiler/scala/tools/sbt
97101
[3] https://github.com/sbt/sbt/tree/0.13/compile/interface/src/main/scala/xsbt
98102
[4] https://github.com/lampepfl/dotty/pull/5783/files
103+
[5] https://github.com/scoverage/scalac-scoverage-plugin

compiler/src/dotty/tools/dotc/Compiler.scala

+1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class Compiler {
5959

6060
/** Phases dealing with the transformation from pickled trees to backend trees */
6161
protected def transformPhases: List[List[Phase]] =
62+
List(new InstrumentCoverage) :: // Perform instrumentation for code coverage (if -coverage-out is set)
6263
List(new FirstTransform, // Some transformations to put trees into a canonical form
6364
new CheckReentrant, // Internal use only: Check that compiled program has no data races involving global vars
6465
new ElimPackagePrefixes, // Eliminate references to package prefixes in Select nodes

compiler/src/dotty/tools/dotc/ast/Desugar.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -1437,6 +1437,7 @@ object desugar {
14371437
ValDef(param.name, param.tpt, selector(idx))
14381438
.withSpan(param.span)
14391439
.withAttachment(UntupledParam, ())
1440+
.withFlags(Synthetic)
14401441
}
14411442
Function(param :: Nil, Block(vdefs, body))
14421443
}
@@ -1693,7 +1694,7 @@ object desugar {
16931694
case (p, n) => makeSyntheticParameter(n + 1, p).withAddedFlags(mods.flags)
16941695
}
16951696
RefinedTypeTree(polyFunctionTpt, List(
1696-
DefDef(nme.apply, applyTParams :: applyVParams :: Nil, res, EmptyTree)
1697+
DefDef(nme.apply, applyTParams :: applyVParams :: Nil, res, EmptyTree).withFlags(Synthetic)
16971698
))
16981699
}
16991700
else {

compiler/src/dotty/tools/dotc/ast/MainProxies.scala

+4-2
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,14 @@ object MainProxies {
105105
.filterNot(_.matches(defn.MainAnnot))
106106
.map(annot => insertTypeSplices.transform(annot.tree))
107107
val mainMeth = DefDef(nme.main, (mainArg :: Nil) :: Nil, TypeTree(defn.UnitType), body)
108-
.withFlags(JavaStatic)
108+
.withFlags(JavaStatic | Synthetic)
109109
.withAnnotations(annots)
110110
val mainTempl = Template(emptyConstructor, Nil, Nil, EmptyValDef, mainMeth :: Nil)
111111
val mainCls = TypeDef(mainFun.name.toTypeName, mainTempl)
112112
.withFlags(Final | Invisible)
113-
if (!ctx.reporter.hasErrors) result = mainCls.withSpan(mainAnnotSpan.toSynthetic) :: Nil
113+
114+
if (!ctx.reporter.hasErrors)
115+
result = mainCls.withSpan(mainAnnotSpan.toSynthetic) :: Nil
114116
}
115117
result
116118
}

compiler/src/dotty/tools/dotc/ast/Trees.scala

+27-27
Original file line numberDiff line numberDiff line change
@@ -610,7 +610,7 @@ object Trees {
610610
override def toString = s"InlineMatch($selector, $cases)"
611611
}
612612

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

1370+
/** The context to use when transforming a tree.
1371+
* It ensures that the source is correct, and that the local context is used if
1372+
* that's necessary for transforming the whole tree.
1373+
* TODO: ensure transform is always called with the correct context as argument
1374+
* @see https://github.com/lampepfl/dotty/pull/13880#discussion_r836395977
1375+
*/
1376+
def transformCtx(tree: Tree)(using Context): Context =
1377+
val sourced =
1378+
if tree.source.exists && tree.source != ctx.source
1379+
then ctx.withSource(tree.source)
1380+
else ctx
1381+
tree match
1382+
case t: (MemberDef | PackageDef | LambdaTypeTree | TermLambdaTypeTree) =>
1383+
localCtx(t)(using sourced)
1384+
case _ =>
1385+
sourced
1386+
13701387
abstract class TreeMap(val cpy: TreeCopier = inst.cpy) { self =>
13711388
def transform(tree: Tree)(using Context): Tree = {
1372-
inContext(
1373-
if tree.source != ctx.source && tree.source.exists
1374-
then ctx.withSource(tree.source)
1375-
else ctx
1376-
){
1389+
inContext(transformCtx(tree)) {
13771390
Stats.record(s"TreeMap.transform/$getClass")
13781391
if (skipTransform(tree)) tree
13791392
else tree match {
@@ -1430,13 +1443,9 @@ object Trees {
14301443
case AppliedTypeTree(tpt, args) =>
14311444
cpy.AppliedTypeTree(tree)(transform(tpt), transform(args))
14321445
case LambdaTypeTree(tparams, body) =>
1433-
inContext(localCtx(tree)) {
1434-
cpy.LambdaTypeTree(tree)(transformSub(tparams), transform(body))
1435-
}
1446+
cpy.LambdaTypeTree(tree)(transformSub(tparams), transform(body))
14361447
case TermLambdaTypeTree(params, body) =>
1437-
inContext(localCtx(tree)) {
1438-
cpy.TermLambdaTypeTree(tree)(transformSub(params), transform(body))
1439-
}
1448+
cpy.TermLambdaTypeTree(tree)(transformSub(params), transform(body))
14401449
case MatchTypeTree(bound, selector, cases) =>
14411450
cpy.MatchTypeTree(tree)(transform(bound), transform(selector), transformSub(cases))
14421451
case ByNameTypeTree(result) =>
@@ -1452,30 +1461,21 @@ object Trees {
14521461
case EmptyValDef =>
14531462
tree
14541463
case tree @ ValDef(name, tpt, _) =>
1455-
inContext(localCtx(tree)) {
1456-
val tpt1 = transform(tpt)
1457-
val rhs1 = transform(tree.rhs)
1458-
cpy.ValDef(tree)(name, tpt1, rhs1)
1459-
}
1464+
val tpt1 = transform(tpt)
1465+
val rhs1 = transform(tree.rhs)
1466+
cpy.ValDef(tree)(name, tpt1, rhs1)
14601467
case tree @ DefDef(name, paramss, tpt, _) =>
1461-
inContext(localCtx(tree)) {
1462-
cpy.DefDef(tree)(name, transformParamss(paramss), transform(tpt), transform(tree.rhs))
1463-
}
1468+
cpy.DefDef(tree)(name, transformParamss(paramss), transform(tpt), transform(tree.rhs))
14641469
case tree @ TypeDef(name, rhs) =>
1465-
inContext(localCtx(tree)) {
1466-
cpy.TypeDef(tree)(name, transform(rhs))
1467-
}
1470+
cpy.TypeDef(tree)(name, transform(rhs))
14681471
case tree @ Template(constr, parents, self, _) if tree.derived.isEmpty =>
14691472
cpy.Template(tree)(transformSub(constr), transform(tree.parents), Nil, transformSub(self), transformStats(tree.body, tree.symbol))
14701473
case Import(expr, selectors) =>
14711474
cpy.Import(tree)(transform(expr), selectors)
14721475
case Export(expr, selectors) =>
14731476
cpy.Export(tree)(transform(expr), selectors)
14741477
case PackageDef(pid, stats) =>
1475-
val pid1 = transformSub(pid)
1476-
inContext(localCtx(tree)) {
1477-
cpy.PackageDef(tree)(pid1, transformStats(stats, ctx.owner))
1478-
}
1478+
cpy.PackageDef(tree)(transformSub(pid), transformStats(stats, ctx.owner))
14791479
case Annotated(arg, annot) =>
14801480
cpy.Annotated(tree)(transform(arg), transform(annot))
14811481
case Thicket(trees) =>

compiler/src/dotty/tools/dotc/config/ScalaSettings.scala

+3
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ trait CommonScalaSettings:
117117
val unchecked: Setting[Boolean] = BooleanSetting("-unchecked", "Enable additional warnings where generated code depends on assumptions.", initialValue = true, aliases = List("--unchecked"))
118118
val language: Setting[List[String]] = MultiStringSetting("-language", "feature", "Enable one or more language features.", aliases = List("--language"))
119119

120+
/* Coverage settings */
121+
val coverageOutputDir = PathSetting("-coverage-out", "Destination for coverage classfiles and instrumentation data.", "", aliases = List("--coverage-out"))
122+
120123
/* Other settings */
121124
val encoding: Setting[String] = StringSetting("-encoding", "encoding", "Specify character encoding used by source files.", Properties.sourceEncoding, aliases = List("--encoding"))
122125
val usejavacp: Setting[Boolean] = BooleanSetting("-usejavacp", "Utilize the java.class.path in classpath resolution.", aliases = List("--use-java-class-path"))

compiler/src/dotty/tools/dotc/core/Definitions.scala

+4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import typer.ImportInfo.RootRef
1313
import Comments.CommentsContext
1414
import Comments.Comment
1515
import util.Spans.NoSpan
16+
import Symbols.requiredModuleRef
1617

1718
import scala.annotation.tailrec
1819

@@ -460,6 +461,9 @@ class Definitions {
460461
}
461462
def NullType: TypeRef = NullClass.typeRef
462463

464+
@tu lazy val InvokerModule = requiredModule("scala.runtime.coverage.Invoker")
465+
@tu lazy val InvokedMethodRef = InvokerModule.requiredMethodRef("invoked")
466+
463467
@tu lazy val ImplicitScrutineeTypeSym =
464468
newPermanentSymbol(ScalaPackageClass, tpnme.IMPLICITkw, EmptyFlags, TypeBounds.empty).entered
465469
def ImplicitScrutineeTypeRef: TypeRef = ImplicitScrutineeTypeSym.typeRef
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package dotty.tools.dotc
2+
package coverage
3+
4+
import scala.collection.mutable
5+
6+
/** Holds a list of statements to include in the coverage reports. */
7+
class Coverage:
8+
private val statementsById = new mutable.LongMap[Statement](256)
9+
10+
def statements: Iterable[Statement] = statementsById.values
11+
12+
def addStatement(stmt: Statement): Unit = statementsById(stmt.id) = stmt
13+
14+
/** A statement that can be invoked, and thus counted as "covered" by code coverage tools. */
15+
case class Statement(
16+
source: String,
17+
location: Location,
18+
id: Int,
19+
start: Int,
20+
end: Int,
21+
line: Int,
22+
desc: String,
23+
symbolName: String,
24+
treeName: String,
25+
branch: Boolean,
26+
ignored: Boolean = false
27+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package dotty.tools.dotc
2+
package coverage
3+
4+
import ast.tpd._
5+
import dotty.tools.dotc.core.Contexts.Context
6+
import dotty.tools.dotc.core.Flags.*
7+
import java.nio.file.Path
8+
9+
/** Information about the location of a coverable piece of code.
10+
*
11+
* @param packageName name of the enclosing package
12+
* @param className name of the closest enclosing class
13+
* @param fullClassName fully qualified name of the closest enclosing class
14+
* @param classType "type" of the closest enclosing class: Class, Trait or Object
15+
* @param method name of the closest enclosing method
16+
* @param sourcePath absolute path of the source file
17+
*/
18+
final case class Location(
19+
packageName: String,
20+
className: String,
21+
fullClassName: String,
22+
classType: String,
23+
method: String,
24+
sourcePath: Path
25+
)
26+
27+
object Location:
28+
/** Extracts the location info of a Tree. */
29+
def apply(tree: Tree)(using ctx: Context): Location =
30+
31+
val enclosingClass = ctx.owner.denot.enclosingClass
32+
val packageName = ctx.owner.denot.enclosingPackageClass.name.toSimpleName.toString
33+
val className = enclosingClass.name.toSimpleName.toString
34+
35+
val classType: String =
36+
if enclosingClass.is(Trait) then "Trait"
37+
else if enclosingClass.is(ModuleClass) then "Object"
38+
else "Class"
39+
40+
Location(
41+
packageName,
42+
className,
43+
s"$packageName.$className",
44+
classType,
45+
ctx.owner.denot.enclosingMethod.name.toSimpleName.toString(),
46+
ctx.source.file.absolute.jpath
47+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package dotty.tools.dotc
2+
package coverage
3+
4+
import java.nio.file.{Path, Paths, Files}
5+
import java.io.Writer
6+
import scala.language.unsafeNulls
7+
8+
/**
9+
* Serializes scoverage data.
10+
* @see https://github.com/scoverage/scalac-scoverage-plugin/blob/main/scalac-scoverage-plugin/src/main/scala/scoverage/Serializer.scala
11+
*/
12+
object Serializer:
13+
14+
private val CoverageFileName = "scoverage.coverage"
15+
private val CoverageDataFormatVersion = "3.0"
16+
17+
/** Write out coverage data to the given data directory, using the default coverage filename */
18+
def serialize(coverage: Coverage, dataDir: String, sourceRoot: String): Unit =
19+
serialize(coverage, Paths.get(dataDir, CoverageFileName).toAbsolutePath, Paths.get(sourceRoot).toAbsolutePath)
20+
21+
/** Write out coverage data to a file. */
22+
def serialize(coverage: Coverage, file: Path, sourceRoot: Path): Unit =
23+
val writer = Files.newBufferedWriter(file)
24+
try
25+
serialize(coverage, writer, sourceRoot)
26+
finally
27+
writer.close()
28+
29+
/** Write out coverage data (info about each statement that can be covered) to a writer.
30+
*/
31+
def serialize(coverage: Coverage, writer: Writer, sourceRoot: Path): Unit =
32+
33+
def getRelativePath(filePath: Path): String =
34+
val relPath = sourceRoot.relativize(filePath)
35+
relPath.toString
36+
37+
def writeHeader(writer: Writer): Unit =
38+
writer.write(s"""# Coverage data, format version: $CoverageDataFormatVersion
39+
|# Statement data:
40+
|# - id
41+
|# - source path
42+
|# - package name
43+
|# - class name
44+
|# - class type (Class, Object or Trait)
45+
|# - full class name
46+
|# - method name
47+
|# - start offset
48+
|# - end offset
49+
|# - line number
50+
|# - symbol name
51+
|# - tree name
52+
|# - is branch
53+
|# - invocations count
54+
|# - is ignored
55+
|# - description (can be multi-line)
56+
|# '\f' sign
57+
|# ------------------------------------------
58+
|""".stripMargin)
59+
60+
def writeStatement(stmt: Statement, writer: Writer): Unit =
61+
// Note: we write 0 for the count because we have not measured the actual coverage at this point
62+
writer.write(s"""${stmt.id}
63+
|${getRelativePath(stmt.location.sourcePath)}
64+
|${stmt.location.packageName}
65+
|${stmt.location.className}
66+
|${stmt.location.classType}
67+
|${stmt.location.fullClassName}
68+
|${stmt.location.method}
69+
|${stmt.start}
70+
|${stmt.end}
71+
|${stmt.line}
72+
|${stmt.symbolName}
73+
|${stmt.treeName}
74+
|${stmt.branch}
75+
|0
76+
|${stmt.ignored}
77+
|${stmt.desc}
78+
|\f
79+
|""".stripMargin)
80+
81+
writeHeader(writer)
82+
coverage.statements.toSeq
83+
.sortBy(_.id)
84+
.foreach(stmt => writeStatement(stmt, writer))

0 commit comments

Comments
 (0)