Include used types in the set of used names

This is a backport of https://github.com/sbt/zinc/pull/87

When `B2.scala` replaces `B.scala` in the new test
`types-in-used-names-a`, the name hash of `listb` does not change because
the signature of `C.listb` is still `List[B]`, however users of
`C.listb` have to be recompiled since the subtyping relationships of its
type have changed.

This commit does this by extending the definition of "used names" to
also include the names of the types of trees, even if these types
do not appear in the source like `List[B]` in `D.scala` (since `B` has
been invalidated, this will force the recompilation of `D.scala`).

This commit does not fix every issue with used types as illustrated by
the pending test `types-in-used-names-b`, `B.scala` is not recompiled
because it uses the type `T` whose hash has not changed, but `T` is
bounded by `S` and `S` has changed, so it should be recompiled.
This should be fixable by including the type bounds underlying a
`TypeRef` in `symbolsInType`.

The test `as-seen-from-a` that did not work before shows that we may not
have to worry about tracking prefixes in `ExtractAPI` anymore, see the
discussion in sbt/zinc#87 for more information.
This commit is contained in:
Guillaume Martres 2016-03-27 21:34:59 +02:00
parent ce611fbdca
commit cae569b313
27 changed files with 168 additions and 39 deletions

View File

@ -51,6 +51,22 @@ abstract class Compat {
def transformedType(tpe: Type): Type = tpe
}
/**
* Traverses given type and collects result of applying a partial function `pf`.
*
* NOTE: This class exists in Scala 2.10 as CollectTypeCollector but does not in earlier
* versions (like 2.9) of Scala compiler that incremental cmpiler supports so we had to
* reimplement that class here.
*/
class CollectTypeTraverser[T](pf: PartialFunction[Type, T]) extends TypeTraverser {
var collected: List[T] = Nil
def traverse(tpe: Type): Unit = {
if (pf.isDefinedAt(tpe))
collected = pf(tpe) :: collected
mapOver(tpe)
}
}
private[this] final class MiscCompat {
// in 2.9, nme.LOCALCHILD was renamed to tpnme.LOCAL_CHILD
def tpnme = nme

View File

@ -29,7 +29,7 @@ object Dependency {
* where it originates from. The Symbol->Classfile mapping is implemented by
* LocateClassFile that we inherit from.
*/
final class Dependency(val global: CallbackGlobal) extends LocateClassFile {
final class Dependency(val global: CallbackGlobal) extends LocateClassFile with GlobalHelpers {
import global._
def newPhase(prev: Phase): Phase = new DependencyPhase(prev)
@ -79,6 +79,12 @@ final class Dependency(val global: CallbackGlobal) extends LocateClassFile {
private class ExtractDependenciesTraverser extends Traverser {
private val _dependencies = collection.mutable.HashSet.empty[Symbol]
protected def addDependency(dep: Symbol): Unit = if (dep ne NoSymbol) _dependencies += dep
protected def addTreeDependency(tree: Tree): Unit = {
addDependency(tree.symbol)
if (tree.tpe != null)
symbolsInType(tree.tpe).foreach(addDependency)
()
}
def dependencies: Iterator[Symbol] = _dependencies.iterator
def topLevelDependencies: Iterator[Symbol] = _dependencies.map(enclosingTopLevelClass).iterator
@ -118,11 +124,11 @@ final class Dependency(val global: CallbackGlobal) extends LocateClassFile {
* this looks fishy, see this thread:
* https://groups.google.com/d/topic/scala-internals/Ms9WUAtokLo/discussion
*/
case id: Ident => addDependency(id.symbol)
case id: Ident => addTreeDependency(id)
case sel @ Select(qual, _) =>
traverse(qual); addDependency(sel.symbol)
traverse(qual); addTreeDependency(sel)
case sel @ SelectFromTypeTree(qual, _) =>
traverse(qual); addDependency(sel.symbol)
traverse(qual); addTreeDependency(sel)
case Template(parents, self, body) =>
// use typeSymbol to dealias type aliases -- we want to track the dependency on the real class in the alias's RHS
@ -151,32 +157,6 @@ final class Dependency(val global: CallbackGlobal) extends LocateClassFile {
super.traverse(m)
case other => super.traverse(other)
}
private def symbolsInType(tp: Type): Set[Symbol] = {
val typeSymbolCollector =
new CollectTypeTraverser({
case tpe if (tpe != null) && !tpe.typeSymbolDirect.isPackage => tpe.typeSymbolDirect
})
typeSymbolCollector.traverse(tp)
typeSymbolCollector.collected.toSet
}
}
/**
* Traverses given type and collects result of applying a partial function `pf`.
*
* NOTE: This class exists in Scala 2.10 as CollectTypeCollector but does not in earlier
* versions (like 2.9) of Scala compiler that incremental cmpiler supports so we had to
* reimplement that class here.
*/
private final class CollectTypeTraverser[T](pf: PartialFunction[Type, T]) extends TypeTraverser {
var collected: List[T] = Nil
def traverse(tpe: Type): Unit = {
if (pf.isDefinedAt(tpe))
collected = pf(tpe) :: collected
mapOver(tpe)
}
}
/** Copied straight from Scala 2.10 as it does not exist in Scala 2.9 compiler */

View File

@ -7,6 +7,8 @@ import scala.tools.nsc._
*
* Extracts simple (unqualified) names mentioned in given in non-definition position by collecting
* all symbols associated with non-definition trees and extracting names from all collected symbols.
* Also extract the names of the types of non-definition trees (see source-dependencies/types-in-used-names
* for an example where this is required).
*
* If given symbol is mentioned both in definition and in non-definition position (e.g. in member
* selection) then that symbol is collected. It means that names of symbols defined and used in the
@ -38,7 +40,7 @@ import scala.tools.nsc._
* The tree walking algorithm walks into TypeTree.original explicitly.
*
*/
class ExtractUsedNames[GlobalType <: CallbackGlobal](val global: GlobalType) extends Compat {
class ExtractUsedNames[GlobalType <: CallbackGlobal](val global: GlobalType) extends Compat with GlobalHelpers {
import global._
@inline def debug(msg: => String) = if (settings.verbose.value) inform(msg)
@ -61,10 +63,12 @@ class ExtractUsedNames[GlobalType <: CallbackGlobal](val global: GlobalType) ext
*/
val inspectedOriginalTrees = collection.mutable.Set.empty[Tree]
def addSymbol(symbol: Symbol): Unit = {
val symbolNameAsString = symbol.name.decode.trim
namesBuffer += symbolNameAsString
}
def addSymbol(symbol: Symbol): Unit =
if (eligibleAsUsedName(symbol)) {
val symbolNameAsString = symbol.name.decode.trim
namesBuffer += symbolNameAsString
()
}
def handleTreeNode(node: Tree): Unit = {
def handleMacroExpansion(original: Tree): Unit = {
@ -91,8 +95,10 @@ class ExtractUsedNames[GlobalType <: CallbackGlobal](val global: GlobalType) ext
// not what we need
case t: TypeTree if t.original != null =>
t.original.foreach(handleTreeNode)
case t if t.hasSymbol && eligibleAsUsedName(t.symbol) =>
case t if t.hasSymbol =>
addSymbol(t.symbol)
if (t.tpe != null)
symbolsInType(t.tpe).foreach(addSymbol)
case _ => ()
}

View File

@ -0,0 +1,18 @@
package xsbt
import scala.tools.nsc.Global
trait GlobalHelpers extends Compat {
val global: CallbackGlobal
import global.{ Tree, Type, Symbol, TypeTraverser }
def symbolsInType(tp: Type): Set[Symbol] = {
val typeSymbolCollector =
new CollectTypeTraverser({
case tpe if (tpe != null) && !tpe.typeSymbolDirect.isPackage => tpe.typeSymbolDirect
})
typeSymbolCollector.traverse(tp)
typeSymbolCollector.collected.toSet
}
}

View File

@ -17,8 +17,9 @@ class ExtractUsedNamesSpecification extends Specification {
* definition.
*/
private val standardNames = Set(
// AnyRef is added as default parent of a class
"scala", "AnyRef",
"scala",
// The default parent of a class is "AnyRef" which is an alias for "Object"
"AnyRef", "Object",
// class receives a default constructor which is internally called "<init>"
"<init>")
@ -69,7 +70,45 @@ class ExtractUsedNamesSpecification extends Specification {
|}""".stripMargin
val compilerForTesting = new ScalaCompilerForUnitTesting(nameHashing = true)
val usedNames = compilerForTesting.extractUsedNamesFromSrc(srcA, srcB)
val expectedNames = standardNames ++ Set("A", "a", "B", "=")
val expectedNames = standardNames ++ Set("A", "a", "B", "=", "Int")
usedNames === expectedNames
}
"extract names in the types of trees" in {
val src1 = """|class X0
|class X1 extends X0
|class Y
|class A {
| type T >: X1 <: X0
|}
|class M
|class N
|class P0
|class P1 extends P0
|object B {
| type S = Y
| val lista: List[A] = ???
| val at: A#T = ???
| val as: S = ???
| def foo(m: M): N = ???
| def bar[Param >: P1 <: P0](p: Param): Param = ???
|}""".stripMargin
val src2 = """|object Test {
| val x = B.lista
| val y = B.at
| val z = B.as
| B.foo(???)
| B.bar(???)
|}""".stripMargin
val compilerForTesting = new ScalaCompilerForUnitTesting(nameHashing = true)
val usedNames = compilerForTesting.extractUsedNamesFromSrc(src1, src2)
val expectedNames = standardNames ++ Set("Test", "B", "x", "y", "z",
"Predef", "???", "Nothing",
"lista", "package", "List", "A",
"at", "T",
"as", "S",
"foo", "M", "N",
"bar", "Param", "P1", "P0")
usedNames === expectedNames
}

View File

@ -0,0 +1,6 @@
abstract class A {
type T
object X {
def foo(x: T): T = x
}
}

View File

@ -0,0 +1,3 @@
class B extends A {
type T = Int
}

View File

@ -0,0 +1 @@
object C extends B

View File

@ -0,0 +1,3 @@
object D {
C.X.foo(12)
}

View File

@ -0,0 +1,3 @@
class B extends A {
type T = String
}

View File

@ -0,0 +1,3 @@
> compile
$ copy-file changes/B2.scala B.scala
-> compile

View File

@ -0,0 +1,7 @@
abstract class A {
type T <: S
type S
object X {
def foo: T = null.asInstanceOf[T]
}
}

View File

@ -0,0 +1,3 @@
class B extends A {
type S <: Int
}

View File

@ -0,0 +1 @@
object C extends B

View File

@ -0,0 +1,3 @@
object D {
val x: Int = C.X.foo
}

View File

@ -0,0 +1,3 @@
class B extends A {
type S <: String
}

View File

@ -0,0 +1,3 @@
> compile
$ copy-file changes/B2.scala B.scala
-> compile

View File

@ -0,0 +1 @@
class A

View File

@ -0,0 +1 @@
class B extends A

View File

@ -0,0 +1,3 @@
object C {
val listb: List[B] = List(new B)
}

View File

@ -0,0 +1,3 @@
object D {
val lista: List[A] = C.listb
}

View File

@ -0,0 +1,4 @@
> compile
$ copy-file changes/B2.scala B.scala
# Compilation of D.scala should fail because B is no longer a subtype of A
-> compile

View File

@ -0,0 +1,5 @@
class A {
type T <: S
type S <: Int
def foo: T = null.asInstanceOf[T]
}

View File

@ -0,0 +1,3 @@
object B {
val x: Int = (new A).foo
}

View File

@ -0,0 +1,5 @@
class A {
type T <: S
type S <: String
def foo: T = null.asInstanceOf[T]
}

View File

@ -0,0 +1,5 @@
> compile
$ copy-file changes/A2.scala A.scala
# Same workaround as #2565
# Compilation of B.scala should fail because A#S is no longer a subtype of Int
-> compile