-
Notifications
You must be signed in to change notification settings - Fork 67
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merged in retronym/jacoco4sbt/topic/scala-awareness (pull request #4)
Filter Scala-related noise from JaCoCo results
- Loading branch information
Showing
9 changed files
with
334 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
60 changes: 60 additions & 0 deletions
60
src/main/scala/de/johoop/jacoco4sbt/ScalaHtmlFormatter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
52
src/main/scala/de/johoop/jacoco4sbt/ScalaLanguageNames.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
23
src/main/scala/de/johoop/jacoco4sbt/filter/AccessorDetector.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} | ||
} |
105 changes: 105 additions & 0 deletions
105
src/main/scala/de/johoop/jacoco4sbt/filter/FilteringClassAnalyzer.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
53 changes: 53 additions & 0 deletions
53
src/main/scala/de/johoop/jacoco4sbt/filter/ScalaForwarderDetector.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
30 changes: 30 additions & 0 deletions
30
src/main/scala/de/johoop/jacoco4sbt/filter/ScalaSyntheticMethod.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |