diff --git a/src/main/kotlin/com/autonomousapps/extension/AbiHandler.kt b/src/main/kotlin/com/autonomousapps/extension/AbiHandler.kt index c3498b25f..819e16434 100644 --- a/src/main/kotlin/com/autonomousapps/extension/AbiHandler.kt +++ b/src/main/kotlin/com/autonomousapps/extension/AbiHandler.kt @@ -97,8 +97,11 @@ public abstract class ExclusionsHandler @Inject constructor(objects: ObjectFacto annotationExclusions.addAll(*annotationRegexes) } - // TODO Excluded for now but left as a toe-hold for future use -// fun excludePaths(@Language("RegExp") vararg pathRegexes: String) { -// pathExclusions.addAll(*pathRegexes) -// } + /** + * Exclude any class from ABI analysis that has a source path that matches [pathRegexes], which means + * those classes will not be considered as part of the module's ABI. + */ + public fun excludePaths(@Language("RegExp") vararg pathRegexes: String) { + pathExclusions.addAll(*pathRegexes) + } } diff --git a/src/main/kotlin/com/autonomousapps/internal/analyzer/AndroidProjectAnalyzer.kt b/src/main/kotlin/com/autonomousapps/internal/analyzer/AndroidProjectAnalyzer.kt index fa06e60c3..b41aade1d 100644 --- a/src/main/kotlin/com/autonomousapps/internal/analyzer/AndroidProjectAnalyzer.kt +++ b/src/main/kotlin/com/autonomousapps/internal/analyzer/AndroidProjectAnalyzer.kt @@ -194,6 +194,10 @@ internal class AndroidLibAnalyzer( if (!hasAbi) return null return project.tasks.register("abiAnalysis$taskNameSuffix") { + sourceFiles.setFrom(javaSourceFiles) + sourceFiles.setFrom(kotlinSourceFiles) + sourceFiles.setFrom(groovySourceFiles) + sourceFiles.setFrom(scalaSourceFiles) exclusions.set(abiExclusions) output.set(outputPaths.abiAnalysisPath) abiDump.set(outputPaths.abiDumpPath) diff --git a/src/main/kotlin/com/autonomousapps/internal/analyzer/JvmProjectAnalyzer.kt b/src/main/kotlin/com/autonomousapps/internal/analyzer/JvmProjectAnalyzer.kt index c60fcd1f9..c8aa946ea 100644 --- a/src/main/kotlin/com/autonomousapps/internal/analyzer/JvmProjectAnalyzer.kt +++ b/src/main/kotlin/com/autonomousapps/internal/analyzer/JvmProjectAnalyzer.kt @@ -63,6 +63,7 @@ internal abstract class JvmAnalyzer( if (!hasAbi) return null return project.tasks.register("abiAnalysis$taskNameSuffix") { + sourceFiles.setFrom(sourceSet.sourceCode) classes.setFrom(sourceSet.classesDirs) exclusions.set(abiExclusions) output.set(outputPaths.abiAnalysisPath) diff --git a/src/main/kotlin/com/autonomousapps/internal/kotlin/PublicApiDump.kt b/src/main/kotlin/com/autonomousapps/internal/kotlin/PublicApiDump.kt index 716fd8431..e56814f67 100644 --- a/src/main/kotlin/com/autonomousapps/internal/kotlin/PublicApiDump.kt +++ b/src/main/kotlin/com/autonomousapps/internal/kotlin/PublicApiDump.kt @@ -34,16 +34,18 @@ internal fun JarFile.classEntries() = Sequence { entries().iterator() }.filter { } internal fun getBinaryAPI(jar: JarFile, visibilityFilter: (String) -> Boolean = { true }): List = - getBinaryAPI(jar.classEntries().map { entry -> jar.getInputStream(entry) }, visibilityFilter) + getBinaryAPI(jar.classEntries().map { entry -> jar.getInputStream(entry) }, visibilityFilter = visibilityFilter) internal fun getBinaryAPI( classes: Set, + sourceFiles: Set, visibilityFilter: (String) -> Boolean = { true } ): List = - getBinaryAPI(classes.asSequence().map { it.inputStream() }, visibilityFilter) + getBinaryAPI(classes.asSequence().map { it.inputStream() }, sourceFiles, visibilityFilter) internal fun getBinaryAPI( classStreams: Sequence, + sourceFiles: Set = emptySet(), visibilityFilter: (String) -> Boolean = { true } ): List { val classNodes = classStreams.map { @@ -59,6 +61,10 @@ internal fun getBinaryAPI( val visibilityMapNew = classNodes.readKotlinVisibilities().filterKeys(visibilityFilter) + val sourceFilePackageReversedBySourceFile = sourceFiles.associateWith { + it.parentFile.invariantSeparatorsPath.split('/').reversed() + } + return classNodes .filter { it != moduleInfo } .map { clazz -> @@ -118,6 +124,21 @@ internal fun getBinaryAPI( // Strip out JDK classes .filterNotToSet { it.startsWith("Ljava/lang") } + val sourceFileName = clazz.sourceFile ?: "${clazz.name.substringAfterLast('/')}." + val clazzPackageReversed = clazz.name.substringBeforeLast('/').split('/').reversed() + val sourceFile = sourceFilePackageReversedBySourceFile + .filterKeys { it.name.startsWith(sourceFileName) } + .maxByOrNull { (_, sourceFilePackageReversed) -> + sourceFilePackageReversed + .asSequence() + .zip(clazzPackageReversed.asSequence()) + .takeWhile { (sourceFilePart, clazzPart) -> sourceFilePart == clazzPart } + .count() + } + ?.key + ?.invariantSeparatorsPath + ?: sourceFileName + ClassBinarySignature( name = name, superName = superName, @@ -130,7 +151,7 @@ internal fun getBinaryAPI( isNotUsedWhenEmpty = metadata.isFileOrMultipartFacade() || isDefaultImpls(metadata), annotations = visibleAnnotations.annotationTypes(), invisibleAnnotations = invisibleAnnotations.annotationTypes(), - sourceFile = clazz.sourceFile + sourceFile = sourceFile ) } } diff --git a/src/main/kotlin/com/autonomousapps/internal/kotlin/abiDependencies.kt b/src/main/kotlin/com/autonomousapps/internal/kotlin/abiDependencies.kt index 641a3071c..6030eeb7f 100644 --- a/src/main/kotlin/com/autonomousapps/internal/kotlin/abiDependencies.kt +++ b/src/main/kotlin/com/autonomousapps/internal/kotlin/abiDependencies.kt @@ -12,9 +12,10 @@ import java.io.File internal fun computeAbi( classFiles: Set, + sourceFiles: Set, exclusions: AbiExclusions, abiDumpFile: File? = null -): Set = getBinaryAPI(classFiles).explodedAbi(exclusions, abiDumpFile) +): Set = getBinaryAPI(classFiles, sourceFiles).explodedAbi(exclusions, abiDumpFile) private fun List.explodedAbi( exclusions: AbiExclusions, diff --git a/src/main/kotlin/com/autonomousapps/tasks/AbiAnalysisTask.kt b/src/main/kotlin/com/autonomousapps/tasks/AbiAnalysisTask.kt index af88feaa5..d922f1187 100644 --- a/src/main/kotlin/com/autonomousapps/tasks/AbiAnalysisTask.kt +++ b/src/main/kotlin/com/autonomousapps/tasks/AbiAnalysisTask.kt @@ -30,6 +30,11 @@ public abstract class AbiAnalysisTask @Inject constructor( description = "Produces a report of the ABI of this project" } + /** Source files from which the class files were generated. May be empty. */ + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + public abstract val sourceFiles: ConfigurableFileCollection + /** Class files generated by any JVM source (Java, Kotlin, Groovy, etc.). May be empty. */ @get:Classpath @get:InputFiles @@ -48,6 +53,7 @@ public abstract class AbiAnalysisTask @Inject constructor( @TaskAction public fun action() { workerExecutor.noIsolation().submit(AbiAnalysisWorkAction::class.java) { + sourceFiles.setFrom(this@AbiAnalysisTask.sourceFiles.asFileTree) // JVM projects classFiles.setFrom(classes.asFileTree.filterToClassFiles().files) // Android projects @@ -60,6 +66,7 @@ public abstract class AbiAnalysisTask @Inject constructor( } public interface AbiAnalysisParameters : WorkParameters { + public val sourceFiles: ConfigurableFileCollection public val classFiles: ConfigurableFileCollection public val exclusions: Property public val output: RegularFileProperty @@ -72,10 +79,11 @@ public abstract class AbiAnalysisTask @Inject constructor( val output = parameters.output.getAndDelete() val outputAbiDump = parameters.abiDump.getAndDelete() + val sourceFiles = parameters.sourceFiles.files val classFiles = parameters.classFiles.files val exclusions = parameters.exclusions.orNull?.fromJson() ?: AbiExclusions.NONE - val explodingAbi = computeAbi(classFiles, exclusions, outputAbiDump) + val explodingAbi = computeAbi(classFiles, sourceFiles, exclusions, outputAbiDump) output.bufferWriteJsonSet(explodingAbi) }