com.autonomousapps.internal.classReferenceParsers.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
package com.autonomousapps.internal
import com.autonomousapps.advice.VariantFile
import com.autonomousapps.internal.asm.ClassReader
import com.autonomousapps.internal.utils.*
import org.gradle.api.logging.Logger
import java.io.File
import java.util.zip.ZipFile
internal sealed class ProjectClassReferenceParser(
protected val variantFiles: Set,
private val layouts: Set,
private val testFiles: Set
) {
private val logger = getLogger()
/**
* Source is either a jar or set of class files.
*/
protected abstract fun parseBytecode(): List
protected fun variantsFromFile(file: File): Set {
return variantsFromPath(file.path)
}
/**
* Associate file paths to variants/source sets.
*/
protected fun variantsFromPath(path: String): Set {
val fileExtension = path.substring(path.lastIndexOf("."))
return variantFiles.filter {
path.endsWith("${it.filePath}${fileExtension}")
}.mapToOrderedSet {
it.variant
}
}
private fun parseLayouts(): List {
return layouts.flatMap { layoutFile ->
val variants = variantsFromFile(layoutFile)
buildDocument(layoutFile).getElementsByTagName("*")
.map { it.nodeName }
.filter { nodeName ->
nodeName.contains(".")
}.map {
VariantClass(it, variants)
}
}
}
private fun parseTestSource(): List {
return testFiles
.filter { it.extension == "class" }
.map { classFile ->
val variants = variantsFromFile(classFile)
val usedClasses = classFile.inputStream().use { BytecodeParser(it.readBytes(), logger).parse() }
variants to usedClasses
}.flatMap { (variants, classes) ->
classes.map {
VariantClass(it, variants)
}
}
}
// TODO some jars only have metadata. What to do about them?
// 1. e.g. kotlin-stdlib-common-1.3.50.jar
// 2. e.g. legacy-support-v4-1.0.0/jars/classes.jar
internal fun analyze(): Set {
val variants = parseBytecode().plus(parseLayouts()).plus(parseTestSource())
return variants.merge()
}
private fun List.merge(): Set {
// a Collection is functionally a map
val map = LinkedHashMap>()
forEach {
val theClass = it.theClass
val variants = it.variants
map.merge(theClass, variants.toSortedSet()) { oldSet, newSet ->
oldSet.apply { addAll(newSet) }
}
}
return map.map { (theClass, variants) ->
VariantClass(theClass, variants)
}.toSortedSet()
}
}
/**
* Given a jar and, optionally, and a set of Android layout files, produce a set of FQCN references
* present in these inputs, as strings. These inputs are part of a single logical whole, viz., the
* Gradle project being analyzed.
*/
internal class JarReader(
variantFiles: Set,
jarFile: File,
layouts: Set,
testFiles: Set
) : ProjectClassReferenceParser(
variantFiles = variantFiles,
layouts = layouts,
testFiles = testFiles
) {
private val logger = getLogger()
private val zipFile = ZipFile(jarFile)
override fun parseBytecode(): List {
return zipFile.asClassFiles()
.map { classEntry ->
val variants = variantsFromPath(classEntry.name)
val usedClasses = zipFile.getInputStream(classEntry).use { BytecodeParser(it.readBytes(), logger).parse() }
variants to usedClasses
}.flatMap { (variants, classes) ->
classes.map { VariantClass(it, variants) }
}
}
}
/**
* Given a set of .class files and, optionally, and a set of Android layout files, produce a set of
* FQCN references present in these inputs, as strings. These inputs are part of a single logical
* whole, viz., the Gradle project being analyzed.
*/
internal class ClassSetReader(
private val classes: Set,
variantFiles: Set,
layouts: Set,
testFiles: Set
) : ProjectClassReferenceParser(
variantFiles = variantFiles,
layouts = layouts,
testFiles = testFiles
) {
private val logger = getLogger()
override fun parseBytecode(): List {
return classes.map { classFile ->
val variants = variantsFromFile(classFile)
val usedClasses = classFile.inputStream().use { BytecodeParser(it.readBytes(), logger).parse() }
variants to usedClasses
}.flatMap { (variants, classes) ->
classes.map {
VariantClass(it, variants)
}
}
}
}
private class BytecodeParser(
private val bytes: ByteArray,
private val logger: Logger
) {
/**
* This (currently, maybe forever) fails to detect constant usage in Kotlin-generated class files. Works just fine
* for Java.
*/
fun parse(): Set {
val constantPool = ConstantPoolParser.getConstantPoolClassReferences(bytes)
// Constant pool has a lot of weird bullshit in it
.filter { JAVA_FQCN_REGEX_SLASHY.matches(it) }
val classEntries = ClassReader(bytes).let { classReader ->
ClassAnalyzer(logger).apply {
classReader.accept(this, 0)
}
}.classes()
return constantPool.plus(classEntries)
// Filter out `java` packages, but not `javax`
.filterNot { it.startsWith("java/") }
.mapToSet { it.replace("/", ".") }
}
}