@@ -16,6 +16,8 @@ import scala.annotation.switch
16
16
import scala .collection .mutable
17
17
18
18
trait MessageRendering {
19
+ import Highlight .*
20
+ import Offsets .*
19
21
20
22
/** Remove ANSI coloring from `str`, useful for getting real length of
21
23
* strings
@@ -25,31 +27,25 @@ trait MessageRendering {
25
27
def stripColor (str : String ): String =
26
28
str.replaceAll(" \u001b\\ [.*?m" , " " )
27
29
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)
37
33
else Nil
38
34
39
35
/** Get the sourcelines before and after the position, as well as the offset
40
36
* for rendering line numbers
41
37
*
42
38
* @return (lines before error, lines after error, line numbers offset)
43
39
*/
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 ) = {
45
41
assert(pos.exists && pos.source.file.exists)
46
42
var maxLen = Int .MinValue
47
43
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)
51
47
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)
53
49
lnum + line.stripLineEnd
54
50
}
55
51
@@ -77,23 +73,75 @@ trait MessageRendering {
77
73
)
78
74
}
79
75
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 = {
83
94
val padding = pos.startColumnPadding
84
- val carets = hl(diagnosticLevel) {
95
+ val carets =
85
96
if (pos.startLine == pos.endLine)
86
97
" ^" * math.max(1 , pos.endColumn - pos.startColumn)
87
98
else " ^"
88
- }
89
- s " $prefix| $padding$carets"
99
+ hl(s " $offsetBox$padding$carets" )
90
100
}
91
101
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
+
92
140
/** The error message (`msg`) aligned under `pos`
93
141
*
94
142
* @return aligned error message
95
143
*/
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 = {
97
145
val padding = msg.linesIterator.foldLeft(pos.startColumnPadding) { (pad, line) =>
98
146
val lineLength = stripColor(line).length
99
147
val maxPad = math.max(0 , ctx.settings.pageWidth.value - offset - lineLength) - offset
@@ -103,35 +151,35 @@ trait MessageRendering {
103
151
}
104
152
105
153
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) }
107
155
.mkString(EOL )
108
156
}
109
157
110
158
/** 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 =
112
160
val path = pos.source.file.path
113
161
if pos.exists then s " $path: ${pos.line + 1 }: ${pos.column}" else path
114
162
115
163
/** The separator between errors containing the source file and error type
116
164
*
117
165
* @return separator containing error location and kind
118
166
*/
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)
123
171
val errId =
124
172
if (message.errorId ne ErrorMessageID .NoExplanationID ) {
125
173
val errorNumber = message.errorId.errorNumber
126
174
s " [E ${" 0" * (3 - errorNumber.toString.length) + errorNumber}] "
127
175
} else " "
128
176
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 )
135
183
}) else " "
136
184
137
185
/** Explanation rendered under "Explanation" header */
@@ -146,7 +194,7 @@ trait MessageRendering {
146
194
sb.toString
147
195
}
148
196
149
- def appendFilterHelp (dia : Diagnostic , sb : mutable.StringBuilder ): Unit =
197
+ private def appendFilterHelp (dia : Diagnostic , sb : mutable.StringBuilder ): Unit =
150
198
import dia ._
151
199
val hasId = msg.errorId.errorNumber >= 0
152
200
val category = dia match {
@@ -166,17 +214,35 @@ trait MessageRendering {
166
214
/** The whole message rendered from `msg` */
167
215
def messageAndPos (dia : Diagnostic )(using Context ): String = {
168
216
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 )
170
224
val sb = mutable.StringBuilder ()
171
- val posString = posStr(pos, levelString, msg )
225
+ val posString = posStr(pos, msg, diagnosticLevel(dia) )
172
226
if (posString.nonEmpty) sb.append(posString).append(EOL )
173
227
if (pos.exists) {
174
228
val pos1 = pos.nonInlined
175
229
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
+ sb.append(EOL ).append(newBox(soft = true ))
240
+ sb.append(EOL ).append(offsetBox).append(i " This location contains code that was inlined from $pos" )
241
+ if inlinedPos.source.file.exists then
242
+ val (srcBefore, srcAfter, _) = sourceLines(inlinedPos)
243
+ val marker = positionMarker(inlinedPos)
244
+ sb.append(EOL ).append((srcBefore ::: marker :: srcAfter).mkString(EOL ))
245
+ sb.append(EOL ).append(endBox)
180
246
}
181
247
else sb.append(msg.message)
182
248
}
@@ -186,15 +252,13 @@ trait MessageRendering {
186
252
sb.toString
187
253
}
188
254
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
- }
255
+ private def hl (str : String )(using Context , Level ): String =
256
+ summon[Level ].value match
257
+ case interfaces.Diagnostic .ERROR => Red (str).show
258
+ case interfaces.Diagnostic .WARNING => Yellow (str).show
259
+ case interfaces.Diagnostic .INFO => Blue (str).show
196
260
197
- def diagnosticLevel (dia : Diagnostic ): String =
261
+ private def diagnosticLevel (dia : Diagnostic ): String =
198
262
dia match {
199
263
case dia : FeatureWarning => " Feature Warning"
200
264
case dia : DeprecationWarning => " Deprecation Warning"
@@ -205,4 +269,28 @@ trait MessageRendering {
205
269
case interfaces.Diagnostic .WARNING => " Warning"
206
270
case interfaces.Diagnostic .INFO => " Info"
207
271
}
272
+
273
+ }
274
+
275
+ private object Highlight {
276
+ opaque type Level = Int
277
+ extension (level : Level ) def value : Int = level
278
+ object Level :
279
+ def apply (level : Int ): Level = level
280
+ }
281
+
282
+ /** Size of the left offset added by the box
283
+ *
284
+ * ```
285
+ * -- Error: ... ------------
286
+ * 4 | foo
287
+ * | ^^^
288
+ * ^^^ // size of this offset
289
+ * ```
290
+ */
291
+ private object Offsets {
292
+ opaque type Offset = Int
293
+ def offset (using o : Offset ): Int = o
294
+ object Offset :
295
+ def apply (level : Int ): Offset = level
208
296
}
0 commit comments