Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions compiler/src/dotty/tools/dotc/transform/InstrumentCoverage.scala
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:

result

private def treeSize(tree: Tree)(using Context): Int =
var count = 0
tree.foreachSubTree(_ => count += 1)
count

private def isClassIncluded(sym: Symbol)(using Context): Boolean =
val fqn = sym.fullName.toText(ctx.printerFn(ctx)).show
coverageExcludeClasslikePatterns.isEmpty || !coverageExcludeClasslikePatterns.exists(
Expand All @@ -147,6 +152,12 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
private class CoverageTransformer(outputPath: String) extends Transformer:
private val ConstOutputPath = Constant(outputPath)

private def warnSkippedLargeTreeCoverage(tree: MemberDef, subject: String, nodeCount: Int)(using Context): Unit =
report.warning(
s"Skipping coverage instrumentation for large $subject ($nodeCount tree nodes exceeds threshold ${InstrumentCoverage.MaxInstrumentableTreeNodes}); compilation will continue but no coverage data will be recorded for it.",
tree.srcPos
)

/** Generates the tree for:
* ```
* Invoker.invoked(id, DIR)
Expand Down Expand Up @@ -330,6 +341,10 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
case tree if tree.isEmpty || tree.isType => tree // empty Thicket, Ident (referring to a type), TypeTree, ...
case tree if !tree.span.exists || tree.span.isZeroExtent => tree // no meaningful position

case tree: ValDef if !tree.rhs.isEmpty && treeSize(tree.rhs) > InstrumentCoverage.MaxInstrumentableTreeNodes =>
warnSkippedLargeTreeCoverage(tree, s"value initializer `${tree.name.show}`", treeSize(tree.rhs))
tree

case tree: Literal =>
val rest = tryInstrument(tree).toTree
rest
Expand Down Expand Up @@ -461,6 +476,9 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
// Inline and erased definitions will not be in the generated code and therefore do not need to be instrumented.
// (Note that a retained inline method will have a `$retained` variant that will be instrumented.)
tree
else if !tree.rhs.isEmpty && treeSize(tree.rhs) > InstrumentCoverage.MaxInstrumentableTreeNodes then
warnSkippedLargeTreeCoverage(tree, s"method body `${tree.name.show}`", treeSize(tree.rhs))
tree
else
// Only transform the params (for the default values) and the rhs, not the name and tpt.
val transformedParamss = transformParamss(tree.paramss)
Expand Down Expand Up @@ -679,6 +697,12 @@ object InstrumentCoverage:
val name: String = "instrumentCoverage"
val description: String = "instrument code for coverage checking"
val ExcludeMethodFlags: FlagSet = Artifact | Erased

/** Maximum number of tree nodes in a method body for coverage instrumentation.
* Beyond this threshold, the instrumented bytecode risks exceeding the JVM's 64KB
* method size limit. The per-statement overhead of `Invoker.invoked()` is ~15 bytes,
* so roughly half of the tree nodes in a large body would each add that overhead. */
val MaxInstrumentableTreeNodes: Int = 3000
val scoverageLocalOn: Regex = """^\s*//\s*\$COVERAGE-ON\$""".r
val scoverageLocalOff: Regex = """^\s*//\s*\$COVERAGE-OFF\$""".r

Expand Down
5 changes: 0 additions & 5 deletions compiler/test/dotc/scoverage-ignore.excludelist
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ applied_constructor_types.scala
backwardCompat-3.0
backwardCompat-3.1
backwardsCompat-implicitParens
bridges.scala
capt1.scala
capture.scala
catch-sub-cases.scala
Expand Down Expand Up @@ -46,7 +45,6 @@ i19637.scala
i19955a.scala
i19955b.scala
i20053b.scala
i20521.scala
i2112.scala
i21313.scala
i2146.scala
Expand All @@ -64,7 +62,6 @@ i25000.scala
i25000b.scala
i3598.scala
i5039.scala
i7034.scala
i8623.scala
i8900a3.scala
i8955.scala
Expand All @@ -73,7 +70,6 @@ i9228.scala
i9880.scala
infersingle.scala
interop-unsound-src
large2.scala
match-single-sub-case.scala
match-sub-cases.scala
match-sub-sub-cases.scala
Expand All @@ -88,7 +84,6 @@ quote-function-applied-to.scala
skolems2.scala
spurious-overload.scala
sub-cases-exhaustivity.scala
t10594.scala
tailrec.scala
traitParams.scala
i25460.scala
Expand Down
16 changes: 16 additions & 0 deletions compiler/test/dotty/tools/dotc/coverage/CoverageTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ class CoverageTests:
def checkInstrumentedRuns(): Unit =
checkCoverageIn(rootSrc.resolve("run"), true)

@Test
def checkCoverageWarnings(): Unit =
checkCoverageWarningsIn(rootSrc.resolve("warn"))

def checkCoverageIn(dir: Path, run: Boolean)(using TestGroup): Unit =
/** Converts \\ (escaped \) to / on windows, to make the tests pass without changing the serialization. */
def fixWindowsPaths(lines: Buffer[String]): Buffer[String] =
Expand Down Expand Up @@ -113,6 +117,18 @@ class CoverageTests:
test.checkCompile()
target

def checkCoverageWarningsIn(dir: Path)(using TestGroup): Unit =
def runOnFile(p: Path): Boolean =
scalaFile.matches(p)
&& (Properties.testsFilter.isEmpty || Properties.testsFilter.exists(p.toString.contains))

Files.walk(dir, 1).filter(runOnFile).forEach { path =>
val target = Files.createTempDirectory("coverage-warning")
val options = defaultOptions.and("-Ycheck:instrumentCoverage", "-coverage-out", target.toString, "-sourceroot", rootSrc.toString)
val relativePath = Paths.get(userDir).relativize(path).toString
compileFile(relativePath, options).checkWarnings()
}

private def findMeasurementFile(targetDir: Path): Path = {
val allFilesInTarget = Files.list(targetDir).collect(Collectors.toList).asScala
allFilesInTarget.filter(_.getFileName.toString.startsWith("scoverage.measurements.")).headOption.getOrElse(
Expand Down
Loading
Loading