From 46058029b5ec0c809f29f77dcb59e8a25166d69f Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Wed, 4 Nov 2015 11:16:53 +0100 Subject: [PATCH 01/12] Consider signatures of method before and after erasure in ExtractAPI The signatures of methods that have value classes as arguments or return type change during the erasure phase. Because we only registered signatures before the erasure, we missed some API changes when a class was changed to a value class (or a value class changed to a class). This commit fixes this problem by recording the signatures of method before and after erasure. Fixes sbt/sbt#1171 --- .../src/main/scala/xsbt/Compat.scala | 7 +- .../src/main/scala/xsbt/ExtractAPI.scala | 69 ++++++++++++++----- .../source-dependencies/value-class/build.sbt | 2 + .../value-class/changes/A0.scala | 1 + .../value-class/changes/A1.scala | 1 + .../value-class/changes/B0.scala | 3 + .../value-class/changes/B1.scala | 3 + .../value-class/changes/C0.scala | 3 + .../value-class/changes/C1.scala | 3 + .../source-dependencies/value-class/test | 30 ++++++++ 10 files changed, 105 insertions(+), 17 deletions(-) create mode 100644 sbt/src/sbt-test/source-dependencies/value-class/build.sbt create mode 100644 sbt/src/sbt-test/source-dependencies/value-class/changes/A0.scala create mode 100644 sbt/src/sbt-test/source-dependencies/value-class/changes/A1.scala create mode 100644 sbt/src/sbt-test/source-dependencies/value-class/changes/B0.scala create mode 100644 sbt/src/sbt-test/source-dependencies/value-class/changes/B1.scala create mode 100644 sbt/src/sbt-test/source-dependencies/value-class/changes/C0.scala create mode 100644 sbt/src/sbt-test/source-dependencies/value-class/changes/C1.scala create mode 100644 sbt/src/sbt-test/source-dependencies/value-class/test diff --git a/compile/interface/src/main/scala/xsbt/Compat.scala b/compile/interface/src/main/scala/xsbt/Compat.scala index 74116c0af..016658238 100644 --- a/compile/interface/src/main/scala/xsbt/Compat.scala +++ b/compile/interface/src/main/scala/xsbt/Compat.scala @@ -1,6 +1,6 @@ package xsbt -import scala.tools.nsc.Global +import scala.tools.nsc.{ Global, Phase } import scala.tools.nsc.symtab.Flags /** @@ -45,6 +45,11 @@ abstract class Compat { val Nullary = global.NullaryMethodType val ScalaObjectClass = definitions.ScalaObjectClass + implicit def withExitingPostErasure(global: Global) = new WithExitingPostErasure(global) + class WithExitingPostErasure(global: Global) { + def exitingPostErasure[T](op: => T) = global afterPostErasure op + } + private[this] final class MiscCompat { // in 2.9, nme.LOCALCHILD was renamed to tpnme.LOCAL_CHILD def tpnme = nme diff --git a/compile/interface/src/main/scala/xsbt/ExtractAPI.scala b/compile/interface/src/main/scala/xsbt/ExtractAPI.scala index 5c2cbf61b..2caea4ba4 100644 --- a/compile/interface/src/main/scala/xsbt/ExtractAPI.scala +++ b/compile/interface/src/main/scala/xsbt/ExtractAPI.scala @@ -178,9 +178,9 @@ class ExtractAPI[GlobalType <: CallbackGlobal](val global: GlobalType, private def viewer(s: Symbol) = (if (s.isModule) s.moduleClass else s).thisType private def printMember(label: String, in: Symbol, t: Type) = println(label + " in " + in + " : " + t + " (debug: " + debugString(t) + " )") - private def defDef(in: Symbol, s: Symbol) = + private def defDef(in: Symbol, s: Symbol): List[xsbti.api.Def] = { - def build(t: Type, typeParams: Array[xsbti.api.TypeParameter], valueParameters: List[xsbti.api.ParameterList]): xsbti.api.Def = + def build(t: Type, typeParams: Array[xsbti.api.TypeParameter], valueParameters: List[xsbti.api.ParameterList]): List[xsbti.api.Def] = { def parameterList(syms: List[Symbol]): xsbti.api.ParameterList = { @@ -192,13 +192,50 @@ class ExtractAPI[GlobalType <: CallbackGlobal](val global: GlobalType, assert(typeParams.isEmpty) assert(valueParameters.isEmpty) build(base, typeParameters(in, typeParams0), Nil) - case MethodType(params, resultType) => - build(resultType, typeParams, parameterList(params) :: valueParameters) + case mType @ MethodType(params, resultType) => + // The types of a method's parameters change between phases: For instance, if a + // parameter is a subtype of AnyVal, then it won't have the same type before and after + // erasure. Therefore we record the type of parameters before AND after erasure to + // make sure that we don't miss some API changes. + // class A(val x: Int) extends AnyVal + // def foo(a: A): Int = A.x <- has type (LA)I before erasure + // <- has type (I)I after erasure + // If we change A from value class to normal class, we need to recompile all clients + // of def foo. + val beforeErasure = parameterList(params) :: valueParameters + val afterErasure = global exitingPostErasure (parameterList(mType.params) :: valueParameters) + + build(resultType, typeParams, beforeErasure) ++ build(resultType, typeParams, afterErasure) case Nullary(resultType) => // 2.9 and later build(resultType, typeParams, valueParameters) case returnType => - val t2 = processType(in, dropConst(returnType)) - new xsbti.api.Def(valueParameters.reverse.toArray, t2, typeParams, simpleName(s), getAccess(s), getModifiers(s), annotations(in, s)) + def makeDef(retTpe: xsbti.api.Type): xsbti.api.Def = + new xsbti.api.Def( + valueParameters.reverse.toArray, + retTpe, + typeParams, + simpleName(s), + getAccess(s), + getModifiers(s), + annotations(in, s)) + + // The return type of a method may change before and after erasure. Consider the + // following method: + // class A(val x: Int) extends AnyVal + // def foo(x: Int): A = new A(x) <- has type (I)LA before erasure + // <- has type (I)I after erasure + // If we change A from value class to normal class, we need to recompile all clients + // of def foo. + val beforeErasure = processType(in, dropConst(returnType)) + val afterErasure = { + val erasedReturn = dropConst(global exitingPostErasure viewer(in).memberInfo(s)) map { + case MethodType(_, r) => r + case other => other + } + processType(in, erasedReturn) + } + + makeDef(beforeErasure) :: makeDef(afterErasure) :: Nil } } def parameterS(s: Symbol): xsbti.api.MethodParameter = @@ -292,22 +329,22 @@ class ExtractAPI[GlobalType <: CallbackGlobal](val global: GlobalType, defs } - private def definition(in: Symbol, sym: Symbol): Option[xsbti.api.Definition] = + private def definition(in: Symbol, sym: Symbol): List[xsbti.api.Definition] = { - def mkVar = Some(fieldDef(in, sym, false, new xsbti.api.Var(_, _, _, _, _))) - def mkVal = Some(fieldDef(in, sym, true, new xsbti.api.Val(_, _, _, _, _))) + def mkVar = List(fieldDef(in, sym, false, new xsbti.api.Var(_, _, _, _, _))) + def mkVal = List(fieldDef(in, sym, true, new xsbti.api.Val(_, _, _, _, _))) if (isClass(sym)) - if (ignoreClass(sym)) None else Some(classLike(in, sym)) + if (ignoreClass(sym)) Nil else List(classLike(in, sym)) else if (sym.isNonClassType) - Some(typeDef(in, sym)) + List(typeDef(in, sym)) else if (sym.isVariable) - if (isSourceField(sym)) mkVar else None + if (isSourceField(sym)) mkVar else Nil else if (sym.isStable) - if (isSourceField(sym)) mkVal else None + if (isSourceField(sym)) mkVal else Nil else if (sym.isSourceMethod && !sym.isSetter) - if (sym.isGetter) mkVar else Some(defDef(in, sym)) + if (sym.isGetter) mkVar else defDef(in, sym) else - None + Nil } private def ignoreClass(sym: Symbol): Boolean = sym.isLocalClass || sym.isAnonymousClass || sym.fullName.endsWith(LocalChild.toString) @@ -530,4 +567,4 @@ class ExtractAPI[GlobalType <: CallbackGlobal](val global: GlobalType, if (annots.isEmpty) processType(in, at.underlying) else annotated(in, annots, at.underlying) } -} \ No newline at end of file +} diff --git a/sbt/src/sbt-test/source-dependencies/value-class/build.sbt b/sbt/src/sbt-test/source-dependencies/value-class/build.sbt new file mode 100644 index 000000000..e850154ac --- /dev/null +++ b/sbt/src/sbt-test/source-dependencies/value-class/build.sbt @@ -0,0 +1,2 @@ +incOptions := incOptions.value.withRecompileAllFraction(1.0) + diff --git a/sbt/src/sbt-test/source-dependencies/value-class/changes/A0.scala b/sbt/src/sbt-test/source-dependencies/value-class/changes/A0.scala new file mode 100644 index 000000000..ad5bf4c56 --- /dev/null +++ b/sbt/src/sbt-test/source-dependencies/value-class/changes/A0.scala @@ -0,0 +1 @@ +class A(val x: Int) diff --git a/sbt/src/sbt-test/source-dependencies/value-class/changes/A1.scala b/sbt/src/sbt-test/source-dependencies/value-class/changes/A1.scala new file mode 100644 index 000000000..dbaa1c3f0 --- /dev/null +++ b/sbt/src/sbt-test/source-dependencies/value-class/changes/A1.scala @@ -0,0 +1 @@ +class A(val x: Int) extends AnyVal diff --git a/sbt/src/sbt-test/source-dependencies/value-class/changes/B0.scala b/sbt/src/sbt-test/source-dependencies/value-class/changes/B0.scala new file mode 100644 index 000000000..0dba978c3 --- /dev/null +++ b/sbt/src/sbt-test/source-dependencies/value-class/changes/B0.scala @@ -0,0 +1,3 @@ +class B { + def foo(a: A): Int = 1 +} diff --git a/sbt/src/sbt-test/source-dependencies/value-class/changes/B1.scala b/sbt/src/sbt-test/source-dependencies/value-class/changes/B1.scala new file mode 100644 index 000000000..c7b689c5e --- /dev/null +++ b/sbt/src/sbt-test/source-dependencies/value-class/changes/B1.scala @@ -0,0 +1,3 @@ +class B { + def bar: A = new A(0) +} diff --git a/sbt/src/sbt-test/source-dependencies/value-class/changes/C0.scala b/sbt/src/sbt-test/source-dependencies/value-class/changes/C0.scala new file mode 100644 index 000000000..c656b25a4 --- /dev/null +++ b/sbt/src/sbt-test/source-dependencies/value-class/changes/C0.scala @@ -0,0 +1,3 @@ +object C extends App { + println(new B().foo(null)) +} diff --git a/sbt/src/sbt-test/source-dependencies/value-class/changes/C1.scala b/sbt/src/sbt-test/source-dependencies/value-class/changes/C1.scala new file mode 100644 index 000000000..ad8384312 --- /dev/null +++ b/sbt/src/sbt-test/source-dependencies/value-class/changes/C1.scala @@ -0,0 +1,3 @@ +object C extends App { + println(new B().bar.x) +} diff --git a/sbt/src/sbt-test/source-dependencies/value-class/test b/sbt/src/sbt-test/source-dependencies/value-class/test new file mode 100644 index 000000000..f78acb2d8 --- /dev/null +++ b/sbt/src/sbt-test/source-dependencies/value-class/test @@ -0,0 +1,30 @@ +$ copy-file changes/A0.scala src/main/scala/A.scala +$ copy-file changes/B0.scala src/main/scala/B.scala +$ copy-file changes/C0.scala src/main/scala/C.scala + +# A is a normal class. B.foo accepts a parameter of type A. C calls B.foo, giving it `null`. +> compile +> run + +# Make A a value class. +$ copy-file changes/A1.scala src/main/scala/A.scala + +# The code no longer compiles because B.foo no longer accepts `null` as an argument. +# This means that we have invalidated C.scala, as expected! +-> compile + +$ copy-file changes/A0.scala src/main/scala/A.scala +$ copy-file changes/B1.scala src/main/scala/B.scala +$ copy-file changes/C1.scala src/main/scala/C.scala + +# A is a normal class. B.bar takes no arguments and returns an instance of A. C calls B.bar. +> compile +> run + +# Make A a value class. +$ copy-file changes/A1.scala src/main/scala/A.scala + +# The code compiles. It will run iff C is recompiled because the signature of B.bar has changed, +# because A is now a value class. +> run + From efff1716185b68135083ca59ea5506b2c25b915e Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Wed, 4 Nov 2015 13:35:53 +0100 Subject: [PATCH 02/12] Restore source compatibility with Scala 2.11 --- compile/interface/src/main/scala/xsbt/Compat.scala | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/compile/interface/src/main/scala/xsbt/Compat.scala b/compile/interface/src/main/scala/xsbt/Compat.scala index 016658238..2c7331100 100644 --- a/compile/interface/src/main/scala/xsbt/Compat.scala +++ b/compile/interface/src/main/scala/xsbt/Compat.scala @@ -45,9 +45,14 @@ abstract class Compat { val Nullary = global.NullaryMethodType val ScalaObjectClass = definitions.ScalaObjectClass + // In 2.11, afterPostErasure has been renamed to exitingPostErasure + implicit def withAfterPostErasure(global: Global) = new WithAfterPostErasure(global) + class WithAfterPostErasure(global: Global) { + def afterPostErasure[T](op: => T): T = sourceCompatibilityOnly + } implicit def withExitingPostErasure(global: Global) = new WithExitingPostErasure(global) class WithExitingPostErasure(global: Global) { - def exitingPostErasure[T](op: => T) = global afterPostErasure op + def exitingPostErasure[T](op: => T): T = global afterPostErasure op } private[this] final class MiscCompat { From 111511dc6d556040c95e8c75e303b2c52eb56ec3 Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Wed, 4 Nov 2015 15:04:59 +0100 Subject: [PATCH 03/12] `afterPostErasure` didn't exist in 2.9 --- compile/interface/src/main/scala/xsbt/Compat.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/compile/interface/src/main/scala/xsbt/Compat.scala b/compile/interface/src/main/scala/xsbt/Compat.scala index 2c7331100..1d38f6dec 100644 --- a/compile/interface/src/main/scala/xsbt/Compat.scala +++ b/compile/interface/src/main/scala/xsbt/Compat.scala @@ -45,11 +45,12 @@ abstract class Compat { val Nullary = global.NullaryMethodType val ScalaObjectClass = definitions.ScalaObjectClass - // In 2.11, afterPostErasure has been renamed to exitingPostErasure + // `afterPostErasure` doesn't exist in Scala < 2.10 implicit def withAfterPostErasure(global: Global) = new WithAfterPostErasure(global) class WithAfterPostErasure(global: Global) { - def afterPostErasure[T](op: => T): T = sourceCompatibilityOnly + def afterPostErasure[T](op: => T): T = op } + // `exitingPostErasure` was called `afterPostErasure` in 2.10 implicit def withExitingPostErasure(global: Global) = new WithExitingPostErasure(global) class WithExitingPostErasure(global: Global) { def exitingPostErasure[T](op: => T): T = global afterPostErasure op From 9764b7f6ef505706abf87d5f326166c0ac63afff Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Wed, 4 Nov 2015 17:44:52 +0100 Subject: [PATCH 04/12] Don't inspect signatures post erasure if macros are involved --- .../src/main/scala/xsbt/ExtractAPI.scala | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/compile/interface/src/main/scala/xsbt/ExtractAPI.scala b/compile/interface/src/main/scala/xsbt/ExtractAPI.scala index 2caea4ba4..19ec6ddd1 100644 --- a/compile/interface/src/main/scala/xsbt/ExtractAPI.scala +++ b/compile/interface/src/main/scala/xsbt/ExtractAPI.scala @@ -180,6 +180,11 @@ class ExtractAPI[GlobalType <: CallbackGlobal](val global: GlobalType, private def printMember(label: String, in: Symbol, t: Type) = println(label + " in " + in + " : " + t + " (debug: " + debugString(t) + " )") private def defDef(in: Symbol, s: Symbol): List[xsbti.api.Def] = { + def isMacro(sym: Symbol): Boolean = + sym.isMacro || (sym.info.members.sorted exists isMacro) || (sym.children exists isMacro) + //sym.isMacro || (sym.children exists isMacro) || (sym.isType && sym.asType.toType.members.sorted.exists(isMacro)) + val inspectPostErasure = !isMacro(in.enclosingTopLevelClass) + def build(t: Type, typeParams: Array[xsbti.api.TypeParameter], valueParameters: List[xsbti.api.ParameterList]): List[xsbti.api.Def] = { def parameterList(syms: List[Symbol]): xsbti.api.ParameterList = @@ -202,10 +207,15 @@ class ExtractAPI[GlobalType <: CallbackGlobal](val global: GlobalType, // <- has type (I)I after erasure // If we change A from value class to normal class, we need to recompile all clients // of def foo. - val beforeErasure = parameterList(params) :: valueParameters - val afterErasure = global exitingPostErasure (parameterList(mType.params) :: valueParameters) + val beforeErasure = + build(resultType, typeParams, parameterList(params) :: valueParameters) + val afterErasure = + if (inspectPostErasure) + build(resultType, typeParams, global exitingPostErasure (parameterList(mType.params) :: valueParameters)) + else + Nil - build(resultType, typeParams, beforeErasure) ++ build(resultType, typeParams, afterErasure) + beforeErasure ++ afterErasure case Nullary(resultType) => // 2.9 and later build(resultType, typeParams, valueParameters) case returnType => @@ -226,16 +236,17 @@ class ExtractAPI[GlobalType <: CallbackGlobal](val global: GlobalType, // <- has type (I)I after erasure // If we change A from value class to normal class, we need to recompile all clients // of def foo. - val beforeErasure = processType(in, dropConst(returnType)) - val afterErasure = { - val erasedReturn = dropConst(global exitingPostErasure viewer(in).memberInfo(s)) map { - case MethodType(_, r) => r - case other => other - } - processType(in, erasedReturn) - } + val beforeErasure = makeDef(processType(in, dropConst(returnType))) + val afterErasure = + if (inspectPostErasure) { + val erasedReturn = dropConst(global exitingPostErasure viewer(in).memberInfo(s)) map { + case MethodType(_, r) => r + case other => other + } + List(makeDef(processType(in, erasedReturn))) + } else Nil - makeDef(beforeErasure) :: makeDef(afterErasure) :: Nil + beforeErasure :: afterErasure } } def parameterS(s: Symbol): xsbti.api.MethodParameter = From 4299ff76aaa4d9a3d82edcf95620a42a1008c155 Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Thu, 5 Nov 2015 07:39:50 +0100 Subject: [PATCH 05/12] Quick and dirty fix for SO --- .../interface/src/main/scala/xsbt/ExtractAPI.scala | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/compile/interface/src/main/scala/xsbt/ExtractAPI.scala b/compile/interface/src/main/scala/xsbt/ExtractAPI.scala index 19ec6ddd1..efea1af20 100644 --- a/compile/interface/src/main/scala/xsbt/ExtractAPI.scala +++ b/compile/interface/src/main/scala/xsbt/ExtractAPI.scala @@ -180,11 +180,13 @@ class ExtractAPI[GlobalType <: CallbackGlobal](val global: GlobalType, private def printMember(label: String, in: Symbol, t: Type) = println(label + " in " + in + " : " + t + " (debug: " + debugString(t) + " )") private def defDef(in: Symbol, s: Symbol): List[xsbti.api.Def] = { - def isMacro(sym: Symbol): Boolean = - sym.isMacro || (sym.info.members.sorted exists isMacro) || (sym.children exists isMacro) - //sym.isMacro || (sym.children exists isMacro) || (sym.isType && sym.asType.toType.members.sorted.exists(isMacro)) - val inspectPostErasure = !isMacro(in.enclosingTopLevelClass) - + def collectAll(acc: Set[Symbol], s: Symbol): Set[Symbol] = + if (acc contains s) acc + else + ((s.info.members.sorted ++ s.children) foldLeft (acc + s)) { + case (acc, sym) => collectAll(acc, sym) + } + val inspectPostErasure = !collectAll(Set.empty, in.enclosingTopLevelClass).exists(_.isMacro) def build(t: Type, typeParams: Array[xsbti.api.TypeParameter], valueParameters: List[xsbti.api.ParameterList]): List[xsbti.api.Def] = { def parameterList(syms: List[Symbol]): xsbti.api.ParameterList = From f1c05cb5eb10221e0720bd250d5cc88741baf939 Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Thu, 5 Nov 2015 09:26:38 +0100 Subject: [PATCH 06/12] Fix source compatibility with Scala < 2.10 --- compile/interface/src/main/scala/xsbt/Compat.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/compile/interface/src/main/scala/xsbt/Compat.scala b/compile/interface/src/main/scala/xsbt/Compat.scala index 1d38f6dec..3b007d252 100644 --- a/compile/interface/src/main/scala/xsbt/Compat.scala +++ b/compile/interface/src/main/scala/xsbt/Compat.scala @@ -86,6 +86,7 @@ abstract class Compat { def enclosingTopLevelClass: Symbol = sym.toplevelClass def toplevelClass: Symbol = sourceCompatibilityOnly + def isMacro: Boolean = false } val DummyValue = 0 From 499c5e722843e7941e891ed8092483e3a169bc29 Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Fri, 6 Nov 2015 11:27:40 +0100 Subject: [PATCH 07/12] Correct fix against SO and macros problem --- .../src/main/scala/xsbt/Compat.scala | 26 +++++++++++++++++++ .../src/main/scala/xsbt/ExtractAPI.scala | 13 +++++----- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/compile/interface/src/main/scala/xsbt/Compat.scala b/compile/interface/src/main/scala/xsbt/Compat.scala index 3b007d252..472858380 100644 --- a/compile/interface/src/main/scala/xsbt/Compat.scala +++ b/compile/interface/src/main/scala/xsbt/Compat.scala @@ -87,6 +87,8 @@ abstract class Compat { def enclosingTopLevelClass: Symbol = sym.toplevelClass def toplevelClass: Symbol = sourceCompatibilityOnly def isMacro: Boolean = false + def orElse(alt: => Symbol) = alt + def asType: TypeSymbol = sym.asInstanceOf[TypeSymbol] } val DummyValue = 0 @@ -101,6 +103,30 @@ abstract class Compat { private[this] final implicit def miscCompat(n: AnyRef): MiscCompat = new MiscCompat + object DetectMacroImpls { + + private implicit def withRootMirror(x: Any): WithRootMirror = new WithRootMirror(x) + private class DummyMirror { + def getClassIfDefined(x: String): Symbol = NoSymbol + } + private class WithRootMirror(x: Any) { + def rootMirror = new DummyMirror + } + private class WithIsScala211(x: Any) { + def isScala211 = false + } + private[this] implicit def withIsScala211(x: Any): WithIsScala211 = new WithIsScala211(x) + + // Copied from scala/scala since these methods do not exists in Scala < 2.11.x + private def Context_210 = if (settings.isScala211) NoSymbol else global.rootMirror.getClassIfDefined("scala.reflect.macros.Context") + lazy val BlackboxContextClass = global.rootMirror.getClassIfDefined("scala.reflect.macros.blackbox.Context").orElse(Context_210) + lazy val WhiteboxContextClass = global.rootMirror.getClassIfDefined("scala.reflect.macros.whitebox.Context").orElse(Context_210) + def isContextCompatible(sym: Symbol) = { + sym.isNonBottomSubClass(BlackboxContextClass) || sym.isNonBottomSubClass(WhiteboxContextClass) + } + + } + object MacroExpansionOf { def unapply(tree: Tree): Option[Tree] = { diff --git a/compile/interface/src/main/scala/xsbt/ExtractAPI.scala b/compile/interface/src/main/scala/xsbt/ExtractAPI.scala index efea1af20..ea198111c 100644 --- a/compile/interface/src/main/scala/xsbt/ExtractAPI.scala +++ b/compile/interface/src/main/scala/xsbt/ExtractAPI.scala @@ -178,15 +178,14 @@ class ExtractAPI[GlobalType <: CallbackGlobal](val global: GlobalType, private def viewer(s: Symbol) = (if (s.isModule) s.moduleClass else s).thisType private def printMember(label: String, in: Symbol, t: Type) = println(label + " in " + in + " : " + t + " (debug: " + debugString(t) + " )") + private def defDef(in: Symbol, s: Symbol): List[xsbti.api.Def] = { - def collectAll(acc: Set[Symbol], s: Symbol): Set[Symbol] = - if (acc contains s) acc - else - ((s.info.members.sorted ++ s.children) foldLeft (acc + s)) { - case (acc, sym) => collectAll(acc, sym) - } - val inspectPostErasure = !collectAll(Set.empty, in.enclosingTopLevelClass).exists(_.isMacro) + import DetectMacroImpls._ + def hasContextAsParameter(meth: MethodSymbol): Boolean = + meth.paramss.flatten exists (p => isContextCompatible(p.info.typeSymbol)) + + val inspectPostErasure = !hasContextAsParameter(s.asMethod) def build(t: Type, typeParams: Array[xsbti.api.TypeParameter], valueParameters: List[xsbti.api.ParameterList]): List[xsbti.api.Def] = { def parameterList(syms: List[Symbol]): xsbti.api.ParameterList = From d41fda6584399decadb45a91f1ecaf27d5bad434 Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Fri, 6 Nov 2015 13:10:31 +0100 Subject: [PATCH 08/12] Restore source compatibility with Scala 2.8 + cosmetic changes --- compile/interface/src/main/scala/xsbt/Compat.scala | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/compile/interface/src/main/scala/xsbt/Compat.scala b/compile/interface/src/main/scala/xsbt/Compat.scala index 472858380..18214ee3a 100644 --- a/compile/interface/src/main/scala/xsbt/Compat.scala +++ b/compile/interface/src/main/scala/xsbt/Compat.scala @@ -1,6 +1,6 @@ package xsbt -import scala.tools.nsc.{ Global, Phase } +import scala.tools.nsc.Global import scala.tools.nsc.symtab.Flags /** @@ -87,8 +87,9 @@ abstract class Compat { def enclosingTopLevelClass: Symbol = sym.toplevelClass def toplevelClass: Symbol = sourceCompatibilityOnly def isMacro: Boolean = false - def orElse(alt: => Symbol) = alt + def orElse(alt: => Symbol) = if (sym ne NoSymbol) sym else alt def asType: TypeSymbol = sym.asInstanceOf[TypeSymbol] + def asMethod: MethodSymbol = sym.asInstanceOf[MethodSymbol] } val DummyValue = 0 @@ -121,6 +122,11 @@ abstract class Compat { private def Context_210 = if (settings.isScala211) NoSymbol else global.rootMirror.getClassIfDefined("scala.reflect.macros.Context") lazy val BlackboxContextClass = global.rootMirror.getClassIfDefined("scala.reflect.macros.blackbox.Context").orElse(Context_210) lazy val WhiteboxContextClass = global.rootMirror.getClassIfDefined("scala.reflect.macros.whitebox.Context").orElse(Context_210) + /** + * Determines whether a symbol may be compatible with Scala macros' `Context` (e.g. could it be + * the `c: Context` parameter of a macro implementation?). In such cases, we should treat the + * method whose parameter this symbol is as a potential macro implementation. + */ def isContextCompatible(sym: Symbol) = { sym.isNonBottomSubClass(BlackboxContextClass) || sym.isNonBottomSubClass(WhiteboxContextClass) } From bfc27f117915077481ddd3b719c4985a3493432a Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Sat, 7 Nov 2015 09:06:26 +0100 Subject: [PATCH 09/12] Make sure AnyVal is involved before checking post erasure --- .../src/main/scala/xsbt/Compat.scala | 23 +++---------------- .../src/main/scala/xsbt/ExtractAPI.scala | 15 ++++++++---- 2 files changed, 14 insertions(+), 24 deletions(-) diff --git a/compile/interface/src/main/scala/xsbt/Compat.scala b/compile/interface/src/main/scala/xsbt/Compat.scala index 18214ee3a..8a5ca6a3d 100644 --- a/compile/interface/src/main/scala/xsbt/Compat.scala +++ b/compile/interface/src/main/scala/xsbt/Compat.scala @@ -86,9 +86,6 @@ abstract class Compat { def enclosingTopLevelClass: Symbol = sym.toplevelClass def toplevelClass: Symbol = sourceCompatibilityOnly - def isMacro: Boolean = false - def orElse(alt: => Symbol) = if (sym ne NoSymbol) sym else alt - def asType: TypeSymbol = sym.asInstanceOf[TypeSymbol] def asMethod: MethodSymbol = sym.asInstanceOf[MethodSymbol] } @@ -104,7 +101,7 @@ abstract class Compat { private[this] final implicit def miscCompat(n: AnyRef): MiscCompat = new MiscCompat - object DetectMacroImpls { + object MirrorHelper { private implicit def withRootMirror(x: Any): WithRootMirror = new WithRootMirror(x) private class DummyMirror { @@ -113,23 +110,9 @@ abstract class Compat { private class WithRootMirror(x: Any) { def rootMirror = new DummyMirror } - private class WithIsScala211(x: Any) { - def isScala211 = false - } - private[this] implicit def withIsScala211(x: Any): WithIsScala211 = new WithIsScala211(x) + lazy val AnyValClass = global.rootMirror.getClassIfDefined("scala.AnyVal") - // Copied from scala/scala since these methods do not exists in Scala < 2.11.x - private def Context_210 = if (settings.isScala211) NoSymbol else global.rootMirror.getClassIfDefined("scala.reflect.macros.Context") - lazy val BlackboxContextClass = global.rootMirror.getClassIfDefined("scala.reflect.macros.blackbox.Context").orElse(Context_210) - lazy val WhiteboxContextClass = global.rootMirror.getClassIfDefined("scala.reflect.macros.whitebox.Context").orElse(Context_210) - /** - * Determines whether a symbol may be compatible with Scala macros' `Context` (e.g. could it be - * the `c: Context` parameter of a macro implementation?). In such cases, we should treat the - * method whose parameter this symbol is as a potential macro implementation. - */ - def isContextCompatible(sym: Symbol) = { - sym.isNonBottomSubClass(BlackboxContextClass) || sym.isNonBottomSubClass(WhiteboxContextClass) - } + def isAnyValSubtype(sym: Symbol): Boolean = sym.isNonBottomSubClass(AnyValClass) } diff --git a/compile/interface/src/main/scala/xsbt/ExtractAPI.scala b/compile/interface/src/main/scala/xsbt/ExtractAPI.scala index ea198111c..b3227376d 100644 --- a/compile/interface/src/main/scala/xsbt/ExtractAPI.scala +++ b/compile/interface/src/main/scala/xsbt/ExtractAPI.scala @@ -181,11 +181,18 @@ class ExtractAPI[GlobalType <: CallbackGlobal](val global: GlobalType, private def defDef(in: Symbol, s: Symbol): List[xsbti.api.Def] = { - import DetectMacroImpls._ - def hasContextAsParameter(meth: MethodSymbol): Boolean = - meth.paramss.flatten exists (p => isContextCompatible(p.info.typeSymbol)) + import MirrorHelper._ - val inspectPostErasure = !hasContextAsParameter(s.asMethod) + // Here we must be careful to also consider the type parameters, because a tuple (Foo, Int) + // may become (Int, Int) for instance. + def hasValueClassAsParameter(meth: MethodSymbol): Boolean = + meth.paramss.flatten map (_.info) exists (t => isAnyValSubtype(t.typeSymbol)) + + // Here too we must care for the type parameters (see above comment). + def hasValueClassAsReturnType(meth: MethodSymbol): Boolean = + isAnyValSubtype(meth.returnType.typeSymbol) + + val inspectPostErasure = hasValueClassAsParameter(s.asMethod) || hasValueClassAsReturnType(s.asMethod) def build(t: Type, typeParams: Array[xsbti.api.TypeParameter], valueParameters: List[xsbti.api.ParameterList]): List[xsbti.api.Def] = { def parameterList(syms: List[Symbol]): xsbti.api.ParameterList = From f5e7fdd62af3bf5374b4f4d602c1b5ff7ec4d20b Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Mon, 9 Nov 2015 16:30:11 +0100 Subject: [PATCH 10/12] Restore source compatibility with Scala 2.8 --- .../src/main/scala/xsbt/ExtractAPI.scala | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/compile/interface/src/main/scala/xsbt/ExtractAPI.scala b/compile/interface/src/main/scala/xsbt/ExtractAPI.scala index b3227376d..7309aeeb9 100644 --- a/compile/interface/src/main/scala/xsbt/ExtractAPI.scala +++ b/compile/interface/src/main/scala/xsbt/ExtractAPI.scala @@ -183,16 +183,26 @@ class ExtractAPI[GlobalType <: CallbackGlobal](val global: GlobalType, { import MirrorHelper._ - // Here we must be careful to also consider the type parameters, because a tuple (Foo, Int) - // may become (Int, Int) for instance. - def hasValueClassAsParameter(meth: MethodSymbol): Boolean = - meth.paramss.flatten map (_.info) exists (t => isAnyValSubtype(t.typeSymbol)) + val hasValueClassAsParameter: Boolean = { + import MirrorHelper._ + s.asMethod.paramss.flatten map (_.info) exists (t => isAnyValSubtype(t.typeSymbol)) + } - // Here too we must care for the type parameters (see above comment). - def hasValueClassAsReturnType(meth: MethodSymbol): Boolean = - isAnyValSubtype(meth.returnType.typeSymbol) + // Note: We only inspect the "outermost type" (i.e. no recursion) because we don't need to + // inspect after erasure a function that would, for instance, return a function that returns + // a subtype of AnyVal. + val hasValueClassAsReturnType: Boolean = { + val tpe = viewer(in).memberInfo(s) + tpe match { + case PolyType(_, base) => isAnyValSubtype(base.typeSymbol) + case MethodType(_, resultType) => isAnyValSubtype(resultType.typeSymbol) + case Nullary(resultType) => isAnyValSubtype(resultType.typeSymbol) + case resultType => isAnyValSubtype(resultType.typeSymbol) + } + } + + val inspectPostErasure = hasValueClassAsParameter || hasValueClassAsReturnType - val inspectPostErasure = hasValueClassAsParameter(s.asMethod) || hasValueClassAsReturnType(s.asMethod) def build(t: Type, typeParams: Array[xsbti.api.TypeParameter], valueParameters: List[xsbti.api.ParameterList]): List[xsbti.api.Def] = { def parameterList(syms: List[Symbol]): xsbti.api.ParameterList = From a244f4e141ab31c4f3384ee73f80f353e959ba3a Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Wed, 11 Nov 2015 14:49:50 +0100 Subject: [PATCH 11/12] Fix Codacy failure by specifying return type --- compile/interface/src/main/scala/xsbt/Compat.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compile/interface/src/main/scala/xsbt/Compat.scala b/compile/interface/src/main/scala/xsbt/Compat.scala index 8a5ca6a3d..5b8c3983b 100644 --- a/compile/interface/src/main/scala/xsbt/Compat.scala +++ b/compile/interface/src/main/scala/xsbt/Compat.scala @@ -46,12 +46,12 @@ abstract class Compat { val ScalaObjectClass = definitions.ScalaObjectClass // `afterPostErasure` doesn't exist in Scala < 2.10 - implicit def withAfterPostErasure(global: Global) = new WithAfterPostErasure(global) + implicit def withAfterPostErasure(global: Global): WithAfterPostErasure = new WithAfterPostErasure(global) class WithAfterPostErasure(global: Global) { def afterPostErasure[T](op: => T): T = op } // `exitingPostErasure` was called `afterPostErasure` in 2.10 - implicit def withExitingPostErasure(global: Global) = new WithExitingPostErasure(global) + implicit def withExitingPostErasure(global: Global): WithExitingPostErasure = new WithExitingPostErasure(global) class WithExitingPostErasure(global: Global) { def exitingPostErasure[T](op: => T): T = global afterPostErasure op } @@ -108,7 +108,7 @@ abstract class Compat { def getClassIfDefined(x: String): Symbol = NoSymbol } private class WithRootMirror(x: Any) { - def rootMirror = new DummyMirror + def rootMirror: DummyMirror = new DummyMirror } lazy val AnyValClass = global.rootMirror.getClassIfDefined("scala.AnyVal") From 5934350aa2b03b19df871d740858a26f2502b7ef Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Wed, 11 Nov 2015 14:55:52 +0100 Subject: [PATCH 12/12] Notes for #2261 --- notes/0.13.10/consider-signatures-after-erasure.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 notes/0.13.10/consider-signatures-after-erasure.md diff --git a/notes/0.13.10/consider-signatures-after-erasure.md b/notes/0.13.10/consider-signatures-after-erasure.md new file mode 100644 index 000000000..f511a5aba --- /dev/null +++ b/notes/0.13.10/consider-signatures-after-erasure.md @@ -0,0 +1,12 @@ + + [@Duhemm]: http://github.com/Duhemm + [1171]: https://github.com/sbt/sbt/issues/1171 + [2261]: https://github.com/sbt/sbt/pull/2261 + +### Fixes with compatibility implications + +### Improvements +- Register signatures of method before and after erasure if they involve value classes [#2261][2261] by [@Duhemm][@Duhemm] + +### Bug fixes +- Incremental compiler misses change to value class, and results to NoSuchMethodError at runtime [#1171][1171] \ No newline at end of file