From 3686ba06164d4db25cc12b2d688ea680f8864fab Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Tue, 15 Aug 2023 17:55:01 +0100 Subject: [PATCH] Vulpix: implement "// warn" tags Also, show compilation errors, if there are any, for a warn test. --- .../tools/dotc/reporting/TestReporter.scala | 16 +++-- .../dotty/tools/vulpix/ParallelTesting.scala | 68 +++++++++++++++++++ tests/warn/i11178.scala | 11 ++- tests/warn/i16451.check | 8 +-- tests/warn/i16451.scala | 8 +-- tests/warn/i5826.check | 18 ++--- tests/warn/i5826.scala | 9 ++- tests/warn/i8932.scala | 4 +- .../warn/suppressed-type-test-warnings.scala | 8 +-- 9 files changed, 105 insertions(+), 45 deletions(-) diff --git a/compiler/test/dotty/tools/dotc/reporting/TestReporter.scala b/compiler/test/dotty/tools/dotc/reporting/TestReporter.scala index 940fc875a021..c0ece68e3b46 100644 --- a/compiler/test/dotty/tools/dotc/reporting/TestReporter.scala +++ b/compiler/test/dotty/tools/dotc/reporting/TestReporter.scala @@ -18,11 +18,12 @@ import interfaces.Diagnostic.{ERROR, WARNING} import scala.io.Codec -class TestReporter protected (outWriter: PrintWriter, filePrintln: String => Unit, logLevel: Int) +class TestReporter protected (outWriter: PrintWriter, logLevel: Int) extends Reporter with UniqueMessagePositions with HideNonSensicalMessages with MessageRendering { - protected final val _errorBuf = mutable.ArrayBuffer.empty[Diagnostic] - final def errors: Iterator[Diagnostic] = _errorBuf.iterator + protected final val _diagnosticBuf = mutable.ArrayBuffer.empty[Diagnostic] + final def diagnostics: Iterator[Diagnostic] = _diagnosticBuf.iterator + final def errors: Iterator[Diagnostic] = diagnostics.filter(_.level >= ERROR) protected final val _messageBuf = mutable.ArrayBuffer.empty[String] final def messages: Iterator[String] = _messageBuf.iterator @@ -79,8 +80,9 @@ extends Reporter with UniqueMessagePositions with HideNonSensicalMessages with M case _ => "" } - if dia.level >= ERROR then _errorBuf.append(dia) - if dia.level >= WARNING then _consoleReporter.doReport(dia) + if dia.level >= WARNING then + _diagnosticBuf.append(dia) + _consoleReporter.doReport(dia) printMessageAndPos(dia, extra) } } @@ -125,10 +127,10 @@ object TestReporter { } def reporter(ps: PrintStream, logLevel: Int): TestReporter = - new TestReporter(new PrintWriter(ps, true), logPrintln, logLevel) + new TestReporter(new PrintWriter(ps, true), logLevel) def simplifiedReporter(writer: PrintWriter): TestReporter = { - val rep = new TestReporter(writer, logPrintln, WARNING) { + val rep = new TestReporter(writer, WARNING) { /** Prints the message with the given position indication in a simplified manner */ override def printMessageAndPos(dia: Diagnostic, extra: String)(using Context): Unit = { def report() = { diff --git a/compiler/test/dotty/tools/vulpix/ParallelTesting.scala b/compiler/test/dotty/tools/vulpix/ParallelTesting.scala index bcf17c37fa0b..4e6fe67aec37 100644 --- a/compiler/test/dotty/tools/vulpix/ParallelTesting.scala +++ b/compiler/test/dotty/tools/vulpix/ParallelTesting.scala @@ -729,6 +729,74 @@ trait ParallelTesting extends RunnerOrchestration { self => override def onSuccess(testSource: TestSource, reporters: Seq[TestReporter], logger: LoggedRunnable): Unit = diffCheckfile(testSource, reporters, logger) + override def maybeFailureMessage(testSource: TestSource, reporters: Seq[TestReporter]): Option[String] = + lazy val (map, expCount) = getWarnMapAndExpectedCount(testSource.sourceFiles.toIndexedSeq) + lazy val obtCount = reporters.foldLeft(0)(_ + _.warningCount) + lazy val (expected, unexpected) = getMissingExpectedWarnings(map, reporters.iterator.flatMap(_.diagnostics)) + def hasMissingAnnotations = expected.nonEmpty || unexpected.nonEmpty + def showDiagnostics = "-> following the diagnostics:\n" + + reporters.flatMap(_.diagnostics.toSeq.sortBy(_.pos.line).map(e => s"${e.pos.line + 1}: ${e.message}")).mkString(" at ", "\n at ", "") + Option: + if reporters.exists(_.compilerCrashed) then s"Compiler crashed when compiling: ${testSource.title}" + else if reporters.exists(_.errorCount > 0) then + s"""Compilation failed for: ${testSource.title} + |$showDiagnostics + |""".stripMargin.trim.linesIterator.mkString("\n", "\n", "") + else if obtCount == 0 then s"\nNo warnings found when compiling warn test $testSource" + else if expCount == 0 then s"\nNo warning expected/defined in $testSource -- use // warn" + else if expCount != obtCount then + s"""|Wrong number of warnings encountered when compiling $testSource + |expected: $expCount, actual: $obtCount + |${expected.mkString("Unfulfilled expectations:\n", "\n", "")} + |${unexpected.mkString("Unexpected warnings:\n", "\n", "")} + |$showDiagnostics + |""".stripMargin.trim.linesIterator.mkString("\n", "\n", "") + else if hasMissingAnnotations then s"\nWarnings found on incorrect row numbers when compiling $testSource\n$showDiagnostics" + else if !map.isEmpty then s"\nExpected warnings(s) have {=}: $map" + else null + end maybeFailureMessage + + def getWarnMapAndExpectedCount(files: Seq[JFile]): (HashMap[String, Integer], Int) = + val comment = raw"//( *)warn".r + val map = new HashMap[String, Integer]() + var count = 0 + def bump(key: String): Unit = + map.get(key) match + case null => map.put(key, 1) + case n => map.put(key, n+1) + count += 1 + files.filter(isSourceFile).foreach { file => + Using(Source.fromFile(file, StandardCharsets.UTF_8.name)) { source => + source.getLines.zipWithIndex.foreach { case (line, lineNbr) => + comment.findAllMatchIn(line).foreach { _ => + bump(s"${file.getPath}:${lineNbr+1}") + } + } + }.get + } + (map, count) + + def getMissingExpectedWarnings(map: HashMap[String, Integer], reporterWarnings: Iterator[Diagnostic]): (List[String], List[String]) = + val unexpected, unpositioned = ListBuffer.empty[String] + def relativize(path: String): String = path.split(JFile.separatorChar).dropWhile(_ != "tests").mkString(JFile.separator) + def seenAt(key: String): Boolean = + map.get(key) match + case null => false + case 1 => map.remove(key) ; true + case n => map.put(key, n - 1) ; true + def sawDiagnostic(d: Diagnostic): Unit = + val srcpos = d.pos.nonInlined + if srcpos.exists then + val key = s"${relativize(srcpos.source.file.toString())}:${srcpos.line + 1}" + if !seenAt(key) then unexpected += key + else + unpositioned += relativize(srcpos.source.file.toString()) + + reporterWarnings.foreach(sawDiagnostic) + + (map.asScala.keys.toList, (unexpected ++ unpositioned).toList) + end getMissingExpectedWarnings + private final class RewriteTest(testSources: List[TestSource], checkFiles: Map[JFile, JFile], times: Int, threadLimit: Option[Int], suppressAllOutput: Boolean)(implicit summaryReport: SummaryReporting) extends Test(testSources, times, threadLimit, suppressAllOutput) { private def verifyOutput(testSource: TestSource, reporters: Seq[TestReporter], logger: LoggedRunnable) = { diff --git a/tests/warn/i11178.scala b/tests/warn/i11178.scala index 47e8b4c3acab..a59b899be365 100644 --- a/tests/warn/i11178.scala +++ b/tests/warn/i11178.scala @@ -3,7 +3,7 @@ case class Foo[+S](s: S) extends Box[S] def unwrap2[A](b: Box[A]): A = b match - case _: Foo[Int] => 0 // error + case _: Foo[Int] => 0 // warn object Test1 { // Invariant case, OK @@ -11,8 +11,7 @@ object Test1 { def test[A](bar: Bar[A]) = bar match { - case _: Bar[Boolean] => ??? // error - case _ => ??? + case _: Bar[Boolean] => ??? // warn } } @@ -22,8 +21,7 @@ object Test2 { def test[A](bar: Bar[A]) = bar match { - case _: Bar[Boolean] => ??? // error - case _ => ??? + case _: Bar[Boolean] => ??? // warn } } @@ -33,7 +31,6 @@ object Test3 { def test[A](bar: Bar[A]) = bar match { - case _: Bar[Boolean] => ??? // error - case _ => ??? + case _: Bar[Boolean] => ??? // warn } } diff --git a/tests/warn/i16451.check b/tests/warn/i16451.check index a966b5c85be4..09c2a7df8179 100644 --- a/tests/warn/i16451.check +++ b/tests/warn/i16451.check @@ -19,25 +19,25 @@ | | longer explanation available when compiling with `-explain` -- [E092] Pattern Match Unchecked Warning: tests/warn/i16451.scala:25:9 ------------------------------------------------ -25 | case x: Wrapper[Color.Red.type] => Some(x) // error: unreachable // error: unchecked +25 | case x: Wrapper[Color.Red.type] => Some(x) // warn: unchecked | ^ |the type test for Wrapper[(Color.Red : Color)] cannot be checked at runtime because its type arguments can't be determined from Wrapper[Color] | | longer explanation available when compiling with `-explain` -- [E092] Pattern Match Unchecked Warning: tests/warn/i16451.scala:29:9 ------------------------------------------------ -29 | case x: Wrapper[Color.Red.type] => Some(x) +29 | case x: Wrapper[Color.Red.type] => Some(x) // warn: unchecked | ^ |the type test for Wrapper[(Color.Red : Color)] cannot be checked at runtime because its type arguments can't be determined from A1 | | longer explanation available when compiling with `-explain` -- [E092] Pattern Match Unchecked Warning: tests/warn/i16451.scala:34:11 ----------------------------------------------- -34 | case x: Wrapper[Color.Red.type] => x +34 | case x: Wrapper[Color.Red.type] => x // warn: unchecked | ^ |the type test for Wrapper[(Color.Red : Color)] cannot be checked at runtime because its type arguments can't be determined from Wrapper[Color] | | longer explanation available when compiling with `-explain` -- [E092] Pattern Match Unchecked Warning: tests/warn/i16451.scala:39:11 ----------------------------------------------- -39 | case x: Wrapper[Color.Red.type] => x +39 | case x: Wrapper[Color.Red.type] => x // warn: unchecked | ^ |the type test for Wrapper[(Color.Red : Color)] cannot be checked at runtime because its type arguments can't be determined from Wrapper[Color] | diff --git a/tests/warn/i16451.scala b/tests/warn/i16451.scala index 1a83d56366f6..138af3632772 100644 --- a/tests/warn/i16451.scala +++ b/tests/warn/i16451.scala @@ -22,21 +22,21 @@ object Test: case x: Wrapper[Color.Green.type] => None // warn: unreachable // also: unchecked (hidden) def test_wrong(x: Wrapper[Color]): Option[Wrapper[Color.Red.type]] = x match - case x: Wrapper[Color.Red.type] => Some(x) // error: unreachable // error: unchecked + case x: Wrapper[Color.Red.type] => Some(x) // warn: unchecked case null => None def t2[A1 <: Wrapper[Color]](x: A1): Option[Wrapper[Color.Red.type]] = x match - case x: Wrapper[Color.Red.type] => Some(x) + case x: Wrapper[Color.Red.type] => Some(x) // warn: unchecked case null => None def test_wrong_seq(xs: Seq[Wrapper[Color]]): Seq[Wrapper[Color.Red.type]] = xs.collect { - case x: Wrapper[Color.Red.type] => x + case x: Wrapper[Color.Red.type] => x // warn: unchecked } def test_wrong_seq2(xs: Seq[Wrapper[Color]]): Seq[Wrapper[Color.Red.type]] = xs.collect { x => x match - case x: Wrapper[Color.Red.type] => x + case x: Wrapper[Color.Red.type] => x // warn: unchecked } def main(args: Array[String]): Unit = diff --git a/tests/warn/i5826.check b/tests/warn/i5826.check index d29df1c8f34b..18ff50a933cb 100644 --- a/tests/warn/i5826.check +++ b/tests/warn/i5826.check @@ -1,9 +1,5 @@ --- [E121] Pattern Match Warning: tests/warn/i5826.scala:9:9 ------------------------------------------------------------ -9 | case _ => 0 // warn: unreachable-only-null - | ^ - | Unreachable case except for null (if this is intentional, consider writing case null => instead). -- [E092] Pattern Match Unchecked Warning: tests/warn/i5826.scala:3:9 -------------------------------------------------- -3 | case ls: List[Int] => ls.head // error, A = List[String] +3 | case ls: List[Int] => ls.head // warn: unchecked | ^ | the type test for List[Int] cannot be checked at runtime because its type arguments can't be determined from A | @@ -14,20 +10,20 @@ |the type test for List[Int] cannot be checked at runtime because its type arguments can't be determined from List[String] | | longer explanation available when compiling with `-explain` --- [E092] Pattern Match Unchecked Warning: tests/warn/i5826.scala:17:9 ------------------------------------------------- -17 | case ls: A[X] => 4 // error +-- [E092] Pattern Match Unchecked Warning: tests/warn/i5826.scala:16:9 ------------------------------------------------- +16 | case ls: A[X] => 4 // warn | ^ |the type test for Foo.this.A[X] cannot be checked at runtime because its type arguments can't be determined from Foo.this.B[X] | | longer explanation available when compiling with `-explain` --- [E092] Pattern Match Unchecked Warning: tests/warn/i5826.scala:22:9 ------------------------------------------------- -22 | case ls: List[Int] => ls.head // error, List extends Int => T +-- [E092] Pattern Match Unchecked Warning: tests/warn/i5826.scala:21:9 ------------------------------------------------- +21 | case ls: List[Int] => ls.head // warn, List extends Int => T | ^ |the type test for List[Int] cannot be checked at runtime because its type arguments can't be determined from A => Int | | longer explanation available when compiling with `-explain` --- [E092] Pattern Match Unchecked Warning: tests/warn/i5826.scala:28:54 ------------------------------------------------ -28 | def test5[T](x: A[T] | B[T] | Option[T]): Boolean = x.isInstanceOf[C[String]] // error +-- [E092] Pattern Match Unchecked Warning: tests/warn/i5826.scala:27:54 ------------------------------------------------ +27 | def test5[T](x: A[T] | B[T] | Option[T]): Boolean = x.isInstanceOf[C[String]] // warn | ^ |the type test for Foo.this.C[String] cannot be checked at runtime because its type arguments can't be determined from Foo.this.A[T] | diff --git a/tests/warn/i5826.scala b/tests/warn/i5826.scala index e7eda8826139..f54d6e58d033 100644 --- a/tests/warn/i5826.scala +++ b/tests/warn/i5826.scala @@ -1,12 +1,11 @@ class Foo { def test[A]: (List[Int] | A) => Int = { - case ls: List[Int] => ls.head // error, A = List[String] + case ls: List[Int] => ls.head // warn: unchecked case _ => 0 } def test2: List[Int] | List[String] => Int = { case ls: List[Int] => ls.head // warn: unchecked - case _ => 0 // warn: unreachable-only-null } trait A[T] @@ -14,18 +13,18 @@ class Foo { // suppose: class C extends A[Int] with B[String] def test3[X]: A[X] | B[X] => Int = { - case ls: A[X] => 4 // error + case ls: A[X] => 4 // warn case _ => 0 } def test4[A](x: List[Int] | (A => Int)) = x match { - case ls: List[Int] => ls.head // error, List extends Int => T + case ls: List[Int] => ls.head // warn, List extends Int => T case _ => 0 } final class C[T] extends A[T] - def test5[T](x: A[T] | B[T] | Option[T]): Boolean = x.isInstanceOf[C[String]] // error + def test5[T](x: A[T] | B[T] | Option[T]): Boolean = x.isInstanceOf[C[String]] // warn def test6[T](x: A[T] | B[T] | Option[T]): Boolean = x.isInstanceOf[C[T]] diff --git a/tests/warn/i8932.scala b/tests/warn/i8932.scala index 84d2f7d4990a..95a4e86e9791 100644 --- a/tests/warn/i8932.scala +++ b/tests/warn/i8932.scala @@ -5,8 +5,8 @@ class Dummy extends Bar[Nothing] with Foo[String] def bugReport[A](foo: Foo[A]): Foo[A] = foo match { - case bar: Bar[A] => bar // error - case dummy: Dummy => ??? + case bar: Bar[A] => bar // warn: unchecked + case dummy: Dummy => ??? // warn: unreachable } def test = bugReport(new Dummy: Foo[String]) diff --git a/tests/warn/suppressed-type-test-warnings.scala b/tests/warn/suppressed-type-test-warnings.scala index 92d86b3307e5..c78e8e263153 100644 --- a/tests/warn/suppressed-type-test-warnings.scala +++ b/tests/warn/suppressed-type-test-warnings.scala @@ -11,19 +11,17 @@ object Test { } def err1[A, B](value: Foo[A, B], a: A => Int): B = value match { - case b: Bar[A] => // spurious // error + case b: Bar[A] => // spurious // warn b.x } def err2[A, B](value: Foo[A, B], a: A => Int): B = value match { - case b: Bar[B] => // spurious // error + case b: Bar[B] => // spurious // warn b.x - case _ => ??? // avoid fatal inexhaustivity warnings suppressing the uncheckable warning } def fail[A, B](value: Foo[A, B], a: A => Int): B = value match { - case b: Bar[Int] => // error + case b: Bar[Int] => // warn b.x - case _ => ??? // avoid fatal inexhaustivity warnings suppressing the uncheckable warning } }