Skip to content

Commit 48d5747

Browse files
Merge pull request #14427 from dotty-staging/reenable-inline-positions
Show inlined positions with source code
2 parents 74c2954 + 8f38788 commit 48d5747

32 files changed

+430
-115
lines changed

.github/workflows/ci.yaml

+4-2
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ jobs:
112112
- name: Cmd Tests
113113
run: |
114114
./project/scripts/sbt ";dist/pack; scala3-bootstrapped/compile; scala3-bootstrapped/test;sjsSandbox/run;sjsSandbox/test;sjsJUnitTests/test;sjsCompilerTests/test ;sbt-test/scripted scala2-compat/* ;configureIDE ;stdlib-bootstrapped/test:run ;stdlib-bootstrapped-tasty-tests/test; scala3-compiler-bootstrapped/scala3CompilerCoursierTest:test"
115-
./project/scripts/bootstrapCmdTests
115+
./project/scripts/cmdTests
116+
./project/scripts/bootstrappedOnlyCmdTests
116117
117118
- name: MiMa
118119
run: |
@@ -447,7 +448,8 @@ jobs:
447448
- name: Test
448449
run: |
449450
./project/scripts/sbt ";dist/pack ;scala3-bootstrapped/compile ;scala3-bootstrapped/test;sjsSandbox/run;sjsSandbox/test;sjsJUnitTests/test;sjsCompilerTests/test ;sbt-test/scripted scala2-compat/* ;configureIDE ;stdlib-bootstrapped/test:run ;stdlib-bootstrapped-tasty-tests/test"
450-
./project/scripts/bootstrapCmdTests
451+
./project/scripts/cmdTests
452+
./project/scripts/bootstrappedOnlyCmdTests
451453
452454
publish_nightly:
453455
runs-on: [self-hosted, Linux]

compiler/src/dotty/tools/dotc/reporting/MessageRendering.scala

+136-48
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import scala.annotation.switch
1616
import scala.collection.mutable
1717

1818
trait 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,35 @@ 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+
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)
180246
}
181247
else sb.append(msg.message)
182248
}
@@ -186,15 +252,13 @@ trait MessageRendering {
186252
sb.toString
187253
}
188254

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
196260

197-
def diagnosticLevel(dia: Diagnostic): String =
261+
private def diagnosticLevel(dia: Diagnostic): String =
198262
dia match {
199263
case dia: FeatureWarning => "Feature Warning"
200264
case dia: DeprecationWarning => "Deprecation Warning"
@@ -205,4 +269,28 @@ trait MessageRendering {
205269
case interfaces.Diagnostic.WARNING => "Warning"
206270
case interfaces.Diagnostic.INFO => "Info"
207271
}
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
208296
}

compiler/src/dotty/tools/dotc/transform/Splicer.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ object Splicer {
4949
val oldContextClassLoader = Thread.currentThread().getContextClassLoader
5050
Thread.currentThread().setContextClassLoader(classLoader)
5151
try {
52-
val interpreter = new Interpreter(spliceExpansionPos, classLoader)
52+
val interpreter = new Interpreter(splicePos, classLoader)
5353

5454
// Some parts of the macro are evaluated during the unpickling performed in quotedExprToTree
5555
val interpretedExpr = interpreter.interpret[Quotes => scala.quoted.Expr[Any]](tree)

compiler/src/dotty/tools/dotc/typer/Inliner.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -853,7 +853,7 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(using Context) {
853853
evidence.tpe match
854854
case fail: Implicits.SearchFailureType =>
855855
val msg = evTyper.missingArgMsg(evidence, tpt.tpe, "")
856-
errorTree(tpt, em"$msg")
856+
errorTree(call, em"$msg")
857857
case _ =>
858858
evidence
859859
return searchImplicit(callTypeArgs.head)

compiler/test-resources/repl/i9227

-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,4 @@ scala> import scala.quoted._; inline def myMacro[T]: Unit = ${ myMacroImpl[T] };
33
1 | import scala.quoted._; inline def myMacro[T]: Unit = ${ myMacroImpl[T] }; def myMacroImpl[T](using Quotes): Expr[Unit] = '{}; println(myMacro[Int])
44
| ^^^^^^^^^^^^
55
| Cannot call macro method myMacroImpl defined in the same source file
6-
| This location contains code that was inlined from rs$line$1:1
76
1 error found

project/scripts/cmdTests

+2-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ echo "testing that paths SourceFile annotations are relativized"
3333
clear_out "$OUT"
3434
"$SBT" "scalac -d $OUT/out.jar -sourceroot tests/pos $(pwd)/tests/pos/i10430/lib.scala $(pwd)/tests/pos/i10430/app.scala"
3535
"$SBT" "scalac -print-tasty -color:never $OUT/out.jar" > "$tmp"
36-
cat "$tmp" # for debugging
36+
# cat "$tmp" # for debugging
3737
grep -q ": i10430/lib.scala" "$tmp"
3838
grep -q ": i10430/app.scala" "$tmp"
3939
grep -q "[i10430/lib.scala]" "$tmp"
@@ -56,6 +56,7 @@ cp tests/neg/i6371/B_2.scala $OUT/B.scala
5656
"$SBT" "scalac $OUT/A.scala -d $OUT1"
5757
rm $OUT/A.scala
5858
"$SBT" "scalac -classpath $OUT1 -d $OUT1 $OUT/B.scala" > "$tmp" 2>&1 || echo "ok"
59+
cat "$tmp" # for debugging
5960
grep -qe "B.scala:2:7" "$tmp"
6061
grep -qe "This location contains code that was inlined from A.scala:3" "$tmp"
6162

tests/neg-macros/delegate-match-1.check

-1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,3 @@
44
| ^
55
| AmbiguousImplicits
66
| both value a1 in class Test1 and value a2 in class Test1 match type A
7-
| This location contains code that was inlined from Test_2.scala:6

tests/neg-macros/delegate-match-2.check

-1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,3 @@
44
| ^
55
| DivergingImplicit
66
| method a1 in class Test produces a diverging implicit search when trying to match type A
7-
| This location contains code that was inlined from Test_2.scala:5

tests/neg-macros/delegate-match-3.check

-1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,3 @@
44
| ^
55
| NoMatchingImplicits
66
| no implicit values were found that match type A
7-
| This location contains code that was inlined from Test_2.scala:3

tests/neg-macros/i11386.check

+14-4
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,21 @@
33
6 | dummy(0) // error
44
| ^
55
| test
6-
| This location contains code that was inlined from Test_2.scala:6
7-
| This location contains code that was inlined from Macro_1.scala:7
6+
|---------------------------------------------------------------------------------------------------------------------
7+
|Inline stack trace
8+
|·····················································································································
9+
|This location contains code that was inlined from Test_2.scala:6
10+
7 | notNull(i)
11+
| ^^^^^^^^^^
12+
·---------------------------------------------------------------------------------------------------------------------
813
-- Error: tests/neg-macros/i11386/Test_2.scala:8:20 --------------------------------------------------------------------
914
8 | dummy(int2String(0)) // error
1015
| ^^^^^^^^^^^^^
1116
| test
12-
| This location contains code that was inlined from Test_2.scala:8
13-
| This location contains code that was inlined from Macro_1.scala:7
17+
|---------------------------------------------------------------------------------------------------------------------
18+
|Inline stack trace
19+
|·····················································································································
20+
|This location contains code that was inlined from Test_2.scala:8
21+
7 | notNull(i)
22+
| ^^^^^^^^^^
23+
·---------------------------------------------------------------------------------------------------------------------

0 commit comments

Comments
 (0)