Skip to content

Commit

Permalink
Add exclusion controls to ABI filtering
Browse files Browse the repository at this point in the history
  • Loading branch information
ZacSweers authored and autonomousapps committed May 15, 2020
1 parent f0dbe46 commit e4e1d2a
Show file tree
Hide file tree
Showing 13 changed files with 306 additions and 19 deletions.
6 changes: 6 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ dependencies {
testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0") {
because("Writing manual stubs for Configuration seems stupid")
}
testImplementation("com.github.tschuchortdev:kotlin-compile-testing:1.2.8") {
because("Easy in-memory compilation as a means to get compiled Kotlin class files")
}
testImplementation("com.squareup.okio:okio:2.6.0") {
because("Easy IO APIs")
}
val truthVersion = "1.0.1"
testImplementation("com.google.truth:truth:$truthVersion") {
because("Groovy's == behavior on Comparable classes is beyond stupid")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import org.gradle.api.Action
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Property
import org.gradle.api.provider.SetProperty
import org.gradle.kotlin.dsl.newInstance
import org.gradle.kotlin.dsl.property
import org.gradle.kotlin.dsl.setProperty
import org.intellij.lang.annotations.Language
import java.io.Serializable
import javax.inject.Inject

Expand Down Expand Up @@ -36,7 +38,9 @@ open class DependencyAnalysisExtension(objects: ObjectFactory) : AbstractExtensi
it.convention(true)
}

internal val issueHandler: IssueHandler = objects.newInstance(IssueHandler::class.java)
internal val issueHandler: IssueHandler = objects.newInstance(IssueHandler::class)

internal val abiHandler: AbiHandler = objects.newInstance(AbiHandler::class)

internal fun getFallbacks() = theVariants.get() + defaultVariants

Expand Down Expand Up @@ -91,6 +95,71 @@ open class DependencyAnalysisExtension(objects: ObjectFactory) : AbstractExtensi
fun issues(action: Action<IssueHandler>) {
action.execute(issueHandler)
}

fun abi(action: Action<AbiHandler>) {
action.execute(abiHandler)
}
}

/**
* Initial goal:
* ```
* abi {
* exclusions {
* ignoreSubPackage("internal")
* ignoreInternalPackages()
* ignoreGeneratedCode()
* excludeAnnotations(".*\\.Generated")
* excludeClasses(".*\\.internal\\..*")
* }
* }
* ```
*/
open class AbiHandler @Inject constructor(objects: ObjectFactory) {

internal val exclusionsHandler: ExclusionsHandler = objects.newInstance(ExclusionsHandler::class)

fun exclusions(action: Action<ExclusionsHandler>) {
action.execute(exclusionsHandler)
}
}

abstract class ExclusionsHandler @Inject constructor(objects: ObjectFactory) {

internal val classExclusions = objects.setProperty<String>().convention(emptySet())
internal val annotationExclusions = objects.setProperty<String>().convention(emptySet())
internal val pathExclusions = objects.setProperty<String>().convention(emptySet())

fun ignoreInternalPackages() {
ignoreSubPackage("internal")
}

fun ignoreSubPackage(packageFragment: String) {
excludeClasses("(.*\\.)?$packageFragment(\\..*)?")
}

/**
* Best-effort attempts to ignore generated code by ignoring any bytecode in classes annotated
* with an annotation ending in `Generated`. It's important to note that the standard
* `javax.annotation.Generated` (or its JDK9+ successor) does _not_ work with this due to it
* using `SOURCE` retention. It's recommended to use your own `Generated` annotation.
*/
fun ignoreGeneratedCode() {
excludeAnnotations(".*\\.Generated")
}

fun excludeClasses(@Language("RegExp") vararg classRegexes: String) {
classExclusions.addAll(*classRegexes)
}

fun excludeAnnotations(@Language("RegExp") vararg annotationRegexes: String) {
annotationExclusions.addAll(*annotationRegexes)
}

// TODO Excluded for now but left as a toe-hold for future use
// fun excludePaths(@Language("RegExp") vararg pathRegexes: String) {
// pathExclusions.addAll(*pathRegexes)
// }
}

/**
Expand Down
18 changes: 17 additions & 1 deletion src/main/kotlin/com/autonomousapps/DependencyAnalysisPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ package com.autonomousapps

import com.android.build.gradle.AppExtension
import com.android.build.gradle.LibraryExtension
import com.autonomousapps.internal.AbiExclusions
import com.autonomousapps.internal.ConfigurationsToDependenciesTransformer
import com.autonomousapps.internal.OutputPaths
import com.autonomousapps.internal.RootOutputPaths
import com.autonomousapps.internal.analyzer.*
import com.autonomousapps.internal.android.AgpVersion
import com.autonomousapps.internal.utils.log
import com.autonomousapps.internal.utils.toJson
import com.autonomousapps.services.InMemoryCache
import com.autonomousapps.tasks.*
import org.gradle.api.GradleException
Expand Down Expand Up @@ -513,8 +515,22 @@ class DependencyAnalysisPlugin : Plugin<Project> {
outputUsedTransitives.set(outputPaths.usedTransitiveDependenciesPath)
}

val lazyAbiJson = lazy {
with(getExtension().abiHandler.exclusionsHandler) {
AbiExclusions(
annotationExclusions = annotationExclusions.get(),
classExclusions = classExclusions.get(),
pathExclusions = pathExclusions.get()
).toJson()
}
}
val abiExclusions = provider(lazyAbiJson::value)

// A report of the project's binary API, or ABI.
val abiAnalysisTask = dependencyAnalyzer.registerAbiAnalysisTask(dependencyReportTask)
val abiAnalysisTask = dependencyAnalyzer.registerAbiAnalysisTask(
dependencyReportTask,
abiExclusions
)

// A report of service loaders
val serviceLoaderTask =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,11 +237,13 @@ internal class AndroidLibAnalyzer(
}

override fun registerAbiAnalysisTask(
dependencyReportTask: TaskProvider<DependencyReportTask>
dependencyReportTask: TaskProvider<DependencyReportTask>,
abiExclusions: Provider<String>
): TaskProvider<AbiAnalysisTask> =
project.tasks.register<AbiAnalysisTask>("abiAnalysis$variantNameCapitalized") {
jar.set(getBundleTaskOutput())
dependencies.set(dependencyReportTask.flatMap { it.allComponentsReport })
exclusions.set(abiExclusions)

output.set(outputPaths.abiAnalysisPath)
abiDump.set(outputPaths.abiDumpPath)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ internal interface DependencyAnalyzer<T : ClassAnalysisTask> {
* no meaningful ABI.
*/
fun registerAbiAnalysisTask(
dependencyReportTask: TaskProvider<DependencyReportTask>
dependencyReportTask: TaskProvider<DependencyReportTask>,
abiExclusions: Provider<String>
): TaskProvider<AbiAnalysisTask>? = null
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,14 @@ internal class JavaAppAnalyzer(project: Project, sourceSet: SourceSet)
internal class JavaLibAnalyzer(project: Project, sourceSet: SourceSet)
: JvmAnalyzer(project, JavaSourceSet(sourceSet)) {

override fun registerAbiAnalysisTask(dependencyReportTask: TaskProvider<DependencyReportTask>) =
override fun registerAbiAnalysisTask(
dependencyReportTask: TaskProvider<DependencyReportTask>,
abiExclusions: Provider<String>
): TaskProvider<AbiAnalysisTask>? =
project.tasks.register<AbiAnalysisTask>("abiAnalysis$variantNameCapitalized") {
jar.set(getJarTask().flatMap { it.archiveFile })
dependencies.set(dependencyReportTask.flatMap { it.allComponentsReport })
exclusions.set(abiExclusions)

output.set(outputPaths.abiAnalysisPath)
abiDump.set(outputPaths.abiDumpPath)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

package com.autonomousapps.internal.kotlin

import com.autonomousapps.internal.AbiExclusions
import com.autonomousapps.internal.asm.ClassReader
import com.autonomousapps.internal.asm.Opcodes
import com.autonomousapps.internal.asm.tree.ClassNode
Expand Down Expand Up @@ -57,8 +58,21 @@ fun getBinaryAPI(classStreams: Sequence<InputStream>, visibilityFilter: (String)
it.isEffectivelyPublic(classAccess, mVisibility)
}

ClassBinarySignature(name, superName, outerClassName, supertypes, memberSignatures, classAccess,
isEffectivelyPublic(mVisibility), metadata.isFileOrMultipartFacade() || isDefaultImpls(metadata)
val annotations = (visibleAnnotations.orEmpty() + invisibleAnnotations.orEmpty())
.map { it.desc.replace("/", ".") }

ClassBinarySignature(
name = name,
superName = superName,
outerName = outerClassName,
supertypes = supertypes,
memberSignatures = memberSignatures,
access = classAccess,
isEffectivelyPublic = isEffectivelyPublic(mVisibility),
isNotUsedWhenEmpty = metadata.isFileOrMultipartFacade() || isDefaultImpls(metadata),
annotations = annotations,
// TODO toe-hold for filtering by directory
sourceFileLocation = null
)
}
}
Expand All @@ -67,12 +81,18 @@ fun getBinaryAPI(classStreams: Sequence<InputStream>, visibilityFilter: (String)
}


fun List<ClassBinarySignature>.filterOutNonPublic(nonPublicPackages: List<String> = emptyList()): List<ClassBinarySignature> {
val nonPublicPaths = nonPublicPackages.map { it.replace('.', '/') + '/' }
internal fun List<ClassBinarySignature>.filterOutNonPublic(
exclusions: AbiExclusions = AbiExclusions.NONE
): List<ClassBinarySignature> {
val classByName = associateBy { it.name }

fun ClassBinarySignature.isInNonPublicPackage() =
nonPublicPaths.any { name.startsWith(it) }
// Library note - this function (plus the exclusions parameter above) are modified from the original
// Kotlin sources this was borrowed from.
fun ClassBinarySignature.isExcluded(): Boolean {
return (sourceFileLocation?.let(exclusions::excludesPath) ?: false) ||
exclusions.excludesClass(canonicalName) ||
annotations.any(exclusions::excludesAnnotation)
}

fun ClassBinarySignature.isPublicAndAccessible(): Boolean =
isEffectivelyPublic &&
Expand All @@ -95,7 +115,7 @@ fun List<ClassBinarySignature>.filterOutNonPublic(nonPublicPackages: List<String
return this.copy(memberSignatures = memberSignatures + inheritedStaticSignatures, supertypes = supertypes - superName)
}

return filter { !it.isInNonPublicPackage() && it.isPublicAndAccessible() }
return filter { !it.isExcluded() && it.isPublicAndAccessible() }
.map { it.flattenNonPublicBases() }
.filterNot { it.isNotUsedWhenEmpty && it.memberSignatures.isEmpty() }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.autonomousapps.internal.kotlin

import com.autonomousapps.internal.Component
import com.autonomousapps.advice.Dependency
import com.autonomousapps.internal.AbiExclusions
import com.autonomousapps.internal.utils.*
import com.autonomousapps.internal.utils.DESC_REGEX
import com.autonomousapps.internal.utils.allItems
Expand All @@ -10,10 +11,19 @@ import java.util.jar.JarFile

/**
* Given a jar and a list of its dependencies (as [Component]s), return the set of [Dependency]s that represents this
* jar's ABI (or public API). [abiDumpFile] is used only to write a rich ABI representation, and may be omitted.
* jar's ABI (or public API).
*
* [exclusions] indicate exclusion rules (generated code, etc).
*
* [abiDumpFile] is used only to write a rich ABI representation, and may be omitted.
*/
fun abiDependencies(jarFile: File, jarDependencies: List<Component>, abiDumpFile: File? = null): Set<Dependency> =
getBinaryAPI(JarFile(jarFile)).filterOutNonPublic()
internal fun abiDependencies(
jarFile: File,
jarDependencies: List<Component>,
exclusions: AbiExclusions,
abiDumpFile: File? = null
): Set<Dependency> =
getBinaryAPI(JarFile(jarFile)).filterOutNonPublic(exclusions)
.also { publicApi ->
abiDumpFile?.let { file ->
file.bufferedWriter().use { writer -> publicApi.dump(writer) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,11 @@ data class ClassBinarySignature(
val memberSignatures: List<MemberBinarySignature>,
val access: AccessFlags,
val isEffectivelyPublic: Boolean,
val isNotUsedWhenEmpty: Boolean
val isNotUsedWhenEmpty: Boolean,
val annotations: List<String>,
val sourceFileLocation: String?
) {
val canonicalName = name.replace("/", ".")
val signature: String
get() = "${access.getModifierString()} class $name" + if (supertypes.isEmpty()) "" else " : ${supertypes.joinToString()}"
}
Expand Down
26 changes: 26 additions & 0 deletions src/main/kotlin/com/autonomousapps/internal/models.kt
Original file line number Diff line number Diff line change
Expand Up @@ -421,3 +421,29 @@ internal data class ConsoleReport(
}
}
}

internal data class AbiExclusions(
val annotationExclusions: Set<String> = emptySet(),
val classExclusions: Set<String> = emptySet(),
val pathExclusions: Set<String> = emptySet()
) {

@Transient
private val annotationRegexes = annotationExclusions.mapToSet(String::toRegex)

@Transient
private val classRegexes = classExclusions.mapToSet(String::toRegex)

@Transient
private val pathRegexes = pathExclusions.mapToSet(String::toRegex)

fun excludesAnnotation(fqcn: String): Boolean = annotationRegexes.any { it.containsMatchIn(fqcn) }

fun excludesClass(fqcn: String) = classRegexes.any { it.containsMatchIn(fqcn) }

fun excludesPath(path: String) = pathRegexes.any { it.containsMatchIn(path) }

companion object {
val NONE = AbiExclusions()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ internal fun <T> Iterable<T>.filterNoneMatchingSorted(unwanted: Iterable<T>): Tr
}
}

internal inline fun <T, R> Iterable<T>.mapToSet(transform: (T) -> R): HashSet<R> {
return mapTo(HashSet(collectionSizeOrDefault(10)), transform)
internal inline fun <T, R> Iterable<T>.mapToSet(transform: (T) -> R): LinkedHashSet<R> {
return mapTo(LinkedHashSet(collectionSizeOrDefault(10)), transform)
}

internal inline fun <T, R> Iterable<T>.mapToOrderedSet(transform: (T) -> R): TreeSet<R> {
Expand Down
11 changes: 10 additions & 1 deletion src/main/kotlin/com/autonomousapps/tasks/AbiAnalysisTask.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import com.autonomousapps.TASK_GROUP_DEP
import com.autonomousapps.advice.Dependency
import com.autonomousapps.internal.*
import com.autonomousapps.internal.kotlin.abiDependencies
import com.autonomousapps.internal.utils.fromJson
import com.autonomousapps.internal.utils.fromJsonList
import com.autonomousapps.internal.utils.getLogger
import com.autonomousapps.internal.utils.toJson
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
import org.gradle.workers.WorkAction
import org.gradle.workers.WorkParameters
Expand All @@ -34,6 +36,10 @@ abstract class AbiAnalysisTask @Inject constructor(
@get:InputFile
abstract val dependencies: RegularFileProperty

@get:Input
@get:Optional
abstract val exclusions: Property<String>

@get:OutputFile
abstract val output: RegularFileProperty

Expand All @@ -45,6 +51,7 @@ abstract class AbiAnalysisTask @Inject constructor(
workerExecutor.noIsolation().submit(AbiAnalysisWorkAction::class.java) {
jar.set(this@AbiAnalysisTask.jar)
dependencies.set(this@AbiAnalysisTask.dependencies)
exclusions.set(this@AbiAnalysisTask.exclusions)
output.set(this@AbiAnalysisTask.output)
abiDump.set(this@AbiAnalysisTask.abiDump)
}
Expand All @@ -54,6 +61,7 @@ abstract class AbiAnalysisTask @Inject constructor(
interface AbiAnalysisParameters : WorkParameters {
val jar: RegularFileProperty
val dependencies: RegularFileProperty
val exclusions: Property<String>
val output: RegularFileProperty
val abiDump: RegularFileProperty
}
Expand All @@ -66,6 +74,7 @@ abstract class AbiAnalysisWorkAction : WorkAction<AbiAnalysisParameters> {
// Inputs
val jarFile = parameters.jar.get().asFile
val dependencies = parameters.dependencies.get().asFile.readText().fromJsonList<Component>()
val exclusions = parameters.exclusions.orNull?.fromJson<AbiExclusions>() ?: AbiExclusions.NONE

// Outputs
val reportFile = parameters.output.get().asFile
Expand All @@ -75,7 +84,7 @@ abstract class AbiAnalysisWorkAction : WorkAction<AbiAnalysisParameters> {
reportFile.delete()
abiDumpFile.delete()

val apiDependencies = abiDependencies(jarFile, dependencies, abiDumpFile)
val apiDependencies = abiDependencies(jarFile, dependencies, exclusions, abiDumpFile)

reportFile.writeText(apiDependencies.toJson())

Expand Down
Loading

0 comments on commit e4e1d2a

Please sign in to comment.