com.autonomousapps.internal.asm.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of dependency-analysis-gradle-plugin Show documentation
Show all versions of dependency-analysis-gradle-plugin Show documentation
Analyzes dependency usage in Android and JVM projects
// Copyright (c) 2024. Tony Robalik.
// SPDX-License-Identifier: Apache-2.0
package com.autonomousapps.internal
import com.autonomousapps.internal.ClassNames.canonicalize
import com.autonomousapps.internal.asm.*
import com.autonomousapps.internal.utils.METHOD_DESCRIPTOR_REGEX
import com.autonomousapps.internal.utils.efficient
import com.autonomousapps.internal.utils.genericTypes
import kotlinx.metadata.jvm.Metadata
import org.gradle.api.logging.Logger
import java.util.concurrent.atomic.AtomicReference
private val logDebug: Boolean get() = isLogDebug()
private const val ASM_VERSION = Opcodes.ASM9
/**
* Passing `-Ddependency.analysis.bytecode.logging=true` will cause additional logs to print during bytecode analysis.
*
* `true` by default, meaning it suppresses console output (prints to debug stream).
*/
private fun isLogDebug(): Boolean {
return !System.getProperty("dependency.analysis.bytecode.logging", "false").toBoolean()
}
/** This will collect the class name and information about annotations. */
internal class ClassNameAndAnnotationsVisitor(private val logger: Logger) : ClassVisitor(ASM_VERSION) {
private lateinit var className: String
private lateinit var access: Access
private var outerClassName: String? = null
private var superClassName: String? = null
private val retentionPolicyHolder = AtomicReference("")
private var isAnnotation = false
private val methods = mutableSetOf()
private val innerClasses = mutableSetOf()
private var methodCount = 0
private var fieldCount = 0
// From old ConstantVisitor
private val constantClasses = mutableSetOf()
internal fun getAnalyzedClass(): AnalyzedClass {
val className = this.className
val access = this.access
val hasNoMembers = fieldCount == 0 && methodCount == 0
return AnalyzedClass(
className = className,
outerClassName = outerClassName,
superClassName = superClassName,
retentionPolicy = retentionPolicyHolder.get(),
isAnnotation = isAnnotation,
hasNoMembers = hasNoMembers,
access = access,
methods = methods.efficient(),
innerClasses = innerClasses.efficient(),
constantClasses = constantClasses
)
}
override fun visit(
version: Int,
access: Int,
name: String,
signature: String?,
superName: String?,
interfaces: Array?
) {
// This _must_ not be canonicalized, unless we also change accesses to be dotty instead of slashy
superClassName = superName
className = canonicalize(name)
if (interfaces?.contains("java/lang/annotation/Annotation") == true) {
isAnnotation = true
}
this.access = Access.fromInt(access)
val implementsClause = if (interfaces.isNullOrEmpty()) {
""
} else {
" implements ${interfaces.joinToString(", ")}"
}
log("ClassNameAndAnnotationsVisitor#visit: ${this.access} $name extends $superName$implementsClause")
}
override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor? {
if ("Ljava/lang/annotation/Retention;" == descriptor) {
log("- ClassNameAndAnnotationsVisitor#visitAnnotation ($className): descriptor=$descriptor visible=$visible")
return RetentionPolicyAnnotationVisitor(logger, className, retentionPolicyHolder)
}
return null
}
override fun visitMethod(
access: Int, name: String?, descriptor: String, signature: String?, exceptions: Array?
): MethodVisitor? {
log("- visitMethod: descriptor=$descriptor name=$name signature=$signature")
if (!("()V" == descriptor && ("" == name || "" == name))) {
// ignore constructors and static initializers
methodCount++
methods.add(Method(descriptor))
}
return null
}
override fun visitField(
access: Int, name: String, descriptor: String?, signature: String?, value: Any?
): FieldVisitor? {
log("- visitField: descriptor=$descriptor name=$name signature=$signature value=$value")
fieldCount++
// from old ConstantVisitor
if (isStaticFinal(access)) {
constantClasses.add(name)
}
return null
}
override fun visitInnerClass(name: String, outerName: String?, innerName: String?, access: Int) {
log("- visitInnerClass: name=$name outerName=$outerName innerName=$innerName")
if (outerName != null) {
outerClassName = canonicalize(outerName)
}
innerClasses.add(canonicalize(name))
}
override fun visitSource(source: String?, debug: String?) {
log("- visitSource: source=$source debug=$debug")
}
override fun visitEnd() {
log("- visitEnd: fieldCount=$fieldCount methodCount=$methodCount")
}
private fun log(msg: String) {
logger.debug(msg)
}
private class RetentionPolicyAnnotationVisitor(
private val logger: Logger,
private val className: String?,
private val retentionPolicyHolder: AtomicReference
) : AnnotationVisitor(ASM_VERSION) {
private fun log(msg: String) {
logger.debug(msg)
}
override fun visitEnum(name: String?, descriptor: String?, value: String?) {
if ("Ljava/lang/annotation/RetentionPolicy;" == descriptor) {
log(" - RetentionPolicyAnnotationVisitor#visitEnum ($className): $value")
retentionPolicyHolder.set(value)
}
}
}
}
internal data class ClassRef(
val classRef: String,
val kind: Kind,
) : Comparable {
enum class Kind {
ANNOTATION_VISIBLE,
ANNOTATION_HIDDEN,
NOT_ANNOTATION,
;
companion object {
fun annotation(visible: Boolean): Kind = if (visible) ANNOTATION_VISIBLE else ANNOTATION_HIDDEN
}
}
override fun compareTo(other: ClassRef): Int {
return compareBy { it.classRef }
.thenBy { it.kind }
.compare(this, other)
}
}
/**
* This will collect the class name and the name of all classes used by this class and the methods of this class.
*/
internal class ClassAnalyzer(private val logger: Logger) : ClassVisitor(ASM_VERSION) {
var source: String? = null
lateinit var className: String
val classes = mutableSetOf()
private val methodAnalyzer = MethodAnalyzer(logger, classes)
private val fieldAnalyzer = FieldAnalyzer(logger, classes)
private fun addClass(className: String?, kind: ClassRef.Kind) {
classes.addClass(className, kind)
}
private fun log(msg: String) {
if (logDebug) {
logger.debug(msg)
} else {
logger.warn(msg)
}
}
override fun visitSource(source: String?, debug: String?) {
log("- visitSource: source=$source debug=$debug")
this.source = source
}
override fun visit(
version: Int,
access: Int,
name: String,
signature: String?,
superName: String?,
interfaces: Array?
) {
log("ClassAnalyzer#visit: $name extends $superName")
className = name
addClass("L$superName;", ClassRef.Kind.NOT_ANNOTATION)
interfaces?.forEach { i ->
addClass("L$i;", ClassRef.Kind.NOT_ANNOTATION)
}
}
override fun visitField(
access: Int,
name: String?,
descriptor: String?,
signature: String?,
value: Any?
): FieldVisitor {
log("ClassAnalyzer#visitField: $descriptor $name")
addClass(descriptor, ClassRef.Kind.NOT_ANNOTATION)
// TODO probably do this for other `visitX` methods as well
signature?.genericTypes()?.forEach {
addClass(it, ClassRef.Kind.NOT_ANNOTATION)
}
return fieldAnalyzer
}
override fun visitMethod(
access: Int,
name: String?,
descriptor: String?,
signature: String?,
exceptions: Array?
): MethodVisitor {
log("ClassAnalyzer#visitMethod: $name $descriptor")
descriptor?.let {
METHOD_DESCRIPTOR_REGEX.findAll(it).forEach { result ->
addClass(result.value, ClassRef.Kind.NOT_ANNOTATION)
}
}
return methodAnalyzer
}
override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor {
log("ClassAnalyzer#visitAnnotation: descriptor=$descriptor visible=$visible")
addClass(descriptor, ClassRef.Kind.annotation(visible))
return AnnotationAnalyzer(visible, logger, classes)
}
override fun visitTypeAnnotation(
typeRef: Int,
typePath: TypePath?,
descriptor: String?,
visible: Boolean
): AnnotationVisitor {
log("ClassAnalyzer#visitTypeAnnotation: typeRef=$typeRef typePath=$typePath descriptor=$descriptor visible=$visible")
addClass(descriptor, ClassRef.Kind.annotation(visible))
return AnnotationAnalyzer(visible, logger, classes)
}
override fun visitEnd() {
log("\n")
}
}
private class MethodAnalyzer(
private val logger: Logger,
private val classes: MutableSet
) : MethodVisitor(ASM_VERSION) {
private fun addClass(className: String?, kind: ClassRef.Kind) {
classes.addClass(className, kind)
}
private fun log(msg: String) {
if (logDebug) {
logger.debug(msg)
} else {
logger.warn(msg)
}
}
override fun visitTypeInsn(opcode: Int, type: String?) {
log("- MethodAnalyzer#visitTypeInsn: $type")
// Type can look like `java/lang/Enum` or `[Lcom/package/Thing;`, which is fucking weird
addClass(if (type?.startsWith("[") == true) type else "L$type;", ClassRef.Kind.NOT_ANNOTATION)
}
override fun visitFieldInsn(opcode: Int, owner: String?, name: String?, descriptor: String?) {
log("- MethodAnalyzer#visitFieldInsn: $owner.$name $descriptor")
addClass("L$owner;", ClassRef.Kind.NOT_ANNOTATION)
addClass(descriptor, ClassRef.Kind.NOT_ANNOTATION)
}
override fun visitMethodInsn(
opcode: Int,
owner: String?,
name: String?,
descriptor: String?,
isInterface: Boolean
) {
log("- MethodAnalyzer#visitMethodInsn: $owner.$name $descriptor")
// Owner can look like `java/lang/Enum` or `[Lcom/package/Thing;`, which is fucking weird
addClass(if (owner?.startsWith("[") == true) owner else "L$owner;", ClassRef.Kind.NOT_ANNOTATION)
descriptor?.let {
METHOD_DESCRIPTOR_REGEX.findAll(it).forEach { result ->
addClass(result.value, ClassRef.Kind.NOT_ANNOTATION)
}
}
}
override fun visitInvokeDynamicInsn(
name: String?,
descriptor: String?,
bootstrapMethodHandle: Handle?,
vararg bootstrapMethodArguments: Any?
) {
log("- MethodAnalyzer#visitInvokeDynamicInsn: $name $descriptor")
addClass(descriptor, ClassRef.Kind.NOT_ANNOTATION)
}
override fun visitLocalVariable(
name: String?,
descriptor: String?,
signature: String?,
start: Label?,
end: Label?,
index: Int
) {
log("- MethodAnalyzer#visitLocalVariable: $name $descriptor")
// TODO probably do this for other `visitX` methods as well
signature?.genericTypes()?.forEach {
addClass(it, ClassRef.Kind.NOT_ANNOTATION)
}
addClass(descriptor, ClassRef.Kind.NOT_ANNOTATION)
}
override fun visitLocalVariableAnnotation(
typeRef: Int,
typePath: TypePath?,
start: Array?,
end: Array?,
index: IntArray?,
descriptor: String?,
visible: Boolean
): AnnotationVisitor {
log("- MethodAnalyzer#visitLocalVariableAnnotation: $descriptor")
addClass(descriptor, ClassRef.Kind.NOT_ANNOTATION)
return AnnotationAnalyzer(visible, logger, classes)
}
override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor {
log("- MethodAnalyzer#visitAnnotation: $descriptor")
addClass(descriptor, ClassRef.Kind.annotation(visible))
return AnnotationAnalyzer(visible, logger, classes)
}
override fun visitInsnAnnotation(
typeRef: Int,
typePath: TypePath?,
descriptor: String?,
visible: Boolean
): AnnotationVisitor {
log("- MethodAnalyzer#visitInsnAnnotation: $descriptor")
addClass(descriptor, ClassRef.Kind.annotation(visible))
return AnnotationAnalyzer(visible, logger, classes)
}
override fun visitParameterAnnotation(parameter: Int, descriptor: String?, visible: Boolean): AnnotationVisitor {
log("- MethodAnalyzer#visitParameterAnnotation: $descriptor")
addClass(descriptor, ClassRef.Kind.ANNOTATION_VISIBLE)
return AnnotationAnalyzer(visible, logger, classes)
}
override fun visitTypeAnnotation(
typeRef: Int,
typePath: TypePath?,
descriptor: String?,
visible: Boolean
): AnnotationVisitor {
log("- MethodAnalyzer#visitTypeAnnotation: $descriptor")
addClass(descriptor, ClassRef.Kind.annotation(visible))
return AnnotationAnalyzer(visible, logger, classes)
}
override fun visitTryCatchBlock(start: Label?, end: Label?, handler: Label?, type: String?) {
log("- MethodAnalyzer#visitTryCatchBlock: $type")
addClass("L$type;", ClassRef.Kind.NOT_ANNOTATION)
}
override fun visitTryCatchAnnotation(
typeRef: Int,
typePath: TypePath?,
descriptor: String?,
visible: Boolean
): AnnotationVisitor {
log("- MethodAnalyzer#visitTryCatchAnnotation: $descriptor")
addClass(descriptor, ClassRef.Kind.annotation(visible))
return AnnotationAnalyzer(visible, logger, classes)
}
}
private class AnnotationAnalyzer(
private val visible: Boolean,
private val logger: Logger,
private val classes: MutableSet,
private val level: Int = 0,
private val arrayName: String? = null
) : AnnotationVisitor(ASM_VERSION) {
private var arraySize = 0
private var isTypeAlias = false
private val arrayElements = mutableSetOf()
// If this is a visible annotation, then internal references are needed at runtime (as well as compile time).
// Our poor shorthand for modeling that is to say that reference is a `Kind.NOT_ANNOTATION`
// nb: this is intentionally if confusingly different from the behavior of `ClassRef.Kind.annotation(visible)`
private val kind = if (visible) ClassRef.Kind.NOT_ANNOTATION else ClassRef.Kind.ANNOTATION_VISIBLE
private fun addClass(className: String?, kind: ClassRef.Kind) {
classes.addClass(className, kind)
if (arrayName == "d2") {
arrayElements.addClass(className, kind)
}
}
private fun log(msg: String) {
if (logDebug) {
logger.debug(msg)
} else {
logger.warn(msg)
}
}
private fun indent() = " ".repeat(level)
override fun visit(name: String?, value: Any?) {
val valueString = stringValueOfArrayElement(value)
log("${indent()}- AnnotationAnalyzer#visit: name=$name, value=(${value?.javaClass?.simpleName}, ${valueString})")
if (arrayName != null) {
arraySize++
if (valueString == "alias") {
isTypeAlias = true
}
}
if (value is String) {
METHOD_DESCRIPTOR_REGEX.findAll(value).forEach { result ->
addClass(result.value, kind)
}
} else if (value is Type) {
addClass(value.descriptor, kind)
}
}
override fun visitEnum(name: String?, descriptor: String?, value: String?) {
log("${indent()}- AnnotationAnalyzer#visitEnum: name=$name, descriptor=$descriptor, value=$value")
addClass(descriptor, kind)
}
override fun visitAnnotation(name: String?, descriptor: String?): AnnotationVisitor {
log("${indent()}- AnnotationAnalyzer#visitAnnotation: name=$name, descriptor=$descriptor")
addClass(descriptor, kind)
return AnnotationAnalyzer(visible, logger, classes, level + 1)
}
override fun visitArray(name: String?): AnnotationVisitor {
log("${indent()}- AnnotationAnalyzer#visitArray: name=$name")
return AnnotationAnalyzer(if (name == "d2") false else visible, logger, classes, level + 1, name)
}
override fun visitEnd() {
if (isTypeAlias()) {
// Transform the "Kind.ANNOTATION" references into "Kind.NOT_ANNOTATION" references so that our
// "is this typealias used?" algorithm works.
classes.addAll(arrayElements.map { ClassRef(it.classRef, ClassRef.Kind.NOT_ANNOTATION) })
}
}
// The elements of the array will look like... (:)
// { String:MyAlias, String:L/com/example/Aliased;, String:alias }
private fun isTypeAlias() = arraySize == 3 && isTypeAlias
}
private class FieldAnalyzer(
private val logger: Logger,
private val classes: MutableSet
) : FieldVisitor(ASM_VERSION) {
private fun log(msg: String) {
if (logDebug) {
logger.debug(msg)
} else {
logger.warn(msg)
}
}
private fun addClass(className: String?, kind: ClassRef.Kind) {
classes.addClass(className, kind)
}
override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor {
log("- FieldAnalyzer#visitAnnotation: $descriptor")
addClass(descriptor, ClassRef.Kind.annotation(visible))
return AnnotationAnalyzer(visible, logger, classes)
}
}
private fun MutableSet.addClass(classRef: String?, kind: ClassRef.Kind) {
classRef?.let {
// Strip array indicators
it.replace("[", "")
// Only add class types (not primitives)
if (it.startsWith("L")) {
add(ClassRef(it.substring(1, it.length - 1), kind))
}
}
}
/* ===================================================================================================
* Below here used for parsing kotlin.Metadata with the ultimate goal of listing all inline functions.
* ===================================================================================================
*/
internal class KotlinClassHeaderBuilder {
var kind: Int = 1
var metadataVersion: IntArray? = null
var bytecodeVersion: IntArray? = null
var data1 = mutableListOf()
var data2 = mutableListOf()
var extraString: String? = null
var packageName: String? = null
var extraInt: Int = 0
fun build(): Metadata {
return Metadata(
kind = kind,
metadataVersion = metadataVersion,
data1 = data1.toTypedArray(),
data2 = data2.toTypedArray(),
extraString = extraString,
packageName = packageName,
extraInt = extraInt
)
}
}
private const val KOTLIN_METADATA = "Lkotlin/Metadata;"
internal class KotlinMetadataVisitor(
private val logger: Logger
) : ClassVisitor(ASM_VERSION) {
internal lateinit var className: String
internal var builder: KotlinClassHeaderBuilder? = null
private fun log(msg: String) {
if (logDebug) {
logger.debug(msg)
} else {
logger.warn(msg)
}
}
override fun visit(
version: Int,
access: Int,
name: String,
signature: String?,
superName: String?,
interfaces: Array?
) {
log("KotlinMetadataVisitor#visit: $name extends $superName")
className = name
}
override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor? {
log("KotlinMetadataVisitor#visitAnnotation: descriptor=$descriptor visible=$visible")
return if (KOTLIN_METADATA == descriptor) {
builder = KotlinClassHeaderBuilder()
KotlinAnnotationVisitor(logger, builder!!)
} else {
null
}
}
private class KotlinAnnotationVisitor(
private val logger: Logger,
private val builder: KotlinClassHeaderBuilder,
private val level: Int = 0,
private val arrayName: String? = null
) : AnnotationVisitor(ASM_VERSION) {
private fun log(msg: String) {
if (logDebug) {
logger.debug(msg)
} else {
logger.warn(msg)
}
}
private fun indent() = " ".repeat(level)
override fun visit(name: String?, value: Any?) {
log("${indent()}- visit: name=$name, value=(${value?.javaClass?.simpleName}, ${stringValueOfArrayElement(value)})")
when (name) {
"k" -> builder.kind = value as Int
"mv" -> builder.metadataVersion = value as IntArray
"bv" -> builder.bytecodeVersion = value as IntArray
"xs" -> builder.extraString = value as String
"pn" -> builder.packageName = value as String
"xi" -> builder.extraInt = value as Int
}
when (arrayName) {
"d1" -> builder.data1.add(value as String)
"d2" -> builder.data2.add(value as String)
}
}
override fun visitArray(name: String?): AnnotationVisitor {
log("${indent()}- visitArray: name=$name")
return KotlinAnnotationVisitor(logger, builder, level + 1, name)
}
}
}
fun stringValueOfArrayElement(value: Any?): String {
return if (value is String && value.contains("\n")) {
"..."
} else {
value.toString()
}
}
private fun isStaticFinal(access: Int): Boolean =
access and Opcodes.ACC_STATIC != 0 && access and Opcodes.ACC_FINAL != 0
© 2015 - 2025 Weber Informatics LLC | Privacy Policy