Skip to content

Commit cc0bc92

Browse files
Fix coverage instrumentation of java and parameterless methods
1. Select and TypeApply trees weren't properly handled for JavaDefined symbols, leading to a crash when selecting a static method with parameter types. 2. Select and Ident trees weren't properly handled, and many occurences of parameterless methods were missed. 3. Some methods like asInstanceOf and isInstanceOf must not be instrumented, otherwise it crashes in Erasure.
1 parent 9cfe76e commit cc0bc92

27 files changed

+1029
-902
lines changed

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

+47-38
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,14 @@ import core.Flags.*
1010
import core.Contexts.{Context, ctx, inContext}
1111
import core.DenotTransformers.IdentityDenotTransformer
1212
import core.Symbols.{defn, Symbol}
13-
import core.Decorators.{toTermName, i}
1413
import core.Constants.Constant
1514
import core.NameOps.isContextFunction
1615
import core.Types.*
1716
import typer.LiftCoverage
1817
import util.{SourcePosition, Property}
1918
import util.Spans.Span
2019
import coverage.*
21-
import localopt.StringInterpolatorOpt.isCompilerIntrinsic
20+
import localopt.StringInterpolatorOpt
2221

2322
/** Implements code coverage by inserting calls to scala.runtime.coverage.Invoker
2423
* ("instruments" the source code).
@@ -62,11 +61,21 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
6261
/** Transforms trees to insert calls to Invoker.invoked to compute the coverage when the code is called */
6362
private class CoverageTransformer extends Transformer:
6463
override def transform(tree: Tree)(using Context): Tree =
64+
import scala.util.chaining.scalaUtilChainingOps
6565
inContext(transformCtx(tree)) { // necessary to position inlined code properly
6666
tree match
6767
// simple cases
6868
case tree: (Import | Export | Literal | This | Super | New) => tree
69-
case tree if tree.isEmpty || tree.isType => tree // empty Thicket, Ident, TypTree, ...
69+
case tree if tree.isEmpty || tree.isType => tree // empty Thicket, Ident (referring to a type), TypeTree, ...
70+
71+
// identifier
72+
case tree: Ident =>
73+
val sym = tree.symbol
74+
if canInstrumentParameterless(sym) then
75+
// call to a local parameterless method f
76+
instrument(tree)
77+
else
78+
tree
7079

7180
// branches
7281
case tree: If =>
@@ -82,20 +91,6 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
8291
finalizer = instrument(transform(tree.finalizer), branch = true)
8392
)
8493

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-
9994
// f(args)
10095
case tree: Apply =>
10196
if canInstrumentApply(tree) then
@@ -106,24 +101,19 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
106101
else
107102
transformApply(tree)
108103

109-
// (f(x))[args]
110-
case TypeApply(fun: Apply, args) =>
104+
// (fun)[args]
105+
case TypeApply(fun, args) =>
111106
cpy.TypeApply(tree)(transform(fun), args)
112107

113108
// a.b
114109
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)
110+
val transformed = cpy.Select(tree)(transform(qual), name)
111+
val sym = tree.symbol
112+
if canInstrumentParameterless(sym) then
113+
// call to a parameterless method
114+
instrument(transformed)
120115
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
116+
transformed
127117

128118
case tree: CaseDef => instrumentCaseDef(tree)
129119
case tree: ValDef =>
@@ -142,7 +132,9 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
142132
val rhs = transform(tree.rhs)
143133
val finalRhs =
144134
if canInstrumentDefDef(tree) then
145-
// Ensure that the rhs is always instrumented, if possible
135+
// Ensure that the rhs is always instrumented, if possible.
136+
// This is useful because methods can be stored and called later, or called by reflection,
137+
// and if the rhs is too simple to be instrumented (like `def f = this`), the method won't show up as covered.
146138
instrumentBody(tree, rhs)
147139
else
148140
rhs
@@ -162,7 +154,7 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
162154
}
163155

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

180172
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))
173+
cpy.Apply(tree)(transform(tree.fun), transform(tree.args))
185174

186175
private inline def instrumentCases(cases: List[CaseDef])(using Context): List[CaseDef] =
187176
cases.map(instrumentCaseDef)
@@ -292,7 +281,7 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
292281
* they shouldn't be lifted.
293282
*/
294283
val sym = fun.symbol
295-
sym.exists && (isShortCircuitedOp(sym) || isCompilerIntrinsic(sym))
284+
sym.exists && (isShortCircuitedOp(sym) || StringInterpolatorOpt.isCompilerIntrinsic(sym))
296285
end
297286

298287
val fun = tree.fun
@@ -312,7 +301,9 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
312301

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

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

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$

tests/coverage/pos/ContextFunctions.scoverage.check

+1-103
Original file line numberDiff line numberDiff line change
@@ -58,108 +58,6 @@ covtest
5858
Imperative
5959
Class
6060
covtest.Imperative
61-
$anonfun
62-
267
63-
294
64-
13
65-
apply
66-
Apply
67-
false
68-
0
69-
false
70-
readName2(using e)(using s)
71-
72-
3
73-
ContextFunctions.scala
74-
covtest
75-
Imperative
76-
Class
77-
covtest.Imperative
78-
readPerson
79-
252
80-
295
81-
13
82-
<init>
83-
Apply
84-
false
85-
0
86-
false
87-
OnError((e) => readName2(using e)(using s))
88-
89-
4
90-
ContextFunctions.scala
91-
covtest
92-
Imperative
93-
Class
94-
covtest.Imperative
95-
readPerson
96-
252
97-
295
98-
13
99-
invoked
100-
Apply
101-
false
102-
0
103-
false
104-
OnError((e) => readName2(using e)(using s))
105-
106-
5
107-
ContextFunctions.scala
108-
covtest
109-
Imperative
110-
Class
111-
covtest.Imperative
112-
$anonfun
113-
267
114-
294
115-
13
116-
invoked
117-
Apply
118-
false
119-
0
120-
false
121-
readName2(using e)(using s)
122-
123-
6
124-
ContextFunctions.scala
125-
covtest
126-
Imperative
127-
Class
128-
covtest.Imperative
129-
$anonfun
130-
267
131-
294
132-
13
133-
apply
134-
Apply
135-
false
136-
0
137-
false
138-
readName2(using e)(using s)
139-
140-
7
141-
ContextFunctions.scala
142-
covtest
143-
Imperative
144-
Class
145-
covtest.Imperative
146-
readPerson
147-
252
148-
295
149-
13
150-
<init>
151-
Apply
152-
false
153-
0
154-
false
155-
OnError((e) => readName2(using e)(using s))
156-
157-
8
158-
ContextFunctions.scala
159-
covtest
160-
Imperative
161-
Class
162-
covtest.Imperative
16361
readPerson
16462
252
16563
309
@@ -171,7 +69,7 @@ false
17169
false
17270
OnError((e) => readName2(using e)(using s)).onError(None)
17371

174-
9
72+
3
17573
ContextFunctions.scala
17674
covtest
17775
Imperative

0 commit comments

Comments
 (0)