@@ -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,76 @@ 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 = {
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 column markers aligned under the error
88
+ *
89
+ * ```
90
+ * | ^^^^^
91
+ * ```
92
+ */
93
+ private def columnMarker(pos: SourcePosition)(using Context, Level, Offset): String = {
82
94
val prefix = " " * (offset - 1)
83
95
val padding = pos.startColumnPadding
84
- val carets = hl(diagnosticLevel) {
96
+ val carets =
85
97
if (pos.startLine == pos.endLine)
86
98
"^" * math.max(1, pos.endColumn - pos.startColumn)
87
99
else "^"
88
- }
89
- s"$prefix|$padding$carets"
100
+ hl(s"$prefix|$padding$carets")
90
101
}
91
102
103
+ /** The horizontal line with the given offset
104
+ *
105
+ * ```
106
+ * |
107
+ * ```
108
+ */
109
+ private def offsetBox(using Context, Level, Offset): String =
110
+ val prefix = " " * (offset - 1)
111
+ hl(s"$prefix|")
112
+
113
+ /** The end of a box section
114
+ *
115
+ * ```
116
+ * |---------------
117
+ * ```
118
+ * Or if there `soft` is true,
119
+ * ```
120
+ * |···············
121
+ * ```
122
+ */
123
+ private def newBox(soft: Boolean = false)(using Context, Level, Offset): String =
124
+ val pageWidth = ctx.settings.pageWidth.value
125
+ val prefix = " " * (offset - 1)
126
+ val line = (if soft then "·" else "-") * (pageWidth - offset)
127
+ hl(s"$prefix|$line")
128
+
129
+ /** The end of a box section
130
+ *
131
+ * ```
132
+ * ·----------------
133
+ * ```
134
+ */
135
+ private def endBox(using Context, Level, Offset): String =
136
+ val pageWidth = ctx.settings.pageWidth.value
137
+ val prefix = " " * (offset - 1)
138
+ val line = "-" * (pageWidth - offset)
139
+ hl(s"$prefix·$line")
140
+
92
141
/** The error message (`msg`) aligned under `pos`
93
142
*
94
143
* @return aligned error message
95
144
*/
96
- def errorMsg(pos: SourcePosition, msg: String, offset: Int )(using Context): String = {
145
+ private def errorMsg(pos: SourcePosition, msg: String)(using Context, Level, Offset ): String = {
97
146
val padding = msg.linesIterator.foldLeft(pos.startColumnPadding) { (pad, line) =>
98
147
val lineLength = stripColor(line).length
99
148
val maxPad = math.max(0, ctx.settings.pageWidth.value - offset - lineLength) - offset
@@ -103,35 +152,32 @@ trait MessageRendering {
103
152
}
104
153
105
154
msg.linesIterator
106
- .map { line => " " * (offset - 1) + "|" + (if line.isEmpty then "" else padding + line) }
155
+ .map { line => offsetBox + (if line.isEmpty then "" else padding + line) }
107
156
.mkString(EOL)
108
157
}
109
158
110
159
/** The source file path, line and column numbers from the given SourcePosition */
111
- def posFileStr(pos: SourcePosition): String =
160
+ protected def posFileStr(pos: SourcePosition): String =
112
161
val path = pos.source.file.path
113
162
if pos.exists then s"$path:${pos.line + 1}:${pos.column}" else path
114
163
115
164
/** The separator between errors containing the source file and error type
116
165
*
117
166
* @return separator containing error location and kind
118
167
*/
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 "
168
+ private def posStr(pos: SourcePosition, message: Message, diagnosticString: String )(using Context, Level, Offset ): String =
169
+ if (pos.source != NoSourcePosition.source) hl({
170
+ val realPos = pos.nonInlined
171
+ val fileAndPos = posFileStr(realPos)
123
172
val errId =
124
173
if (message.errorId ne ErrorMessageID.NoExplanationID) {
125
174
val errorNumber = message.errorId.errorNumber
126
175
s"[E${"0" * (3 - errorNumber.toString.length) + errorNumber}] "
127
176
} else ""
128
177
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))
178
+ if (message.kind == "") diagnosticString
179
+ else s"${message.kind} $diagnosticString"
180
+ boxTitle(s"$errId$kind: $fileAndPos")
135
181
}) else ""
136
182
137
183
/** Explanation rendered under "Explanation" header */
@@ -146,7 +192,7 @@ trait MessageRendering {
146
192
sb.toString
147
193
}
148
194
149
- def appendFilterHelp(dia: Diagnostic, sb: mutable.StringBuilder): Unit =
195
+ private def appendFilterHelp(dia: Diagnostic, sb: mutable.StringBuilder): Unit =
150
196
import dia._
151
197
val hasId = msg.errorId.errorNumber >= 0
152
198
val category = dia match {
@@ -166,17 +212,34 @@ trait MessageRendering {
166
212
/** The whole message rendered from `msg` */
167
213
def messageAndPos(dia: Diagnostic)(using Context): String = {
168
214
import dia._
169
- val levelString = diagnosticLevel(dia)
215
+ val pos1 = pos.nonInlined
216
+ val inlineStack = inlinePosStack(pos).filter(_ != pos1)
217
+ val maxLineNumber =
218
+ if pos.exists then (pos1 :: inlineStack).map(_.endLine).max + 1
219
+ else 0
220
+ given Level = Level(level)
221
+ given Offset = Offset(maxLineNumber.toString.length + 2)
170
222
val sb = mutable.StringBuilder()
171
- val posString = posStr(pos, levelString, msg )
223
+ val posString = posStr(pos, msg, diagnosticLevel(dia) )
172
224
if (posString.nonEmpty) sb.append(posString).append(EOL)
173
225
if (pos.exists) {
174
226
val pos1 = pos.nonInlined
175
227
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))
228
+ val (srcBefore, srcAfter, offset) = sourceLines(pos1)
229
+ val marker = columnMarker(pos1)
230
+ val err = errorMsg(pos1, msg.message)
231
+ sb.append((srcBefore ::: marker :: err :: srcAfter).mkString(EOL))
232
+
233
+ if inlineStack.nonEmpty then
234
+ sb.append(EOL).append(newBox())
235
+ sb.append(EOL).append(offsetBox).append(i"Inline stack trace")
236
+ for inlinedPos <- inlineStack if inlinedPos != pos1 do
237
+ val (srcBefore, srcAfter, offset) = sourceLines(inlinedPos)
238
+ val marker = columnMarker(inlinedPos)
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
+ sb.append(EOL).append((srcBefore ::: marker :: srcAfter).mkString(EOL))
242
+ sb.append(EOL).append(endBox)
180
243
}
181
244
else sb.append(msg.message)
182
245
}
@@ -186,15 +249,13 @@ trait MessageRendering {
186
249
sb.toString
187
250
}
188
251
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
- }
252
+ private def hl(str: String)(using Context, Level): String =
253
+ summon[Level].value match
254
+ case interfaces.Diagnostic.ERROR => Red(str).show
255
+ case interfaces.Diagnostic.WARNING => Yellow(str).show
256
+ case interfaces.Diagnostic.INFO => Blue(str).show
196
257
197
- def diagnosticLevel(dia: Diagnostic): String =
258
+ private def diagnosticLevel(dia: Diagnostic): String =
198
259
dia match {
199
260
case dia: FeatureWarning => "Feature Warning"
200
261
case dia: DeprecationWarning => "Deprecation Warning"
@@ -205,4 +266,28 @@ trait MessageRendering {
205
266
case interfaces.Diagnostic.WARNING => "Warning"
206
267
case interfaces.Diagnostic.INFO => "Info"
207
268
}
269
+
270
+ }
271
+
272
+ private object Highlight {
273
+ opaque type Level = Int
274
+ extension (level: Level) def value: Int = level
275
+ object Level:
276
+ def apply(level: Int): Level = level
277
+ }
278
+
279
+ /** Size of the left offset added by the box
280
+ *
281
+ * ```
282
+ * -- Error: ... ------------
283
+ * 4 | foo
284
+ * | ^^^
285
+ * ^^^ // size of this offset
286
+ * ```
287
+ */
288
+ private object Offsets {
289
+ opaque type Offset = Int
290
+ def offset(using o: Offset): Int = o
291
+ object Offset:
292
+ def apply(level: Int): Offset = level
208
293
}
0 commit comments