Skip to content

Commit

Permalink
Look for InlineMembers in class files, not just jar files.
Browse files Browse the repository at this point in the history
  • Loading branch information
autonomousapps committed Nov 9, 2023
1 parent 7036c9c commit 7b2b6f5
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 82 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,20 @@ package com.autonomousapps
import com.autonomousapps.Flags.compatibility
import com.autonomousapps.internal.GradleVersions
import com.autonomousapps.internal.android.AgpVersion
import com.autonomousapps.internal.utils.getLogger
import com.autonomousapps.subplugin.ProjectPlugin
import com.autonomousapps.subplugin.RootPlugin
import org.gradle.api.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.logging.Logger

internal const val TASK_GROUP_DEP = "dependency-analysis"
internal const val TASK_GROUP_DEP_INTERNAL = "dependency-analysis-internal"

/** For use in contexts where a logger isn't easily available */
internal val PROJECT_LOGGER: Logger = getLogger<DependencyAnalysisPlugin>()

@Suppress("unused")
class DependencyAnalysisPlugin : Plugin<Project> {

Expand Down
7 changes: 2 additions & 5 deletions src/main/kotlin/com/autonomousapps/internal/JarExploder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.autonomousapps.internal.utils.asSequenceOfClassFiles
import com.autonomousapps.internal.utils.getLogger
import com.autonomousapps.model.KtFile
import com.autonomousapps.model.PhysicalArtifact
import com.autonomousapps.model.PhysicalArtifact.Mode
import com.autonomousapps.model.intermediates.AndroidLinterDependency
import com.autonomousapps.model.intermediates.ExplodedJar
import com.autonomousapps.model.intermediates.ExplodingJar
Expand All @@ -18,10 +19,6 @@ internal class JarExploder(
private val inMemoryCache: InMemoryCache
) {

private enum class Mode {
ZIP, CLASSES
}

private val logger = getLogger<ExplodeJarTask>()

fun explodedJars(): Set<ExplodedJar> {
Expand Down Expand Up @@ -76,7 +73,7 @@ internal class JarExploder(
}

Mode.CLASSES -> {
ktFiles = KtFile.fromDirectory(artifact.file).toSet()
ktFiles = KtFile.fromDirectory(artifact.file)

artifact.file.walkBottomUp()
.filter { it.isFile && it.name.endsWith(".class") }
Expand Down
13 changes: 13 additions & 0 deletions src/main/kotlin/com/autonomousapps/internal/utils/Files.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.autonomousapps.internal.utils

import java.io.File

internal object Files {
fun relativize(file: File, after: String): String {
return file.absolutePath.substringAfter(after)
}

fun asPackagePath(file: File): String {
return relativize(file, "build/classes/kotlin/main/")
}
}
8 changes: 5 additions & 3 deletions src/main/kotlin/com/autonomousapps/model/KtFile.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.autonomousapps.model

import com.squareup.moshi.JsonClass
import kotlinx.metadata.jvm.KotlinModuleMetadata
import kotlinx.metadata.jvm.UnstableMetadataApi
import java.io.File
import java.io.InputStream
import java.util.zip.ZipFile
Expand Down Expand Up @@ -46,18 +47,19 @@ data class KtFile(

private fun fromFile(file: File): Set<KtFile> = fromInputStream(file.inputStream())

@OptIn(UnstableMetadataApi::class)
private fun fromInputStream(input: InputStream): Set<KtFile> {
val bytes = input.use { it.readBytes() }
val metadata = KotlinModuleMetadata.read(bytes)
val module = metadata?.toKmModule()
val module = metadata.kmModule

return module?.packageParts?.flatMap { (packageName, parts) ->
return module.packageParts.flatMap { (packageName, parts) ->
parts.fileFacades.map { facade ->
// com/example/library/ConstantsKt --> [com.example.library.ConstantsKt, ConstantsKt]
val fqcn = facade.replace('/', '.')
KtFile(fqcn, fqcn.removePrefix("$packageName."))
}
}.orEmpty().toSortedSet()
}.toSortedSet()
}
}
}
46 changes: 39 additions & 7 deletions src/main/kotlin/com/autonomousapps/model/PhysicalArtifact.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.autonomousapps.model

import com.autonomousapps.PROJECT_LOGGER
import com.autonomousapps.internal.utils.toCoordinates
import com.squareup.moshi.JsonClass
import org.gradle.api.artifacts.result.ResolvedArtifactResult
Expand All @@ -9,12 +10,24 @@ import java.io.File
internal data class PhysicalArtifact(
val coordinates: Coordinates,
/** Physical artifact on disk; a jar file or directory pointing to class files. */
val file: File
val file: File,
) : Comparable<PhysicalArtifact> {

fun isJar(): Boolean = file.name.endsWith(".jar")
enum class Mode {
ZIP,
CLASSES
}

init {
check(isJar() || containsClassFiles()) {
"'file' must either be a jar or a directory that contains class files. Was '$file'"
}
}

val mode: Mode = if (isJar()) Mode.ZIP else Mode.CLASSES

fun containsClassFiles(): Boolean = file.walkBottomUp().any { f -> f.name.endsWith(".class") }
fun isJar(): Boolean = isJar(file)
fun containsClassFiles(): Boolean = containsClassFiles(file)

override fun compareTo(other: PhysicalArtifact): Int {
return coordinates.compareTo(other.coordinates).let {
Expand All @@ -26,9 +39,28 @@ internal data class PhysicalArtifact(
internal fun of(
artifact: ResolvedArtifactResult,
file: File,
) = PhysicalArtifact(
coordinates = artifact.toCoordinates(),
file = file
)
): PhysicalArtifact? {
if (!isValidArtifact(file)) {
PROJECT_LOGGER.debug(
"$artifact is not valid as a PhysicalArtifact. $file is neither a jar nor a class-files-containing directory"
)
return null
}

return PhysicalArtifact(
coordinates = artifact.toCoordinates(),
file = file
)
}

/**
* The [ArtifactCollection][org.gradle.api.artifacts.ArtifactCollection] in
* [ArtifactsReportTask][com.autonomousapps.tasks.ArtifactsReportTask.compileArtifacts] sometimes contains empty
* directories from Gradle transforms, and these are not valid as [PhysicalArtifact]s.
*/
private fun isValidArtifact(file: File): Boolean = isJar(file) || containsClassFiles(file)

private fun isJar(file: File): Boolean = file.name.endsWith(".jar")
private fun containsClassFiles(file: File): Boolean = file.walkBottomUp().any { f -> f.name.endsWith(".class") }
}
}
176 changes: 109 additions & 67 deletions src/main/kotlin/com/autonomousapps/tasks/FindInlineMembersTask.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import com.autonomousapps.internal.KotlinMetadataVisitor
import com.autonomousapps.internal.asm.ClassReader
import com.autonomousapps.internal.utils.*
import com.autonomousapps.model.InlineMemberCapability
import com.autonomousapps.model.KtFile
import com.autonomousapps.model.PhysicalArtifact
import com.autonomousapps.model.PhysicalArtifact.Mode
import com.autonomousapps.model.intermediates.InlineMemberDependency
import com.autonomousapps.services.InMemoryCache
import kotlinx.metadata.Flag
import kotlinx.metadata.KmDeclarationContainer
import kotlinx.metadata.KmFunction
import kotlinx.metadata.KmProperty
import kotlinx.metadata.isInline
import kotlinx.metadata.jvm.KotlinClassMetadata
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
Expand Down Expand Up @@ -96,9 +98,9 @@ internal class InlineMembersFinder(

fun find(): Set<InlineMemberDependency> = artifacts.asSequence()
.filter {
it.file.name.endsWith(".jar")
it.isJar() || it.containsClassFiles()
}.map { artifact ->
artifact to findInlineMembers(artifact)
artifact to findInlineMembers(artifact, artifact.mode)
}.filterNot { (_, inlineMembers) ->
inlineMembers.isEmpty()
}.map { (artifact, inlineMembers) ->
Expand All @@ -117,84 +119,124 @@ internal class InlineMembersFinder(
* An import statement with either of those would import the `kotlin.jdk7.use()` inline function, contributed by the
* "org.jetbrains.kotlin:kotlin-stdlib-jdk7" module.
*/
private fun findInlineMembers(artifact: PhysicalArtifact): Set<InlineMemberCapability.InlineMember> {
private fun findInlineMembers(
artifact: PhysicalArtifact,
mode: Mode,
): Set<InlineMemberCapability.InlineMember> {
val alreadyFoundInlineMembers = inMemoryCache.inlineMember(artifact.file.absolutePath)
if (alreadyFoundInlineMembers != null) {
return alreadyFoundInlineMembers
}

val zipFile = ZipFile(artifact.file)
val entries = zipFile.entries().toList()
// Only look at jars that have actual Kotlin classes in them
if (entries.none { it.name.endsWith(".kotlin_module") }) {
return emptySet()
fun packageName(fileLike: String): String {
return if (fileLike.contains('/')) {
// entry is in a package
fileLike.substringBeforeLast('/').replace('/', '.')
} else {
// entry is in root; no package
""
}
}

return entries.asSequenceOfClassFiles()
.mapNotNull { entry ->
// TODO an entry with `META-INF/proguard/androidx-annotations.pro`
val classReader = zipFile.getInputStream(entry).use { ClassReader(it.readBytes()) }
val metadataVisitor = KotlinMetadataVisitor(logger)
classReader.accept(metadataVisitor, 0)

val inlineMembers = metadataVisitor.builder?.let { header ->
when (val metadata = KotlinClassMetadata.read(header.build())) {
is KotlinClassMetadata.Class -> inlineMembers(metadata.kmClass)
is KotlinClassMetadata.FileFacade -> inlineMembers(metadata.kmPackage)
is KotlinClassMetadata.MultiFileClassPart -> inlineMembers(metadata.kmPackage)
is KotlinClassMetadata.SyntheticClass -> {
logger.debug("Ignoring SyntheticClass $entry")
emptySet()
}

is KotlinClassMetadata.MultiFileClassFacade -> {
logger.debug("Ignoring MultiFileClassFacade $entry")
emptySet()
}

is KotlinClassMetadata.Unknown -> {
logger.debug("Ignoring Unknown $entry")
emptySet()
}
}
} ?: emptySet()

// return early if no members found
if (inlineMembers.isEmpty()) return@mapNotNull null

val pn = if (entry.name.contains('/')) {
// entry is in a package
entry.name.substringBeforeLast('/').replace('/', '.')
} else {
// entry is in root; no package
""
val inlineMembers = when (mode) {
Mode.ZIP -> {
val zipFile = ZipFile(artifact.file)
val entries = zipFile.entries().toList()
// Only look at jars that have actual Kotlin classes in them
if (entries.none { it.name.endsWith(".kotlin_module") }) {
return emptySet()
}

// return non-empty members
InlineMemberCapability.InlineMember(
packageName = pn,
// Guaranteed to be non-empty
inlineMembers = inlineMembers
)
}.toSortedSet()
.also {
inMemoryCache.inlineMembers(artifact.file.absolutePath, it)
entries.asSequenceOfClassFiles()
.mapNotNull { entry ->
// TODO an entry with `META-INF/proguard/androidx-annotations.pro`
val inlineMembers = readClass(
zipFile.getInputStream(entry).use { ClassReader(it.readBytes()) },
entry.toString()
) ?: return@mapNotNull null

// return non-empty members
InlineMemberCapability.InlineMember(
packageName = packageName(entry.name),
// Guaranteed to be non-empty
inlineMembers = inlineMembers
)
}.toSortedSet()
}
}

private fun inlineMembers(kmDeclaration: KmDeclarationContainer): Set<String> {
return (inlineFunctions(kmDeclaration.functions) + inlineProperties(kmDeclaration.properties)).toSortedSet()
Mode.CLASSES -> {
if (KtFile.fromDirectory(artifact.file).isEmpty()) {
return emptySet()
}

artifact.file.walkBottomUp()
.filter { it.isFile && it.name.endsWith(".class") }
.mapNotNull { classFile ->
val inlineMembers = readClass(
classFile.inputStream().use { ClassReader(it.readBytes()) },
classFile.toString()
) ?: return@mapNotNull null

// return non-empty members
InlineMemberCapability.InlineMember(
packageName = packageName(Files.asPackagePath(classFile)),
// Guaranteed to be non-empty
inlineMembers = inlineMembers
)
}.toSortedSet()
}
}

// cache
inMemoryCache.inlineMembers(artifact.file.absolutePath, inlineMembers)

return inlineMembers
}

private fun inlineFunctions(functions: List<KmFunction>): Sequence<String> {
return functions.asSequence()
.filter { Flag.Function.IS_INLINE(it.flags) }
.map { it.name }
/** Returned set is either null or non-empty. */
private fun readClass(classReader: ClassReader, classFile: String): Set<String>? {
val metadataVisitor = KotlinMetadataVisitor(logger)
classReader.accept(metadataVisitor, 0)

val inlineMembers = metadataVisitor.builder?.let { header ->
when (val metadata = KotlinClassMetadata.read(header.build())) {
is KotlinClassMetadata.Class -> inlineMembers(metadata.kmClass)
is KotlinClassMetadata.FileFacade -> inlineMembers(metadata.kmPackage)
is KotlinClassMetadata.MultiFileClassPart -> inlineMembers(metadata.kmPackage)
is KotlinClassMetadata.SyntheticClass -> {
logger.debug("Ignoring SyntheticClass $classFile")
null
}

is KotlinClassMetadata.MultiFileClassFacade -> {
logger.debug("Ignoring MultiFileClassFacade $classFile")
null
}

is KotlinClassMetadata.Unknown -> {
logger.debug("Ignoring Unknown $classFile")
null
}
}
} ?: return null

// It's part of the contract to never return an empty set
return inlineMembers.ifEmpty { null }
}

private fun inlineProperties(properties: List<KmProperty>): Sequence<String> {
return properties.asSequence()
.filter { Flag.PropertyAccessor.IS_INLINE(it.flags) }
.map { it.name }
private fun inlineMembers(kmDeclaration: KmDeclarationContainer): Set<String> {
fun inlineFunctions(functions: List<KmFunction>): Sequence<String> {
return functions.asSequence()
.filter { it.isInline }
.map { it.name }
}

fun inlineProperties(properties: List<KmProperty>): Sequence<String> {
return properties.asSequence()
.filter { it.getter.isInline }
.map { it.name }
}

return (inlineFunctions(kmDeclaration.functions) + inlineProperties(kmDeclaration.properties)).toSortedSet()
}
}
Loading

0 comments on commit 7b2b6f5

Please sign in to comment.