Skip to content

Add underlining to safe-init stack traces #14683

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions compiler/src/dotty/tools/dotc/transform/init/Errors.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package init

import ast.tpd._
import core._
import util.SourcePosition
import Decorators._, printing.SyntaxHighlighting
import Types._, Symbols._, Contexts._

Expand All @@ -26,28 +27,44 @@ 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) + " ]"
val code = SyntaxHighlighting.highlight(pos.lineContent.trim.nn)
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 {
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/util/SourceFile.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
18 changes: 12 additions & 6 deletions tests/init/neg/cycle-structure.check
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]
| ^
3 changes: 2 additions & 1 deletion tests/init/neg/default-this.check
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]
| ^^^^^^^^^^^^^^^^^^^^
12 changes: 8 additions & 4 deletions tests/init/neg/enum-desugared.check
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@
|
| 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
| ^^^^^^^^^^^^^^^^^^^^
| Cannot prove that the value is fully initialized. May only use initialized value as method arguments.
|
| 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 ]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
3 changes: 2 additions & 1 deletion tests/init/neg/enum.check
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]
| ^
24 changes: 16 additions & 8 deletions tests/init/neg/inherit-non-hot.check
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
3 changes: 2 additions & 1 deletion tests/init/neg/inlined-method.check
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
21 changes: 14 additions & 7 deletions tests/init/neg/local-warm4.check
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]
| ^^^^^^^^^
14 changes: 8 additions & 6 deletions tests/init/neg/t3273.check
Original file line number Diff line number Diff line change
@@ -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 ]
| ^^^^
6 changes: 4 additions & 2 deletions tests/init/neg/unsound2.check
Original file line number Diff line number Diff line change
@@ -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 ]
| ^^^^^^^^^^^^^^^
3 changes: 2 additions & 1 deletion tests/init/neg/unsound3.check
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]
| ^^^^^
6 changes: 4 additions & 2 deletions tests/init/neg/unsound4.check
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]
| ^^^^^^^^^^^^^^^^^