Skip to content

Commit 570aa50

Browse files
TheElectronWillsmarter
authored andcommitted
Fix coverage instrumentation for curried and context functions
1 parent a8a6439 commit 570aa50

File tree

7 files changed

+572
-73
lines changed

7 files changed

+572
-73
lines changed

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

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,18 @@ import java.io.File
55
import java.util.concurrent.atomic.AtomicInteger
66

77
import collection.mutable
8-
import core.Flags.JavaDefined
8+
import core.Flags.*
99
import core.Contexts.{Context, ctx, inContext}
1010
import core.DenotTransformers.IdentityDenotTransformer
1111
import core.Symbols.{defn, Symbol}
1212
import core.Decorators.{toTermName, i}
1313
import core.Constants.Constant
14+
import core.NameOps.isContextFunction
15+
import core.Types.*
1416
import typer.LiftCoverage
1517
import util.{SourcePosition, Property}
1618
import util.Spans.Span
1719
import coverage.*
18-
import dotty.tools.dotc.util.SourceFile
1920

2021
/** Implements code coverage by inserting calls to scala.runtime.Invoker
2122
* ("instruments" the source code).
@@ -98,20 +99,26 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
9899
// a.f(args)
99100
case tree @ Apply(fun: Select, args) =>
100101
// don't transform the first Select, but do transform `a.b` in `a.b.f(args)`
101-
val transformedFun = cpy.Select(fun)(transform(fun.qualifier), fun.name)
102-
if needsLift(tree) then
103-
val transformed = cpy.Apply(tree)(transformedFun, args) // args will be transformed in instrumentLifted
104-
instrumentLifted(transformed)(using ignoreLiteralsContext)
102+
if canInstrumentApply(tree) then
103+
val transformedFun = cpy.Select(fun)(transform(fun.qualifier), fun.name)
104+
if needsLift(tree) then
105+
val transformed = cpy.Apply(tree)(transformedFun, args) // args will be transformed in instrumentLifted
106+
instrumentLifted(transformed)(using ignoreLiteralsContext)
107+
else
108+
val transformed = cpy.Apply(tree)(transformedFun, transform(args)(using ignoreLiteralsContext))
109+
instrument(transformed)(using ignoreLiteralsContext)
105110
else
106-
val transformed = cpy.Apply(tree)(transformedFun, transform(args)(using ignoreLiteralsContext))
107-
instrument(transformed)(using ignoreLiteralsContext)
111+
tree
108112

109113
// f(args)
110114
case tree: Apply =>
111-
if needsLift(tree) then
112-
instrumentLifted(tree)(using ignoreLiteralsContext) // see comment about Literals
115+
if canInstrumentApply(tree) then
116+
if needsLift(tree) then
117+
instrumentLifted(tree)(using ignoreLiteralsContext) // see comment about Literals
118+
else
119+
instrument(super.transform(tree)(using ignoreLiteralsContext))
113120
else
114-
instrument(super.transform(tree)(using ignoreLiteralsContext))
121+
tree
115122

116123
// (f(x))[args]
117124
case TypeApply(fun: Apply, args) =>
@@ -226,18 +233,49 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
226233
* should not be changed to {val $x = f(); T($x)}(1) but to {val $x = f(); val $y = 1; T($x)($y)}
227234
*/
228235
def needsLift(tree: Apply)(using Context): Boolean =
229-
// We don't want to lift a || getB(), to avoid calling getB if a is true.
230-
// Same idea with a && getB(): if a is false, getB shouldn't be called.
231236
def isBooleanOperator(fun: Tree) =
232-
fun.symbol.exists &&
233-
fun.symbol.isInstanceOf[Symbol] &&
234-
fun.symbol == defn.Boolean_&& || fun.symbol == defn.Boolean_||
237+
// We don't want to lift a || getB(), to avoid calling getB if a is true.
238+
// Same idea with a && getB(): if a is false, getB shouldn't be called.
239+
val sym = fun.symbol
240+
sym.exists &&
241+
sym == defn.Boolean_&& || sym == defn.Boolean_||
242+
243+
def isContextual(fun: Apply): Boolean =
244+
val args = fun.args
245+
args.nonEmpty && args.head.symbol.isAllOf(Given | Implicit)
235246

236247
val fun = tree.fun
248+
val nestedApplyNeedsLift = fun match
249+
case a: Apply => needsLift(a)
250+
case _ => false
237251

238-
fun.isInstanceOf[Apply] || // nested apply
252+
nestedApplyNeedsLift ||
239253
!isBooleanOperator(fun) && !tree.args.isEmpty && !tree.args.forall(LiftCoverage.noLift)
240254

255+
def canInstrumentApply(tree: Apply)(using Context): Boolean =
256+
val tpe = tree.typeOpt
257+
tpe match
258+
case AppliedType(tycon: NamedType, _) =>
259+
/* If the last expression in a block is a context function, we'll try to
260+
summon its arguments at the current point, even if the expected type
261+
is a function application. Therefore, this is not valid:
262+
```
263+
def f = (t: Exception) ?=> (c: String) ?=> result
264+
265+
({
266+
invoked()
267+
f(using e)
268+
})(using s)
269+
```
270+
*/
271+
!tycon.name.isContextFunction
272+
case m: MethodType =>
273+
// def f(a: Ta)(b: Tb)
274+
// f(a)(b) cannot be rewritten to {invoked();f(a)}(b)
275+
false
276+
case _ =>
277+
true
278+
241279
object InstrumentCoverage:
242280
val name: String = "instrumentCoverage"
243281
val description: String = "instrument code for coverage cheking"
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package covtest
2+
3+
class OnError(op: Exception => Any):
4+
def onError(fallback: Any): Any = fallback
5+
6+
class Imperative:
7+
8+
def readName2 = (t: Exception) ?=> (c: String) ?=> {
9+
"name"
10+
}
11+
12+
def readPerson(s: String) =
13+
val e: Exception = null
14+
OnError((e) => readName2(using e)(using s)).onError(None)
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
# Coverage data, format version: 3.0
2+
# Statement data:
3+
# - id
4+
# - source path
5+
# - package name
6+
# - class name
7+
# - class type (Class, Object or Trait)
8+
# - full class name
9+
# - method name
10+
# - start offset
11+
# - end offset
12+
# - line number
13+
# - symbol name
14+
# - tree name
15+
# - is branch
16+
# - invocations count
17+
# - is ignored
18+
# - description (can be multi-line)
19+
# ' ' sign
20+
# ------------------------------------------
21+
1
22+
expect/ContextFunctions.scala
23+
covtest
24+
Imperative
25+
Class
26+
covtest.Imperative
27+
$anonfun
28+
178
29+
184
30+
8
31+
<none>
32+
Literal
33+
false
34+
0
35+
false
36+
"name"
37+
38+
2
39+
expect/ContextFunctions.scala
40+
covtest
41+
Imperative
42+
Class
43+
covtest.Imperative
44+
readPerson
45+
243
46+
247
47+
12
48+
<none>
49+
Literal
50+
false
51+
0
52+
false
53+
null
54+
55+
3
56+
expect/ContextFunctions.scala
57+
covtest
58+
Imperative
59+
Class
60+
covtest.Imperative
61+
$anonfun
62+
267
63+
294
64+
13
65+
apply
66+
Apply
67+
false
68+
0
69+
false
70+
readName2(using e)(using s)
71+
72+
4
73+
expect/ContextFunctions.scala
74+
covtest
75+
Imperative
76+
Class
77+
covtest.Imperative
78+
readPerson
79+
252
80+
295
81+
13
82+
<init>
83+
Apply
84+
false
85+
0
86+
false
87+
OnError((e) => readName2(using e)(using s))
88+
89+
5
90+
expect/ContextFunctions.scala
91+
covtest
92+
Imperative
93+
Class
94+
covtest.Imperative
95+
$anonfun
96+
267
97+
294
98+
13
99+
invoked
100+
Apply
101+
false
102+
0
103+
false
104+
readName2(using e)(using s)
105+
106+
6
107+
expect/ContextFunctions.scala
108+
covtest
109+
Imperative
110+
Class
111+
covtest.Imperative
112+
$anonfun
113+
267
114+
294
115+
13
116+
apply
117+
Apply
118+
false
119+
0
120+
false
121+
readName2(using e)(using s)
122+
123+
7
124+
expect/ContextFunctions.scala
125+
covtest
126+
Imperative
127+
Class
128+
covtest.Imperative
129+
readPerson
130+
252
131+
295
132+
13
133+
invoked
134+
Apply
135+
false
136+
0
137+
false
138+
OnError((e) => readName2(using e)(using s))
139+
140+
8
141+
expect/ContextFunctions.scala
142+
covtest
143+
Imperative
144+
Class
145+
covtest.Imperative
146+
readPerson
147+
252
148+
295
149+
13
150+
<init>
151+
Apply
152+
false
153+
0
154+
false
155+
OnError((e) => readName2(using e)(using s))
156+
157+
9
158+
expect/ContextFunctions.scala
159+
covtest
160+
Imperative
161+
Class
162+
covtest.Imperative
163+
readPerson
164+
252
165+
309
166+
13
167+
onError
168+
Apply
169+
false
170+
0
171+
false
172+
OnError((e) => readName2(using e)(using s)).onError(None)
173+

tests/coverage/expect/Currying.scala

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package covtest
2+
3+
class ContextFunctions2:
4+
def f1(a: Int)(b: Int)(c: Int) = a+b+c
5+
def f2 = (a: Int) => (b: Int) => (c: Int) =>
6+
a+b+c
7+
8+
def g1 = (a: Int) ?=> (b: Int) ?=> (c: Int) ?=>
9+
a+b+c
10+
11+
def g2(using a: Int)(using b: Int)(using c: Int) = a+b+c
12+
13+
f1(0)(1)(2)
14+
f2(0)(1)(2)
15+
g1(using 0)(using 1)(using 2)
16+
g2(using 0)(using 1)(using 2)

0 commit comments

Comments
 (0)