Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add in initial support for code coverage #13880

Merged
merged 15 commits into from
Apr 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 =
TheElectronWill marked this conversation as resolved.
Show resolved Hide resolved
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:
TheElectronWill marked this conversation as resolved.
Show resolved Hide resolved
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 =
TheElectronWill marked this conversation as resolved.
Show resolved Hide resolved
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
TheElectronWill marked this conversation as resolved.
Show resolved Hide resolved
*/
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