diff --git a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala index c2224f6c4567..80788a1ef715 100644 --- a/compiler/src/dotty/tools/dotc/transform/init/Errors.scala +++ b/compiler/src/dotty/tools/dotc/transform/init/Errors.scala @@ -5,6 +5,7 @@ package init import ast.tpd._ import core._ +import util.SourcePosition import Decorators._, printing.SyntaxHighlighting import Types._, Symbols._, Contexts._ @@ -26,13 +27,11 @@ object Errors { def toErrors: Errors = this :: Nil def stacktrace(using Context): String = if (trace.isEmpty) "" else " Calling trace:\n" + { - var indentCount = 0 var last: String = "" val sb = new StringBuilder trace.foreach { tree => - indentCount += 1 val pos = tree.sourcePos - val prefix = s"${ " " * indentCount }-> " + val prefix = "-> " val line = if pos.source.exists then val loc = "[ " + pos.source.file.name + ":" + (pos.line + 1) + " ]" @@ -40,14 +39,32 @@ object Errors { i"$code\t$loc" else tree.show + val positionMarkerLine = + if pos.exists && pos.source.exists then + positionMarker(pos) + else "" - if (last != line) sb.append(prefix + line + "\n") + if (last != line) sb.append(prefix + line + "\n" + positionMarkerLine ) last = line } sb.toString } + /** Used to underline source positions in the stack trace + * pos.source must exist + */ + private def positionMarker(pos: SourcePosition): String = { + val trimmed = pos.lineContent.takeWhile(c => c.isWhitespace).length + val padding = pos.startColumnPadding.substring(trimmed).nn + " " + val carets = + if (pos.startLine == pos.endLine) + "^" * math.max(1, pos.endColumn - pos.startColumn) + else "^" + + s"$padding$carets\n" + } + /** Flatten UnsafePromotion errors */ def flatten: Errors = this match { diff --git a/compiler/src/dotty/tools/dotc/util/SourceFile.scala b/compiler/src/dotty/tools/dotc/util/SourceFile.scala index 6df6c42b59b2..79378be78480 100644 --- a/compiler/src/dotty/tools/dotc/util/SourceFile.scala +++ b/compiler/src/dotty/tools/dotc/util/SourceFile.scala @@ -212,7 +212,7 @@ class SourceFile(val file: AbstractFile, computeContent: => Array[Char]) extends var idx = startOfLine(offset) val pad = new StringBuilder while (idx != offset) { - pad.append(if (idx < length && content()(idx) == '\t') '\t' else ' ') + pad.append(if (idx < content().length && content()(idx) == '\t') '\t' else ' ') idx += 1 } pad.result() diff --git a/tests/init/neg/cycle-structure.check b/tests/init/neg/cycle-structure.check index 79e38ee80bb4..2838532e08ed 100644 --- a/tests/init/neg/cycle-structure.check +++ b/tests/init/neg/cycle-structure.check @@ -2,13 +2,19 @@ 2 | val x1 = b.x // error | ^^^ | Access field A.this.b.x on a value with an unknown initialization status. Calling trace: - | -> val x = A(this) [ cycle-structure.scala:9 ] - | -> case class A(b: B) { [ cycle-structure.scala:1 ] + | -> val x = A(this) [ cycle-structure.scala:9 ] + | ^^^^^^^ + | -> case class A(b: B) { [ cycle-structure.scala:1 ] + | ^ -- Error: tests/init/neg/cycle-structure.scala:8:15 -------------------------------------------------------------------- 8 | val x1 = a.x // error | ^^^ | Access field B.this.a.x on a value with an unknown initialization status. Calling trace: - | -> val x = A(this) [ cycle-structure.scala:9 ] - | -> case class A(b: B) { [ cycle-structure.scala:1 ] - | -> val x = B(this) [ cycle-structure.scala:3 ] - | -> case class B(a: A) { [ cycle-structure.scala:7 ] + | -> val x = A(this) [ cycle-structure.scala:9 ] + | ^^^^^^^ + | -> case class A(b: B) { [ cycle-structure.scala:1 ] + | ^ + | -> val x = B(this) [ cycle-structure.scala:3 ] + | ^^^^^^^ + | -> case class B(a: A) { [ cycle-structure.scala:7 ] + | ^ diff --git a/tests/init/neg/default-this.check b/tests/init/neg/default-this.check index b9a74db0dc95..22ff677c4113 100644 --- a/tests/init/neg/default-this.check +++ b/tests/init/neg/default-this.check @@ -2,4 +2,5 @@ 9 | compare() // error | ^^^^^^^ |Cannot prove that the value is fully initialized. Only initialized values may be used as arguments. Calling trace: - | -> val result = updateThenCompare(5) [ default-this.scala:11 ] + |-> val result = updateThenCompare(5) [ default-this.scala:11 ] + | ^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/init/neg/enum-desugared.check b/tests/init/neg/enum-desugared.check index 6417b8c5cea7..dfea2038d0e6 100644 --- a/tests/init/neg/enum-desugared.check +++ b/tests/init/neg/enum-desugared.check @@ -5,8 +5,10 @@ | | The unsafe promotion may cause the following problem: | Calling the external method method name may cause initialization errors. Calling trace: - | -> Array(this.LazyErrorId, this.NoExplanationID) // error // error [ enum-desugared.scala:17 ] - | -> override def productPrefix: String = this.name() [ enum-desugared.scala:29 ] + | -> Array(this.LazyErrorId, this.NoExplanationID) // error // error [ enum-desugared.scala:17 ] + | ^^^^^^^^^^^^^^^^ + | -> override def productPrefix: String = this.name() [ enum-desugared.scala:29 ] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -- Error: tests/init/neg/enum-desugared.scala:17:33 -------------------------------------------------------------------- 17 | Array(this.LazyErrorId, this.NoExplanationID) // error // error | ^^^^^^^^^^^^^^^^^^^^ @@ -14,5 +16,7 @@ | | The unsafe promotion may cause the following problem: | Calling the external method method ordinal may cause initialization errors. Calling trace: - | -> Array(this.LazyErrorId, this.NoExplanationID) // error // error [ enum-desugared.scala:17 ] - | -> def errorNumber: Int = this.ordinal() - 2 [ enum-desugared.scala:8 ] + | -> Array(this.LazyErrorId, this.NoExplanationID) // error // error [ enum-desugared.scala:17 ] + | ^^^^^^^^^^^^^^^^^^^^ + | -> def errorNumber: Int = this.ordinal() - 2 [ enum-desugared.scala:8 ] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/init/neg/enum.check b/tests/init/neg/enum.check index eb48920b47ea..08c69e9bc246 100644 --- a/tests/init/neg/enum.check +++ b/tests/init/neg/enum.check @@ -5,4 +5,5 @@ | | The unsafe promotion may cause the following problem: | Calling the external method method name may cause initialization errors. Calling trace: - | -> NoExplanationID // error [ enum.scala:4 ] + | -> NoExplanationID // error [ enum.scala:4 ] + | ^ diff --git a/tests/init/neg/inherit-non-hot.check b/tests/init/neg/inherit-non-hot.check index af95b2a7284b..432ddfa276a5 100644 --- a/tests/init/neg/inherit-non-hot.check +++ b/tests/init/neg/inherit-non-hot.check @@ -3,14 +3,22 @@ | ^^^^^^^^^^^ | Cannot prove that the value is fully initialized. May only assign fully initialized value. | Calling trace: - | -> val c = new C [ inherit-non-hot.scala:19 ] - | -> class C extends A { [ inherit-non-hot.scala:15 ] - | -> val bAgain = toB.getBAgain [ inherit-non-hot.scala:16 ] + | -> val c = new C [ inherit-non-hot.scala:19 ] + | ^^^^^ + | -> class C extends A { [ inherit-non-hot.scala:15 ] + | ^ + | -> val bAgain = toB.getBAgain [ inherit-non-hot.scala:16 ] + | ^^^ | | The unsafe promotion may cause the following problem: | Call method Foo.B.this.aCopy.toB on a value with an unknown initialization. Calling trace: - | -> val c = new C [ inherit-non-hot.scala:19 ] - | -> class C extends A { [ inherit-non-hot.scala:15 ] - | -> val bAgain = toB.getBAgain [ inherit-non-hot.scala:16 ] - | -> if b == null then b = new B(this) // error [ inherit-non-hot.scala:6 ] - | -> def getBAgain: B = aCopy.toB [ inherit-non-hot.scala:12 ] + | -> val c = new C [ inherit-non-hot.scala:19 ] + | ^^^^^ + | -> class C extends A { [ inherit-non-hot.scala:15 ] + | ^ + | -> val bAgain = toB.getBAgain [ inherit-non-hot.scala:16 ] + | ^^^ + | -> if b == null then b = new B(this) // error [ inherit-non-hot.scala:6 ] + | ^^^^^^^^^^^ + | -> def getBAgain: B = aCopy.toB [ inherit-non-hot.scala:12 ] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/init/neg/inlined-method.check b/tests/init/neg/inlined-method.check index bd787205746c..851e8dea40fe 100644 --- a/tests/init/neg/inlined-method.check +++ b/tests/init/neg/inlined-method.check @@ -2,4 +2,5 @@ 8 | scala.runtime.Scala3RunTime.assertFailed(message) // error | ^^^^^^^ |Cannot prove that the value is fully initialized. Only initialized values may be used as arguments. Calling trace: - | -> Assertion.failAssert(this) [ inlined-method.scala:2 ] + |-> Assertion.failAssert(this) [ inlined-method.scala:2 ] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/init/neg/local-warm4.check b/tests/init/neg/local-warm4.check index fda1ee1b928c..22265d1f4d61 100644 --- a/tests/init/neg/local-warm4.check +++ b/tests/init/neg/local-warm4.check @@ -2,10 +2,17 @@ 18 | a = newA // error | ^^^^ | Cannot prove that the value is fully initialized. May only assign fully initialized value. Calling trace: - | -> val a = new A(5) [ local-warm4.scala:26 ] - | -> class A(x: Int) extends Foo(x) { [ local-warm4.scala:6 ] - | -> val b = new B(y) [ local-warm4.scala:10 ] - | -> class B(x: Int) extends A(x) { [ local-warm4.scala:13 ] - | -> class A(x: Int) extends Foo(x) { [ local-warm4.scala:6 ] - | -> increment() [ local-warm4.scala:9 ] - | -> updateA() [ local-warm4.scala:21 ] + | -> val a = new A(5) [ local-warm4.scala:26 ] + | ^^^^^^^^ + | -> class A(x: Int) extends Foo(x) { [ local-warm4.scala:6 ] + | ^ + | -> val b = new B(y) [ local-warm4.scala:10 ] + | ^^^^^^^^ + | -> class B(x: Int) extends A(x) { [ local-warm4.scala:13 ] + | ^ + | -> class A(x: Int) extends Foo(x) { [ local-warm4.scala:6 ] + | ^ + | -> increment() [ local-warm4.scala:9 ] + | ^^^^^^^^^^^ + | -> updateA() [ local-warm4.scala:21 ] + | ^^^^^^^^^ diff --git a/tests/init/neg/t3273.check b/tests/init/neg/t3273.check index 531e8f6f5ad3..8c7f0f32e2d8 100644 --- a/tests/init/neg/t3273.check +++ b/tests/init/neg/t3273.check @@ -1,16 +1,18 @@ -- Error: tests/init/neg/t3273.scala:4:42 ------------------------------------------------------------------------------ 4 | val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error | ^^^^^^^^^^^^^^^ - | Cannot prove that the value is fully initialized. Only initialized values may be used as arguments. + | Cannot prove that the value is fully initialized. Only initialized values may be used as arguments. | - | The unsafe promotion may cause the following problem: - | Access non-initialized value num1. Calling trace: + | The unsafe promotion may cause the following problem: + | Access non-initialized value num1. Calling trace: | -> val num1: LazyList[Int] = 1 #:: num1.map(_ + 1) // error [ t3273.scala:4 ] + | ^^^^ -- Error: tests/init/neg/t3273.scala:5:61 ------------------------------------------------------------------------------ 5 | val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | Cannot prove that the value is fully initialized. Only initialized values may be used as arguments. + | Cannot prove that the value is fully initialized. Only initialized values may be used as arguments. | - | The unsafe promotion may cause the following problem: - | Access non-initialized value num2. Calling trace: + | The unsafe promotion may cause the following problem: + | Access non-initialized value num2. Calling trace: | -> val num2: LazyList[Int] = 1 #:: num2.iterator.map(_ + 1).to(LazyList) // error [ t3273.scala:5 ] + | ^^^^ diff --git a/tests/init/neg/unsound2.check b/tests/init/neg/unsound2.check index 346caec4cb1b..3e54219e0819 100644 --- a/tests/init/neg/unsound2.check +++ b/tests/init/neg/unsound2.check @@ -1,6 +1,8 @@ -- Error: tests/init/neg/unsound2.scala:5:26 --------------------------------------------------------------------------- 5 | def getN: Int = a.n // error | ^^^ - | Access field B.this.a.n on a value with an unknown initialization status. Calling trace: - | -> println(foo(x).getB) [ unsound2.scala:8 ] + | Access field B.this.a.n on a value with an unknown initialization status. Calling trace: + | -> println(foo(x).getB) [ unsound2.scala:8 ] + | ^^^^^^ | -> def foo(y: Int): B = if (y > 10) then B(bar(y - 1), foo(y - 1).getN) else B(bar(y), 10) [ unsound2.scala:2 ] + | ^^^^^^^^^^^^^^^ diff --git a/tests/init/neg/unsound3.check b/tests/init/neg/unsound3.check index 71766cf2d10b..6f8a440f186e 100644 --- a/tests/init/neg/unsound3.check +++ b/tests/init/neg/unsound3.check @@ -2,4 +2,5 @@ 10 | if (x < 12) then foo().getC().b else newB // error | ^^^^^^^^^^^^^^ | Access field C.this.foo().getC().b on a value with an unknown initialization status. Calling trace: - | -> val b = foo() [ unsound3.scala:12 ] + | -> val b = foo() [ unsound3.scala:12 ] + | ^^^^^ diff --git a/tests/init/neg/unsound4.check b/tests/init/neg/unsound4.check index 4ed254444928..c09e878c8e42 100644 --- a/tests/init/neg/unsound4.check +++ b/tests/init/neg/unsound4.check @@ -2,5 +2,7 @@ 3 | val aAgain = foo(5) // error | ^ | Access non-initialized value aAgain. Calling trace: - | -> val aAgain = foo(5) // error [ unsound4.scala:3 ] - | -> def foo(x: Int): A = if (x < 5) then this else foo(x - 1).aAgain [ unsound4.scala:2 ] + | -> val aAgain = foo(5) // error [ unsound4.scala:3 ] + | ^^^^^^ + | -> def foo(x: Int): A = if (x < 5) then this else foo(x - 1).aAgain [ unsound4.scala:2 ] + | ^^^^^^^^^^^^^^^^^