blob: c9058ff409893285e71a055b7e49379244f70cb5 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// scalastyle:off classforname
package org.apache.spark.tools
import scala.collection.mutable
import scala.reflect.runtime.{universe => unv}
import scala.reflect.runtime.universe.runtimeMirror
import scala.util.Try
import org.clapper.classutil.ClassFinder
/**
* A tool for generating classes to be excluded during binary checking with MIMA. It is expected
* that this tool is run with ./spark-class.
*
* MIMA itself only supports JVM-level visibility and doesn't account for package-private classes.
* This tool looks at all currently package-private classes and generates exclusions for them. Note
* that this approach is not sound. It can lead to false positives if we move or rename a previously
* package-private class. It can lead to false negatives if someone explicitly makes a class
* package-private that wasn't before. This exists only to help catch certain classes of changes
* which might be difficult to catch during review.
*/
object GenerateMIMAIgnore {
private val classLoader = Thread.currentThread().getContextClassLoader
private val mirror = runtimeMirror(classLoader)
private def isPackagePrivate(sym: unv.Symbol) =
!sym.privateWithin.fullName.startsWith("<none>")
private def isPackagePrivateModule(moduleSymbol: unv.ModuleSymbol) =
!moduleSymbol.privateWithin.fullName.startsWith("<none>")
/**
* For every class checks via scala reflection if the class itself or contained members
* are package private.
* Returns the tuple of such classes and members.
*/
private def privateWithin(packageName: String): (Set[String], Set[String]) = {
val classes = getClasses(packageName)
val ignoredClasses = mutable.HashSet[String]()
val ignoredMembers = mutable.HashSet[String]()
for (className <- classes) {
try {
val classSymbol = mirror.classSymbol(Class.forName(className, false, classLoader))
val moduleSymbol = mirror.staticModule(className)
val directlyPrivateSpark =
isPackagePrivate(classSymbol) ||
isPackagePrivateModule(moduleSymbol) ||
classSymbol.isPrivate
/* Inner classes defined within a private[spark] class or object are effectively
invisible, so we account for them as package private. */
lazy val indirectlyPrivateSpark = {
val maybeOuter = className.toString.takeWhile(_ != '$')
if (maybeOuter != className) {
isPackagePrivate(mirror.classSymbol(Class.forName(maybeOuter, false, classLoader))) ||
isPackagePrivateModule(mirror.staticModule(maybeOuter))
} else {
false
}
}
if (directlyPrivateSpark || indirectlyPrivateSpark) {
ignoredClasses += className
}
// check if this class has package-private/annotated members.
ignoredMembers ++= getAnnotatedOrPackagePrivateMembers(classSymbol)
} catch {
// scalastyle:off println
case _: Throwable => println("Error instrumenting class:" + className)
// scalastyle:on println
}
}
(ignoredClasses.flatMap(c => Seq(c, c.replace("$", "#"))).toSet, ignoredMembers.toSet)
}
/**
* Scala reflection does not let us see inner function even if they are upgraded
* to public for some reason. So had to resort to java reflection to get all inner
* functions with $$ in there name.
*/
def getInnerFunctions(classSymbol: unv.ClassSymbol): Seq[String] = {
try {
Class.forName(classSymbol.fullName, false, classLoader).getMethods.map(_.getName)
.filter(_.contains("$$")).map(classSymbol.fullName + "." + _)
} catch {
case t: Throwable =>
// scalastyle:off println
println("[WARN] Unable to detect inner functions for class:" + classSymbol.fullName)
// scalastyle:on println
Seq.empty[String]
}
}
private def getAnnotatedOrPackagePrivateMembers(classSymbol: unv.ClassSymbol) = {
classSymbol.typeSignature.members.filterNot(x =>
x.fullName.startsWith("java") || x.fullName.startsWith("scala")
).filter(x => isPackagePrivate(x)).map(_.fullName) ++ getInnerFunctions(classSymbol)
}
def main(args: Array[String]) {
import scala.tools.nsc.io.File
val (privateClasses, privateMembers) = privateWithin("org.apache.spark")
val previousContents = Try(File(".generated-mima-class-excludes").lines()).
getOrElse(Iterator.empty).mkString("\n")
File(".generated-mima-class-excludes")
.writeAll(previousContents + privateClasses.mkString("\n"))
// scalastyle:off println
println("Created : .generated-mima-class-excludes in current directory.")
val previousMembersContents = Try(File(".generated-mima-member-excludes").lines)
.getOrElse(Iterator.empty).mkString("\n")
File(".generated-mima-member-excludes").writeAll(previousMembersContents +
privateMembers.mkString("\n"))
println("Created : .generated-mima-member-excludes in current directory.")
// scalastyle:on println
}
private def shouldExclude(name: String) = {
// Heuristic to remove JVM classes that do not correspond to user-facing classes in Scala
name.contains("anon") ||
name.endsWith("$class") ||
name.contains("$sp") ||
name.contains("hive") ||
name.contains("Hive")
}
/**
* Scans all classes accessible from the context class loader which belong to the given package
* and subpackages both from directories and jars present on the classpath.
*/
private def getClasses(packageName: String): Set[String] = {
val finder = ClassFinder()
finder
.getClasses
.map(_.name)
.filter(_.startsWith(packageName))
.filterNot(shouldExclude)
.toSet
}
}
// scalastyle:on classforname