From 97e8d0fd25db5eaeb39eeb68f03b52efde192f8a Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Sat, 27 Sep 2025 09:55:50 -0700 Subject: [PATCH 1/3] Suppression matches inlined positions The inlined stack includes the nonInlined pos itself, which is filtered by rendering. --- .../tools/dotc/reporting/MessageRendering.scala | 7 +------ compiler/src/dotty/tools/dotc/reporting/WConf.scala | 5 ++++- .../src/dotty/tools/dotc/util/SourcePosition.scala | 11 +++++++++-- tests/warn/i24082.scala | 13 +++++++++++++ 4 files changed, 27 insertions(+), 9 deletions(-) create mode 100644 tests/warn/i24082.scala diff --git a/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala b/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala index a306f74858d4..45badc8164fa 100644 --- a/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala +++ b/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala @@ -30,11 +30,6 @@ trait MessageRendering { def stripColor(str: String): String = str.replaceAll("\u001b\\[.*?m", "") - /** List of all the inline calls that surround the position */ - def inlinePosStack(pos: SourcePosition): List[SourcePosition] = - if pos.outer != null && pos.outer.exists then pos :: inlinePosStack(pos.outer) - else Nil - /** Get the sourcelines before and after the position, as well as the offset * for rendering line numbers * @@ -240,7 +235,7 @@ trait MessageRendering { def messageAndPos(dia: Diagnostic)(using Context): String = { import dia.* val pos1 = pos.nonInlined - val inlineStack = inlinePosStack(pos).filter(_ != pos1) + val inlineStack = pos.inlinePosStack.filter(_ != pos1) val maxLineNumber = if pos.exists then (pos1 :: inlineStack).map(_.endLine).max + 1 else 0 diff --git a/compiler/src/dotty/tools/dotc/reporting/WConf.scala b/compiler/src/dotty/tools/dotc/reporting/WConf.scala index 2cb170ec1764..27ff24d50092 100644 --- a/compiler/src/dotty/tools/dotc/reporting/WConf.scala +++ b/compiler/src/dotty/tools/dotc/reporting/WConf.scala @@ -151,7 +151,10 @@ class Suppression(val annotPos: SourcePosition, val filters: List[MessageFilter] _used = supersededState def matches(dia: Diagnostic): Boolean = val pos = dia.pos - pos.exists && start <= pos.start && pos.end <= end && filters.forall(_.matches(dia)) + def posMatches = + start <= pos.start && pos.end <= end + || pos.inlinePosStack.exists(p => start <= p.start && p.end <= end) + pos.exists && posMatches && filters.forall(_.matches(dia)) override def toString = s"Suppress in ${annotPos.source} $start..$end [${filters.mkString(", ")}]" end Suppression diff --git a/compiler/src/dotty/tools/dotc/util/SourcePosition.scala b/compiler/src/dotty/tools/dotc/util/SourcePosition.scala index e4f56bd85c0b..081935eb6cc8 100644 --- a/compiler/src/dotty/tools/dotc/util/SourcePosition.scala +++ b/compiler/src/dotty/tools/dotc/util/SourcePosition.scala @@ -11,7 +11,7 @@ import scala.annotation.internal.sharable /** A source position is comprised of a span and a source file */ case class SourcePosition(source: SourceFile, span: Span, outer: SourcePosition = NoSourcePosition) -extends SrcPos, interfaces.SourcePosition, Showable { +extends SrcPos, interfaces.SourcePosition, Showable: def sourcePos(using Context) = this @@ -82,7 +82,14 @@ extends SrcPos, interfaces.SourcePosition, Showable { s"${if (source.exists) source.file.toString else "(no source)"}:$span" def toText(printer: Printer): Text = printer.toText(this) -} + +object SourcePosition: + extension (pos: SourcePosition) + /** List of all the inline calls that surround the position. */ + def inlinePosStack: List[SourcePosition] = + if pos.outer != null && pos.outer.exists then pos :: pos.outer.inlinePosStack + else pos :: Nil +end SourcePosition /** A sentinel for a non-existing source position */ @sharable object NoSourcePosition extends SourcePosition(NoSource, NoSpan) { diff --git a/tests/warn/i24082.scala b/tests/warn/i24082.scala new file mode 100644 index 000000000000..d2f7920e86cf --- /dev/null +++ b/tests/warn/i24082.scala @@ -0,0 +1,13 @@ +//> using options -deprecation -Werror + +import annotation.* + +@deprecated +case object A { + inline def use: Any = A +} + +@nowarn +object test { + A.use +} From ca6b9c027c0c6814cad73afb3cd08f5903a68a4f Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Sat, 27 Sep 2025 17:04:55 -0700 Subject: [PATCH 2/3] Filter pos by span not outer --- .../dotc/reporting/MessageRendering.scala | 23 ++++++++++++++++--- .../tools/dotc/util/SourcePosition.scala | 2 +- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala b/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala index 45badc8164fa..20f47681805b 100644 --- a/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala +++ b/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala @@ -231,11 +231,29 @@ trait MessageRendering { if origin.nonEmpty then addHelp("origin=")(origin) - /** The whole message rendered from `msg` */ + /** The whole message rendered from `dia.msg`. + * + * For a position in an inline expansion, choose `pos1` + * which is the most specific position in the call written + * by the user. For a diagnostic at EOF, where the last char + * of source text is a newline, adjust the position to render + * before the newline, at the end of the last line of text. + * + * The rendering begins with a label and position (`posString`). + * Then `sourceLines` with embedded caret `positionMarker` + * and rendered message. + * + * Then an `Inline stack trace` showing context for inlined code. + * Inlined positions are taken which don't contain `pos1`. + * (That should probably be positions not contained by outermost.) + * Note that position equality includes `outer` position; + * usually we intend to test `contains` or `coincidesWith`. + * + */ def messageAndPos(dia: Diagnostic)(using Context): String = { import dia.* val pos1 = pos.nonInlined - val inlineStack = pos.inlinePosStack.filter(_ != pos1) + val inlineStack = pos.inlinePosStack.filterNot(_.contains(pos1)) val maxLineNumber = if pos.exists then (pos1 :: inlineStack).map(_.endLine).max + 1 else 0 @@ -256,7 +274,6 @@ trait MessageRendering { val posString = posStr(adjusted, msg, diagnosticLevel(dia)) if (posString.nonEmpty) sb.append(posString).append(EOL) if (pos.exists) { - val pos1 = pos.nonInlined if (pos1.exists && pos1.source.file.exists) { val readjusted = if pos1 == pos then adjusted diff --git a/compiler/src/dotty/tools/dotc/util/SourcePosition.scala b/compiler/src/dotty/tools/dotc/util/SourcePosition.scala index 081935eb6cc8..a4ab9972dc8f 100644 --- a/compiler/src/dotty/tools/dotc/util/SourcePosition.scala +++ b/compiler/src/dotty/tools/dotc/util/SourcePosition.scala @@ -15,7 +15,7 @@ extends SrcPos, interfaces.SourcePosition, Showable: def sourcePos(using Context) = this - /** Is `that` a source position contained in this source position ? + /** Is `that` a source position contained in this source position? * `outer` is not taken into account. */ def contains(that: SourcePosition): Boolean = this.source == that.source && this.span.contains(that.span) From 1bfafe2df1e5f76672276e2e591bce71239ef6d2 Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Mon, 29 Sep 2025 11:13:49 -0700 Subject: [PATCH 3/3] Use adjusted pos when rendering diagnostic The previous code was unclear about what pos to adjust. The diagnostic pos yields an innermost "user pos" which is adjusted at EOF. The inline stack is already filtered by position for rendering. --- .../dotc/reporting/MessageRendering.scala | 80 +++++++++---------- tests/neg/i23815.check | 6 +- 2 files changed, 41 insertions(+), 45 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala b/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala index 20f47681805b..d435cf06cd0d 100644 --- a/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala +++ b/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala @@ -250,16 +250,8 @@ trait MessageRendering { * usually we intend to test `contains` or `coincidesWith`. * */ - def messageAndPos(dia: Diagnostic)(using Context): String = { - import dia.* - val pos1 = pos.nonInlined - val inlineStack = pos.inlinePosStack.filterNot(_.contains(pos1)) - val maxLineNumber = - if pos.exists then (pos1 :: inlineStack).map(_.endLine).max + 1 - else 0 - given Level = Level(level) - given Offset = Offset(maxLineNumber.toString.length + 2) - val sb = StringBuilder() + def messageAndPos(dia: Diagnostic)(using Context): String = + // adjust a pos at EOF if preceded by newline def adjust(pos: SourcePosition): SourcePosition = if pos.span.isSynthetic && pos.span.isZeroExtent @@ -270,53 +262,57 @@ trait MessageRendering { pos.withSpan(pos.span.shift(-1)) else pos - val adjusted = adjust(pos) - val posString = posStr(adjusted, msg, diagnosticLevel(dia)) - if (posString.nonEmpty) sb.append(posString).append(EOL) - if (pos.exists) { - if (pos1.exists && pos1.source.file.exists) { - val readjusted = - if pos1 == pos then adjusted - else adjust(pos1) - val (srcBefore, srcAfter, offset) = sourceLines(readjusted) - val marker = positionMarker(readjusted) - val err = errorMsg(readjusted, msg.message) - sb.append((srcBefore ::: marker :: err :: srcAfter).mkString(EOL)) - - if inlineStack.nonEmpty then - sb.append(EOL).append(newBox()) - sb.append(EOL).append(offsetBox).append(i"Inline stack trace") - for inlinedPos <- inlineStack if inlinedPos != pos1 do - sb.append(EOL).append(newBox(soft = true)) - sb.append(EOL).append(offsetBox).append(i"This location contains code that was inlined from $pos") - if inlinedPos.source.file.exists then - val (srcBefore, srcAfter, _) = sourceLines(inlinedPos) - val marker = positionMarker(inlinedPos) - sb.append(EOL).append((srcBefore ::: marker :: srcAfter).mkString(EOL)) - sb.append(EOL).append(endBox) - } - else sb.append(msg.message) - } + val msg = dia.msg + val pos = dia.pos + val pos1 = adjust(pos.nonInlined) // innermost pos contained by call.pos + val outermost = pos.outermost // call.pos + val inlineStack = pos.inlinePosStack.filterNot(outermost.contains(_)) + given Level = Level(dia.level) + given Offset = + val maxLineNumber = + if pos.exists then (pos1 :: inlineStack).map(_.endLine).max + 1 + else 0 + Offset(maxLineNumber.toString.length + 2) + val sb = StringBuilder() + val posString = posStr(pos1, msg, diagnosticLevel(dia)) + if posString.nonEmpty then sb.append(posString).append(EOL) + if pos.exists && pos1.exists && pos1.source.file.exists then + val (srcBefore, srcAfter, offset) = sourceLines(pos1) + val marker = positionMarker(pos1) + val err = errorMsg(pos1, msg.message) + sb.append((srcBefore ::: marker :: err :: srcAfter).mkString(EOL)) + + if inlineStack.nonEmpty then + sb.append(EOL).append(newBox()) + sb.append(EOL).append(offsetBox).append(i"Inline stack trace") + for inlinedPos <- inlineStack do + sb.append(EOL).append(newBox(soft = true)) + sb.append(EOL).append(offsetBox).append(i"This location contains code that was inlined from $pos") + if inlinedPos.source.file.exists then + val (srcBefore, srcAfter, _) = sourceLines(inlinedPos) + val marker = positionMarker(inlinedPos) + sb.append(EOL).append((srcBefore ::: marker :: srcAfter).mkString(EOL)) + sb.append(EOL).append(endBox) + end if else sb.append(msg.message) - if (dia.isVerbose) + if dia.isVerbose then appendFilterHelp(dia, sb) if Diagnostic.shouldExplain(dia) then sb.append(EOL).append(newBox()) sb.append(EOL).append(offsetBox).append(" Explanation (enabled by `-explain`)") sb.append(EOL).append(newBox(soft = true)) - dia.msg.explanation.split(raw"\R").foreach { line => + dia.msg.explanation.split(raw"\R").foreach: line => sb.append(EOL).append(offsetBox).append(if line.isEmpty then "" else " ").append(line) - } sb.append(EOL).append(endBox) else if dia.msg.canExplain then sb.append(EOL).append(offsetBox) sb.append(EOL).append(offsetBox).append(" longer explanation available when compiling with `-explain`") sb.toString - } + end messageAndPos - private def hl(str: String)(using Context, Level): String = + private def hl(str: String)(using Context, Level): String = summon[Level].value match case interfaces.Diagnostic.ERROR => Red(str).show case interfaces.Diagnostic.WARNING => Yellow(str).show diff --git a/tests/neg/i23815.check b/tests/neg/i23815.check index 342c1ce82e0c..33622404c82d 100644 --- a/tests/neg/i23815.check +++ b/tests/neg/i23815.check @@ -4,6 +4,6 @@ | ':' or '{' expected, but 'end of statement' found | Nested package statements that are not at the beginning of the file require braces or ':' with an indented body. -- [E040] Syntax Error: tests/neg/i23815.scala:9:8 --------------------------------------------------------------------- - 9 |// error - | ^ - | '}' expected, but eof found +9 |// error + | ^ + | '}' expected, but eof found