Skip to content

Commit

Permalink
Make exclusion annotations configurable
Browse files Browse the repository at this point in the history
  • Loading branch information
dwijnand committed Aug 23, 2021
1 parent d09144d commit 8dc9587
Show file tree
Hide file tree
Showing 33 changed files with 141 additions and 130 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.typesafe.tools.mima.core

private[mima] final case class AnnotInfo(name: String)
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ private[mima] sealed abstract class ClassInfo(val owner: PackageInfo) extends In
final var _methods: Members[MethodInfo] = NoMembers
final var _flags: Int = 0
final var _scopedPrivate: Boolean = false
final var _experimental: Boolean = false
final var _annotations: List[AnnotInfo] = Nil
final var _implClass: ClassInfo = NoClass
final var _moduleClass: ClassInfo = NoClass
final var _module: ClassInfo = NoClass
Expand All @@ -76,7 +76,7 @@ private[mima] sealed abstract class ClassInfo(val owner: PackageInfo) extends In
final def methods: Members[MethodInfo] = afterLoading(_methods)
final def flags: Int = afterLoading(_flags)
final def isScopedPrivate: Boolean = afterLoading(_scopedPrivate)
final def isExperimental: Boolean = afterLoading(_experimental)
final def annotations: List[AnnotInfo] = afterLoading(_annotations)
final def implClass: ClassInfo = { owner.setImplClasses; _implClass } // returns NoClass if this is not a trait
final def moduleClass: ClassInfo = { owner.setModules; if (_moduleClass == NoClass) this else _moduleClass }
final def module: ClassInfo = { owner.setModules; if (_module == NoClass) this else _module }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ private[mima] final class FieldInfo(owner: ClassInfo, bytecodeName: String, flag
private[mima] final class MethodInfo(owner: ClassInfo, bytecodeName: String, flags: Int, descriptor: String)
extends MemberInfo(owner, bytecodeName, flags, descriptor)
{
final var _experimental = false
final def isExperimental = _experimental
final var _annotations: List[AnnotInfo] = Nil
final def annotations: List[AnnotInfo] = _annotations

def methodString: String = s"$shortMethodString in ${owner.classString}"
def shortMethodString: String = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ object MimaUnpickler {
if (buf.bytes.length == 0) return

val doPrint = false
//val doPrint = path.contains("v1") && !path.contains("experimental.class") && !path.contains("experimental2.class")
//val doPrint = path.contains("v1") && !path.contains("exclude.class")
if (doPrint) {
println(s"unpickling $path")
PicklePrinter.printPickle(buf)
Expand Down Expand Up @@ -215,8 +215,7 @@ object MimaUnpickler {
case TypeRefInfo(_, sym, Nil) => s"$sym"
case _ => "?"
}
cls._experimental |= annotName == "scala.annotation.experimental"
cls._experimental |= annotName == "scala.annotation.experimental2"
cls._annotations :+= AnnotInfo(annotName)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,44 +9,40 @@ import TastyFormat._, NameTags._, TastyTagOps._, TastyRefs._

object TastyUnpickler {
def unpickleClass(in: TastyReader, clazz: ClassInfo, path: String): Unit = {
//val doPrint = false
//val doPrint = path.contains("v1") && !path.contains("experimental2.tasty")
val doPrint = false
//val doPrint = path.contains("v1") && !path.contains("exclude.tasty")
//if (doPrint) TastyPrinter.printClassNames(in.fork, path)
//if (doPrint) TastyPrinter.printPickle(in.fork, path)
unpickle(in, clazz, path)
}
if (doPrint) TastyPrinter.printPickle(in.fork, path)

def unpickle(in: TastyReader, clazz: ClassInfo, path: String): Unit = {
readHeader(in)
val names = readNames(in)
val tree = unpickleTree(getTreeReader(in, names), names)

object trav extends Traverser {
copyAnnotations(tree, clazz)
}

def copyAnnotations(tree: Tree, clazz: ClassInfo): Unit = {
new Traverser {
var pkgNames = List.empty[Name]
var clsNames = List.empty[Name]

def dropHead[A](xs: List[A], head: A) = xs match {
case `head` :: ys => ys
case x :: _ => throw new AssertionError(s"assertion failed: Expected head=$head but it was $x")
case _ => throw new AssertionError(s"assertion failed: Expected head=$head but list was empty")
}

override def traversePkg(pkg: Pkg): Unit = {
val pkgName = pkg.path match {
case TypeRefPkg(fullyQual) => fullyQual
case p: UnknownPath => SimpleName(p.show)
}
pkgNames ::= pkgName
pkgNames ::= getPkgName(pkg)
super.traversePkg(pkg)
pkgNames = dropHead(pkgNames, pkgName)
pkgNames = pkgNames.tail
}

override def traverseClsDef(clsDef: ClsDef): Unit = {
forEachClass(clsDef, pkgNames, clsNames)
clsNames ::= clsDef.name
super.traverseClsDef(clsDef)
clsNames = dropHead(clsNames, clsDef.name)
clsNames = clsNames.tail
}
}.traverse(tree)

def getPkgName(pkg: Pkg) = pkg.path match {
case TypeRefPkg(fullyQual) => fullyQual
case p: UnknownPath => SimpleName(p.show)
}

def forEachClass(clsDef: ClsDef, pkgNames: List[Name], clsNames: List[Name]): Unit = {
Expand All @@ -55,22 +51,16 @@ object TastyUnpickler {
val clsName = (clsDef.name :: clsNames).reverseIterator.mkString("$")
val cls = clazz.owner.classes.getOrElse(clsName, NoClass)
if (cls != NoClass) {
cls._experimental |= clsDef.annots.exists(_.tycon.toString == "scala.annotation.experimental")
cls._experimental |= clsDef.annots.exists(_.tycon.toString == "scala.annotation.experimental2")
cls._annotations ++= clsDef.annots.map(annot => AnnotInfo(annot.tycon.toString))

for (defDef <- clsDef.template.meths) {
val isExperimental =
defDef.annots.exists(_.tycon.toString == "scala.annotation.experimental") ||
defDef.annots.exists(_.tycon.toString == "scala.annotation.experimental2")
if (isExperimental)
for (meth <- cls.lookupClassMethods(new MethodInfo(cls, defDef.name.source, 0, "()V")))
meth._experimental = true
val annots = defDef.annots.map(annot => AnnotInfo(annot.tycon.toString))
for (meth <- cls.lookupClassMethods(new MethodInfo(cls, defDef.name.source, 0, "()V")))
meth._annotations ++= annots
}
}
}
}

trav.traverse(tree)
}

def unpickleTree(in: TastyReader, names: Names): Tree = {
Expand Down Expand Up @@ -219,7 +209,7 @@ object TastyUnpickler {

sealed trait Path extends Type
final case class UnknownPath(tag: Int) extends Path { def show = s"UnknownPath(${astTagToString(tag)})" }
final case class TypeRefPkg(fullyQual: Name) extends Path { def show = s"$fullyQual" }
final case class TypeRefPkg(fullyQual: Name) extends Path { def show = s"$fullyQual" }

final case class Annot(tycon: Type, fullAnnotation: Tree) extends Tree { def show = s"@$tycon" }

Expand Down Expand Up @@ -320,16 +310,16 @@ object TastyUnpickler {

val name = tag match {
case UTF8 => val start = currentAddr; goto(end); SimpleName(new String(bytes.slice(start.index, end.index), "UTF-8"))
case QUALIFIED => QualifiedName(readName(), PathSep, readName().asSimpleName)
case EXPANDED => QualifiedName(readName(), ExpandedSep, readName().asSimpleName)
case EXPANDPREFIX => QualifiedName(readName(), ExpandPrefixSep, readName().asSimpleName)
case QUALIFIED => QualifiedName(readName(), Name.PathSep, readName().asSimpleName)
case EXPANDED => QualifiedName(readName(), Name.ExpandedSep, readName().asSimpleName)
case EXPANDPREFIX => QualifiedName(readName(), Name.ExpandPrefixSep, readName().asSimpleName)

case UNIQUE => UniqueName(sep = readName().asSimpleName, num = readNat(), qual = ifBefore(end)(readName(), Empty))
case UNIQUE => UniqueName(sep = readName().asSimpleName, num = readNat(), qual = ifBefore(end)(readName(), Name.Empty))
case DEFAULTGETTER => DefaultName(readName(), readNat())

case SUPERACCESSOR => PrefixName(SuperPrefix, readName())
case INLINEACCESSOR => PrefixName(InlinePrefix, readName())
case BODYRETAINER => SuffixName(readName(), BodyRetainerSuffix)
case SUPERACCESSOR => PrefixName( Name.SuperPrefix, readName())
case INLINEACCESSOR => PrefixName(Name.InlinePrefix, readName())
case BODYRETAINER => SuffixName(readName(), Name.BodyRetainerSuffix)
case OBJECTCLASS => ObjectName(readName())

case SIGNED => val name = readName(); readSignedRest(name, name)
Expand Down Expand Up @@ -375,14 +365,16 @@ object TastyUnpickler {

final case class SignedName(qual: Name, sig: MethodSignature[ErasedTypeRef], target: Name) extends Name

val Empty = SimpleName("")
val PathSep = SimpleName(".")
val ExpandedSep = SimpleName("$$")
val ExpandPrefixSep = SimpleName("$")
val InlinePrefix = SimpleName("inline$")
val SuperPrefix = SimpleName("super$")
val BodyRetainerSuffix = SimpleName("$retainedBody")
val Constructor = SimpleName("<init>")
object Name {
val Empty = SimpleName("")
val PathSep = SimpleName(".")
val ExpandedSep = SimpleName("$$")
val ExpandPrefixSep = SimpleName("$")
val InlinePrefix = SimpleName("inline$")
val SuperPrefix = SimpleName("super$")
val BodyRetainerSuffix = SimpleName("$retainedBody")
val Constructor = SimpleName("<init>")
}

object nme {
def NoSymbol = SimpleName("NoSymbol")
Expand Down Expand Up @@ -416,18 +408,18 @@ object TastyUnpickler {

def apply(tname: Name): ErasedTypeRef = {
def name(qual: Name, tname: SimpleName, isModule: Boolean) = {
val qualified = if (qual == Empty) tname else QualifiedName(qual, PathSep, tname)
val qualified = if (qual == Name.Empty) tname else QualifiedName(qual, Name.PathSep, tname)
if (isModule) ObjectName(qualified) else qualified
}
def specialised(qual: Name, terminal: String, isModule: Boolean, arrayDims: Int = 0): ErasedTypeRef = terminal match {
case InnerRegex(inner) => specialised(qual, inner, isModule, arrayDims + 1)
case clazz => ErasedTypeRef(name(qual, SimpleName(clazz), isModule).toTypeName, arrayDims)
}
def transform(name: Name, isModule: Boolean = false): ErasedTypeRef = name match {
case ObjectName(classKind) => transform(classKind, isModule = true)
case SimpleName(raw) => specialised(Empty, raw, isModule) // unqualified in the <empty> package
case QualifiedName(path, PathSep, sel) => specialised(path, sel.raw, isModule)
case _ => throw new AssertionError(s"Unexpected erased type ref ${name.debug}")
case ObjectName(classKind) => transform(classKind, isModule = true)
case SimpleName(raw) => specialised(Name.Empty, raw, isModule) // unqualified in the <empty> package
case QualifiedName(path, Name.PathSep, sel) => specialised(path, sel.raw, isModule)
case _ => throw new AssertionError(s"Unexpected erased type ref ${name.debug}")
}
transform(tname.toTermName)
}
Expand All @@ -447,7 +439,7 @@ object TastyUnpickler {

object SourceEncoder extends StringBuilderEncoder {
def traverse(sb: StringBuilder, name: Name): StringBuilder = name match {
case Constructor => sb
case Name.Constructor => sb
case SimpleName(raw) => sb ++= raw
case ObjectName(base) => traverse(sb, base)
case TypeName(base) => traverse(sb, base)
Expand Down
10 changes: 5 additions & 5 deletions core/src/main/scala/com/typesafe/tools/mima/lib/MiMaLib.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,21 @@ final class MiMaLib(cp: Seq[File], log: Logging = ConsoleLogging) {
pkg
}

private def traversePackages(oldpkg: PackageInfo, newpkg: PackageInfo): List[Problem] = {
private def traversePackages(oldpkg: PackageInfo, newpkg: PackageInfo, excludeAnnots: List[AnnotInfo]): List[Problem] = {
log.verbose(s"traversing $oldpkg")
Analyzer.analyze(oldpkg, newpkg, log) ++ oldpkg.packages.values.toSeq.sortBy(_.name).flatMap { p =>
Analyzer.analyze(oldpkg, newpkg, log, excludeAnnots) ++ oldpkg.packages.values.toSeq.sortBy(_.name).flatMap { p =>
val q = newpkg.packages.getOrElse(p.name, NoPackageInfo)
traversePackages(p, q)
traversePackages(p, q, excludeAnnots)
}
}

/** Return a list of problems for the two versions of the library. */
def collectProblems(oldJarOrDir: File, newJarOrDir: File): List[Problem] = {
def collectProblems(oldJarOrDir: File, newJarOrDir: File, excludeAnnots: List[String]): List[Problem] = {
val oldPackage = createPackage(oldJarOrDir)
val newPackage = createPackage(newJarOrDir)
log.debug(s"[old version in: ${oldPackage.definitions}]")
log.debug(s"[new version in: ${newPackage.definitions}]")
log.debug(s"classpath: ${classpath.asClassPathString}")
traversePackages(oldPackage, newPackage)
traversePackages(oldPackage, newPackage, excludeAnnots.map(AnnotInfo(_)))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@ import com.typesafe.tools.mima.lib.analyze.method.MethodChecker
import com.typesafe.tools.mima.lib.analyze.template.TemplateChecker

object Analyzer {
def analyze(oldpkg: PackageInfo, newpkg: PackageInfo, log: Logging): List[Problem] = {
def analyze(oldpkg: PackageInfo, newpkg: PackageInfo, log: Logging, excludeAnnots: List[AnnotInfo]): List[Problem] = {
for {
oldclazz <- oldpkg.accessibleClasses.toList.sortBy(_.bytecodeName)
_ = log.verbose(s"analyzing $oldclazz")
_ = oldclazz.forceLoad
// if it is missing a trait implementation class, then no error should be reported
// since there should be already errors, i.e., missing methods...
if !oldclazz.isImplClass
if !oldclazz.isExperimental
if !excludeAnnots.exists(oldclazz.annotations.contains)
problem <- newpkg.classes.get(oldclazz.bytecodeName) match {
case Some(newclazz) => analyze(oldclazz, newclazz, log)
case Some(newclazz) => analyze(oldclazz, newclazz, log, excludeAnnots)
case None => List(MissingClassProblem(oldclazz))
}
} yield {
Expand All @@ -26,7 +26,7 @@ object Analyzer {
}
}

def analyze(oldclazz: ClassInfo, newclazz: ClassInfo, log: Logging): List[Problem] = {
def analyze(oldclazz: ClassInfo, newclazz: ClassInfo, log: Logging, excludeAnnots: List[AnnotInfo]): List[Problem] = {
if ((if (newclazz.isModuleClass) newclazz.module else newclazz).isScopedPrivate) Nil
else {
TemplateChecker.check(oldclazz, newclazz) match {
Expand All @@ -37,7 +37,7 @@ object Analyzer {
case maybeProblem =>
maybeProblem.toList :::
FieldChecker.check(oldclazz, newclazz) :::
MethodChecker.check(oldclazz, newclazz)
MethodChecker.check(oldclazz, newclazz, excludeAnnots)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,24 @@ package com.typesafe.tools.mima.lib.analyze.method
import com.typesafe.tools.mima.core._

private[analyze] object MethodChecker {
def check(oldclazz: ClassInfo, newclazz: ClassInfo): List[Problem] =
checkExisting(oldclazz, newclazz) ::: checkNew(oldclazz, newclazz)
def check(oldclazz: ClassInfo, newclazz: ClassInfo, excludeAnnots: List[AnnotInfo]): List[Problem] =
checkExisting(oldclazz, newclazz, excludeAnnots) ::: checkNew(oldclazz, newclazz, excludeAnnots)

/** Analyze incompatibilities that may derive from changes in existing methods. */
private def checkExisting(oldclazz: ClassInfo, newclazz: ClassInfo): List[Problem] = {
for (oldmeth <- oldclazz.methods.value; problem <- checkExisting1(oldmeth, newclazz)) yield problem
private def checkExisting(oldclazz: ClassInfo, newclazz: ClassInfo, excludeAnnots: List[AnnotInfo]): List[Problem] = {
for (oldmeth <- oldclazz.methods.value; problem <- checkExisting1(oldmeth, newclazz, excludeAnnots)) yield problem
}

/** Analyze incompatibilities that may derive from new methods in `newclazz`. */
private def checkNew(oldclazz: ClassInfo, newclazz: ClassInfo): List[Problem] = {
private def checkNew(oldclazz: ClassInfo, newclazz: ClassInfo, excludeAnnots: List[AnnotInfo]): List[Problem] = {
val problems1 = if (newclazz.isClass) Nil else checkEmulatedConcreteMethodsProblems(oldclazz, newclazz)
val problems2 = checkDeferredMethodsProblems(oldclazz, newclazz)
val problems3 = checkInheritedNewAbstractMethodProblems(oldclazz, newclazz)
val problems2 = checkDeferredMethodsProblems(oldclazz, newclazz, excludeAnnots)
val problems3 = checkInheritedNewAbstractMethodProblems(oldclazz, newclazz, excludeAnnots)
problems1 ::: problems2 ::: problems3
}

private def checkExisting1(oldmeth: MethodInfo, newclazz: ClassInfo): Option[Problem] = {
if (oldmeth.nonAccessible || oldmeth.isExperimental)
private def checkExisting1(oldmeth: MethodInfo, newclazz: ClassInfo, excludeAnnots: List[AnnotInfo]): Option[Problem] = {
if (oldmeth.nonAccessible || excludeAnnots.exists(oldmeth.annotations.contains))
None
else if (newclazz.isClass) {
if (oldmeth.isDeferred)
Expand Down Expand Up @@ -135,10 +135,10 @@ private[analyze] object MethodChecker {
} yield problem
}.toList

private def checkDeferredMethodsProblems(oldclazz: ClassInfo, newclazz: ClassInfo): List[Problem] = {
private def checkDeferredMethodsProblems(oldclazz: ClassInfo, newclazz: ClassInfo, excludeAnnots: List[AnnotInfo]): List[Problem] = {
for {
newmeth <- newclazz.deferredMethods.iterator
if !newmeth.isExperimental
if !excludeAnnots.exists(newmeth.annotations.contains)
problem <- oldclazz.lookupMethods(newmeth).find(_.descriptor == newmeth.descriptor) match {
case None => Some(ReversedMissingMethodProblem(newmeth))
case Some(oldmeth) if newclazz.isClass && oldmeth.isConcrete => Some(ReversedAbstractMethodProblem(newmeth))
Expand All @@ -147,7 +147,7 @@ private[analyze] object MethodChecker {
} yield problem
}.toList

private def checkInheritedNewAbstractMethodProblems(oldclazz: ClassInfo, newclazz: ClassInfo): List[Problem] = {
private def checkInheritedNewAbstractMethodProblems(oldclazz: ClassInfo, newclazz: ClassInfo, excludeAnnots: List[AnnotInfo]): List[Problem] = {
def allInheritedTypes(clazz: ClassInfo) = clazz.superClasses ++ clazz.allInterfaces
val diffInheritedTypes = allInheritedTypes(newclazz).diff(allInheritedTypes(oldclazz))

Expand All @@ -159,7 +159,7 @@ private[analyze] object MethodChecker {
newInheritedType <- diffInheritedTypes.iterator
// if `newInheritedType` is a trait, then the trait's concrete methods should be counted as deferred methods
newDeferredMethod <- newInheritedType.deferredMethodsInBytecode
if !newDeferredMethod.isExperimental
if !excludeAnnots.exists(newDeferredMethod.annotations.contains)
// checks that the newDeferredMethod did not already exist in one of the oldclazz supertypes
if noInheritedMatchingMethod(oldclazz, newDeferredMethod)(_ => true) &&
// checks that no concrete implementation of the newDeferredMethod is provided by one of the newclazz supertypes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ object CollectProblemsTest {

def collectProblems(cp: Seq[File], v1: File, v2: File, direction: Direction): List[Problem] = {
val (lhs, rhs) = direction.ordered(v1, v2)
new MiMaLib(cp).collectProblems(lhs, rhs)
new MiMaLib(cp).collectProblems(lhs, rhs, excludeAnnots)
}

def readOracleFile(oracleFile: File): List[String] = {
Expand Down Expand Up @@ -51,4 +51,6 @@ object CollectProblemsTest {
Failure(new Exception("CollectProblemsTest failure", null, false, false) {})
}
}

private val excludeAnnots = List("mima.annotation.exclude")
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
object App {
def main(args: Array[String]): Unit = println(new pkg1.pkg2.Foo())
def main(args: Array[String]): Unit = ()
}
Loading

0 comments on commit 8dc9587

Please sign in to comment.