From feb9ebb3506de251f8a8eed640c4199d8d7c5fb9 Mon Sep 17 00:00:00 2001 From: Oleg Golberg Date: Mon, 12 Feb 2024 12:09:21 -0500 Subject: [PATCH 1/4] roots impl --- .../com/toasttab/expediter/Expediter.kt | 2 +- .../toasttab/expediter/parser/TypeParsers.kt | 7 ++-- .../ClasspathApplicationTypesProvider.kt | 3 +- .../expediter/scanner/ClasspathScanner.kt | 32 ++++++++--------- .../expediter/types/InspectedTypes.kt | 35 +++++++++++++++++-- .../com/toasttab/expediter/types/Type.kt | 2 +- .../toasttab/expediter/types/TypeSource.kt | 16 +++++++++ .../toasttab/expediter/gradle/Artifacts.kt | 20 +++++++++++ .../expediter/gradle/ExpediterTask.kt | 25 ++++++++++--- .../gradle/config/ApplicationSpec.kt | 16 +++++++-- .../gradle/config/ExpediterExtension.kt | 2 +- .../expediter/gradle/config/RootSpec.kt | 14 ++++++++ .../gradle/service/ApplicationTypeCache.kt | 8 +++-- .../gradle/ExpediterPluginIntegrationTest.kt | 8 +++-- .../cross library/src/main/java/Caller.java | 10 ++++++ .../test/ExpediterIntegrationTest.kt | 4 ++- 16 files changed, 169 insertions(+), 35 deletions(-) create mode 100644 model/src/main/kotlin/com/toasttab/expediter/types/TypeSource.kt create mode 100644 plugin/src/main/kotlin/com/toasttab/expediter/gradle/Artifacts.kt create mode 100644 plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/RootSpec.kt create mode 100644 plugin/src/test/projects/ExpediterPluginIntegrationTest/cross library/src/main/java/Caller.java diff --git a/core/src/main/kotlin/com/toasttab/expediter/Expediter.kt b/core/src/main/kotlin/com/toasttab/expediter/Expediter.kt index 99bb1f0..d3caff0 100644 --- a/core/src/main/kotlin/com/toasttab/expediter/Expediter.kt +++ b/core/src/main/kotlin/com/toasttab/expediter/Expediter.kt @@ -119,7 +119,7 @@ class Expediter( fun findIssues(): Set { return ( - inspectedTypes.classes.flatMap { appType -> + inspectedTypes.reachableClasses().flatMap { appType -> findIssues(appType) } + inspectedTypes.duplicateTypes ).filter { !ignore.ignore(it) } diff --git a/core/src/main/kotlin/com/toasttab/expediter/parser/TypeParsers.kt b/core/src/main/kotlin/com/toasttab/expediter/parser/TypeParsers.kt index f6cbd9b..8d643b7 100644 --- a/core/src/main/kotlin/com/toasttab/expediter/parser/TypeParsers.kt +++ b/core/src/main/kotlin/com/toasttab/expediter/parser/TypeParsers.kt @@ -16,6 +16,7 @@ package com.toasttab.expediter.parser import com.toasttab.expediter.types.ApplicationType +import com.toasttab.expediter.types.TypeSource import com.toasttab.expediter.types.FieldAccessType import com.toasttab.expediter.types.MemberAccess import com.toasttab.expediter.types.MemberSymbolicReference @@ -35,7 +36,7 @@ import protokt.v1.toasttab.expediter.v1.TypeDescriptor import java.io.InputStream object TypeParsers { - fun applicationType(stream: InputStream, source: String) = ApplicationTypeParser(source).apply { + fun applicationType(stream: InputStream, source: TypeSource) = ApplicationTypeParser(source).apply { ClassReader(stream).accept(this, SKIP_DEBUG) }.get() @@ -44,7 +45,7 @@ object TypeParsers { }.get() } -private class ApplicationTypeParser(private val source: String) : ClassVisitor(ASM9, TypeDescriptorParser()) { +private class ApplicationTypeParser(private val source: TypeSource) : ClassVisitor(ASM9, TypeDescriptorParser()) { private val refs: MutableSet> = hashSetOf() private val referencedTypes: MutableSet = hashSetOf() @@ -99,6 +100,7 @@ private class ApplicationTypeParser(private val source: String) : ClassVisitor(A ) referencedTypes.addAll(SignatureParser.parseMethod(descriptor).referencedTypes()) + referencedTypes.add(owner) } override fun visitFieldInsn(opcode: Int, owner: String, name: String, descriptor: String) { @@ -118,6 +120,7 @@ private class ApplicationTypeParser(private val source: String) : ClassVisitor(A ) referencedTypes.addAll(SignatureParser.parseType(descriptor).referencedTypes()) + referencedTypes.add(owner) } override fun visitInvokeDynamicInsn( diff --git a/core/src/main/kotlin/com/toasttab/expediter/provider/ClasspathApplicationTypesProvider.kt b/core/src/main/kotlin/com/toasttab/expediter/provider/ClasspathApplicationTypesProvider.kt index 1abc949..cf177ad 100644 --- a/core/src/main/kotlin/com/toasttab/expediter/provider/ClasspathApplicationTypesProvider.kt +++ b/core/src/main/kotlin/com/toasttab/expediter/provider/ClasspathApplicationTypesProvider.kt @@ -17,10 +17,11 @@ package com.toasttab.expediter.provider import com.toasttab.expediter.parser.TypeParsers import com.toasttab.expediter.scanner.ClasspathScanner +import com.toasttab.expediter.types.TypeSource import java.io.File class ClasspathApplicationTypesProvider( - elements: Iterable + elements: Iterable ) : ApplicationTypesProvider { private val scanner = ClasspathScanner(elements) diff --git a/core/src/main/kotlin/com/toasttab/expediter/scanner/ClasspathScanner.kt b/core/src/main/kotlin/com/toasttab/expediter/scanner/ClasspathScanner.kt index 329a5f2..e83ae85 100644 --- a/core/src/main/kotlin/com/toasttab/expediter/scanner/ClasspathScanner.kt +++ b/core/src/main/kotlin/com/toasttab/expediter/scanner/ClasspathScanner.kt @@ -15,7 +15,7 @@ package com.toasttab.expediter.scanner -import java.io.File +import com.toasttab.expediter.types.TypeSource import java.io.InputStream import java.lang.Exception import java.lang.RuntimeException @@ -23,16 +23,16 @@ import java.util.jar.JarInputStream import java.util.zip.ZipFile class ClasspathScanner( - private val elements: Iterable + private val elements: Iterable ) { - fun scan(parse: (stream: InputStream, source: String) -> T): List = elements.flatMap { types(it, parse) } + fun scan(parse: (stream: InputStream, source: TypeSource) -> T): List = elements.flatMap { types(it, parse) } private fun isClassFile(name: String) = name.endsWith(".class") && !name.startsWith("META-INF/versions") && // mrjars not supported yet !name.endsWith("package-info.class") && !name.endsWith("module-info.class") - private fun scanJarStream(stream: JarInputStream, source: String, parse: (stream: InputStream, source: String) -> T) = generateSequence { stream.nextJarEntry } + private fun scanJarStream(stream: JarInputStream, source: TypeSource, parse: (stream: InputStream, source: TypeSource) -> T) = generateSequence { stream.nextJarEntry } .filter { isClassFile(it.name) } .map { try { parse(stream, source) } catch (e: Exception) { @@ -41,34 +41,34 @@ class ClasspathScanner( } .toList() - fun scanJar(path: File, parse: (stream: InputStream, source: String) -> T): List = path.inputStream().use { - scanJarStream(JarInputStream(it), path.name, parse) + fun scanJar(source: TypeSource, parse: (stream: InputStream, source: TypeSource) -> T): List = source.file.inputStream().use { + scanJarStream(JarInputStream(it), source, parse) } - fun scanAar(path: File, parse: (stream: InputStream, source: String) -> T): List = - ZipFile(path).use { aar -> + fun scanAar(source: TypeSource, parse: (stream: InputStream, source: TypeSource) -> T): List = + ZipFile(source.file).use { aar -> when (val classesEntry = aar.getEntry("classes.jar")) { null -> emptyList() else -> { JarInputStream(aar.getInputStream(classesEntry)).use { - scanJarStream(it, path.name, parse) + scanJarStream(it, source, parse) } } } } - fun scanClassDir(path: File, parse: (stream: InputStream, source: String) -> T): List = - path.walkTopDown().filter { isClassFile(it.name) }.map { + fun scanClassDir(source: TypeSource, parse: (stream: InputStream, source: TypeSource) -> T): List = + source.file.walkTopDown().filter { isClassFile(it.name) }.map { it.inputStream().use { classStream -> - parse(classStream, path.name) + parse(classStream, source) } }.toList() - private fun types(path: File, parse: (stream: InputStream, source: String) -> T) = + private fun types(source: TypeSource, parse: (stream: InputStream, source: TypeSource) -> T) = when { - path.isDirectory -> scanClassDir(path, parse) - path.name.endsWith(".jar") -> scanJar(path, parse) - path.name.endsWith(".aar") -> scanAar(path, parse) + source.file.isDirectory -> scanClassDir(source, parse) + source.file.name.endsWith(".jar") -> scanJar(source, parse) + source.file.name.endsWith(".aar") -> scanAar(source, parse) else -> emptyList() } } diff --git a/core/src/main/kotlin/com/toasttab/expediter/types/InspectedTypes.kt b/core/src/main/kotlin/com/toasttab/expediter/types/InspectedTypes.kt index 73a7e39..6613060 100644 --- a/core/src/main/kotlin/com/toasttab/expediter/types/InspectedTypes.kt +++ b/core/src/main/kotlin/com/toasttab/expediter/types/InspectedTypes.kt @@ -19,6 +19,7 @@ import com.toasttab.expediter.issue.Issue import com.toasttab.expediter.parser.SignatureParser import com.toasttab.expediter.parser.TypeSignature import com.toasttab.expediter.provider.PlatformTypeProvider +import java.util.LinkedList import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentMap @@ -28,7 +29,7 @@ class ApplicationTypeContainer( ) { companion object { fun create(all: List): ApplicationTypeContainer { - val duplicates = HashMap>() + val duplicates = HashMap>() val types = HashMap() for (type in all) { @@ -48,7 +49,7 @@ class ApplicationTypeContainer( } } - return ApplicationTypeContainer(types, duplicates.map { (k, v) -> Issue.DuplicateType(k, v) }) + return ApplicationTypeContainer(types, duplicates.map { (k, v) -> Issue.DuplicateType(k, v.map { it.file.name }) }) } } } @@ -116,5 +117,35 @@ class InspectedTypes( } val classes: Collection get() = appTypes.appTypes.values + + fun reachableClasses(): Collection { + val reachable = hashMapOf() + + val todo = LinkedList(appTypes.appTypes.values.filter { + it.source.type == SourceType.SOURCE_SET || it.source.type == SourceType.PROJECT_DEPENDENCY + }) + + while (todo.isNotEmpty()) { + val next = todo.remove() + + if (reachable.put(next.name, next) == null) { + for (ref in next.referencedTypes) { + val refType = appTypes.appTypes[ref] + if (refType != null) { + todo.add(refType) + } + } + + val hierarchy = resolveHierarchy(next) + + if (hierarchy is ResolvedTypeHierarchy.CompleteTypeHierarchy) { + todo.addAll(hierarchy.superTypes.filterIsInstance()) + } + } + } + + return reachable.values + } + val duplicateTypes: Collection get() = appTypes.duplicates } diff --git a/model/src/main/kotlin/com/toasttab/expediter/types/Type.kt b/model/src/main/kotlin/com/toasttab/expediter/types/Type.kt index 029ddc2..601c9af 100644 --- a/model/src/main/kotlin/com/toasttab/expediter/types/Type.kt +++ b/model/src/main/kotlin/com/toasttab/expediter/types/Type.kt @@ -35,7 +35,7 @@ class ApplicationType( override val descriptor: TypeDescriptor, val memberAccess: Set>, val referencedTypes: Set, - val source: String + val source: TypeSource ) : Type { override fun toString() = "ApplicationType[$name]" } diff --git a/model/src/main/kotlin/com/toasttab/expediter/types/TypeSource.kt b/model/src/main/kotlin/com/toasttab/expediter/types/TypeSource.kt new file mode 100644 index 0000000..02bf25c --- /dev/null +++ b/model/src/main/kotlin/com/toasttab/expediter/types/TypeSource.kt @@ -0,0 +1,16 @@ +package com.toasttab.expediter.types + +import java.io.File + +data class TypeSource ( + val file: File, + val type: SourceType, + val name: String +) + +enum class SourceType { + UNKNOWN, + SOURCE_SET, + PROJECT_DEPENDENCY, + DEPENDENCY +} \ No newline at end of file diff --git a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/Artifacts.kt b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/Artifacts.kt new file mode 100644 index 0000000..3864ffb --- /dev/null +++ b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/Artifacts.kt @@ -0,0 +1,20 @@ +package com.toasttab.expediter.gradle + +import com.toasttab.expediter.types.SourceType +import com.toasttab.expediter.types.TypeSource +import org.gradle.api.artifacts.component.ModuleComponentIdentifier +import org.gradle.api.artifacts.component.ProjectComponentIdentifier +import org.gradle.api.artifacts.result.ResolvedArtifactResult +import org.gradle.api.file.SourceDirectorySet +import java.io.File + +fun ResolvedArtifactResult.source() = + when (val cid = id.componentIdentifier) { + is ModuleComponentIdentifier -> TypeSource(file, SourceType.DEPENDENCY, cid.displayName) + is ProjectComponentIdentifier -> TypeSource(file, SourceType.PROJECT_DEPENDENCY, cid.projectPath) + else -> TypeSource(file, SourceType.UNKNOWN, cid.displayName) + } + +fun File.source() = TypeSource(this, SourceType.UNKNOWN, name) + +fun SourceDirectorySet.source() = TypeSource(classesDirectory.get().asFile, SourceType.SOURCE_SET, name) \ No newline at end of file diff --git a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/ExpediterTask.kt b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/ExpediterTask.kt index 9988044..a93d419 100644 --- a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/ExpediterTask.kt +++ b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/ExpediterTask.kt @@ -32,6 +32,7 @@ import org.gradle.api.GradleException import org.gradle.api.artifacts.ArtifactCollection import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.FileCollection +import org.gradle.api.file.SourceDirectorySet import org.gradle.api.provider.Property import org.gradle.api.tasks.CacheableTask import org.gradle.api.tasks.Input @@ -41,6 +42,7 @@ import org.gradle.api.tasks.Optional import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.SourceSet import org.gradle.api.tasks.TaskAction import protokt.v1.toasttab.expediter.v1.TypeDescriptors import java.io.File @@ -50,16 +52,19 @@ import java.util.zip.GZIPInputStream abstract class ExpediterTask : DefaultTask() { private val applicationConfigurationArtifacts = mutableListOf() private val platformConfigurationArtifacts = mutableListOf() + private val applicationSourceSets: MutableSet = mutableSetOf() @get:Internal abstract val cache: Property @get:InputFiles @get:PathSensitive(PathSensitivity.ABSOLUTE) + @Suppress("UNUSED") val applicationArtifacts get() = applicationConfigurationArtifacts.asFileCollection() @get:InputFiles @get:PathSensitive(PathSensitivity.ABSOLUTE) + @Suppress("UNUSED") val platformArtifacts get() = platformConfigurationArtifacts.asFileCollection() private fun Collection.asFileCollection() = if (isEmpty()) { @@ -72,6 +77,12 @@ abstract class ExpediterTask : DefaultTask() { @get:PathSensitive(PathSensitivity.ABSOLUTE) abstract val files: ConfigurableFileCollection + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + val sourceSets: FileCollection get() = project.objects.fileCollection().apply { + setFrom(applicationSourceSets.map { it.classesDirectory }) + } + fun artifactCollection(artifactCollection: ArtifactCollection) { applicationConfigurationArtifacts.add(artifactCollection) } @@ -80,6 +91,10 @@ abstract class ExpediterTask : DefaultTask() { platformConfigurationArtifacts.add(artifactCollection) } + fun sourceSet(sourceDirectorySet: SourceDirectorySet) { + applicationSourceSets.add(sourceDirectorySet) + } + @OutputFile lateinit var report: File @@ -113,10 +128,10 @@ abstract class ExpediterTask : DefaultTask() { providers.add(JvmTypeProvider.forTarget(it)) } - if (!platformArtifacts.isEmpty) { + if (platformConfigurationArtifacts.isNotEmpty()) { providers.add( InMemoryPlatformTypeProvider( - ClasspathScanner(platformArtifacts).scan { i, _ -> TypeParsers.typeDescriptor(i) } + ClasspathScanner(platformConfigurationArtifacts.flatMap { it.artifacts.map { it.source() } }).scan { i, _ -> TypeParsers.typeDescriptor(i) } ) ) } @@ -133,7 +148,7 @@ abstract class ExpediterTask : DefaultTask() { } } - if (jvmVersion == null && animalSnifferSignatures.isEmpty && typeDescriptors.isEmpty && platformArtifacts.isEmpty) { + if (jvmVersion == null && animalSnifferSignatures.isEmpty && typeDescriptors.isEmpty && platformConfigurationArtifacts.isEmpty()) { logger.warn("No platform APIs specified, falling back to the platform classloader of the current JVM.") providers.add(PlatformClassloaderTypeProvider) @@ -145,9 +160,11 @@ abstract class ExpediterTask : DefaultTask() { } }.toSet() + val typeSources = applicationConfigurationArtifacts.flatMap { it.artifacts.map { it.source() } } + files.map { it.source() } + applicationSourceSets.map { it.source() } + val issues = Expediter( ignore, - cache.get().resolve(applicationArtifacts + files), + cache.get().resolve(typeSources), PlatformTypeProviderChain(providers) ).findIssues().subtract(ignores) diff --git a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/ApplicationSpec.kt b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/ApplicationSpec.kt index d933fc8..874a619 100644 --- a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/ApplicationSpec.kt +++ b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/ApplicationSpec.kt @@ -15,10 +15,22 @@ package com.toasttab.expediter.gradle.config -open class ApplicationSpec { +import org.gradle.api.Action +import org.gradle.api.model.ObjectFactory +import org.gradle.kotlin.dsl.newInstance +import javax.inject.Inject + +open class ApplicationSpec @Inject constructor( + private val objectFactory: ObjectFactory +) { val configurations: MutableList = mutableListOf() val files: MutableList = mutableListOf() val sourceSets: MutableList = mutableListOf() + val rootSpec: RootSpec = objectFactory.newInstance() + + fun roots(configure: Action) { + configure.execute(rootSpec) + } fun configuration(configuration: String) { configurations.add(configuration) @@ -38,7 +50,7 @@ open class ApplicationSpec { fun orDefaultIfEmpty(): ApplicationSpec { return if (isEmpty()) { - ApplicationSpec().apply { + objectFactory.newInstance().apply { configuration("runtimeClasspath") sourceSet("main") } diff --git a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/ExpediterExtension.kt b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/ExpediterExtension.kt index 259a2db..1f77dba 100644 --- a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/ExpediterExtension.kt +++ b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/ExpediterExtension.kt @@ -72,7 +72,7 @@ abstract class ExpediterExtension( } for (sourceSet in spec.sourceSets) { - files.from(project.sourceSet(sourceSet).java.classesDirectory) + sourceSet(project.sourceSet(sourceSet).java) } } diff --git a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/RootSpec.kt b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/RootSpec.kt new file mode 100644 index 0000000..431f69f --- /dev/null +++ b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/RootSpec.kt @@ -0,0 +1,14 @@ +package com.toasttab.expediter.gradle.config + +class RootSpec( + var type: RootType = RootType.ALL +) { + fun project() { + type = RootType.PROJECT_CLASSES + } +} + +enum class RootType { + ALL, + PROJECT_CLASSES +} \ No newline at end of file diff --git a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/service/ApplicationTypeCache.kt b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/service/ApplicationTypeCache.kt index 0b0ba16..bd40e0e 100644 --- a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/service/ApplicationTypeCache.kt +++ b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/service/ApplicationTypeCache.kt @@ -2,15 +2,19 @@ package com.toasttab.expediter.gradle.service import com.toasttab.expediter.provider.ClasspathApplicationTypesProvider import com.toasttab.expediter.types.ApplicationTypeContainer +import com.toasttab.expediter.types.TypeSource import org.gradle.api.services.BuildService import org.gradle.api.services.BuildServiceParameters import java.io.File import java.util.concurrent.ConcurrentHashMap +private typealias CacheKey = List +private fun Iterable.key() = map { it.file.path }.toList() + abstract class ApplicationTypeCache : BuildService { - private val cache = ConcurrentHashMap, ApplicationTypeContainer>() + private val cache = ConcurrentHashMap() - fun resolve(files: Iterable) = cache.computeIfAbsent(files.map { it.path }.toList()) { + fun resolve(files: Iterable) = cache.computeIfAbsent(files.key()) { ApplicationTypeContainer.create(ClasspathApplicationTypesProvider(files).types()) } } diff --git a/plugin/src/test/kotlin/com/toasttab/expediter/gradle/ExpediterPluginIntegrationTest.kt b/plugin/src/test/kotlin/com/toasttab/expediter/gradle/ExpediterPluginIntegrationTest.kt index 0c7eda4..7c270ab 100644 --- a/plugin/src/test/kotlin/com/toasttab/expediter/gradle/ExpediterPluginIntegrationTest.kt +++ b/plugin/src/test/kotlin/com/toasttab/expediter/gradle/ExpediterPluginIntegrationTest.kt @@ -141,7 +141,9 @@ class ExpediterPluginIntegrationTest { @Test fun multimodule(project: TestProject) { - project.createRunner().withArguments("app:expedite").buildAndFail() + project.createRunner().withArguments("app:expedite").buildAndFail().also { + println(it.output) + } val report = IssueReport.fromJson(project.dir.resolve("app/build/expediter.json").readText()) @@ -176,7 +178,9 @@ class ExpediterPluginIntegrationTest { @Test fun `cross library`(project: TestProject) { - project.createRunner().withArguments("check").buildAndFail() + project.createRunner().withArguments("check").buildAndFail().also { + println(it.output) + } val report = IssueReport.fromJson(project.dir.resolve("build/expediter.json").readText()) diff --git a/plugin/src/test/projects/ExpediterPluginIntegrationTest/cross library/src/main/java/Caller.java b/plugin/src/test/projects/ExpediterPluginIntegrationTest/cross library/src/main/java/Caller.java new file mode 100644 index 0000000..1497a42 --- /dev/null +++ b/plugin/src/test/projects/ExpediterPluginIntegrationTest/cross library/src/main/java/Caller.java @@ -0,0 +1,10 @@ +import com.fasterxml.jackson.databind.ObjectMapper; +import java.lang.String; + +class Caller { + String x; + + void foo() { + ObjectMapper mapper = new ObjectMapper(); + } +} \ No newline at end of file diff --git a/tests/src/test/kotlin/com/toasttab/expediter/test/ExpediterIntegrationTest.kt b/tests/src/test/kotlin/com/toasttab/expediter/test/ExpediterIntegrationTest.kt index d4a6c26..cbf8639 100644 --- a/tests/src/test/kotlin/com/toasttab/expediter/test/ExpediterIntegrationTest.kt +++ b/tests/src/test/kotlin/com/toasttab/expediter/test/ExpediterIntegrationTest.kt @@ -24,6 +24,8 @@ import com.toasttab.expediter.types.FieldAccessType import com.toasttab.expediter.types.MemberAccess import com.toasttab.expediter.types.MemberSymbolicReference import com.toasttab.expediter.types.MethodAccessType +import com.toasttab.expediter.types.SourceType +import com.toasttab.expediter.types.TypeSource import org.junit.jupiter.api.Test import strikt.api.expectThat import strikt.assertions.containsExactlyInAnyOrder @@ -36,7 +38,7 @@ class ExpediterIntegrationTest { @Test fun integrate() { val testClasspath = System.getProperty("test-classpath") - val scanner = ClasspathApplicationTypesProvider(testClasspath.split(':').map { File(it) }) + val scanner = ClasspathApplicationTypesProvider(testClasspath.split(':').map { TypeSource(File(it), SourceType.UNKNOWN, it) }) val p = Expediter(Ignore.NOTHING, scanner, PlatformClassloaderTypeProvider).findIssues() expectThat(p).containsExactlyInAnyOrder( From 63f3eea3773395e6b43c4afd389381b701c645a5 Mon Sep 17 00:00:00 2001 From: Oleg Golberg Date: Tue, 13 Feb 2024 10:30:18 -0500 Subject: [PATCH 2/4] roots wip --- .../com/toasttab/expediter/Expediter.kt | 11 +++-- .../toasttab/expediter/parser/TypeParsers.kt | 6 +-- .../ClasspathApplicationTypesProvider.kt | 5 +-- .../toasttab/expediter/roots/RootSelector.kt | 11 +++++ .../expediter/scanner/ClasspathScanner.kt | 16 ++++---- .../expediter/types/InspectedTypes.kt | 40 +++++++++---------- .../expediter/types/ClassfileSource.kt | 23 +++++++++++ .../com/toasttab/expediter/types/Type.kt | 2 +- .../toasttab/expediter/types/TypeSource.kt | 16 -------- .../toasttab/expediter/gradle/Artifacts.kt | 14 +++---- .../expediter/gradle/ExpediterPlugin.kt | 2 - .../expediter/gradle/ExpediterTask.kt | 10 +++-- .../gradle/config/ExpediterExtension.kt | 6 +++ .../expediter/gradle/config/RootSpec.kt | 22 +++++++--- .../gradle/service/ApplicationTypeCache.kt | 7 ++-- .../gradle/ExpediterPluginIntegrationTest.kt | 4 ++ .../src/main/java/test/Caller.java | 4 +- .../android lib/build.gradle.kts | 4 ++ .../test/ExpediterIntegrationTest.kt | 6 +-- 19 files changed, 128 insertions(+), 81 deletions(-) create mode 100644 core/src/main/kotlin/com/toasttab/expediter/roots/RootSelector.kt create mode 100644 model/src/main/kotlin/com/toasttab/expediter/types/ClassfileSource.kt delete mode 100644 model/src/main/kotlin/com/toasttab/expediter/types/TypeSource.kt diff --git a/core/src/main/kotlin/com/toasttab/expediter/Expediter.kt b/core/src/main/kotlin/com/toasttab/expediter/Expediter.kt index d3caff0..6dce22c 100644 --- a/core/src/main/kotlin/com/toasttab/expediter/Expediter.kt +++ b/core/src/main/kotlin/com/toasttab/expediter/Expediter.kt @@ -20,6 +20,7 @@ import com.toasttab.expediter.ignore.Ignore import com.toasttab.expediter.issue.Issue import com.toasttab.expediter.provider.ApplicationTypesProvider import com.toasttab.expediter.provider.PlatformTypeProvider +import com.toasttab.expediter.roots.RootSelector import com.toasttab.expediter.types.ApplicationType import com.toasttab.expediter.types.ApplicationTypeContainer import com.toasttab.expediter.types.InspectedTypes @@ -40,13 +41,15 @@ import protokt.v1.toasttab.expediter.v1.TypeFlavor class Expediter( private val ignore: Ignore, private val appTypes: ApplicationTypeContainer, - private val platformTypeProvider: PlatformTypeProvider + private val platformTypeProvider: PlatformTypeProvider, + private val rootSelector: RootSelector ) { constructor( ignore: Ignore, appTypes: ApplicationTypesProvider, - platformTypeProvider: PlatformTypeProvider - ) : this(ignore, ApplicationTypeContainer.create(appTypes.types()), platformTypeProvider) + platformTypeProvider: PlatformTypeProvider, + rootSelector: RootSelector = RootSelector.All + ) : this(ignore, ApplicationTypeContainer.create(appTypes.types()), platformTypeProvider, rootSelector) private val inspectedTypes: InspectedTypes by lazy { InspectedTypes(appTypes, platformTypeProvider) @@ -119,7 +122,7 @@ class Expediter( fun findIssues(): Set { return ( - inspectedTypes.reachableClasses().flatMap { appType -> + inspectedTypes.reachableClasses(rootSelector).flatMap { appType -> findIssues(appType) } + inspectedTypes.duplicateTypes ).filter { !ignore.ignore(it) } diff --git a/core/src/main/kotlin/com/toasttab/expediter/parser/TypeParsers.kt b/core/src/main/kotlin/com/toasttab/expediter/parser/TypeParsers.kt index 8d643b7..aef4aad 100644 --- a/core/src/main/kotlin/com/toasttab/expediter/parser/TypeParsers.kt +++ b/core/src/main/kotlin/com/toasttab/expediter/parser/TypeParsers.kt @@ -16,7 +16,7 @@ package com.toasttab.expediter.parser import com.toasttab.expediter.types.ApplicationType -import com.toasttab.expediter.types.TypeSource +import com.toasttab.expediter.types.ClassfileSource import com.toasttab.expediter.types.FieldAccessType import com.toasttab.expediter.types.MemberAccess import com.toasttab.expediter.types.MemberSymbolicReference @@ -36,7 +36,7 @@ import protokt.v1.toasttab.expediter.v1.TypeDescriptor import java.io.InputStream object TypeParsers { - fun applicationType(stream: InputStream, source: TypeSource) = ApplicationTypeParser(source).apply { + fun applicationType(stream: InputStream, source: ClassfileSource) = ApplicationTypeParser(source).apply { ClassReader(stream).accept(this, SKIP_DEBUG) }.get() @@ -45,7 +45,7 @@ object TypeParsers { }.get() } -private class ApplicationTypeParser(private val source: TypeSource) : ClassVisitor(ASM9, TypeDescriptorParser()) { +private class ApplicationTypeParser(private val source: ClassfileSource) : ClassVisitor(ASM9, TypeDescriptorParser()) { private val refs: MutableSet> = hashSetOf() private val referencedTypes: MutableSet = hashSetOf() diff --git a/core/src/main/kotlin/com/toasttab/expediter/provider/ClasspathApplicationTypesProvider.kt b/core/src/main/kotlin/com/toasttab/expediter/provider/ClasspathApplicationTypesProvider.kt index cf177ad..345888a 100644 --- a/core/src/main/kotlin/com/toasttab/expediter/provider/ClasspathApplicationTypesProvider.kt +++ b/core/src/main/kotlin/com/toasttab/expediter/provider/ClasspathApplicationTypesProvider.kt @@ -17,11 +17,10 @@ package com.toasttab.expediter.provider import com.toasttab.expediter.parser.TypeParsers import com.toasttab.expediter.scanner.ClasspathScanner -import com.toasttab.expediter.types.TypeSource -import java.io.File +import com.toasttab.expediter.types.ClassfileSource class ClasspathApplicationTypesProvider( - elements: Iterable + elements: Iterable ) : ApplicationTypesProvider { private val scanner = ClasspathScanner(elements) diff --git a/core/src/main/kotlin/com/toasttab/expediter/roots/RootSelector.kt b/core/src/main/kotlin/com/toasttab/expediter/roots/RootSelector.kt new file mode 100644 index 0000000..2944628 --- /dev/null +++ b/core/src/main/kotlin/com/toasttab/expediter/roots/RootSelector.kt @@ -0,0 +1,11 @@ +package com.toasttab.expediter.roots + +import com.toasttab.expediter.types.ApplicationType + +interface RootSelector { + fun isRootType(type: ApplicationType): Boolean + + object All: RootSelector { + override fun isRootType(type: ApplicationType) = true + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/toasttab/expediter/scanner/ClasspathScanner.kt b/core/src/main/kotlin/com/toasttab/expediter/scanner/ClasspathScanner.kt index e83ae85..74d2153 100644 --- a/core/src/main/kotlin/com/toasttab/expediter/scanner/ClasspathScanner.kt +++ b/core/src/main/kotlin/com/toasttab/expediter/scanner/ClasspathScanner.kt @@ -15,7 +15,7 @@ package com.toasttab.expediter.scanner -import com.toasttab.expediter.types.TypeSource +import com.toasttab.expediter.types.ClassfileSource import java.io.InputStream import java.lang.Exception import java.lang.RuntimeException @@ -23,16 +23,16 @@ import java.util.jar.JarInputStream import java.util.zip.ZipFile class ClasspathScanner( - private val elements: Iterable + private val elements: Iterable ) { - fun scan(parse: (stream: InputStream, source: TypeSource) -> T): List = elements.flatMap { types(it, parse) } + fun scan(parse: (stream: InputStream, source: ClassfileSource) -> T): List = elements.flatMap { types(it, parse) } private fun isClassFile(name: String) = name.endsWith(".class") && !name.startsWith("META-INF/versions") && // mrjars not supported yet !name.endsWith("package-info.class") && !name.endsWith("module-info.class") - private fun scanJarStream(stream: JarInputStream, source: TypeSource, parse: (stream: InputStream, source: TypeSource) -> T) = generateSequence { stream.nextJarEntry } + private fun scanJarStream(stream: JarInputStream, source: ClassfileSource, parse: (stream: InputStream, source: ClassfileSource) -> T) = generateSequence { stream.nextJarEntry } .filter { isClassFile(it.name) } .map { try { parse(stream, source) } catch (e: Exception) { @@ -41,11 +41,11 @@ class ClasspathScanner( } .toList() - fun scanJar(source: TypeSource, parse: (stream: InputStream, source: TypeSource) -> T): List = source.file.inputStream().use { + fun scanJar(source: ClassfileSource, parse: (stream: InputStream, source: ClassfileSource) -> T): List = source.file.inputStream().use { scanJarStream(JarInputStream(it), source, parse) } - fun scanAar(source: TypeSource, parse: (stream: InputStream, source: TypeSource) -> T): List = + fun scanAar(source: ClassfileSource, parse: (stream: InputStream, source: ClassfileSource) -> T): List = ZipFile(source.file).use { aar -> when (val classesEntry = aar.getEntry("classes.jar")) { null -> emptyList() @@ -57,14 +57,14 @@ class ClasspathScanner( } } - fun scanClassDir(source: TypeSource, parse: (stream: InputStream, source: TypeSource) -> T): List = + fun scanClassDir(source: ClassfileSource, parse: (stream: InputStream, source: ClassfileSource) -> T): List = source.file.walkTopDown().filter { isClassFile(it.name) }.map { it.inputStream().use { classStream -> parse(classStream, source) } }.toList() - private fun types(source: TypeSource, parse: (stream: InputStream, source: TypeSource) -> T) = + private fun types(source: ClassfileSource, parse: (stream: InputStream, source: ClassfileSource) -> T) = when { source.file.isDirectory -> scanClassDir(source, parse) source.file.name.endsWith(".jar") -> scanJar(source, parse) diff --git a/core/src/main/kotlin/com/toasttab/expediter/types/InspectedTypes.kt b/core/src/main/kotlin/com/toasttab/expediter/types/InspectedTypes.kt index 6613060..608ca1e 100644 --- a/core/src/main/kotlin/com/toasttab/expediter/types/InspectedTypes.kt +++ b/core/src/main/kotlin/com/toasttab/expediter/types/InspectedTypes.kt @@ -19,6 +19,7 @@ import com.toasttab.expediter.issue.Issue import com.toasttab.expediter.parser.SignatureParser import com.toasttab.expediter.parser.TypeSignature import com.toasttab.expediter.provider.PlatformTypeProvider +import com.toasttab.expediter.roots.RootSelector import java.util.LinkedList import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentMap @@ -29,7 +30,7 @@ class ApplicationTypeContainer( ) { companion object { fun create(all: List): ApplicationTypeContainer { - val duplicates = HashMap>() + val duplicates = HashMap>() val types = HashMap() for (type in all) { @@ -118,34 +119,33 @@ class InspectedTypes( val classes: Collection get() = appTypes.appTypes.values - fun reachableClasses(): Collection { - val reachable = hashMapOf() - - val todo = LinkedList(appTypes.appTypes.values.filter { - it.source.type == SourceType.SOURCE_SET || it.source.type == SourceType.PROJECT_DEPENDENCY - }) + fun reachableClasses(rootSelector: RootSelector): Collection { + if (rootSelector == RootSelector.All) { + return appTypes.appTypes.values + } else { + val reachable = hashMapOf() + val todo = LinkedList(appTypes.appTypes.values.filter(rootSelector::isRootType)) - while (todo.isNotEmpty()) { - val next = todo.remove() + while (todo.isNotEmpty()) { + val next = todo.remove() - if (reachable.put(next.name, next) == null) { - for (ref in next.referencedTypes) { - val refType = appTypes.appTypes[ref] - if (refType != null) { - todo.add(refType) + if (reachable.put(next.name, next) == null) { + for (ref in next.referencedTypes) { + appTypes.appTypes[ref]?.let(todo::add) } - } - val hierarchy = resolveHierarchy(next) + val hierarchy = resolveHierarchy(next) - if (hierarchy is ResolvedTypeHierarchy.CompleteTypeHierarchy) { - todo.addAll(hierarchy.superTypes.filterIsInstance()) + if (hierarchy is ResolvedTypeHierarchy.CompleteTypeHierarchy) { + todo.addAll(hierarchy.superTypes.filterIsInstance()) + } } } - } - return reachable.values + return reachable.values + } } val duplicateTypes: Collection get() = appTypes.duplicates } + diff --git a/model/src/main/kotlin/com/toasttab/expediter/types/ClassfileSource.kt b/model/src/main/kotlin/com/toasttab/expediter/types/ClassfileSource.kt new file mode 100644 index 0000000..579c4d4 --- /dev/null +++ b/model/src/main/kotlin/com/toasttab/expediter/types/ClassfileSource.kt @@ -0,0 +1,23 @@ +package com.toasttab.expediter.types + +import java.io.File + +/** + * Reference to a class file with additional metadata describing where the class file comes from. + */ +data class ClassfileSource ( + val file: File, + val type: ClassfileSourceType, + val name: String +) + +enum class ClassfileSourceType { + /** unmanaged by the build system, e.g. raw jar file */ + UNKNOWN, + /** compiled from source in the current project/subproject */ + SOURCE_SET, + /** different subproject of the same project */ + SUBPROJECT_DEPENDENCY, + /** external dependency managed by the build system */ + EXTERNAL_DEPENDENCY +} \ No newline at end of file diff --git a/model/src/main/kotlin/com/toasttab/expediter/types/Type.kt b/model/src/main/kotlin/com/toasttab/expediter/types/Type.kt index 601c9af..7b02925 100644 --- a/model/src/main/kotlin/com/toasttab/expediter/types/Type.kt +++ b/model/src/main/kotlin/com/toasttab/expediter/types/Type.kt @@ -35,7 +35,7 @@ class ApplicationType( override val descriptor: TypeDescriptor, val memberAccess: Set>, val referencedTypes: Set, - val source: TypeSource + val source: ClassfileSource ) : Type { override fun toString() = "ApplicationType[$name]" } diff --git a/model/src/main/kotlin/com/toasttab/expediter/types/TypeSource.kt b/model/src/main/kotlin/com/toasttab/expediter/types/TypeSource.kt deleted file mode 100644 index 02bf25c..0000000 --- a/model/src/main/kotlin/com/toasttab/expediter/types/TypeSource.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.toasttab.expediter.types - -import java.io.File - -data class TypeSource ( - val file: File, - val type: SourceType, - val name: String -) - -enum class SourceType { - UNKNOWN, - SOURCE_SET, - PROJECT_DEPENDENCY, - DEPENDENCY -} \ No newline at end of file diff --git a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/Artifacts.kt b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/Artifacts.kt index 3864ffb..ddace45 100644 --- a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/Artifacts.kt +++ b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/Artifacts.kt @@ -1,7 +1,7 @@ package com.toasttab.expediter.gradle -import com.toasttab.expediter.types.SourceType -import com.toasttab.expediter.types.TypeSource +import com.toasttab.expediter.types.ClassfileSourceType +import com.toasttab.expediter.types.ClassfileSource import org.gradle.api.artifacts.component.ModuleComponentIdentifier import org.gradle.api.artifacts.component.ProjectComponentIdentifier import org.gradle.api.artifacts.result.ResolvedArtifactResult @@ -10,11 +10,11 @@ import java.io.File fun ResolvedArtifactResult.source() = when (val cid = id.componentIdentifier) { - is ModuleComponentIdentifier -> TypeSource(file, SourceType.DEPENDENCY, cid.displayName) - is ProjectComponentIdentifier -> TypeSource(file, SourceType.PROJECT_DEPENDENCY, cid.projectPath) - else -> TypeSource(file, SourceType.UNKNOWN, cid.displayName) + is ModuleComponentIdentifier -> ClassfileSource(file, ClassfileSourceType.EXTERNAL_DEPENDENCY, cid.displayName) + is ProjectComponentIdentifier -> ClassfileSource(file, ClassfileSourceType.SUBPROJECT_DEPENDENCY, cid.projectPath) + else -> ClassfileSource(file, ClassfileSourceType.UNKNOWN, cid.displayName) } -fun File.source() = TypeSource(this, SourceType.UNKNOWN, name) +fun File.source() = ClassfileSource(this, ClassfileSourceType.UNKNOWN, name) -fun SourceDirectorySet.source() = TypeSource(classesDirectory.get().asFile, SourceType.SOURCE_SET, name) \ No newline at end of file +fun SourceDirectorySet.source() = ClassfileSource(classesDirectory.get().asFile, ClassfileSourceType.SOURCE_SET, name) \ No newline at end of file diff --git a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/ExpediterPlugin.kt b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/ExpediterPlugin.kt index 4f0861d..804a1a8 100644 --- a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/ExpediterPlugin.kt +++ b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/ExpediterPlugin.kt @@ -27,8 +27,6 @@ import org.gradle.kotlin.dsl.registerIfAbsent import org.gradle.kotlin.dsl.withType class ExpediterPlugin : Plugin { - private fun Project.sourceSet(sourceSet: String) = extensions.getByType().getByName(sourceSet) - override fun apply(project: Project) { val cache = project.gradle.sharedServices.registerIfAbsent("expediterTypeCache", ApplicationTypeCache::class.java) { } project.extensions.create("expediter", cache) diff --git a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/ExpediterTask.kt b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/ExpediterTask.kt index a93d419..24028c7 100644 --- a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/ExpediterTask.kt +++ b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/ExpediterTask.kt @@ -16,6 +16,7 @@ package com.toasttab.expediter.gradle import com.toasttab.expediter.Expediter +import com.toasttab.expediter.gradle.config.RootType import com.toasttab.expediter.gradle.service.ApplicationTypeCache import com.toasttab.expediter.ignore.Ignore import com.toasttab.expediter.issue.IssueReport @@ -42,7 +43,6 @@ import org.gradle.api.tasks.Optional import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitivity -import org.gradle.api.tasks.SourceSet import org.gradle.api.tasks.TaskAction import protokt.v1.toasttab.expediter.v1.TypeDescriptors import java.io.File @@ -120,6 +120,9 @@ abstract class ExpediterTask : DefaultTask() { @Input var failOnIssues: Boolean = false + @Input + var roots: RootType = RootType.ALL + @TaskAction fun execute() { val providers = mutableListOf() @@ -160,12 +163,13 @@ abstract class ExpediterTask : DefaultTask() { } }.toSet() - val typeSources = applicationConfigurationArtifacts.flatMap { it.artifacts.map { it.source() } } + files.map { it.source() } + applicationSourceSets.map { it.source() } + val typeSources = applicationConfigurationArtifacts.flatMapTo(LinkedHashSet()) { it.artifacts.map { it.source() } } + files.map { it.source() } + applicationSourceSets.map { it.source() } val issues = Expediter( ignore, cache.get().resolve(typeSources), - PlatformTypeProviderChain(providers) + PlatformTypeProviderChain(providers), + roots.selector, ).findIssues().subtract(ignores) val issueReport = IssueReport( diff --git a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/ExpediterExtension.kt b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/ExpediterExtension.kt index 1f77dba..7e8db16 100644 --- a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/ExpediterExtension.kt +++ b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/ExpediterExtension.kt @@ -56,6 +56,10 @@ abstract class ExpediterExtension( defaultChecks.ignore(configure) } + fun roots(configure: Action) { + defaultChecks.application.roots(configure) + } + private fun Project.sourceSet(sourceSet: String) = extensions.getByType().getByName(sourceSet) fun check(name: String, configure: Action) { @@ -122,6 +126,8 @@ abstract class ExpediterExtension( report = project.layout.buildDirectory.file("${key.reportName}.json").get().asFile failOnIssues = spec.failOnIssues + + roots = spec.application.rootSpec.type } } } diff --git a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/RootSpec.kt b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/RootSpec.kt index 431f69f..38b78a2 100644 --- a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/RootSpec.kt +++ b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/RootSpec.kt @@ -1,14 +1,24 @@ package com.toasttab.expediter.gradle.config -class RootSpec( - var type: RootType = RootType.ALL -) { +import com.toasttab.expediter.roots.RootSelector +import com.toasttab.expediter.types.ApplicationType +import com.toasttab.expediter.types.ClassfileSourceType + +open class RootSpec { + var type: RootType = RootType.PROJECT_CLASSES + + fun all() { + type = RootType.ALL + } + fun project() { type = RootType.PROJECT_CLASSES } } -enum class RootType { - ALL, - PROJECT_CLASSES +enum class RootType(val selector: RootSelector) { + ALL(RootSelector.All), + PROJECT_CLASSES(object : RootSelector { + override fun isRootType(type: ApplicationType) = type.source.type == ClassfileSourceType.SOURCE_SET || type.source.type == ClassfileSourceType.SUBPROJECT_DEPENDENCY + }) } \ No newline at end of file diff --git a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/service/ApplicationTypeCache.kt b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/service/ApplicationTypeCache.kt index bd40e0e..6e8c6f3 100644 --- a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/service/ApplicationTypeCache.kt +++ b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/service/ApplicationTypeCache.kt @@ -2,19 +2,18 @@ package com.toasttab.expediter.gradle.service import com.toasttab.expediter.provider.ClasspathApplicationTypesProvider import com.toasttab.expediter.types.ApplicationTypeContainer -import com.toasttab.expediter.types.TypeSource +import com.toasttab.expediter.types.ClassfileSource import org.gradle.api.services.BuildService import org.gradle.api.services.BuildServiceParameters -import java.io.File import java.util.concurrent.ConcurrentHashMap private typealias CacheKey = List -private fun Iterable.key() = map { it.file.path }.toList() +private fun Iterable.key() = map { it.file.path }.toList() abstract class ApplicationTypeCache : BuildService { private val cache = ConcurrentHashMap() - fun resolve(files: Iterable) = cache.computeIfAbsent(files.key()) { + fun resolve(files: Iterable) = cache.computeIfAbsent(files.key()) { ApplicationTypeContainer.create(ClasspathApplicationTypesProvider(files).types()) } } diff --git a/plugin/src/test/kotlin/com/toasttab/expediter/gradle/ExpediterPluginIntegrationTest.kt b/plugin/src/test/kotlin/com/toasttab/expediter/gradle/ExpediterPluginIntegrationTest.kt index 7c270ab..b9e5a42 100644 --- a/plugin/src/test/kotlin/com/toasttab/expediter/gradle/ExpediterPluginIntegrationTest.kt +++ b/plugin/src/test/kotlin/com/toasttab/expediter/gradle/ExpediterPluginIntegrationTest.kt @@ -26,6 +26,8 @@ import org.junit.jupiter.api.Test import strikt.api.expectThat import strikt.assertions.contains import strikt.assertions.containsExactlyInAnyOrder +import strikt.assertions.filterIsInstance +import strikt.assertions.isEmpty import kotlin.io.path.readText @TestKit @@ -209,6 +211,8 @@ class ExpediterPluginIntegrationTest { expectThat(report.issues).contains( Issue.MissingType("kotlin/io/path/DirectoryEntriesReader", "java/nio/file/Files") ) + + expectThat(report.issues).filterIsInstance().isEmpty() } @Test diff --git a/plugin/src/test/projects/ExpediterPluginIntegrationTest/android compat/src/main/java/test/Caller.java b/plugin/src/test/projects/ExpediterPluginIntegrationTest/android compat/src/main/java/test/Caller.java index ab28646..bd5c76a 100644 --- a/plugin/src/test/projects/ExpediterPluginIntegrationTest/android compat/src/main/java/test/Caller.java +++ b/plugin/src/test/projects/ExpediterPluginIntegrationTest/android compat/src/main/java/test/Caller.java @@ -16,7 +16,7 @@ package test; import java.util.concurrent.ConcurrentHashMap; - +import com.fasterxml.jackson.databind.ObjectMapper; public class Caller { void f() { ConcurrentHashMap map = new ConcurrentHashMap<>(); @@ -26,5 +26,7 @@ void f() { map.computeIfAbsent("a", k -> "b"); map.hashCode(); + + ObjectMapper mapper = new ObjectMapper(); } } \ No newline at end of file diff --git a/plugin/src/test/projects/ExpediterPluginIntegrationTest/android lib/build.gradle.kts b/plugin/src/test/projects/ExpediterPluginIntegrationTest/android lib/build.gradle.kts index 62687b9..48739ce 100644 --- a/plugin/src/test/projects/ExpediterPluginIntegrationTest/android lib/build.gradle.kts +++ b/plugin/src/test/projects/ExpediterPluginIntegrationTest/android lib/build.gradle.kts @@ -10,6 +10,10 @@ expediter { failOnIssues = true application { + roots { + all() + } + configuration("releaseRuntimeClasspath") configuration("debugRuntimeClasspath") } diff --git a/tests/src/test/kotlin/com/toasttab/expediter/test/ExpediterIntegrationTest.kt b/tests/src/test/kotlin/com/toasttab/expediter/test/ExpediterIntegrationTest.kt index cbf8639..57ea46b 100644 --- a/tests/src/test/kotlin/com/toasttab/expediter/test/ExpediterIntegrationTest.kt +++ b/tests/src/test/kotlin/com/toasttab/expediter/test/ExpediterIntegrationTest.kt @@ -24,8 +24,8 @@ import com.toasttab.expediter.types.FieldAccessType import com.toasttab.expediter.types.MemberAccess import com.toasttab.expediter.types.MemberSymbolicReference import com.toasttab.expediter.types.MethodAccessType -import com.toasttab.expediter.types.SourceType -import com.toasttab.expediter.types.TypeSource +import com.toasttab.expediter.types.ClassfileSourceType +import com.toasttab.expediter.types.ClassfileSource import org.junit.jupiter.api.Test import strikt.api.expectThat import strikt.assertions.containsExactlyInAnyOrder @@ -38,7 +38,7 @@ class ExpediterIntegrationTest { @Test fun integrate() { val testClasspath = System.getProperty("test-classpath") - val scanner = ClasspathApplicationTypesProvider(testClasspath.split(':').map { TypeSource(File(it), SourceType.UNKNOWN, it) }) + val scanner = ClasspathApplicationTypesProvider(testClasspath.split(':').map { ClassfileSource(File(it), ClassfileSourceType.UNKNOWN, it) }) val p = Expediter(Ignore.NOTHING, scanner, PlatformClassloaderTypeProvider).findIssues() expectThat(p).containsExactlyInAnyOrder( From 5677b0cd7b8c9b3206623d2edbfdac3d8b276d6e Mon Sep 17 00:00:00 2001 From: Oleg Golberg Date: Mon, 19 Feb 2024 14:49:38 -0500 Subject: [PATCH 3/4] more changes --- .../com/toasttab/expediter/Expediter.kt | 8 +++---- .../{RootSelector.kt => RootsSelector.kt} | 6 ++--- .../expediter/types/InspectedTypes.kt | 24 +++++++------------ .../toasttab/expediter/types/TypeHierarchy.kt | 10 +++++--- .../expediter/types/ClassfileSource.kt | 13 ++++++---- .../toasttab/expediter/gradle/Artifacts.kt | 4 ++-- .../expediter/gradle/ExpediterPlugin.kt | 2 -- .../expediter/gradle/ExpediterTask.kt | 13 +++++----- .../gradle/config/ApplicationSpec.kt | 4 ++-- .../gradle/config/ExpediterExtension.kt | 2 +- .../{RootSpec.kt => RootsSelectorSpec.kt} | 12 +++++----- .../test/ExpediterIntegrationTest.kt | 4 ++-- 12 files changed, 50 insertions(+), 52 deletions(-) rename core/src/main/kotlin/com/toasttab/expediter/roots/{RootSelector.kt => RootsSelector.kt} (77%) rename plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/{RootSpec.kt => RootsSelectorSpec.kt} (70%) diff --git a/core/src/main/kotlin/com/toasttab/expediter/Expediter.kt b/core/src/main/kotlin/com/toasttab/expediter/Expediter.kt index 6dce22c..2890141 100644 --- a/core/src/main/kotlin/com/toasttab/expediter/Expediter.kt +++ b/core/src/main/kotlin/com/toasttab/expediter/Expediter.kt @@ -20,7 +20,7 @@ import com.toasttab.expediter.ignore.Ignore import com.toasttab.expediter.issue.Issue import com.toasttab.expediter.provider.ApplicationTypesProvider import com.toasttab.expediter.provider.PlatformTypeProvider -import com.toasttab.expediter.roots.RootSelector +import com.toasttab.expediter.roots.RootsSelector import com.toasttab.expediter.types.ApplicationType import com.toasttab.expediter.types.ApplicationTypeContainer import com.toasttab.expediter.types.InspectedTypes @@ -42,13 +42,13 @@ class Expediter( private val ignore: Ignore, private val appTypes: ApplicationTypeContainer, private val platformTypeProvider: PlatformTypeProvider, - private val rootSelector: RootSelector + private val rootSelector: RootsSelector ) { constructor( ignore: Ignore, appTypes: ApplicationTypesProvider, platformTypeProvider: PlatformTypeProvider, - rootSelector: RootSelector = RootSelector.All + rootSelector: RootsSelector = RootsSelector.All ) : this(ignore, ApplicationTypeContainer.create(appTypes.types()), platformTypeProvider, rootSelector) private val inspectedTypes: InspectedTypes by lazy { @@ -122,7 +122,7 @@ class Expediter( fun findIssues(): Set { return ( - inspectedTypes.reachableClasses(rootSelector).flatMap { appType -> + inspectedTypes.reachableTypes(rootSelector).flatMap { appType -> findIssues(appType) } + inspectedTypes.duplicateTypes ).filter { !ignore.ignore(it) } diff --git a/core/src/main/kotlin/com/toasttab/expediter/roots/RootSelector.kt b/core/src/main/kotlin/com/toasttab/expediter/roots/RootsSelector.kt similarity index 77% rename from core/src/main/kotlin/com/toasttab/expediter/roots/RootSelector.kt rename to core/src/main/kotlin/com/toasttab/expediter/roots/RootsSelector.kt index 2944628..4470e5b 100644 --- a/core/src/main/kotlin/com/toasttab/expediter/roots/RootSelector.kt +++ b/core/src/main/kotlin/com/toasttab/expediter/roots/RootsSelector.kt @@ -2,10 +2,10 @@ package com.toasttab.expediter.roots import com.toasttab.expediter.types.ApplicationType -interface RootSelector { +interface RootsSelector { fun isRootType(type: ApplicationType): Boolean - object All: RootSelector { + object All : RootsSelector { override fun isRootType(type: ApplicationType) = true } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/com/toasttab/expediter/types/InspectedTypes.kt b/core/src/main/kotlin/com/toasttab/expediter/types/InspectedTypes.kt index 608ca1e..a5278bd 100644 --- a/core/src/main/kotlin/com/toasttab/expediter/types/InspectedTypes.kt +++ b/core/src/main/kotlin/com/toasttab/expediter/types/InspectedTypes.kt @@ -19,7 +19,7 @@ import com.toasttab.expediter.issue.Issue import com.toasttab.expediter.parser.SignatureParser import com.toasttab.expediter.parser.TypeSignature import com.toasttab.expediter.provider.PlatformTypeProvider -import com.toasttab.expediter.roots.RootSelector +import com.toasttab.expediter.roots.RootsSelector import java.util.LinkedList import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentMap @@ -77,7 +77,7 @@ class InspectedTypes( } } - private fun traverse(type: Type): TypeHierarchy { + private fun hierarchy(type: Type): TypeHierarchy { val cached = hierarchyCache[type.name] if (cached == null) { @@ -94,7 +94,7 @@ class InspectedTypes( if (superType == null) { superTypes.add(OptionalType.MissingType(signature.name)) } else { - val hierarchy = traverse(superType) + val hierarchy = hierarchy(superType) superTypes.add(OptionalType.PresentType(superType)) superTypes.addAll(hierarchy.superTypes) } @@ -114,13 +114,13 @@ class InspectedTypes( } fun resolveHierarchy(type: Type): ResolvedTypeHierarchy { - return traverse(type).resolve() + return hierarchy(type).resolve() } val classes: Collection get() = appTypes.appTypes.values - fun reachableClasses(rootSelector: RootSelector): Collection { - if (rootSelector == RootSelector.All) { + fun reachableTypes(rootSelector: RootsSelector): Collection { + if (rootSelector == RootsSelector.All) { return appTypes.appTypes.values } else { val reachable = hashMapOf() @@ -130,15 +130,8 @@ class InspectedTypes( val next = todo.remove() if (reachable.put(next.name, next) == null) { - for (ref in next.referencedTypes) { - appTypes.appTypes[ref]?.let(todo::add) - } - - val hierarchy = resolveHierarchy(next) - - if (hierarchy is ResolvedTypeHierarchy.CompleteTypeHierarchy) { - todo.addAll(hierarchy.superTypes.filterIsInstance()) - } + todo.addAll(next.referencedTypes.mapNotNull(appTypes.appTypes::get)) + todo.addAll(hierarchy(next).presentSuperTypes().filterIsInstance()) } } @@ -148,4 +141,3 @@ class InspectedTypes( val duplicateTypes: Collection get() = appTypes.duplicates } - diff --git a/core/src/main/kotlin/com/toasttab/expediter/types/TypeHierarchy.kt b/core/src/main/kotlin/com/toasttab/expediter/types/TypeHierarchy.kt index 1939fe7..78d5a4f 100644 --- a/core/src/main/kotlin/com/toasttab/expediter/types/TypeHierarchy.kt +++ b/core/src/main/kotlin/com/toasttab/expediter/types/TypeHierarchy.kt @@ -20,16 +20,20 @@ class TypeHierarchy( val superTypes: Set ) { fun resolve(): ResolvedTypeHierarchy { - val missing = superTypes.filterIsInstance() + val missing = missingSuperTypes() return if (missing.isNotEmpty()) { ResolvedTypeHierarchy.IncompleteTypeHierarchy(type, missing.toSet()) } else { ResolvedTypeHierarchy.CompleteTypeHierarchy( type, - superTypes.asSequence().filterIsInstance().map { it.type } + presentSuperTypes() ) } } + + fun missingSuperTypes() = superTypes.filterIsInstance() + + fun presentSuperTypes() = superTypes.filterIsInstance().map { it.type } } /** @@ -67,7 +71,7 @@ sealed interface ResolvedTypeHierarchy : OptionalResolvedTypeHierarchy, Identifi override val name get() = type.name class IncompleteTypeHierarchy(override val type: Type, val missingType: Set) : ResolvedTypeHierarchy - class CompleteTypeHierarchy(override val type: Type, val superTypes: Sequence) : ResolvedTypeHierarchy { + class CompleteTypeHierarchy(override val type: Type, val superTypes: Iterable) : ResolvedTypeHierarchy { val allTypes: Sequence get() = sequenceOf(type) + superTypes } } diff --git a/model/src/main/kotlin/com/toasttab/expediter/types/ClassfileSource.kt b/model/src/main/kotlin/com/toasttab/expediter/types/ClassfileSource.kt index 579c4d4..d43f64a 100644 --- a/model/src/main/kotlin/com/toasttab/expediter/types/ClassfileSource.kt +++ b/model/src/main/kotlin/com/toasttab/expediter/types/ClassfileSource.kt @@ -5,19 +5,22 @@ import java.io.File /** * Reference to a class file with additional metadata describing where the class file comes from. */ -data class ClassfileSource ( +data class ClassfileSource( val file: File, val type: ClassfileSourceType, val name: String ) enum class ClassfileSourceType { - /** unmanaged by the build system, e.g. raw jar file */ + /** unmanaged by the build system, e.g. a raw jar file */ UNKNOWN, + /** compiled from source in the current project/subproject */ SOURCE_SET, - /** different subproject of the same project */ + + /** a different subproject within the same project */ SUBPROJECT_DEPENDENCY, - /** external dependency managed by the build system */ + + /** an external dependency managed by the build system */ EXTERNAL_DEPENDENCY -} \ No newline at end of file +} diff --git a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/Artifacts.kt b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/Artifacts.kt index ddace45..c23548b 100644 --- a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/Artifacts.kt +++ b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/Artifacts.kt @@ -1,7 +1,7 @@ package com.toasttab.expediter.gradle -import com.toasttab.expediter.types.ClassfileSourceType import com.toasttab.expediter.types.ClassfileSource +import com.toasttab.expediter.types.ClassfileSourceType import org.gradle.api.artifacts.component.ModuleComponentIdentifier import org.gradle.api.artifacts.component.ProjectComponentIdentifier import org.gradle.api.artifacts.result.ResolvedArtifactResult @@ -17,4 +17,4 @@ fun ResolvedArtifactResult.source() = fun File.source() = ClassfileSource(this, ClassfileSourceType.UNKNOWN, name) -fun SourceDirectorySet.source() = ClassfileSource(classesDirectory.get().asFile, ClassfileSourceType.SOURCE_SET, name) \ No newline at end of file +fun SourceDirectorySet.source() = ClassfileSource(classesDirectory.get().asFile, ClassfileSourceType.SOURCE_SET, name) diff --git a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/ExpediterPlugin.kt b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/ExpediterPlugin.kt index 804a1a8..aafcf4f 100644 --- a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/ExpediterPlugin.kt +++ b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/ExpediterPlugin.kt @@ -19,9 +19,7 @@ import com.toasttab.expediter.gradle.config.ExpediterExtension import com.toasttab.expediter.gradle.service.ApplicationTypeCache import org.gradle.api.Plugin import org.gradle.api.Project -import org.gradle.api.tasks.SourceSetContainer import org.gradle.kotlin.dsl.create -import org.gradle.kotlin.dsl.getByType import org.gradle.kotlin.dsl.register import org.gradle.kotlin.dsl.registerIfAbsent import org.gradle.kotlin.dsl.withType diff --git a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/ExpediterTask.kt b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/ExpediterTask.kt index 24028c7..765f698 100644 --- a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/ExpediterTask.kt +++ b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/ExpediterTask.kt @@ -67,6 +67,13 @@ abstract class ExpediterTask : DefaultTask() { @Suppress("UNUSED") val platformArtifacts get() = platformConfigurationArtifacts.asFileCollection() + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + @Suppress("UNUSED") + val sourceSets: FileCollection get() = project.objects.fileCollection().apply { + setFrom(applicationSourceSets.map { it.classesDirectory }) + } + private fun Collection.asFileCollection() = if (isEmpty()) { project.objects.fileCollection() } else { @@ -77,12 +84,6 @@ abstract class ExpediterTask : DefaultTask() { @get:PathSensitive(PathSensitivity.ABSOLUTE) abstract val files: ConfigurableFileCollection - @get:InputFiles - @get:PathSensitive(PathSensitivity.RELATIVE) - val sourceSets: FileCollection get() = project.objects.fileCollection().apply { - setFrom(applicationSourceSets.map { it.classesDirectory }) - } - fun artifactCollection(artifactCollection: ArtifactCollection) { applicationConfigurationArtifacts.add(artifactCollection) } diff --git a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/ApplicationSpec.kt b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/ApplicationSpec.kt index 874a619..12be378 100644 --- a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/ApplicationSpec.kt +++ b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/ApplicationSpec.kt @@ -26,9 +26,9 @@ open class ApplicationSpec @Inject constructor( val configurations: MutableList = mutableListOf() val files: MutableList = mutableListOf() val sourceSets: MutableList = mutableListOf() - val rootSpec: RootSpec = objectFactory.newInstance() + val rootSpec: RootsSelectorSpec = objectFactory.newInstance() - fun roots(configure: Action) { + fun roots(configure: Action) { configure.execute(rootSpec) } diff --git a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/ExpediterExtension.kt b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/ExpediterExtension.kt index 7e8db16..5a1d346 100644 --- a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/ExpediterExtension.kt +++ b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/ExpediterExtension.kt @@ -56,7 +56,7 @@ abstract class ExpediterExtension( defaultChecks.ignore(configure) } - fun roots(configure: Action) { + fun roots(configure: Action) { defaultChecks.application.roots(configure) } diff --git a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/RootSpec.kt b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/RootsSelectorSpec.kt similarity index 70% rename from plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/RootSpec.kt rename to plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/RootsSelectorSpec.kt index 38b78a2..00d7d18 100644 --- a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/RootSpec.kt +++ b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/RootsSelectorSpec.kt @@ -1,10 +1,10 @@ package com.toasttab.expediter.gradle.config -import com.toasttab.expediter.roots.RootSelector +import com.toasttab.expediter.roots.RootsSelector import com.toasttab.expediter.types.ApplicationType import com.toasttab.expediter.types.ClassfileSourceType -open class RootSpec { +open class RootsSelectorSpec { var type: RootType = RootType.PROJECT_CLASSES fun all() { @@ -16,9 +16,9 @@ open class RootSpec { } } -enum class RootType(val selector: RootSelector) { - ALL(RootSelector.All), - PROJECT_CLASSES(object : RootSelector { +enum class RootType(val selector: RootsSelector) { + ALL(RootsSelector.All), + PROJECT_CLASSES(object : RootsSelector { override fun isRootType(type: ApplicationType) = type.source.type == ClassfileSourceType.SOURCE_SET || type.source.type == ClassfileSourceType.SUBPROJECT_DEPENDENCY }) -} \ No newline at end of file +} diff --git a/tests/src/test/kotlin/com/toasttab/expediter/test/ExpediterIntegrationTest.kt b/tests/src/test/kotlin/com/toasttab/expediter/test/ExpediterIntegrationTest.kt index 57ea46b..ccf78f8 100644 --- a/tests/src/test/kotlin/com/toasttab/expediter/test/ExpediterIntegrationTest.kt +++ b/tests/src/test/kotlin/com/toasttab/expediter/test/ExpediterIntegrationTest.kt @@ -20,12 +20,12 @@ import com.toasttab.expediter.ignore.Ignore import com.toasttab.expediter.issue.Issue import com.toasttab.expediter.provider.ClasspathApplicationTypesProvider import com.toasttab.expediter.provider.PlatformClassloaderTypeProvider +import com.toasttab.expediter.types.ClassfileSource +import com.toasttab.expediter.types.ClassfileSourceType import com.toasttab.expediter.types.FieldAccessType import com.toasttab.expediter.types.MemberAccess import com.toasttab.expediter.types.MemberSymbolicReference import com.toasttab.expediter.types.MethodAccessType -import com.toasttab.expediter.types.ClassfileSourceType -import com.toasttab.expediter.types.ClassfileSource import org.junit.jupiter.api.Test import strikt.api.expectThat import strikt.assertions.containsExactlyInAnyOrder From 34ccf369963447b0cd16bd7860602bde1e41f06b Mon Sep 17 00:00:00 2001 From: Oleg Golberg Date: Sat, 24 Feb 2024 09:27:21 -0500 Subject: [PATCH 4/4] update --- README.md | 40 +++++++++++++++++-- .../com/toasttab/expediter/Expediter.kt | 6 +-- .../toasttab/expediter/roots/RootSelector.kt | 11 +++++ .../toasttab/expediter/roots/RootsSelector.kt | 11 ----- .../expediter/types/InspectedTypes.kt | 8 ++-- .../expediter/gradle/ExpediterTask.kt | 8 ++-- .../gradle/{Artifacts.kt => Sources.kt} | 10 +++++ .../gradle/config/ApplicationSpec.kt | 6 +-- .../gradle/config/ExpediterExtension.kt | 4 +- .../gradle/config/RootSelectorSpec.kt | 24 +++++++++++ .../gradle/config/RootsSelectorSpec.kt | 24 ----------- .../gradle/ExpediterPluginIntegrationTest.kt | 8 +--- 12 files changed, 100 insertions(+), 60 deletions(-) create mode 100644 core/src/main/kotlin/com/toasttab/expediter/roots/RootSelector.kt delete mode 100644 core/src/main/kotlin/com/toasttab/expediter/roots/RootsSelector.kt rename plugin/src/main/kotlin/com/toasttab/expediter/gradle/{Artifacts.kt => Sources.kt} (71%) create mode 100644 plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/RootSelectorSpec.kt delete mode 100644 plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/RootsSelectorSpec.kt diff --git a/README.md b/README.md index 20616f5..5de3981 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ to be using. In the ideal world, this would never happen, because you are enforc is using semver correctly. But in the real world, you have a runtime error on your hands and you won't catch it until you exercise a specific code path in production or - hopefully - tests. -This tool can detect _some_ binary incompatibilities at build time. Specifically, it can report +This tool can detect certain binary incompatibilities at build time. Specifically, it will report * Missing classes * Duplicate classes @@ -35,7 +35,9 @@ Conceptually, the inputs for running the tool are _application classes_ and _pla classes are the classes that need to be validated, and the platform APIs are the APIs provided by the runtime environment of the application, e.g. the JVM or the Android SDK. -Typically, the application classes are the classes compiled directly by the project and its runtime dependencies. +Typically, the application classes are the classes compiled directly by the project and the classes from the project's +runtime dependencies that they reference. + The platform APIs can be specified by the JVM, a set of dependencies, or serialized type descriptors. ## Basic setup @@ -50,7 +52,12 @@ plugins { ## Application classes -By default, the application classes are the classes from the main source set and the runtime dependencies of the project. +By default, the application classes are selected from the main source set and the runtime dependencies of the project. +The classes compiled from the main source set and subproject dependencies are treated as _roots_. The roots and all +classes from the external runtime dependencies reachable from the roots then form the set of application classes. + +The concept of roots makes it easier to filter out unused classes from third-party dependencies, which would otherwise +produce noise. You can customize this behavior, e.g. change the Gradle configuration that describes the dependencies. @@ -65,6 +72,33 @@ expediter { For example, in an Android project, you will want to use a different configuration, such as `productionReleaseRuntime`. +You can also customize how the roots are chosen. This is the implicit default setup, where the roots are the classes +compiled from the current project and other subprojects of the same projects. + +```kotlin +expediter { + application { + roots { + project() + } + } +} +``` + +This is a different setup, where the roots are all classes compiled from the current project and all classes in its +runtime dependencies. Beware that with this setup, there will likely be a lot of noise from unused classes providig +optional functionality. + +```kotlin +expediter { + application { + roots { + all() + } + } +} +``` + ## Platform APIs Platform APIs can be provided by the JVM or specified as a set of dependencies or published type descriptors in the diff --git a/core/src/main/kotlin/com/toasttab/expediter/Expediter.kt b/core/src/main/kotlin/com/toasttab/expediter/Expediter.kt index 2890141..4b0b098 100644 --- a/core/src/main/kotlin/com/toasttab/expediter/Expediter.kt +++ b/core/src/main/kotlin/com/toasttab/expediter/Expediter.kt @@ -20,7 +20,7 @@ import com.toasttab.expediter.ignore.Ignore import com.toasttab.expediter.issue.Issue import com.toasttab.expediter.provider.ApplicationTypesProvider import com.toasttab.expediter.provider.PlatformTypeProvider -import com.toasttab.expediter.roots.RootsSelector +import com.toasttab.expediter.roots.RootSelector import com.toasttab.expediter.types.ApplicationType import com.toasttab.expediter.types.ApplicationTypeContainer import com.toasttab.expediter.types.InspectedTypes @@ -42,13 +42,13 @@ class Expediter( private val ignore: Ignore, private val appTypes: ApplicationTypeContainer, private val platformTypeProvider: PlatformTypeProvider, - private val rootSelector: RootsSelector + private val rootSelector: RootSelector ) { constructor( ignore: Ignore, appTypes: ApplicationTypesProvider, platformTypeProvider: PlatformTypeProvider, - rootSelector: RootsSelector = RootsSelector.All + rootSelector: RootSelector = RootSelector.All ) : this(ignore, ApplicationTypeContainer.create(appTypes.types()), platformTypeProvider, rootSelector) private val inspectedTypes: InspectedTypes by lazy { diff --git a/core/src/main/kotlin/com/toasttab/expediter/roots/RootSelector.kt b/core/src/main/kotlin/com/toasttab/expediter/roots/RootSelector.kt new file mode 100644 index 0000000..85302b8 --- /dev/null +++ b/core/src/main/kotlin/com/toasttab/expediter/roots/RootSelector.kt @@ -0,0 +1,11 @@ +package com.toasttab.expediter.roots + +import com.toasttab.expediter.types.ApplicationType + +interface RootSelector { + fun isRoot(type: ApplicationType): Boolean + + object All : RootSelector { + override fun isRoot(type: ApplicationType) = true + } +} diff --git a/core/src/main/kotlin/com/toasttab/expediter/roots/RootsSelector.kt b/core/src/main/kotlin/com/toasttab/expediter/roots/RootsSelector.kt deleted file mode 100644 index 4470e5b..0000000 --- a/core/src/main/kotlin/com/toasttab/expediter/roots/RootsSelector.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.toasttab.expediter.roots - -import com.toasttab.expediter.types.ApplicationType - -interface RootsSelector { - fun isRootType(type: ApplicationType): Boolean - - object All : RootsSelector { - override fun isRootType(type: ApplicationType) = true - } -} diff --git a/core/src/main/kotlin/com/toasttab/expediter/types/InspectedTypes.kt b/core/src/main/kotlin/com/toasttab/expediter/types/InspectedTypes.kt index a5278bd..9d9915b 100644 --- a/core/src/main/kotlin/com/toasttab/expediter/types/InspectedTypes.kt +++ b/core/src/main/kotlin/com/toasttab/expediter/types/InspectedTypes.kt @@ -19,7 +19,7 @@ import com.toasttab.expediter.issue.Issue import com.toasttab.expediter.parser.SignatureParser import com.toasttab.expediter.parser.TypeSignature import com.toasttab.expediter.provider.PlatformTypeProvider -import com.toasttab.expediter.roots.RootsSelector +import com.toasttab.expediter.roots.RootSelector import java.util.LinkedList import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentMap @@ -119,12 +119,12 @@ class InspectedTypes( val classes: Collection get() = appTypes.appTypes.values - fun reachableTypes(rootSelector: RootsSelector): Collection { - if (rootSelector == RootsSelector.All) { + fun reachableTypes(rootSelector: RootSelector): Collection { + if (rootSelector == RootSelector.All) { return appTypes.appTypes.values } else { val reachable = hashMapOf() - val todo = LinkedList(appTypes.appTypes.values.filter(rootSelector::isRootType)) + val todo = LinkedList(appTypes.appTypes.values.filter(rootSelector::isRoot)) while (todo.isNotEmpty()) { val next = todo.remove() diff --git a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/ExpediterTask.kt b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/ExpediterTask.kt index 765f698..586dcd0 100644 --- a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/ExpediterTask.kt +++ b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/ExpediterTask.kt @@ -158,13 +158,13 @@ abstract class ExpediterTask : DefaultTask() { providers.add(PlatformClassloaderTypeProvider) } - val ignores = ignoreFiles.flatMap { - it.inputStream().buffered().use { - IssueReport.fromJson(it).issues + val ignores = ignoreFiles.flatMap { f -> + f.inputStream().buffered().use { s -> + IssueReport.fromJson(s).issues } }.toSet() - val typeSources = applicationConfigurationArtifacts.flatMapTo(LinkedHashSet()) { it.artifacts.map { it.source() } } + files.map { it.source() } + applicationSourceSets.map { it.source() } + val typeSources = applicationConfigurationArtifacts.sources() + files.sources() + applicationSourceSets.sources() val issues = Expediter( ignore, diff --git a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/Artifacts.kt b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/Sources.kt similarity index 71% rename from plugin/src/main/kotlin/com/toasttab/expediter/gradle/Artifacts.kt rename to plugin/src/main/kotlin/com/toasttab/expediter/gradle/Sources.kt index c23548b..af0f75c 100644 --- a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/Artifacts.kt +++ b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/Sources.kt @@ -2,9 +2,11 @@ package com.toasttab.expediter.gradle import com.toasttab.expediter.types.ClassfileSource import com.toasttab.expediter.types.ClassfileSourceType +import org.gradle.api.artifacts.ArtifactCollection import org.gradle.api.artifacts.component.ModuleComponentIdentifier import org.gradle.api.artifacts.component.ProjectComponentIdentifier import org.gradle.api.artifacts.result.ResolvedArtifactResult +import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.SourceDirectorySet import java.io.File @@ -18,3 +20,11 @@ fun ResolvedArtifactResult.source() = fun File.source() = ClassfileSource(this, ClassfileSourceType.UNKNOWN, name) fun SourceDirectorySet.source() = ClassfileSource(classesDirectory.get().asFile, ClassfileSourceType.SOURCE_SET, name) + +fun ArtifactCollection.sources() = artifacts.map { it.source() } + +fun ConfigurableFileCollection.sources() = map { it.source() } + +fun Collection.sources() = flatMapTo(LinkedHashSet(), ArtifactCollection::sources) + +fun Set.sources() = map { it.source() } diff --git a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/ApplicationSpec.kt b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/ApplicationSpec.kt index 12be378..f277f91 100644 --- a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/ApplicationSpec.kt +++ b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/ApplicationSpec.kt @@ -26,10 +26,10 @@ open class ApplicationSpec @Inject constructor( val configurations: MutableList = mutableListOf() val files: MutableList = mutableListOf() val sourceSets: MutableList = mutableListOf() - val rootSpec: RootsSelectorSpec = objectFactory.newInstance() + val rootSelectorSpec: RootSelectorSpec = objectFactory.newInstance() - fun roots(configure: Action) { - configure.execute(rootSpec) + fun roots(configure: Action) { + configure.execute(rootSelectorSpec) } fun configuration(configuration: String) { diff --git a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/ExpediterExtension.kt b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/ExpediterExtension.kt index 5a1d346..82814d2 100644 --- a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/ExpediterExtension.kt +++ b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/ExpediterExtension.kt @@ -56,7 +56,7 @@ abstract class ExpediterExtension( defaultChecks.ignore(configure) } - fun roots(configure: Action) { + fun roots(configure: Action) { defaultChecks.application.roots(configure) } @@ -127,7 +127,7 @@ abstract class ExpediterExtension( failOnIssues = spec.failOnIssues - roots = spec.application.rootSpec.type + roots = spec.application.rootSelectorSpec.type } } } diff --git a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/RootSelectorSpec.kt b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/RootSelectorSpec.kt new file mode 100644 index 0000000..a060f62 --- /dev/null +++ b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/RootSelectorSpec.kt @@ -0,0 +1,24 @@ +package com.toasttab.expediter.gradle.config + +import com.toasttab.expediter.roots.RootSelector +import com.toasttab.expediter.types.ApplicationType +import com.toasttab.expediter.types.ClassfileSourceType + +open class RootSelectorSpec { + var type: RootType = RootType.PROJECT_CLASSES + + fun all() { + type = RootType.ALL + } + + fun project() { + type = RootType.PROJECT_CLASSES + } +} + +enum class RootType(val selector: RootSelector) { + ALL(RootSelector.All), + PROJECT_CLASSES(object : RootSelector { + override fun isRoot(type: ApplicationType) = type.source.type == ClassfileSourceType.SOURCE_SET || type.source.type == ClassfileSourceType.SUBPROJECT_DEPENDENCY + }) +} diff --git a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/RootsSelectorSpec.kt b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/RootsSelectorSpec.kt deleted file mode 100644 index 00d7d18..0000000 --- a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/config/RootsSelectorSpec.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.toasttab.expediter.gradle.config - -import com.toasttab.expediter.roots.RootsSelector -import com.toasttab.expediter.types.ApplicationType -import com.toasttab.expediter.types.ClassfileSourceType - -open class RootsSelectorSpec { - var type: RootType = RootType.PROJECT_CLASSES - - fun all() { - type = RootType.ALL - } - - fun project() { - type = RootType.PROJECT_CLASSES - } -} - -enum class RootType(val selector: RootsSelector) { - ALL(RootsSelector.All), - PROJECT_CLASSES(object : RootsSelector { - override fun isRootType(type: ApplicationType) = type.source.type == ClassfileSourceType.SOURCE_SET || type.source.type == ClassfileSourceType.SUBPROJECT_DEPENDENCY - }) -} diff --git a/plugin/src/test/kotlin/com/toasttab/expediter/gradle/ExpediterPluginIntegrationTest.kt b/plugin/src/test/kotlin/com/toasttab/expediter/gradle/ExpediterPluginIntegrationTest.kt index b9e5a42..3bd6e23 100644 --- a/plugin/src/test/kotlin/com/toasttab/expediter/gradle/ExpediterPluginIntegrationTest.kt +++ b/plugin/src/test/kotlin/com/toasttab/expediter/gradle/ExpediterPluginIntegrationTest.kt @@ -143,9 +143,7 @@ class ExpediterPluginIntegrationTest { @Test fun multimodule(project: TestProject) { - project.createRunner().withArguments("app:expedite").buildAndFail().also { - println(it.output) - } + project.createRunner().withArguments("app:expedite").buildAndFail() val report = IssueReport.fromJson(project.dir.resolve("app/build/expediter.json").readText()) @@ -180,9 +178,7 @@ class ExpediterPluginIntegrationTest { @Test fun `cross library`(project: TestProject) { - project.createRunner().withArguments("check").buildAndFail().also { - println(it.output) - } + project.createRunner().withArguments("check").buildAndFail() val report = IssueReport.fromJson(project.dir.resolve("build/expediter.json").readText())