Skip to content

Commit 3fbbbd9

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 3fbbbd9

27 files changed

+1028
-902
lines changed

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

+46-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).
@@ -66,7 +65,16 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
6665
tree match
6766
// simple cases
6867
case tree: (Import | Export | Literal | This | Super | New) => tree
69-
case tree if tree.isEmpty || tree.isType => tree // empty Thicket, Ident, TypTree, ...
68+
case tree if tree.isEmpty || tree.isType => tree // empty Thicket, Ident (referring to a type), TypeTree, ...
69+
70+
// identifier
71+
case tree: Ident =>
72+
val sym = tree.symbol
73+
if canInstrumentParameterless(sym) then
74+
// call to a local parameterless method f
75+
instrument(tree)
76+
else
77+
tree
7078

7179
// branches
7280
case tree: If =>
@@ -82,20 +90,6 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
8290
finalizer = instrument(transform(tree.finalizer), branch = true)
8391
)
8492

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-
9993
// f(args)
10094
case tree: Apply =>
10195
if canInstrumentApply(tree) then
@@ -106,24 +100,19 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
106100
else
107101
transformApply(tree)
108102

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

113107
// a.b
114108
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)
109+
val transformed = cpy.Select(tree)(transform(qual), name)
110+
val sym = tree.symbol
111+
if canInstrumentParameterless(sym) then
112+
// call to a parameterless method
113+
instrument(transformed)
120114
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
115+
transformed
127116

128117
case tree: CaseDef => instrumentCaseDef(tree)
129118
case tree: ValDef =>
@@ -142,7 +131,9 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
142131
val rhs = transform(tree.rhs)
143132
val finalRhs =
144133
if canInstrumentDefDef(tree) then
145-
// Ensure that the rhs is always instrumented, if possible
134+
// Ensure that the rhs is always instrumented, if possible.
135+
// This is useful because methods can be stored and called later, or called by reflection,
136+
// and if the rhs is too simple to be instrumented (like `def f = this`), the method won't show up as covered.
146137
instrumentBody(tree, rhs)
147138
else
148139
rhs
@@ -162,7 +153,7 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
162153
}
163154

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

180171
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))
172+
cpy.Apply(tree)(transform(tree.fun), transform(tree.args))
185173

186174
private inline def instrumentCases(cases: List[CaseDef])(using Context): List[CaseDef] =
187175
cases.map(instrumentCaseDef)
@@ -292,7 +280,7 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
292280
* they shouldn't be lifted.
293281
*/
294282
val sym = fun.symbol
295-
sym.exists && (isShortCircuitedOp(sym) || isCompilerIntrinsic(sym))
283+
sym.exists && (isShortCircuitedOp(sym) || StringInterpolatorOpt.isCompilerIntrinsic(sym))
296284
end
297285

298286
val fun = tree.fun
@@ -312,7 +300,9 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
312300

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

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