diff --git a/compiler/src/dotty/tools/dotc/transform/InstrumentCoverage.scala b/compiler/src/dotty/tools/dotc/transform/InstrumentCoverage.scala index 449402f17fce..ef57ea6f85ad 100644 --- a/compiler/src/dotty/tools/dotc/transform/InstrumentCoverage.scala +++ b/compiler/src/dotty/tools/dotc/transform/InstrumentCoverage.scala @@ -5,6 +5,7 @@ import java.io.File import ast.tpd.* import collection.mutable +import core.Comments.Comment import core.Flags.* import core.Contexts.{Context, ctx, inContext} import core.DenotTransformers.IdentityDenotTransformer @@ -40,6 +41,7 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer: private var coverageExcludeClasslikePatterns: List[Pattern] = Nil private var coverageExcludeFilePatterns: List[Pattern] = Nil + private val coverageLocalExclusions: mutable.ListBuffer[Span] = mutable.ListBuffer.empty override def run(using ctx: Context): Unit = val outputPath = ctx.settings.coverageOutputDir.value @@ -62,6 +64,22 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer: coverageExcludeClasslikePatterns = ctx.settings.coverageExcludeClasslikes.value.map(_.r.pattern) coverageExcludeFilePatterns = ctx.settings.coverageExcludeFiles.value.map(_.r.pattern) + var currentStartingComment: Option[Comment] = None + ctx.compilationUnit.comments.foreach { + case comment if InstrumentCoverage.scoverageLocalOff.matches(comment.raw) && currentStartingComment.isEmpty => + currentStartingComment = Some(comment) + case comment if InstrumentCoverage.scoverageLocalOn.matches(comment.raw) => + currentStartingComment.foreach { start => + currentStartingComment = None + coverageLocalExclusions += start.span.withEnd(comment.span.end) + } + case _ => + } + + currentStartingComment.headOption.foreach { start => + coverageLocalExclusions += start.span.withEnd(ctx.source.length - 1) + } + ctx.base.coverage.nn.removeStatementsFromFile(ctx.compilationUnit.source.file.absolute.jpath) super.run @@ -79,6 +97,10 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer: _.matcher(normalizedPath).matches ) + private def isTreeExcluded(tree: Tree)(using Context): Boolean = + coverageLocalExclusions.exists: excludedSpans => + excludedSpans.contains(tree.span) + override protected def newTransformer(using Context) = CoverageTransformer(ctx.settings.coverageOutputDir.value) @@ -179,7 +201,7 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer: * @return instrumentation result, with the preparation statement, coverage call and tree separated */ private def tryInstrument(tree: Apply)(using Context): InstrumentedParts = - if canInstrumentApply(tree) then + if !isTreeExcluded(tree) && canInstrumentApply(tree) then // Create a call to Invoker.invoked(coverageDirectory, newStatementId) val coverageCall = createInvokeCall(tree, tree.sourcePos) @@ -206,7 +228,7 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer: private def tryInstrument(tree: Ident)(using Context): InstrumentedParts = val sym = tree.symbol - if canInstrumentParameterless(sym) then + if !isTreeExcluded(tree) && canInstrumentParameterless(sym) then // call to a local parameterless method f val coverageCall = createInvokeCall(tree, tree.sourcePos) InstrumentedParts.singleExpr(coverageCall, tree) @@ -214,14 +236,18 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer: InstrumentedParts.notCovered(tree) private def tryInstrument(tree: Literal)(using Context): InstrumentedParts = - val coverageCall = createInvokeCall(tree, tree.sourcePos) - InstrumentedParts.singleExpr(coverageCall, tree) + if !isTreeExcluded(tree) then + val coverageCall = createInvokeCall(tree, tree.sourcePos) + InstrumentedParts.singleExpr(coverageCall, tree) + else + InstrumentedParts.notCovered(tree) + private def tryInstrument(tree: Select)(using Context): InstrumentedParts = val sym = tree.symbol val qual = transform(tree.qualifier).ensureConforms(tree.qualifier.tpe) val transformed = cpy.Select(tree)(qual, tree.name) - if canInstrumentParameterless(sym) then + if !isTreeExcluded(tree) && canInstrumentParameterless(sym) then // call to a parameterless method val coverageCall = createInvokeCall(tree, tree.sourcePos) InstrumentedParts.singleExpr(coverageCall, transformed) @@ -606,6 +632,8 @@ object InstrumentCoverage: val name: String = "instrumentCoverage" val description: String = "instrument code for coverage checking" val ExcludeMethodFlags: FlagSet = Artifact | Erased + val scoverageLocalOn: Regex = """^\s*//\s*\$COVERAGE-ON\$""".r + val scoverageLocalOff: Regex = """^\s*//\s*\$COVERAGE-OFF\$""".r /** * An instrumented Tree, in 3 parts. diff --git a/tests/coverage/pos/SimpleMethodsIgnoredLocally.scala b/tests/coverage/pos/SimpleMethodsIgnoredLocally.scala new file mode 100644 index 000000000000..d55cf8556b31 --- /dev/null +++ b/tests/coverage/pos/SimpleMethodsIgnoredLocally.scala @@ -0,0 +1,29 @@ +package covtest + +class C: + def a: C = this + def b: Unit = return + def c: Unit = () + def d: Int = 12 + def e: Null = null + + // $COVERAGE-OFF$ + def block: Int = + "literal" + 0 + + def cond: Boolean = + if false then true + else false + + def partialCond: Unit = + if false then () + + // $COVERAGE-ON$ + + def new1: C = new {} + + def tryCatch: Unit = + try () + catch + case e: Exception => 1 diff --git a/tests/coverage/pos/SimpleMethodsIgnoredLocally.scoverage.check b/tests/coverage/pos/SimpleMethodsIgnoredLocally.scoverage.check new file mode 100644 index 000000000000..f0b8703f1068 --- /dev/null +++ b/tests/coverage/pos/SimpleMethodsIgnoredLocally.scoverage.check @@ -0,0 +1,508 @@ +# 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 +SimpleMethods.scala +covtest +C +Class +covtest.C +a +28 +33 +4 +def a + +1 +SimpleMethods.scala +covtest +C +Class +covtest.C +b +60 +66 +5 + +Literal +false +0 +false +return + +2 +SimpleMethods.scala +covtest +C +Class +covtest.C +b +46 +51 +5 +b +DefDef +false +0 +false +def b + +3 +SimpleMethods.scala +covtest +C +Class +covtest.C +c +83 +85 +6 + +Literal +false +0 +false +() + +4 +SimpleMethods.scala +covtest +C +Class +covtest.C +c +69 +74 +6 +c +DefDef +false +0 +false +def c + +5 +SimpleMethods.scala +covtest +C +Class +covtest.C +d +101 +103 +7 + +Literal +false +0 +false +12 + +6 +SimpleMethods.scala +covtest +C +Class +covtest.C +d +88 +93 +7 +d +DefDef +false +0 +false +def d + +7 +SimpleMethods.scala +covtest +C +Class +covtest.C +e +120 +124 +8 + +Literal +false +0 +false +null + +8 +SimpleMethods.scala +covtest +C +Class +covtest.C +e +106 +111 +8 +e +DefDef +false +0 +false +def e + +9 +SimpleMethods.scala +covtest +C +Class +covtest.C +block +149 +158 +11 + +Literal +false +0 +false +"literal" + +10 +SimpleMethods.scala +covtest +C +Class +covtest.C +block +163 +164 +12 + +Literal +false +0 +false +0 + +11 +SimpleMethods.scala +covtest +C +Class +covtest.C +block +128 +137 +10 +block +DefDef +false +0 +false +def block + +12 +SimpleMethods.scala +covtest +C +Class +covtest.C +cond +195 +200 +15 + +Literal +false +0 +false +false + +13 +SimpleMethods.scala +covtest +C +Class +covtest.C +cond +206 +210 +15 + +Literal +false +0 +false +true + +14 +SimpleMethods.scala +covtest +C +Class +covtest.C +cond +206 +210 +15 + +Literal +true +0 +false +true + +15 +SimpleMethods.scala +covtest +C +Class +covtest.C +cond +220 +225 +16 + +Literal +false +0 +false +false + +16 +SimpleMethods.scala +covtest +C +Class +covtest.C +cond +220 +225 +16 + +Literal +true +0 +false +false + +17 +SimpleMethods.scala +covtest +C +Class +covtest.C +cond +168 +176 +14 +cond +DefDef +false +0 +false +def cond + +18 +SimpleMethods.scala +covtest +C +Class +covtest.C +partialCond +260 +265 +19 + +Literal +false +0 +false +false + +19 +SimpleMethods.scala +covtest +C +Class +covtest.C +partialCond +271 +273 +19 + +Literal +false +0 +false +() + +20 +SimpleMethods.scala +covtest +C +Class +covtest.C +partialCond +271 +273 +19 + +Literal +true +0 +false +() + +21 +SimpleMethods.scala +covtest +C +Class +covtest.C +partialCond +273 +273 +19 + +Literal +true +0 +false + + +22 +SimpleMethods.scala +covtest +C +Class +covtest.C +partialCond +229 +244 +18 +partialCond +DefDef +false +0 +false +def partialCond + +23 +SimpleMethods.scala +covtest +C +Class +covtest.C +new1 +277 +285 +21 +new1 +DefDef +false +0 +false +def new1 + +24 +SimpleMethods.scala +covtest +C +Class +covtest.C +tryCatch +330 +332 +24 + +Literal +false +0 +false +() + +25 +SimpleMethods.scala +covtest +C +Class +covtest.C +tryCatch +330 +332 +24 + +Literal +true +0 +false +() + +26 +SimpleMethods.scala +covtest +C +Class +covtest.C +tryCatch +370 +371 +26 + +Literal +false +0 +false +1 + +27 +SimpleMethods.scala +covtest +C +Class +covtest.C +tryCatch +367 +371 +26 + +Block +true +0 +false +=> 1 + +28 +SimpleMethods.scala +covtest +C +Class +covtest.C +tryCatch +301 +313 +23 +tryCatch +DefDef +false +0 +false +def tryCatch +