From 91a91083c14e418e35088cbde2006a35cbc00b8f Mon Sep 17 00:00:00 2001 From: Chris Kipp Date: Tue, 28 Sep 2021 16:17:38 +0200 Subject: [PATCH] Refactor ScoverageOptions out to its own file. This renames the plugin.scala to ScoveragePlugin.scala and also moves the parsing of the options out into it's own file. I also clean up some of the vars that weren't really necessary --- .../scala/scoverage/ScoverageOptions.scala | 116 ++++++++++++++++ .../{plugin.scala => ScoveragePlugin.scala} | 130 +++++------------- .../scoverage/RegexCoverageFilterTest.scala | 2 +- .../scala/scoverage/ScoverageCompiler.scala | 7 +- 4 files changed, 157 insertions(+), 98 deletions(-) create mode 100644 scalac-scoverage-plugin/src/main/scala/scoverage/ScoverageOptions.scala rename scalac-scoverage-plugin/src/main/scala/scoverage/{plugin.scala => ScoveragePlugin.scala} (86%) diff --git a/scalac-scoverage-plugin/src/main/scala/scoverage/ScoverageOptions.scala b/scalac-scoverage-plugin/src/main/scala/scoverage/ScoverageOptions.scala new file mode 100644 index 00000000..13c2f367 --- /dev/null +++ b/scalac-scoverage-plugin/src/main/scala/scoverage/ScoverageOptions.scala @@ -0,0 +1,116 @@ +package scoverage + +/** Base options that can be passed into scoverage + * + * @param excludedPackages packages to be excluded in coverage + * @param excludedFiles files to be excluded in coverage + * @param excludedSymbols symbols to be excluded in coverage + * @param dataDir the directory that the coverage files should be written to + * @param reportTestName whether or not the test names should be reported + * @param sourceRoot the source root of your project + */ +case class ScoverageOptions( + excludedPackages: Seq[String], + excludedFiles: Seq[String], + excludedSymbols: Seq[String], + dataDir: String, + reportTestName: Boolean, + sourceRoot: String +) + +object ScoverageOptions { + + private[scoverage] val help = Some( + Seq( + "-P:scoverage:dataDir: where the coverage files should be written\n", + "-P:scoverage:sourceRoot: the root dir of your sources, used for path relativization\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", + " be excluded from coverage." + ).mkString("\n") + ) + + private def parseExclusionOption( + inOption: String + ): Seq[String] = + inOption + .split(";") + .collect { + case value if value.trim().nonEmpty => value.trim() + } + .toIndexedSeq + + private val ExcludedPackages = "excludedPackages:(.*)".r + private val ExcludedFiles = "excludedFiles:(.*)".r + private val ExcludedSymbols = "excludedSymbols:(.*)".r + private val DataDir = "dataDir:(.*)".r + private val SourceRoot = "sourceRoot:(.*)".r + private val ExtraAfterPhase = "extraAfterPhase:(.*)".r + private val ExtraBeforePhase = "extraBeforePhase:(.*)".r + + /** Default that is _only_ used for initializing purposes. dataDir and + * sourceRoot are both just empty strings here, but we nevery actually + * allow for this to be the case when the plugin runs, and this is checked + * before it does. + */ + def default() = ScoverageOptions( + excludedPackages = Seq.empty, + excludedFiles = Seq.empty, + excludedSymbols = Seq( + "scala.reflect.api.Exprs.Expr", + "scala.reflect.api.Trees.Tree", + "scala.reflect.macros.Universe.Tree" + ), + dataDir = "", + reportTestName = false, + sourceRoot = "" + ) + + def processPhaseOptions( + opts: List[String] + ): (Option[String], Option[String]) = { + + val afterPhase: Option[String] = + opts.collectFirst { case ExtraAfterPhase(phase) => phase } + val beforePhase: Option[String] = + opts.collectFirst { case ExtraBeforePhase(phase) => phase } + + (afterPhase, beforePhase) + } + + def parse( + scalacOptions: List[String], + errFn: String => Unit, + base: ScoverageOptions + ): ScoverageOptions = { + + var options = base + + scalacOptions.foreach { + case ExcludedPackages(packages) => + options = options.copy(excludedFiles = parseExclusionOption(packages)) + case ExcludedFiles(files) => + options = options.copy(excludedFiles = parseExclusionOption(files)) + case ExcludedSymbols(symbols) => + options = options.copy(excludedSymbols = parseExclusionOption(symbols)) + case DataDir(dir) => + options = options.copy(dataDir = dir) + case SourceRoot(root) => + options.copy(sourceRoot = root) + // NOTE that both the extra phases are actually parsed out early on, so + // we just ignore them here + case ExtraAfterPhase(afterPhase) => () + case ExtraBeforePhase(beforePhase) => () + case "reportTestName" => + options.copy(reportTestName = true) + case opt => errFn("Unknown option: " + opt) + } + + options + } + +} diff --git a/scalac-scoverage-plugin/src/main/scala/scoverage/plugin.scala b/scalac-scoverage-plugin/src/main/scala/scoverage/ScoveragePlugin.scala similarity index 86% rename from scalac-scoverage-plugin/src/main/scala/scoverage/plugin.scala rename to scalac-scoverage-plugin/src/main/scala/scoverage/ScoveragePlugin.scala index 08beb312..996e2312 100644 --- a/scalac-scoverage-plugin/src/main/scala/scoverage/plugin.scala +++ b/scalac-scoverage-plugin/src/main/scala/scoverage/ScoveragePlugin.scala @@ -14,126 +14,59 @@ import scala.tools.nsc.transform.TypingTransformers /** @author Stephen Samuel */ class ScoveragePlugin(val global: Global) extends Plugin { - override val name: String = "scoverage" - override val description: String = "scoverage code coverage compiler plugin" - private val (extraAfterPhase, extraBeforePhase) = processPhaseOptions( - pluginOptions - ) + override val name: String = ScoveragePlugin.name + override val description: String = ScoveragePlugin.description + + // TODO I'm not 100% sure why, but historically these have been parsed out + // first. One thing to play around with in the future would be to not do this + // here and rather do it later when we utilize setOpts and instead just + // initialize then in the instrumentationCompoent. This will save us + // iterating over these options twice. + private val (extraAfterPhase, extraBeforePhase) = + ScoverageOptions.processPhaseOptions( + pluginOptions + ) + val instrumentationComponent = new ScoverageInstrumentationComponent( global, extraAfterPhase, extraBeforePhase ) + override val components: List[PluginComponent] = List( instrumentationComponent ) - private def parseExclusionEntry( - entryName: String, - inOption: String - ): Seq[String] = - inOption - .substring(entryName.length) - .split(";") - .map(_.trim) - .toIndexedSeq - .filterNot(_.isEmpty) - override def init(opts: List[String], error: String => Unit): Boolean = { - val options = new ScoverageOptions - - for (opt <- opts) { - if (opt.startsWith("excludedPackages:")) { - options.excludedPackages = parseExclusionEntry("excludedPackages:", opt) - } else if (opt.startsWith("excludedFiles:")) { - options.excludedFiles = parseExclusionEntry("excludedFiles:", opt) - } else if (opt.startsWith("excludedSymbols:")) { - options.excludedSymbols = parseExclusionEntry("excludedSymbols:", opt) - } else if (opt.startsWith("dataDir:")) { - options.dataDir = opt.substring("dataDir:".length) - } else if (opt.startsWith("sourceRoot:")) { - options.sourceRoot = opt.substring("sourceRoot:".length()) - } else if ( - opt - .startsWith("extraAfterPhase:") || opt.startsWith("extraBeforePhase:") - ) { - // skip here, these flags are processed elsewhere - } else if (opt == "reportTestName") { - options.reportTestName = true - } else { - error("Unknown option: " + opt) - } - } - if (!opts.exists(_.startsWith("dataDir:"))) + + val options = + ScoverageOptions.parse(opts, error, ScoverageOptions.default()) + + if (options.dataDir.isEmpty()) throw new RuntimeException( "Cannot invoke plugin without specifying " ) - if (!opts.exists(_.startsWith("sourceRoot:"))) + + if (options.sourceRoot.isEmpty()) throw new RuntimeException( "Cannot invoke plugin without specifying " ) + instrumentationComponent.setOptions(options) true } - override val optionsHelp: Option[String] = Some( - Seq( - "-P:scoverage:dataDir: where the coverage files should be written\n", - "-P:scoverage:sourceRoot: the root dir of your sources, used for path relativization\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", - " be excluded from coverage." - ).mkString("\n") - ) + override val optionsHelp: Option[String] = ScoverageOptions.help - // copied from scala 2.11 private def pluginOptions: List[String] = { // Process plugin options of form plugin:option def namec = name + ":" - global.settings.pluginOptions.value filter (_ startsWith namec) map (_ stripPrefix namec) - } - - private def processPhaseOptions( - opts: List[String] - ): (Option[String], Option[String]) = { - var afterPhase: Option[String] = None - var beforePhase: Option[String] = None - for (opt <- opts) { - if (opt.startsWith("extraAfterPhase:")) { - afterPhase = Some(opt.substring("extraAfterPhase:".length)) - } - if (opt.startsWith("extraBeforePhase:")) { - beforePhase = Some(opt.substring("extraBeforePhase:".length)) - } - } - (afterPhase, beforePhase) + global.settings.pluginOptions.value + .filter(_.startsWith(namec)) + .map(_.stripPrefix(namec)) } } -// TODO refactor this into a case class. We'll also refactor how we parse the -// options to get rid of all these vars -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 - var reportTestName: Boolean = false - // TODO again, we'll refactor this later so this won't have a default here. - // However for tests we'll have to create this. However, make sure you create - // either both in temp or neither in temp, since on windows your temp dir - // will be in another drive, so the relativize functinality won't work if - // correctly. - var sourceRoot: String = IOUtils.getTempPath -} - class ScoverageInstrumentationComponent( val global: Global, extraAfterPhase: Option[String], @@ -147,7 +80,7 @@ class ScoverageInstrumentationComponent( val statementIds = new AtomicInteger(0) val coverage = new Coverage - override val phaseName: String = "scoverage-instrumentation" + override val phaseName: String = ScoveragePlugin.phaseName override val runsAfter: List[String] = List("typer") ::: extraAfterPhase.toList override val runsBefore: List[String] = @@ -158,7 +91,7 @@ class ScoverageInstrumentationComponent( * You must call "setOptions" before running any commands that rely on * the options. */ - private var options: ScoverageOptions = new ScoverageOptions() + private var options: ScoverageOptions = ScoverageOptions.default() private var coverageFilter: CoverageFilter = AllCoverageFilter private val isScalaJsEnabled: Boolean = { @@ -194,7 +127,6 @@ class ScoverageInstrumentationComponent( s"Instrumentation completed [${coverage.statements.size} statements]" ) - // TODO do we need to verify this sourceRoot exists? How does semanticdb do this? Serializer.serialize( coverage, Serializer.coverageFile(options.dataDir), @@ -920,3 +852,9 @@ class ScoverageInstrumentationComponent( } } } + +object ScoveragePlugin { + val name: String = "scoverage" + val description: String = "scoverage code coverage compiler plugin" + val phaseName: String = "scoverage-instrumentation" +} diff --git a/scalac-scoverage-plugin/src/test/scala/scoverage/RegexCoverageFilterTest.scala b/scalac-scoverage-plugin/src/test/scala/scoverage/RegexCoverageFilterTest.scala index bec1c693..781df1e0 100644 --- a/scalac-scoverage-plugin/src/test/scala/scoverage/RegexCoverageFilterTest.scala +++ b/scalac-scoverage-plugin/src/test/scala/scoverage/RegexCoverageFilterTest.scala @@ -83,7 +83,7 @@ class RegexCoverageFilterTest extends FunSuite { ) } - val options = new ScoverageOptions() + val options = ScoverageOptions.default() test("isSymbolIncluded should return true for empty excludes") { assert(new RegexCoverageFilter(Nil, Nil, Nil).isSymbolIncluded("x")) diff --git a/scalac-scoverage-plugin/src/test/scala/scoverage/ScoverageCompiler.scala b/scalac-scoverage-plugin/src/test/scala/scoverage/ScoverageCompiler.scala index d461ce5c..f4e51456 100644 --- a/scalac-scoverage-plugin/src/test/scala/scoverage/ScoverageCompiler.scala +++ b/scalac-scoverage-plugin/src/test/scala/scoverage/ScoverageCompiler.scala @@ -121,7 +121,12 @@ class ScoverageCompiler( val instrumentationComponent = new ScoverageInstrumentationComponent(this, None, None) - instrumentationComponent.setOptions(new ScoverageOptions()) + val coverageOptions = ScoverageOptions + .default() + .copy(dataDir = IOUtils.getTempPath) + .copy(sourceRoot = IOUtils.getTempPath) + + instrumentationComponent.setOptions(coverageOptions) val testStore = new ScoverageTestStoreComponent(this) val validator = new PositionValidator(this)