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 } }