Skip to content

Commit 88140bc

Browse files
Scoverage: fix broken warnings related to tail recursion and infinite loop detection (#25941)
Fixes a bunch of warnings broken by scoverage related to tail recursion and infinite loops detection. Warnings were broken by: - Coverage modifying the trees to inject instrumentation, breaking assumptions by the loop detection and tail recursion phases. - Coverage modifying the spans of the trees when instrumenting them, which breaks the positions at which warnings are reported. The solution by phases affected: **`InstrumentCoverage`** - phase that instruments the trees to inject coverage probes: - Do not instrument literal booleans in `if` conditions - they are getting in the way of rewriting `if` statements with literal conditions into their respective `then` or `else` branches. - Preserve the spans of the trees when instrumenting them, so that warnings are reported at the original position of the code. **`CheckLoopingImplicits`** - phase that detects infinite loops in initializers: - Ignore coverage probes as ones not capable of leading to an infinite loop. - Strip the coverage instrumentation from RHS of defs before analyzing them for loops. **`TailRec`** - phase that rewrites tail recursive calls into loops: - Modified `isInfiniteRecCall` - the method that detects infinite loops in tail recursive calls - to allow coverage probes as ones not capable of breaking an infinite loop. - In `transform`, modify the default getter index in recursive call argument position to peel through the coverage instrumentation to recover the original default-getter parameter index. ## How much have you relied on LLM-based tools in this contribution? Moderately, for codebase analysis and tracing. ## How was the solution tested? `sbt "testCompilation --enable-coverage-phase"`
1 parent 7e96767 commit 88140bc

6 files changed

Lines changed: 94 additions & 115 deletions

File tree

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ class CheckLoopingImplicits extends MiniPhase:
5454
)
5555

5656
def checkNotLooping(t: Tree): Unit = t match
57+
case t if InstrumentCoverage.isCoverageProbe(t) =>
58+
()
5759
case t: Ident =>
5860
checkNotSelfRef(t)
5961
case t @ Select(qual, _) =>
@@ -105,7 +107,7 @@ class CheckLoopingImplicits extends MiniPhase:
105107
if sym.isOneOf(GivenOrImplicit | Lazy | ExtensionMethod)
106108
|| sym.name == nme.apply && sym.owner.is(Module) && sym.owner.sourceModule.isOneOf(GivenOrImplicit)
107109
then
108-
checkNotLooping(mdef.rhs)
110+
checkNotLooping(InstrumentCoverage.stripLeadingCoverage(mdef.rhs))
109111
mdef
110112
end transform
111113
end CheckLoopingImplicits

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

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,11 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
341341
val allProbes = inheritedProbes :+ coverageCall
342342
allProbes match
343343
case single :: Nil => InstrumentedParts.singleExprTree(single, transformed)
344-
case multiple => Block(multiple, transformed)
344+
case multiple => InstrumentCoverage.blockWithExprSpan(multiple, transformed)
345+
346+
private def transformCondition(tree: Tree)(using Context): Tree = tree match
347+
case Literal(Constant(_: Boolean)) => tree
348+
case _ => transform(tree)
345349

346350
override def transform(tree: Tree)(using Context): Tree =
347351
inContext(transformCtx(tree)) { // necessary to position inlined code properly
@@ -366,7 +370,7 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
366370
// branches
367371
case tree: If =>
368372
cpy.If(tree)(
369-
cond = transform(tree.cond),
373+
cond = transformCondition(tree.cond),
370374
thenp = transformBranch(tree.thenp),
371375
elsep = transformBranch(tree.elsep)
372376
)
@@ -400,7 +404,7 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
400404
// This is especially important for trees like (expr[T])(args),
401405
// for which the wrong transformation crashes the compiler.
402406
// See tests/coverage/pos/PolymorphicExtensions.scala
403-
Block(
407+
InstrumentCoverage.blockWithExprSpan(
404408
pre :+ coverageCall,
405409
cpy.TypeApply(tree)(expr, args)
406410
)
@@ -750,6 +754,33 @@ object InstrumentCoverage:
750754
val scoverageLocalOn: Regex = """^\s*//\s*\$COVERAGE-ON\$""".r
751755
val scoverageLocalOff: Regex = """^\s*//\s*\$COVERAGE-OFF\$""".r
752756

757+
/** Coverage probes are synthetic bookkeeping calls that should be transparent to
758+
* later warning logic and should not steal source positions from the user tree
759+
* they wrap.
760+
*/
761+
def isCoverageProbe(tree: Tree)(using Context): Boolean = tree match
762+
case Apply(fun, Literal(Constant(_: Int)) :: Literal(Constant(_: String)) :: Nil) =>
763+
fun.symbol == defn.InvokedMethodRef.symbol
764+
case _ =>
765+
false
766+
767+
/** Remove leading synthetic coverage wrappers to recover the user-written tree. */
768+
def stripLeadingCoverage(tree: Tree)(using Context): Tree = tree match
769+
case Typed(expr, _) =>
770+
stripLeadingCoverage(expr)
771+
case Inlined(_, Nil, expr) =>
772+
stripLeadingCoverage(expr)
773+
case Block(stats, expr) if stats.forall(isCoverageProbe) =>
774+
stripLeadingCoverage(expr)
775+
case _ =>
776+
tree
777+
778+
/** Keep wrapper blocks pointed at the wrapped expression span so later warnings
779+
* still highlight user code instead of synthetic `Invoker.invoked` scaffolding.
780+
*/
781+
def blockWithExprSpan(stats: List[Tree], expr: Tree)(using Context): Tree =
782+
Block(stats, expr).withSpan(expr.span)
783+
753784
/**
754785
* An instrumented Tree, in 3 parts.
755786
* @param pre preparation code, e.g. lifted arguments. May be empty.
@@ -762,13 +793,13 @@ object InstrumentCoverage:
762793
/** Turns this into an actual Tree. */
763794
def toTree(using Context): Tree =
764795
if invokeCall.isEmpty then expr
765-
else if pre.isEmpty then Block(invokeCall :: Nil, expr)
766-
else Block(pre :+ invokeCall, expr)
796+
else if pre.isEmpty then blockWithExprSpan(invokeCall :: Nil, expr)
797+
else blockWithExprSpan(pre :+ invokeCall, expr)
767798

768799
object InstrumentedParts:
769800
def notCovered(expr: Tree) = InstrumentedParts(Nil, EmptyTree, expr)
770801
def singleExpr(invokeCall: Apply, expr: Tree) = InstrumentedParts(Nil, invokeCall, expr)
771802

772803
/** Shortcut for `singleExpr(call, expr).toTree` */
773804
def singleExprTree(invokeCall: Apply, expr: Tree)(using Context): Tree =
774-
Block(invokeCall :: Nil, expr)
805+
blockWithExprSpan(invokeCall :: Nil, expr)

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

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ class TailRec extends MiniPhase {
195195
*/
196196
def isInfiniteRecCall(tree: Tree): Boolean = {
197197
def tailArgOrPureExpr(stat: Tree): Boolean = stat match {
198+
case stat if InstrumentCoverage.isCoverageProbe(stat) => true
198199
case stat: ValDef if stat.name.is(TailTempName) || !stat.symbol.is(Mutable) => tailArgOrPureExpr(stat.rhs)
199200
case Assign(lhs: Ident, rhs) if lhs.symbol.name.is(TailLocalName) =>
200201
tailArgOrPureExpr(rhs) || varForRewrittenThis.exists(_ == lhs.symbol && rhs.tpe.isStable)
@@ -325,11 +326,31 @@ class TailRec extends MiniPhase {
325326
method.matches(calledMethod) &&
326327
enclosingClass.appliedRef.widen <:< prefix.tpe.widenDealias
327328

329+
// Argument shape under coverage: `{ Invoker.invoked(...); f$default$n(...) }`;
330+
// strip the probe block and recover `f$default$n`'s parameter index.
331+
def defaultGetterIndex(arg: Tree): Option[Int] =
332+
def fromSymbol(sym: Symbol): Option[Int] =
333+
if sym.exists && sym.name.is(DefaultGetterName) then
334+
val DefaultGetterName(_, index) = sym.name: @unchecked
335+
Some(index)
336+
else
337+
None
338+
339+
val stripped = InstrumentCoverage.stripLeadingCoverage(arg)
340+
fromSymbol(stripped.symbol).orElse {
341+
stripped match
342+
case id: Ident =>
343+
id.symbol.defTree match
344+
case vdef: ValDef => defaultGetterIndex(vdef.rhs)
345+
case _ => None
346+
case _ =>
347+
None
348+
}
349+
328350
if isRecursiveCall then
329351
if ctx.settings.Whas.recurseWithDefault then
330-
tree.args.find(_.symbol.name.is(DefaultGetterName)) match
331-
case Some(arg) =>
332-
val DefaultGetterName(_, index) = arg.symbol.name: @unchecked
352+
tree.args.iterator.flatMap(defaultGetterIndex).nextOption() match
353+
case Some(index) =>
333354
report.warning(RecurseWithDefault(calledMethod.info.firstParamNames(index)), tree.srcPos)
334355
case _ =>
335356

compiler/test/dotc/scoverage-ignore.excludelist

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,32 +15,25 @@ i10889.scala
1515
i11247.scala
1616
i11556.scala
1717
i12739.scala
18-
i13011.scala
19-
i13542.scala
2018
i14164.scala
2119
i14947.scala
2220
i15165.scala
2321
i15864.scala
2422
i18263.orig.scala
2523
i18263.scala
2624
i18589
27-
i19505.scala
2825
i19955a.scala
2926
i19955b.scala
3027
i20053b.scala
3128
i21313.scala
3229
i2146.scala
3330
i23179.scala
34-
i23277.scala
3531
i23489.scala
36-
i23541.scala
37-
i23693.scala
3832
i24039.scala
3933
i5039.scala
4034
i8623.scala
4135
i8900a3.scala
4236
i9228.scala
43-
i9880.scala
4437
minicheck.scala
4538
minicheck-toplevel.scala
4639
mt-scrutinee-widen3.scala

tests/coverage/pos/SimpleMethods.scoverage.check

Lines changed: 15 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -229,23 +229,6 @@ C
229229
Class
230230
covtest.C
231231
cond
232-
195
233-
200
234-
15
235-
<none>
236-
Literal
237-
false
238-
0
239-
false
240-
false
241-
242-
13
243-
SimpleMethods.scala
244-
covtest
245-
C
246-
Class
247-
covtest.C
248-
cond
249232
206
250233
210
251234
15
@@ -256,7 +239,7 @@ false
256239
false
257240
true
258241

259-
14
242+
13
260243
SimpleMethods.scala
261244
covtest
262245
C
@@ -273,7 +256,7 @@ true
273256
false
274257
true
275258

276-
15
259+
14
277260
SimpleMethods.scala
278261
covtest
279262
C
@@ -290,7 +273,7 @@ false
290273
false
291274
false
292275

293-
16
276+
15
294277
SimpleMethods.scala
295278
covtest
296279
C
@@ -307,7 +290,7 @@ true
307290
false
308291
false
309292

310-
17
293+
16
311294
SimpleMethods.scala
312295
covtest
313296
C
@@ -324,24 +307,7 @@ false
324307
false
325308
def cond
326309

327-
18
328-
SimpleMethods.scala
329-
covtest
330-
C
331-
Class
332-
covtest.C
333-
partialCond
334-
260
335-
265
336-
19
337-
<none>
338-
Literal
339-
false
340-
0
341-
false
342-
false
343-
344-
19
310+
17
345311
SimpleMethods.scala
346312
covtest
347313
C
@@ -358,7 +324,7 @@ false
358324
false
359325
()
360326

361-
20
327+
18
362328
SimpleMethods.scala
363329
covtest
364330
C
@@ -375,7 +341,7 @@ true
375341
false
376342
()
377343

378-
21
344+
19
379345
SimpleMethods.scala
380346
covtest
381347
C
@@ -392,7 +358,7 @@ true
392358
false
393359

394360

395-
22
361+
20
396362
SimpleMethods.scala
397363
covtest
398364
C
@@ -409,7 +375,7 @@ false
409375
false
410376
def partialCond
411377

412-
23
378+
21
413379
SimpleMethods.scala
414380
covtest
415381
C
@@ -426,7 +392,7 @@ false
426392
false
427393
new {}
428394

429-
24
395+
22
430396
SimpleMethods.scala
431397
covtest
432398
C
@@ -443,7 +409,7 @@ false
443409
false
444410
def new1
445411

446-
25
412+
23
447413
SimpleMethods.scala
448414
covtest
449415
C
@@ -460,7 +426,7 @@ false
460426
false
461427
()
462428

463-
26
429+
24
464430
SimpleMethods.scala
465431
covtest
466432
C
@@ -477,7 +443,7 @@ true
477443
false
478444
()
479445

480-
27
446+
25
481447
SimpleMethods.scala
482448
covtest
483449
C
@@ -494,7 +460,7 @@ false
494460
false
495461
1
496462

497-
28
463+
26
498464
SimpleMethods.scala
499465
covtest
500466
C
@@ -511,7 +477,7 @@ true
511477
false
512478
=> 1
513479

514-
29
480+
27
515481
SimpleMethods.scala
516482
covtest
517483
C

0 commit comments

Comments
 (0)