Skip to content

Commit b72702e

Browse files
Coverage: handle TypeApply in a more robust way, fix scala#15771
1 parent 731522a commit b72702e

File tree

2 files changed

+203
-132
lines changed

2 files changed

+203
-132
lines changed

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

Lines changed: 199 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@ import localopt.StringInterpolatorOpt
2323
* The result can then be consumed by the Scoverage tool.
2424
*/
2525
class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
26+
import InstrumentCoverage.{name, description, InstrumentedParts}
2627

27-
override def phaseName = InstrumentCoverage.name
28+
override def phaseName = name
2829

29-
override def description = InstrumentCoverage.description
30+
override def description = description
3031

3132
// Enabled by argument "-coverage-out OUTPUT_DIR"
3233
override def isEnabled(using ctx: Context) =
@@ -55,10 +56,148 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
5556

5657
Serializer.serialize(coverage, outputPath, ctx.settings.sourceroot.value)
5758

58-
override protected def newTransformer(using Context) = CoverageTransformer()
59+
override protected def newTransformer(using Context) =
60+
CoverageTransformer(ctx.settings.coverageOutputDir.value)
5961

6062
/** 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+
62201
override def transform(tree: Tree)(using Context): Tree =
63202
inContext(transformCtx(tree)) { // necessary to position inlined code properly
64203
tree match
@@ -68,69 +207,54 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
68207

69208
// identifier
70209
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
77211

78212
// branches
79213
case tree: If =>
80214
cpy.If(tree)(
81215
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))
84218
)
85219
case tree: Try =>
86220
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))
90224
)
91225

92226
// f(args)
93227
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
101229

102230
// (fun)[args]
103231
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+
)
122250

123251
// 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)
132257

133-
case tree: CaseDef => instrumentCaseDef(tree)
134258
case tree: ValDef =>
135259
// only transform the rhs
136260
val rhs = transform(tree.rhs)
@@ -168,71 +292,21 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
168292
super.transform(tree)
169293
}
170294

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-
193295
private def instrumentCaseDef(tree: CaseDef)(using Context): CaseDef =
194296
val pat = tree.pat
195297
val guard = tree.guard
196298
val friendlyEnd = if guard.span.exists then guard.span.end else pat.span.end
197299
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
222300

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)
224303

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
228308

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)
236310

237311
/** Instruments the body of a DefDef. Handles corner cases. */
238312
private def instrumentBody(parent: DefDef, body: Tree)(using Context): Tree =
@@ -256,21 +330,8 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
256330
val namePos = parent.namePos
257331
val pos = namePos.withSpan(namePos.span.withStart(parent.span.start))
258332
// 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
274335

275336
/**
276337
* Checks if the apply needs a lift in the coverage phase.
@@ -371,14 +432,20 @@ object InstrumentCoverage:
371432
val name: String = "instrumentCoverage"
372433
val description: String = "instrument code for coverage checking"
373434

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

Comments
 (0)