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

Fix coverage instrumentation of Java-defined and parameterless methods #15648

Merged
merged 4 commits into from
Jul 25, 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
97 changes: 54 additions & 43 deletions compiler/src/dotty/tools/dotc/transform/InstrumentCoverage.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,21 @@ package dotty.tools.dotc
package transform

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

import ast.tpd.*
import collection.mutable
import core.Flags.*
import core.Contexts.{Context, ctx, inContext}
import core.DenotTransformers.IdentityDenotTransformer
import core.Symbols.{defn, Symbol}
import core.Decorators.{toTermName, i}
import core.Constants.Constant
import core.NameOps.isContextFunction
import core.Types.*
import coverage.*
import typer.LiftCoverage
import util.{SourcePosition, Property}
import util.SourcePosition
import util.Spans.Span
import coverage.*
import localopt.StringInterpolatorOpt.isCompilerIntrinsic
import localopt.StringInterpolatorOpt

/** Implements code coverage by inserting calls to scala.runtime.coverage.Invoker
* ("instruments" the source code).
Expand All @@ -44,7 +42,7 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
val outputPath = ctx.settings.coverageOutputDir.value

// Ensure the dir exists
val dataDir = new File(outputPath)
val dataDir = File(outputPath)
val newlyCreated = dataDir.mkdirs()

if !newlyCreated then
Expand All @@ -66,7 +64,16 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
tree match
// simple cases
case tree: (Import | Export | Literal | This | Super | New) => tree
case tree if tree.isEmpty || tree.isType => tree // empty Thicket, Ident, TypTree, ...
case tree if tree.isEmpty || tree.isType => tree // empty Thicket, Ident (referring to a type), TypeTree, ...

// identifier
case tree: Ident =>
val sym = tree.symbol
if canInstrumentParameterless(sym) then
// call to a local parameterless method f
instrument(tree)
else
tree

// branches
case tree: If =>
Expand All @@ -82,20 +89,6 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
finalizer = instrument(transform(tree.finalizer), branch = true)
)

// a.f(args)
case tree @ Apply(fun: Select, args) =>
// don't transform the first Select, but do transform `a.b` in `a.b.f(args)`
val transformedFun = cpy.Select(fun)(transform(fun.qualifier), fun.name)
if canInstrumentApply(tree) then
if needsLift(tree) then
val transformed = cpy.Apply(tree)(transformedFun, args) // args will be transformed in instrumentLifted
instrumentLifted(transformed)
else
val transformed = transformApply(tree, transformedFun)
instrument(transformed)
else
transformApply(tree, transformedFun)

// f(args)
case tree: Apply =>
if canInstrumentApply(tree) then
Expand All @@ -106,24 +99,19 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
else
transformApply(tree)

// (f(x))[args]
case TypeApply(fun: Apply, args) =>
// (fun)[args]
case TypeApply(fun, args) =>
cpy.TypeApply(tree)(transform(fun), args)

// a.b
case Select(qual, name) =>
if qual.symbol.exists && qual.symbol.is(JavaDefined) then
//Java class can't be used as a value, we can't instrument the
//qualifier ({<Probe>;System}.xyz() is not possible !) instrument it
//as it is
instrument(tree)
val transformed = cpy.Select(tree)(transform(qual), name)
val sym = tree.symbol
if canInstrumentParameterless(sym) then
// call to a parameterless method
instrument(transformed)
else
val transformed = cpy.Select(tree)(transform(qual), name)
if transformed.qualifier.isDef then
// instrument calls to methods without parameter list
instrument(transformed)
else
transformed
transformed

case tree: CaseDef => instrumentCaseDef(tree)
case tree: ValDef =>
Expand All @@ -142,7 +130,9 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
val rhs = transform(tree.rhs)
val finalRhs =
if canInstrumentDefDef(tree) then
// Ensure that the rhs is always instrumented, if possible
// Ensure that the rhs is always instrumented, if possible.
// This is useful because methods can be stored and called later, or called by reflection,
// and if the rhs is too simple to be instrumented (like `def f = this`), the method won't show up as covered.
instrumentBody(tree, rhs)
else
rhs
Expand All @@ -162,7 +152,7 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
}

/** Lifts and instruments an application.
* Note that if only one arg needs to be lifted, we just lift everything.
* Note that if only one arg needs to be lifted, we just lift everything (see LiftCoverage).
*/
private def instrumentLifted(tree: Apply)(using Context) =
// lifting
Expand All @@ -178,10 +168,7 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
)

private inline def transformApply(tree: Apply)(using Context): Apply =
transformApply(tree, transform(tree.fun))

private inline def transformApply(tree: Apply, transformedFun: Tree)(using Context): Apply =
cpy.Apply(tree)(transformedFun, transform(tree.args))
cpy.Apply(tree)(transform(tree.fun), transform(tree.args))

private inline def instrumentCases(cases: List[CaseDef])(using Context): List[CaseDef] =
cases.map(instrumentCaseDef)
Expand All @@ -201,7 +188,7 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
private def recordStatement(tree: Tree, pos: SourcePosition, branch: Boolean)(using ctx: Context): Int =
val id = statementId
statementId += 1
val statement = new Statement(
val statement = Statement(
source = ctx.source.file.name,
location = Location(tree),
id = id,
Expand Down Expand Up @@ -292,7 +279,7 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
* they shouldn't be lifted.
*/
val sym = fun.symbol
sym.exists && (isShortCircuitedOp(sym) || isCompilerIntrinsic(sym))
sym.exists && (isShortCircuitedOp(sym) || StringInterpolatorOpt.isCompilerIntrinsic(sym))
end

val fun = tree.fun
Expand All @@ -312,7 +299,9 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:

/** Check if an Apply can be instrumented. Prevents this phase from generating incorrect code. */
private def canInstrumentApply(tree: Apply)(using Context): Boolean =
!tree.symbol.isOneOf(Synthetic | Artifact) && // no need to instrument synthetic apply
val sym = tree.symbol
!sym.isOneOf(Synthetic | Artifact) && // no need to instrument synthetic apply
!isCompilerIntrinsicMethod(sym) &&
(tree.typeOpt match
case AppliedType(tycon: NamedType, _) =>
/* If the last expression in a block is a context function, we'll try to
Expand All @@ -339,6 +328,28 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
true
)

/** Is this the symbol of a parameterless method that we can instrument?
* Note: it is crucial that `asInstanceOf` and `isInstanceOf`, among others,
* do NOT get instrumented, because that would generate invalid code and crash
* in post-erasure checking.
*/
private def canInstrumentParameterless(sym: Symbol)(using Context): Boolean =
sym.is(Method, butNot = Synthetic | Artifact) &&
sym.info.isParameterless &&
!isCompilerIntrinsicMethod(sym)

/** Does sym refer to a "compiler intrinsic" method, which only exist during compilation,
* like Any.isInstanceOf?
* If this returns true, the call souldn't be instrumented.
*/
private def isCompilerIntrinsicMethod(sym: Symbol)(using Context): Boolean =
val owner = sym.maybeOwner
owner.exists && (
owner.eq(defn.AnyClass) ||
owner.isPrimitiveValueClass ||
owner.maybeOwner == defn.CompiletimePackageClass
)

object InstrumentCoverage:
val name: String = "instrumentCoverage"
val description: String = "instrument code for coverage checking"
9 changes: 3 additions & 6 deletions compiler/test/dotty/tools/dotc/coverage/CoverageTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import scala.jdk.CollectionConverters.*
import scala.util.Properties.userDir
import scala.language.unsafeNulls
import scala.collection.mutable.Buffer
import dotty.tools.dotc.util.DiffUtil

@Category(Array(classOf[BootstrappedOnlyTests]))
class CoverageTests:
Expand Down Expand Up @@ -56,12 +57,8 @@ class CoverageTests:
val expected = fixWindowsPaths(Files.readAllLines(expectFile).asScala)
val obtained = fixWindowsPaths(Files.readAllLines(targetFile).asScala)
if expected != obtained then
// FIXME: zip will drop part of the output if one is shorter (i.e. will not print anything of one is a refix of the other)
for ((exp, actual),i) <- expected.zip(obtained).filter(_ != _).zipWithIndex do
Console.err.println(s"wrong line ${i+1}:")
Console.err.println(s" expected: $exp")
Console.err.println(s" actual : $actual")
fail(s"$targetFile differs from expected $expectFile")
val instructions = FileDiff.diffMessage(expectFile.toString, targetFile.toString)
fail(s"Coverage report differs from expected data.\n$instructions")

})

Expand Down
5 changes: 5 additions & 0 deletions tests/coverage/pos/CompiletimeUninitialized.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package covtest

import scala.compiletime.uninitialized
class Foo:
var x: AnyRef = uninitialized
20 changes: 20 additions & 0 deletions tests/coverage/pos/CompiletimeUninitialized.scoverage.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Coverage data, format version: 3.0
# 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)
# ' ' sign
# ------------------------------------------
40 changes: 37 additions & 3 deletions tests/coverage/pos/Constructor.scoverage.check
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,23 @@ C
Class
covtest.C
<init>
62
63
5
x
Select
false
0
false
x

3
Constructor.scala
covtest
C
Class
covtest.C
<init>
60
64
5
Expand All @@ -69,7 +86,7 @@ false
false
f(x)

3
4
Constructor.scala
covtest
O$
Expand All @@ -86,7 +103,7 @@ false
false
def g

4
5
Constructor.scala
covtest
O$
Expand All @@ -103,7 +120,24 @@ false
false
def y

5
6
Constructor.scala
covtest
O$
Object
covtest.O$
<init>
112
113
10
y
Ident
false
0
false
y

7
Constructor.scala
covtest
O$
Expand Down
Loading