@@ -60,27 +60,13 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
60
60
61
61
/** Transforms trees to insert calls to Invoker.invoked to compute the coverage when the code is called */
62
62
private class CoverageTransformer extends Transformer :
63
- private val IgnoreLiterals = new Property .Key [Boolean ]
64
-
65
- private def ignoreLiteralsContext (using ctx : Context ): Context =
66
- ctx.fresh.setProperty(IgnoreLiterals , true )
67
-
68
63
override def transform (tree : Tree )(using ctx : Context ): Tree =
69
64
inContext(transformCtx(tree)) { // necessary to position inlined code properly
70
65
tree match
71
66
// simple cases
72
- case tree : (Import | Export | This | Super | New ) => tree
67
+ case tree : (Import | Export | Literal | This | Super | New ) => tree
73
68
case tree if tree.isEmpty || tree.isType => tree // empty Thicket, Ident, TypTree, ...
74
69
75
- // Literals must be instrumented (at least) when returned by a def,
76
- // otherwise `def d = "literal"` is not covered when called from a test.
77
- // They can be left untouched when passed in a parameter of an Apply.
78
- case tree : Literal =>
79
- if ctx.property(IgnoreLiterals ).contains(true ) then
80
- tree
81
- else
82
- instrument(tree)
83
-
84
70
// branches
85
71
case tree : If =>
86
72
cpy.If (tree)(
@@ -92,32 +78,32 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
92
78
cpy.Try (tree)(
93
79
expr = instrument(transform(tree.expr), branch = true ),
94
80
cases = instrumentCases(tree.cases),
95
- finalizer = instrument(transform(tree.finalizer), true )
81
+ finalizer = instrument(transform(tree.finalizer), branch = true )
96
82
)
97
83
98
84
// a.f(args)
99
85
case tree @ Apply (fun : Select , args) =>
100
86
// don't transform the first Select, but do transform `a.b` in `a.b.f(args)`
87
+ val transformedFun = cpy.Select (fun)(transform(fun.qualifier), fun.name)
101
88
if canInstrumentApply(tree) then
102
- val transformedFun = cpy.Select (fun)(transform(fun.qualifier), fun.name)
103
89
if needsLift(tree) then
104
90
val transformed = cpy.Apply (tree)(transformedFun, args) // args will be transformed in instrumentLifted
105
- instrumentLifted(transformed)( using ignoreLiteralsContext)
91
+ instrumentLifted(transformed)
106
92
else
107
- val transformed = cpy. Apply (tree)(transformedFun, transform(args)( using ignoreLiteralsContext) )
108
- instrument(transformed)( using ignoreLiteralsContext)
93
+ val transformed = transformApply (tree, transformedFun )
94
+ instrument(transformed)
109
95
else
110
- tree
96
+ transformApply( tree, transformedFun)
111
97
112
98
// f(args)
113
99
case tree : Apply =>
114
100
if canInstrumentApply(tree) then
115
101
if needsLift(tree) then
116
- instrumentLifted(tree)( using ignoreLiteralsContext) // see comment about Literals
102
+ instrumentLifted(tree)
117
103
else
118
- instrument(super .transform (tree)( using ignoreLiteralsContext ))
104
+ instrument(transformApply (tree))
119
105
else
120
- tree
106
+ transformApply( tree)
121
107
122
108
// (f(x))[args]
123
109
case TypeApply (fun : Apply , args) =>
@@ -142,15 +128,19 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
142
128
case tree : ValDef =>
143
129
// only transform the rhs
144
130
val rhs = transform(tree.rhs)
145
- cpy.ValDef (tree)(rhs= rhs)
131
+ cpy.ValDef (tree)(rhs = rhs)
146
132
147
133
case tree : DefDef =>
148
- // only transform the params (for the default values) and the rhs
149
- // force instrumentation of literals and other small trees in the rhs,
150
- // to ensure that the method call are recorded
134
+ // Only transform the params (for the default values) and the rhs.
151
135
val paramss = transformParamss(tree.paramss)
152
136
val rhs = transform(tree.rhs)
153
- cpy.DefDef (tree)(tree.name, paramss, tree.tpt, rhs)
137
+ val finalRhs =
138
+ if canInstrumentDefDef(tree) then
139
+ // Ensure that the rhs is always instrumented, if possible
140
+ instrumentBody(tree, rhs)
141
+ else
142
+ rhs
143
+ cpy.DefDef (tree)(tree.name, paramss, tree.tpt, finalRhs)
154
144
155
145
case tree : PackageDef =>
156
146
// only transform the statements of the package
@@ -168,7 +158,7 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
168
158
/** Lifts and instruments an application.
169
159
* Note that if only one arg needs to be lifted, we just lift everything.
170
160
*/
171
- def instrumentLifted (tree : Apply )(using Context ) =
161
+ private def instrumentLifted (tree : Apply )(using Context ) =
172
162
// lifting
173
163
val buffer = mutable.ListBuffer [Tree ]()
174
164
val liftedApply = LiftCoverage .liftForCoverage(buffer, tree)
@@ -179,40 +169,93 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
179
169
Block (
180
170
instrumentedArgs,
181
171
instrumentedApply
182
- ).withSpan(instrumentedApply.span)
172
+ )
183
173
184
- def instrumentCases ( cases : List [ CaseDef ] )(using Context ): List [ CaseDef ] =
185
- cases.map(instrumentCaseDef )
174
+ private inline def transformApply ( tree : Apply )(using Context ): Apply =
175
+ transformApply(tree, transform(tree.fun) )
186
176
187
- def instrumentCaseDef (tree : CaseDef )(using Context ): CaseDef =
188
- cpy.CaseDef (tree)(tree.pat , transform(tree.guard), transform(tree.body ))
177
+ private inline def transformApply (tree : Apply , transformedFun : Tree )(using Context ): Apply =
178
+ cpy.Apply (tree)(transformedFun , transform(tree.args ))
189
179
190
- def instrument (tree : Tree , branch : Boolean = false )(using Context ): Tree =
180
+ private inline def instrumentCases (cases : List [CaseDef ])(using Context ): List [CaseDef ] =
181
+ cases.map(instrumentCaseDef)
182
+
183
+ private def instrumentCaseDef (tree : CaseDef )(using Context ): CaseDef =
184
+ val pat = tree.pat
185
+ val guard = tree.guard
186
+ val friendlyEnd = if guard.span.exists then guard.span.end else pat.span.end
187
+ val pos = tree.sourcePos.withSpan(tree.span.withEnd(friendlyEnd)) // user-friendly span
188
+ // ensure that the body is always instrumented by inserting a call to Invoker.invoked at its beginning
189
+ val instrumentedBody = instrument(transform(tree.body), pos, false )
190
+ cpy.CaseDef (tree)(tree.pat, transform(tree.guard), instrumentedBody)
191
+
192
+ /** Records information about a new coverable statement. Generates a unique id for it.
193
+ * @return the statement's id
194
+ */
195
+ private def recordStatement (tree : Tree , pos : SourcePosition , branch : Boolean )(using ctx : Context ): Int =
196
+ val id = statementId
197
+ statementId += 1
198
+ val statement = new Statement (
199
+ source = ctx.source.file.name,
200
+ location = Location (tree),
201
+ id = id,
202
+ start = pos.start,
203
+ end = pos.end,
204
+ line = pos.line,
205
+ desc = tree.source.content.slice(pos.start, pos.end).mkString,
206
+ symbolName = tree.symbol.name.toSimpleName.toString,
207
+ treeName = tree.getClass.getSimpleName.nn,
208
+ branch
209
+ )
210
+ coverage.addStatement(statement)
211
+ id
212
+
213
+ private inline def syntheticSpan (pos : SourcePosition ): Span = pos.span.toSynthetic
214
+
215
+ /** Shortcut for instrument(tree, tree.sourcePos, branch) */
216
+ private inline def instrument (tree : Tree , branch : Boolean = false )(using Context ): Tree =
191
217
instrument(tree, tree.sourcePos, branch)
192
218
193
- def instrument (tree : Tree , pos : SourcePosition , branch : Boolean )(using ctx : Context ): Tree =
219
+ /** Instruments a statement, if it has a position. */
220
+ private def instrument (tree : Tree , pos : SourcePosition , branch : Boolean )(using Context ): Tree =
194
221
if pos.exists && ! pos.span.isZeroExtent then
195
- statementId += 1
196
- val id = statementId
197
- val statement = new Statement (
198
- source = ctx.source.file.name,
199
- location = Location (tree),
200
- id = id,
201
- start = pos.start,
202
- end = pos.end,
203
- line = pos.line,
204
- desc = tree.source.content.slice(pos.start, pos.end).mkString,
205
- symbolName = tree.symbol.name.toSimpleName.toString(),
206
- treeName = tree.getClass.getSimpleName.nn,
207
- branch
208
- )
209
- coverage.addStatement(statement)
210
- val span = Span (pos.start, pos.end) // synthetic span
211
- Block (List (invokeCall(id, span)), tree).withSpan(span)
222
+ val statementId = recordStatement(tree, pos, branch)
223
+ insertInvokeCall(tree, pos, statementId)
212
224
else
213
225
tree
214
226
215
- def invokeCall (id : Int , span : Span )(using Context ): Tree =
227
+ /** Instruments the body of a DefDef. Handles corner cases. */
228
+ private def instrumentBody (parent : DefDef , body : Tree )(using Context ): Tree =
229
+ /* recurse on closures, so that we insert the call at the leaf:
230
+
231
+ def g: (a: Ta) ?=> (b: Tb) = {
232
+ // nothing here <-- not here!
233
+ def $anonfun(using a: Ta) =
234
+ Invoked.invoked(id, DIR) <-- here
235
+ <userCode>
236
+ closure($anonfun)
237
+ }
238
+ */
239
+ body match
240
+ case b @ Block ((meth : DefDef ) :: Nil , closure : Closure )
241
+ if meth.symbol == closure.meth.symbol && defn.isContextFunctionType(body.tpe) =>
242
+ val instr = cpy.DefDef (meth)(rhs = instrumentBody(parent, meth.rhs))
243
+ cpy.Block (b)(instr :: Nil , closure)
244
+ case _ =>
245
+ // compute user-friendly position to highlight more text in the coverage UI
246
+ val namePos = parent.namePos
247
+ val pos = namePos.withSpan(namePos.span.withStart(parent.span.start))
248
+ // record info and insert call to Invoker.invoked
249
+ val statementId = recordStatement(parent, pos, false )
250
+ insertInvokeCall(body, pos, statementId)
251
+
252
+ /** Returns the tree, prepended by a call to Invoker.invoker */
253
+ private def insertInvokeCall (tree : Tree , pos : SourcePosition , statementId : Int )(using Context ): Tree =
254
+ val callSpan = syntheticSpan(pos)
255
+ Block (invokeCall(statementId, callSpan) :: Nil , tree).withSpan(callSpan.union(tree.span))
256
+
257
+ /** Generates Invoked.invoked(id, DIR) */
258
+ private def invokeCall (id : Int , span : Span )(using Context ): Tree =
216
259
val outputPath = ctx.settings.coverageOutputDir.value
217
260
ref(defn.InvokedMethodRef ).withSpan(span)
218
261
.appliedToArgs(
@@ -229,7 +272,7 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
229
272
* ```
230
273
* should not be changed to {val $x = f(); T($x)}(1) but to {val $x = f(); val $y = 1; T($x)($y)}
231
274
*/
232
- def needsLift (tree : Apply )(using Context ): Boolean =
275
+ private def needsLift (tree : Apply )(using Context ): Boolean =
233
276
def isBooleanOperator (fun : Tree ) =
234
277
// We don't want to lift a || getB(), to avoid calling getB if a is true.
235
278
// Same idea with a && getB(): if a is false, getB shouldn't be called.
@@ -249,9 +292,17 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
249
292
nestedApplyNeedsLift ||
250
293
! isBooleanOperator(fun) && ! tree.args.isEmpty && ! tree.args.forall(LiftCoverage .noLift)
251
294
252
- def canInstrumentApply (tree : Apply )(using Context ): Boolean =
253
- val tpe = tree.typeOpt
254
- tpe match
295
+ /** Check if the body of a DefDef can be instrumented with instrumentBody. */
296
+ private def canInstrumentDefDef (tree : DefDef )(using Context ): Boolean =
297
+ // No need to force the instrumentation of synthetic definitions
298
+ // (it would work, but it looks better without).
299
+ ! tree.symbol.isOneOf(Accessor | Synthetic | Artifact ) &&
300
+ ! tree.rhs.isEmpty
301
+
302
+ /** Check if an Apply can be instrumented. Prevents this phase from generating incorrect code. */
303
+ private def canInstrumentApply (tree : Apply )(using Context ): Boolean =
304
+ ! tree.symbol.isOneOf(Synthetic | Artifact ) && // no need to instrument synthetic apply
305
+ (tree.typeOpt match
255
306
case AppliedType (tycon : NamedType , _) =>
256
307
/* If the last expression in a block is a context function, we'll try to
257
308
summon its arguments at the current point, even if the expected type
@@ -267,11 +318,15 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
267
318
*/
268
319
! tycon.name.isContextFunction
269
320
case m : MethodType =>
270
- // def f(a: Ta)(b: Tb)
271
- // f(a)(b) cannot be rewritten to {invoked();f(a)}(b)
321
+ /* def f(a: Ta)(b: Tb)
322
+ f(a)(b)
323
+
324
+ Here, f(a)(b) cannot be rewritten to {invoked();f(a)}(b)
325
+ */
272
326
false
273
327
case _ =>
274
328
true
329
+ )
275
330
276
331
object InstrumentCoverage :
277
332
val name : String = " instrumentCoverage"
0 commit comments