Skip to content

Commit 19eff87

Browse files
Merge pull request #15648 from TheElectronWill/fix-coverage-2
Fix coverage instrumentation of Java-defined and parameterless methods
2 parents f36967c + 1ab427b commit 19eff87

30 files changed

+1064
-913
lines changed

compiler/src/dotty/tools/dotc/transform/InstrumentCoverage.scala

+54-43
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,21 @@ package dotty.tools.dotc
22
package transform
33

44
import java.io.File
5-
import java.util.concurrent.atomic.AtomicInteger
65

76
import ast.tpd.*
87
import collection.mutable
98
import core.Flags.*
109
import core.Contexts.{Context, ctx, inContext}
1110
import core.DenotTransformers.IdentityDenotTransformer
1211
import core.Symbols.{defn, Symbol}
13-
import core.Decorators.{toTermName, i}
1412
import core.Constants.Constant
1513
import core.NameOps.isContextFunction
1614
import core.Types.*
15+
import coverage.*
1716
import typer.LiftCoverage
18-
import util.{SourcePosition, Property}
17+
import util.SourcePosition
1918
import util.Spans.Span
20-
import coverage.*
21-
import localopt.StringInterpolatorOpt.isCompilerIntrinsic
19+
import localopt.StringInterpolatorOpt
2220

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

4644
// Ensure the dir exists
47-
val dataDir = new File(outputPath)
45+
val dataDir = File(outputPath)
4846
val newlyCreated = dataDir.mkdirs()
4947

5048
if !newlyCreated then
@@ -66,7 +64,16 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
6664
tree match
6765
// simple cases
6866
case tree: (Import | Export | Literal | This | Super | New) => tree
69-
case tree if tree.isEmpty || tree.isType => tree // empty Thicket, Ident, TypTree, ...
67+
case tree if tree.isEmpty || tree.isType => tree // empty Thicket, Ident (referring to a type), TypeTree, ...
68+
69+
// identifier
70+
case tree: Ident =>
71+
val sym = tree.symbol
72+
if canInstrumentParameterless(sym) then
73+
// call to a local parameterless method f
74+
instrument(tree)
75+
else
76+
tree
7077

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

85-
// a.f(args)
86-
case tree @ Apply(fun: Select, args) =>
87-
// don't transform the first Select, but do transform `a.b` in `a.b.f(args)`
88-
val transformedFun = cpy.Select(fun)(transform(fun.qualifier), fun.name)
89-
if canInstrumentApply(tree) then
90-
if needsLift(tree) then
91-
val transformed = cpy.Apply(tree)(transformedFun, args) // args will be transformed in instrumentLifted
92-
instrumentLifted(transformed)
93-
else
94-
val transformed = transformApply(tree, transformedFun)
95-
instrument(transformed)
96-
else
97-
transformApply(tree, transformedFun)
98-
9992
// f(args)
10093
case tree: Apply =>
10194
if canInstrumentApply(tree) then
@@ -106,24 +99,19 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
10699
else
107100
transformApply(tree)
108101

109-
// (f(x))[args]
110-
case TypeApply(fun: Apply, args) =>
102+
// (fun)[args]
103+
case TypeApply(fun, args) =>
111104
cpy.TypeApply(tree)(transform(fun), args)
112105

113106
// a.b
114107
case Select(qual, name) =>
115-
if qual.symbol.exists && qual.symbol.is(JavaDefined) then
116-
//Java class can't be used as a value, we can't instrument the
117-
//qualifier ({<Probe>;System}.xyz() is not possible !) instrument it
118-
//as it is
119-
instrument(tree)
108+
val transformed = cpy.Select(tree)(transform(qual), name)
109+
val sym = tree.symbol
110+
if canInstrumentParameterless(sym) then
111+
// call to a parameterless method
112+
instrument(transformed)
120113
else
121-
val transformed = cpy.Select(tree)(transform(qual), name)
122-
if transformed.qualifier.isDef then
123-
// instrument calls to methods without parameter list
124-
instrument(transformed)
125-
else
126-
transformed
114+
transformed
127115

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

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

180170
private inline def transformApply(tree: Apply)(using Context): Apply =
181-
transformApply(tree, transform(tree.fun))
182-
183-
private inline def transformApply(tree: Apply, transformedFun: Tree)(using Context): Apply =
184-
cpy.Apply(tree)(transformedFun, transform(tree.args))
171+
cpy.Apply(tree)(transform(tree.fun), transform(tree.args))
185172

186173
private inline def instrumentCases(cases: List[CaseDef])(using Context): List[CaseDef] =
187174
cases.map(instrumentCaseDef)
@@ -201,7 +188,7 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
201188
private def recordStatement(tree: Tree, pos: SourcePosition, branch: Boolean)(using ctx: Context): Int =
202189
val id = statementId
203190
statementId += 1
204-
val statement = new Statement(
191+
val statement = Statement(
205192
source = ctx.source.file.name,
206193
location = Location(tree),
207194
id = id,
@@ -292,7 +279,7 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
292279
* they shouldn't be lifted.
293280
*/
294281
val sym = fun.symbol
295-
sym.exists && (isShortCircuitedOp(sym) || isCompilerIntrinsic(sym))
282+
sym.exists && (isShortCircuitedOp(sym) || StringInterpolatorOpt.isCompilerIntrinsic(sym))
296283
end
297284

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

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

331+
/** Is this the symbol of a parameterless method that we can instrument?
332+
* Note: it is crucial that `asInstanceOf` and `isInstanceOf`, among others,
333+
* do NOT get instrumented, because that would generate invalid code and crash
334+
* in post-erasure checking.
335+
*/
336+
private def canInstrumentParameterless(sym: Symbol)(using Context): Boolean =
337+
sym.is(Method, butNot = Synthetic | Artifact) &&
338+
sym.info.isParameterless &&
339+
!isCompilerIntrinsicMethod(sym)
340+
341+
/** Does sym refer to a "compiler intrinsic" method, which only exist during compilation,
342+
* like Any.isInstanceOf?
343+
* If this returns true, the call souldn't be instrumented.
344+
*/
345+
private def isCompilerIntrinsicMethod(sym: Symbol)(using Context): Boolean =
346+
val owner = sym.maybeOwner
347+
owner.exists && (
348+
owner.eq(defn.AnyClass) ||
349+
owner.isPrimitiveValueClass ||
350+
owner.maybeOwner == defn.CompiletimePackageClass
351+
)
352+
342353
object InstrumentCoverage:
343354
val name: String = "instrumentCoverage"
344355
val description: String = "instrument code for coverage checking"

compiler/test/dotty/tools/dotc/coverage/CoverageTests.scala

+3-6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import scala.jdk.CollectionConverters.*
1515
import scala.util.Properties.userDir
1616
import scala.language.unsafeNulls
1717
import scala.collection.mutable.Buffer
18+
import dotty.tools.dotc.util.DiffUtil
1819

1920
@Category(Array(classOf[BootstrappedOnlyTests]))
2021
class CoverageTests:
@@ -56,12 +57,8 @@ class CoverageTests:
5657
val expected = fixWindowsPaths(Files.readAllLines(expectFile).asScala)
5758
val obtained = fixWindowsPaths(Files.readAllLines(targetFile).asScala)
5859
if expected != obtained then
59-
// 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)
60-
for ((exp, actual),i) <- expected.zip(obtained).filter(_ != _).zipWithIndex do
61-
Console.err.println(s"wrong line ${i+1}:")
62-
Console.err.println(s" expected: $exp")
63-
Console.err.println(s" actual : $actual")
64-
fail(s"$targetFile differs from expected $expectFile")
60+
val instructions = FileDiff.diffMessage(expectFile.toString, targetFile.toString)
61+
fail(s"Coverage report differs from expected data.\n$instructions")
6562

6663
})
6764

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package covtest
2+
3+
import scala.compiletime.uninitialized
4+
class Foo:
5+
var x: AnyRef = uninitialized
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Coverage data, format version: 3.0
2+
# Statement data:
3+
# - id
4+
# - source path
5+
# - package name
6+
# - class name
7+
# - class type (Class, Object or Trait)
8+
# - full class name
9+
# - method name
10+
# - start offset
11+
# - end offset
12+
# - line number
13+
# - symbol name
14+
# - tree name
15+
# - is branch
16+
# - invocations count
17+
# - is ignored
18+
# - description (can be multi-line)
19+
# ' ' sign
20+
# ------------------------------------------

tests/coverage/pos/Constructor.scoverage.check

+37-3
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,23 @@ C
5959
Class
6060
covtest.C
6161
<init>
62+
62
63+
63
64+
5
65+
x
66+
Select
67+
false
68+
0
69+
false
70+
x
71+
72+
3
73+
Constructor.scala
74+
covtest
75+
C
76+
Class
77+
covtest.C
78+
<init>
6279
60
6380
64
6481
5
@@ -69,7 +86,7 @@ false
6986
false
7087
f(x)
7188

72-
3
89+
4
7390
Constructor.scala
7491
covtest
7592
O$
@@ -86,7 +103,7 @@ false
86103
false
87104
def g
88105

89-
4
106+
5
90107
Constructor.scala
91108
covtest
92109
O$
@@ -103,7 +120,24 @@ false
103120
false
104121
def y
105122

106-
5
123+
6
124+
Constructor.scala
125+
covtest
126+
O$
127+
Object
128+
covtest.O$
129+
<init>
130+
112
131+
113
132+
10
133+
y
134+
Ident
135+
false
136+
0
137+
false
138+
y
139+
140+
7
107141
Constructor.scala
108142
covtest
109143
O$

0 commit comments

Comments
 (0)