Skip to content

Commit a436521

Browse files
committed
Improve stub error messages (SCP-009 proposal)
The following commit message is a squash of several commit messages. - This is the 1st commit message: Add position to stub error messages Stub errors happen when we've started the initialization of a symbol but key information of this symbol is missing (the information cannot be found in any entry of the classpath not sources). When this error happens, we better have a good error message with a position to the place where the stub error came from. This commit goes into this direction by adding a `pos` value to `StubSymbol` and filling it in in all the use sites (especifically `UnPickler`). This commit also changes some tests that test stub errors-related issues. Concretely, `t6440` is using special Partest infrastructure and doens't pretty print the position, while `t5148` which uses the conventional infrastructure does. Hence the difference in the changes for both tests. - This is the commit message #2: Add partest infrastructure to test stub errors `StubErrorMessageTest` is the friend I introduce in this commit to help state stub errors. The strategy to test them is easy and builds upon previous concepts: we reuse `StoreReporterDirectTest` and add some methods that will compile the code and simulate a missing classpath entry by removing the class files from the class directory (the folder where Scalac compiles to). This first iteration allow us to programmatically check that stub errors are emitted under certain conditions. - This is the commit message #3: Improve contents of stub error message This commit does three things: * Keep track of completing symbol while unpickling First, it removes the previous `symbolOnCompletion` definition to be more restrictive/clear and use only positions, since only positions are used to report the error (the rest of the information comes from the context of the `UnPickler`). Second, it adds a new variable called `lazyCompletingSymbol` that is responsible for keeping a reference to the symbol that produces the stub error. This symbol will usually (always?) come from the classpath entries and therefore we don't have its position (that's why we keep track of `symbolOnCompletion` as well). This is the one that we have to explicitly use in the stub error message, the culprit so to speak. Aside from these two changes, this commit modifies the existing tests that are affected by the change in the error message, which is more precise now, and adds new tests for stub errors that happen in complex inner cases and in return type of `MethodType`. * Check that order of initialization is correct With the changes introduced previously to keep track of position of symbols coming from source files, we may ask ourselves: is this going to work always? What happens if two symbols the initialization of two symbols is intermingled and the stub error message gets the wrong position? This commit adds a test case and modifications to the test infrastructure to double check empirically that this does not happen. Usually, this interaction in symbol initialization won't happen because the `UnPickler` will lazily load all the buckets necessary for a symbol to be truly initialized, with the pertinent addresses from which this information has to be deserialized. This ensures that this operation is atomic and no other symbol initialization can happen in the meantime. Even though the previous paragraph is the feeling I got from reading the sources, this commit creates a test to double-check it. My attempt to be better safe than sorry. * Improve contents of the stub error message This commit modifies the format of the previous stub error message by being more precise in its formulation. It follows the structured format: ``` s"""|Symbol '${name.nameKind} ${owner.fullName}.$name' is missing from the classpath. |This symbol is required by '${lazyCompletingSymbol.kindString} ${lazyCompletingSymbol.fullName}'. ``` This format has the advantage that is more readable and explicit on what's happening. First, we report what is missing. Then, why it was required. Hopefully, people working on direct dependencies will find the new message friendlier. Having a good test suite to check the previously added code is important. This commit checks that stub errors happen in presence of well-known and widely used Scala features. These include: * Higher kinded types. * Type definitions. * Inheritance and subclasses. * Typeclasses and implicits. - This is the commit message #4: Use `lastTreeToTyper` to get better positions The previous strategy to get the last user-defined position for knowing what was the root cause (the trigger) of stub errors relied on instrumenting `def info`. This instrumentation, while easy to implement, is inefficient since we register the positions for symbols that are already completed. However, we cannot do it only for uncompleted symbols (!hasCompleteInfo) because the positions won't be correct anymore -- definitions using stub symbols (val b = new B) are for the compiler completed, but their use throws stub errors. This means that if we initialize symbols between a definition and its use, we'll use their positions instead of the position of `b`. To work around this we use `lastTreeToTyper`. We assume that stub errors will be thrown by Typer at soonest. The benefit of this approach is better error messages. The positions used in them are now as concrete as possible since they point to the exact tree that **uses** a symbol, instead of the one that **defines** it. Have a look at `StubErrorComplexInnerClass` for an example. This commit removes the previous infrastructure and replaces it by the new one. It also removes the fields positions from the subclasses of `StubSymbol`s. - This is the commit message #5: Keep track of completing symbols Make sure that cycles don't happen by keeping track of all the symbols that are being completed by `completeInternal`. Stub errors only need the last completing symbols, but the whole stack of symbols may be useful to reporting other error like cyclic initialization issues. I've added this per Jason's suggestion. I've implemented with a list because `remove` in an array buffer is linear. Array was not an option because I would need to resize it myself. I think that even though list is not as efficient memory-wise, it probably doesn't matter since the stack will usually be small. - This is the commit message #6: Remove `isPackage` from `newStubSymbol` Remove `isPackage` since in 2.12.x its value is not used.
1 parent 23e5ed9 commit a436521

28 files changed

+462
-35
lines changed

src/compiler/scala/tools/nsc/Global.scala

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,19 @@ class Global(var currentSettings: Settings, var reporter: Reporter)
8686

8787
def erasurePhase: Phase = if (currentRun.isDefined) currentRun.erasurePhase else NoPhase
8888

89+
/* Override `newStubSymbol` defined in `SymbolTable` to provide us access
90+
* to the last tree to typer, whose position is the trigger of stub errors. */
91+
override def newStubSymbol(owner: Symbol,
92+
name: Name,
93+
missingMessage: String): Symbol = {
94+
val stubSymbol = super.newStubSymbol(owner, name, missingMessage)
95+
val stubErrorPosition = {
96+
val lastTreeToTyper = analyzer.lastTreeToTyper
97+
if (lastTreeToTyper != EmptyTree) lastTreeToTyper.pos else stubSymbol.pos
98+
}
99+
stubSymbol.setPos(stubErrorPosition)
100+
}
101+
89102
// platform specific elements
90103

91104
protected class GlobalPlatform extends {

src/compiler/scala/tools/nsc/symtab/classfile/ClassfileParser.scala

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1051,8 +1051,11 @@ abstract class ClassfileParser {
10511051
val sflags = jflags.toScalaFlags
10521052
val owner = ownerForFlags(jflags)
10531053
val scope = getScope(jflags)
1054-
def newStub(name: Name) =
1055-
owner.newStubSymbol(name, s"Class file for ${entry.externalName} not found").setFlag(JAVA)
1054+
def newStub(name: Name) = {
1055+
val stub = owner.newStubSymbol(name, s"Class file for ${entry.externalName} not found")
1056+
stub.setPos(owner.pos)
1057+
stub.setFlag(JAVA)
1058+
}
10561059

10571060
val (innerClass, innerModule) = if (file == NoAbstractFile) {
10581061
(newStub(name.toTypeName), newStub(name.toTermName))
@@ -1174,7 +1177,11 @@ abstract class ClassfileParser {
11741177
if (enclosing == clazz) entry.scope lookup name
11751178
else lookupMemberAtTyperPhaseIfPossible(enclosing, name)
11761179
}
1177-
def newStub = enclosing.newStubSymbol(name, s"Unable to locate class corresponding to inner class entry for $name in owner ${entry.outerName}")
1180+
def newStub = {
1181+
enclosing
1182+
.newStubSymbol(name, s"Unable to locate class corresponding to inner class entry for $name in owner ${entry.outerName}")
1183+
.setPos(enclosing.pos)
1184+
}
11781185
member.orElse(newStub)
11791186
}
11801187
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package scala.tools.partest
2+
3+
trait StubErrorMessageTest extends StoreReporterDirectTest {
4+
// Stub to feed to partest, unused
5+
def code = throw new Error("Use `userCode` instead of `code`.")
6+
7+
val classpath = List(sys.props("partest.lib"), testOutput.path)
8+
.mkString(sys.props("path.separator"))
9+
10+
def compileCode(codes: String*) = {
11+
val global = newCompiler("-cp", classpath, "-d", testOutput.path)
12+
val sourceFiles = newSources(codes: _*)
13+
withRun(global)(_ compileSources sourceFiles)
14+
}
15+
16+
def removeClasses(inPackage: String, classNames: Seq[String]): Unit = {
17+
val pkg = new File(testOutput.path, inPackage)
18+
classNames.foreach { className =>
19+
val classFile = new File(pkg, s"$className.class")
20+
assert(classFile.exists)
21+
assert(classFile.delete())
22+
}
23+
}
24+
25+
def removeFromClasspath(): Unit
26+
def codeA: String
27+
def codeB: String
28+
def userCode: String
29+
def extraUserCode: String = ""
30+
31+
def show(): Unit = {
32+
compileCode(codeA)
33+
assert(filteredInfos.isEmpty, filteredInfos)
34+
35+
compileCode(codeB)
36+
assert(filteredInfos.isEmpty, filteredInfos)
37+
removeFromClasspath()
38+
39+
if (extraUserCode == "") compileCode(userCode)
40+
else compileCode(userCode, extraUserCode)
41+
import scala.reflect.internal.util.Position
42+
filteredInfos.map { report =>
43+
print(if (report.severity == storeReporter.ERROR) "error: " else "")
44+
println(Position.formatMessage(report.pos, report.msg, true))
45+
}
46+
}
47+
}

src/reflect/scala/reflect/internal/Symbols.scala

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,15 @@ trait Symbols extends api.Symbols { self: SymbolTable =>
193193

194194
private[reflect] case class SymbolKind(accurate: String, sanitized: String, abbreviation: String)
195195

196+
protected def newStubSymbol(owner: Symbol,
197+
name: Name,
198+
missingMessage: String): Symbol = {
199+
name match {
200+
case n: TypeName => new StubClassSymbol(owner, n, missingMessage)
201+
case _ => new StubTermSymbol(owner, name.toTermName, missingMessage)
202+
}
203+
}
204+
196205
/** The class for all symbols */
197206
abstract class Symbol protected[Symbols] (initOwner: Symbol, initPos: Position, initName: Name)
198207
extends SymbolContextApiImpl
@@ -504,9 +513,9 @@ trait Symbols extends api.Symbols { self: SymbolTable =>
504513
* failure to the point when that name is used for something, which is
505514
* often to the point of never.
506515
*/
507-
def newStubSymbol(name: Name, missingMessage: String, isPackage: Boolean = false): Symbol = name match {
508-
case n: TypeName => new StubClassSymbol(this, n, missingMessage)
509-
case _ => new StubTermSymbol(this, name.toTermName, missingMessage)
516+
def newStubSymbol(name: Name, missingMessage: String): Symbol = {
517+
// Invoke the overriden `newStubSymbol` in Global that gives us access to typer
518+
Symbols.this.newStubSymbol(this, name, missingMessage)
510519
}
511520

512521
/** Given a field, construct a term symbol that represents the source construct that gave rise the field */
@@ -3427,7 +3436,7 @@ trait Symbols extends api.Symbols { self: SymbolTable =>
34273436
private def fail[T](alt: T): T = {
34283437
// Avoid issuing lots of redundant errors
34293438
if (!hasFlag(IS_ERROR)) {
3430-
globalError(missingMessage)
3439+
globalError(pos, missingMessage)
34313440
if (settings.debug.value)
34323441
(new Throwable).printStackTrace
34333442

src/reflect/scala/reflect/internal/pickling/UnPickler.scala

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -246,14 +246,15 @@ abstract class UnPickler {
246246
adjust(mirrorThatLoaded(owner).missingHook(owner, name)) orElse {
247247
// (4) Create a stub symbol to defer hard failure a little longer.
248248
val advice = moduleAdvice(s"${owner.fullName}.$name")
249+
val lazyCompletingSymbol = completingStack.headOption.getOrElse(NoSymbol)
249250
val missingMessage =
250-
s"""|missing or invalid dependency detected while loading class file '$filename'.
251-
|Could not access ${name.longString} in ${owner.kindString} ${owner.fullName},
252-
|because it (or its dependencies) are missing. Check your build definition for
253-
|missing or conflicting dependencies. (Re-run with `-Ylog-classpath` to see the problematic classpath.)
251+
s"""|Symbol '${name.nameKind} ${owner.fullName}.$name' is missing from the classpath.
252+
|This symbol is required by '${lazyCompletingSymbol.kindString} ${lazyCompletingSymbol.fullName}'.
253+
|Make sure that ${name.longString} is in your classpath and check for conflicting dependencies with `-Ylog-classpath`.
254254
|A full rebuild may help if '$filename' was compiled against an incompatible version of ${owner.fullName}.$advice""".stripMargin
255255
val stubName = if (tag == EXTref) name else name.toTypeName
256-
owner.newStubSymbol(stubName, missingMessage)
256+
// The position of the error message is set by `newStubSymbol`
257+
NoSymbol.newStubSymbol(stubName, missingMessage)
257258
}
258259
}
259260
}
@@ -696,11 +697,18 @@ abstract class UnPickler {
696697
new TypeError(e.msg)
697698
}
698699

700+
/** Keep track of the symbols pending to be initialized.
701+
*
702+
* Useful for reporting on stub errors and cyclic errors.
703+
*/
704+
private var completingStack = List.empty[Symbol]
705+
699706
/** A lazy type which when completed returns type at index `i`. */
700707
private class LazyTypeRef(i: Int) extends LazyType with FlagAgnosticCompleter {
701708
private val definedAtRunId = currentRunId
702709
private val p = phase
703710
protected def completeInternal(sym: Symbol) : Unit = try {
711+
completingStack = sym :: completingStack
704712
val tp = at(i, () => readType(sym.isTerm)) // after NMT_TRANSITION, revert `() => readType(sym.isTerm)` to `readType`
705713

706714
// This is a temporary fix allowing to read classes generated by an older, buggy pickler.
@@ -723,7 +731,10 @@ abstract class UnPickler {
723731
}
724732
catch {
725733
case e: MissingRequirementError => throw toTypeError(e)
734+
} finally {
735+
completingStack = completingStack.tail
726736
}
737+
727738
override def complete(sym: Symbol) : Unit = {
728739
completeInternal(sym)
729740
if (!isCompilerUniverse) markAllCompleted(sym)

test/files/neg/t5148.check

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
1-
error: missing or invalid dependency detected while loading class file 'Imports.class'.
2-
Could not access term memberHandlers in class scala.tools.nsc.interpreter.IMain,
3-
because it (or its dependencies) are missing. Check your build definition for
4-
missing or conflicting dependencies. (Re-run with `-Ylog-classpath` to see the problematic classpath.)
1+
t5148.scala:4: error: Symbol 'term scala.tools.nsc.interpreter.IMain.memberHandlers' is missing from the classpath.
2+
This symbol is required by 'method scala.tools.nsc.interpreter.Imports.allReqAndHandlers'.
3+
Make sure that term memberHandlers is in your classpath and check for conflicting dependencies with `-Ylog-classpath`.
54
A full rebuild may help if 'Imports.class' was compiled against an incompatible version of scala.tools.nsc.interpreter.IMain.
6-
error: missing or invalid dependency detected while loading class file 'Imports.class'.
7-
Could not access type Wrapper in class scala.tools.nsc.interpreter.IMain.Request,
8-
because it (or its dependencies) are missing. Check your build definition for
9-
missing or conflicting dependencies. (Re-run with `-Ylog-classpath` to see the problematic classpath.)
10-
A full rebuild may help if 'Imports.class' was compiled against an incompatible version of scala.tools.nsc.interpreter.IMain.Request.
11-
error: missing or invalid dependency detected while loading class file 'Imports.class'.
12-
Could not access type Request in class scala.tools.nsc.interpreter.IMain,
13-
because it (or its dependencies) are missing. Check your build definition for
14-
missing or conflicting dependencies. (Re-run with `-Ylog-classpath` to see the problematic classpath.)
15-
A full rebuild may help if 'Imports.class' was compiled against an incompatible version of scala.tools.nsc.interpreter.IMain.
16-
three errors found
5+
class IMain extends Imports
6+
^
7+
t5148.scala:4: error: Symbol 'type <none>.Request.Wrapper' is missing from the classpath.
8+
This symbol is required by 'value scala.tools.nsc.interpreter.Imports.wrapper'.
9+
Make sure that type Wrapper is in your classpath and check for conflicting dependencies with `-Ylog-classpath`.
10+
A full rebuild may help if 'Imports.class' was compiled against an incompatible version of <none>.Request.
11+
class IMain extends Imports
12+
^
13+
two errors found
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
error: newSource1.scala:4: Symbol 'type stuberrors.A' is missing from the classpath.
2+
This symbol is required by 'class stuberrors.B'.
3+
Make sure that type A is in your classpath and check for conflicting dependencies with `-Ylog-classpath`.
4+
A full rebuild may help if 'B.class' was compiled against an incompatible version of stuberrors.
5+
new B
6+
^
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
object Test extends scala.tools.partest.StubErrorMessageTest {
2+
def codeA = """
3+
package stuberrors
4+
class A
5+
"""
6+
7+
def codeB = """
8+
package stuberrors
9+
class B extends A
10+
"""
11+
12+
def userCode = """
13+
package stuberrors
14+
class C {
15+
new B
16+
}
17+
"""
18+
19+
def removeFromClasspath(): Unit = {
20+
removeClasses("stuberrors", List("A"))
21+
}
22+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
error: newSource1.scala:9: Symbol 'type stuberrors.A' is missing from the classpath.
2+
This symbol is required by 'class stuberrors.B.BB'.
3+
Make sure that type A is in your classpath and check for conflicting dependencies with `-Ylog-classpath`.
4+
A full rebuild may help if 'B.class' was compiled against an incompatible version of stuberrors.
5+
new b.BB
6+
^
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
object Test extends scala.tools.partest.StubErrorMessageTest {
2+
def codeA = """
3+
package stuberrors
4+
class A
5+
"""
6+
7+
def codeB = """
8+
package stuberrors
9+
class B {
10+
def foo: String = ???
11+
12+
// unused and should fail, but not loaded
13+
def unsafeFoo: A = ???
14+
// used, B.info -> BB.info -> unpickling A -> stub error
15+
class BB extends A
16+
}
17+
"""
18+
19+
def userCode = """
20+
package stuberrors
21+
class C {
22+
def aloha = {
23+
val b = new B
24+
val d = new extra.D
25+
d.foo
26+
println(b.foo)
27+
new b.BB
28+
}
29+
}
30+
"""
31+
32+
override def extraUserCode = """
33+
package extra
34+
class D {
35+
def foo = "Hello, World"
36+
}
37+
""".stripMargin
38+
39+
def removeFromClasspath(): Unit = {
40+
removeClasses("stuberrors", List("A"))
41+
}
42+
}

test/files/run/StubErrorHK.check

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
error: newSource1.scala:4: Symbol 'type stuberrors.A' is missing from the classpath.
2+
This symbol is required by 'type stuberrors.B.D'.
3+
Make sure that type A is in your classpath and check for conflicting dependencies with `-Ylog-classpath`.
4+
A full rebuild may help if 'B.class' was compiled against an incompatible version of stuberrors.
5+
println(new B)
6+
^

test/files/run/StubErrorHK.scala

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
object Test extends scala.tools.partest.StubErrorMessageTest {
2+
def codeA = """
3+
package stuberrors
4+
class A
5+
"""
6+
7+
def codeB = """
8+
package stuberrors
9+
class B[D <: A]
10+
"""
11+
12+
def userCode = """
13+
package stuberrors
14+
object C extends App {
15+
println(new B)
16+
}
17+
"""
18+
19+
def removeFromClasspath(): Unit = {
20+
removeClasses("stuberrors", List("A"))
21+
}
22+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
error: newSource1.scala:13: Symbol 'type stuberrors.A' is missing from the classpath.
2+
This symbol is required by 'method stuberrors.B.foo'.
3+
Make sure that type A is in your classpath and check for conflicting dependencies with `-Ylog-classpath`.
4+
A full rebuild may help if 'B.class' was compiled against an incompatible version of stuberrors.
5+
b.foo
6+
^
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
object Test extends scala.tools.partest.StubErrorMessageTest {
2+
def codeA = """
3+
package stuberrors
4+
class A
5+
class AA
6+
"""
7+
8+
def codeB = """
9+
package stuberrors
10+
11+
abstract class B {
12+
def bar: String = ???
13+
def foo: A = new A
14+
def baz: String = ???
15+
}
16+
"""
17+
18+
def userCode = """
19+
package stuberrors
20+
21+
abstract class C extends App {
22+
val b = new B {}
23+
24+
// Use other symbols in the meanwhile
25+
val aa = new AA
26+
val dummy = 1
27+
println(dummy)
28+
29+
// Should blow up
30+
b.foo
31+
}
32+
"""
33+
34+
def removeFromClasspath(): Unit = {
35+
removeClasses("stuberrors", List("A"))
36+
}
37+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
error: newSource1.scala:13: Symbol 'type stuberrors.A' is missing from the classpath.
2+
This symbol is required by 'method stuberrors.B.foo'.
3+
Make sure that type A is in your classpath and check for conflicting dependencies with `-Ylog-classpath`.
4+
A full rebuild may help if 'B.class' was compiled against an incompatible version of stuberrors.
5+
b.foo
6+
^

0 commit comments

Comments
 (0)