Skip to content

Commit c0da8f9

Browse files
Backport "Unsuppress unchecked warnings" to LTS (#20669)
Backports #18377 to the LTS branch. PR submitted by the release tooling. [skip ci]
2 parents 77b782e + dcbc291 commit c0da8f9

24 files changed

+323
-155
lines changed

compiler/src/dotty/tools/dotc/reporting/Message.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ abstract class Message(val errorId: ErrorMessageID)(using Context) { self =>
377377
override def canExplain = true
378378

379379
/** Override with `true` for messages that should always be shown even if their
380-
* position overlaps another messsage of a different class. On the other hand
380+
* position overlaps another message of a different class. On the other hand
381381
* multiple messages of the same class with overlapping positions will lead
382382
* to only a single message of that class to be issued.
383383
*/

compiler/src/dotty/tools/dotc/reporting/messages.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -895,9 +895,9 @@ extends Message(PatternMatchExhaustivityID) {
895895
}
896896
}
897897

898-
class UncheckedTypePattern(msgFn: => String)(using Context)
898+
class UncheckedTypePattern(argType: Type, whyNot: String)(using Context)
899899
extends PatternMatchMsg(UncheckedTypePatternID) {
900-
def msg(using Context) = msgFn
900+
def msg(using Context) = i"the type test for $argType cannot be checked at runtime because $whyNot"
901901
def explain(using Context) =
902902
i"""|Type arguments and type refinements are erased during compile time, thus it's
903903
|impossible to check them at run-time.

compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala

+3-3
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ object TypeTestsCasts {
7474
}.apply(tp)
7575

7676
/** Returns true if the type arguments of `P` can be determined from `X` */
77-
def typeArgsTrivial(X: Type, P: AppliedType)(using Context) = inContext(ctx.fresh.setExploreTyperState().setFreshGADTBounds) {
77+
def typeArgsDeterminable(X: Type, P: AppliedType)(using Context) = inContext(ctx.fresh.setExploreTyperState().setFreshGADTBounds) {
7878
val AppliedType(tycon, _) = P
7979

8080
def underlyingLambda(tp: Type): TypeLambda = tp.ensureLambdaSub match {
@@ -155,7 +155,7 @@ object TypeTestsCasts {
155155
case x =>
156156
// always false test warnings are emitted elsewhere
157157
TypeComparer.provablyDisjoint(x, tpe.derivedAppliedType(tycon, targs.map(_ => WildcardType)))
158-
|| typeArgsTrivial(X, tpe)
158+
|| typeArgsDeterminable(X, tpe)
159159
||| i"its type arguments can't be determined from $X"
160160
}
161161
case AndType(tp1, tp2) => recur(X, tp1) && recur(X, tp2)
@@ -363,7 +363,7 @@ object TypeTestsCasts {
363363
if !isTrusted && !isUnchecked then
364364
val whyNot = whyUncheckable(expr.tpe, argType, tree.span)
365365
if whyNot.nonEmpty then
366-
report.uncheckedWarning(em"the type test for $argType cannot be checked at runtime because $whyNot", expr.srcPos)
366+
report.uncheckedWarning(UncheckedTypePattern(argType, whyNot), expr.srcPos)
367367
transformTypeTest(expr, argType,
368368
flagUnrelated = enclosingInlineds.isEmpty) // if test comes from inlined code, dont't flag it even if it always false
369369
}

compiler/src/dotty/tools/dotc/transform/patmat/Space.scala

+11-8
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ object SpaceEngine {
394394
project(pat)
395395

396396
case Typed(_, tpt) =>
397-
Typ(erase(tpt.tpe.stripAnnots, isValue = true), decomposed = false)
397+
Typ(erase(tpt.tpe.stripAnnots, isValue = true, isTyped = true), decomposed = false)
398398

399399
case This(_) =>
400400
Typ(pat.tpe.stripAnnots, decomposed = false)
@@ -458,28 +458,31 @@ object SpaceEngine {
458458
*
459459
* @param inArray whether `tp` is a type argument to `Array`
460460
* @param isValue whether `tp` is the type which match against values
461+
* @param isTyped whether `tp` is the type from a `Typed` tree
461462
*
462463
* If `isValue` is true, then pattern-bound symbols are erased to its upper bound.
463464
* This is needed to avoid spurious unreachable warnings. See tests/patmat/i6197.scala.
464465
*/
465-
private def erase(tp: Type, inArray: Boolean = false, isValue: Boolean = false)(using Context): Type =
466-
trace(i"erase($tp${if inArray then " inArray" else ""}${if isValue then " isValue" else ""})", debug)(tp match {
466+
private def erase(tp: Type, inArray: Boolean = false, isValue: Boolean = false, isTyped: Boolean = false)(using Context): Type =
467+
trace(i"erase($tp${if inArray then " inArray" else ""}${if isValue then " isValue" else ""}${if isTyped then " isTyped" else ""})", debug)(tp match {
467468
case tp @ AppliedType(tycon, args) if tycon.typeSymbol.isPatternBound =>
468469
WildcardType
469470

470471
case tp @ AppliedType(tycon, args) =>
471-
val inArray = tycon.isRef(defn.ArrayClass)
472-
val args2 = args.map(arg => erase(arg, inArray = inArray, isValue = false))
472+
val inArray = tycon.isRef(defn.ArrayClass) || tp.translucentSuperType.isRef(defn.ArrayClass)
473+
val args2 =
474+
if isTyped && !inArray then args.map(_ => WildcardType)
475+
else args.map(arg => erase(arg, inArray = inArray, isValue = false))
473476
tp.derivedAppliedType(erase(tycon, inArray, isValue = false), args2)
474477

475478
case tp @ OrType(tp1, tp2) =>
476-
OrType(erase(tp1, inArray, isValue), erase(tp2, inArray, isValue), tp.isSoft)
479+
OrType(erase(tp1, inArray, isValue, isTyped), erase(tp2, inArray, isValue, isTyped), tp.isSoft)
477480

478481
case AndType(tp1, tp2) =>
479-
AndType(erase(tp1, inArray, isValue), erase(tp2, inArray, isValue))
482+
AndType(erase(tp1, inArray, isValue, isTyped), erase(tp2, inArray, isValue, isTyped))
480483

481484
case tp @ RefinedType(parent, _, _) =>
482-
erase(parent, inArray, isValue)
485+
erase(parent, inArray, isValue, isTyped)
483486

484487
case tref: TypeRef if tref.symbol.isPatternBound =>
485488
if inArray then tref.underlying

compiler/test/dotty/tools/dotc/CompilationTests.scala

+9
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,15 @@ class CompilationTests {
108108
).times(2).checkCompile()
109109
}
110110

111+
// Warning tests ------------------------------------------------------------
112+
113+
@Test def warn: Unit = {
114+
implicit val testGroup: TestGroup = TestGroup("compileWarn")
115+
aggregateTests(
116+
compileFilesInDir("tests/warn", defaultOptions),
117+
).checkWarnings()
118+
}
119+
111120
// Negative tests ------------------------------------------------------------
112121

113122
@Test def negAll: Unit = {

compiler/test/dotty/tools/dotc/reporting/TestReporter.scala

+9-7
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ import interfaces.Diagnostic.{ERROR, WARNING}
1818

1919
import scala.io.Codec
2020

21-
class TestReporter protected (outWriter: PrintWriter, filePrintln: String => Unit, logLevel: Int)
21+
class TestReporter protected (outWriter: PrintWriter, logLevel: Int)
2222
extends Reporter with UniqueMessagePositions with HideNonSensicalMessages with MessageRendering {
2323

24-
protected final val _errorBuf = mutable.ArrayBuffer.empty[Diagnostic]
25-
final def errors: Iterator[Diagnostic] = _errorBuf.iterator
24+
protected final val _diagnosticBuf = mutable.ArrayBuffer.empty[Diagnostic]
25+
final def diagnostics: Iterator[Diagnostic] = _diagnosticBuf.iterator
26+
final def errors: Iterator[Diagnostic] = diagnostics.filter(_.level >= ERROR)
2627

2728
protected final val _messageBuf = mutable.ArrayBuffer.empty[String]
2829
final def messages: Iterator[String] = _messageBuf.iterator
@@ -79,8 +80,9 @@ extends Reporter with UniqueMessagePositions with HideNonSensicalMessages with M
7980
case _ => ""
8081
}
8182

82-
if dia.level >= ERROR then _errorBuf.append(dia)
83-
if dia.level >= WARNING then _consoleReporter.doReport(dia)
83+
if dia.level >= WARNING then
84+
_diagnosticBuf.append(dia)
85+
_consoleReporter.doReport(dia)
8486
printMessageAndPos(dia, extra)
8587
}
8688
}
@@ -125,10 +127,10 @@ object TestReporter {
125127
}
126128

127129
def reporter(ps: PrintStream, logLevel: Int): TestReporter =
128-
new TestReporter(new PrintWriter(ps, true), logPrintln, logLevel)
130+
new TestReporter(new PrintWriter(ps, true), logLevel)
129131

130132
def simplifiedReporter(writer: PrintWriter): TestReporter = {
131-
val rep = new TestReporter(writer, logPrintln, WARNING) {
133+
val rep = new TestReporter(writer, WARNING) {
132134
/** Prints the message with the given position indication in a simplified manner */
133135
override def printMessageAndPos(dia: Diagnostic, extra: String)(using Context): Unit = {
134136
def report() = {

compiler/test/dotty/tools/vulpix/ParallelTesting.scala

+117-49
Original file line numberDiff line numberDiff line change
@@ -226,14 +226,14 @@ trait ParallelTesting extends RunnerOrchestration { self =>
226226
Try(testSource match {
227227
case testSource @ JointCompilationSource(name, files, flags, outDir, fromTasty, decompilation) =>
228228
val reporter =
229-
if (fromTasty) compileFromTasty(flags, suppressErrors, outDir)
230-
else compile(testSource.sourceFiles, flags, suppressErrors, outDir)
229+
if (fromTasty) compileFromTasty(flags, outDir)
230+
else compile(testSource.sourceFiles, flags, outDir)
231231
List(reporter)
232232

233233
case testSource @ SeparateCompilationSource(_, dir, flags, outDir) =>
234234
testSource.compilationGroups.map { (group, files) =>
235235
if group.compiler.isEmpty then
236-
compile(files, flags, suppressErrors, outDir)
236+
compile(files, flags, outDir)
237237
else
238238
compileWithOtherCompiler(group.compiler, files, flags, outDir)
239239
}
@@ -469,7 +469,7 @@ trait ParallelTesting extends RunnerOrchestration { self =>
469469
registerCompletion()
470470
throw e
471471

472-
protected def compile(files0: Array[JFile], flags0: TestFlags, suppressErrors: Boolean, targetDir: JFile): TestReporter = {
472+
protected def compile(files0: Array[JFile], flags0: TestFlags, targetDir: JFile): TestReporter = {
473473
import scala.util.Properties.*
474474

475475
def flattenFiles(f: JFile): Array[JFile] =
@@ -634,7 +634,7 @@ trait ParallelTesting extends RunnerOrchestration { self =>
634634

635635
reporter
636636

637-
protected def compileFromTasty(flags0: TestFlags, suppressErrors: Boolean, targetDir: JFile): TestReporter = {
637+
protected def compileFromTasty(flags0: TestFlags, targetDir: JFile): TestReporter = {
638638
val tastyOutput = new JFile(targetDir.getPath + "_from-tasty")
639639
tastyOutput.mkdir()
640640
val flags = flags0 and ("-d", tastyOutput.getPath) and "-from-tasty"
@@ -653,6 +653,12 @@ trait ParallelTesting extends RunnerOrchestration { self =>
653653
private def mkLogLevel = if suppressErrors || suppressAllOutput then ERROR + 1 else ERROR
654654
private def mkReporter = TestReporter.reporter(realStdout, logLevel = mkLogLevel)
655655

656+
protected def diffCheckfile(testSource: TestSource, reporters: Seq[TestReporter], logger: LoggedRunnable) =
657+
checkFile(testSource).foreach(diffTest(testSource, _, reporterOutputLines(reporters), reporters, logger))
658+
659+
private def reporterOutputLines(reporters: Seq[TestReporter]): List[String] =
660+
reporters.flatMap(_.consoleOutput.split("\n")).toList
661+
656662
private[ParallelTesting] def executeTestSuite(): this.type = {
657663
assert(testSourcesCompleted == 0, "not allowed to re-use a `CompileRun`")
658664
if filteredSources.nonEmpty then
@@ -717,6 +723,80 @@ trait ParallelTesting extends RunnerOrchestration { self =>
717723
private final class PosTest(testSources: List[TestSource], times: Int, threadLimit: Option[Int], suppressAllOutput: Boolean)(implicit summaryReport: SummaryReporting)
718724
extends Test(testSources, times, threadLimit, suppressAllOutput)
719725

726+
private final class WarnTest(testSources: List[TestSource], times: Int, threadLimit: Option[Int], suppressAllOutput: Boolean)(implicit summaryReport: SummaryReporting)
727+
extends Test(testSources, times, threadLimit, suppressAllOutput):
728+
override def suppressErrors = true
729+
override def onSuccess(testSource: TestSource, reporters: Seq[TestReporter], logger: LoggedRunnable): Unit =
730+
diffCheckfile(testSource, reporters, logger)
731+
732+
override def maybeFailureMessage(testSource: TestSource, reporters: Seq[TestReporter]): Option[String] =
733+
lazy val (map, expCount) = getWarnMapAndExpectedCount(testSource.sourceFiles.toIndexedSeq)
734+
lazy val obtCount = reporters.foldLeft(0)(_ + _.warningCount)
735+
lazy val (expected, unexpected) = getMissingExpectedWarnings(map, reporters.iterator.flatMap(_.diagnostics))
736+
def hasMissingAnnotations = expected.nonEmpty || unexpected.nonEmpty
737+
def showDiagnostics = "-> following the diagnostics:\n" +
738+
reporters.flatMap(_.diagnostics.toSeq.sortBy(_.pos.line).map(e => s"${e.pos.line + 1}: ${e.message}")).mkString(" at ", "\n at ", "")
739+
Option:
740+
if reporters.exists(_.compilerCrashed) then s"Compiler crashed when compiling: ${testSource.title}"
741+
else if reporters.exists(_.errorCount > 0) then
742+
s"""Compilation failed for: ${testSource.title}
743+
|$showDiagnostics
744+
|""".stripMargin.trim.linesIterator.mkString("\n", "\n", "")
745+
else if obtCount == 0 then s"\nNo warnings found when compiling warn test $testSource"
746+
else if expCount == 0 then s"\nNo warning expected/defined in $testSource -- use // warn"
747+
else if expCount != obtCount then
748+
s"""|Wrong number of warnings encountered when compiling $testSource
749+
|expected: $expCount, actual: $obtCount
750+
|${expected.mkString("Unfulfilled expectations:\n", "\n", "")}
751+
|${unexpected.mkString("Unexpected warnings:\n", "\n", "")}
752+
|$showDiagnostics
753+
|""".stripMargin.trim.linesIterator.mkString("\n", "\n", "")
754+
else if hasMissingAnnotations then s"\nWarnings found on incorrect row numbers when compiling $testSource\n$showDiagnostics"
755+
else if !map.isEmpty then s"\nExpected warnings(s) have {<warning position>=<unreported warning>}: $map"
756+
else null
757+
end maybeFailureMessage
758+
759+
def getWarnMapAndExpectedCount(files: Seq[JFile]): (HashMap[String, Integer], Int) =
760+
val comment = raw"//( *)warn".r
761+
val map = new HashMap[String, Integer]()
762+
var count = 0
763+
def bump(key: String): Unit =
764+
map.get(key) match
765+
case null => map.put(key, 1)
766+
case n => map.put(key, n+1)
767+
count += 1
768+
files.filter(isSourceFile).foreach { file =>
769+
Using(Source.fromFile(file, StandardCharsets.UTF_8.name)) { source =>
770+
source.getLines.zipWithIndex.foreach { case (line, lineNbr) =>
771+
comment.findAllMatchIn(line).foreach { _ =>
772+
bump(s"${file.getPath}:${lineNbr+1}")
773+
}
774+
}
775+
}.get
776+
}
777+
(map, count)
778+
779+
def getMissingExpectedWarnings(map: HashMap[String, Integer], reporterWarnings: Iterator[Diagnostic]): (List[String], List[String]) =
780+
val unexpected, unpositioned = ListBuffer.empty[String]
781+
def relativize(path: String): String = path.split(JFile.separatorChar).dropWhile(_ != "tests").mkString(JFile.separator)
782+
def seenAt(key: String): Boolean =
783+
map.get(key) match
784+
case null => false
785+
case 1 => map.remove(key) ; true
786+
case n => map.put(key, n - 1) ; true
787+
def sawDiagnostic(d: Diagnostic): Unit =
788+
val srcpos = d.pos.nonInlined
789+
if srcpos.exists then
790+
val key = s"${relativize(srcpos.source.file.toString())}:${srcpos.line + 1}"
791+
if !seenAt(key) then unexpected += key
792+
else
793+
unpositioned += relativize(srcpos.source.file.toString())
794+
795+
reporterWarnings.foreach(sawDiagnostic)
796+
797+
(map.asScala.keys.toList, (unexpected ++ unpositioned).toList)
798+
end getMissingExpectedWarnings
799+
720800
private final class RewriteTest(testSources: List[TestSource], checkFiles: Map[JFile, JFile], times: Int, threadLimit: Option[Int], suppressAllOutput: Boolean)(implicit summaryReport: SummaryReporting)
721801
extends Test(testSources, times, threadLimit, suppressAllOutput) {
722802
private def verifyOutput(testSource: TestSource, reporters: Seq[TestReporter], logger: LoggedRunnable) = {
@@ -808,10 +888,7 @@ trait ParallelTesting extends RunnerOrchestration { self =>
808888
end maybeFailureMessage
809889

810890
override def onSuccess(testSource: TestSource, reporters: Seq[TestReporter], logger: LoggedRunnable): Unit =
811-
checkFile(testSource).foreach(diffTest(testSource, _, reporterOutputLines(reporters), reporters, logger))
812-
813-
def reporterOutputLines(reporters: Seq[TestReporter]): List[String] =
814-
reporters.flatMap(_.consoleOutput.split("\n")).toList
891+
diffCheckfile(testSource, reporters, logger)
815892

816893
// In neg-tests we allow two or three types of error annotations.
817894
// Normally, `// error` must be annotated on the correct line number.
@@ -1014,20 +1091,11 @@ trait ParallelTesting extends RunnerOrchestration { self =>
10141091
* compilation without generating errors and that they do not crash the
10151092
* compiler
10161093
*/
1017-
def checkCompile()(implicit summaryReport: SummaryReporting): this.type = {
1018-
val test = new PosTest(targets, times, threadLimit, shouldFail || shouldSuppressOutput).executeTestSuite()
1019-
1020-
cleanup()
1094+
def checkCompile()(implicit summaryReport: SummaryReporting): this.type =
1095+
checkPass(new PosTest(targets, times, threadLimit, shouldFail || shouldSuppressOutput), "Pos")
10211096

1022-
if (!shouldFail && test.didFail) {
1023-
fail(s"Expected no errors when compiling, failed for the following reason(s):\n${reasonsForFailure(test)}\n")
1024-
}
1025-
else if (shouldFail && !test.didFail && test.skipCount == 0) {
1026-
fail("Pos test should have failed, but didn't")
1027-
}
1028-
1029-
this
1030-
}
1097+
def checkWarnings()(implicit summaryReport: SummaryReporting): this.type =
1098+
checkPass(new WarnTest(targets, times, threadLimit, shouldFail || shouldSuppressOutput), "Warn")
10311099

10321100
/** Creates a "neg" test run, which makes sure that each test generates the
10331101
* correct number of errors at the correct positions. It also makes sure
@@ -1047,35 +1115,16 @@ trait ParallelTesting extends RunnerOrchestration { self =>
10471115
end checkExpectedErrors
10481116

10491117
/** Creates a "fuzzy" test run, which makes sure that each test compiles (or not) without crashing */
1050-
def checkNoCrash()(implicit summaryReport: SummaryReporting): this.type = {
1051-
val test = new NoCrashTest(targets, times, threadLimit, shouldSuppressOutput).executeTestSuite()
1052-
1053-
cleanup()
1054-
1055-
if (test.didFail) {
1056-
fail("Fuzzy test shouldn't have crashed, but did")
1057-
}
1058-
1059-
this
1060-
}
1118+
def checkNoCrash()(implicit summaryReport: SummaryReporting): this.type =
1119+
checkFail(new NoCrashTest(targets, times, threadLimit, shouldSuppressOutput), "Fuzzy")
10611120

10621121
/** Creates a "run" test run, which is a superset of "pos". In addition to
10631122
* making sure that all tests pass compilation and that they do not crash
10641123
* the compiler; it also makes sure that all tests can run with the
10651124
* expected output
10661125
*/
1067-
def checkRuns()(implicit summaryReport: SummaryReporting): this.type = {
1068-
val test = new RunTest(targets, times, threadLimit, shouldFail || shouldSuppressOutput).executeTestSuite()
1069-
1070-
cleanup()
1071-
1072-
if !shouldFail && test.didFail then
1073-
fail(s"Run test failed, but should not, reasons:\n${ reasonsForFailure(test) }")
1074-
else if shouldFail && !test.didFail && test.skipCount == 0 then
1075-
fail("Run test should have failed, but did not")
1076-
1077-
this
1078-
}
1126+
def checkRuns()(implicit summaryReport: SummaryReporting): this.type =
1127+
checkPass(new RunTest(targets, times, threadLimit, shouldFail || shouldSuppressOutput), "Run")
10791128

10801129
/** Tests `-rewrite`, which makes sure that the rewritten files still compile
10811130
* and agree with the expected result (if specified).
@@ -1100,15 +1149,34 @@ trait ParallelTesting extends RunnerOrchestration { self =>
11001149
target.copy(dir = copyToDir(outDir, dir))
11011150
}
11021151

1103-
val test = new RewriteTest(copiedTargets, checkFileMap, times, threadLimit, shouldFail || shouldSuppressOutput).executeTestSuite()
1152+
val test = new RewriteTest(copiedTargets, checkFileMap, times, threadLimit, shouldFail || shouldSuppressOutput)
1153+
1154+
checkFail(test, "Rewrite")
1155+
}
1156+
1157+
private def checkPass(test: Test, desc: String): this.type =
1158+
test.executeTestSuite()
1159+
1160+
cleanup()
1161+
1162+
if !shouldFail && test.didFail then
1163+
fail(s"$desc test failed, but should not, reasons:\n${reasonsForFailure(test)}")
1164+
else if shouldFail && !test.didFail && test.skipCount == 0 then
1165+
fail(s"$desc test should have failed, but didn't")
1166+
1167+
this
1168+
1169+
private def checkFail(test: Test, desc: String): this.type =
1170+
test.executeTestSuite()
11041171

11051172
cleanup()
11061173

1107-
if test.didFail then
1108-
fail("Rewrite test failed")
1174+
if shouldFail && !test.didFail && test.skipCount == 0 then
1175+
fail(s"$desc test shouldn't have failed, but did. Reasons:\n${reasonsForFailure(test)}")
1176+
else if !shouldFail && test.didFail then
1177+
fail(s"$desc test failed")
11091178

11101179
this
1111-
}
11121180

11131181
/** Deletes output directories and files */
11141182
private def cleanup(): this.type = {

0 commit comments

Comments
 (0)