diff options
Diffstat (limited to 'bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis')
12 files changed, 490 insertions, 0 deletions
diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/Analyze.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/Analyze.scala new file mode 100644 index 00000000000..ed15c1bcc74 --- /dev/null +++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/Analyze.scala @@ -0,0 +1,28 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.plugin.classanalysis + +import org.objectweb.asm._ +import java.io.{InputStream, File} +import com.yahoo.container.plugin.util.IO.withFileInputStream + +/** + * Main entry point for class analysis + * @author tonytv + */ +object Analyze { + def analyzeClass(classFile : File) : ClassFileMetaData = { + try { + withFileInputStream(classFile) { fileInputStream => + analyzeClass(fileInputStream) + } + } catch { + case e : RuntimeException => throw new RuntimeException("An error occurred when analyzing " + classFile.getPath, e) + } + } + + def analyzeClass(inputStream : InputStream) : ClassFileMetaData = { + val visitor = new AnalyzeClassVisitor() + new ClassReader(inputStream).accept(visitor, ClassReader.SKIP_DEBUG) + visitor.result + } +} diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AnalyzeClassVisitor.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AnalyzeClassVisitor.scala new file mode 100644 index 00000000000..b57a07d5c30 --- /dev/null +++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AnalyzeClassVisitor.scala @@ -0,0 +1,102 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.plugin.classanalysis + +import org.objectweb.asm._ +import com.yahoo.osgi.annotation.{ExportPackage, Version} +import collection.mutable + +/** + * Picks up classes used in class files. + * @author tonytv + */ +private class AnalyzeClassVisitor extends ClassVisitor(Opcodes.ASM5) with AnnotationVisitorTrait with AttributeVisitorTrait { + private var name : String = null + protected val imports : ImportsSet = mutable.Set() + protected var exportPackageAnnotation: Option[ExportPackageAnnotation] = None + + + override def visitAttribute(attribute: Attribute): Unit = super.visitAttribute(attribute) + + override def visitMethod(access: Int, name: String, desc: String, signature: String, + exceptions: Array[String]): MethodVisitor = { + + imports ++= (Type.getReturnType(desc) +: Type.getArgumentTypes(desc)).flatMap(getClassName) + + imports ++= Option(exceptions) getOrElse(Array()) flatMap internalNameToClassName + + AnalyzeSignatureVisitor.analyzeMethod(signature, this) + new AnalyzeMethodVisitor(this) + } + + override def visitField(access: Int, name: String, desc: String, signature: String, value: AnyRef): FieldVisitor = { + imports ++= getClassName(Type.getType(desc)).toList + + AnalyzeSignatureVisitor.analyzeField(signature, this) + new FieldVisitor(Opcodes.ASM5) with SubVisitorTrait with AttributeVisitorTrait with AnnotationVisitorTrait { + val analyzeClassVisitor = AnalyzeClassVisitor.this + + override def visitAnnotation(desc: String, visible: Boolean): AnnotationVisitor = super.visitAnnotation(desc, visible) + override def visitAttribute(attribute: Attribute): Unit = super.visitAttribute(attribute) + override def visitEnd(): Unit = super.visitEnd() + } + } + + override def visit(version: Int, access: Int, name: String, signature: String, superName: String, interfaces: Array[String]) { + this.name = internalNameToClassName(name).get + + imports ++= (superName +: interfaces) flatMap internalNameToClassName + AnalyzeSignatureVisitor.analyzeClass(signature, this) + } + + override def visitInnerClass(name: String, outerName: String, innerName: String, access: Int) {} + override def visitOuterClass(owner: String, name: String, desc: String) {} + override def visitSource(source: String, debug: String) {} + override def visitEnd() {} + + def addImports(imports: TraversableOnce[String]) { + this.imports ++= imports + } + + override def visitAnnotation(desc: String, visible: Boolean): AnnotationVisitor = { + if (Type.getType(desc).getClassName == classOf[ExportPackage].getName) { + visitExportPackage() + } else { + super.visitAnnotation(desc, visible) + } + } + + def visitExportPackage(): AnnotationVisitor = { + def defaultVersionValue[T](name: String) = classOf[Version].getMethod(name).getDefaultValue().asInstanceOf[T] + + new AnnotationVisitor(Opcodes.ASM5) { + var major: Int = defaultVersionValue("major") + var minor: Int = defaultVersionValue("minor") + var micro: Int = defaultVersionValue("micro") + var qualifier: String = defaultVersionValue("qualifier") + + override def visit(name: String, value: AnyRef) { + def valueAsInt = value.asInstanceOf[Int] + + name match { + case "major" => major = valueAsInt + case "minor" => minor = valueAsInt + case "micro" => micro = valueAsInt + case "qualifier" => qualifier = value.asInstanceOf[String] + } + } + + override def visitEnd() { + exportPackageAnnotation = Some(ExportPackageAnnotation(major, minor, micro, qualifier)) + } + + override def visitEnum(name: String, desc: String, value: String) {} + override def visitArray(name: String): AnnotationVisitor = this + override def visitAnnotation(name: String, desc: String): AnnotationVisitor = this + } + } + + def result = { + assert(!imports.contains("int")) + new ClassFileMetaData(name, imports.toSet, exportPackageAnnotation) + } +} diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AnalyzeMethodVisitor.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AnalyzeMethodVisitor.scala new file mode 100644 index 00000000000..5d65b3972c0 --- /dev/null +++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AnalyzeMethodVisitor.scala @@ -0,0 +1,88 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.plugin.classanalysis + +import org.objectweb.asm._ + +/** + * Picks up classes used in method bodies. + * @author tonytv + */ +private class AnalyzeMethodVisitor(val analyzeClassVisitor : AnalyzeClassVisitor) + extends MethodVisitor(Opcodes.ASM5) with AnnotationVisitorTrait with AttributeVisitorTrait with SubVisitorTrait { + + + override def visitParameterAnnotation(parameter: Int, desc: String, visible: Boolean): AnnotationVisitor = super.visitParameterAnnotation(parameter, desc, visible) + override def visitAnnotationDefault(): AnnotationVisitor = super.visitAnnotationDefault() + override def visitAttribute(attribute: Attribute): Unit = super.visitAttribute(attribute) + override def visitAnnotation(desc: String, visible: Boolean): AnnotationVisitor = super.visitAnnotation(desc, visible) + override def visitEnd(): Unit = super.visitEnd() + + override def visitMultiANewArrayInsn(desc: String, dims: Int) { + imports ++= getClassName(Type.getType(desc)).toList + } + + + override def visitMethodInsn(opcode: Int, owner: String, name: String, desc: String, itf: Boolean) { + imports ++= internalNameToClassName(owner) + imports ++= Type.getArgumentTypes(desc).flatMap(getClassName) + imports ++= getClassName(Type.getReturnType(desc)) + } + + override def visitFieldInsn(opcode: Int, owner: String, name: String, desc: String) { + imports ++= internalNameToClassName(owner) ++ getClassName(Type.getType(desc)).toList + + } + + override def visitTypeInsn(opcode: Int, `type` : String) { + imports ++= internalNameToClassName(`type`) + } + + override def visitTryCatchBlock(start: Label, end: Label, handler: Label, `type` : String) { + if (`type` != null) //null means finally block + imports ++= internalNameToClassName(`type`) + } + + override def visitLocalVariable(name: String, desc: String, signature: String, start: Label, end: Label, index: Int) { + imports += Type.getType(desc).getClassName + } + + override def visitLdcInsn(constant: AnyRef) { + constant match { + case typeConstant: Type => imports ++= getClassName(typeConstant) + case _ => + } + } + + override def visitInvokeDynamicInsn(name: String, desc: String, bootstrapMethod: Handle, bootstrapMethodArgs: AnyRef*) { + bootstrapMethodArgs.foreach { + case typeConstant: Type => + imports ++= getClassName(typeConstant) + case handle: Handle => + imports ++= internalNameToClassName(handle.getOwner) + imports ++= Type.getArgumentTypes(desc).flatMap(getClassName) + case _ : Number => + case _ : String => + case other => throw new AssertionError(s"Unexpected type ${other.getClass} with value '$other'") + } + } + + override def visitMaxs(maxStack: Int, maxLocals: Int) {} + override def visitLineNumber(line: Int, start: Label) {} + //only for debugging + override def visitLookupSwitchInsn(dflt: Label, keys: Array[Int], labels: Array[Label]) {} + + + override def visitTableSwitchInsn(min: Int, max: Int, dflt: Label, labels: Label*): Unit = super.visitTableSwitchInsn(min, max, dflt, labels: _*) + override def visitIincInsn(`var` : Int, increment: Int) {} + override def visitLabel(label: Label) {} + override def visitJumpInsn(opcode: Int, label: Label) {} + override def visitVarInsn(opcode: Int, `var` : Int) {} + override def visitIntInsn(opcode: Int, operand: Int) {} + override def visitInsn(opcode: Int) {} + override def visitFrame(`type` : Int, nLocal: Int, local: Array[AnyRef], nStack: Int, stack: Array[AnyRef]) {} + override def visitCode() {} +} + + + + diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AnalyzeSignatureVisitor.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AnalyzeSignatureVisitor.scala new file mode 100644 index 00000000000..693e0e17482 --- /dev/null +++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AnalyzeSignatureVisitor.scala @@ -0,0 +1,68 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.plugin.classanalysis + +import org.objectweb.asm.Opcodes +import org.objectweb.asm.signature.{SignatureReader, SignatureVisitor} + + +/** + * @author tonytv + */ + +private class AnalyzeSignatureVisitor(val analyzeClassVisitor: AnalyzeClassVisitor) + extends SignatureVisitor(Opcodes.ASM5) + with SubVisitorTrait { + + + override def visitEnd(): Unit = super.visitEnd() + + override def visitClassType(className: String) { + imports ++= internalNameToClassName(className) + } + + override def visitFormalTypeParameter(name: String) {} + + override def visitClassBound() = this + + override def visitInterfaceBound() = this + + override def visitSuperclass() = this + + override def visitInterface() = this + + override def visitParameterType() = this + + override def visitReturnType() = this + + override def visitExceptionType() = this + + override def visitBaseType(descriptor: Char) {} + + override def visitTypeVariable(name: String) {} + + override def visitArrayType() = this + + override def visitInnerClassType(name: String) {} + + override def visitTypeArgument() {} + + override def visitTypeArgument(wildcard: Char) = this +} + + +object AnalyzeSignatureVisitor { + def analyzeClass(signature: String, analyzeClassVisitor: AnalyzeClassVisitor) { + if (signature != null) { + new SignatureReader(signature).accept(new AnalyzeSignatureVisitor(analyzeClassVisitor)) + } + } + + def analyzeMethod(signature: String, analyzeClassVisitor: AnalyzeClassVisitor) { + analyzeClass(signature, analyzeClassVisitor) + } + + def analyzeField(signature: String, analyzeClassVisitor: AnalyzeClassVisitor) { + if (signature != null) + new SignatureReader(signature).acceptType(new AnalyzeSignatureVisitor(analyzeClassVisitor)) + } +} diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AnnotationVisitorTrait.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AnnotationVisitorTrait.scala new file mode 100644 index 00000000000..8beb47c765f --- /dev/null +++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AnnotationVisitorTrait.scala @@ -0,0 +1,39 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.plugin.classanalysis + +import org.objectweb.asm.{Opcodes, AnnotationVisitor, Type} + +/** + * Picks up classes used in annotations. + * @author tonytv + */ +private trait AnnotationVisitorTrait { + protected val imports: ImportsSet + + def visitAnnotation(desc: String, visible: Boolean): AnnotationVisitor = { + imports ++= getClassName(Type.getType(desc)).toList + + visitAnnotationDefault() + } + + def visitAnnotationDefault(): AnnotationVisitor = + new AnnotationVisitor(Opcodes.ASM5) { + override def visit(name: String, value: AnyRef) {} + + override def visitEnum(name: String, desc: String, value: String) { + imports ++= getClassName(Type.getType(desc)).toList + } + + override def visitArray(name: String): AnnotationVisitor = this + + override def visitAnnotation(name: String, desc: String): AnnotationVisitor = { + imports ++= getClassName(Type.getType(desc)).toList + this + } + + override def visitEnd() {} + } + + def visitParameterAnnotation(parameter: Int, desc: String, visible: Boolean): AnnotationVisitor = + visitAnnotation(desc, visible) +} diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AttributeVisitorTrait.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AttributeVisitorTrait.scala new file mode 100644 index 00000000000..0603cebf5af --- /dev/null +++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/AttributeVisitorTrait.scala @@ -0,0 +1,15 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.plugin.classanalysis + +import org.objectweb.asm.{Type, Attribute} + +/** + * @author tonytv + */ +private trait AttributeVisitorTrait { + protected val imports: ImportsSet + + def visitAttribute(attribute: Attribute) { + imports ++= getClassName(Type.getObjectType(attribute.`type`)).toList + } +} diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/ClassFileMetaData.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/ClassFileMetaData.scala new file mode 100644 index 00000000000..00ed9efb360 --- /dev/null +++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/ClassFileMetaData.scala @@ -0,0 +1,10 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.plugin.classanalysis + +/** + * The result of analyzing a .class file. + * @author tonytv + */ +sealed case class ClassFileMetaData(name:String, + referencedClasses : Set[String], + exportPackage : Option[ExportPackageAnnotation]) diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/ExportPackageAnnotation.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/ExportPackageAnnotation.scala new file mode 100644 index 00000000000..00265b80761 --- /dev/null +++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/ExportPackageAnnotation.scala @@ -0,0 +1,24 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.plugin.classanalysis + +import com.yahoo.container.plugin.util.Strings + +/** + * @author tonytv + */ +case class ExportPackageAnnotation(major: Int, minor: Int, micro: Int, qualifier: String) { + requireNonNegative(major, "major") + requireNonNegative(minor, "minor") + requireNonNegative(micro, "micro") + require(qualifier.matches("""(\p{Alpha}|\p{Digit}|_|-)*"""), + exportPackageError("qualifier must follow the format (alpha|digit|'_'|'-')* but was '%s'.".format(qualifier))) + + + private def requireNonNegative(i: Int, fieldName: String) { + require(i >= 0, exportPackageError("%s must be non-negative but was %d.".format(fieldName, i))) + } + + private def exportPackageError(s: String) = "ExportPackage anntotation: " + s + + def osgiVersion : String = (List(major, minor, micro) ++ Strings.noneIfEmpty(qualifier)).mkString(".") +} diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/PackageTally.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/PackageTally.scala new file mode 100644 index 00000000000..ecbf9448dfd --- /dev/null +++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/PackageTally.scala @@ -0,0 +1,46 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.plugin.classanalysis + +import com.yahoo.container.plugin.util.Maps + +/** + * + * @author tonytv + */ +final class PackageTally (private val definedPackagesMap : Map[String, Option[ExportPackageAnnotation]], + referencedPackagesUnfiltered : Set[String]) { + + val referencedPackages = referencedPackagesUnfiltered diff definedPackages + + def definedPackages = definedPackagesMap.keySet + + def exportedPackages = definedPackagesMap collect { case (name, Some(export)) => (name, export) } + + /** + * Represents the classes for two package tallies that are deployed as a single unit. + * + * ExportPackageAnnotations from this has precedence over the other. + */ + def combine(other: PackageTally): PackageTally = { + new PackageTally( + Maps.combine(definedPackagesMap, other.definedPackagesMap)(_ orElse _), + referencedPackages ++ other.referencedPackages) + } +} + + +object PackageTally { + def fromAnalyzedClassFiles(analyzedClassFiles : Seq[ClassFileMetaData]) : PackageTally = { + combine( + for (metaData <- analyzedClassFiles) + yield { + new PackageTally( + Map(Packages.packageName(metaData.name) -> metaData.exportPackage), + metaData.referencedClasses.map(Packages.packageName)) + }) + } + + def combine(packageTallies : Iterable[PackageTally]) : PackageTally = (empty /: packageTallies)(_.combine(_)) + + val empty : PackageTally = new PackageTally(Map(), Set()) +} diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/Packages.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/Packages.scala new file mode 100644 index 00000000000..2900d3f1551 --- /dev/null +++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/Packages.scala @@ -0,0 +1,27 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.plugin.classanalysis + +/** + * Utility methods related to packages. + * @author tonytv + */ +object Packages { + case class PackageMetaData(definedPackages: Set[String], referencedExternalPackages: Set[String]) + + def packageName(fullClassName: String) = { + def nullIfNotFound(index : Int) = if (index == -1) 0 else index + + fullClassName.substring(0, nullIfNotFound(fullClassName.lastIndexOf("."))) + } + + + + def analyzePackages(allClasses: Seq[ClassFileMetaData]): PackageMetaData = { + val (definedPackages, referencedClasses) = + (for (classMetaData <- allClasses) + yield (packageName(classMetaData.name), classMetaData.referencedClasses.map(packageName))). + unzip + + PackageMetaData(definedPackages.toSet, referencedClasses.flatten.toSet diff definedPackages.toSet) + } +} diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/SubVisitorTrait.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/SubVisitorTrait.scala new file mode 100644 index 00000000000..d4e7f4fb028 --- /dev/null +++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/SubVisitorTrait.scala @@ -0,0 +1,19 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.plugin.classanalysis + +import collection.mutable + +/** + * A visitor that's run for sub construct of a class + * and forwards all its imports the the owning ClassVisitor at the end. + * @author tonytv + */ +private trait SubVisitorTrait { + val analyzeClassVisitor : AnalyzeClassVisitor + + val imports : ImportsSet = mutable.Set() + + def visitEnd() { + analyzeClassVisitor.addImports(imports) + } +} diff --git a/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/package.scala b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/package.scala new file mode 100644 index 00000000000..a94bc7710d2 --- /dev/null +++ b/bundle-plugin/src/main/scala/com/yahoo/container/plugin/classanalysis/package.scala @@ -0,0 +1,24 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.plugin + +import org.objectweb.asm.Type +import collection.mutable + +package object classanalysis { + type ImportsSet = mutable.Set[String] + + def internalNameToClassName(internalClassName: String) : Option[String] = { + getClassName(Type.getObjectType(internalClassName)) + } + + def getClassName(aType: Type): Option[String] = { + import Type._ + + aType.getSort match { + case ARRAY => getClassName(aType.getElementType) + case OBJECT => Some(aType.getClassName) + case _ => None + } + } +} + |