From a3eaa64ce70447168cac96fe8c3a3c8deaacb770 Mon Sep 17 00:00:00 2001 From: Mike Bland Date: Wed, 17 Dec 2025 16:19:12 -0500 Subject: [PATCH] Update `-Wconf:src` to match Scala 2 behavior Adds `^` and `$` anchors to the `-Wconf:src` regex to ensure it matches entire path segments by default. Mostly replicates the Scala 2 `src` filter anchoring logic except for the `rootDir` bit, which Scala 3 doesn't support: - https://github.com/mbland/scala/blob/v2.13.18/src/compiler/scala/tools/nsc/Reporting.scala#L862-L875 - https://docs.scala-lang.org/scala3/guides/migration/options-lookup.html Applies the new `anchored` function after parsing the `-Wconf:src` argument as a regex first. This guards against pathological cases of invalid patterns that may become valid after anchoring, such as `\` becoming `/\$`. (One of the existing test cases covers this specific invalid regex case.) Adds new test cases, including cases to validate the existing behavior of normalizing paths without resolving symlinks. This is to help ensure that the Scala 2 issue from scala/bug#13145 (which scala/scala#11192 resolves) doesn't ever appear. Also extracts the `diagnosticWarning`, `virtualFile`, and `plainFile` helper methods to reduce duplication between new and existing test cases. --- .../tools/dotc/config/ScalaSettings.scala | 4 +- .../dotty/tools/dotc/reporting/WConf.scala | 17 ++- .../dotc/config/ScalaSettingsTests.scala | 102 +++++++++++------- 3 files changed, 83 insertions(+), 40 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index 246c7f1600c0..45a5acd6ed78 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -244,7 +244,9 @@ private sealed trait WarningSettings: | The message name is printed with the warning in verbose warning mode. | | - Source location: src=regex - | The regex is evaluated against the full source path. + | The regex must match the canonical path relative to any path segment + | (`b/.*Test.scala` matches `/a/b/XTest.scala` but not `/ab/Test.scala`). + | Use unix-style paths, separated by `/`. | | - Origin of warning: origin=regex | The regex must match the full name (`package.Class.method`) of the deprecated entity. diff --git a/compiler/src/dotty/tools/dotc/reporting/WConf.scala b/compiler/src/dotty/tools/dotc/reporting/WConf.scala index 42258f67938a..1a75bdb9168f 100644 --- a/compiler/src/dotty/tools/dotc/reporting/WConf.scala +++ b/compiler/src/dotty/tools/dotc/reporting/WConf.scala @@ -2,6 +2,7 @@ package dotty.tools package dotc package reporting +import scala.collection.mutable import scala.language.unsafeNulls import dotty.tools.dotc.core.Contexts.* @@ -72,6 +73,20 @@ object WConf: try Right(s.r) catch case e: PatternSyntaxException => Left(s"invalid pattern `$s`: ${e.getMessage}") + // Ensures a filter matches only complete path segments and exact endings. + // + // `parseFilter` invokes `regex(conf).map(anchored)` because the original + // pattern must be a valid regex before adding the anchor characters. + private def anchored(pattern: Regex) = + val orig = pattern.toString + val result = new mutable.StringBuilder + + if (!orig.startsWith("/")) result += '/' + result ++= orig + if (!orig.endsWith("$")) result += '$' + + result.toString.r + @sharable val Splitter = raw"([^=]+)=(.+)".r @sharable val ErrorId = raw"E?(\d+)".r @@ -105,7 +120,7 @@ object WConf: case "unchecked" => Right(Unchecked) case _ => Left(s"unknown category: $conf") - case "src" => regex(conf).map(SourcePattern.apply) + case "src" => regex(conf).map(anchored).map(SourcePattern.apply) case "origin" => regex(conf).map(Origin.apply) case _ => Left(s"unknown filter: $filter") diff --git a/compiler/test/dotty/tools/dotc/config/ScalaSettingsTests.scala b/compiler/test/dotty/tools/dotc/config/ScalaSettingsTests.scala index c36e15a0eb36..22976fc2e059 100644 --- a/compiler/test/dotty/tools/dotc/config/ScalaSettingsTests.scala +++ b/compiler/test/dotty/tools/dotc/config/ScalaSettingsTests.scala @@ -10,8 +10,8 @@ import core.Decorators.toMessage import dotty.tools.io.{Path, PlainFile} import java.net.URI -import java.nio.file.Files -import scala.util.Using +import java.nio.file.{Files, Paths} +import scala.util.{Success, Using} import scala.annotation.nowarn @@ -215,29 +215,28 @@ class ScalaSettingsTests: val wconf = reporting.WConf.fromSettings(wconfStr) wconf.map(_.action(warning)) + private def diagnosticWarning(source: util.SourceFile) = reporting.Diagnostic.Warning( + "A warning".toMessage, + util.SourcePosition(source = source, span = util.Spans.Span(1L)) + ) + + private def virtualFile(uri: String) = + util.SourceFile.virtual(new URI(uri), "") + + private def plainFile(path: Path) = + util.SourceFile(new PlainFile(path), "UTF-8") + @Test def `WConf src filter silences warnings from a matching path for virtual file`: Unit = val result = wconfSrcFilterTest( argsStr = "-Wconf:src=path/.*:s", - warning = reporting.Diagnostic.Warning( - "A warning".toMessage, - util.SourcePosition( - source = util.SourceFile.virtual(new URI("file:///some/path/file.scala"), ""), - span = util.Spans.Span(1L) - ) - ) + warning = diagnosticWarning(virtualFile("file:///some/path/file.scala")) ) assertEquals(result, Right(reporting.Action.Silent)) @Test def `WConf src filter doesn't silence warnings from a non-matching path`: Unit = val result = wconfSrcFilterTest( argsStr = "-Wconf:src=another/.*:s", - warning = reporting.Diagnostic.Warning( - "A warning".toMessage, - util.SourcePosition( - source = util.SourceFile.virtual(new URI("file:///some/path/file.scala"), ""), - span = util.Spans.Span(1L) - ) - ) + warning = diagnosticWarning(virtualFile("file:///some/path/file.scala")) ) assertEquals(result, Right(reporting.Action.Warning)) @@ -245,13 +244,7 @@ class ScalaSettingsTests: val result = Using.resource(Files.createTempFile("myfile", ".scala").nn) { file => wconfSrcFilterTest( argsStr = "-Wconf:src=myfile.*?\\.scala:s", - warning = reporting.Diagnostic.Warning( - "A warning".toMessage, - util.SourcePosition( - source = util.SourceFile(new PlainFile(Path(file)), "UTF-8"), - span = util.Spans.Span(1L) - ) - ) + warning = diagnosticWarning(plainFile(Path(file))) ) }(using Files.deleteIfExists(_)) assertEquals(result, Right(reporting.Action.Silent)) @@ -260,27 +253,60 @@ class ScalaSettingsTests: val result = Using.resource(Files.createTempFile("myfile", ".scala").nn) { file => wconfSrcFilterTest( argsStr = "-Wconf:src=another.*?\\.scala:s", - warning = reporting.Diagnostic.Warning( - "A warning".toMessage, - util.SourcePosition( - source = util.SourceFile(new PlainFile(Path(file)), "UTF-8"), - span = util.Spans.Span(1L) - ) - ) + warning = diagnosticWarning(plainFile(Path(file))) ) }(using Files.deleteIfExists(_)) assertEquals(result, Right(reporting.Action.Warning)) + @Test def `Wconf src filter matches symbolic links without resolving them`: Unit = + val result = Using.Manager { use => + def f(file: java.nio.file.Path) = use(file)(using Files.deleteIfExists(_)) + + val tempDir = f(Files.createTempDirectory("wconf-src-symlink-test")) + val externalDir = f(Files.createDirectory(Paths.get(tempDir.toString, "external"))) + val cacheDir = f(Files.createDirectory(Paths.get(tempDir.toString, "cache"))) + val actualFile = f(Files.createFile(Paths.get(cacheDir.toString, "myfile.scala"))) + val symlinkPath = Paths.get(externalDir.toString, "myfile.scala") + + // This may fail with an IOException if symlinks are disabled on Windows: + // https://docs.oracle.com/javase/8/docs/api/java/nio/file/Files.html#createSymbolicLink-java.nio.file.Path-java.nio.file.Path-java.nio.file.attribute.FileAttribute...- + val symlink = f(Files.createSymbolicLink(symlinkPath, actualFile)) + + wconfSrcFilterTest( + argsStr = "-Wconf:src=external/.*\\.scala:s", + warning = diagnosticWarning(plainFile(Path(symlink))) + ) + } + assertEquals(result, Success(Right(reporting.Action.Silent))) + + @Test def `Wconf src filter normalizes paths`: Unit = + val path = Path("foo/./bar/../quux/../baz/File.scala") + val result = wconfSrcFilterTest( + argsStr = "-Wconf:src=foo/baz/.*\\.scala:s", + warning = diagnosticWarning(plainFile(path)) + ) + assertEquals(result, Right(reporting.Action.Silent)) + + @Test def `Wconf src filter only matches entire directory path components`: Unit = + val path = Path("foobar/File.scala") + val result = wconfSrcFilterTest( + argsStr = "-Wconf:src=bar/.*\\.scala:s", + warning = diagnosticWarning(plainFile(path)) + ) + assertEquals(result, Right(reporting.Action.Warning)) + + @Test def `Wconf src filter only matches exact path endings`: Unit = + val path = Path("foobar/File.scala++") + val result = wconfSrcFilterTest( + argsStr = "-Wconf:src=foobar/.*\\.scala:s", + warning = diagnosticWarning(plainFile(path)) + ) + assertEquals(result, Right(reporting.Action.Warning)) + @Test def `WConf src filter reports an error on an invalid regex`: Unit = val result = wconfSrcFilterTest( argsStr = """-Wconf:src=\:s""", - warning = reporting.Diagnostic.Warning( - "A warning".toMessage, - util.SourcePosition( - source = util.SourceFile.virtual(new URI("file:///some/path/file.scala"), ""), - span = util.Spans.Span(1L) - ) - ), + warning = diagnosticWarning(virtualFile("file:///some/path/file.scala")) ) assertTrue( result.left.exists(errors => @@ -294,7 +320,7 @@ class ScalaSettingsTests: warning = reporting.Diagnostic.DeprecationWarning( "A warning".toMessage, util.SourcePosition( - source = util.SourceFile.virtual(new URI("file:///some/path/file.scala"), ""), + source = virtualFile("file:///some/path/file.scala"), span = util.Spans.Span(1L) ), origin="",