Skip to content

Commit 7384432

Browse files
TheElectronWillmichelou
authored andcommitted
Ensure that cases and DefDefs are always instrumented for coverage
1 parent 0f88cbe commit 7384432

26 files changed

+1295
-668
lines changed

compiler/src/dotty/tools/dotc/ast/Desugar.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1437,6 +1437,7 @@ object desugar {
14371437
ValDef(param.name, param.tpt, selector(idx))
14381438
.withSpan(param.span)
14391439
.withAttachment(UntupledParam, ())
1440+
.withFlags(Synthetic)
14401441
}
14411442
Function(param :: Nil, Block(vdefs, body))
14421443
}
@@ -1693,7 +1694,7 @@ object desugar {
16931694
case (p, n) => makeSyntheticParameter(n + 1, p).withAddedFlags(mods.flags)
16941695
}
16951696
RefinedTypeTree(polyFunctionTpt, List(
1696-
DefDef(nme.apply, applyTParams :: applyVParams :: Nil, res, EmptyTree)
1697+
DefDef(nme.apply, applyTParams :: applyVParams :: Nil, res, EmptyTree).withFlags(Synthetic)
16971698
))
16981699
}
16991700
else {

compiler/src/dotty/tools/dotc/ast/MainProxies.scala

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,14 @@ object MainProxies {
105105
.filterNot(_.matches(defn.MainAnnot))
106106
.map(annot => insertTypeSplices.transform(annot.tree))
107107
val mainMeth = DefDef(nme.main, (mainArg :: Nil) :: Nil, TypeTree(defn.UnitType), body)
108-
.withFlags(JavaStatic)
108+
.withFlags(JavaStatic | Synthetic)
109109
.withAnnotations(annots)
110110
val mainTempl = Template(emptyConstructor, Nil, Nil, EmptyValDef, mainMeth :: Nil)
111111
val mainCls = TypeDef(mainFun.name.toTypeName, mainTempl)
112112
.withFlags(Final | Invisible)
113-
if (!ctx.reporter.hasErrors) result = mainCls.withSpan(mainAnnotSpan.toSynthetic) :: Nil
113+
114+
if (!ctx.reporter.hasErrors)
115+
result = mainCls.withSpan(mainAnnotSpan.toSynthetic) :: Nil
114116
}
115117
result
116118
}

compiler/src/dotty/tools/dotc/ast/Trees.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -610,7 +610,7 @@ object Trees {
610610
override def toString = s"InlineMatch($selector, $cases)"
611611
}
612612

613-
/** case pat if guard => body; only appears as child of a Match */
613+
/** case pat if guard => body */
614614
case class CaseDef[-T >: Untyped] private[ast] (pat: Tree[T], guard: Tree[T], body: Tree[T])(implicit @constructorOnly src: SourceFile)
615615
extends Tree[T] {
616616
type ThisTree[-T >: Untyped] = CaseDef[T]

compiler/src/dotty/tools/dotc/coverage/Coverage.scala

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,5 @@ case class Statement(
2323
symbolName: String,
2424
treeName: String,
2525
branch: Boolean,
26-
var count: Int = 0,
2726
ignored: Boolean = false
28-
):
29-
/** Records that this statement has been invoked one more time. */
30-
def invoked(): Unit =
31-
count += 1
32-
33-
def isInvoked: Boolean =
34-
count > 0
27+
)

compiler/src/dotty/tools/dotc/coverage/Serializer.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ object Serializer:
5858
|""".stripMargin)
5959

6060
def writeStatement(stmt: Statement, writer: Writer): Unit =
61+
// Note: we write 0 for the count because we have not measured the actual coverage at this point
6162
writer.write(s"""${stmt.id}
6263
|${getRelativePath(stmt.location.sourcePath)}
6364
|${stmt.location.packageName}
@@ -71,7 +72,7 @@ object Serializer:
7172
|${stmt.symbolName}
7273
|${stmt.treeName}
7374
|${stmt.branch}
74-
|${stmt.count}
75+
|0
7576
|${stmt.ignored}
7677
|${stmt.desc}
7778
|\f

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

Lines changed: 116 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -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

276331
object InstrumentCoverage:
277332
val name: String = "instrumentCoverage"

docs/_docs/usage/coverage.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ In Scala 2, all these steps were performed by external tools. In particular, ste
1515

1616
In Scala 3, the compiler itself takes care of step 1. To use this feature, add the compile option `-coverage-out:DIR`, where `DIR` is the destination of the measurement files.
1717

18-
You can also set `-coverage-sourceroot:PATHS_ROOT` to customize how the path of your source files are resolved.
18+
You can also set `-sourceroot:PATHS_ROOT` to customize how the path of your source files are resolved.
19+
Note that `-sourceroot` also sets the root path of the SemanticDB files.
1920

2021
## How-to with sbt
2122

0 commit comments

Comments
 (0)