diff --git a/.gitignore b/.gitignore index f378adb24bc8..e4947b9bfebb 100644 --- a/.gitignore +++ b/.gitignore @@ -98,4 +98,3 @@ docs/_spec/.jekyll-metadata # scaladoc related scaladoc/output/ - diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 4257f51d8255..5b72f614f8df 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -124,6 +124,8 @@ trait CommonScalaSettings: /* Coverage settings */ val coverageOutputDir = PathSetting("-coverage-out", "Destination for coverage classfiles and instrumentation data.", "", aliases = List("--coverage-out")) + val coverageExcludeClasslikes: Setting[List[String]] = MultiStringSetting("-coverage-exclude-classlikes", "packages, classes and modules", "List of regexes for packages, classes and modules to exclude from coverage.", aliases = List("--coverage-exclude-classlikes")) + val coverageExcludeFiles: Setting[List[String]] = MultiStringSetting("-coverage-exclude-files", "files", "List of regexes for files to exclude from coverage.", aliases = List("--coverage-exclude-files")) /* Other settings */ val encoding: Setting[String] = StringSetting("-encoding", "encoding", "Specify character encoding used by source files.", Properties.sourceEncoding, aliases = List("--encoding")) diff --git a/compiler/src/dotty/tools/dotc/transform/InstrumentCoverage.scala b/compiler/src/dotty/tools/dotc/transform/InstrumentCoverage.scala index eac44e982603..a76919e47164 100644 --- a/compiler/src/dotty/tools/dotc/transform/InstrumentCoverage.scala +++ b/compiler/src/dotty/tools/dotc/transform/InstrumentCoverage.scala @@ -13,12 +13,15 @@ import core.Constants.Constant import core.NameOps.isContextFunction import core.StdNames.nme import core.Types.* +import core.Decorators.* import coverage.* import typer.LiftCoverage import util.{SourcePosition, SourceFile} import util.Spans.Span import localopt.StringInterpolatorOpt import inlines.Inlines +import scala.util.matching.Regex +import java.util.regex.Pattern /** Implements code coverage by inserting calls to scala.runtime.coverage.Invoker * ("instruments" the source code). @@ -41,6 +44,9 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer: // stores all instrumented statements private val coverage = Coverage() + private var coverageExcludeClasslikePatterns: List[Pattern] = Nil + private var coverageExcludeFilePatterns: List[Pattern] = Nil + override def run(using ctx: Context): Unit = val outputPath = ctx.settings.coverageOutputDir.value @@ -54,10 +60,26 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer: .filter(_.nn.getName.nn.startsWith("scoverage")) .foreach(_.nn.delete()) end if + + coverageExcludeClasslikePatterns = ctx.settings.coverageExcludeClasslikes.value.map(_.r.pattern) + coverageExcludeFilePatterns = ctx.settings.coverageExcludeFiles.value.map(_.r.pattern) + super.run Serializer.serialize(coverage, outputPath, ctx.settings.sourceroot.value) + private def isClassIncluded(sym: Symbol)(using Context): Boolean = + val fqn = sym.fullName.toText(ctx.printerFn(ctx)).show + coverageExcludeClasslikePatterns.isEmpty || !coverageExcludeClasslikePatterns.exists( + _.matcher(fqn).nn.matches + ) + + private def isFileIncluded(file: SourceFile)(using Context): Boolean = + val normalizedPath = file.path.replace(".scala", "") + coverageExcludeFilePatterns.isEmpty || !coverageExcludeFilePatterns.exists( + _.matcher(normalizedPath).nn.matches + ) + override protected def newTransformer(using Context) = CoverageTransformer(ctx.settings.coverageOutputDir.value) @@ -269,8 +291,17 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer: transformDefDef(tree) case tree: PackageDef => - // only transform the statements of the package - cpy.PackageDef(tree)(tree.pid, transform(tree.stats)) + if isFileIncluded(tree.srcPos.sourcePos.source) && isClassIncluded(tree.symbol) then + // only transform the statements of the package + cpy.PackageDef(tree)(tree.pid, transform(tree.stats)) + else + tree + + case tree: TypeDef => + if isFileIncluded(tree.srcPos.sourcePos.source) && isClassIncluded(tree.symbol) then + super.transform(tree) + else + tree case tree: Assign => // only transform the rhs diff --git a/tests/coverage/pos/ExcludeClass.scala b/tests/coverage/pos/ExcludeClass.scala new file mode 100644 index 000000000000..a73a8ed34e71 --- /dev/null +++ b/tests/coverage/pos/ExcludeClass.scala @@ -0,0 +1,19 @@ +//> using options -coverage-exclude-classlikes:covtest.Klass + +package covtest + +class Klass { + def abs(i: Int) = + if i > 0 then + i + else + -i +} + +class Klass2 { + def abs(i: Int) = + if i > 0 then + i + else + -i +} diff --git a/tests/coverage/pos/ExcludeClass.scoverage.check b/tests/coverage/pos/ExcludeClass.scoverage.check new file mode 100644 index 000000000000..5e77f0ce21a1 --- /dev/null +++ b/tests/coverage/pos/ExcludeClass.scoverage.check @@ -0,0 +1,71 @@ +# Coverage data, format version: 3.0 +# Statement data: +# - id +# - source path +# - package name +# - class name +# - class type (Class, Object or Trait) +# - full class name +# - method name +# - start offset +# - end offset +# - line number +# - symbol name +# - tree name +# - is branch +# - invocations count +# - is ignored +# - description (can be multi-line) +# ' ' sign +# ------------------------------------------ +0 +ExcludeClass.scala +covtest +Klass2 +Class +covtest.Klass2 +abs +219 +220 +16 +i +Ident +true +0 +false +i + +1 +ExcludeClass.scala +covtest +Klass2 +Class +covtest.Klass2 +abs +236 +238 +18 +unary_- +Select +true +0 +false +-i + +2 +ExcludeClass.scala +covtest +Klass2 +Class +covtest.Klass2 +abs +177 +184 +14 +abs +DefDef +false +0 +false +def abs + diff --git a/tests/coverage/pos/ExcludeDef.scala b/tests/coverage/pos/ExcludeDef.scala new file mode 100644 index 000000000000..bddbe12022ef --- /dev/null +++ b/tests/coverage/pos/ExcludeDef.scala @@ -0,0 +1,9 @@ +//> using options -coverage-exclude-classlikes:covtest\..* + +package covtest + +def abs(i: Int) = + if i > 0 then + i + else + -i diff --git a/tests/coverage/pos/ExcludeDef.scoverage.check b/tests/coverage/pos/ExcludeDef.scoverage.check new file mode 100644 index 000000000000..1bdba951d6ae --- /dev/null +++ b/tests/coverage/pos/ExcludeDef.scoverage.check @@ -0,0 +1,20 @@ +# Coverage data, format version: 3.0 +# Statement data: +# - id +# - source path +# - package name +# - class name +# - class type (Class, Object or Trait) +# - full class name +# - method name +# - start offset +# - end offset +# - line number +# - symbol name +# - tree name +# - is branch +# - invocations count +# - is ignored +# - description (can be multi-line) +# ' ' sign +# ------------------------------------------ diff --git a/tests/coverage/pos/ExcludeFile.scala b/tests/coverage/pos/ExcludeFile.scala new file mode 100644 index 000000000000..b9f15244ebe3 --- /dev/null +++ b/tests/coverage/pos/ExcludeFile.scala @@ -0,0 +1,11 @@ +//> using options -coverage-exclude-files:.*ExcludeFile + +package covtest + +class Klass { + def abs(i: Int) = + if i > 0 then + i + else + -i +} diff --git a/tests/coverage/pos/ExcludeFile.scoverage.check b/tests/coverage/pos/ExcludeFile.scoverage.check new file mode 100644 index 000000000000..1bdba951d6ae --- /dev/null +++ b/tests/coverage/pos/ExcludeFile.scoverage.check @@ -0,0 +1,20 @@ +# Coverage data, format version: 3.0 +# Statement data: +# - id +# - source path +# - package name +# - class name +# - class type (Class, Object or Trait) +# - full class name +# - method name +# - start offset +# - end offset +# - line number +# - symbol name +# - tree name +# - is branch +# - invocations count +# - is ignored +# - description (can be multi-line) +# ' ' sign +# ------------------------------------------ diff --git a/tests/coverage/pos/ExcludeOtherStuff.scala b/tests/coverage/pos/ExcludeOtherStuff.scala new file mode 100644 index 000000000000..6a333560c59e --- /dev/null +++ b/tests/coverage/pos/ExcludeOtherStuff.scala @@ -0,0 +1,15 @@ +//> using options -coverage-exclude-classlikes:covtest.Oject,covtest.Tait + +package covtest + +object Oject { + def abs(i: Int) = + if i > 0 then + i + else + -i +} + +trait Tait { + def abs(i: Int): Int +} diff --git a/tests/coverage/pos/ExcludeOtherStuff.scoverage.check b/tests/coverage/pos/ExcludeOtherStuff.scoverage.check new file mode 100644 index 000000000000..1bdba951d6ae --- /dev/null +++ b/tests/coverage/pos/ExcludeOtherStuff.scoverage.check @@ -0,0 +1,20 @@ +# Coverage data, format version: 3.0 +# Statement data: +# - id +# - source path +# - package name +# - class name +# - class type (Class, Object or Trait) +# - full class name +# - method name +# - start offset +# - end offset +# - line number +# - symbol name +# - tree name +# - is branch +# - invocations count +# - is ignored +# - description (can be multi-line) +# ' ' sign +# ------------------------------------------ diff --git a/tests/coverage/pos/ExcludePackage.scala b/tests/coverage/pos/ExcludePackage.scala new file mode 100644 index 000000000000..771ac2c1f062 --- /dev/null +++ b/tests/coverage/pos/ExcludePackage.scala @@ -0,0 +1,11 @@ +//> using options -coverage-exclude-classlikes:covtest + +package covtest + +class Klass { + def abs(i: Int) = + if i > 0 then + i + else + -i +} diff --git a/tests/coverage/pos/ExcludePackage.scoverage.check b/tests/coverage/pos/ExcludePackage.scoverage.check new file mode 100644 index 000000000000..1bdba951d6ae --- /dev/null +++ b/tests/coverage/pos/ExcludePackage.scoverage.check @@ -0,0 +1,20 @@ +# Coverage data, format version: 3.0 +# Statement data: +# - id +# - source path +# - package name +# - class name +# - class type (Class, Object or Trait) +# - full class name +# - method name +# - start offset +# - end offset +# - line number +# - symbol name +# - tree name +# - is branch +# - invocations count +# - is ignored +# - description (can be multi-line) +# ' ' sign +# ------------------------------------------