Skip to content

Commit 0e350d5

Browse files
committed
Fix undercompilation when depending on inner class
To communicate to sbt a dependency between a class currently being compiled and some other class on the classpath, we use the `binaryDependency` callback which requires passing the "binary class name": for a class A in a package pkg, this is just "pkg.A", but for an inner class B in A, that would be "pkg.A$B", in other words the last component of the binary class name is the name of the corresponding classfile. Before this commit, we computed this name by extracting it from the path of the `associatedFile` but it turns out that `associatedFile` for an inner Scala class returns the classfile of the corresponding top-level class! This happens because the compiler never actually reads inner Scala classfiles since all the information is present in the top-level .class and .tasty files. This means that under separate compilation a dependency to an inner class was not recorded which could lead to undercompilation (see added tests). This commit fixes this by computing binary class names manually: they're just made of the fullName of the enclosing package, followed by ".", followed by the flatName of the current class. This is similar to what is done in the Scala 2 compiler bridge in Zinc.
1 parent 8442319 commit 0e350d5

File tree

18 files changed

+148
-18
lines changed

18 files changed

+148
-18
lines changed

compiler/src/dotty/tools/dotc/core/Symbols.scala

+2
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,8 @@ object Symbols {
250250

251251
/** The source or class file from which this class or
252252
* the class containing this symbol was generated, null if not applicable.
253+
* Note that this the returned classfile might be the top-level class
254+
* containing this symbol instead of the directly enclosing class.
253255
* Overridden in ClassSymbol
254256
*/
255257
def associatedFile(using Context): AbstractFile =

compiler/src/dotty/tools/dotc/sbt/ExtractDependencies.scala

+26-18
Original file line numberDiff line numberDiff line change
@@ -112,32 +112,22 @@ class ExtractDependencies extends Phase {
112112
def binaryDependency(file: File, binaryClassName: String) =
113113
ctx.sbtCallback.binaryDependency(file, binaryClassName, fromClassName, sourceFile, dep.context)
114114

115-
def processExternalDependency(depFile: AbstractFile) = {
116-
def binaryClassName(classSegments: List[String]) =
117-
classSegments.mkString(".").stripSuffix(".class")
118-
115+
def processExternalDependency(depFile: AbstractFile, binaryClassName: String) = {
119116
depFile match {
120117
case ze: ZipArchive#Entry => // The dependency comes from a JAR
121-
for (zip <- ze.underlyingSource; zipFile <- Option(zip.file)) {
122-
val classSegments = io.File(ze.path).segments
123-
binaryDependency(zipFile, binaryClassName(classSegments))
124-
}
125-
118+
ze.underlyingSource match
119+
case Some(zip) if zip.file != null =>
120+
binaryDependency(zip.file, binaryClassName)
121+
case _ =>
126122
case pf: PlainFile => // The dependency comes from a class file
127-
val packages = dep.to.ownersIterator
128-
.count(x => x.is(PackageClass) && !x.isEffectiveRoot)
129-
// We can recover the fully qualified name of a classfile from
130-
// its path
131-
val classSegments = pf.givenPath.segments.takeRight(packages + 1)
132123
// FIXME: pf.file is null for classfiles coming from the modulepath
133124
// (handled by JrtClassPath) because they cannot be represented as
134125
// java.io.File, since the `binaryDependency` callback must take a
135126
// java.io.File, this means that we cannot record dependencies coming
136127
// from the modulepath. For now this isn't a big deal since we only
137128
// support having the standard Java library on the modulepath.
138-
if (pf.file != null)
139-
binaryDependency(pf.file, binaryClassName(classSegments))
140-
129+
if pf.file != null then
130+
binaryDependency(pf.file, binaryClassName)
141131
case _ =>
142132
report.warning(s"sbt-deps: Ignoring dependency $depFile of class ${depFile.getClass}}")
143133
}
@@ -149,7 +139,25 @@ class ExtractDependencies extends Phase {
149139
def allowLocal = dep.context == DependencyByInheritance || dep.context == LocalDependencyByInheritance
150140
if (depFile.extension == "class") {
151141
// Dependency is external -- source is undefined
152-
processExternalDependency(depFile)
142+
143+
// The fully qualified name on the JVM of the class corresponding to `dep.to`
144+
val binaryClassName = {
145+
val builder = new StringBuilder
146+
val pkg = dep.to.enclosingPackageClass
147+
if (!pkg.isEffectiveRoot) {
148+
builder.append(pkg.fullName.mangledString)
149+
builder.append(".")
150+
}
151+
val flatName = dep.to.flatName
152+
// We create fake companion object symbols to hold the static members
153+
// of Java classes, make sure to use the name of the actual Java class
154+
// here.
155+
val clsFlatName = if (dep.to.is(JavaDefined)) flatName.stripModuleClassSuffix else flatName
156+
builder.append(clsFlatName.mangledString)
157+
builder.toString
158+
}
159+
160+
processExternalDependency(depFile, binaryClassName)
153161
} else if (allowLocal || depFile.file != sourceFile) {
154162
// We cannot ignore dependencies coming from the same source file because
155163
// the dependency info needs to propagate. See source-dependencies/trait-trait-211.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
object A {
2+
class InnerClass {
3+
def foo: Int = 1
4+
}
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
object B {
2+
def main(args: Array[String]): Unit = {
3+
val innerClass = new A.InnerClass
4+
val x = innerClass.foo
5+
println(x)
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import complete.DefaultParsers._
2+
3+
val checkIterations = inputKey[Unit]("Verifies the accumlated number of iterations of incremental compilation.")
4+
5+
checkIterations := {
6+
val analysis = (compile in Compile).value.asInstanceOf[sbt.internal.inc.Analysis]
7+
8+
val expected: Int = (Space ~> NatBasic).parsed
9+
val actual: Int = analysis.compilations.allCompilations.size
10+
assert(expected == actual, s"Expected $expected compilations, got $actual")
11+
}
12+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
object A {
2+
class InnerClass {
3+
def foo: String = "foo"
4+
}
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
object B {
2+
def main(args: Array[String]): Unit = {
3+
val innerClass = new A.InnerClass
4+
val x = innerClass.foo
5+
println(x)
6+
}
7+
val forceRecompileDummy = ""
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import sbt._
2+
import Keys._
3+
4+
object DottyInjectedPlugin extends AutoPlugin {
5+
override def requires = plugins.JvmPlugin
6+
override def trigger = allRequirements
7+
8+
override val projectSettings = Seq(
9+
scalaVersion := sys.props("plugin.scalaVersion")
10+
)
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % sys.props("plugin.version"))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
> run
2+
3+
# Recompile B (no meaningful change, this is just so that the dependencies on A.InnerClass are
4+
# recorded using the `binaryDependency` sbt callback, which will only work correctly
5+
# if we call it with the inner class binary name and not the top-level class name).
6+
$ copy-file changes/B2.scala B.scala
7+
> compile
8+
9+
# Change the signature of A.Inner#foo, this requires B to be recompiled,
10+
# otherwise run will fail:
11+
$ copy-file changes/A2.scala A.scala
12+
> run
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
object A {
2+
object InnerObject {
3+
def bla: Int = 2
4+
}
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
object B {
2+
def main(args: Array[String]): Unit = {
3+
val o = A.InnerObject.bla
4+
println(o)
5+
}
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import complete.DefaultParsers._
2+
3+
val checkIterations = inputKey[Unit]("Verifies the accumlated number of iterations of incremental compilation.")
4+
5+
checkIterations := {
6+
val analysis = (compile in Compile).value.asInstanceOf[sbt.internal.inc.Analysis]
7+
8+
val expected: Int = (Space ~> NatBasic).parsed
9+
val actual: Int = analysis.compilations.allCompilations.size
10+
assert(expected == actual, s"Expected $expected compilations, got $actual")
11+
}
12+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
object A {
2+
object InnerObject {
3+
def bla: String = "bla"
4+
}
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
object B {
2+
def main(args: Array[String]): Unit = {
3+
val o = A.InnerObject.bla
4+
println(o)
5+
}
6+
val forceRecompileDummy = ""
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import sbt._
2+
import Keys._
3+
4+
object DottyInjectedPlugin extends AutoPlugin {
5+
override def requires = plugins.JvmPlugin
6+
override def trigger = allRequirements
7+
8+
override val projectSettings = Seq(
9+
scalaVersion := sys.props("plugin.scalaVersion")
10+
)
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % sys.props("plugin.version"))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
> run
2+
3+
# Recompile B (no meaningful change, this is just so that the dependency on A.InnerObject are
4+
# recorded using the `binaryDependency` sbt callback, which will only work correctly
5+
# if we call it with the inner object binary name and not the top-level class name).
6+
$ copy-file changes/B2.scala B.scala
7+
> compile
8+
9+
# Change the signature of A.Inner#foo, this requires B to be recompiled,
10+
# otherwise run will fail:
11+
$ copy-file changes/A2.scala A.scala
12+
> run

0 commit comments

Comments
 (0)