diff --git a/compiler/src/dotty/tools/dotc/transform/InstrumentCoverage.scala b/compiler/src/dotty/tools/dotc/transform/InstrumentCoverage.scala index fd859158b566..1c6289ae6d23 100644 --- a/compiler/src/dotty/tools/dotc/transform/InstrumentCoverage.scala +++ b/compiler/src/dotty/tools/dotc/transform/InstrumentCoverage.scala @@ -6,6 +6,7 @@ import java.nio.file.{Files, Path} import ast.tpd import ast.tpd.* +import ast.desugar.TrailingForMap import collection.mutable import core.Comments.Comment import core.Flags.* @@ -32,6 +33,7 @@ object LiftCoverage extends LiftImpure: // Property indicating whether we're currently lifting the arguments of an application private val LiftingArgs = new Property.Key[Boolean] + private val SelectedReceiverApply = Property.StickyKey[tpd.Apply]() val CoverageLiftedTemp = Property.StickyKey[Unit]() private inline def liftingArgs(using Context): Boolean = @@ -68,6 +70,12 @@ object LiftCoverage extends LiftImpure: def isCoverageLiftedTemp(sym: Symbol)(using Context): Boolean = sym.defTree.hasAttachment(CoverageLiftedTemp) + def selectedReceiverApply(tree: tpd.Tree)(using Context): Option[tpd.Apply] = + tree.getAttachment(SelectedReceiverApply) + + def markSelectedReceiverApply(tree: tpd.Apply)(using Context): Unit = + tree.putAttachment(SelectedReceiverApply, tree) + override protected def onLiftedDef(tree: tpd.Tree)(using Context): Unit = tree.putAttachment(CoverageLiftedTemp, ()) @@ -93,10 +101,33 @@ object LiftCoverage extends LiftImpure: case _ if valueType.existsPart(_.typeSymbol == defn.TypeBox_CAP) => valueType case _ => super.liftedExprType(expr) + private def markSelectedReceiverDef( + defs: mutable.ListBuffer[tpd.Tree], + from: Int, + selectedReceiver: tpd.Apply + )(using Context): Unit = + if defs.length > from then + defs.last match + case stat: tpd.ValDef => stat.putAttachment(SelectedReceiverApply, selectedReceiver) + case _ => () + def liftForCoverage(defs: mutable.ListBuffer[tpd.Tree], tree: tpd.Apply)(using Context) = - val liftedFun = liftApp(defs, tree.fun) - val liftedArgs = liftArgs(defs, tree.fun.tpe, tree.args)(using liftingArgsContext) - tpd.cpy.Apply(tree)(liftedFun, liftedArgs) + def recur(tree: tpd.Apply): tpd.Tree = + val liftedFun = tree.fun match + case sel @ tpd.Select(app: tpd.Apply, name) if selectedReceiverApply(app).nonEmpty => + val selectedReceiver = tpd.cpy.Select(sel)(recur(app), name) + val defsBeforeReceiver = defs.length + val liftedReceiver = liftApp(defs, selectedReceiver) + markSelectedReceiverDef(defs, defsBeforeReceiver, app) + liftedReceiver + case _ => + liftApp(defs, tree.fun) + val liftedArgs = liftArgs(defs, tree.fun.tpe, tree.args)(using liftingArgsContext) + tpd.cpy.Apply(tree)(liftedFun, liftedArgs) + end recur + + recur(tree) + end liftForCoverage /** Implements code coverage by inserting calls to scala.runtime.coverage.Invoker * ("instruments" the source code). @@ -324,6 +355,15 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer: private def allConstArgs(args: List[Tree]) = args.forall(arg => arg.isInstanceOf[Literal] || arg.isInstanceOf[Ident]) + + private def withSelectedReceiverProbes(stats: List[Tree])(using Context): List[Tree] = + stats.flatMap: stat => + LiftCoverage.selectedReceiverApply(stat) match + case Some(app) => + createInvokeCall(app, app.sourcePos) :: stat :: Nil + case _ => + stat :: Nil + /** * Tries to instrument an `Apply`. * These "tryInstrument" methods are useful to tweak the generation of coverage instrumentation, @@ -345,14 +385,17 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer: if tree.fun.symbol eq defn.throwMethod then tree else cpy.Apply(tree)(transformInnerApply(tree.fun), transformApplyArgs(tree.args, erasedParamStatuses(tree))) - if needsLift(tree) then + if needsLift(app) then // Lifts the arguments. Note that if only one argument needs to be lifted, we lift them all. // Also, tree.fun can be lifted too. // See LiftCoverage for the internal working of this lifting. val liftedDefs = mutable.ListBuffer[Tree]() val liftedApp = LiftCoverage.liftForCoverage(liftedDefs, app) + val prefix = + if tree.hasAttachment(TrailingForMap) then liftedDefs.toList + else withSelectedReceiverProbes(liftedDefs.toList) - InstrumentedParts(liftedDefs.toList, coverageCall, liftedApp) + InstrumentedParts(prefix, coverageCall, liftedApp) else // Instrument without lifting InstrumentedParts.singleExpr(coverageCall, app) @@ -360,6 +403,7 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer: // Transform recursively but don't instrument the tree itself val transformed = cpy.Apply(tree)(transformInnerApply(tree.fun), transformApplyArgs(tree.args, erasedParamStatuses(tree))) InstrumentedParts.notCovered(transformed) + end tryInstrument private def tryInstrument(tree: Ident)(using Context): InstrumentedParts = val sym = tree.symbol @@ -728,6 +772,32 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer: * should not be changed to {val $x = f(); T($x)}(1) but to {val $x = f(); val $y = 1; T($x)($y)} */ private def needsLift(tree: Apply)(using Context): Boolean = + def hasSelectedApply(fun: Tree): Boolean = fun match + case Select(app: Apply, _) => + val nestedNeedsProbe = hasSelectedApply(app.fun) + val needsProbe = selectedReceiverNeedsProbe(app) + if needsProbe then LiftCoverage.markSelectedReceiverApply(app) + nestedNeedsProbe || needsProbe + case TypeApply(fn, _) => hasSelectedApply(fn) + case _ => false + end hasSelectedApply + + def applicationEvaluationNeedsLift(tree: Apply): Boolean = + val fun = tree.fun + val nestedApplyNeedsLift = fun match + case a: Apply => applicationEvaluationNeedsLift(a) + case _ => false + + nestedApplyNeedsLift || + !isUnliftableFun(fun) && !tree.args.isEmpty && !tree.args.forall(LiftCoverage.noLift) + end applicationEvaluationNeedsLift + + def selectedReceiverNeedsProbe(tree: Apply): Boolean = + !LiftCoverage.isUnsafeAssumeSeparate(tree) + && canInstrumentApply(tree) + && applicationEvaluationNeedsLift(tree) + end selectedReceiverNeedsProbe + def isShortCircuitedOp(sym: Symbol) = sym == defn.Boolean_&& || sym == defn.Boolean_|| @@ -755,7 +825,12 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer: case _ => false nestedApplyNeedsLift || - !isUnliftableFun(fun) && !tree.args.isEmpty && !tree.args.forall(LiftCoverage.noLift) + !isUnliftableFun(fun) + && ( + !tree.hasAttachment(TrailingForMap) && hasSelectedApply(fun) + || !tree.args.isEmpty && !tree.args.forall(LiftCoverage.noLift) + ) + end needsLift private def isContextFunctionApply(fun: Tree)(using Context): Boolean = fun match diff --git a/tests/coverage/pos/DefaultArgs.scoverage.check b/tests/coverage/pos/DefaultArgs.scoverage.check index c75f1854a0f8..118d5f8263d9 100644 --- a/tests/coverage/pos/DefaultArgs.scoverage.check +++ b/tests/coverage/pos/DefaultArgs.scoverage.check @@ -399,6 +399,40 @@ DefaultArgs Object covtest.DefaultArgs staticCaller +706 +720 +28 +staticMethod +Apply +false +0 +false +staticMethod() + +23 +DefaultArgs.scala +covtest +DefaultArgs +Object +covtest.DefaultArgs +staticCaller +706 +738 +28 ++ +Apply +false +0 +false +staticMethod() + staticMethod(5) + +24 +DefaultArgs.scala +covtest +DefaultArgs +Object +covtest.DefaultArgs +staticCaller 676 692 27 diff --git a/tests/coverage/pos/i21695/test.scoverage.check b/tests/coverage/pos/i21695/test.scoverage.check index d9ee3a172e3f..dd9cbdf279f6 100644 --- a/tests/coverage/pos/i21695/test.scoverage.check +++ b/tests/coverage/pos/i21695/test.scoverage.check @@ -59,6 +59,23 @@ A Trait example.A create +94 +111 +8 +addService +Apply +false +0 +false +x1.addService(x2) + +3 +i21695/A.scala +example +A +Trait +example.A +create 69 79 7 diff --git a/tests/coverage/run/chained-apply/test.check b/tests/coverage/run/chained-apply/test.check new file mode 100644 index 000000000000..74f682a3b1eb --- /dev/null +++ b/tests/coverage/run/chained-apply/test.check @@ -0,0 +1,13 @@ +foo +addMode +foo +addMode +bar +foo +addMode +argBar +memberAddMode +bar +depFoo +depAddMode +dep.bar diff --git a/tests/coverage/run/chained-apply/test.measurement.check b/tests/coverage/run/chained-apply/test.measurement.check new file mode 100644 index 000000000000..23cf283535f3 --- /dev/null +++ b/tests/coverage/run/chained-apply/test.measurement.check @@ -0,0 +1,33 @@ +32 +20 +8 +6 +7 +19 +13 +12 +22 +23 +21 +3 +2 +25 +27 +26 +11 +9 +10 +28 +1 +0 +24 +30 +16 +14 +15 +31 +18 +17 +29 +5 +4 diff --git a/tests/coverage/run/chained-apply/test.scala b/tests/coverage/run/chained-apply/test.scala new file mode 100644 index 000000000000..33fb86decf4e --- /dev/null +++ b/tests/coverage/run/chained-apply/test.scala @@ -0,0 +1,40 @@ +class C: + def addMode(c: C): C = + println("memberAddMode") + this + + def bar(): C = + println("bar") + this + +class D: + def bar(): this.type = + println("dep.bar") + this + +def foo(): C = + println("foo") + C() + +def bar(): C = + println("argBar") + C() + +def addMode(c: C): C = + println("addMode") + c + +def depFoo(): D = + println("depFoo") + D() + +def depAddMode(d: D): d.type = + println("depAddMode") + d + +@main +def Test: Unit = + addMode(foo()) + addMode(foo()).bar() + addMode(foo()).addMode(bar()).bar() + depAddMode(depFoo()).bar() diff --git a/tests/coverage/run/chained-apply/test.scoverage.check b/tests/coverage/run/chained-apply/test.scoverage.check new file mode 100644 index 000000000000..76abfd93cdf9 --- /dev/null +++ b/tests/coverage/run/chained-apply/test.scoverage.check @@ -0,0 +1,581 @@ +# Coverage data, format version: 3.0 +# Statement data: +# - id +# - source path +# - package name +# - class name +# - class type (Class, Object or Trait) +# - full class name +# - method name +# - start offset +# - end offset +# - line number +# - symbol name +# - tree name +# - is branch +# - invocations count +# - is ignored +# - description (can be multi-line) +# ' ' sign +# ------------------------------------------ +0 +chained-apply/test.scala + +C +Class +.C +addMode +38 +62 +3 +println +Apply +false +0 +false +println("memberAddMode") + +1 +chained-apply/test.scala + +C +Class +.C +addMode +11 +22 +2 +addMode +DefDef +false +0 +false +def addMode + +2 +chained-apply/test.scala + +C +Class +.C +bar +94 +108 +7 +println +Apply +false +0 +false +println("bar") + +3 +chained-apply/test.scala + +C +Class +.C +bar +75 +82 +6 +bar +DefDef +false +0 +false +def bar + +4 +chained-apply/test.scala + +D +Class +.D +bar +157 +175 +12 +println +Apply +false +0 +false +println("dep.bar") + +5 +chained-apply/test.scala + +D +Class +.D +bar +130 +137 +11 +bar +DefDef +false +0 +false +def bar + +6 +chained-apply/test.scala + +test$package +Object +.test$package +foo +203 +217 +16 +println +Apply +false +0 +false +println("foo") + +7 +chained-apply/test.scala + +test$package +Object +.test$package +foo +220 +223 +17 + +Apply +false +0 +false +C() + +8 +chained-apply/test.scala + +test$package +Object +.test$package +foo +186 +193 +15 +foo +DefDef +false +0 +false +def foo + +9 +chained-apply/test.scala + +test$package +Object +.test$package +bar +242 +259 +20 +println +Apply +false +0 +false +println("argBar") + +10 +chained-apply/test.scala + +test$package +Object +.test$package +bar +262 +265 +21 + +Apply +false +0 +false +C() + +11 +chained-apply/test.scala + +test$package +Object +.test$package +bar +225 +232 +19 +bar +DefDef +false +0 +false +def bar + +12 +chained-apply/test.scala + +test$package +Object +.test$package +addMode +292 +310 +24 +println +Apply +false +0 +false +println("addMode") + +13 +chained-apply/test.scala + +test$package +Object +.test$package +addMode +267 +278 +23 +addMode +DefDef +false +0 +false +def addMode + +14 +chained-apply/test.scala + +test$package +Object +.test$package +depFoo +336 +353 +28 +println +Apply +false +0 +false +println("depFoo") + +15 +chained-apply/test.scala + +test$package +Object +.test$package +depFoo +356 +359 +29 + +Apply +false +0 +false +D() + +16 +chained-apply/test.scala + +test$package +Object +.test$package +depFoo +316 +326 +27 +depFoo +DefDef +false +0 +false +def depFoo + +17 +chained-apply/test.scala + +test$package +Object +.test$package +depAddMode +394 +415 +32 +println +Apply +false +0 +false +println("depAddMode") + +18 +chained-apply/test.scala + +test$package +Object +.test$package +depAddMode +361 +375 +31 +depAddMode +DefDef +false +0 +false +def depAddMode + +19 +chained-apply/test.scala + +test$package +Object +.test$package +Test +446 +460 +37 +addMode +Apply +false +0 +false +addMode(foo()) + +20 +chained-apply/test.scala + +test$package +Object +.test$package +Test +454 +459 +37 +foo +Apply +false +0 +false +foo() + +21 +chained-apply/test.scala + +test$package +Object +.test$package +Test +463 +483 +38 +bar +Apply +false +0 +false +addMode(foo()).bar() + +22 +chained-apply/test.scala + +test$package +Object +.test$package +Test +471 +476 +38 +foo +Apply +false +0 +false +foo() + +23 +chained-apply/test.scala + +test$package +Object +.test$package +Test +463 +477 +38 +addMode +Apply +false +0 +false +addMode(foo()) + +24 +chained-apply/test.scala + +test$package +Object +.test$package +Test +486 +521 +39 +bar +Apply +false +0 +false +addMode(foo()).addMode(bar()).bar() + +25 +chained-apply/test.scala + +test$package +Object +.test$package +Test +494 +499 +39 +foo +Apply +false +0 +false +foo() + +26 +chained-apply/test.scala + +test$package +Object +.test$package +Test +509 +514 +39 +bar +Apply +false +0 +false +bar() + +27 +chained-apply/test.scala + +test$package +Object +.test$package +Test +486 +500 +39 +addMode +Apply +false +0 +false +addMode(foo()) + +28 +chained-apply/test.scala + +test$package +Object +.test$package +Test +486 +515 +39 +addMode +Apply +false +0 +false +addMode(foo()).addMode(bar()) + +29 +chained-apply/test.scala + +test$package +Object +.test$package +Test +524 +550 +40 +bar +Apply +false +0 +false +depAddMode(depFoo()).bar() + +30 +chained-apply/test.scala + +test$package +Object +.test$package +Test +535 +543 +40 +depFoo +Apply +false +0 +false +depFoo() + +31 +chained-apply/test.scala + +test$package +Object +.test$package +Test +524 +544 +40 +depAddMode +Apply +false +0 +false +depAddMode(depFoo()) + +32 +chained-apply/test.scala + +test$package +Object +.test$package +Test +421 +435 +36 +Test +DefDef +false +0 +false +@main\ndef Test + diff --git a/tests/coverage/run/type-apply/test.measurement.check b/tests/coverage/run/type-apply/test.measurement.check index e199fee6b817..3ea5a577a3a2 100644 --- a/tests/coverage/run/type-apply/test.measurement.check +++ b/tests/coverage/run/type-apply/test.measurement.check @@ -1,7 +1,7 @@ 6 -1 2 3 4 +1 5 0 diff --git a/tests/pos-custom-args/captures/coverage-freshcontext-addmode.scala b/tests/pos-custom-args/captures/coverage-freshcontext-addmode.scala new file mode 100644 index 000000000000..9f2998559ee5 --- /dev/null +++ b/tests/pos-custom-args/captures/coverage-freshcontext-addmode.scala @@ -0,0 +1,3 @@ +class C { def setA(a: Int): this.type = this } +def addMode(c: C): c.type = c +val x = addMode(identity(new C()).setA(???)).setA(???)