Skip to content

Commit fa179d0

Browse files
committed
Multi-span error reporting for overriding
1 parent 1fe413d commit fa179d0

File tree

7 files changed

+139
-42
lines changed

7 files changed

+139
-42
lines changed

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

Lines changed: 76 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import io.AbstractFile
1212
import printing.Highlighting.{Blue, Red, Yellow}
1313
import printing.SyntaxHighlighting
1414
import Diagnostic.*
15-
import util.{SourcePosition, NoSourcePosition}
15+
import util.{SourceFile, SourcePosition, NoSourcePosition}
1616
import util.Chars.{ LF, CR, FF, SU }
1717
import scala.annotation.switch
1818

@@ -253,47 +253,21 @@ trait MessageRendering {
253253
else
254254
pos
255255

256-
/** Render a message using multi-span information from Message.parts. */
257-
def messageAndPosFromParts(dia: Diagnostic)(using Context): String =
258-
val msg = dia.msg
259-
val pos = dia.pos
260-
val pos1 = adjust(pos.nonInlined)
261-
val msgParts = msg.parts
262-
263-
if msgParts.isEmpty then
264-
return msg.leading.getOrElse("") + (if msg.leading.isDefined then "\n" else "") + msg.message
265-
266-
// Collect all positions from message parts
267-
val validParts = msgParts.filter(_.srcPos.exists)
268-
269-
if validParts.isEmpty then
270-
return msg.leading.getOrElse("") + (if msg.leading.isDefined then "\n" else "") + msg.message
256+
/** Render parts for a single source file.
257+
* @param parts the message parts to render (all must be from the same source)
258+
* @param sb the StringBuilder to append to
259+
*/
260+
private def renderPartsForFile(
261+
parts: List[Message.MessagePart],
262+
sb: StringBuilder
263+
)(using Context, Level, Offset): Unit =
264+
if parts.isEmpty then return
271265

272-
// Check all positions are in the same source file
273-
val source = validParts.head.srcPos.source
274-
if !validParts.forall(_.srcPos.source == source) || !source.file.exists then
275-
// TODO: support rendering source positions across multiple files
276-
return msg.leading.getOrElse("") + (if msg.leading.isDefined then "\n" else "") + msg.message
266+
val source = parts.head.srcPos.source
277267

278268
// Find the line range covering all positions
279-
val minLine = validParts.map(_.srcPos.startLine).min
280-
val maxLine = validParts.map(_.srcPos.endLine).max
281-
val maxLineNumber = maxLine + 1
282-
283-
given Level = Level(dia.level)
284-
given Offset = Offset(maxLineNumber.toString.length + 2)
285-
286-
val sb = StringBuilder()
287-
288-
// Title using the primary position
289-
val posString = posStr(pos1, msg, diagnosticLevel(dia))
290-
if posString.nonEmpty then sb.append(posString).append(EOL)
291-
292-
// Display leading text if present
293-
msg.leading.foreach { leadingText =>
294-
sb.append(leadingText)
295-
if !leadingText.endsWith(EOL) then sb.append(EOL)
296-
}
269+
val minLine = parts.map(_.srcPos.startLine).min
270+
val maxLine = parts.map(_.srcPos.endLine).max
297271

298272
// Render the unified code snippet
299273
val startOffset = source.lineToOffset(minLine)
@@ -316,6 +290,7 @@ trait MessageRendering {
316290
}
317291

318292
val lines = linesFrom(syntax)
293+
val maxLineNumber = maxLine + 1
319294
val lineNumberWidth = maxLineNumber.toString.length
320295

321296
// Render each line with its markers and messages
@@ -329,7 +304,7 @@ trait MessageRendering {
329304
sb.append(lnum).append(lineContent.stripLineEnd).append(EOL)
330305

331306
// Find all positions that should show markers after this line
332-
val partsOnLine = validParts.filter(_.srcPos.startLine == lineNum)
307+
val partsOnLine = parts.filter(_.srcPos.startLine == lineNum)
333308
.sortBy(p => (p.srcPos.startColumn, !p.isPrimary))
334309

335310
if partsOnLine.size == 1 then
@@ -382,17 +357,76 @@ trait MessageRendering {
382357
connectorLine.append(msgPadding).append(msgText)
383358

384359
sb.append(connectorLine).append(EOL)
360+
end renderPartsForFile
361+
362+
/** Group consecutive parts by their source file. */
363+
private def groupPartsByFile(parts: List[Message.MessagePart]): List[(SourceFile, List[Message.MessagePart])] =
364+
if parts.isEmpty then Nil
365+
else
366+
val head = parts.head
367+
val source = head.srcPos.source
368+
val (sameSrc, rest) = parts.span(_.srcPos.source == source)
369+
(source, sameSrc) :: groupPartsByFile(rest)
370+
371+
/** Render a message using multi-span information from Message.parts. */
372+
def messageAndPosFromParts(dia: Diagnostic)(using Context): String =
373+
val msg = dia.msg
374+
val pos = dia.pos
375+
val pos1 = adjust(pos.nonInlined)
376+
val msgParts = msg.parts
377+
378+
if msgParts.isEmpty then
379+
return msg.leading.getOrElse("") + (if msg.leading.isDefined then "\n" else "") + msg.message
380+
381+
// Collect all positions from message parts
382+
val validParts = msgParts.filter(p => p.srcPos.exists && p.srcPos.source.file.exists)
383+
384+
if validParts.isEmpty then
385+
return msg.leading.map(_ + "\n").getOrElse("") + msg.message
386+
387+
// Group parts by consecutive source files
388+
val groupedParts = groupPartsByFile(validParts)
389+
390+
// Calculate the maximum line number across all files for consistent offset
391+
val maxLineNumber = validParts.map(_.srcPos.endLine).max + 1
392+
393+
given Level = Level(dia.level)
394+
given Offset = Offset(maxLineNumber.toString.length + 2)
395+
396+
val sb = StringBuilder()
397+
398+
// Title using the primary position
399+
val posString = posStr(pos1, msg, diagnosticLevel(dia))
400+
if posString.nonEmpty then sb.append(posString).append(EOL)
401+
402+
// Display leading text if present
403+
msg.leading.foreach { leadingText =>
404+
sb.append(leadingText)
405+
if !leadingText.endsWith(EOL) then sb.append(EOL)
406+
}
407+
408+
// Track the current file
409+
// When starting, we set it to the file of the diagnostic
410+
var currentFile: SourceFile = pos1.source
411+
412+
// Render each group of parts
413+
for (source, parts) <- groupedParts do
414+
// Add a file indicator line when switching to a different file
415+
if source != currentFile then
416+
sb.append("... ").append(renderPath(source.file)).append(EOL)
417+
currentFile = source
418+
renderPartsForFile(parts, sb)
385419

386420
// Add explanation if needed
387421
if Diagnostic.shouldExplain(dia) then
388-
sb.append(EOL).append(newBox())
422+
sb.append(newBox())
389423
sb.append(EOL).append(offsetBox).append(" Explanation (enabled by `-explain`)")
390424
sb.append(EOL).append(newBox(soft = true))
391425
dia.msg.explanation.split(raw"\R").foreach: line =>
392426
sb.append(EOL).append(offsetBox).append(if line.isEmpty then "" else " ").append(line)
393427
sb.append(EOL).append(endBox)
394428
else if dia.msg.canExplain then
395-
sb.append(EOL).append(offsetBox)
429+
sb.append(offsetBox)
396430
sb.append(EOL).append(offsetBox).append(" longer explanation available when compiling with `-explain`")
397431

398432
sb.toString

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1212,6 +1212,30 @@ extends DeclarationMsg(OverrideErrorID), NoDisambiguation:
12121212
def explain(using Context) =
12131213
if canExplain then err.whyNoMatchStr(memberTp, otherTp) else ""
12141214

1215+
/** Whether the overridden symbol has a valid source position */
1216+
private def otherHasValidPos(using Context): Boolean =
1217+
val otherPos = other.sourcePos
1218+
otherPos.exists && otherPos.source.file.exists
1219+
1220+
override def leading(using Context): Option[String] =
1221+
if otherHasValidPos then Some(message) else None
1222+
1223+
override def parts(using Context): List[Message.MessagePart] =
1224+
if otherHasValidPos then
1225+
List(
1226+
Message.MessagePart(
1227+
i"the overridden ${other.showKind} is here",
1228+
other.sourcePos,
1229+
isPrimary = false
1230+
),
1231+
Message.MessagePart(
1232+
i"the overriding ${member.showKind} $core",
1233+
member.sourcePos,
1234+
isPrimary = true
1235+
)
1236+
)
1237+
else Nil
1238+
12151239
class ForwardReferenceExtendsOverDefinition(value: Symbol, definition: Symbol)(using Context)
12161240
extends ReferenceMsg(ForwardReferenceExtendsOverDefinitionID) {
12171241
extension (sym: Symbol) def srcLine = sym.line + 1
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
-- [E164] Declaration Error: tests/neg/override-multi-file-error/B.scala:2:15 ------------------------------------------
2+
error overriding method foo in class A of type => Int;
3+
method foo of type => String has incompatible type
4+
... tests/neg/override-multi-file-error/A.scala
5+
2 | def foo: Int = 0
6+
| -
7+
| the overridden method is here
8+
... tests/neg/override-multi-file-error/B.scala
9+
2 | override def foo: String = "0" // error
10+
| ^
11+
| the overriding method has incompatible type
12+
|
13+
| longer explanation available when compiling with `-explain`
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
class A:
2+
def foo: Int = 0
3+
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
class B extends A:
2+
override def foo: String = "0" // error
3+
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
-- [E164] Declaration Error: tests/neg/override-multi-span.scala:6:15 --------------------------------------------------
2+
error overriding method foo in class A of type => Int;
3+
method foo of type => String has incompatible type
4+
2 | def foo: Int = 0
5+
| -
6+
| the overridden method is here
7+
3 |def main(): Unit =
8+
4 | println("Hello")
9+
5 |class B extends A:
10+
6 | override def foo: String = "0" // error
11+
| ^
12+
| the overriding method has incompatible type
13+
|
14+
| longer explanation available when compiling with `-explain`
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class A:
2+
def foo: Int = 0
3+
def main(): Unit =
4+
println("Hello")
5+
class B extends A:
6+
override def foo: String = "0" // error

0 commit comments

Comments
 (0)