@@ -16,6 +16,8 @@ import scala.annotation.switch
1616import scala .collection .mutable
1717
1818trait MessageRendering {
19+ import Highlight .*
20+ import Offsets .*
1921
2022 /** Remove ANSI coloring from `str`, useful for getting real length of
2123 * strings
@@ -25,31 +27,25 @@ trait MessageRendering {
2527 def stripColor (str : String ): String =
2628 str.replaceAll(" \u001b\\ [.*?m" , " " )
2729
28- /** When inlining a method call, if there's an error we'd like to get the
29- * outer context and the `pos` at which the call was inlined.
30- *
31- * @return a list of strings with inline locations
32- */
33- def outer (pos : SourcePosition , prefix : String )(using Context ): List [String ] =
34- if (pos.outer.exists)
35- i " $prefix| This location contains code that was inlined from $pos" ::
36- outer(pos.outer, prefix)
30+ /** List of all the inline calls that surround the position */
31+ def inlinePosStack (pos : SourcePosition ): List [SourcePosition ] =
32+ if pos.outer != null && pos.outer.exists then pos :: inlinePosStack(pos.outer)
3733 else Nil
3834
3935 /** Get the sourcelines before and after the position, as well as the offset
4036 * for rendering line numbers
4137 *
4238 * @return (lines before error, lines after error, line numbers offset)
4339 */
44- def sourceLines (pos : SourcePosition , diagnosticLevel : String )(using Context ): (List [String ], List [String ], Int ) = {
40+ private def sourceLines (pos : SourcePosition )(using Context , Level , Offset ): (List [String ], List [String ], Int ) = {
4541 assert(pos.exists && pos.source.file.exists)
4642 var maxLen = Int .MinValue
4743 def render (offsetAndLine : (Int , String )): String = {
48- val (offset , line) = offsetAndLine
49- val lineNbr = pos.source.offsetToLine(offset)
50- val prefix = s " ${lineNbr + 1 } | "
44+ val (offset1 , line) = offsetAndLine
45+ val lineNbr = ( pos.source.offsetToLine(offset1) + 1 ).toString
46+ val prefix = String .format( s " % ${offset - 2 } s |" , lineNbr)
5147 maxLen = math.max(maxLen, prefix.length)
52- val lnum = hl(diagnosticLevel)( " " * math.max(0 , maxLen - prefix.length) + prefix)
48+ val lnum = hl(" " * math.max(0 , maxLen - prefix.length - 1 ) + prefix)
5349 lnum + line.stripLineEnd
5450 }
5551
@@ -77,23 +73,75 @@ trait MessageRendering {
7773 )
7874 }
7975
80- /** The column markers aligned under the error */
81- def columnMarker (pos : SourcePosition , offset : Int , diagnosticLevel : String )(using Context ): String = {
82- val prefix = " " * (offset - 1 )
76+ /** Generate box containing the report title
77+ *
78+ * ```
79+ * -- Error: source.scala ---------------------
80+ * ```
81+ */
82+ private def boxTitle (title : String )(using Context , Level , Offset ): String =
83+ val pageWidth = ctx.settings.pageWidth.value
84+ val line = " -" * (pageWidth - title.length - 4 )
85+ hl(s " -- $title $line" )
86+
87+ /** The position markers aligned under the error
88+ *
89+ * ```
90+ * | ^^^^^
91+ * ```
92+ */
93+ private def positionMarker (pos : SourcePosition )(using Context , Level , Offset ): String = {
8394 val padding = pos.startColumnPadding
84- val carets = hl(diagnosticLevel) {
95+ val carets =
8596 if (pos.startLine == pos.endLine)
8697 " ^" * math.max(1 , pos.endColumn - pos.startColumn)
8798 else " ^"
88- }
89- s " $prefix| $padding$carets"
99+ hl(s " $offsetBox$padding$carets" )
90100 }
91101
102+ /** The horizontal line with the given offset
103+ *
104+ * ```
105+ * |
106+ * ```
107+ */
108+ private def offsetBox (using Context , Level , Offset ): String =
109+ val prefix = " " * (offset - 1 )
110+ hl(s " $prefix| " )
111+
112+ /** The end of a box section
113+ *
114+ * ```
115+ * |---------------
116+ * ```
117+ * Or if there `soft` is true,
118+ * ```
119+ * |···············
120+ * ```
121+ */
122+ private def newBox (soft : Boolean = false )(using Context , Level , Offset ): String =
123+ val pageWidth = ctx.settings.pageWidth.value
124+ val prefix = " " * (offset - 1 )
125+ val line = (if soft then " ·" else " -" ) * (pageWidth - offset)
126+ hl(s " $prefix| $line" )
127+
128+ /** The end of a box section
129+ *
130+ * ```
131+ * ·----------------
132+ * ```
133+ */
134+ private def endBox (using Context , Level , Offset ): String =
135+ val pageWidth = ctx.settings.pageWidth.value
136+ val prefix = " " * (offset - 1 )
137+ val line = " -" * (pageWidth - offset)
138+ hl(s " ${prefix}· $line" )
139+
92140 /** The error message (`msg`) aligned under `pos`
93141 *
94142 * @return aligned error message
95143 */
96- def errorMsg (pos : SourcePosition , msg : String , offset : Int )(using Context ): String = {
144+ private def errorMsg (pos : SourcePosition , msg : String )(using Context , Level , Offset ): String = {
97145 val padding = msg.linesIterator.foldLeft(pos.startColumnPadding) { (pad, line) =>
98146 val lineLength = stripColor(line).length
99147 val maxPad = math.max(0 , ctx.settings.pageWidth.value - offset - lineLength) - offset
@@ -103,35 +151,35 @@ trait MessageRendering {
103151 }
104152
105153 msg.linesIterator
106- .map { line => " " * (offset - 1 ) + " | " + (if line.isEmpty then " " else padding + line) }
154+ .map { line => offsetBox + (if line.isEmpty then " " else padding + line) }
107155 .mkString(EOL )
108156 }
109157
110158 /** The source file path, line and column numbers from the given SourcePosition */
111- def posFileStr (pos : SourcePosition ): String =
159+ protected def posFileStr (pos : SourcePosition ): String =
112160 val path = pos.source.file.path
113161 if pos.exists then s " $path: ${pos.line + 1 }: ${pos.column}" else path
114162
115163 /** The separator between errors containing the source file and error type
116164 *
117165 * @return separator containing error location and kind
118166 */
119- def posStr (pos : SourcePosition , diagnosticLevel : String , message : Message )(using Context ): String =
120- if (pos.source != NoSourcePosition .source) hl(diagnosticLevel)( {
121- val fileAndPos = posFileStr( pos.nonInlined)
122- val file = if fileAndPos.isEmpty || fileAndPos.endsWith( " " ) then fileAndPos else s " $fileAndPos "
167+ private def posStr (pos : SourcePosition , message : Message , diagnosticString : String )(using Context , Level , Offset ): String =
168+ if (pos.source != NoSourcePosition .source) hl({
169+ val realPos = pos.nonInlined
170+ val fileAndPos = posFileStr(realPos)
123171 val errId =
124172 if (message.errorId ne ErrorMessageID .NoExplanationID ) {
125173 val errorNumber = message.errorId.errorNumber
126174 s " [E ${" 0" * (3 - errorNumber.toString.length) + errorNumber}] "
127175 } else " "
128176 val kind =
129- if (message.kind == " " ) diagnosticLevel
130- else s " ${message.kind} $diagnosticLevel "
131- val prefix = s " -- ${errId}${kind} : $file "
132-
133- prefix +
134- ( " - " * math.max(ctx.settings.pageWidth.value - stripColor(prefix).length, 0 ) )
177+ if (message.kind == " " ) diagnosticString
178+ else s " ${message.kind} $diagnosticString "
179+ val title =
180+ 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)
181+ else s " $errId$kind : $fileAndPos "
182+ boxTitle(title )
135183 }) else " "
136184
137185 /** Explanation rendered under "Explanation" header */
@@ -146,7 +194,7 @@ trait MessageRendering {
146194 sb.toString
147195 }
148196
149- def appendFilterHelp (dia : Diagnostic , sb : mutable.StringBuilder ): Unit =
197+ private def appendFilterHelp (dia : Diagnostic , sb : mutable.StringBuilder ): Unit =
150198 import dia ._
151199 val hasId = msg.errorId.errorNumber >= 0
152200 val category = dia match {
@@ -166,17 +214,34 @@ trait MessageRendering {
166214 /** The whole message rendered from `msg` */
167215 def messageAndPos (dia : Diagnostic )(using Context ): String = {
168216 import dia ._
169- val levelString = diagnosticLevel(dia)
217+ val pos1 = pos.nonInlined
218+ val inlineStack = inlinePosStack(pos).filter(_ != pos1)
219+ val maxLineNumber =
220+ if pos.exists then (pos1 :: inlineStack).map(_.endLine).max + 1
221+ else 0
222+ given Level = Level (level)
223+ given Offset = Offset (maxLineNumber.toString.length + 2 )
170224 val sb = mutable.StringBuilder ()
171- val posString = posStr(pos, levelString, msg )
225+ val posString = posStr(pos, msg, diagnosticLevel(dia) )
172226 if (posString.nonEmpty) sb.append(posString).append(EOL )
173227 if (pos.exists) {
174228 val pos1 = pos.nonInlined
175229 if (pos1.exists && pos1.source.file.exists) {
176- val (srcBefore, srcAfter, offset) = sourceLines(pos1, levelString)
177- val marker = columnMarker(pos1, offset, levelString)
178- val err = errorMsg(pos1, msg.message, offset)
179- sb.append((srcBefore ::: marker :: err :: outer(pos, " " * (offset - 1 )) ::: srcAfter).mkString(EOL ))
230+ val (srcBefore, srcAfter, offset) = sourceLines(pos1)
231+ val marker = positionMarker(pos1)
232+ val err = errorMsg(pos1, msg.message)
233+ sb.append((srcBefore ::: marker :: err :: srcAfter).mkString(EOL ))
234+
235+ if inlineStack.nonEmpty then
236+ sb.append(EOL ).append(newBox())
237+ sb.append(EOL ).append(offsetBox).append(i " Inline stack trace " )
238+ for inlinedPos <- inlineStack if inlinedPos != pos1 do
239+ val (srcBefore, srcAfter, offset) = sourceLines(inlinedPos)
240+ val marker = positionMarker(inlinedPos)
241+ sb.append(EOL ).append(newBox(soft = true ))
242+ sb.append(EOL ).append(offsetBox).append(i " This location contains code that was inlined from $pos" )
243+ sb.append(EOL ).append((srcBefore ::: marker :: srcAfter).mkString(EOL ))
244+ sb.append(EOL ).append(endBox)
180245 }
181246 else sb.append(msg.message)
182247 }
@@ -186,15 +251,13 @@ trait MessageRendering {
186251 sb.toString
187252 }
188253
189- def hl (diagnosticLevel : String )(str : String )(using Context ): String = diagnosticLevel match {
190- case " Info" => Blue (str).show
191- case " Error" => Red (str).show
192- case _ =>
193- assert(diagnosticLevel.contains(" Warning" ))
194- Yellow (str).show
195- }
254+ private def hl (str : String )(using Context , Level ): String =
255+ summon[Level ].value match
256+ case interfaces.Diagnostic .ERROR => Red (str).show
257+ case interfaces.Diagnostic .WARNING => Yellow (str).show
258+ case interfaces.Diagnostic .INFO => Blue (str).show
196259
197- def diagnosticLevel (dia : Diagnostic ): String =
260+ private def diagnosticLevel (dia : Diagnostic ): String =
198261 dia match {
199262 case dia : FeatureWarning => " Feature Warning"
200263 case dia : DeprecationWarning => " Deprecation Warning"
@@ -205,4 +268,28 @@ trait MessageRendering {
205268 case interfaces.Diagnostic .WARNING => " Warning"
206269 case interfaces.Diagnostic .INFO => " Info"
207270 }
271+
272+ }
273+
274+ private object Highlight {
275+ opaque type Level = Int
276+ extension (level : Level ) def value : Int = level
277+ object Level :
278+ def apply (level : Int ): Level = level
279+ }
280+
281+ /** Size of the left offset added by the box
282+ *
283+ * ```
284+ * -- Error: ... ------------
285+ * 4 | foo
286+ * | ^^^
287+ * ^^^ // size of this offset
288+ * ```
289+ */
290+ private object Offsets {
291+ opaque type Offset = Int
292+ def offset (using o : Offset ): Int = o
293+ object Offset :
294+ def apply (level : Int ): Offset = level
208295}
0 commit comments