@@ -60,27 +60,13 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
6060
6161 /** Transforms trees to insert calls to Invoker.invoked to compute the coverage when the code is called */
6262 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-
6863 override def transform (tree : Tree )(using ctx : Context ): Tree =
6964 inContext(transformCtx(tree)) { // necessary to position inlined code properly
7065 tree match
7166 // simple cases
72- case tree : (Import | Export | This | Super | New ) => tree
67+ case tree : (Import | Export | Literal | This | Super | New ) => tree
7368 case tree if tree.isEmpty || tree.isType => tree // empty Thicket, Ident, TypTree, ...
7469
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-
8470 // branches
8571 case tree : If =>
8672 cpy.If (tree)(
@@ -92,32 +78,32 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
9278 cpy.Try (tree)(
9379 expr = instrument(transform(tree.expr), branch = true ),
9480 cases = instrumentCases(tree.cases),
95- finalizer = instrument(transform(tree.finalizer), true )
81+ finalizer = instrument(transform(tree.finalizer), branch = true )
9682 )
9783
9884 // a.f(args)
9985 case tree @ Apply (fun : Select , args) =>
10086 // 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)
10188 if canInstrumentApply(tree) then
102- val transformedFun = cpy.Select (fun)(transform(fun.qualifier), fun.name)
10389 if needsLift(tree) then
10490 val transformed = cpy.Apply (tree)(transformedFun, args) // args will be transformed in instrumentLifted
105- instrumentLifted(transformed)( using ignoreLiteralsContext)
91+ instrumentLifted(transformed)
10692 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)
10995 else
110- tree
96+ transformApply( tree, transformedFun)
11197
11298 // f(args)
11399 case tree : Apply =>
114100 if canInstrumentApply(tree) then
115101 if needsLift(tree) then
116- instrumentLifted(tree)( using ignoreLiteralsContext) // see comment about Literals
102+ instrumentLifted(tree)
117103 else
118- instrument(super .transform (tree)( using ignoreLiteralsContext ))
104+ instrument(transformApply (tree))
119105 else
120- tree
106+ transformApply( tree)
121107
122108 // (f(x))[args]
123109 case TypeApply (fun : Apply , args) =>
@@ -142,15 +128,19 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
142128 case tree : ValDef =>
143129 // only transform the rhs
144130 val rhs = transform(tree.rhs)
145- cpy.ValDef (tree)(rhs= rhs)
131+ cpy.ValDef (tree)(rhs = rhs)
146132
147133 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.
151135 val paramss = transformParamss(tree.paramss)
152136 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)
154144
155145 case tree : PackageDef =>
156146 // only transform the statements of the package
@@ -168,7 +158,7 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
168158 /** Lifts and instruments an application.
169159 * Note that if only one arg needs to be lifted, we just lift everything.
170160 */
171- def instrumentLifted (tree : Apply )(using Context ) =
161+ private def instrumentLifted (tree : Apply )(using Context ) =
172162 // lifting
173163 val buffer = mutable.ListBuffer [Tree ]()
174164 val liftedApply = LiftCoverage .liftForCoverage(buffer, tree)
@@ -179,40 +169,93 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
179169 Block (
180170 instrumentedArgs,
181171 instrumentedApply
182- ).withSpan(instrumentedApply.span)
172+ )
183173
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) )
186176
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 ))
189179
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 =
191217 instrument(tree, tree.sourcePos, branch)
192218
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 =
194221 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)
212224 else
213225 tree
214226
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 =
216259 val outputPath = ctx.settings.coverageOutputDir.value
217260 ref(defn.InvokedMethodRef ).withSpan(span)
218261 .appliedToArgs(
@@ -229,7 +272,7 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
229272 * ```
230273 * should not be changed to {val $x = f(); T($x)}(1) but to {val $x = f(); val $y = 1; T($x)($y)}
231274 */
232- def needsLift (tree : Apply )(using Context ): Boolean =
275+ private def needsLift (tree : Apply )(using Context ): Boolean =
233276 def isBooleanOperator (fun : Tree ) =
234277 // We don't want to lift a || getB(), to avoid calling getB if a is true.
235278 // Same idea with a && getB(): if a is false, getB shouldn't be called.
@@ -249,9 +292,17 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
249292 nestedApplyNeedsLift ||
250293 ! isBooleanOperator(fun) && ! tree.args.isEmpty && ! tree.args.forall(LiftCoverage .noLift)
251294
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
255306 case AppliedType (tycon : NamedType , _) =>
256307 /* If the last expression in a block is a context function, we'll try to
257308 summon its arguments at the current point, even if the expected type
@@ -267,11 +318,15 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
267318 */
268319 ! tycon.name.isContextFunction
269320 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+ */
272326 false
273327 case _ =>
274328 true
329+ )
275330
276331object InstrumentCoverage :
277332 val name : String = " instrumentCoverage"
0 commit comments