Skip to content

Commit f78e47d

Browse files
committed
Refactor: move multi-span handling logic to Message
1 parent 609bfdf commit f78e47d

File tree

4 files changed

+76
-111
lines changed

4 files changed

+76
-111
lines changed

compiler/src/dotty/tools/dotc/cc/SepCheck.scala

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -443,22 +443,39 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser:
443443
|No clashing definitions were found. This might point to an internal error.""",
444444
tree.srcPos)
445445

446-
class UseAfterConsume(ref: Capability, consumedLoc: SrcPos, useLoc: SrcPos)(using Context) extends reporting.Diagnostic.Error(
447-
em"""Separation failure: Illegal access to $ref, which was passed to a
448-
|consume parameter or was used as a prefix to a consume method
449-
|and therefore is no longer available.""",
450-
useLoc.sourcePos
451-
):
452-
addSubdiag(em"The capability was consumed here.", consumedLoc.sourcePos)
453-
addPrimaryNote(em"Then, it was used here")
446+
class UseAfterConsume(ref: Capability, consumedLoc: SrcPos, useLoc: SrcPos)(using Context) extends reporting.Message(reporting.ErrorMessageID.NoExplanationID):
447+
def kind = reporting.MessageKind.NoKind
448+
449+
protected def msg(using Context): String = ""
450+
451+
protected def explain(using Context): String = ""
452+
453+
override def leading(using Context): Option[String] = Some(
454+
em"""Separation failure: Illegal access to $ref, which was passed to a
455+
|consume parameter or was used as a prefix to a consume method
456+
|and therefore is no longer available.""".message
457+
)
458+
459+
override def parts(using Context): List[reporting.Message.MessagePart] = List(
460+
reporting.Message.MessagePart(
461+
"The capability was consumed here.",
462+
consumedLoc.sourcePos,
463+
isPrimary = false
464+
),
465+
reporting.Message.MessagePart(
466+
"Then, it was used here",
467+
useLoc.sourcePos,
468+
isPrimary = true
469+
)
470+
)
454471

455472
/** Report a failure where a previously consumed capability is used again,
456473
* @param ref the capability that is used after being consumed
457474
* @param loc the position where the capability was consumed
458475
* @param pos the position where the capability was used again
459476
*/
460477
def consumeError(ref: Capability, loc: SrcPos, pos: SrcPos)(using Context): Unit =
461-
ctx.reporter.report(UseAfterConsume(ref, loc, pos))
478+
report.error(UseAfterConsume(ref, loc, pos), pos)
462479

463480
/** Report a failure where a capability is consumed in a loop.
464481
* @param ref the capability

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

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ import dotty.tools.dotc.util.chaining.*
1111
import java.util.{Collections, Optional, List => JList}
1212
import core.Decorators.toMessage
1313

14-
import collection.mutable.ArrayBuffer
15-
1614
object Diagnostic:
1715

1816
def shouldExplain(dia: Diagnostic)(using Context): Boolean =
@@ -120,30 +118,5 @@ class Diagnostic(
120118
override def diagnosticRelatedInformation: JList[interfaces.DiagnosticRelatedInformation] =
121119
Collections.emptyList()
122120
override def toString: String = s"$getClass at $pos L${pos.line+1}: $message"
123-
124-
private val subdiags: ArrayBuffer[Subdiagnostic] = ArrayBuffer.empty
125-
126-
private var primaryNote: Message | Null = null
127-
128-
def addSubdiag(diag: Subdiagnostic): Unit =
129-
subdiags += diag
130-
131-
def addPrimaryNote(msg: Message): Unit =
132-
assert(primaryNote eq null)
133-
primaryNote = msg
134-
135-
def getPrimaryNote: Option[Message] =
136-
if primaryNote eq null then None else Some(primaryNote.nn)
137-
138-
def addSubdiag(msg: Message, pos: SourcePosition): Unit =
139-
addSubdiag(Subdiagnostic(msg, pos))
140-
141-
def withSubdiags(diags: List[Subdiagnostic]): this.type =
142-
diags.foreach(addSubdiag)
143-
this
144-
145-
def getSubdiags: List[Subdiagnostic] = subdiags.toList
146121
end Diagnostic
147122

148-
class Subdiagnostic(val msg: Message, val pos: SourcePosition)
149-

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,13 @@ object Message:
300300
super.toText(sym)
301301
end Printer
302302

303+
/** A part of a multi-span message, associating text with a source position.
304+
* @param text the message text for this part
305+
* @param srcPos the source position where this part applies
306+
* @param isPrimary whether this is the primary message (true) or a secondary note (false)
307+
*/
308+
case class MessagePart(text: String, srcPos: util.SourcePosition, isPrimary: Boolean)
309+
303310
end Message
304311

305312
/** A `Message` contains all semantic information necessary to easily
@@ -370,6 +377,17 @@ abstract class Message(val errorId: ErrorMessageID)(using Context) { self =>
370377
*/
371378
protected def explain(using Context): String
372379

380+
/** Optional leading text to be displayed before the source snippet.
381+
* If present along with parts, triggers multi-span rendering.
382+
*/
383+
def leading(using Context): Option[String] = None
384+
385+
/** Optional list of message parts for multi-span error messages.
386+
* Each part associates text with a source position and indicates
387+
* whether it's a primary message or a secondary note.
388+
*/
389+
def parts(using Context): List[MessagePart] = Nil
390+
373391
/** What gets printed after the message proper */
374392
protected def msgPostscript(using Context): String =
375393
if ctx eq NoContext then ""

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

Lines changed: 32 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -253,55 +253,31 @@ trait MessageRendering {
253253
else
254254
pos
255255

256-
/** Render diagnostics with positions in different files separately */
257-
private def renderSeparateSpans(dia: Diagnostic)(using Context): String =
256+
/** Render a message using multi-span information from Message.parts. */
257+
def messageAndPosFromParts(dia: Diagnostic)(using Context): String =
258258
val msg = dia.msg
259259
val pos = dia.pos
260260
val pos1 = adjust(pos.nonInlined)
261-
given Level = Level(dia.level)
262-
given Offset =
263-
val maxLineNumber = if pos.exists then pos1.endLine + 1 else 0
264-
Offset(maxLineNumber.toString.length + 2)
265-
266-
val sb = StringBuilder()
267-
val posString = posStr(pos1, msg, diagnosticLevel(dia))
268-
if posString.nonEmpty then sb.append(posString).append(EOL)
261+
val msgParts = msg.parts
269262

270-
if pos.exists && pos1.exists && pos1.source.file.exists then
271-
val (srcBefore, srcAfter, offset) = sourceLines(pos1)
272-
val marker = positionMarker(pos1)
273-
val err = errorMsg(pos1, msg.message)
274-
sb.append((srcBefore ::: marker :: err :: srcAfter).mkString(EOL))
275-
else
276-
sb.append(msg.message)
263+
if msgParts.isEmpty then
264+
return msg.leading.getOrElse("") + (if msg.leading.isDefined then "\n" else "") + msg.message
277265

278-
dia.getSubdiags.foreach(addSubdiagnostic(sb, _))
279-
sb.toString
266+
// Collect all positions from message parts
267+
val validParts = msgParts.filter(_.srcPos.exists)
280268

281-
def messageAndPosMultiSpan(dia: Diagnostic)(using Context): String =
282-
val msg = dia.msg
283-
val pos = dia.pos
284-
val pos1 = adjust(pos.nonInlined)
285-
val subdiags = dia.getSubdiags
286-
287-
// Collect all positions with their associated messages
288-
case class PosAndMsg(pos: SourcePosition, msg: Message, isPrimary: Boolean)
289-
val allPosAndMsg = PosAndMsg(pos1, msg, true) :: subdiags.map(s => PosAndMsg(adjust(s.pos), s.msg, false))
290-
val validPosAndMsg = allPosAndMsg.filter(_.pos.exists)
291-
292-
if validPosAndMsg.isEmpty then
293-
return msg.message
269+
if validParts.isEmpty then
270+
return msg.leading.getOrElse("") + (if msg.leading.isDefined then "\n" else "") + msg.message
294271

295272
// Check all positions are in the same source file
296-
val source = validPosAndMsg.head.pos.source
297-
if !validPosAndMsg.forall(_.pos.source == source) || !source.file.exists then
298-
// Cannot render multi-span if positions are in different files
299-
// Fall back to showing them separately
300-
return renderSeparateSpans(dia)
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
301277

302278
// Find the line range covering all positions
303-
val minLine = validPosAndMsg.map(_.pos.startLine).min
304-
val maxLine = validPosAndMsg.map(_.pos.endLine).max
279+
val minLine = validParts.map(_.srcPos.startLine).min
280+
val maxLine = validParts.map(_.srcPos.endLine).max
305281
val maxLineNumber = maxLine + 1
306282

307283
given Level = Level(dia.level)
@@ -313,12 +289,13 @@ trait MessageRendering {
313289
val posString = posStr(pos1, msg, diagnosticLevel(dia))
314290
if posString.nonEmpty then sb.append(posString).append(EOL)
315291

316-
// Always display primary error message before code snippet
317-
sb.append(msg.message)
318-
if !msg.message.endsWith(EOL) then sb.append(EOL)
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+
}
319297

320298
// Render the unified code snippet
321-
// Get syntax-highlighted content for the entire range
322299
val startOffset = source.lineToOffset(minLine)
323300
val endOffset = source.nextLine(source.lineToOffset(maxLine))
324301
val content = source.content.slice(startOffset, endOffset)
@@ -352,19 +329,13 @@ trait MessageRendering {
352329
sb.append(lnum).append(lineContent.stripLineEnd).append(EOL)
353330

354331
// Find all positions that should show markers after this line
355-
// A position shows its marker after its start line
356-
val positionsOnLine = validPosAndMsg.filter(_.pos.startLine == lineNum)
357-
.sortBy(pm => (pm.pos.startColumn, !pm.isPrimary)) // Primary positions first if same column
358-
359-
for posAndMsg <- positionsOnLine do
360-
// Use '^' for primary error, '-' for sub-diagnostics
361-
val markerChar = if posAndMsg.isPrimary then '^' else '-'
362-
val marker = positionMarker(posAndMsg.pos, markerChar)
363-
// For primary position: use PrimaryNote if available, otherwise use primary message
364-
val messageToShow =
365-
if posAndMsg.isPrimary then dia.getPrimaryNote.map(_.message).getOrElse(posAndMsg.msg.message)
366-
else posAndMsg.msg.message
367-
val err = errorMsg(posAndMsg.pos, messageToShow)
332+
val partsOnLine = validParts.filter(_.srcPos.startLine == lineNum)
333+
.sortBy(p => (p.srcPos.startColumn, !p.isPrimary))
334+
335+
for part <- partsOnLine do
336+
val markerChar = if part.isPrimary then '^' else '-'
337+
val marker = positionMarker(part.srcPos, markerChar)
338+
val err = errorMsg(part.srcPos, part.text)
368339
sb.append(marker).append(EOL)
369340
sb.append(err).append(EOL)
370341

@@ -381,7 +352,7 @@ trait MessageRendering {
381352
sb.append(EOL).append(offsetBox).append(" longer explanation available when compiling with `-explain`")
382353

383354
sb.toString
384-
end messageAndPosMultiSpan
355+
end messageAndPosFromParts
385356

386357
/** The whole message rendered from `dia.msg`.
387358
*
@@ -403,9 +374,11 @@ trait MessageRendering {
403374
*
404375
*/
405376
def messageAndPos(dia: Diagnostic)(using Context): String =
406-
if dia.getSubdiags.nonEmpty then messageAndPosMultiSpan(dia)
377+
val msg = dia.msg
378+
// Check if message provides its own multi-span structure
379+
if msg.leading.isDefined || msg.parts.nonEmpty then
380+
messageAndPosFromParts(dia)
407381
else
408-
val msg = dia.msg
409382
val pos = dia.pos
410383
val pos1 = adjust(pos.nonInlined) // innermost pos contained by call.pos
411384
val outermost = pos.outermost // call.pos
@@ -439,8 +412,6 @@ trait MessageRendering {
439412
end if
440413
else sb.append(msg.message)
441414

442-
dia.getSubdiags.foreach(addSubdiagnostic(sb, _))
443-
444415
if dia.isVerbose then
445416
appendFilterHelp(dia, sb)
446417

@@ -458,20 +429,6 @@ trait MessageRendering {
458429
sb.toString
459430
end messageAndPos
460431

461-
private def addSubdiagnostic(sb: StringBuilder, subdiag: Subdiagnostic)(using Context, Level, Offset): Unit =
462-
val pos1 = adjust(subdiag.pos)
463-
val msg = subdiag.msg
464-
assert(pos1.exists && pos1.source.file.exists)
465-
466-
val posString = posStr(pos1, msg, "Note", isSubdiag = true)
467-
val (srcBefore, srcAfter, offset) = sourceLines(pos1)
468-
val marker = positionMarker(pos1, '-') // Use '-' for sub-diagnostics
469-
val err = errorMsg(pos1, msg.message)
470-
471-
val diagText = (posString :: srcBefore ::: marker :: err :: srcAfter).mkString(EOL)
472-
sb.append(EOL)
473-
sb.append(diagText)
474-
475432
private def hl(str: String)(using Context, Level): String =
476433
summon[Level].value match
477434
case interfaces.Diagnostic.ERROR => Red(str).show

0 commit comments

Comments
 (0)