Skip to content

Commit

Permalink
Merged in retronym/jacoco4sbt/topic/scala-awareness (pull request #4)
Browse files Browse the repository at this point in the history
Filter Scala-related noise from JaCoCo results
  • Loading branch information
Joachim Hofer committed Oct 19, 2013
2 parents 2e253ab + 93b4bc3 commit f914fa3
Show file tree
Hide file tree
Showing 9 changed files with 334 additions and 2 deletions.
8 changes: 8 additions & 0 deletions src/main/scala/de/johoop/jacoco4sbt/FormattedReport.scala
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ case class HTMLReport(encoding: String = "utf-8") extends FormattedReport {
}
}

case class ScalaHTMLReport(encoding: String = "utf-8") extends FormattedReport {
def visitor(directory: File) = {
val formatter = new ScalaHtmlFormatter
formatter setOutputEncoding encoding
formatter createVisitor new FileMultiReportOutput(new File(directory, "html"))
}
}

case class XMLReport(encoding: String = "utf-8") extends FormattedReport {
def visitor(directory: File) = {
val formatter = new XMLFormatter
Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/de/johoop/jacoco4sbt/JacocoPlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ object JacocoPlugin extends Plugin {
private object JacocoDefaults extends Reporting with Keys {
val settings = Seq(
outputDirectory := crossTarget.value / "jacoco",
reportFormats := Seq(HTMLReport()),
reportFormats := Seq(ScalaHTMLReport()),
reportTitle := "Jacoco Coverage Report",
sourceTabWidth := 2,
sourceEncoding := "utf-8",
Expand Down
3 changes: 2 additions & 1 deletion src/main/scala/de/johoop/jacoco4sbt/Report.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import html.HTMLFormatter

import java.io.File
import java.io.FileInputStream
import de.johoop.jacoco4sbt.filter.FilteringAnalyzer

class Report(executionDataFile: File, classDirectories: Seq[File],
sourceDirectories: Seq[File], sourceEncoding: String, tabWidth: Int,
Expand Down Expand Up @@ -51,7 +52,7 @@ class Report(executionDataFile: File, classDirectories: Seq[File],

private def analyzeStructure(executionDataStore: ExecutionDataStore, sessionInfoStore: SessionInfoStore) = {
val coverageBuilder = new CoverageBuilder
val analyzer = new Analyzer(executionDataStore, coverageBuilder)
val analyzer = new FilteringAnalyzer(executionDataStore, coverageBuilder)

classDirectories foreach { analyzer analyzeAll _ }

Expand Down
60 changes: 60 additions & 0 deletions src/main/scala/de/johoop/jacoco4sbt/ScalaHtmlFormatter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package de.johoop.jacoco4sbt;

import org.jacoco.core.analysis.ICoverageNode;
import org.jacoco.report.html.HTMLFormatter;
import org.jacoco.report.internal.html.resources.Styles;
import org.jacoco.report.internal.html.table.BarColumn;
import org.jacoco.report.internal.html.table.CounterColumn;
import org.jacoco.report.internal.html.table.LabelColumn;
import org.jacoco.report.internal.html.table.Table;

/**
* Omits displaying instruction and branch coverage in the coverage tables, as Scala generates null checks which make these too noisy
*
* TODO: Find a way to remove them from the annotated source code reports, too.
*/
public class ScalaHtmlFormatter extends HTMLFormatter {
private Table table;

public ScalaHtmlFormatter() {
setLanguageNames(new ScalaLanguageNames());
}

public Table getTable() {
if (table == null) {
table = createTable();
}
return table;
}

private Table createTable() {
final Table t = new Table();
t.add("Element", null, new LabelColumn(), false);

// Just show line coverage in Scala projects.
// t.add("Missed Instructions", Styles.BAR, new BarColumn(ICoverageNode.CounterEntity.INSTRUCTION,
// locale), true);
// t.add("Cov.", Styles.CTR2,
// new PercentageColumn(ICoverageNode.CounterEntity.INSTRUCTION, locale), false);
// t.add("Missed Branches", Styles.BAR, new BarColumn(ICoverageNode.CounterEntity.BRANCH, locale),
// false);
// t.add("Cov.", Styles.CTR2, new PercentageColumn(ICoverageNode.CounterEntity.BRANCH, locale),
// false);
// addMissedTotalColumns(t, "Cxty", ICoverageNode.CounterEntity.COMPLEXITY);

t.add("Missed Lines", Styles.BAR, new BarColumn(ICoverageNode.CounterEntity.LINE, getLocale()), true);
t.add("Total Lines", Styles.CTR1, CounterColumn.newTotal(ICoverageNode.CounterEntity.LINE, getLocale()), false);

addMissedTotalColumns(t, "Methods", ICoverageNode.CounterEntity.METHOD);
addMissedTotalColumns(t, "Classes", ICoverageNode.CounterEntity.CLASS);
return t;
}

private void addMissedTotalColumns(final Table table, final String label,
final ICoverageNode.CounterEntity entity) {
table.add("Missed", Styles.CTR1,
CounterColumn.newMissed(entity, getLocale()), false);
table.add(label, Styles.CTR2, CounterColumn.newTotal(entity, getLocale()),
false);
}
}
52 changes: 52 additions & 0 deletions src/main/scala/de/johoop/jacoco4sbt/ScalaLanguageNames.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package de.johoop.jacoco4sbt

import org.jacoco.report.JavaNames
import scala.reflect.NameTransformer._
import de.johoop.jacoco4sbt.filter.ScalaForwarderDetector

class ScalaLanguageNames extends JavaNames {
override def getPackageName(vmname: String): String =
super.getPackageName(decode(vmname))

override def getClassName(vmname: String, vmsignature: String, vmsuperclass: String, vminterfaces: Array[String]): String = {
if (vmname.contains("anonfun$"))
vmname.split("""anonfun\$""").toList match {
case List(pre, post) =>
getClassName(cleanClassName(pre)) + " anonfun$" + post
case _ =>
getClassName(cleanClassName(vmname))
}
else if (vmname.contains("$anon$"))
vminterfaces.map(getClassName).mkString("new " + getClassName(vmsuperclass), " with ", "{ ... }")
else getClassName(cleanClassName(vmname))
}

override def getQualifiedClassName(vmname: String): String =
super.getQualifiedClassName(cleanClassName(vmname))

override def getMethodName(vmclassname: String, vmmethodname: String, vmdesc: String, vmsignature: String): String =
super.getMethodName(vmclassname, getMethodName(vmmethodname), vmdesc, vmsignature)

override def getQualifiedMethodName(vmclassname: String, vmmethodname: String, vmdesc: String, vmsignature: String): String =
super.getQualifiedMethodName(vmclassname, getMethodName(vmmethodname), vmdesc, vmsignature)

private def cleanClassName(name: String) = {
decode(name.stripSuffix("$class"))
}

private def getClassName(vmname: String) = {
val pos: Int = vmname.lastIndexOf('/')
val name: String = if (pos == -1) vmname else vmname.substring(pos + 1)
cleanClassName(
if (name.endsWith("$$")) name.dropRight(2).replace('$', '.') // ambiguous, we could be an inner class of the object or the class.
else if (name.endsWith("$")) name.dropRight(1).replace('$', '.') + " (object)"
else name.replace('$', '.')
)
}

private def getMethodName(vmname: String) = {
val pos: Int = vmname.lastIndexOf("$$")
val name: String = if (pos == -1) vmname else vmname.substring(pos + 2)
decode(name.stripSuffix(ScalaForwarderDetector.LazyComputeSuffix))
}
}
23 changes: 23 additions & 0 deletions src/main/scala/de/johoop/jacoco4sbt/filter/AccessorDetector.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package de.johoop.jacoco4sbt.filter

import org.objectweb.asm.tree._
import org.objectweb.asm.Opcodes
import scala.collection.JavaConverters._
import Opcodes._

/** Detects accessor methods that do nothing other than load and return a field */
object AccessorDetector {
def isAccessor(node: MethodNode): Boolean = {
(node.instructions.size() < 10) && {
val insn: List[AbstractInsnNode] = node.instructions.iterator().asInstanceOf[java.util.ListIterator[AbstractInsnNode]].asScala.toList
val filtered = insn.filter {
case _: LabelNode | _: LineNumberNode => false
case _ => true
}
filtered.map(_.getOpcode) match {
case ALOAD :: GETFIELD :: (IRETURN | LRETURN | FRETURN | DRETURN | ARETURN | RETURN) :: Nil => true
case _ => false
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package de.johoop.jacoco4sbt.filter

import scala.collection.mutable
import org.jacoco.core.internal.analysis.{MethodAnalyzer, StringPool, ClassAnalyzer}
import org.jacoco.core.internal.flow.{ClassProbesAdapter, MethodProbesVisitor}
import org.jacoco.core.internal.instr.InstrSupport
import org.objectweb.asm._
import org.jacoco.core.analysis.{Analyzer, ICoverageVisitor, IMethodCoverage}
import org.jacoco.core.internal.data.CRC64
import org.jacoco.core.data.ExecutionDataStore
import org.objectweb.asm.tree.{JumpInsnNode, MethodInsnNode, MethodNode, ClassNode}
import scala.collection.JavaConverters._

/**
* Filters coverage results from Scala synthetic methods:
* - trait forwarders
* - case class toString / equals / apply / unapply
*
* These are identified by the heuristic that they have the same line number as a constructor, or
* the same line as other one-line methods if we are in a module class.
*
* This filtering should really happen in Jacoco core, but the API for this is not available and
* scheduled for Q1 2014.
*
* See [[https://github.com/jacoco/jacoco/wiki/FilteringOptions]] and [[https://github.com/jacoco/jacoco/issues/139]]
* for more discussion of the JaCoCo roadmap.
*
* These filters are based on [[https://github.com/timezra/jacoco/commit/b6146ebed8b8e7507ec634ee565fe03f3e940fdd]],
* but extended to correctly exclude synthetics in module classes.
*/
private final class FilteringClassAnalyzer(classid: Long, classNode: ClassNode, probes: Array[Boolean],
stringPool: StringPool, coverageVisitor: ICoverageVisitor) extends ClassAnalyzer(classid, probes, stringPool) {

private val className = classNode.name

private val coverages = mutable.Buffer[IMethodCoverage]()

override def visitMethod(access: Int, name: String, desc: String,
signature: String, exceptions: Array[String]): MethodProbesVisitor = {
InstrSupport.assertNotInstrumented(name, getCoverage.getName)
if ((access & Opcodes.ACC_SYNTHETIC) != 0)
null
else {
new MethodAnalyzer(stringPool.get(name), stringPool.get(desc), stringPool.get(signature), probes) {
override def visitEnd() {
super.visitEnd()
val hasInstructions = getCoverage.getInstructionCounter.getTotalCount > 0
if (hasInstructions)
coverages += getCoverage
}
}
}
}

override def visitEnd() {
try visitFiltered()
finally {
super.visitEnd()
coverageVisitor.visitCoverage(getCoverage)
}
}

private val isModuleClass = className.endsWith("$")

private val methods: Seq[MethodNode] = classNode.methods.asInstanceOf[java.util.List[MethodNode]].asScala

private def visitFiltered() {
for {
mc <- coverages
methodNode = methods.find(m => m.name == mc.getName && m.desc == mc.getDesc).get
if !ignore(mc, methodNode)
} getCoverage.addMethod(mc)
}

private def ignore(mc: IMethodCoverage, node: MethodNode): Boolean = {
import node.name
import ScalaSyntheticMethod._, ScalaForwarderDetector._, AccessorDetector._
def isModuleStaticInit = isModuleClass && name == "<clinit>"

(
isSyntheticMethod(className, name, mc.getFirstLine, mc.getLastLine) // equals/hashCode/unapply et al
|| isModuleStaticInit // static init, `otherwise `case class Foo` reports uncovered code if `object Foo` is not accessed
|| isScalaForwarder(className, node)
|| isAccessor(node)
)
}
}

final class FilteringAnalyzer(executionData: ExecutionDataStore,
coverageVisitor: ICoverageVisitor) extends Analyzer(executionData, coverageVisitor) {
override def analyzeClass(reader: ClassReader) {
val classNode = new ClassNode()
reader.accept(classNode, 0)
val visitor = createFilteringVisitor(CRC64.checksum(reader.b), classNode)
reader.accept(visitor, 0)
}

private def createFilteringVisitor(classid: Long, classNode: ClassNode): ClassVisitor = {
val data = executionData.get(classid)
val probes = if (data == null) null else data.getProbes
val stringPool = new StringPool
val analyzer = new FilteringClassAnalyzer(classid, classNode, probes, stringPool, coverageVisitor)
new ClassProbesAdapter(analyzer)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package de.johoop.jacoco4sbt.filter

import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.{JumpInsnNode, MethodNode, MethodInsnNode}
import scala.collection.JavaConverters._

/** Detects forwarder methods added by Scala
* - classes and objects that mix in traits have a forwarder to the method body
* in the trait implementation class
* - classes contains static forwarders to methods in the companion object (for convenient Java interop)
* - methods in (boxed) value classes forward to the method body in the companion object
* - implicit classes creates a factory method beside the class.
* - lazy vals have an accessor that forwards to $lzycompute, which is the method
* with the interesting code.
*/
object ScalaForwarderDetector {
val LazyComputeSuffix: String = "$lzycompute"
def isScalaForwarder(className: String, node: MethodNode): Boolean = {
if (node.instructions.size() > 100) return false

val insn = node.instructions.iterator().asScala.toList
val hasJump = insn.exists {
case insn: JumpInsnNode => true
case _ => false
}
val hasForwarderCall = insn.exists {
case insn: MethodInsnNode =>
isScalaForwarder(className, node.name, insn.getOpcode, insn.owner, insn.name, insn.desc, hasJump)
case _ => false
}
hasForwarderCall
}

def isScalaForwarder(className: String, methodName: String, opcode: Int, calledMethodOwner: String,
calledMethodName: String, desc: String, hasJump: Boolean): Boolean = {
def callingCompanionModule = calledMethodOwner == (className + "$")
val callingImplClass = calledMethodOwner.endsWith("$class")
val callingImplicitClass = calledMethodOwner.endsWith("$" + methodName) || calledMethodOwner == methodName
def extensionName = methodName + "$extension"
import Opcodes._

val staticForwarder = opcode == INVOKEVIRTUAL && callingCompanionModule && calledMethodName == methodName
val traitForwarder = opcode == INVOKESTATIC && callingImplClass && calledMethodName == methodName
val extensionMethodForwarder = opcode == INVOKEVIRTUAL && callingCompanionModule && calledMethodName == extensionName
val implicitClassFactory = opcode == INVOKESPECIAL && callingImplicitClass && calledMethodName == "<init>"
val lazyAccessor = opcode == INVOKESPECIAL && calledMethodName.endsWith(LazyComputeSuffix)
val forwards = (
(staticForwarder || traitForwarder || extensionMethodForwarder || implicitClassFactory) && !hasJump // second condition a sanity check
|| lazyAccessor
)
forwards
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package de.johoop.jacoco4sbt.filter

object ScalaSyntheticMethod {
def isSyntheticMethod(owner: String, name: String, firstLine: Int, lastLine: Int) = {
val isModuleClass = owner.endsWith("$")
val isOneLiner = firstLine == lastLine
isOneLiner && (
(isModuleClass && isSyntheticObjectMethodName(name))
|| isSyntheticInstanceMethodName(name)
)
}

private def isSyntheticInstanceMethodName(name: String): Boolean = isCaseInstanceMethod(name)
private def isSyntheticObjectMethodName(name: String): Boolean = isCaseCompanionMethod(name) || isAnyValCompanionMethod(name)

private def isCaseInstanceMethod(name: String) = name match {
case "canEqual" | "copy" | "equals" | "hashCode" |"productPrefix" |
"productArity" | "productElement" | "productIterator" | "toString" => true
case _ if name.startsWith("copy$default") => true
case _ => false
}
private def isCaseCompanionMethod(name: String) = name match {
case "apply" | "unapply" | "unapplySeq" | "readResolve" => true
case _ => false
}
private def isAnyValCompanionMethod(name: String) = name match {
case "equals$extension" | "hashCode$extension" => true
case _ => false
}
}

0 comments on commit f914fa3

Please sign in to comment.