diff --git a/scalac-scoverage-plugin/src/main/scala/scoverage/CoverageFilter.scala b/scalac-scoverage-plugin/src/main/scala/scoverage/CoverageFilter.scala index 99c77430..1636d08c 100644 --- a/scalac-scoverage-plugin/src/main/scala/scoverage/CoverageFilter.scala +++ b/scalac-scoverage-plugin/src/main/scala/scoverage/CoverageFilter.scala @@ -12,6 +12,7 @@ trait CoverageFilter { def isClassIncluded(className: String): Boolean def isFileIncluded(file: SourceFile): Boolean def isLineIncluded(position: Position): Boolean + def isSymbolIncluded(symbolName: String): Boolean def getExcludedLineNumbers(sourceFile: SourceFile): List[Range] } @@ -20,13 +21,16 @@ object AllCoverageFilter extends CoverageFilter { override def isLineIncluded(position: Position): Boolean = true override def isClassIncluded(className: String): Boolean = true override def isFileIncluded(file: SourceFile): Boolean = true + override def isSymbolIncluded(symbolName: String): Boolean = true } class RegexCoverageFilter(excludedPackages: Seq[String], - excludedFiles: Seq[String]) extends CoverageFilter { + excludedFiles: Seq[String], + excludedSymbols: Seq[String]) extends CoverageFilter { val excludedClassNamePatterns = excludedPackages.map(_.r.pattern) val excludedFilePatterns = excludedFiles.map(_.r.pattern) + val excludedSymbolPatterns = excludedSymbols.map(_.r.pattern) /** * We cache the excluded ranges to avoid scanning the source code files @@ -64,6 +68,10 @@ class RegexCoverageFilter(excludedPackages: Seq[String], } } + override def isSymbolIncluded(symbolName: String): Boolean = { + excludedSymbolPatterns.isEmpty || !excludedSymbolPatterns.exists(_.matcher(symbolName).matches) + } + /** * Checks the given sourceFile for any magic comments which exclude lines * from coverage. Returns a list of Ranges of lines that should be excluded. diff --git a/scalac-scoverage-plugin/src/main/scala/scoverage/plugin.scala b/scalac-scoverage-plugin/src/main/scala/scoverage/plugin.scala index 5bab490c..d0bec2f2 100644 --- a/scalac-scoverage-plugin/src/main/scala/scoverage/plugin.scala +++ b/scalac-scoverage-plugin/src/main/scala/scoverage/plugin.scala @@ -25,6 +25,8 @@ class ScoveragePlugin(val global: Global) extends Plugin { options.excludedPackages = opt.substring("excludedPackages:".length).split(";").map(_.trim).filterNot(_.isEmpty) } else if (opt.startsWith("excludedFiles:")) { options.excludedFiles = opt.substring("excludedFiles:".length).split(";").map(_.trim).filterNot(_.isEmpty) + } else if (opt.startsWith("excludedSymbols:")) { + options.excludedSymbols = opt.substring("excludedSymbols:".length).split(";").map(_.trim).filterNot(_.isEmpty) } else if (opt.startsWith("dataDir:")) { options.dataDir = opt.substring("dataDir:".length) } else if (opt.startsWith("extraAfterPhase:") || opt.startsWith("extraBeforePhase:")){ @@ -42,6 +44,7 @@ class ScoveragePlugin(val global: Global) extends Plugin { "-P:scoverage:dataDir: where the coverage files should be written\n", "-P:scoverage:excludedPackages:; semicolon separated list of regexs for packages to exclude", "-P:scoverage:excludedFiles:; semicolon separated list of regexs for paths to exclude", + "-P:scoverage:excludedSymbols:; semicolon separated list of regexs for symbols to exclude", "-P:scoverage:extraAfterPhase: phase after which scoverage phase runs (must be after typer phase)", "-P:scoverage:extraBeforePhase: phase before which scoverage phase runs (must be before patmat phase)", " Any classes whose fully qualified name matches the regex will", @@ -73,6 +76,7 @@ class ScoveragePlugin(val global: Global) extends Plugin { class ScoverageOptions { var excludedPackages: Seq[String] = Nil var excludedFiles: Seq[String] = Nil + var excludedSymbols: Seq[String] = Seq("scala.reflect.api.Exprs.Expr", "scala.reflect.api.Trees.Tree", "scala.reflect.macros.Universe.Tree") var dataDir: String = IOUtils.getTempPath } @@ -101,7 +105,7 @@ class ScoverageInstrumentationComponent(val global: Global, extraAfterPhase: Opt def setOptions(options: ScoverageOptions): Unit = { this.options = options - coverageFilter = new RegexCoverageFilter(options.excludedPackages, options.excludedFiles) + coverageFilter = new RegexCoverageFilter(options.excludedPackages, options.excludedFiles, options.excludedSymbols) new File(options.dataDir).mkdirs() // ensure data directory is created } @@ -221,6 +225,7 @@ class ScoverageInstrumentationComponent(val global: Global, extraAfterPhase: Opt def isClassIncluded(symbol: Symbol): Boolean = coverageFilter.isClassIncluded(symbol.fullNameString) def isFileIncluded(source: SourceFile): Boolean = coverageFilter.isFileIncluded(source) def isStatementIncluded(pos: Position): Boolean = coverageFilter.isLineIncluded(pos) + def isSymbolIncluded(symbol: Symbol): Boolean = coverageFilter.isSymbolIncluded(symbol.fullNameString) def updateLocation(t: Tree) { Location(global)(t) match { @@ -371,7 +376,7 @@ class ScoverageInstrumentationComponent(val global: Global, extraAfterPhase: Opt // will catch macro implementations, as they must end with Expr, however will catch // any method that ends in Expr. // todo add way of allowing methods that return Expr - case d: DefDef if d.symbol != null && d.tpt.symbol.fullNameString == "scala.reflect.api.Exprs.Expr" => + case d: DefDef if d.symbol != null && !isSymbolIncluded(d.tpt.symbol) => tree // we can ignore primary constructors because they are just empty at this stage, the body is added later. diff --git a/scalac-scoverage-plugin/src/test/scala/scoverage/RegexCoverageFilterTest.scala b/scalac-scoverage-plugin/src/test/scala/scoverage/RegexCoverageFilterTest.scala index f7911187..c5ad5ee3 100644 --- a/scalac-scoverage-plugin/src/test/scala/scoverage/RegexCoverageFilterTest.scala +++ b/scalac-scoverage-plugin/src/test/scala/scoverage/RegexCoverageFilterTest.scala @@ -12,31 +12,31 @@ class RegexCoverageFilterTest extends FreeSpec with Matchers with MockitoSugar { "isClassIncluded" - { "should return true for empty excludes" in { - assert(new RegexCoverageFilter(Nil, Nil).isClassIncluded("x")) + assert(new RegexCoverageFilter(Nil, Nil, Nil).isClassIncluded("x")) } "should not crash for empty input" in { - assert(new RegexCoverageFilter(Nil, Nil).isClassIncluded("")) + assert(new RegexCoverageFilter(Nil, Nil, Nil).isClassIncluded("")) } "should exclude scoverage -> scoverage" in { - assert(!new RegexCoverageFilter(Seq("scoverage"), Nil).isClassIncluded("scoverage")) + assert(!new RegexCoverageFilter(Seq("scoverage"), Nil, Nil).isClassIncluded("scoverage")) } "should include scoverage -> scoverageeee" in { - assert(new RegexCoverageFilter(Seq("scoverage"), Nil).isClassIncluded("scoverageeee")) + assert(new RegexCoverageFilter(Seq("scoverage"), Nil, Nil).isClassIncluded("scoverageeee")) } "should exclude scoverage* -> scoverageeee" in { - assert(!new RegexCoverageFilter(Seq("scoverage*"), Nil).isClassIncluded("scoverageeee")) + assert(!new RegexCoverageFilter(Seq("scoverage*"), Nil, Nil).isClassIncluded("scoverageeee")) } "should include eee -> scoverageeee" in { - assert(new RegexCoverageFilter(Seq("eee"), Nil).isClassIncluded("scoverageeee")) + assert(new RegexCoverageFilter(Seq("eee"), Nil, Nil).isClassIncluded("scoverageeee")) } "should exclude .*eee -> scoverageeee" in { - assert(!new RegexCoverageFilter(Seq(".*eee"), Nil).isClassIncluded("scoverageeee")) + assert(!new RegexCoverageFilter(Seq(".*eee"), Nil, Nil).isClassIncluded("scoverageeee")) } } "isFileIncluded" - { @@ -44,19 +44,57 @@ class RegexCoverageFilterTest extends FreeSpec with Matchers with MockitoSugar { Mockito.when(abstractFile.path).thenReturn("sammy.scala") "should return true for empty excludes" in { val file = new BatchSourceFile(abstractFile, Array.emptyCharArray) - new RegexCoverageFilter(Nil, Nil).isFileIncluded(file) shouldBe true + new RegexCoverageFilter(Nil, Nil, Nil).isFileIncluded(file) shouldBe true } "should exclude by filename" in { val file = new BatchSourceFile(abstractFile, Array.emptyCharArray) - new RegexCoverageFilter(Nil, Seq("sammy")).isFileIncluded(file) shouldBe false + new RegexCoverageFilter(Nil, Seq("sammy"), Nil).isFileIncluded(file) shouldBe false } "should exclude by regex wildcard" in { val file = new BatchSourceFile(abstractFile, Array.emptyCharArray) - new RegexCoverageFilter(Nil, Seq("sam.*")).isFileIncluded(file) shouldBe false + new RegexCoverageFilter(Nil, Seq("sam.*"), Nil).isFileIncluded(file) shouldBe false } "should not exclude non matching regex" in { val file = new BatchSourceFile(abstractFile, Array.emptyCharArray) - new RegexCoverageFilter(Nil, Seq("qweqeqwe")).isFileIncluded(file) shouldBe true + new RegexCoverageFilter(Nil, Seq("qweqeqwe"), Nil).isFileIncluded(file) shouldBe true + } + } + "isSymbolIncluded" - { + val options = new ScoverageOptions() + "should return true for empty excludes" in { + assert(new RegexCoverageFilter(Nil, Nil, Nil).isSymbolIncluded("x")) + } + + "should not crash for empty input" in { + assert(new RegexCoverageFilter(Nil, Nil, Nil).isSymbolIncluded("")) + } + + "should exclude scoverage -> scoverage" in { + assert(!new RegexCoverageFilter(Nil, Nil, Seq("scoverage")).isSymbolIncluded("scoverage")) + } + + "should include scoverage -> scoverageeee" in { + assert(new RegexCoverageFilter(Nil, Nil, Seq("scoverage")).isSymbolIncluded("scoverageeee")) + } + "should exclude scoverage* -> scoverageeee" in { + assert(!new RegexCoverageFilter(Nil, Nil, Seq("scoverage*")).isSymbolIncluded("scoverageeee")) + } + + "should include eee -> scoverageeee" in { + assert(new RegexCoverageFilter(Nil, Nil, Seq("eee")).isSymbolIncluded("scoverageeee")) + } + + "should exclude .*eee -> scoverageeee" in { + assert(!new RegexCoverageFilter(Nil, Nil, Seq(".*eee")).isSymbolIncluded("scoverageeee")) + } + "should exclude scala.reflect.api.Exprs.Expr" in { + assert(!new RegexCoverageFilter(Nil, Nil, options.excludedSymbols).isSymbolIncluded("scala.reflect.api.Exprs.Expr")) + } + "should exclude scala.reflect.macros.Universe.Tree" in { + assert(!new RegexCoverageFilter(Nil, Nil, options.excludedSymbols).isSymbolIncluded("scala.reflect.macros.Universe.Tree")) + } + "should exclude scala.reflect.api.Trees.Tree" in { + assert(!new RegexCoverageFilter(Nil, Nil, options.excludedSymbols).isSymbolIncluded("scala.reflect.api.Trees.Tree")) } } "getExcludedLineNumbers" - { @@ -72,7 +110,7 @@ class RegexCoverageFilterTest extends FreeSpec with Matchers with MockitoSugar { |8 """.stripMargin - val numbers = new RegexCoverageFilter(Nil, Nil).getExcludedLineNumbers(mockSourceFile(file)) + val numbers = new RegexCoverageFilter(Nil, Nil, Nil).getExcludedLineNumbers(mockSourceFile(file)) numbers === List.empty } "should exclude lines between magic comments" in { @@ -95,7 +133,7 @@ class RegexCoverageFilterTest extends FreeSpec with Matchers with MockitoSugar { |16 """.stripMargin - val numbers = new RegexCoverageFilter(Nil, Nil).getExcludedLineNumbers(mockSourceFile(file)) + val numbers = new RegexCoverageFilter(Nil, Nil, Nil).getExcludedLineNumbers(mockSourceFile(file)) numbers === List(Range(4, 9), Range(12, 14)) } "should exclude all lines after an upaired magic comment" in { @@ -117,7 +155,7 @@ class RegexCoverageFilterTest extends FreeSpec with Matchers with MockitoSugar { |15 """.stripMargin - val numbers = new RegexCoverageFilter(Nil, Nil).getExcludedLineNumbers(mockSourceFile(file)) + val numbers = new RegexCoverageFilter(Nil, Nil, Nil).getExcludedLineNumbers(mockSourceFile(file)) numbers === List(Range(4, 9), Range(12, 16)) } "should allow text comments on the same line as the markers" in { @@ -139,7 +177,7 @@ class RegexCoverageFilterTest extends FreeSpec with Matchers with MockitoSugar { |15 """.stripMargin - val numbers = new RegexCoverageFilter(Nil, Nil).getExcludedLineNumbers(mockSourceFile(file)) + val numbers = new RegexCoverageFilter(Nil, Nil, Nil).getExcludedLineNumbers(mockSourceFile(file)) numbers === List(Range(4, 9), Range(12, 16)) } }