@@ -23,10 +23,11 @@ import localopt.StringInterpolatorOpt
23
23
* The result can then be consumed by the Scoverage tool.
24
24
*/
25
25
class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer :
26
+ import InstrumentCoverage .{name , description , InstrumentedParts }
26
27
27
- override def phaseName = InstrumentCoverage . name
28
+ override def phaseName = name
28
29
29
- override def description = InstrumentCoverage . description
30
+ override def description = description
30
31
31
32
// Enabled by argument "-coverage-out OUTPUT_DIR"
32
33
override def isEnabled (using ctx : Context ) =
@@ -55,10 +56,148 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
55
56
56
57
Serializer .serialize(coverage, outputPath, ctx.settings.sourceroot.value)
57
58
58
- override protected def newTransformer (using Context ) = CoverageTransformer ()
59
+ override protected def newTransformer (using Context ) =
60
+ CoverageTransformer (ctx.settings.coverageOutputDir.value)
59
61
60
62
/** Transforms trees to insert calls to Invoker.invoked to compute the coverage when the code is called */
61
- private class CoverageTransformer extends Transformer :
63
+ private class CoverageTransformer (outputPath : String ) extends Transformer :
64
+ private val ConstOutputPath = Constant (outputPath)
65
+
66
+ /** Generates the tree for:
67
+ * ```
68
+ * Invoker.invoked(id, DIR)
69
+ * ```
70
+ * where DIR is the _outputPath_ defined by the coverage settings.
71
+ */
72
+ private def invokeCall (id : Int , span : Span )(using Context ): GenericApply =
73
+ ref(defn.InvokedMethodRef ).withSpan(span)
74
+ .appliedToArgs(
75
+ Literal (Constant (id)) :: Literal (ConstOutputPath ) :: Nil
76
+ ).withSpan(span)
77
+
78
+ /**
79
+ * Records information about a new coverable statement. Generates a unique id for it.
80
+ *
81
+ * @param tree the tree to add to the coverage report
82
+ * @param pos the position to save in the report
83
+ * @param branch true if it's a branch (branches are considered differently by most coverage analysis tools)
84
+ * @param ctx the current context
85
+ * @return the statement's id
86
+ */
87
+ private def recordStatement (tree : Tree , pos : SourcePosition , branch : Boolean )(using ctx : Context ): Int =
88
+ val id = statementId
89
+ statementId += 1
90
+ val statement = Statement (
91
+ source = ctx.source.file.name,
92
+ location = Location (tree),
93
+ id = id,
94
+ start = pos.start,
95
+ end = pos.end,
96
+ line = pos.line,
97
+ desc = tree.source.content.slice(pos.start, pos.end).mkString,
98
+ symbolName = tree.symbol.name.toSimpleName.toString,
99
+ treeName = tree.getClass.getSimpleName.nn,
100
+ branch
101
+ )
102
+ coverage.addStatement(statement)
103
+ id
104
+
105
+ /**
106
+ * Adds a new statement to the current `Coverage` and creates a corresponding call
107
+ * to `Invoker.invoke` with its id, and the given position.
108
+ *
109
+ * Note that the entire tree won't be saved in the coverage analysis, only some
110
+ * data related to the tree is recorded (e.g. its type, its parent class, ...).
111
+ *
112
+ * @param tree the tree to add to the coverage report
113
+ * @param pos the position to save in the report
114
+ * @param branch true if it's a branch
115
+ * @return the tree corresponding to the call to `Invoker.invoke`
116
+ */
117
+ private def createInvokeCall (tree : Tree , pos : SourcePosition , branch : Boolean = false )(using Context ): Apply =
118
+ val statementId = recordStatement(tree, pos, branch)
119
+ val span = pos.span.toSynthetic
120
+ invokeCall(statementId, span).asInstanceOf [Apply ]
121
+
122
+ /**
123
+ * Tries to instrument a DefDef.
124
+ * These "tryInstrument" methods are useful to tweak the generation of coverage instrumentation,
125
+ * in particular in `case TypeApply` in the [[transform ]] method.
126
+ *
127
+ * @param tree the tree to instrument
128
+ * @return instrumentation result
129
+ */
130
+ private def tryInstrument (tree : DefDef )(using Context ): InstrumentedParts =
131
+ if tree.symbol.isOneOf(Inline | Erased ) then
132
+ // Inline and erased definitions will not be in the generated code and therefore do not need to be instrumented.
133
+ // Note that a retained inline method will have a `$retained` variant that will be instrumented.
134
+ return InstrumentedParts .notCovered(tree)
135
+
136
+ // Only transform the params (for the default values) and the rhs.
137
+ val paramss = transformParamss(tree.paramss)
138
+ val rhs = transform(tree.rhs)
139
+
140
+ ???
141
+
142
+ private def tryInstrument (tree : Apply )(using Context ): InstrumentedParts =
143
+ if ! canInstrumentApply(tree) then
144
+ return InstrumentedParts .notCovered(tree)
145
+
146
+ // Create a call to Invoker.invoked(coverageDirectory, newStatementId)
147
+ val coverageCall = createInvokeCall(tree, tree.sourcePos)
148
+
149
+ if needsLift(tree) then
150
+ // Lifts the arguments. Note that if only one argument needs to be lifted, we lift them all.
151
+ // See LiftCoverage for the internal working of this lifting.
152
+ val app = cpy.Apply (tree)(transform(tree.fun), tree.args)
153
+ val liftedDefs = mutable.ListBuffer [Tree ]()
154
+ val liftedApp = LiftCoverage .liftForCoverage(liftedDefs, app)
155
+
156
+ // Instrument the arguments
157
+ liftedDefs.mapInPlace(transform)
158
+
159
+ // Builds the parts
160
+ InstrumentedParts (liftedDefs.toList, coverageCall, liftedApp)
161
+ else
162
+ // Instrument without lifting
163
+ val app = cpy.Apply (tree)(transform(tree.fun), transform(tree.args))
164
+ InstrumentedParts .singleExpr(coverageCall, app)
165
+ InstrumentedParts .notCovered(tree)
166
+
167
+ private def tryInstrument (tree : Ident )(using Context ): InstrumentedParts =
168
+ val sym = tree.symbol
169
+ if canInstrumentParameterless(sym) then
170
+ // call to a local parameterless method f
171
+ val coverageCall = createInvokeCall(tree, tree.sourcePos)
172
+ InstrumentedParts .singleExpr(coverageCall, tree)
173
+ else
174
+ InstrumentedParts .notCovered(tree)
175
+
176
+ private def tryInstrument (tree : Select )(using Context ): InstrumentedParts =
177
+ val transformed = cpy.Select (tree)(transform(tree.qualifier), tree.name)
178
+ val sym = tree.symbol
179
+ if canInstrumentParameterless(sym) then
180
+ // call to a parameterless method
181
+ val coverageCall = createInvokeCall(tree, tree.sourcePos)
182
+ InstrumentedParts .singleExpr(coverageCall, transformed)
183
+ else
184
+ InstrumentedParts .notCovered(transformed)
185
+
186
+ /** Generic tryInstrument */
187
+ private def tryInstrument (tree : Tree )(using Context ): InstrumentedParts =
188
+ tree match
189
+ case t : Apply => tryInstrument(t)
190
+ case t : Ident => tryInstrument(t)
191
+ case t : Select => tryInstrument(t)
192
+ case _ => InstrumentedParts .notCovered(tree)
193
+
194
+ /**
195
+ * Instrument a branch and returns the resulting tree.
196
+ */
197
+ private def instrumentBranch (tree : Tree )(using Context ): Tree =
198
+ val coverageCall = createInvokeCall(tree, tree.sourcePos, branch = true )
199
+ InstrumentedParts .singleExpr(coverageCall, tree).toTree
200
+
62
201
override def transform (tree : Tree )(using Context ): Tree =
63
202
inContext(transformCtx(tree)) { // necessary to position inlined code properly
64
203
tree match
@@ -68,69 +207,54 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
68
207
69
208
// identifier
70
209
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
210
+ tryInstrument(tree).toTree
77
211
78
212
// branches
79
213
case tree : If =>
80
214
cpy.If (tree)(
81
215
cond = transform(tree.cond),
82
- thenp = instrument (transform(tree.thenp), branch = true ),
83
- elsep = instrument (transform(tree.elsep), branch = true )
216
+ thenp = instrumentBranch (transform(tree.thenp)),
217
+ elsep = instrumentBranch (transform(tree.elsep))
84
218
)
85
219
case tree : Try =>
86
220
cpy.Try (tree)(
87
- expr = instrument (transform(tree.expr), branch = true ),
88
- cases = instrumentCases( tree.cases),
89
- finalizer = instrument (transform(tree.finalizer), branch = true )
221
+ expr = instrumentBranch (transform(tree.expr)),
222
+ cases = tree.cases.map(instrumentCaseDef ),
223
+ finalizer = instrumentBranch (transform(tree.finalizer))
90
224
)
91
225
92
226
// f(args)
93
227
case tree : Apply =>
94
- if canInstrumentApply(tree) then
95
- if needsLift(tree) then
96
- instrumentLifted(tree)
97
- else
98
- instrument(transformApply(tree))
99
- else
100
- transformApply(tree)
228
+ tryInstrument(tree).toTree
101
229
102
230
// (fun)[args]
103
231
case TypeApply (fun, args) =>
104
- val tfun = transform (fun)
105
- tfun match
106
- case InstrumentCoverage . InstrumentedBlock (invokeCall, expr) =>
107
- // expr[T] shouldn't be transformed to
108
- // {invoked(...), expr}[T]
109
- //
110
- // but to
111
- // {invoked(...), expr[T]}
112
- //
113
- // This is especially important for trees like ( expr[T])(args),
114
- // for which the wrong transformation crashes the compiler.
115
- // See tests/coverage/pos/PolymorphicExtensions.scala
116
- Block (
117
- invokeCall :: Nil ,
118
- cpy. TypeApply (tree)(expr, args)
119
- )
120
- case _ =>
121
- cpy. TypeApply (tree)(tfun, args )
232
+ val InstrumentedParts (pre, coverageCall, expr) = tryInstrument (fun)
233
+ if coverageCall.isEmpty then
234
+ // `fun` cannot be instrumented, and `args` is a type so we keep this tree as it is
235
+ tree
236
+ else
237
+ // expr[T] shouldn't be transformed to:
238
+ // {invoked(...), expr}[T]
239
+ //
240
+ // but to:
241
+ // {invoked(...), expr[T]}
242
+ //
243
+ // This is especially important for trees like (expr[T])(args),
244
+ // for which the wrong transformation crashes the compiler.
245
+ // See tests/coverage/pos/PolymorphicExtensions.scala
246
+ Block (
247
+ pre :+ coverageCall,
248
+ cpy. TypeApply (tree)(expr, args)
249
+ )
122
250
123
251
// a.b
124
- case Select (qual, name) =>
125
- val transformed = cpy.Select (tree)(transform(qual), name)
126
- val sym = tree.symbol
127
- if canInstrumentParameterless(sym) then
128
- // call to a parameterless method
129
- instrument(transformed)
130
- else
131
- transformed
252
+ case tree : Select =>
253
+ tryInstrument(tree).toTree
254
+
255
+ case tree : CaseDef =>
256
+ instrumentCaseDef(tree)
132
257
133
- case tree : CaseDef => instrumentCaseDef(tree)
134
258
case tree : ValDef =>
135
259
// only transform the rhs
136
260
val rhs = transform(tree.rhs)
@@ -168,71 +292,21 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
168
292
super .transform(tree)
169
293
}
170
294
171
- /** Lifts and instruments an application.
172
- * Note that if only one arg needs to be lifted, we just lift everything (see LiftCoverage).
173
- */
174
- private def instrumentLifted (tree : Apply )(using Context ) =
175
- // lifting
176
- val buffer = mutable.ListBuffer [Tree ]()
177
- val liftedApply = LiftCoverage .liftForCoverage(buffer, tree)
178
-
179
- // instrumentation
180
- val instrumentedArgs = buffer.toList.map(transform)
181
- val instrumentedApply = instrument(liftedApply)
182
- Block (
183
- instrumentedArgs,
184
- instrumentedApply
185
- )
186
-
187
- private inline def transformApply (tree : Apply )(using Context ): Apply =
188
- cpy.Apply (tree)(transform(tree.fun), transform(tree.args))
189
-
190
- private inline def instrumentCases (cases : List [CaseDef ])(using Context ): List [CaseDef ] =
191
- cases.map(instrumentCaseDef)
192
-
193
295
private def instrumentCaseDef (tree : CaseDef )(using Context ): CaseDef =
194
296
val pat = tree.pat
195
297
val guard = tree.guard
196
298
val friendlyEnd = if guard.span.exists then guard.span.end else pat.span.end
197
299
val pos = tree.sourcePos.withSpan(tree.span.withEnd(friendlyEnd)) // user-friendly span
198
- // ensure that the body is always instrumented by inserting a call to Invoker.invoked at its beginning
199
- val instrumentedBody = instrument(transform(tree.body), pos, false )
200
- cpy.CaseDef (tree)(tree.pat, transform(tree.guard), instrumentedBody)
201
-
202
- /** Records information about a new coverable statement. Generates a unique id for it.
203
- * @return the statement's id
204
- */
205
- private def recordStatement (tree : Tree , pos : SourcePosition , branch : Boolean )(using ctx : Context ): Int =
206
- val id = statementId
207
- statementId += 1
208
- val statement = Statement (
209
- source = ctx.source.file.name,
210
- location = Location (tree),
211
- id = id,
212
- start = pos.start,
213
- end = pos.end,
214
- line = pos.line,
215
- desc = tree.source.content.slice(pos.start, pos.end).mkString,
216
- symbolName = tree.symbol.name.toSimpleName.toString,
217
- treeName = tree.getClass.getSimpleName.nn,
218
- branch
219
- )
220
- coverage.addStatement(statement)
221
- id
222
300
223
- private inline def syntheticSpan (pos : SourcePosition ): Span = pos.span.toSynthetic
301
+ // recursively transform the guard, but keep the pat
302
+ val transformedGuard = transform(tree.guard)
224
303
225
- /** Shortcut for instrument(tree, tree.sourcePos, branch) */
226
- private inline def instrument (tree : Tree , branch : Boolean = false )(using Context ): Tree =
227
- instrument(tree, tree.sourcePos, branch)
304
+ // ensure that the body is always instrumented by inserting a call to Invoker.invoked at its beginning
305
+ val transformedBody = transform(tree.body)
306
+ val coverageCall = createInvokeCall(transformedBody, pos)
307
+ val instrumentedBody = InstrumentedParts .singleExpr(coverageCall, transformedBody).toTree
228
308
229
- /** Instruments a statement, if it has a position. */
230
- private def instrument (tree : Tree , pos : SourcePosition , branch : Boolean )(using Context ): Tree =
231
- if pos.exists && ! pos.span.isZeroExtent then
232
- val statementId = recordStatement(tree, pos, branch)
233
- insertInvokeCall(tree, pos, statementId)
234
- else
235
- tree
309
+ cpy.CaseDef (tree)(tree.pat, transformedGuard, instrumentedBody)
236
310
237
311
/** Instruments the body of a DefDef. Handles corner cases. */
238
312
private def instrumentBody (parent : DefDef , body : Tree )(using Context ): Tree =
@@ -256,21 +330,8 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
256
330
val namePos = parent.namePos
257
331
val pos = namePos.withSpan(namePos.span.withStart(parent.span.start))
258
332
// record info and insert call to Invoker.invoked
259
- val statementId = recordStatement(parent, pos, false )
260
- insertInvokeCall(body, pos, statementId)
261
-
262
- /** Returns the tree, prepended by a call to Invoker.invoked */
263
- private def insertInvokeCall (tree : Tree , pos : SourcePosition , statementId : Int )(using Context ): Tree =
264
- val callSpan = syntheticSpan(pos)
265
- Block (invokeCall(statementId, callSpan) :: Nil , tree).withSpan(callSpan.union(tree.span))
266
-
267
- /** Generates Invoker.invoked(id, DIR) */
268
- private def invokeCall (id : Int , span : Span )(using Context ): Tree =
269
- val outputPath = ctx.settings.coverageOutputDir.value
270
- ref(defn.InvokedMethodRef ).withSpan(span)
271
- .appliedToArgs(
272
- List (Literal (Constant (id)), Literal (Constant (outputPath)))
273
- ).withSpan(span)
333
+ val coverageCall = createInvokeCall(parent, pos)
334
+ InstrumentedParts .singleExpr(coverageCall, body).toTree
274
335
275
336
/**
276
337
* Checks if the apply needs a lift in the coverage phase.
@@ -371,14 +432,20 @@ object InstrumentCoverage:
371
432
val name : String = " instrumentCoverage"
372
433
val description : String = " instrument code for coverage checking"
373
434
374
- /** Extractor object for trees produced by `insertInvokeCall`. */
375
- object InstrumentedBlock :
376
- private def isInvokedCall (app : Apply )(using Context ): Boolean =
377
- app.span.isSynthetic && app.symbol == defn.InvokedMethodRef .symbol
378
-
379
- def unapply (t : Tree )(using Context ): Option [(Apply , Tree )] =
380
- t match
381
- case Block ((app : Apply ) :: Nil , expr) if isInvokedCall(app) =>
382
- Some ((app, expr))
383
- case _ =>
384
- None
435
+ /**
436
+ * An instrumented Tree, in 3 parts.
437
+ * @param pre preparation code, e.g. lifted arguments. May be empty.
438
+ * @param invokeCall call to Invoker.invoked(dir, id), or an empty tree.
439
+ * @param expr the instrumented expression, executed just after the invokeCall
440
+ */
441
+ case class InstrumentedParts (pre : List [Tree ], invokeCall : Apply | Thicket , expr : Tree ):
442
+ require(pre.isEmpty || (pre.nonEmpty && ! invokeCall.isEmpty), " if pre isn't empty then invokeCall shouldn't be empty" )
443
+
444
+ def toTree (using Context ): Tree =
445
+ if invokeCall.isEmpty then expr
446
+ else if pre.isEmpty then Block (invokeCall :: Nil , expr)
447
+ else Block (pre :+ invokeCall, expr)
448
+
449
+ object InstrumentedParts :
450
+ def notCovered (expr : Tree ) = InstrumentedParts (Nil , EmptyTree , expr)
451
+ def singleExpr (invokeCall : Apply , expr : Tree ) = InstrumentedParts (Nil , invokeCall, expr)
0 commit comments