diff --git a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala index b98f07080181..3c9998132250 100644 --- a/compiler/src/dotty/tools/dotc/cc/SepCheck.scala +++ b/compiler/src/dotty/tools/dotc/cc/SepCheck.scala @@ -443,17 +443,21 @@ class SepCheck(checker: CheckCaptures.CheckerAPI) extends tpd.TreeTraverser: |No clashing definitions were found. This might point to an internal error.""", tree.srcPos) + class UseAfterConsume(ref: Capability, consumedLoc: SrcPos, useLoc: SrcPos)(using Context) extends reporting.Diagnostic.Error( + em"""Separation failure: Illegal access to $ref, which was passed to a + |consume parameter or was used as a prefix to a consume method + |and therefore is no longer available.""", + useLoc.sourcePos + ): + addSubdiag(em"... $ref was consumed here.", consumedLoc.sourcePos) + /** Report a failure where a previously consumed capability is used again, * @param ref the capability that is used after being consumed * @param loc the position where the capability was consumed * @param pos the position where the capability was used again */ def consumeError(ref: Capability, loc: SrcPos, pos: SrcPos)(using Context): Unit = - report.error( - em"""Separation failure: Illegal access to $ref, which was passed to a - |consume parameter or was used as a prefix to a consume method on line ${loc.line + 1} - |and therefore is no longer available.""", - pos) + ctx.reporter.report(UseAfterConsume(ref, loc, pos)) /** Report a failure where a capability is consumed in a loop. * @param ref the capability diff --git a/compiler/src/dotty/tools/dotc/reporting/Diagnostic.scala b/compiler/src/dotty/tools/dotc/reporting/Diagnostic.scala index a4c30b4658e9..90b841a1adcb 100644 --- a/compiler/src/dotty/tools/dotc/reporting/Diagnostic.scala +++ b/compiler/src/dotty/tools/dotc/reporting/Diagnostic.scala @@ -11,6 +11,8 @@ import dotty.tools.dotc.util.chaining.* import java.util.{Collections, Optional, List => JList} import core.Decorators.toMessage +import collection.mutable.ArrayBuffer + object Diagnostic: def shouldExplain(dia: Diagnostic)(using Context): Boolean = @@ -117,6 +119,22 @@ 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" + + private val subdiags: ArrayBuffer[Subdiagnostic] = ArrayBuffer.empty + + def addSubdiag(diag: Subdiagnostic): Unit = + subdiags += diag + + def addSubdiag(msg: Message, pos: SourcePosition): Unit = + addSubdiag(Subdiagnostic(msg, pos)) + + def withSubdiags(diags: List[Subdiagnostic]): this.type = + diags.foreach(addSubdiag) + this + + def getSubdiags: List[Subdiagnostic] = subdiags.toList end Diagnostic + +class Subdiagnostic(val msg: Message, val pos: SourcePosition) + diff --git a/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala b/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala index 9366050a5a17..68b4b74089f2 100644 --- a/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala +++ b/compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala @@ -77,10 +77,11 @@ 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 * @@ -169,7 +170,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, @@ -191,7 +193,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 @@ -232,6 +234,18 @@ 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 + /** The whole message rendered from `dia.msg`. * * For a position in an inline expansion, choose `pos1` @@ -252,17 +266,6 @@ 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 @@ -296,6 +299,9 @@ trait MessageRendering { sb.append(EOL).append(endBox) end if else sb.append(msg.message) + + dia.getSubdiags.foreach(addSubdiagnostic(sb, _)) + if dia.isVerbose then appendFilterHelp(dia, sb) @@ -313,6 +319,20 @@ trait MessageRendering { sb.toString end messageAndPos + private def addSubdiagnostic(sb: StringBuilder, subdiag: Subdiagnostic)(using Context, Level, Offset): Unit = + val pos1 = adjust(subdiag.pos) + val msg = subdiag.msg + assert(pos1.exists && pos1.source.file.exists) + + val posString = posStr(pos1, msg, "Note", isSubdiag = true) + val (srcBefore, srcAfter, offset) = sourceLines(pos1) + val marker = positionMarker(pos1) + val err = errorMsg(pos1, msg.message) + + val diagText = (posString :: srcBefore ::: marker :: err :: srcAfter).mkString(EOL) + sb.append(EOL) + sb.append(diagText) + private def hl(str: String)(using Context, Level): String = summon[Level].value match case interfaces.Diagnostic.ERROR => Red(str).show