diff --git a/build.gradle.kts b/build.gradle.kts
index 73190a278..2451673e9 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -2,8 +2,6 @@
// SPDX-License-Identifier: Apache-2.0
@file:Suppress("UnstableApiUsage", "HasPlatformType", "PropertyName")
-import org.jetbrains.kotlin.cli.common.toBooleanLenient
-
plugins {
id("java-gradle-plugin")
id("com.gradle.plugin-publish")
@@ -182,7 +180,7 @@ fun maxParallelForks() =
val isCi = providers.environmentVariable("CI")
.getOrElse("false")
- .toBooleanLenient()!!
+ .toBoolean()
// This will slow down tests on CI, but maybe it won't run out of metaspace.
fun forkEvery(): Long = if (isCi) 40 else 0
diff --git a/src/functionalTest/groovy/com/autonomousapps/AbstractFunctionalSpec.groovy b/src/functionalTest/groovy/com/autonomousapps/AbstractFunctionalSpec.groovy
index 82f20904b..60f321507 100644
--- a/src/functionalTest/groovy/com/autonomousapps/AbstractFunctionalSpec.groovy
+++ b/src/functionalTest/groovy/com/autonomousapps/AbstractFunctionalSpec.groovy
@@ -11,13 +11,16 @@ import spock.lang.Specification
abstract class AbstractFunctionalSpec extends Specification {
+ @SuppressWarnings('unused')
+ protected static final String FLAG_LOG_BYTECODE = "-D${Flags.FLAG_BYTECODE_LOGGING}=true"
+
protected static final GRADLE_7_5 = GradleVersion.version('7.5.1')
protected static final GRADLE_7_6 = GradleVersion.version('7.6.2')
protected static final GRADLE_8_0 = GradleVersion.version('8.0.2')
protected static final GRADLE_8_4 = GradleVersion.version('8.4')
protected static final GRADLE_8_9 = GradleVersion.version('8.9')
protected static final GRADLE_8_10 = GradleVersion.version('8.10.2')
- protected static final GRADLE_8_11 = GradleVersion.version('8.11-rc-1')
+ protected static final GRADLE_8_11 = GradleVersion.version('8.11')
protected static final GRADLE_LATEST = GRADLE_8_10
@@ -26,11 +29,26 @@ abstract class AbstractFunctionalSpec extends Specification {
protected static final SUPPORTED_GRADLE_VERSIONS = [
GradleVersions.minGradleVersion,
GRADLE_LATEST,
-// GRADLE_8_11,
+ //GRADLE_8_11,
]
protected GradleProject gradleProject = null
+ /**
+ * Default environment variables on Github Actions
+ */
+ private static boolean isCi = System.getenv("CI") == "true"
+
+ def cleanup() {
+ // Delete fixtures on CI to prevent disk space growing out of bounds
+ if (gradleProject != null && isCi) {
+ try {
+ gradleProject.rootDir.deleteDir()
+ } catch (Throwable t) {
+ }
+ }
+ }
+
protected static Boolean quick() {
return System.getProperty('com.autonomousapps.quick').toBoolean()
}
diff --git a/src/functionalTest/groovy/com/autonomousapps/jvm/DuplicateClasspathSpec.groovy b/src/functionalTest/groovy/com/autonomousapps/jvm/DuplicateClasspathSpec.groovy
index c9ffe555a..c36fe2558 100644
--- a/src/functionalTest/groovy/com/autonomousapps/jvm/DuplicateClasspathSpec.groovy
+++ b/src/functionalTest/groovy/com/autonomousapps/jvm/DuplicateClasspathSpec.groovy
@@ -1,7 +1,8 @@
package com.autonomousapps.jvm
+import com.autonomousapps.jvm.projects.BinaryIncompatibilityProject
import com.autonomousapps.jvm.projects.DuplicateClasspathProject
-import org.gradle.testkit.runner.TaskOutcome
+import com.autonomousapps.utils.Colors
import static com.autonomousapps.advice.truth.BuildHealthSubject.buildHealth
import static com.autonomousapps.utils.Runner.build
@@ -17,13 +18,19 @@ final class DuplicateClasspathSpec extends AbstractJvmSpec {
gradleProject = project.gradleProject
when:
- // This first invocation "fixes" the dependency declarations
+ // This first invocation fixes the dependency declarations
build(gradleVersion, gradleProject.rootDir, ':consumer:fixDependencies')
- // This second invocation fails because the fix (+ sort) causes the JVM to load the wrong class during compilation.
- def result = buildAndFail(gradleVersion, gradleProject.rootDir, 'buildHealth')
+ // This used to fail because of classpath duplication and the wrong dep getting loaded first. Recent improvements
+ // have improved this case, however.
+ build(gradleVersion, gradleProject.rootDir, 'buildHealth')
then:
- assertThat(result.task(':consumer:compileJava').outcome).isEqualTo(TaskOutcome.FAILED)
+ assertThat(gradleProject.rootDir.toPath().resolve('consumer/build.gradle').text).contains(
+ '''\
+ dependencies {
+ implementation project(':producer-2')
+ }'''.stripIndent()
+ )
where:
gradleVersion << [GRADLE_LATEST]
@@ -35,7 +42,7 @@ final class DuplicateClasspathSpec extends AbstractJvmSpec {
gradleProject = project.gradleProject
when:
- def result = build(gradleVersion, gradleProject.rootDir, 'buildHealth')
+ def result = buildAndFail(gradleVersion, gradleProject.rootDir, 'buildHealth')
then:
assertThat(result.output).contains(
@@ -45,11 +52,11 @@ final class DuplicateClasspathSpec extends AbstractJvmSpec {
Source set: main
\\--- compile classpath
- +--- com/example/producer/Producer$Inner.class is provided by multiple dependencies: [:producer-1, :producer-2]
- \\--- com/example/producer/Producer.class is provided by multiple dependencies: [:producer-1, :producer-2]
+ +--- com/example/producer/Producer is provided by multiple dependencies: [:producer-1, :producer-2]
+ \\--- com/example/producer/Producer$Inner is provided by multiple dependencies: [:producer-1, :producer-2]
\\--- runtime classpath
- +--- com/example/producer/Producer$Inner.class is provided by multiple dependencies: [:producer-1, :producer-2]
- \\--- com/example/producer/Producer.class is provided by multiple dependencies: [:producer-1, :producer-2]'''
+ +--- com/example/producer/Producer is provided by multiple dependencies: [:producer-1, :producer-2]
+ \\--- com/example/producer/Producer$Inner is provided by multiple dependencies: [:producer-1, :producer-2]'''
.stripIndent()
)
@@ -61,4 +68,108 @@ final class DuplicateClasspathSpec extends AbstractJvmSpec {
where:
gradleVersion << [GRADLE_LATEST]
}
+
+ def "can report on which of the duplicates is needed for binary compatibility (#gradleVersion)"() {
+ given:
+ def project = new BinaryIncompatibilityProject()
+ gradleProject = project.gradleProject
+
+ when:
+ def result = build(
+ gradleVersion, gradleProject.rootDir,
+ ':consumer:reason', '--id', ':producer-1',
+ //FLAG_LOG_BYTECODE,
+ )
+
+ then:
+ assertThat(Colors.decolorize(result.output)).contains(
+ '''\
+ ------------------------------------------------------------
+ You asked about the dependency ':producer-1'.
+ There is no advice regarding this dependency.
+ ------------------------------------------------------------
+
+ Shortest path from :consumer to :producer-1 for compileClasspath:
+ :consumer
+ \\--- :unused
+ \\--- :producer-1
+
+ Shortest path from :consumer to :producer-1 for runtimeClasspath:
+ :consumer
+ \\--- :unused
+ \\--- :producer-1
+
+ Shortest path from :consumer to :producer-1 for testCompileClasspath:
+ :consumer
+ \\--- :unused
+ \\--- :producer-1
+
+ Shortest path from :consumer to :producer-1 for testRuntimeClasspath:
+ :consumer
+ \\--- :unused
+ \\--- :producer-1
+
+ Source: main
+ ------------
+ * Is binary-incompatible, and should be removed from the classpath:
+ Expected METHOD com/example/producer/Person.(Ljava/lang/String;Ljava/lang/String;)V, but was com/example/producer/Person.(Ljava/lang/String;)V
+
+ Source: test
+ ------------
+ (no usages)'''.stripIndent()
+ )
+
+ where:
+ gradleVersion << [GRADLE_LATEST]
+ }
+
+ def "suggests removing a binary-incompatible duplicate (#gradleVersion)"() {
+ given:
+ def project = new BinaryIncompatibilityProject(true)
+ gradleProject = project.gradleProject
+
+ when:
+ def result = build(
+ gradleVersion, gradleProject.rootDir,
+ ':consumer:reason', '--id', ':producer-1',
+ //FLAG_LOG_BYTECODE,
+ )
+
+ then:
+ assertThat(Colors.decolorize(result.output)).contains(
+ '''\
+ ------------------------------------------------------------
+ You asked about the dependency ':producer-1'.
+ You have been advised to remove this dependency from 'implementation'.
+ ------------------------------------------------------------
+
+ Shortest path from :consumer to :producer-1 for compileClasspath:
+ :consumer
+ \\--- :producer-1
+
+ Shortest path from :consumer to :producer-1 for runtimeClasspath:
+ :consumer
+ \\--- :producer-1
+
+ Shortest path from :consumer to :producer-1 for testCompileClasspath:
+ :consumer
+ \\--- :producer-1
+
+ Shortest path from :consumer to :producer-1 for testRuntimeClasspath:
+ :consumer
+ \\--- :producer-1
+
+ Source: main
+ ------------
+ * Is binary-incompatible, and should be removed from the classpath:
+ Expected METHOD com/example/producer/Person.(Ljava/lang/String;Ljava/lang/String;)V, but was com/example/producer/Person.(Ljava/lang/String;)V
+
+ Source: test
+ ------------
+ (no usages)'''.stripIndent()
+ )
+
+ where:
+ gradleVersion << [GRADLE_LATEST]
+ }
}
diff --git a/src/functionalTest/groovy/com/autonomousapps/jvm/projects/BinaryIncompatibilityProject.groovy b/src/functionalTest/groovy/com/autonomousapps/jvm/projects/BinaryIncompatibilityProject.groovy
new file mode 100644
index 000000000..af534b300
--- /dev/null
+++ b/src/functionalTest/groovy/com/autonomousapps/jvm/projects/BinaryIncompatibilityProject.groovy
@@ -0,0 +1,196 @@
+package com.autonomousapps.jvm.projects
+
+import com.autonomousapps.AbstractProject
+import com.autonomousapps.kit.GradleProject
+import com.autonomousapps.kit.Source
+
+import static com.autonomousapps.kit.gradle.Dependency.project
+
+final class BinaryIncompatibilityProject extends AbstractProject {
+
+ final GradleProject gradleProject
+
+ BinaryIncompatibilityProject(boolean extra = false) {
+ this.gradleProject = build(extra)
+ }
+
+ private GradleProject build(boolean extra) {
+ return newGradleProjectBuilder()
+ // :consumer uses the Producer class.
+ // This class is provided by both
+ // 1. :producer-2, which is a direct dependency, and
+ // 2. :producer-1, which is a transitive dependency (of :unused)
+ // These classes have incompatible definitions. :consumer _requires_ the version provided by :producer-2.
+ .withSubproject('consumer') { s ->
+ s.sources = sourcesConsumer
+ s.withBuildScript { bs ->
+ bs.plugins = javaLibrary + plugins.gradleDependenciesSorter
+ bs.dependencies(
+ project('implementation', ':unused'),
+ project('implementation', ':producer-2'),
+ )
+
+ // If this is a direct dep, then we should suggest removing it due to binary-incompatibility
+ if (extra) {
+ bs.dependencies += project('implementation', ':producer-1')
+ }
+ }
+ }
+ // Used
+ .withSubproject('producer-1') { s ->
+ s.sources = sourcesProducer1
+ s.withBuildScript { bs ->
+ bs.plugins = javaLibrary
+ }
+ }
+ // Unused, except its transitive is
+ .withSubproject('unused') { s ->
+ s.sources = sourcesUnused
+ s.withBuildScript { bs ->
+ bs.plugins = javaLibrary
+ bs.dependencies(
+ project('api', ':producer-1'),
+ )
+ }
+ }
+ // Used?
+ .withSubproject('producer-2') { s ->
+ s.sources = sourcesProducer2
+ s.withBuildScript { bs ->
+ bs.plugins = javaLibrary
+ }
+ }
+ .write()
+ }
+
+ private static final List sourcesConsumer = [
+ Source.java(
+ '''\
+ package com.example.consumer;
+
+ import com.example.producer.Person;
+
+ public class Consumer {
+ private Person person = new Person("Emma", "Goldman");
+ private String usesField = person.firstName;
+ private String usesMethod = person.getLastName();
+
+ private void usePerson() {
+ String m = Person.MAGIC;
+ System.out.println(person.firstName);
+ System.out.println(person.lastName);
+ System.out.println(person.getFirstName());
+ System.out.println(person.getLastName());
+
+ // Person::ugh takes and returns Object, here we pass Void and print a String.
+ System.out.println(person.ugh(null));
+ }
+ }
+ '''
+ )
+ .withPath('com.example.consumer', 'Consumer')
+ .build(),
+ ]
+
+ private static final List sourcesProducer1 = [
+ Source.java(
+ '''\
+ package com.example.producer;
+
+ public class Person {
+
+ private final String name;
+
+ public Person(String name) {
+ this.name = name;
+ }
+ }
+ '''
+ )
+ .withPath('com.example.producer', 'Person')
+ .build(),
+ ]
+
+ // Same class file as sourcesProducer1, incompatible definition
+ private static final List sourcesProducer2 = [
+ Source.java(
+ '''\
+ package com.example.producer;
+
+ import java.util.ArrayList;
+ import java.util.List;
+
+ /**
+ * This class contains fields with different visibilities so we can exercise our bytecode analysis thoroughly.
+ */
+ public class Person {
+
+ public static final String MAGIC = "42";
+
+ public final String firstName;
+ public final String lastName;
+
+ protected int nameLength;
+
+ String[] names = new String[2];
+
+ private final boolean unused = false;
+
+ public Person(String firstName, String lastName) {
+ this.firstName = firstName;
+ this.lastName = lastName;
+ this.nameLength = firstName.length() + lastName.length();
+
+ this.names[0] = firstName;
+ this.names[1] = lastName;
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ protected int getNameLength() {
+ return nameLength;
+ }
+
+ List getNames() {
+ List list = new ArrayList<>();
+ list.add(firstName);
+ list.add(lastName);
+ return list;
+ }
+
+ public Object ugh(Object anything) {
+ return firstName;
+ }
+
+ private void notImplemented() {
+ throw new RuntimeException("ohnoes");
+ }
+ }
+ '''
+ )
+ .withPath('com.example.producer', 'Person')
+ .build(),
+ ]
+
+ private static final List sourcesUnused = [
+ Source.java(
+ '''\
+ package com.example.unused;
+
+ import com.example.producer.Person;
+
+ public class Unused {
+ public Person producer;
+ }
+ '''
+ )
+ .withPath('com.example.unused', 'Unused')
+ .build(),
+ ]
+}
diff --git a/src/functionalTest/groovy/com/autonomousapps/jvm/projects/DuplicateClasspathProject.groovy b/src/functionalTest/groovy/com/autonomousapps/jvm/projects/DuplicateClasspathProject.groovy
index 5ad5377e4..c77615c33 100644
--- a/src/functionalTest/groovy/com/autonomousapps/jvm/projects/DuplicateClasspathProject.groovy
+++ b/src/functionalTest/groovy/com/autonomousapps/jvm/projects/DuplicateClasspathProject.groovy
@@ -19,6 +19,22 @@ final class DuplicateClasspathProject extends AbstractProject {
private GradleProject build() {
return newGradleProjectBuilder()
+ .withRootProject { r ->
+ r.withBuildScript { bs ->
+ bs.withGroovy(
+ '''\
+ dependencyAnalysis {
+ issues {
+ all {
+ onAny {
+ severity 'fail'
+ }
+ }
+ }
+ }'''.stripIndent()
+ )
+ }
+ }
// :consumer uses the Producer class.
// This class is provided by both
// 1. :producer-2, which is a direct dependency, and
@@ -126,6 +142,11 @@ final class DuplicateClasspathProject extends AbstractProject {
}
public static class Inner {}
+
+ // This is here only for manually inspecting asm.kt log output
+ public class NonStatic {
+ String name = Producer.this.firstName;
+ }
}
'''
)
@@ -155,11 +176,10 @@ final class DuplicateClasspathProject extends AbstractProject {
private final Set consumerAdvice = [
Advice.ofRemove(projectCoordinates(':unused'), 'implementation'),
- Advice.ofAdd(projectCoordinates(':producer-1'), 'implementation')
]
final Set expectedProjectAdvice = [
- projectAdviceForDependencies(':consumer', consumerAdvice),
+ projectAdviceForDependencies(':consumer', consumerAdvice, true),
emptyProjectAdviceFor(':unused'),
emptyProjectAdviceFor(':producer-1'),
emptyProjectAdviceFor(':producer-2'),
diff --git a/src/main/kotlin/com/autonomousapps/Flags.kt b/src/main/kotlin/com/autonomousapps/Flags.kt
index 5ef2ec25b..55502f9a2 100644
--- a/src/main/kotlin/com/autonomousapps/Flags.kt
+++ b/src/main/kotlin/com/autonomousapps/Flags.kt
@@ -18,6 +18,9 @@ object Flags {
private const val FLAG_PRINT_BUILD_HEALTH = "dependency.analysis.print.build.health"
private const val FLAG_PROJECT_INCLUDES = "dependency.analysis.project.includes"
+ // Used in tests
+ internal const val FLAG_BYTECODE_LOGGING = "dependency.analysis.bytecode.logging"
+
/**
* Android build variant to not analyze i.e.
*
@@ -52,6 +55,18 @@ object Flags {
.getOrElse(default)
}
+ /**
+ * 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).
+ *
+ * This is called from the runtime (not build time), so we use [System.getProperty] instead of
+ * [project.providers.systemProperty][org.gradle.api.provider.ProviderFactory.systemProperty].
+ */
+ internal fun logBytecodeDebug(): Boolean {
+ return !System.getProperty(FLAG_BYTECODE_LOGGING, "false").toBoolean()
+ }
+
internal fun Project.compatibility(): Compatibility {
return getGradlePropForConfiguration(FLAG_DISABLE_COMPATIBILITY, Compatibility.WARN.name).let {
@Suppress("DEPRECATION") val value = it.toUpperCase(Locale.US)
diff --git a/src/main/kotlin/com/autonomousapps/internal/BytecodeParsers.kt b/src/main/kotlin/com/autonomousapps/internal/BytecodeParsers.kt
index cdc07cb25..af9dbd308 100644
--- a/src/main/kotlin/com/autonomousapps/internal/BytecodeParsers.kt
+++ b/src/main/kotlin/com/autonomousapps/internal/BytecodeParsers.kt
@@ -7,7 +7,8 @@ import com.autonomousapps.internal.asm.ClassReader
import com.autonomousapps.internal.utils.JAVA_FQCN_REGEX_SLASHY
import com.autonomousapps.internal.utils.asSequenceOfClassFiles
import com.autonomousapps.internal.utils.getLogger
-import com.autonomousapps.model.intermediates.ExplodingBytecode
+import com.autonomousapps.model.intermediates.consumer.ExplodingBytecode
+import com.autonomousapps.model.intermediates.consumer.MemberAccess
import org.gradle.api.logging.Logger
import java.io.File
@@ -49,6 +50,7 @@ internal class ClassFilesParser(
nonAnnotationClasses = explodedClass.nonAnnotationClasses,
annotationClasses = explodedClass.annotationClasses,
invisibleAnnotationClasses = explodedClass.invisibleAnnotationClasses,
+ binaryClassAccesses = explodedClass.binaryClasses,
)
}
.toSet()
@@ -102,10 +104,12 @@ private class BytecodeReader(
className = canonicalize(classAnalyzer.className),
nonAnnotationClasses = constantPool.asSequence().plus(usedNonAnnotationClasses).fixup(classAnalyzer),
annotationClasses = usedVisibleAnnotationClasses.asSequence().fixup(classAnalyzer),
- invisibleAnnotationClasses = usedInvisibleAnnotationClasses.asSequence().fixup(classAnalyzer)
+ invisibleAnnotationClasses = usedInvisibleAnnotationClasses.asSequence().fixup(classAnalyzer),
+ binaryClasses = classAnalyzer.getBinaryClasses().fixup(classAnalyzer),
)
}
+ // Change this in concert with the Map.fixup() function below
private fun Sequence.fixup(classAnalyzer: ClassAnalyzer): Set {
return this
// Filter out `java` packages, but not `javax`
@@ -116,6 +120,16 @@ private class BytecodeReader(
.map { canonicalize(it) }
.toSortedSet()
}
+
+ // TODO(tsr): decide whether to dottify (canonicalize) the class names or leave them slashy
+ // Change this in concert with the Sequence.fixup() function above
+ private fun Map>.fixup(classAnalyzer: ClassAnalyzer): Map> {
+ return this
+ // Filter out `java` packages, but not `javax`
+ .filterKeys { !it.startsWith("java/") }
+ // Filter out a "used class" that is exactly the class under analysis
+ .filterKeys { it != classAnalyzer.className }
+ }
}
private class ExplodedClass(
@@ -124,4 +138,5 @@ private class ExplodedClass(
val nonAnnotationClasses: Set,
val annotationClasses: Set,
val invisibleAnnotationClasses: Set,
+ val binaryClasses: Map>,
)
diff --git a/src/main/kotlin/com/autonomousapps/internal/JarExploder.kt b/src/main/kotlin/com/autonomousapps/internal/JarExploder.kt
index 4dafd5397..918feecbf 100644
--- a/src/main/kotlin/com/autonomousapps/internal/JarExploder.kt
+++ b/src/main/kotlin/com/autonomousapps/internal/JarExploder.kt
@@ -9,7 +9,7 @@ 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.producer.ExplodedJar
import com.autonomousapps.model.intermediates.ExplodingJar
import com.autonomousapps.services.InMemoryCache
import com.autonomousapps.tasks.ExplodeJarTask
diff --git a/src/main/kotlin/com/autonomousapps/internal/OutputPaths.kt b/src/main/kotlin/com/autonomousapps/internal/OutputPaths.kt
index 5bcf5bc96..6bb048146 100644
--- a/src/main/kotlin/com/autonomousapps/internal/OutputPaths.kt
+++ b/src/main/kotlin/com/autonomousapps/internal/OutputPaths.kt
@@ -27,7 +27,7 @@ internal class OutputPaths(
val externalDependenciesPath = file("${intermediatesDir}/external-dependencies.txt")
val duplicateCompileClasspathPath = file("${intermediatesDir}/duplicate-classes-compile.json")
val duplicateCompileRuntimePath = file("${intermediatesDir}/duplicate-classes-runtime.json")
- val allDeclaredDepsPath = file("${intermediatesDir}/exploded-jars.json")
+ val explodedJarsPath = file("${intermediatesDir}/exploded-jars.json")
val inlineUsagePath = file("${intermediatesDir}/inline-usage.json")
val typealiasUsagePath = file("${intermediatesDir}/typealias-usage.json")
val inlineUsageErrorsPath = file("${intermediatesDir}/inline-usage-errors.txt")
diff --git a/src/main/kotlin/com/autonomousapps/internal/advice/ProjectHealthConsoleReportBuilder.kt b/src/main/kotlin/com/autonomousapps/internal/advice/ProjectHealthConsoleReportBuilder.kt
index 054c86f7d..d599e482b 100644
--- a/src/main/kotlin/com/autonomousapps/internal/advice/ProjectHealthConsoleReportBuilder.kt
+++ b/src/main/kotlin/com/autonomousapps/internal/advice/ProjectHealthConsoleReportBuilder.kt
@@ -182,7 +182,7 @@ internal class ProjectHealthConsoleReportBuilder(
append(" \\--- ")
}
- appendReproducibleNewLine("${d.classReference} is provided by multiple dependencies: $deps")
+ appendReproducibleNewLine("${d.className} is provided by multiple dependencies: $deps")
}
}
}
diff --git a/src/main/kotlin/com/autonomousapps/internal/asm.kt b/src/main/kotlin/com/autonomousapps/internal/asm.kt
index 27a7e8f14..aa7167c5c 100644
--- a/src/main/kotlin/com/autonomousapps/internal/asm.kt
+++ b/src/main/kotlin/com/autonomousapps/internal/asm.kt
@@ -2,27 +2,23 @@
// SPDX-License-Identifier: Apache-2.0
package com.autonomousapps.internal
+import com.autonomousapps.Flags
import com.autonomousapps.internal.ClassNames.canonicalize
import com.autonomousapps.internal.asm.*
+import com.autonomousapps.internal.kotlin.AccessFlags
import com.autonomousapps.internal.utils.METHOD_DESCRIPTOR_REGEX
import com.autonomousapps.internal.utils.efficient
import com.autonomousapps.internal.utils.genericTypes
+import com.autonomousapps.model.intermediates.producer.Member
+import com.autonomousapps.model.intermediates.consumer.MemberAccess
import kotlinx.metadata.jvm.Metadata
import org.gradle.api.logging.Logger
+import java.util.SortedSet
import java.util.concurrent.atomic.AtomicReference
-private val logDebug: Boolean get() = isLogDebug()
+private val logDebug: Boolean get() = Flags.logBytecodeDebug()
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) {
@@ -30,10 +26,13 @@ internal class ClassNameAndAnnotationsVisitor(private val logger: Logger) : Clas
private lateinit var access: Access
private var outerClassName: String? = null
private var superClassName: String? = null
+ private var interfaces: Set? = null
private val retentionPolicyHolder = AtomicReference("")
private var isAnnotation = false
private val methods = mutableSetOf()
private val innerClasses = mutableSetOf()
+ private val effectivelyPublicFields = mutableSetOf()
+ private val effectivelyPublicMethods = mutableSetOf()
private var methodCount = 0
private var fieldCount = 0
@@ -49,14 +48,17 @@ internal class ClassNameAndAnnotationsVisitor(private val logger: Logger) : Clas
return AnalyzedClass(
className = className,
outerClassName = outerClassName,
- superClassName = superClassName,
+ superClassName = superClassName!!,
+ interfaces = interfaces.orEmpty(),
retentionPolicy = retentionPolicyHolder.get(),
isAnnotation = isAnnotation,
hasNoMembers = hasNoMembers,
access = access,
methods = methods.efficient(),
innerClasses = innerClasses.efficient(),
- constantClasses = constantClasses
+ constantClasses = constantClasses.efficient(),
+ effectivelyPublicFields = effectivelyPublicFields,
+ effectivelyPublicMethods = effectivelyPublicMethods,
)
}
@@ -69,7 +71,8 @@ internal class ClassNameAndAnnotationsVisitor(private val logger: Logger) : Clas
interfaces: Array?
) {
// This _must_ not be canonicalized, unless we also change accesses to be dotty instead of slashy
- superClassName = superName
+ this.superClassName = superName
+ this.interfaces = interfaces?.toSortedSet().orEmpty()
className = canonicalize(name)
if (interfaces?.contains("java/lang/annotation/Annotation") == true) {
@@ -82,33 +85,45 @@ internal class ClassNameAndAnnotationsVisitor(private val logger: Logger) : Clas
} else {
" implements ${interfaces.joinToString(", ")}"
}
- log("ClassNameAndAnnotationsVisitor#visit: ${this.access} $name extends $superName$implementsClause")
+ 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")
+ 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?
+ access: Int, name: String, descriptor: String, signature: String?, exceptions: Array?
): MethodVisitor? {
- log("- visitMethod: descriptor=$descriptor name=$name signature=$signature")
+ log { "- visitMethod: ${Access.fromInt(access)} descriptor=$descriptor name=$name signature=$signature" }
+
if (!("()V" == descriptor && ("" == name || "" == name))) {
// ignore constructors and static initializers
methodCount++
methods.add(Method(descriptor))
}
+
+ if (isEffectivelyPublic(access)) {
+ effectivelyPublicMethods.add(
+ Member.Method(
+ access = access,
+ name = name,
+ descriptor = descriptor,
+ )
+ )
+ }
+
return null
}
override fun visitField(
- access: Int, name: String, descriptor: String?, signature: String?, value: Any?
+ access: Int, name: String, descriptor: String, signature: String?, value: Any?
): FieldVisitor? {
- log("- visitField: descriptor=$descriptor name=$name signature=$signature value=$value")
+ log { "- visitField: ${Access.fromInt(access)} descriptor=$descriptor name=$name signature=$signature value=$value" }
fieldCount++
// from old ConstantVisitor
@@ -116,11 +131,25 @@ internal class ClassNameAndAnnotationsVisitor(private val logger: Logger) : Clas
constantClasses.add(name)
}
+ if (isEffectivelyPublic(access)) {
+ effectivelyPublicFields.add(
+ Member.Field(
+ access = access,
+ name = name,
+ descriptor = descriptor,
+ )
+ )
+ }
+
return null
}
+ override fun visitOuterClass(owner: String?, name: String?, descriptor: String?) {
+ log { "- visitOuterClass: owner=$owner name=$name descriptor=$descriptor" }
+ }
+
override fun visitInnerClass(name: String, outerName: String?, innerName: String?, access: Int) {
- log("- visitInnerClass: name=$name outerName=$outerName innerName=$innerName")
+ log { "- visitInnerClass: ${Access.fromInt(access)} name=$name outerName=$outerName innerName=$innerName" }
if (outerName != null) {
outerClassName = canonicalize(outerName)
}
@@ -128,15 +157,17 @@ internal class ClassNameAndAnnotationsVisitor(private val logger: Logger) : Clas
}
override fun visitSource(source: String?, debug: String?) {
- log("- visitSource: source=$source debug=$debug")
+ log { "- visitSource: source=$source debug=$debug" }
}
override fun visitEnd() {
- log("- visitEnd: fieldCount=$fieldCount methodCount=$methodCount")
+ log { "- visitEnd: fieldCount=$fieldCount methodCount=$methodCount" }
}
- private fun log(msg: String) {
- logger.debug(msg)
+ private fun log(msgProvider: () -> String) {
+ if (!logDebug) {
+ logger.quiet(msgProvider())
+ }
}
private class RetentionPolicyAnnotationVisitor(
@@ -145,13 +176,15 @@ internal class ClassNameAndAnnotationsVisitor(private val logger: Logger) : Clas
private val retentionPolicyHolder: AtomicReference
) : AnnotationVisitor(ASM_VERSION) {
- private fun log(msg: String) {
- logger.debug(msg)
+ private fun log(msgProvider: () -> String) {
+ if (!logDebug) {
+ logger.quiet(msgProvider())
+ }
}
override fun visitEnum(name: String?, descriptor: String?, value: String?) {
if ("Ljava/lang/annotation/RetentionPolicy;" == descriptor) {
- log(" - RetentionPolicyAnnotationVisitor#visitEnum ($className): $value")
+ log { " - RetentionPolicyAnnotationVisitor#visitEnum ($className): $value" }
retentionPolicyHolder.set(value)
}
}
@@ -189,24 +222,25 @@ internal class ClassAnalyzer(private val logger: Logger) : ClassVisitor(ASM_VERS
var source: String? = null
lateinit var className: String
val classes = mutableSetOf()
+ private val binaryClasses = mutableMapOf>()
- private val methodAnalyzer = MethodAnalyzer(logger, classes)
+ private val methodAnalyzer = MethodAnalyzer(logger, classes, binaryClasses)
private val fieldAnalyzer = FieldAnalyzer(logger, classes)
+ fun getBinaryClasses(): Map> = binaryClasses
+
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)
+ private fun log(msgProvider: () -> String) {
+ if (!logDebug) {
+ logger.quiet(msgProvider())
}
}
override fun visitSource(source: String?, debug: String?) {
- log("- visitSource: source=$source debug=$debug")
+ log { "- visitSource: source=$source debug=$debug" }
this.source = source
}
@@ -215,11 +249,12 @@ internal class ClassAnalyzer(private val logger: Logger) : ClassVisitor(ASM_VERS
access: Int,
name: String,
signature: String?,
- superName: String?,
+ superName: String,
interfaces: Array?
) {
- log("ClassAnalyzer#visit: $name extends $superName")
+ log { "ClassAnalyzer#visit: ${Access.fromInt(access)} $name extends $superName" }
className = name
+
addClass("L$superName;", ClassRef.Kind.NOT_ANNOTATION)
interfaces?.forEach { i ->
addClass("L$i;", ClassRef.Kind.NOT_ANNOTATION)
@@ -228,40 +263,40 @@ internal class ClassAnalyzer(private val logger: Logger) : ClassVisitor(ASM_VERS
override fun visitField(
access: Int,
- name: String?,
- descriptor: String?,
+ name: String,
+ descriptor: String,
signature: String?,
value: Any?
): FieldVisitor {
- log("ClassAnalyzer#visitField: $descriptor $name")
+ log { "ClassAnalyzer#visitField: ${Access.fromInt(access)} $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?,
+ name: String,
+ descriptor: String,
signature: String?,
exceptions: Array?
): MethodVisitor {
- log("ClassAnalyzer#visitMethod: $name $descriptor")
+ log { "ClassAnalyzer#visitMethod: ${Access.fromInt(access)} $name $descriptor" }
- descriptor?.let {
- METHOD_DESCRIPTOR_REGEX.findAll(it).forEach { result ->
- addClass(result.value, ClassRef.Kind.NOT_ANNOTATION)
- }
+ METHOD_DESCRIPTOR_REGEX.findAll(descriptor).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")
+ log { "ClassAnalyzer#visitAnnotation: descriptor=$descriptor visible=$visible" }
addClass(descriptor, ClassRef.Kind.annotation(visible))
return AnnotationAnalyzer(visible, logger, classes)
}
@@ -272,59 +307,77 @@ internal class ClassAnalyzer(private val logger: Logger) : ClassVisitor(ASM_VERS
descriptor: String?,
visible: Boolean
): AnnotationVisitor {
- log("ClassAnalyzer#visitTypeAnnotation: typeRef=$typeRef typePath=$typePath descriptor=$descriptor visible=$visible")
+ 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")
+ log { "\n" }
}
}
private class MethodAnalyzer(
private val logger: Logger,
- private val classes: MutableSet
+ private val classes: MutableSet,
+ private val binaryClasses: MutableMap>,
) : 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)
+ private fun log(msgProvider: () -> String) {
+ if (!logDebug) {
+ logger.quiet(msgProvider())
}
}
override fun visitTypeInsn(opcode: Int, type: String?) {
- log("- MethodAnalyzer#visitTypeInsn: $type")
+ 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")
+ override fun visitFieldInsn(opcode: Int, owner: String, name: String, descriptor: String) {
+ log { "- MethodAnalyzer#visitFieldInsn: $owner.$name $descriptor" }
+
+ val field = MemberAccess.Field(
+ owner = owner,
+ name = name,
+ descriptor = descriptor,
+ )
+ binaryClasses.merge(owner, sortedSetOf(field)) { acc, inc ->
+ acc.apply { addAll(inc) }
+ }
+
addClass("L$owner;", ClassRef.Kind.NOT_ANNOTATION)
addClass(descriptor, ClassRef.Kind.NOT_ANNOTATION)
}
override fun visitMethodInsn(
opcode: Int,
- owner: String?,
- name: String?,
- descriptor: String?,
+ owner: String,
+ name: String,
+ descriptor: String,
isInterface: Boolean
) {
- log("- MethodAnalyzer#visitMethodInsn: $owner.$name $descriptor")
+ 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)
- }
+ addClass(if (owner.startsWith("[")) owner else "L$owner;", ClassRef.Kind.NOT_ANNOTATION)
+ METHOD_DESCRIPTOR_REGEX.findAll(descriptor).forEach { result ->
+ addClass(result.value, ClassRef.Kind.NOT_ANNOTATION)
+ }
+
+ val method = MemberAccess.Method(
+ owner = owner,
+ name = name,
+ descriptor = descriptor,
+ )
+ binaryClasses.merge(owner, sortedSetOf(method)) { acc, inc ->
+ acc.apply { addAll(inc) }
}
}
@@ -334,7 +387,7 @@ private class MethodAnalyzer(
bootstrapMethodHandle: Handle?,
vararg bootstrapMethodArguments: Any?
) {
- log("- MethodAnalyzer#visitInvokeDynamicInsn: $name $descriptor")
+ log { "- MethodAnalyzer#visitInvokeDynamicInsn: $name $descriptor" }
addClass(descriptor, ClassRef.Kind.NOT_ANNOTATION)
}
@@ -346,7 +399,7 @@ private class MethodAnalyzer(
end: Label?,
index: Int
) {
- log("- MethodAnalyzer#visitLocalVariable: $name $descriptor")
+ log { "- MethodAnalyzer#visitLocalVariable: $name $descriptor" }
// TODO probably do this for other `visitX` methods as well
signature?.genericTypes()?.forEach {
addClass(it, ClassRef.Kind.NOT_ANNOTATION)
@@ -363,13 +416,13 @@ private class MethodAnalyzer(
descriptor: String?,
visible: Boolean
): AnnotationVisitor {
- log("- MethodAnalyzer#visitLocalVariableAnnotation: $descriptor")
+ 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")
+ log { "- MethodAnalyzer#visitAnnotation: $descriptor" }
addClass(descriptor, ClassRef.Kind.annotation(visible))
return AnnotationAnalyzer(visible, logger, classes)
}
@@ -380,13 +433,13 @@ private class MethodAnalyzer(
descriptor: String?,
visible: Boolean
): AnnotationVisitor {
- log("- MethodAnalyzer#visitInsnAnnotation: $descriptor")
+ 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")
+ log { "- MethodAnalyzer#visitParameterAnnotation: $descriptor" }
addClass(descriptor, ClassRef.Kind.ANNOTATION_VISIBLE)
return AnnotationAnalyzer(visible, logger, classes)
}
@@ -397,13 +450,13 @@ private class MethodAnalyzer(
descriptor: String?,
visible: Boolean
): AnnotationVisitor {
- log("- MethodAnalyzer#visitTypeAnnotation: $descriptor")
+ 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")
+ log { "- MethodAnalyzer#visitTryCatchBlock: $type" }
addClass("L$type;", ClassRef.Kind.NOT_ANNOTATION)
}
@@ -413,7 +466,7 @@ private class MethodAnalyzer(
descriptor: String?,
visible: Boolean
): AnnotationVisitor {
- log("- MethodAnalyzer#visitTryCatchAnnotation: $descriptor")
+ log { "- MethodAnalyzer#visitTryCatchAnnotation: $descriptor" }
addClass(descriptor, ClassRef.Kind.annotation(visible))
return AnnotationAnalyzer(visible, logger, classes)
}
@@ -444,11 +497,9 @@ private class AnnotationAnalyzer(
}
}
- private fun log(msg: String) {
- if (logDebug) {
- logger.debug(msg)
- } else {
- logger.warn(msg)
+ private fun log(msgProvider: () -> String) {
+ if (!logDebug) {
+ logger.quiet(msgProvider())
}
}
@@ -456,7 +507,7 @@ private class AnnotationAnalyzer(
override fun visit(name: String?, value: Any?) {
val valueString = stringValueOfArrayElement(value)
- log("${indent()}- AnnotationAnalyzer#visit: name=$name, value=(${value?.javaClass?.simpleName}, ${valueString})")
+ log { "${indent()}- AnnotationAnalyzer#visit: name=$name, value=(${value?.javaClass?.simpleName}, ${valueString})" }
if (arrayName != null) {
arraySize++
@@ -475,18 +526,18 @@ private class AnnotationAnalyzer(
}
override fun visitEnum(name: String?, descriptor: String?, value: String?) {
- log("${indent()}- AnnotationAnalyzer#visitEnum: name=$name, descriptor=$descriptor, value=$value")
+ 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")
+ 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")
+ log { "${indent()}- AnnotationAnalyzer#visitArray: name=$name" }
return AnnotationAnalyzer(if (name == "d2") false else visible, logger, classes, level + 1, name)
}
@@ -505,14 +556,12 @@ private class AnnotationAnalyzer(
private class FieldAnalyzer(
private val logger: Logger,
- private val classes: MutableSet
+ private val classes: MutableSet,
) : FieldVisitor(ASM_VERSION) {
- private fun log(msg: String) {
- if (logDebug) {
- logger.debug(msg)
- } else {
- logger.warn(msg)
+ private fun log(msgProvider: () -> String) {
+ if (!logDebug) {
+ logger.quiet(msgProvider())
}
}
@@ -521,7 +570,7 @@ private class FieldAnalyzer(
}
override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor {
- log("- FieldAnalyzer#visitAnnotation: $descriptor")
+ log { "- FieldAnalyzer#visitAnnotation: $descriptor" }
addClass(descriptor, ClassRef.Kind.annotation(visible))
return AnnotationAnalyzer(visible, logger, classes)
}
@@ -576,11 +625,9 @@ internal class KotlinMetadataVisitor(
internal lateinit var className: String
internal var builder: KotlinClassHeaderBuilder? = null
- private fun log(msg: String) {
- if (logDebug) {
- logger.debug(msg)
- } else {
- logger.warn(msg)
+ private fun log(msgProvider: () -> String) {
+ if (!logDebug) {
+ logger.quiet(msgProvider())
}
}
@@ -592,12 +639,12 @@ internal class KotlinMetadataVisitor(
superName: String?,
interfaces: Array?
) {
- log("KotlinMetadataVisitor#visit: $name extends $superName")
+ log { "KotlinMetadataVisitor#visit: $name extends $superName" }
className = name
}
override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor? {
- log("KotlinMetadataVisitor#visitAnnotation: descriptor=$descriptor visible=$visible")
+ log { "KotlinMetadataVisitor#visitAnnotation: descriptor=$descriptor visible=$visible" }
return if (KOTLIN_METADATA == descriptor) {
builder = KotlinClassHeaderBuilder()
KotlinAnnotationVisitor(logger, builder!!)
@@ -613,18 +660,16 @@ internal class KotlinMetadataVisitor(
private val arrayName: String? = null
) : AnnotationVisitor(ASM_VERSION) {
- private fun log(msg: String) {
- if (logDebug) {
- logger.debug(msg)
- } else {
- logger.warn(msg)
+ private fun log(msgProvider: () -> String) {
+ if (!logDebug) {
+ logger.quiet(msgProvider())
}
}
private fun indent() = " ".repeat(level)
override fun visit(name: String?, value: Any?) {
- log("${indent()}- visit: name=$name, value=(${value?.javaClass?.simpleName}, ${stringValueOfArrayElement(value)})")
+ log { "${indent()}- visit: name=$name, value=(${value?.javaClass?.simpleName}, ${stringValueOfArrayElement(value)})" }
when (name) {
"k" -> builder.kind = value as Int
@@ -642,7 +687,7 @@ internal class KotlinMetadataVisitor(
}
override fun visitArray(name: String?): AnnotationVisitor {
- log("${indent()}- visitArray: name=$name")
+ log { "${indent()}- visitArray: name=$name" }
return KotlinAnnotationVisitor(logger, builder, level + 1, name)
}
}
@@ -658,3 +703,8 @@ fun stringValueOfArrayElement(value: Any?): String {
private fun isStaticFinal(access: Int): Boolean =
access and Opcodes.ACC_STATIC != 0 && access and Opcodes.ACC_FINAL != 0
+
+private fun isEffectivelyPublic(access: Int): Boolean {
+ val flags = AccessFlags(access)
+ return flags.isPublic || flags.isProtected
+}
diff --git a/src/main/kotlin/com/autonomousapps/internal/kotlin/abiDependencies.kt b/src/main/kotlin/com/autonomousapps/internal/kotlin/abiDependencies.kt
index feea1401b..316af9102 100644
--- a/src/main/kotlin/com/autonomousapps/internal/kotlin/abiDependencies.kt
+++ b/src/main/kotlin/com/autonomousapps/internal/kotlin/abiDependencies.kt
@@ -7,9 +7,8 @@ import com.autonomousapps.internal.ClassNames.canonicalize
import com.autonomousapps.internal.utils.DESC_REGEX
import com.autonomousapps.internal.utils.allItems
import com.autonomousapps.internal.utils.flatMapToSet
-import com.autonomousapps.model.intermediates.ExplodingAbi
+import com.autonomousapps.model.intermediates.consumer.ExplodingAbi
import java.io.File
-import java.util.jar.JarFile
internal fun computeAbi(
classFiles: Set,
@@ -67,7 +66,7 @@ private fun List.explodedAbi(
// We don't report that the JDK is part of the ABI
.filterNot { it.startsWith("java/") }
.map { canonicalize(it) }
- .toSortedSet()
+ .toSortedSet(),
)
}.toSortedSet()
}
diff --git a/src/main/kotlin/com/autonomousapps/internal/kotlin/asmUtils.kt b/src/main/kotlin/com/autonomousapps/internal/kotlin/asmUtils.kt
index 82abfc293..7b5e3d5a5 100644
--- a/src/main/kotlin/com/autonomousapps/internal/kotlin/asmUtils.kt
+++ b/src/main/kotlin/com/autonomousapps/internal/kotlin/asmUtils.kt
@@ -169,9 +169,11 @@ internal val MEMBER_SORT_ORDER = compareBy(
{ it.desc }
)
-internal data class AccessFlags(val access: Int) {
+// TODO move
+data class AccessFlags(val access: Int) {
val isPublic: Boolean get() = isPublic(access)
val isProtected: Boolean get() = isProtected(access)
+ val isPrivate: Boolean get() = isPrivate(access)
val isStatic: Boolean get() = isStatic(access)
val isFinal: Boolean get() = isFinal(access)
val isSynthetic: Boolean get() = isSynthetic(access)
@@ -182,6 +184,7 @@ internal data class AccessFlags(val access: Int) {
internal fun isPublic(access: Int) = access and Opcodes.ACC_PUBLIC != 0 || access and Opcodes.ACC_PROTECTED != 0
internal fun isProtected(access: Int) = access and Opcodes.ACC_PROTECTED != 0
+internal fun isPrivate(access: Int) = access and Opcodes.ACC_PRIVATE != 0
internal fun isStatic(access: Int) = access and Opcodes.ACC_STATIC != 0
internal fun isFinal(access: Int) = access and Opcodes.ACC_FINAL != 0
internal fun isSynthetic(access: Int) = access and Opcodes.ACC_SYNTHETIC != 0
diff --git a/src/main/kotlin/com/autonomousapps/internal/models.kt b/src/main/kotlin/com/autonomousapps/internal/models.kt
index 32237ee4c..e2e8ca918 100644
--- a/src/main/kotlin/com/autonomousapps/internal/models.kt
+++ b/src/main/kotlin/com/autonomousapps/internal/models.kt
@@ -6,12 +6,14 @@ import com.autonomousapps.internal.asm.Opcodes
import com.autonomousapps.internal.utils.efficient
import com.autonomousapps.internal.utils.filterNotToSet
import com.autonomousapps.internal.utils.mapToSet
+import com.autonomousapps.model.intermediates.producer.BinaryClass
+import com.autonomousapps.model.intermediates.producer.Member
import com.squareup.moshi.JsonClass
import java.lang.annotation.RetentionPolicy
import java.util.regex.Pattern
/** Metadata from an Android manifest. */
-data class Manifest(
+internal data class Manifest(
/** The package name per ``. */
val packageName: String,
/** A map of component type to components. */
@@ -28,28 +30,28 @@ data class Manifest(
}
}
-data class AnalyzedClass(
+internal data class AnalyzedClass(
val className: String,
val outerClassName: String?,
val superClassName: String?,
val retentionPolicy: RetentionPolicy?,
/**
- * Ignoring constructors and static initializers. Such a class will not prejudice the compileOnly
- * algorithm against declaring the containing jar "annotations-only". See for example
- * `org.jetbrains.annotations.ApiStatus`. This outer class only exists as a sort of "namespace"
- * for the annotations it contains.
+ * Ignoring constructors and static initializers. Such a class will not prejudice the compileOnly algorithm against
+ * declaring the containing jar "annotations-only". See for example `org.jetbrains.annotations.ApiStatus`. This outer
+ * class only exists as a sort of "namespace" for the annotations it contains.
*/
val hasNoMembers: Boolean,
val access: Access,
val methods: Set,
val innerClasses: Set,
val constantFields: Set,
+ val binaryClass: BinaryClass,
) : Comparable {
-
constructor(
className: String,
outerClassName: String?,
- superClassName: String?,
+ superClassName: String,
+ interfaces: Set,
retentionPolicy: String?,
isAnnotation: Boolean,
hasNoMembers: Boolean,
@@ -57,6 +59,8 @@ data class AnalyzedClass(
methods: Set,
innerClasses: Set,
constantClasses: Set,
+ effectivelyPublicFields: Set,
+ effectivelyPublicMethods: Set,
) : this(
className = className,
outerClassName = outerClassName,
@@ -66,7 +70,14 @@ data class AnalyzedClass(
access = access,
methods = methods,
innerClasses = innerClasses,
- constantFields = constantClasses
+ constantFields = constantClasses,
+ binaryClass = BinaryClass(
+ className = className.replace('.', '/'),
+ superClassName = superClassName.replace('.', '/'),
+ interfaces = interfaces,
+ effectivelyPublicFields = effectivelyPublicFields,
+ effectivelyPublicMethods = effectivelyPublicMethods,
+ ),
)
companion object {
@@ -83,7 +94,8 @@ data class AnalyzedClass(
override fun compareTo(other: AnalyzedClass): Int = className.compareTo(other.className)
}
-enum class Access {
+// TODO(tsr): this is very similar to code in asmUtils.kt.
+internal enum class Access {
PUBLIC,
PROTECTED,
PRIVATE,
@@ -111,7 +123,7 @@ enum class Access {
}
}
-data class Method internal constructor(val types: Set) {
+internal data class Method internal constructor(val types: Set) {
constructor(descriptor: String) : this(findTypes(descriptor))
diff --git a/src/main/kotlin/com/autonomousapps/internal/reason/DependencyAdviceExplainer.kt b/src/main/kotlin/com/autonomousapps/internal/reason/DependencyAdviceExplainer.kt
index 271d7f6ba..cf759fb68 100644
--- a/src/main/kotlin/com/autonomousapps/internal/reason/DependencyAdviceExplainer.kt
+++ b/src/main/kotlin/com/autonomousapps/internal/reason/DependencyAdviceExplainer.kt
@@ -188,6 +188,7 @@ internal class DependencyAdviceExplainer(
val reasons = usage.reasons.filter { it !is Reason.Unused && it !is Reason.Undeclared }
val isCompileOnly = reasons.any { it is Reason.CompileTimeAnnotations }
+
reasons.forEach { reason ->
append("""* """)
val prefix = when (variant.kind) {
diff --git a/src/main/kotlin/com/autonomousapps/model/Capability.kt b/src/main/kotlin/com/autonomousapps/model/Capability.kt
index 255c9f3ff..016c3e713 100644
--- a/src/main/kotlin/com/autonomousapps/model/Capability.kt
+++ b/src/main/kotlin/com/autonomousapps/model/Capability.kt
@@ -3,6 +3,9 @@
package com.autonomousapps.model
import com.autonomousapps.internal.utils.LexicographicIterableComparator
+import com.autonomousapps.internal.utils.filterToOrderedSet
+import com.autonomousapps.model.intermediates.consumer.MemberAccess
+import com.autonomousapps.model.intermediates.producer.BinaryClass
import com.squareup.moshi.JsonClass
import dev.zacsweers.moshix.sealed.annotations.TypeLabel
@@ -99,11 +102,138 @@ data class AnnotationProcessorCapability(
}
}
+@TypeLabel("binaryClass")
+@JsonClass(generateAdapter = false)
+data class BinaryClassCapability(
+ val binaryClasses: Set,
+) : Capability() {
+
+ internal data class PartitionResult(
+ val matchingClasses: Set,
+ val nonMatchingClasses: Set,
+ ) {
+
+ companion object {
+ fun empty(): PartitionResult = PartitionResult(emptySet(), emptySet())
+ }
+
+ class Builder {
+ val matchingClasses = sortedSetOf()
+ val nonMatchingClasses = sortedSetOf()
+
+ fun build(): PartitionResult {
+ return PartitionResult(
+ matchingClasses = matchingClasses,
+ nonMatchingClasses = nonMatchingClasses,
+ )
+ }
+ }
+ }
+
+ override fun merge(other: Capability): Capability {
+ return BinaryClassCapability(
+ binaryClasses = binaryClasses + (other as BinaryClassCapability).binaryClasses,
+ )
+ }
+
+ internal fun findMatchingClasses(memberAccess: MemberAccess): PartitionResult {
+ val relevant = findRelevantBinaryClasses(memberAccess)
+
+ // lenient
+ if (relevant.isEmpty()) return PartitionResult.empty()
+
+ return relevant
+ .map { bin -> bin.partition(memberAccess) }
+ .fold(PartitionResult.Builder()) { acc, (match, nonMatch) ->
+ acc.apply {
+ match?.let { matchingClasses.add(it) }
+ nonMatch?.let { nonMatchingClasses.add(it) }
+ }
+ }
+ .build()
+ }
+
+ /**
+ * Example:
+ * 1. [memberAccess] is for `groovy/lang/MetaClass#getProperty`.
+ * 2. That method is actually provided by `groovy/lang/MetaObjectProtocol`, which `groovy/lang/MetaClass` implements.
+ *
+ * All of the above ("this" class, its super class, and its interfaces) are relevant for search purposes. Note we
+ * don't inspect the member names for this check.
+ */
+ private fun findRelevantBinaryClasses(memberAccess: MemberAccess): Set {
+ // direct references
+ val relevant = binaryClasses.filterTo(mutableSetOf()) { bin ->
+ bin.className == memberAccess.owner
+ }
+
+ // Walk up the class hierarchy
+ fun walkUp(): Int {
+ binaryClasses.filterTo(relevant) { bin ->
+ bin.className in relevant.map { it.superClassName }
+ || bin.className in relevant.flatMap { it.interfaces }
+ }
+ return relevant.size
+ }
+
+ // TODO(tsr): this could be more performant
+ do {
+ val size = relevant.size
+ val newSize = walkUp()
+ } while (newSize > size)
+
+ return relevant
+ }
+
+ /**
+ * Partitions and returns artificial pair of [BinaryClasses][BinaryClass]. Non-null elements indicate relevant (to
+ * [memberAccess] matching and non-matching members of this `BinaryClass`. Matching members are binary-compatible; and
+ * non-matching members have the same [name][com.autonomousapps.model.intermediates.producer.Member.name] but
+ * incompatible [descriptors][com.autonomousapps.model.intermediates.producer.Member.descriptor], and are therefore
+ * binary-incompatible.
+ *
+ * nb: We don't want this as a method directly in BinaryClass because it can't safely assert the prerequisite that
+ * it's only called on "relevant" classes. THIS class, however, can, via findRelevantBinaryClasses.
+ */
+ private fun BinaryClass.partition(memberAccess: MemberAccess): Pair {
+ // There can be only one match
+ val matchingFields = effectivelyPublicFields.firstOrNull { it.matches(memberAccess) }
+ val matchingMethods = effectivelyPublicMethods.firstOrNull { it.matches(memberAccess) }
+
+ // There can be many non-matches
+ val nonMatchingFields = effectivelyPublicFields.filterToOrderedSet { it.doesNotMatch(memberAccess) }
+ val nonMatchingMethods = effectivelyPublicMethods.filterToOrderedSet { it.doesNotMatch(memberAccess) }
+
+ // Create a view of the binary class containing only the matching members.
+ val match = if (matchingFields != null || matchingMethods != null) {
+ copy(
+ effectivelyPublicFields = matchingFields?.let { setOf(it) }.orEmpty(),
+ effectivelyPublicMethods = matchingMethods?.let { setOf(it) }.orEmpty()
+ )
+ } else {
+ null
+ }
+
+ // Create a view of the binary class containing only the non-matching members.
+ val nonMatch = if (nonMatchingFields.isNotEmpty() || nonMatchingMethods.isNotEmpty()) {
+ copy(
+ effectivelyPublicFields = nonMatchingFields,
+ effectivelyPublicMethods = nonMatchingMethods,
+ )
+ } else {
+ null
+ }
+
+ return match to nonMatch
+ }
+}
+
@TypeLabel("class")
@JsonClass(generateAdapter = false)
data class ClassCapability(
val classes: Set,
) : Capability() {
+
override fun merge(other: Capability): Capability {
return ClassCapability(classes + (other as ClassCapability).classes)
}
diff --git a/src/main/kotlin/com/autonomousapps/model/DuplicateClass.kt b/src/main/kotlin/com/autonomousapps/model/DuplicateClass.kt
index 15f975397..50a917b2c 100644
--- a/src/main/kotlin/com/autonomousapps/model/DuplicateClass.kt
+++ b/src/main/kotlin/com/autonomousapps/model/DuplicateClass.kt
@@ -5,21 +5,30 @@ import com.autonomousapps.model.declaration.Variant
import com.squareup.moshi.JsonClass
/**
- * A fully-qualified [classReference] (`/`-delimited) that is provided by multiple [dependencies], and associated with a
+ * A fully-qualified [className] (`/`-delimited) that is provided by multiple [dependencies], and associated with a
* [variant].
*/
@JsonClass(generateAdapter = false)
data class DuplicateClass(
+ /** The variant (e.g., "main" or "test") associated with this class. */
val variant: Variant,
+ /** The name of the classpath that has this duplication, e.g. "compile" or "runtime". */
val classpathName: String,
- val classReference: String,
+ /** E.g., `java/lang/String`. */
+ val className: String,
+ /** The set of dependencies that provide this class. */
val dependencies: Set,
) : Comparable {
+ internal companion object {
+ const val COMPILE_CLASSPATH_NAME = "compile"
+ const val RUNTIME_CLASSPATH_NAME = "runtime"
+ }
+
override fun compareTo(other: DuplicateClass): Int {
return compareBy(DuplicateClass::variant)
.thenBy(DuplicateClass::classpathName)
- .thenBy(DuplicateClass::classReference)
+ .thenBy(DuplicateClass::className)
.thenBy(LexicographicIterableComparator()) { it.dependencies }
.compare(this, other)
}
diff --git a/src/main/kotlin/com/autonomousapps/model/ProjectVariant.kt b/src/main/kotlin/com/autonomousapps/model/ProjectVariant.kt
index e20344900..a48f3446a 100644
--- a/src/main/kotlin/com/autonomousapps/model/ProjectVariant.kt
+++ b/src/main/kotlin/com/autonomousapps/model/ProjectVariant.kt
@@ -8,6 +8,7 @@ import com.autonomousapps.internal.utils.flatMapToSet
import com.autonomousapps.internal.utils.fromJson
import com.autonomousapps.model.CodeSource.Kind
import com.autonomousapps.model.declaration.Variant
+import com.autonomousapps.model.intermediates.consumer.MemberAccess
import com.squareup.moshi.JsonClass
import org.gradle.api.file.Directory
@@ -95,6 +96,16 @@ data class ProjectVariant(
codeSource.flatMapToOrderedSet { it.imports }
}
+ /**
+ * Every member access from this project to classes in another module. cf [usedClasses], which is a flat set of
+ * referenced class names.
+ */
+ val memberAccesses: Set by unsafeLazy {
+ codeSource.flatMapToOrderedSet { src ->
+ src.binaryClassAccesses.entries.flatMap { entry -> entry.value }
+ }
+ }
+
val javaImports: Set by unsafeLazy {
codeSource.filter { it.kind == Kind.JAVA }
.flatMapToOrderedSet { it.imports }
diff --git a/src/main/kotlin/com/autonomousapps/model/Source.kt b/src/main/kotlin/com/autonomousapps/model/Source.kt
index 7b5117095..4d06946fb 100644
--- a/src/main/kotlin/com/autonomousapps/model/Source.kt
+++ b/src/main/kotlin/com/autonomousapps/model/Source.kt
@@ -3,6 +3,7 @@
package com.autonomousapps.model
import com.autonomousapps.internal.parse.AndroidResParser
+import com.autonomousapps.model.intermediates.consumer.MemberAccess
import com.squareup.moshi.JsonClass
import dev.zacsweers.moshix.sealed.annotations.TypeLabel
@@ -66,6 +67,9 @@ data class CodeSource(
/** Every import in this source file. */
val imports: Set,
+
+ /** Every [MemberAccess] to another class from [this class][className]. */
+ val binaryClassAccesses: Map>,
) : Source(relativePath) {
enum class Kind {
diff --git a/src/main/kotlin/com/autonomousapps/model/intermediates/DependencyTraceReport.kt b/src/main/kotlin/com/autonomousapps/model/intermediates/DependencyTraceReport.kt
index 8e9b1203f..5cf7d915b 100644
--- a/src/main/kotlin/com/autonomousapps/model/intermediates/DependencyTraceReport.kt
+++ b/src/main/kotlin/com/autonomousapps/model/intermediates/DependencyTraceReport.kt
@@ -69,6 +69,11 @@ internal data class DependencyTraceReport(
coordinates: Coordinates,
bucket: Bucket,
) {
+ if (isBinaryIncompatible(reasons, coordinates)) {
+ // If we already know this dependency is binary-incompatible, don't do anything.
+ return
+ }
+
val currTrace = map[coordinates]
when (val currBucket = currTrace?.bucket) {
// new value, set it
@@ -104,11 +109,25 @@ internal data class DependencyTraceReport(
coordinates: Coordinates,
reason: Reason,
) {
- map.merge(coordinates, mutableSetOf(reason)) { acc, inc ->
- acc.apply { addAll(inc) }
+ if (reason is Reason.BinaryIncompatible) {
+ // If the new reason is BinaryIncompatible, set it
+ map[coordinates] = mutableSetOf(reason)
+ dependencies[coordinates] = Trace(coordinates, Bucket.NONE)
+ } else if (!isBinaryIncompatible(map, coordinates)) {
+ // If we already set the reason to BinaryIncompatible, don't allow any other Reason
+ map.merge(coordinates, mutableSetOf(reason)) { acc, inc ->
+ acc.apply { addAll(inc) }
+ }
}
}
+ private fun isBinaryIncompatible(
+ map: MutableMap>,
+ coordinates: Coordinates,
+ ): Boolean {
+ return map[coordinates]?.filterIsInstance()?.isNotEmpty() == true
+ }
+
fun build(): DependencyTraceReport = DependencyTraceReport(
buildType = buildType,
flavor = flavor,
diff --git a/src/main/kotlin/com/autonomousapps/model/intermediates/ExplodingJar.kt b/src/main/kotlin/com/autonomousapps/model/intermediates/ExplodingJar.kt
index fc8ddf994..3e3b7426d 100644
--- a/src/main/kotlin/com/autonomousapps/model/intermediates/ExplodingJar.kt
+++ b/src/main/kotlin/com/autonomousapps/model/intermediates/ExplodingJar.kt
@@ -8,6 +8,7 @@ import com.autonomousapps.internal.ClassNames
import com.autonomousapps.internal.utils.mapToOrderedSet
import com.autonomousapps.internal.utils.reallyAll
import com.autonomousapps.model.KtFile
+import com.autonomousapps.model.intermediates.producer.BinaryClass
import java.lang.annotation.RetentionPolicy
/**
@@ -39,8 +40,12 @@ internal class ExplodingJar(
) {
/**
- * The set of classes provided by this jar. May be empty.
+ * The set of classes provided by this jar, including information about their superclass, interfaces, and public
+ * members. May be empty. cf [classNames].
*/
+ val binaryClasses: Set = analyzedClasses.mapToOrderedSet { it.binaryClass }
+
+ /** The set of classes provided by this jar. May be empty. cf [binaryClasses]. */
val classNames: Set = analyzedClasses.mapToOrderedSet { it.className }
/**
diff --git a/src/main/kotlin/com/autonomousapps/model/intermediates/Reason.kt b/src/main/kotlin/com/autonomousapps/model/intermediates/Reason.kt
index b40998627..b505d346f 100644
--- a/src/main/kotlin/com/autonomousapps/model/intermediates/Reason.kt
+++ b/src/main/kotlin/com/autonomousapps/model/intermediates/Reason.kt
@@ -4,6 +4,9 @@ package com.autonomousapps.model.intermediates
import com.autonomousapps.internal.utils.capitalizeSafely
import com.autonomousapps.model.AndroidResSource
+import com.autonomousapps.model.intermediates.consumer.MemberAccess
+import com.autonomousapps.model.intermediates.producer.BinaryClass
+import com.autonomousapps.model.intermediates.producer.Member
import com.squareup.moshi.JsonClass
import dev.zacsweers.moshix.sealed.annotations.TypeLabel
@@ -21,7 +24,9 @@ internal sealed class Reason(open val reason: String) {
append(reason)
- if (prefix.isEmpty()) {
+ if (this@Reason is BinaryIncompatible) {
+ // do nothing
+ } else if (prefix.isEmpty()) {
append(" (implies ${effectiveConfiguration}).")
} else {
append(" (implies ${prefix}${effectiveConfiguration.capitalizeSafely()}).")
@@ -68,6 +73,67 @@ internal sealed class Reason(open val reason: String) {
}
}
+ @TypeLabel("binaryIncompatible")
+ @JsonClass(generateAdapter = false)
+ data class BinaryIncompatible(override val reason: String) : Reason("Is binary-incompatible") {
+ constructor(
+ memberAccesses: Set,
+ nonMatchingClasses: Set,
+ ) : this(reasonString(memberAccesses, nonMatchingClasses))
+
+ override fun toString(): String = reason
+ override val configurationName: String = "n/a"
+
+ private companion object {
+ fun reasonString(memberAccesses: Set, nonMatchingClasses: Set): String {
+ require(memberAccesses.isNotEmpty()) { "memberAccesses must not be empty" }
+ require(nonMatchingClasses.isNotEmpty()) { "nonMatchingClasses must not be empty" }
+
+ // A list of pairs, where each pair is of an access to a set of non-matches
+ val nonMatches = memberAccesses
+ .map { access ->
+ access to when (access) {
+ is MemberAccess.Field -> nonMatchingClasses.winnowedBy(access) { it.effectivelyPublicFields }
+ is MemberAccess.Method -> nonMatchingClasses.winnowedBy(access) { it.effectivelyPublicMethods }
+ }
+ }
+ // We don't want to display information for matches, that would be redundant.
+ .filter { (_, nonMatches) -> nonMatches.isNotEmpty() }
+
+ return buildString {
+ appendLine("Is binary-incompatible, and should be removed from the classpath:")
+ nonMatches.forEachIndexed { i, (access, incompatibleMembers) ->
+ if (i > 0) appendLine()
+
+ when (access) {
+ is MemberAccess.Field -> {
+ append(" Expected FIELD ${access.descriptor} ${access.owner}.${access.name}, but was ")
+ append(incompatibleMembers.joinToString { "${it.className}.${it.memberName} ${it.descriptor}" })
+ }
+
+ is MemberAccess.Method -> {
+ append(" Expected METHOD ${access.owner}.${access.name}${access.descriptor}, but was ")
+ append(incompatibleMembers.joinToString { "${it.className}.${it.memberName}${it.descriptor}" })
+ }
+ }
+ }
+ }
+ }
+
+ private fun Set.winnowedBy(
+ access: MemberAccess,
+ selector: (BinaryClass) -> Set,
+ ): Set {
+ return asSequence()
+ .map { bin -> bin.className to selector(bin) }
+ .map { (className, fields) -> className to fields.filter { it.doesNotMatch(access) } }
+ .filterNot { (_, fields) -> fields.isEmpty() }
+ .flatMap { (className, fields) -> fields.map { it.asPrintable(className) } }
+ .toSortedSet()
+ }
+ }
+ }
+
@TypeLabel("compile_time_anno")
@JsonClass(generateAdapter = false)
data class CompileTimeAnnotations(override val reason: String) : Reason(reason) {
diff --git a/src/main/kotlin/com/autonomousapps/model/intermediates/consumer.kt b/src/main/kotlin/com/autonomousapps/model/intermediates/consumer.kt
deleted file mode 100644
index 04c21f131..000000000
--- a/src/main/kotlin/com/autonomousapps/model/intermediates/consumer.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (c) 2024. Tony Robalik.
-// SPDX-License-Identifier: Apache-2.0
-package com.autonomousapps.model.intermediates
-
-import com.autonomousapps.model.CodeSource
-import com.squareup.moshi.JsonClass
-
-/** A single source file (e.g., `.java`, `.kt`) in this project. */
-@JsonClass(generateAdapter = false)
-internal data class ExplodingSourceCode(
- val relativePath: String,
- val className: String,
- val kind: CodeSource.Kind,
- val imports: Set
-) : Comparable {
-
- override fun compareTo(other: ExplodingSourceCode): Int = relativePath.compareTo(other.relativePath)
-}
-
-@JsonClass(generateAdapter = false)
-internal data class ExplodingBytecode(
- val relativePath: String,
-
- /** The name of this class. */
- val className: String,
-
- /** The path to the source file for this class. TODO: how does this differ from [relativePath]? */
- val sourceFile: String?,
-
- /** Every class discovered in the bytecode of [className], and not as an annotation. */
- val nonAnnotationClasses: Set,
-
- /** Every class discovered in the bytecode of [className], and as a visible annotation. */
- val annotationClasses: Set,
-
- /** Every class discovered in the bytecode of [className], and as an invisible annotation. */
- val invisibleAnnotationClasses: Set,
-)
-
-@JsonClass(generateAdapter = false)
-internal data class ExplodingAbi(
- val className: String,
- val sourceFile: String?,
- /** Every class discovered in the bytecode of [className], and which is exposed as part of the ABI. */
- val exposedClasses: Set
-) : Comparable {
- override fun compareTo(other: ExplodingAbi): Int = className.compareTo(other.className)
-}
diff --git a/src/main/kotlin/com/autonomousapps/model/intermediates/consumer/ExplodingAbi.kt b/src/main/kotlin/com/autonomousapps/model/intermediates/consumer/ExplodingAbi.kt
new file mode 100644
index 000000000..d0df38cd2
--- /dev/null
+++ b/src/main/kotlin/com/autonomousapps/model/intermediates/consumer/ExplodingAbi.kt
@@ -0,0 +1,15 @@
+// Copyright (c) 2024. Tony Robalik.
+// SPDX-License-Identifier: Apache-2.0
+package com.autonomousapps.model.intermediates.consumer
+
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = false)
+internal data class ExplodingAbi(
+ val className: String,
+ val sourceFile: String?,
+ /** Every class discovered in the bytecode of [className], and which is exposed as part of the ABI. */
+ val exposedClasses: Set,
+) : Comparable {
+ override fun compareTo(other: ExplodingAbi): Int = className.compareTo(other.className)
+}
diff --git a/src/main/kotlin/com/autonomousapps/model/intermediates/consumer/ExplodingBytecode.kt b/src/main/kotlin/com/autonomousapps/model/intermediates/consumer/ExplodingBytecode.kt
new file mode 100644
index 000000000..abf0f59c3
--- /dev/null
+++ b/src/main/kotlin/com/autonomousapps/model/intermediates/consumer/ExplodingBytecode.kt
@@ -0,0 +1,28 @@
+// Copyright (c) 2024. Tony Robalik.
+// SPDX-License-Identifier: Apache-2.0
+package com.autonomousapps.model.intermediates.consumer
+
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = false)
+internal data class ExplodingBytecode(
+ val relativePath: String,
+
+ /** The name of this class. */
+ val className: String,
+
+ /** The path to the source file for this class. TODO: how does this differ from [relativePath]? */
+ val sourceFile: String?,
+
+ /** Every class discovered in the bytecode of [className], and not as an annotation. */
+ val nonAnnotationClasses: Set,
+
+ /** Every class discovered in the bytecode of [className], and as a visible annotation. */
+ val annotationClasses: Set,
+
+ /** Every class discovered in the bytecode of [className], and as an invisible annotation. */
+ val invisibleAnnotationClasses: Set,
+
+ /** Every [MemberAccess] to another class from [this class][className]. */
+ val binaryClassAccesses: Map>,
+)
diff --git a/src/main/kotlin/com/autonomousapps/model/intermediates/consumer/ExplodingSourceCode.kt b/src/main/kotlin/com/autonomousapps/model/intermediates/consumer/ExplodingSourceCode.kt
new file mode 100644
index 000000000..c497de315
--- /dev/null
+++ b/src/main/kotlin/com/autonomousapps/model/intermediates/consumer/ExplodingSourceCode.kt
@@ -0,0 +1,18 @@
+// Copyright (c) 2024. Tony Robalik.
+// SPDX-License-Identifier: Apache-2.0
+package com.autonomousapps.model.intermediates.consumer
+
+import com.autonomousapps.model.CodeSource
+import com.squareup.moshi.JsonClass
+
+/** A single source file (e.g., `.java`, `.kt`) in this project. */
+@JsonClass(generateAdapter = false)
+internal data class ExplodingSourceCode(
+ val relativePath: String,
+ val className: String,
+ val kind: CodeSource.Kind,
+ val imports: Set
+) : Comparable {
+
+ override fun compareTo(other: ExplodingSourceCode): Int = relativePath.compareTo(other.relativePath)
+}
diff --git a/src/main/kotlin/com/autonomousapps/model/intermediates/consumer/MemberAccess.kt b/src/main/kotlin/com/autonomousapps/model/intermediates/consumer/MemberAccess.kt
new file mode 100644
index 000000000..3474ff1fc
--- /dev/null
+++ b/src/main/kotlin/com/autonomousapps/model/intermediates/consumer/MemberAccess.kt
@@ -0,0 +1,66 @@
+// Copyright (c) 2024. Tony Robalik.
+// SPDX-License-Identifier: Apache-2.0
+package com.autonomousapps.model.intermediates.consumer
+
+import com.squareup.moshi.JsonClass
+import dev.zacsweers.moshix.sealed.annotations.TypeLabel
+
+/**
+ * Represents, from the consumer's bytecode, the access of a member from a producer. E.g., if "our" code calls
+ * `"some string".substring(1)`, that a member access on the substring method of the String class.
+ *
+ * nb: Borrowing heavily from `asmUtils.kt` and similar but substantially different from
+ * [Member][com.autonomousapps.model.intermediates.producer.Member] on the producer side.
+ *
+ * TODO: ideally this would be internal. Do the Capabilities really need to be public?
+ */
+@JsonClass(generateAdapter = false, generator = "sealed:type")
+sealed class MemberAccess(
+ /** The class that owns this member, e.g., `java/lang/String`. */
+ open val owner: String,
+ /** The name of this member, e.g., `substring`. */
+ open val name: String,
+ /** The descriptor of this member, e.g., `(I)Ljava/Lang/String;` for a [Method] or `I` for a [Field]. */
+ open val descriptor: String,
+) : Comparable {
+
+ override fun compareTo(other: MemberAccess): Int {
+ if (this is Field && other !is Field) return -1
+ if (this !is Field && other is Field) return 1
+
+ return compareBy(MemberAccess::owner)
+ .thenBy(MemberAccess::name)
+ .thenBy(MemberAccess::descriptor)
+ .compare(this, other)
+ }
+
+ @TypeLabel("method")
+ @JsonClass(generateAdapter = false)
+ data class Method(
+ /** `java/lang/String` */
+ override val owner: String,
+ /** substring */
+ override val name: String,
+ /** (I)Ljava/Lang/String; */
+ override val descriptor: String,
+ ) : MemberAccess(
+ owner = owner,
+ name = name,
+ descriptor = descriptor,
+ )
+
+ @TypeLabel("field")
+ @JsonClass(generateAdapter = false)
+ data class Field(
+ /** kotlin/io/encoding/Base64 */
+ override val owner: String,
+ /** bytesPerGroup */
+ override val name: String,
+ /** I */
+ override val descriptor: String,
+ ) : MemberAccess(
+ owner = owner,
+ name = name,
+ descriptor = descriptor,
+ )
+}
diff --git a/src/main/kotlin/com/autonomousapps/model/intermediates/producer/BinaryClass.kt b/src/main/kotlin/com/autonomousapps/model/intermediates/producer/BinaryClass.kt
new file mode 100644
index 000000000..9f9044b40
--- /dev/null
+++ b/src/main/kotlin/com/autonomousapps/model/intermediates/producer/BinaryClass.kt
@@ -0,0 +1,46 @@
+// Copyright (c) 2024. Tony Robalik.
+// SPDX-License-Identifier: Apache-2.0
+package com.autonomousapps.model.intermediates.producer
+
+import com.autonomousapps.internal.utils.LexicographicIterableComparator
+import com.squareup.moshi.JsonClass
+import java.util.SortedSet
+
+// TODO: ideally this would be internal. Do the Capabilities really need to be public?
+@JsonClass(generateAdapter = false)
+data class BinaryClass(
+ val className: String,
+ val superClassName: String,
+ val interfaces: Set,
+ val effectivelyPublicFields: Set,
+ val effectivelyPublicMethods: Set,
+) : Comparable {
+
+ override fun compareTo(other: BinaryClass): Int {
+ return compareBy(BinaryClass::className)
+ .thenBy(BinaryClass::superClassName)
+ .thenBy(LexicographicIterableComparator()) { it.interfaces }
+ .thenBy(LexicographicIterableComparator()) { it.effectivelyPublicFields }
+ .thenBy(LexicographicIterableComparator()) { it.effectivelyPublicMethods }
+ .compare(this, other)
+ }
+
+ internal class Builder(
+ val className: String,
+ val superClassName: String,
+ val interfaces: SortedSet,
+ val effectivelyPublicFields: SortedSet,
+ val effectivelyPublicMethods: SortedSet,
+ ) {
+
+ fun build(): BinaryClass {
+ return BinaryClass(
+ className = className,
+ superClassName = superClassName,
+ interfaces = interfaces,
+ effectivelyPublicFields = effectivelyPublicFields,
+ effectivelyPublicMethods = effectivelyPublicMethods,
+ )
+ }
+ }
+}
diff --git a/src/main/kotlin/com/autonomousapps/model/intermediates/producer/ExplodedJar.kt b/src/main/kotlin/com/autonomousapps/model/intermediates/producer/ExplodedJar.kt
new file mode 100644
index 000000000..0ac8e4793
--- /dev/null
+++ b/src/main/kotlin/com/autonomousapps/model/intermediates/producer/ExplodedJar.kt
@@ -0,0 +1,107 @@
+// Copyright (c) 2024. Tony Robalik.
+// SPDX-License-Identifier: Apache-2.0
+package com.autonomousapps.model.intermediates.producer
+
+import com.autonomousapps.internal.utils.ifNotEmpty
+import com.autonomousapps.model.AndroidLinterCapability
+import com.autonomousapps.model.BinaryClassCapability
+import com.autonomousapps.model.Capability
+import com.autonomousapps.model.ClassCapability
+import com.autonomousapps.model.ConstantCapability
+import com.autonomousapps.model.Coordinates
+import com.autonomousapps.model.InferredCapability
+import com.autonomousapps.model.KtFile
+import com.autonomousapps.model.PhysicalArtifact
+import com.autonomousapps.model.SecurityProviderCapability
+import com.autonomousapps.model.intermediates.DependencyView
+import com.autonomousapps.model.intermediates.ExplodingJar
+import com.squareup.moshi.JsonClass
+import java.io.File
+
+/**
+ * A library or project, along with the set of classes declared by, and other information contained within, this
+ * exploded jar. This is the serialized form of [ExplodingJar].
+ */
+@JsonClass(generateAdapter = false)
+internal data class ExplodedJar(
+
+ override val coordinates: Coordinates,
+ val jarFile: File,
+
+ /**
+ * True if this dependency contains only annotation that are only needed at compile-time (`CLASS`
+ * and `SOURCE` level retention policies). False otherwise.
+ */
+ val isCompileOnlyAnnotations: Boolean = false,
+ /**
+ * The set of classes that are service providers (they extend [java.security.Provider]). May be
+ * empty.
+ */
+ val securityProviders: Set = emptySet(),
+ /**
+ * Android Lint registry, if there is one. May be null.
+ */
+ val androidLintRegistry: String? = null,
+ /**
+ * True if this component contains _only_ an Android Lint jar/registry. If this is true,
+ * [androidLintRegistry] must be non-null.
+ */
+ val isLintJar: Boolean = false,
+ /**
+ * The classes (with binary member signatures) provided by this library.
+ */
+ val binaryClasses: Set,
+ /**
+ * The classes declared by this library.
+ */
+ val classes: Set,
+ /**
+ * A map of each class declared by this library to the set of constants it defines. The latter may
+ * be empty for any given declared class.
+ */
+ val constantFields: Map>,
+ /**
+ * All of the "Kt" files within this component.
+ */
+ val ktFiles: Set,
+) : DependencyView {
+
+ internal constructor(
+ artifact: PhysicalArtifact,
+ exploding: ExplodingJar,
+ ) : this(
+ coordinates = artifact.coordinates,
+ jarFile = artifact.file,
+ isCompileOnlyAnnotations = exploding.isCompileOnlyCandidate,
+ securityProviders = exploding.securityProviders,
+ androidLintRegistry = exploding.androidLintRegistry,
+ isLintJar = exploding.isLintJar,
+ binaryClasses = exploding.binaryClasses,
+ classes = exploding.classNames,
+ constantFields = exploding.constants,
+ ktFiles = exploding.ktFiles
+ )
+
+ override fun compareTo(other: ExplodedJar): Int {
+ return coordinates.compareTo(other.coordinates).let {
+ if (it == 0) jarFile.compareTo(other.jarFile) else it
+ }
+ }
+
+ init {
+ if (isLintJar && androidLintRegistry == null) {
+ throw IllegalStateException("Android lint jar for $coordinates must contain a lint registry")
+ }
+ }
+
+ override fun toCapabilities(): List {
+ val capabilities = mutableListOf()
+ capabilities += InferredCapability(isCompileOnlyAnnotations)
+ binaryClasses.ifNotEmpty { capabilities += BinaryClassCapability(it) }
+ classes.ifNotEmpty { capabilities += ClassCapability(it) }
+ constantFields.ifNotEmpty { capabilities += ConstantCapability(it, ktFiles) }
+ securityProviders.ifNotEmpty { capabilities += SecurityProviderCapability(it) }
+ androidLintRegistry?.let { capabilities += AndroidLinterCapability(it, isLintJar) }
+ return capabilities
+ }
+}
diff --git a/src/main/kotlin/com/autonomousapps/model/intermediates/producer/Member.kt b/src/main/kotlin/com/autonomousapps/model/intermediates/producer/Member.kt
new file mode 100644
index 000000000..877703c10
--- /dev/null
+++ b/src/main/kotlin/com/autonomousapps/model/intermediates/producer/Member.kt
@@ -0,0 +1,104 @@
+// Copyright (c) 2024. Tony Robalik.
+// SPDX-License-Identifier: Apache-2.0
+package com.autonomousapps.model.intermediates.producer
+
+import com.autonomousapps.internal.kotlin.AccessFlags
+import com.autonomousapps.model.intermediates.consumer.MemberAccess
+import com.squareup.moshi.JsonClass
+import dev.zacsweers.moshix.sealed.annotations.TypeLabel
+
+/**
+ * Represents a member of a [class][BinaryClass].
+ *
+ * nb: Borrowing heavily from `asmUtils.kt` and similar but substantially different from
+ * [MemberAccess][com.autonomousapps.model.intermediates.consumer.MemberAccess] on the consumer side.
+ */
+@JsonClass(generateAdapter = false, generator = "sealed:type")
+sealed class Member(
+ open val access: Int,
+ open val name: String,
+ open val descriptor: String,
+) : Comparable {
+
+ internal class Printable(
+ val className: String,
+ val memberName: String,
+ val descriptor: String,
+ ) : Comparable {
+ override fun compareTo(other: Printable): Int {
+ return compareBy(Printable::className)
+ .thenBy(Printable::memberName)
+ .thenBy(Printable::descriptor)
+ .compare(this, other)
+ }
+ }
+
+ internal fun asPrintable(className: String): Printable {
+ return Printable(
+ className = className,
+ memberName = name,
+ descriptor = descriptor,
+ )
+ }
+
+ override fun compareTo(other: Member): Int {
+ if (this is Field && other !is Field) return -1
+ if (this !is Field && other is Field) return 1
+
+ return compareBy(Member::name)
+ .thenBy(Member::descriptor)
+ .thenBy(Member::accessComparator)
+ .compare(this, other)
+ }
+
+ /** Returns true for matching name and descriptor. */
+ fun matches(memberAccess: MemberAccess): Boolean {
+ return name == memberAccess.name && descriptor == memberAccess.descriptor
+ }
+
+ /** Returns true for matching name and non-matching descriptor. */
+ fun doesNotMatch(memberAccess: MemberAccess): Boolean {
+ return name == memberAccess.name && descriptor != memberAccess.descriptor
+ }
+
+ protected val accessFlags get() = AccessFlags(access)
+
+ private val accessComparator: Int = when {
+ accessFlags.isPublic -> 3
+ accessFlags.isProtected -> 2
+ accessFlags.isPrivate -> 1
+ else -> 0 // package-private
+ }
+
+ abstract val signature: String
+
+ @TypeLabel("method")
+ @JsonClass(generateAdapter = false)
+ data class Method(
+ override val access: Int,
+ override val name: String,
+ override val descriptor: String,
+ ) : Member(
+ access = access,
+ name = name,
+ descriptor = descriptor,
+ ) {
+ override val signature: String
+ get() = "${accessFlags.getModifierString()} fun $name $descriptor"
+ }
+
+ @TypeLabel("field")
+ @JsonClass(generateAdapter = false)
+ data class Field(
+ override val access: Int,
+ override val name: String,
+ override val descriptor: String,
+ ) : Member(
+ access = access,
+ name = name,
+ descriptor = descriptor,
+ ) {
+ override val signature: String
+ get() = "${accessFlags.getModifierString()} field $name $descriptor"
+ }
+}
diff --git a/src/main/kotlin/com/autonomousapps/model/intermediates/producers.kt b/src/main/kotlin/com/autonomousapps/model/intermediates/producers.kt
index 5cd524887..9f8686877 100644
--- a/src/main/kotlin/com/autonomousapps/model/intermediates/producers.kt
+++ b/src/main/kotlin/com/autonomousapps/model/intermediates/producers.kt
@@ -2,12 +2,10 @@
// SPDX-License-Identifier: Apache-2.0
package com.autonomousapps.model.intermediates
-import com.autonomousapps.internal.utils.ifNotEmpty
import com.autonomousapps.internal.utils.toCoordinates
import com.autonomousapps.model.*
import com.squareup.moshi.JsonClass
import org.gradle.api.artifacts.result.ResolvedArtifactResult
-import java.io.File
internal interface DependencyView : Comparable where T : DependencyView {
val coordinates: Coordinates
@@ -22,7 +20,8 @@ internal interface DependencyView : Comparable where T : DependencyView
* Example registry: `nl.littlerobots.rxlint.RxIssueRegistry`.
*
* nb: Deliberately does not implement [DependencyView]. For various reasons, this information gets embedded in
- * [ExplodedJar], which is the preferred access point for deeper analysis.
+ * [ExplodedJar][com.autonomousapps.model.intermediates.producer.ExplodedJar], which is the preferred access point for
+ * deeper analysis.
*/
@JsonClass(generateAdapter = false)
internal data class AndroidLinterDependency(
@@ -140,85 +139,3 @@ internal data class ServiceLoaderDependency(
override fun toCapabilities(): List = listOf(ServiceLoaderCapability(providerFile, providerClasses))
}
-
-/**
- * A library or project, along with the set of classes declared by, and other information contained within, this
- * exploded jar. This is the serialized form of [ExplodingJar].
- */
-@JsonClass(generateAdapter = false)
-internal data class ExplodedJar(
-
- override val coordinates: Coordinates,
- val jarFile: File,
-
- /**
- * True if this dependency contains only annotation that are only needed at compile-time (`CLASS`
- * and `SOURCE` level retention policies). False otherwise.
- */
- val isCompileOnlyAnnotations: Boolean = false,
- /**
- * The set of classes that are service providers (they extend [java.security.Provider]). May be
- * empty.
- */
- val securityProviders: Set = emptySet(),
- /**
- * Android Lint registry, if there is one. May be null.
- */
- val androidLintRegistry: String? = null,
- /**
- * True if this component contains _only_ an Android Lint jar/registry. If this is true,
- * [androidLintRegistry] must be non-null.
- */
- val isLintJar: Boolean = false,
- /**
- * The classes declared by this library.
- */
- val classes: Set,
- /**
- * A map of each class declared by this library to the set of constants it defines. The latter may
- * be empty for any given declared class.
- */
- val constantFields: Map>,
- /**
- * All of the "Kt" files within this component.
- */
- val ktFiles: Set,
-) : DependencyView {
-
- internal constructor(
- artifact: PhysicalArtifact,
- exploding: ExplodingJar,
- ) : this(
- coordinates = artifact.coordinates,
- jarFile = artifact.file,
- isCompileOnlyAnnotations = exploding.isCompileOnlyCandidate,
- securityProviders = exploding.securityProviders,
- androidLintRegistry = exploding.androidLintRegistry,
- isLintJar = exploding.isLintJar,
- classes = exploding.classNames,
- constantFields = exploding.constants,
- ktFiles = exploding.ktFiles
- )
-
- override fun compareTo(other: ExplodedJar): Int {
- return coordinates.compareTo(other.coordinates).let {
- if (it == 0) jarFile.compareTo(other.jarFile) else it
- }
- }
-
- init {
- if (isLintJar && androidLintRegistry == null) {
- throw IllegalStateException("Android lint jar for $coordinates must contain a lint registry")
- }
- }
-
- override fun toCapabilities(): List {
- val capabilities = mutableListOf()
- capabilities += InferredCapability(isCompileOnlyAnnotations = isCompileOnlyAnnotations)
- classes.ifNotEmpty { capabilities += ClassCapability(it) }
- constantFields.ifNotEmpty { capabilities += ConstantCapability(it, ktFiles) }
- securityProviders.ifNotEmpty { capabilities += SecurityProviderCapability(it) }
- androidLintRegistry?.let { capabilities += AndroidLinterCapability(it, isLintJar) }
- return capabilities
- }
-}
diff --git a/src/main/kotlin/com/autonomousapps/subplugin/ProjectPlugin.kt b/src/main/kotlin/com/autonomousapps/subplugin/ProjectPlugin.kt
index ffa023e0e..c35f53fac 100644
--- a/src/main/kotlin/com/autonomousapps/subplugin/ProjectPlugin.kt
+++ b/src/main/kotlin/com/autonomousapps/subplugin/ProjectPlugin.kt
@@ -23,6 +23,7 @@ import com.autonomousapps.internal.artifacts.Publisher.Companion.interProjectPub
import com.autonomousapps.internal.utils.addAll
import com.autonomousapps.internal.utils.log
import com.autonomousapps.internal.utils.toJson
+import com.autonomousapps.model.DuplicateClass
import com.autonomousapps.model.declaration.SourceSetKind
import com.autonomousapps.model.declaration.Variant
import com.autonomousapps.services.GlobalDslService
@@ -784,7 +785,7 @@ internal class ProjectPlugin(private val project: Project) {
androidLinters.set(task.flatMap { it.output })
}
- output.set(outputPaths.allDeclaredDepsPath)
+ output.set(outputPaths.explodedJarsPath)
}
// Find the inline members of this project's dependencies.
@@ -916,7 +917,7 @@ internal class ProjectPlugin(private val project: Project) {
// Discover duplicates on compile and runtime classpaths
val duplicateClassesCompile =
tasks.register("discoverDuplicationForCompile$taskNameSuffix") {
- description("compile")
+ withClasspathName(DuplicateClass.COMPILE_CLASSPATH_NAME)
setClasspath(
configurations[dependencyAnalyzer.compileConfigurationName].artifactsFor(dependencyAnalyzer.attributeValueJar)
)
@@ -925,7 +926,7 @@ internal class ProjectPlugin(private val project: Project) {
}
val duplicateClassesRuntime =
tasks.register("discoverDuplicationForRuntime$taskNameSuffix") {
- description("runtime")
+ withClasspathName(DuplicateClass.RUNTIME_CLASSPATH_NAME)
setClasspath(
configurations[dependencyAnalyzer.runtimeConfigurationName].artifactsFor(dependencyAnalyzer.attributeValueJar)
)
@@ -948,6 +949,8 @@ internal class ProjectPlugin(private val project: Project) {
dependencies.set(synthesizeDependenciesTask.flatMap { it.outputDir })
syntheticProject.set(synthesizeProjectViewTask.flatMap { it.output })
kapt.set(isKaptApplied())
+ duplicateClassesReports.add(duplicateClassesCompile.flatMap { it.output })
+ duplicateClassesReports.add(duplicateClassesRuntime.flatMap { it.output })
output.set(outputPaths.dependencyTraceReportPath)
}
diff --git a/src/main/kotlin/com/autonomousapps/tasks/ClassListExploderTask.kt b/src/main/kotlin/com/autonomousapps/tasks/ClassListExploderTask.kt
index b73fedcdc..54517273f 100644
--- a/src/main/kotlin/com/autonomousapps/tasks/ClassListExploderTask.kt
+++ b/src/main/kotlin/com/autonomousapps/tasks/ClassListExploderTask.kt
@@ -59,7 +59,7 @@ abstract class ClassListExploderTask @Inject constructor(
val usedClasses = ClassFilesParser(
classes = parameters.classFiles.asFileTree.files,
- buildDir = parameters.buildDir.get().asFile
+ buildDir = parameters.buildDir.get().asFile,
).analyze()
output.bufferWriteJsonSet(usedClasses)
diff --git a/src/main/kotlin/com/autonomousapps/tasks/CodeSourceExploderTask.kt b/src/main/kotlin/com/autonomousapps/tasks/CodeSourceExploderTask.kt
index ad634c2fd..cb1dad5de 100644
--- a/src/main/kotlin/com/autonomousapps/tasks/CodeSourceExploderTask.kt
+++ b/src/main/kotlin/com/autonomousapps/tasks/CodeSourceExploderTask.kt
@@ -6,7 +6,7 @@ import com.autonomousapps.internal.parse.SourceListener
import com.autonomousapps.internal.utils.bufferWriteJsonSet
import com.autonomousapps.internal.utils.getAndDelete
import com.autonomousapps.model.CodeSource.Kind
-import com.autonomousapps.model.intermediates.ExplodingSourceCode
+import com.autonomousapps.model.intermediates.consumer.ExplodingSourceCode
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
diff --git a/src/main/kotlin/com/autonomousapps/tasks/ComputeUsagesTask.kt b/src/main/kotlin/com/autonomousapps/tasks/ComputeUsagesTask.kt
index bbe8cc233..15071a5e1 100644
--- a/src/main/kotlin/com/autonomousapps/tasks/ComputeUsagesTask.kt
+++ b/src/main/kotlin/com/autonomousapps/tasks/ComputeUsagesTask.kt
@@ -9,11 +9,15 @@ import com.autonomousapps.model.declaration.Declaration
import com.autonomousapps.model.intermediates.DependencyTraceReport
import com.autonomousapps.model.intermediates.DependencyTraceReport.Kind
import com.autonomousapps.model.intermediates.Reason
+import com.autonomousapps.model.intermediates.consumer.MemberAccess
+import com.autonomousapps.model.intermediates.producer.BinaryClass
import com.autonomousapps.visitor.GraphViewReader
import com.autonomousapps.visitor.GraphViewVisitor
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.RegularFile
import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
import org.gradle.workers.WorkAction
@@ -49,6 +53,10 @@ abstract class ComputeUsagesTask @Inject constructor(
@get:Input
abstract val kapt: Property
+ @get:PathSensitive(PathSensitivity.RELATIVE)
+ @get:InputFiles
+ abstract val duplicateClassesReports: ListProperty
+
@get:OutputFile
abstract val output: RegularFileProperty
@@ -59,6 +67,7 @@ abstract class ComputeUsagesTask @Inject constructor(
dependencies.set(this@ComputeUsagesTask.dependencies)
syntheticProject.set(this@ComputeUsagesTask.syntheticProject)
kapt.set(this@ComputeUsagesTask.kapt)
+ duplicateClassesReports.set(this@ComputeUsagesTask.duplicateClassesReports)
output.set(this@ComputeUsagesTask.output)
}
}
@@ -69,6 +78,7 @@ abstract class ComputeUsagesTask @Inject constructor(
val dependencies: DirectoryProperty
val syntheticProject: RegularFileProperty
val kapt: Property
+ val duplicateClassesReports: ListProperty
val output: RegularFileProperty
}
@@ -78,6 +88,10 @@ abstract class ComputeUsagesTask @Inject constructor(
private val declarations = parameters.declarations.fromJsonSet()
private val project = parameters.syntheticProject.fromJson()
private val dependencies = project.dependencies(parameters.dependencies.get())
+ private val duplicateClasses = parameters.duplicateClassesReports.get().asSequence()
+ .map { it.fromJsonSet() }
+ .flatten()
+ .toSortedSet()
override fun execute() {
val output = parameters.output.getAndDelete()
@@ -86,7 +100,8 @@ abstract class ComputeUsagesTask @Inject constructor(
project = project,
dependencies = dependencies,
graph = graph,
- declarations = declarations
+ declarations = declarations,
+ duplicateClasses = duplicateClasses,
)
val visitor = GraphVisitor(project, parameters.kapt.get())
reader.accept(visitor)
@@ -154,6 +169,10 @@ private class GraphVisitor(
isAnnotationProcessorCandidate = usesAnnotationProcessor(dependencyCoordinates, capability, context)
}
+ is BinaryClassCapability -> {
+ checkBinaryCompatibility(dependencyCoordinates, capability, context)
+ }
+
is ClassCapability -> {
// We want to track this in addition to tracking one of the below, so it's not part of the same if/else-if
// chain.
@@ -377,6 +396,101 @@ private class GraphVisitor(
}
}
+ private fun checkBinaryCompatibility(
+ coordinates: Coordinates,
+ binaryClassCapability: BinaryClassCapability,
+ context: GraphViewVisitor.Context,
+ ) {
+ // Can't be incompatible if the code compiles in the context of no duplication
+ if (context.duplicateClasses.isEmpty()) return
+
+ // TODO(tsr): special handling for @Composable
+ val memberAccessOwners = context.project.memberAccesses.mapToSet { it.owner }
+ val relevantDuplicates = context.duplicateClasses
+ .filter { duplicate -> coordinates in duplicate.dependencies && duplicate.className in memberAccessOwners }
+ .filter { duplicate -> duplicate.classpathName == DuplicateClass.COMPILE_CLASSPATH_NAME }
+
+ // Can't be incompatible if the code compiles in the context of no relevant duplication
+ if (relevantDuplicates.isEmpty()) return
+
+ val relevantDuplicateClassNames = relevantDuplicates.mapToOrderedSet { it.className }
+ val relevantMemberAccesses = context.project.memberAccesses
+ .filterToOrderedSet { access -> access.owner in relevantDuplicateClassNames }
+
+ val partitionResult = relevantMemberAccesses.mapToSet { access ->
+ binaryClassCapability.findMatchingClasses(access)
+ }.reduce()
+ val matchingBinaryClasses = partitionResult.matchingClasses
+ val nonMatchingBinaryClasses = partitionResult.nonMatchingClasses
+
+ // There must be a compatible BinaryClass. for each MemberAccess for the usage to be binary-compatible
+ val isBinaryCompatible = relevantMemberAccesses.all { access ->
+ when (access) {
+ is MemberAccess.Field -> {
+ matchingBinaryClasses.any { bin ->
+ bin.effectivelyPublicFields.any { field ->
+ field.matches(access)
+ }
+ }
+ }
+
+ is MemberAccess.Method -> {
+ matchingBinaryClasses.any { bin ->
+ bin.effectivelyPublicMethods.any { method ->
+ method.matches(access)
+ }
+ }
+ }
+ }
+ }
+
+ if (!isBinaryCompatible) {
+ reportBuilder[coordinates, Kind.DEPENDENCY] = Reason.BinaryIncompatible(
+ relevantMemberAccesses, nonMatchingBinaryClasses
+ )
+ }
+ }
+
+ // TODO: I think this could be more efficient
+ private fun Set.reduce(): BinaryClassCapability.PartitionResult {
+ val matches = sortedSetOf()
+ val nonMatches = sortedSetOf()
+
+ forEach { result ->
+ matches.addAll(result.matchingClasses)
+ nonMatches.addAll(result.nonMatchingClasses)
+ }
+
+ return BinaryClassCapability.PartitionResult(
+ matchingClasses = matches.reduce(),
+ nonMatchingClasses = nonMatches.reduce(),
+ )
+ }
+
+ private fun Set.reduce(): Set {
+ val builders = mutableMapOf()
+
+ forEach { bin ->
+ builders.merge(
+ bin.className,
+ BinaryClass.Builder(
+ className = bin.className,
+ superClassName = bin.superClassName,
+ interfaces = bin.interfaces.toSortedSet(),
+ effectivelyPublicFields = bin.effectivelyPublicFields.toSortedSet(),
+ effectivelyPublicMethods = bin.effectivelyPublicMethods.toSortedSet(),
+ )
+ ) { acc, inc ->
+ acc.apply {
+ effectivelyPublicFields.addAll(inc.effectivelyPublicFields)
+ effectivelyPublicMethods.addAll(inc.effectivelyPublicMethods)
+ }
+ }
+ }
+
+ return builders.values.mapToOrderedSet { it.build() }
+ }
+
private fun isImplementation(
coordinates: Coordinates,
typealiasCapability: TypealiasCapability,
diff --git a/src/main/kotlin/com/autonomousapps/tasks/DiscoverClasspathDuplicationTask.kt b/src/main/kotlin/com/autonomousapps/tasks/DiscoverClasspathDuplicationTask.kt
index 0375b8ce7..4f464aef7 100644
--- a/src/main/kotlin/com/autonomousapps/tasks/DiscoverClasspathDuplicationTask.kt
+++ b/src/main/kotlin/com/autonomousapps/tasks/DiscoverClasspathDuplicationTask.kt
@@ -23,7 +23,7 @@ abstract class DiscoverClasspathDuplicationTask : DefaultTask() {
group = TASK_GROUP_DEP
}
- internal fun description(name: String) {
+ internal fun withClasspathName(name: String) {
description = "Discovers duplicates on the $name classpath"
classpathName.set(name)
}
@@ -85,7 +85,8 @@ abstract class DiscoverClasspathDuplicationTask : DefaultTask() {
DuplicateClass(
variant = project.variant,
classpathName = classpathName,
- classReference = classReference,
+ // java/lang/String.class -> java/lang/String
+ className = classReference.removeSuffix(".class"),
dependencies = dependency.toSortedSet(),
)
}
@@ -96,7 +97,7 @@ abstract class DiscoverClasspathDuplicationTask : DefaultTask() {
val coordinates = artifact.toCoordinates()
- // Create multimap of classname to [dependencies]
+ // Create multimap of class name to [dependencies]
zip.asSequenceOfClassFiles()
.map(ZipEntry::getName)
.forEach { duplicatesMap.put(it, coordinates) }
diff --git a/src/main/kotlin/com/autonomousapps/tasks/SynthesizeDependenciesTask.kt b/src/main/kotlin/com/autonomousapps/tasks/SynthesizeDependenciesTask.kt
index 7e8bc827c..ed5bb6b1d 100644
--- a/src/main/kotlin/com/autonomousapps/tasks/SynthesizeDependenciesTask.kt
+++ b/src/main/kotlin/com/autonomousapps/tasks/SynthesizeDependenciesTask.kt
@@ -8,6 +8,7 @@ import com.autonomousapps.internal.utils.fromJsonSet
import com.autonomousapps.internal.utils.fromNullableJsonSet
import com.autonomousapps.model.*
import com.autonomousapps.model.intermediates.*
+import com.autonomousapps.model.intermediates.producer.ExplodedJar
import com.autonomousapps.services.InMemoryCache
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
diff --git a/src/main/kotlin/com/autonomousapps/tasks/SynthesizeProjectViewTask.kt b/src/main/kotlin/com/autonomousapps/tasks/SynthesizeProjectViewTask.kt
index b46ca1fa7..5671bbfd1 100644
--- a/src/main/kotlin/com/autonomousapps/tasks/SynthesizeProjectViewTask.kt
+++ b/src/main/kotlin/com/autonomousapps/tasks/SynthesizeProjectViewTask.kt
@@ -8,9 +8,10 @@ import com.autonomousapps.model.*
import com.autonomousapps.model.declaration.SourceSetKind
import com.autonomousapps.model.declaration.Variant
import com.autonomousapps.model.intermediates.AnnotationProcessorDependency
-import com.autonomousapps.model.intermediates.ExplodingAbi
-import com.autonomousapps.model.intermediates.ExplodingBytecode
-import com.autonomousapps.model.intermediates.ExplodingSourceCode
+import com.autonomousapps.model.intermediates.consumer.ExplodingAbi
+import com.autonomousapps.model.intermediates.consumer.ExplodingBytecode
+import com.autonomousapps.model.intermediates.consumer.ExplodingSourceCode
+import com.autonomousapps.model.intermediates.consumer.MemberAccess
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
@@ -172,6 +173,14 @@ abstract class SynthesizeProjectViewTask @Inject constructor(
nonAnnotationClasses.addAll(bytecode.nonAnnotationClasses)
annotationClasses.addAll(bytecode.annotationClasses)
invisibleAnnotationClasses.addAll(bytecode.invisibleAnnotationClasses)
+
+ // TODO(tsr): flatten into a single set? Do we need the map?
+ // Merge the two maps
+ bytecode.binaryClassAccesses.forEach { (className, memberAccesses) ->
+ binaryClassAccesses.merge(className, memberAccesses.toMutableSet()) { acc, inc ->
+ acc.apply { addAll(inc) }
+ }
+ }
},
CodeSourceBuilder::concat
)
@@ -259,6 +268,7 @@ private class CodeSourceBuilder(val className: String) {
val invisibleAnnotationClasses = mutableSetOf()
val exposedClasses = mutableSetOf()
val imports = mutableSetOf()
+ val binaryClassAccesses = mutableMapOf>()
fun concat(other: CodeSourceBuilder): CodeSourceBuilder {
nonAnnotationClasses.addAll(other.nonAnnotationClasses)
@@ -282,6 +292,7 @@ private class CodeSourceBuilder(val className: String) {
usedInvisibleAnnotationClasses = invisibleAnnotationClasses,
exposedClasses = exposedClasses,
imports = imports,
+ binaryClassAccesses = binaryClassAccesses,
)
}
}
diff --git a/src/main/kotlin/com/autonomousapps/visitor/GraphViewReader.kt b/src/main/kotlin/com/autonomousapps/visitor/GraphViewReader.kt
index ea94f704f..036388569 100644
--- a/src/main/kotlin/com/autonomousapps/visitor/GraphViewReader.kt
+++ b/src/main/kotlin/com/autonomousapps/visitor/GraphViewReader.kt
@@ -4,6 +4,7 @@ package com.autonomousapps.visitor
import com.autonomousapps.model.Dependency
import com.autonomousapps.model.DependencyGraphView
+import com.autonomousapps.model.DuplicateClass
import com.autonomousapps.model.ProjectVariant
import com.autonomousapps.model.declaration.Declaration
@@ -12,10 +13,11 @@ internal class GraphViewReader(
private val dependencies: Set,
private val graph: DependencyGraphView,
private val declarations: Set,
+ private val duplicateClasses: Set,
) {
fun accept(visitor: GraphViewVisitor) {
- val context = DefaultContext(project, dependencies, graph, declarations)
+ val context = DefaultContext(project, dependencies, graph, declarations, duplicateClasses)
dependencies.forEach { dependency ->
visitor.visit(dependency, context)
}
@@ -27,4 +29,5 @@ internal class DefaultContext(
override val dependencies: Set,
override val graph: DependencyGraphView,
override val declarations: Set,
+ override val duplicateClasses: Set,
) : GraphViewVisitor.Context
diff --git a/src/main/kotlin/com/autonomousapps/visitor/GraphViewVisitor.kt b/src/main/kotlin/com/autonomousapps/visitor/GraphViewVisitor.kt
index 9ce4ae836..c02eb2e9c 100644
--- a/src/main/kotlin/com/autonomousapps/visitor/GraphViewVisitor.kt
+++ b/src/main/kotlin/com/autonomousapps/visitor/GraphViewVisitor.kt
@@ -4,6 +4,7 @@ package com.autonomousapps.visitor
import com.autonomousapps.model.Dependency
import com.autonomousapps.model.DependencyGraphView
+import com.autonomousapps.model.DuplicateClass
import com.autonomousapps.model.ProjectVariant
import com.autonomousapps.model.declaration.Declaration
@@ -15,5 +16,6 @@ internal interface GraphViewVisitor {
val dependencies: Set
val graph: DependencyGraphView
val declarations: Set
+ val duplicateClasses: Set
}
}