Skip to content

Commit

Permalink
Make coverage instrumentation more robust (#16235)
Browse files Browse the repository at this point in the history
*As with the last coverage PR, don't worry: most of the changes are
"expect" files for tests. On top of that, many files have only changed
by the order in which the statements are recorded, but this order
doesn't matter.*

Small changes which, together, make the instrumentation more robust and
fix many bugs:
1. Address comments in #15739 by introducing a type `InstrumentedParts`.
The initial problem was that `TypeApply` cannot be instrumented in a
straightforward way: `expr[T]` cannot be turned into `{invoked(...);
expr}[T]` but must be `{invoked(...); expr[T]}`. To do this, we first
try to instrument `expr` and then, if it was successfully instrumented,
we move the call to `invoked(...)` to the right place. This gives us the
following code in `transform`:
```scala
case TypeApply(fun, args) =>
  val InstrumentedParts(pre, coverageCall, expr) = tryInstrument(fun) // may generate a call to invoked(...), but it's not part of the resulting tree yet
  if coverageCall.isEmpty then
    tree
  else
    Block(
      pre :+ coverageCall, // put the call at the right place (pre contains lifted definitions, if any)
      cpy.TypeApply(tree)(expr, args)
  )
```
2. Exclude more trees from instrumentation, like `Erased` trees and
calls to the parents' constructor in `Template#parents`.
3. Escape special characters in `Serializer`.
4. Reposition `Inlined` trees to the current source in order to avoid
referencing an unreachable compilation unit. This might be the most
controversial change because I've called `Inlines.dropInlined` 👀.
Any suggestion is welcome!
  • Loading branch information
smarter authored Oct 24, 2022
2 parents 3381850 + 02fd9de commit f3fec58
Show file tree
Hide file tree
Showing 44 changed files with 2,205 additions and 1,078 deletions.
1 change: 0 additions & 1 deletion compiler/src/dotty/tools/dotc/coverage/Coverage.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ class Coverage:

/** A statement that can be invoked, and thus counted as "covered" by code coverage tools. */
case class Statement(
source: String,
location: Location,
id: Int,
start: Int,
Expand Down
17 changes: 10 additions & 7 deletions compiler/src/dotty/tools/dotc/coverage/Location.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,35 @@ import ast.tpd._
import dotty.tools.dotc.core.Contexts.Context
import dotty.tools.dotc.core.Flags.*
import java.nio.file.Path
import dotty.tools.dotc.util.SourceFile

/** Information about the location of a coverable piece of code.
*
* @param packageName name of the enclosing package
* @param className name of the closest enclosing class
* @param fullClassName fully qualified name of the closest enclosing class
* @param classType "type" of the closest enclosing class: Class, Trait or Object
* @param method name of the closest enclosing method
* @param method name of the closest enclosing method
* @param sourcePath absolute path of the source file
*/
final case class Location(
packageName: String,
className: String,
fullClassName: String,
classType: String,
method: String,
methodName: String,
sourcePath: Path
)

object Location:
/** Extracts the location info of a Tree. */
def apply(tree: Tree)(using ctx: Context): Location =
def apply(tree: Tree, source: SourceFile)(using ctx: Context): Location =

val enclosingClass = ctx.owner.denot.enclosingClass
val packageName = ctx.owner.denot.enclosingPackageClass.name.toSimpleName.toString
val ownerDenot = ctx.owner.denot
val enclosingClass = ownerDenot.enclosingClass
val packageName = ownerDenot.enclosingPackageClass.fullName.toSimpleName.toString
val className = enclosingClass.name.toSimpleName.toString
val methodName = ownerDenot.enclosingMethod.name.toSimpleName.toString

val classType: String =
if enclosingClass.is(Trait) then "Trait"
Expand All @@ -42,6 +45,6 @@ object Location:
className,
s"$packageName.$className",
classType,
ctx.owner.denot.enclosingMethod.name.toSimpleName.toString(),
ctx.source.file.absolute.jpath
methodName,
source.file.absolute.jpath
)
39 changes: 32 additions & 7 deletions compiler/src/dotty/tools/dotc/coverage/Serializer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package coverage
import java.nio.file.{Path, Paths, Files}
import java.io.Writer
import scala.language.unsafeNulls
import scala.collection.mutable.StringBuilder

/**
* Serializes scoverage data.
Expand Down Expand Up @@ -62,25 +63,49 @@ object Serializer:
def writeStatement(stmt: Statement, writer: Writer): Unit =
// Note: we write 0 for the count because we have not measured the actual coverage at this point
writer.write(s"""${stmt.id}
|${getRelativePath(stmt.location.sourcePath)}
|${stmt.location.packageName}
|${stmt.location.className}
|${getRelativePath(stmt.location.sourcePath).escaped}
|${stmt.location.packageName.escaped}
|${stmt.location.className.escaped}
|${stmt.location.classType}
|${stmt.location.fullClassName}
|${stmt.location.method}
|${stmt.location.fullClassName.escaped}
|${stmt.location.methodName.escaped}
|${stmt.start}
|${stmt.end}
|${stmt.line}
|${stmt.symbolName}
|${stmt.symbolName.escaped}
|${stmt.treeName}
|${stmt.branch}
|0
|${stmt.ignored}
|${stmt.desc}
|${stmt.desc.escaped}
|\f
|""".stripMargin)

writeHeader(writer)
coverage.statements.toSeq
.sortBy(_.id)
.foreach(stmt => writeStatement(stmt, writer))

/** Makes a String suitable for output in the coverage statement data as a single line.
* Escaped characters: '\\' (backslash), '\n', '\r', '\f'
*/
extension (str: String) def escaped: String =
val builder = StringBuilder(str.length)
var i = 0
while
i < str.length
do
str.charAt(i) match
case '\\' =>
builder ++= "\\\\"
case '\n' =>
builder ++= "\\n"
case '\r' =>
builder ++= "\\r"
case '\f' =>
builder ++= "\\f"
case c =>
builder += c
i += 1
end while
builder.result()
Loading

0 comments on commit f3fec58

Please sign in to comment.