Skip to content

Commit f3f8939

Browse files
dottaszeiger
authored andcommitted
Exploring inner modules for binary incompatibilities. Fix #127
The former implementation of PackageInfo#accessibleClasses expects that for any module `Foo` compiled by scalac, both `Foo.class` and `Foo$.class` classfiles are produced. Because `Foo$.class` and `Foo.class` are essentially mirrors, and users never access directly `Foo$.class`, MiMa only traverses the `Foo.class`. This works well for top-level modules, but it breaks for inner modules, because no mirror class is created for inner modules (only the module class is created). The fix consist in treating inner modules differently, hence making sure inner modules are part of the set returned by PackageInfo#accessibleClasses.
1 parent e083f6d commit f3f8939

File tree

8 files changed

+44
-5
lines changed

8 files changed

+44
-5
lines changed

core/src/main/scala/com/typesafe/tools/mima/core/ClassInfo.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,10 @@ abstract class ClassInfo(val owner: PackageInfo) extends HasDeclarationName with
6565

6666
override def canEqual(other: Any) = other.isInstanceOf[ClassInfo]
6767

68-
def formattedFullName = formatClassName(if (isObject) fullName.init else fullName)
68+
def formattedFullName = formatClassName(if (isModule) fullName.init else fullName)
6969

7070
def declarationPrefix = {
71-
if (isObject) "object"
71+
if (isModule) "object"
7272
else if (isTrait) "trait"
7373
else if (loaded && isInterface) "interface" // java interfaces and traits with no implementation methods
7474
else "class"
@@ -290,7 +290,7 @@ abstract class ClassInfo(val owner: PackageInfo) extends HasDeclarationName with
290290
ClassfileParser.isInterface(flags)
291291
}
292292

293-
def isObject: Boolean = bytecodeName.endsWith("$")
293+
def isModule: Boolean = bytecodeName.endsWith("$")
294294

295295
/** Is this class public? */
296296
/*

core/src/main/scala/com/typesafe/tools/mima/core/PackageInfo.scala

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,9 @@ abstract class PackageInfo(val owner: PackageInfo) {
6464
def classes: mutable.Map[String, ClassInfo]
6565

6666
lazy val accessibleClasses: Set[ClassInfo] = {
67+
// FIXME: Make this a tailrecursive implementation
6768
/** Fixed point iteration for finding all accessible classes. */
68-
def accessibleClassesUnder(prefix: Set[ClassInfo]): Set[ClassInfo] = {
69+
def accessibleClassesUnder(prefix: Set[ClassInfo]): Set[ClassInfo] = {
6970
val vclasses = (classes.valuesIterator filter (isAccessible(_, prefix))).toSet
7071
if (vclasses.isEmpty) vclasses
7172
else vclasses union accessibleClassesUnder(vclasses)
@@ -77,7 +78,28 @@ abstract class PackageInfo(val owner: PackageInfo) {
7778
else {
7879
val idx = clazz.decodedName.lastIndexOf("$")
7980
if (idx < 0) prefix.isEmpty // class name contains no $
80-
else prefix exists (_.decodedName == clazz.decodedName.substring(0, idx)) // prefix before dollar is an accessible class detected previously
81+
else {
82+
// A top-level module is compiled into a class plus a module class (for instance,
83+
// for a top-level `object A`, scalac creates two classfiles `A.class` and `A$.class`.).
84+
// While, for an inner module, scalac creates only one classfile, i.e., the module class
85+
// classfile.
86+
//
87+
// `clazz` is an inner module if:
88+
// 1) `clazz` is a module, and
89+
// 2) `clazz` decoded name starts with one of the passed `prefix`.
90+
def isInnerModule: Boolean = {
91+
clazz.isModule &&
92+
prefix.exists { outer =>
93+
// this condition is to avoid looping indefinitely
94+
clazz.decodedName != outer.decodedName &&
95+
// checks if `outer` is a prefix of `clazz` name
96+
clazz.decodedName.startsWith(outer.decodedName)
97+
}
98+
}
99+
// class, trait, and top-level module go through this condition
100+
(prefix exists (_.decodedName == clazz.decodedName.substring(0, idx))) || // prefix before dollar is an accessible class detected previously
101+
isInnerModule
102+
}
81103
}
82104
}
83105
clazz.isPublic && isReachable
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
object A#B does not have a correspondent in new version
2+
method B()A#B# in class A does not have a correspondent in new version
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class A {
2+
object B {
3+
def foo: Int = 2
4+
}
5+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
class A {
2+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
object A#B does not have a correspondent in new version
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
object A {
2+
object B {
3+
def foo: Int = 2
4+
}
5+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
object A {
2+
}

0 commit comments

Comments
 (0)