Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions compiler/src/dotty/tools/dotc/cc/SepCheck.scala
Original file line number Diff line number Diff line change
Expand Up @@ -461,10 +461,7 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser:
*/
def consumeError(ref: Capability, loc: (SrcPos, TypeRole), pos: SrcPos)(using Context): Unit =
val (locPos, role) = loc
report.error(
em"""Separation failure: Illegal access to $ref, which was ${role.howConsumed}
|on line ${locPos.line + 1} and therefore is no longer available.""",
pos)
report.error(reporting.UseAfterConsume(ref, locPos.sourcePos, pos.sourcePos, role.howConsumed), pos)

/** Report a failure where a capability is consumed in a loop.
* @param ref the capability
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/reporting/Diagnostic.scala
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,6 @@ class Diagnostic(
msg.message.replaceAll("\u001B\\[[;\\d]*m", "")
override def diagnosticRelatedInformation: JList[interfaces.DiagnosticRelatedInformation] =
Collections.emptyList()

override def toString: String = s"$getClass at $pos L${pos.line+1}: $message"
end Diagnostic

18 changes: 18 additions & 0 deletions compiler/src/dotty/tools/dotc/reporting/Message.scala
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,13 @@ object Message:
super.toText(sym)
end Printer

/** A part of a multi-span message, associating text with a source position.
* @param text the message text for this part
* @param srcPos the source position where this part applies
* @param isPrimary whether this is the primary message (true) or a secondary note (false)
*/
case class MessagePart(text: String, srcPos: util.SourcePosition, isPrimary: Boolean)

end Message

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

/** Optional leading text to be displayed before the source snippet.
* If present along with parts, triggers multi-span rendering.
*/
def leading(using Context): Option[String] = None

/** Optional list of message parts for multi-span error messages.
* Each part associates text with a source position and indicates
* whether it's a primary message or a secondary note.
*/
def parts(using Context): List[MessagePart] = Nil

/** What gets printed after the message proper */
protected def msgPostscript(using Context): String =
if ctx eq NoContext then ""
Expand Down
288 changes: 224 additions & 64 deletions compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala
Original file line number Diff line number Diff line change
Expand Up @@ -77,24 +77,32 @@ trait MessageRendering {
* -- Error: source.scala ---------------------
* ```
*/
private def boxTitle(title: String)(using Context, Level, Offset): String =
private def boxTitle(title: String, isSubtitle: Boolean = false)(using Context, Level, Offset): String =
val pageWidth = ctx.settings.pageWidth.value
val line = "-" * (pageWidth - title.length - 4)
hl(s"-- $title $line")
val starter = if isSubtitle then ".." else "--"
hl(s"$starter $title $line")

/** The position markers aligned under the error
*
* ```
* | ^^^^^
* ```
* or for sub-diagnostics:
* ```
* | -----
* ```
*
* @param pos the source position to mark
* @param markerChar the character to use for marking ('^' for primary errors, '-' for notes)
*/
private def positionMarker(pos: SourcePosition)(using Context, Level, Offset): String = {
private def positionMarker(pos: SourcePosition, markerChar: Char = '^')(using Context, Level, Offset): String = {
val padding = pos.startColumnPadding
val carets =
val markers =
if (pos.startLine == pos.endLine)
"^" * math.max(1, pos.endColumn - pos.startColumn)
else "^"
hl(s"$offsetBox$padding$carets")
markerChar.toString * math.max(1, pos.endColumn - pos.startColumn)
else markerChar.toString
hl(s"$offsetBox$padding$markers")
}

/** The horizontal line with the given offset
Expand Down Expand Up @@ -169,7 +177,8 @@ trait MessageRendering {
private def posStr(
pos: SourcePosition,
message: Message,
diagnosticString: String
diagnosticString: String,
isSubdiag: Boolean = false
)(using Context, Level, Offset): String =
assert(
message.errorId.isActive,
Expand All @@ -191,7 +200,7 @@ trait MessageRendering {
val title =
if fileAndPos.isEmpty then s"$errId$kind:" // this happens in dotty.tools.repl.ScriptedTests // TODO add name of source or remove `:` (and update test files)
else s"$errId$kind: $fileAndPos"
boxTitle(title)
boxTitle(title, isSubtitle = isSubdiag)
})
else ""
end posStr
Expand Down Expand Up @@ -232,6 +241,163 @@ trait MessageRendering {
if origin.nonEmpty then
addHelp("origin=")(origin)

// adjust a pos at EOF if preceded by newline
private def adjust(pos: SourcePosition): SourcePosition =
if pos.span.isSynthetic
&& pos.span.isZeroExtent
&& pos.span.exists
&& pos.span.start == pos.source.length
&& pos.source(pos.span.start - 1) == '\n'
then
pos.withSpan(pos.span.shift(-1))
else
pos

/** Render a message using multi-span information from Message.parts. */
def messageAndPosFromParts(dia: Diagnostic)(using Context): String =
val msg = dia.msg
val pos = dia.pos
val pos1 = adjust(pos.nonInlined)
val msgParts = msg.parts

if msgParts.isEmpty then
return msg.leading.getOrElse("") + (if msg.leading.isDefined then "\n" else "") + msg.message

// Collect all positions from message parts
val validParts = msgParts.filter(_.srcPos.exists)

if validParts.isEmpty then
return msg.leading.getOrElse("") + (if msg.leading.isDefined then "\n" else "") + msg.message

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

// Find the line range covering all positions
val minLine = validParts.map(_.srcPos.startLine).min
val maxLine = validParts.map(_.srcPos.endLine).max
val maxLineNumber = maxLine + 1

given Level = Level(dia.level)
given Offset = Offset(maxLineNumber.toString.length + 2)

val sb = StringBuilder()

// Title using the primary position
val posString = posStr(pos1, msg, diagnosticLevel(dia))
if posString.nonEmpty then sb.append(posString).append(EOL)

// Display leading text if present
msg.leading.foreach { leadingText =>
sb.append(leadingText)
if !leadingText.endsWith(EOL) then sb.append(EOL)
}

// Render the unified code snippet
val startOffset = source.lineToOffset(minLine)
val endOffset = source.nextLine(source.lineToOffset(maxLine))
val content = source.content.slice(startOffset, endOffset)
val syntax =
if (summon[Context].settings.color.value != "never" && !summon[Context].isJava)
SyntaxHighlighting.highlight(new String(content)).toCharArray
else content

// Split syntax-highlighted content into lines
def linesFrom(arr: Array[Char]): List[String] = {
def pred(c: Char) = (c: @switch) match {
case LF | CR | FF | SU => true
case _ => false
}
val (line, rest0) = arr.span(!pred(_))
val (_, rest) = rest0.span(pred)
new String(line) :: { if (rest.isEmpty) Nil else linesFrom(rest) }
}

val lines = linesFrom(syntax)
val lineNumberWidth = maxLineNumber.toString.length

// Render each line with its markers and messages
for (lineNum <- minLine to maxLine) do
val lineIdx = lineNum - minLine
if lineIdx < lines.length then
val lineContent = lines(lineIdx)
val lineNbr = (lineNum + 1).toString
val linePrefix = String.format(s"%${lineNumberWidth}s |", lineNbr)
val lnum = hl(" " * math.max(0, offset - linePrefix.length - 1) + linePrefix)
sb.append(lnum).append(lineContent.stripLineEnd).append(EOL)

// Find all positions that should show markers after this line
val partsOnLine = validParts.filter(_.srcPos.startLine == lineNum)
.sortBy(p => (p.srcPos.startColumn, !p.isPrimary))

if partsOnLine.size == 1 then
// Single marker on this line
val part = partsOnLine.head
val markerChar = if part.isPrimary then '^' else '-'
val marker = positionMarker(part.srcPos, markerChar)
val err = errorMsg(part.srcPos, part.text)
sb.append(marker).append(EOL)
sb.append(err).append(EOL)
else if partsOnLine.size > 1 then
// Multiple markers on same line
val markerLine = StringBuilder()
markerLine.append(offsetBox)

var currentCol = 0
for part <- partsOnLine do
val markerChar = if part.isPrimary then '^' else '-'
val targetCol = part.srcPos.startColumn
val padding = " " * (targetCol - currentCol)
markerLine.append(padding).append(markerChar)
currentCol = targetCol + 1

sb.append(markerLine).append(EOL)

// Render messages from right to left with connector bars
val sortedByColumn = partsOnLine.reverse // rightmost first
for (part, idx) <- sortedByColumn.zipWithIndex do
val remainingParts = sortedByColumn.drop(idx + 1) // parts still waiting for messages

// Build connector line with vertical bars for remaining parts
val connectorLine = StringBuilder()
connectorLine.append(offsetBox)

var col = 0
// First, add vertical bars for all remaining (not-yet-shown) parts
for p <- partsOnLine do
if remainingParts.contains(p) then
val targetCol = p.srcPos.startColumn
val padding = " " * (targetCol - col)
connectorLine.append(padding).append("|")
col = targetCol + 1

// Then add the message for the current part, aligned to its column
val msgText = part.text
val msgCol = part.srcPos.startColumn
// If we've added bars, col is position after last bar; if not, col is 0
// We want the message to start at msgCol, with at least one space separation
val msgPadding = if col == 0 then " " * msgCol else " " * Math.max(1, msgCol - col)
connectorLine.append(msgPadding).append(msgText)

sb.append(connectorLine).append(EOL)

// Add explanation if needed
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 =>
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 messageAndPosFromParts

/** The whole message rendered from `dia.msg`.
*
* For a position in an inline expansion, choose `pos1`
Expand All @@ -252,65 +418,59 @@ trait MessageRendering {
*
*/
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
&& pos.span.exists
&& pos.span.start == pos.source.length
&& pos.source(pos.span.start - 1) == '\n'
then
pos.withSpan(pos.span.shift(-1))
else
pos
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
// Check if message provides its own multi-span structure
if msg.leading.isDefined || msg.parts.nonEmpty then
messageAndPosFromParts(dia)
else
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 then
appendFilterHelp(dia, sb)

if Diagnostic.shouldExplain(dia) 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(offsetBox).append(" Explanation (enabled by `-explain`)")
sb.append(EOL).append(newBox(soft = true))
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)
end if
else sb.append(msg.message)
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 =>
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`")
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
sb.toString
end messageAndPos

private def hl(str: String)(using Context, Level): String =
Expand Down
Loading
Loading