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 99bb1f0..4b0b098 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.classes.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/parser/TypeParsers.kt b/core/src/main/kotlin/com/toasttab/expediter/parser/TypeParsers.kt index f6cbd9b..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,6 +16,7 @@ package com.toasttab.expediter.parser import com.toasttab.expediter.types.ApplicationType +import com.toasttab.expediter.types.ClassfileSource 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: ClassfileSource) = 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: ClassfileSource) : 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..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,10 +17,10 @@ package com.toasttab.expediter.provider import com.toasttab.expediter.parser.TypeParsers import com.toasttab.expediter.scanner.ClasspathScanner -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..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/scanner/ClasspathScanner.kt b/core/src/main/kotlin/com/toasttab/expediter/scanner/ClasspathScanner.kt index 329a5f2..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 java.io.File +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: String) -> 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: String, parse: (stream: InputStream, source: String) -> 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,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: ClassfileSource, parse: (stream: InputStream, source: ClassfileSource) -> 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: ClassfileSource, parse: (stream: InputStream, source: ClassfileSource) -> 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: ClassfileSource, parse: (stream: InputStream, source: ClassfileSource) -> 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: ClassfileSource, parse: (stream: InputStream, source: ClassfileSource) -> 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..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,6 +19,8 @@ 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 @@ -28,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) { @@ -48,7 +50,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 }) }) } } } @@ -75,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) { @@ -92,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) } @@ -112,9 +114,30 @@ class InspectedTypes( } fun resolveHierarchy(type: Type): ResolvedTypeHierarchy { - return traverse(type).resolve() + return hierarchy(type).resolve() } val classes: Collection get() = appTypes.appTypes.values + + 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::isRoot)) + + while (todo.isNotEmpty()) { + val next = todo.remove() + + if (reachable.put(next.name, next) == null) { + todo.addAll(next.referencedTypes.mapNotNull(appTypes.appTypes::get)) + todo.addAll(hierarchy(next).presentSuperTypes().filterIsInstance()) + } + } + + return reachable.values + } + } + 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 new file mode 100644 index 0000000..d43f64a --- /dev/null +++ b/model/src/main/kotlin/com/toasttab/expediter/types/ClassfileSource.kt @@ -0,0 +1,26 @@ +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. a raw jar file */ + UNKNOWN, + + /** compiled from source in the current project/subproject */ + SOURCE_SET, + + /** a different subproject within the same project */ + SUBPROJECT_DEPENDENCY, + + /** an external dependency managed by the build system */ + EXTERNAL_DEPENDENCY +} 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..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: String + val source: ClassfileSource ) : Type { override fun toString() = "ApplicationType[$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 4f0861d..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,16 +19,12 @@ 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 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 9988044..586dcd0 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 @@ -32,6 +33,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 @@ -50,18 +52,28 @@ 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() + @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 { @@ -80,6 +92,10 @@ abstract class ExpediterTask : DefaultTask() { platformConfigurationArtifacts.add(artifactCollection) } + fun sourceSet(sourceDirectorySet: SourceDirectorySet) { + applicationSourceSets.add(sourceDirectorySet) + } + @OutputFile lateinit var report: File @@ -105,6 +121,9 @@ abstract class ExpediterTask : DefaultTask() { @Input var failOnIssues: Boolean = false + @Input + var roots: RootType = RootType.ALL + @TaskAction fun execute() { val providers = mutableListOf() @@ -113,10 +132,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,22 +152,25 @@ 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) } - 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.sources() + files.sources() + applicationSourceSets.sources() + val issues = Expediter( ignore, - cache.get().resolve(applicationArtifacts + files), - PlatformTypeProviderChain(providers) + cache.get().resolve(typeSources), + PlatformTypeProviderChain(providers), + roots.selector, ).findIssues().subtract(ignores) val issueReport = IssueReport( diff --git a/plugin/src/main/kotlin/com/toasttab/expediter/gradle/Sources.kt b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/Sources.kt new file mode 100644 index 0000000..af0f75c --- /dev/null +++ b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/Sources.kt @@ -0,0 +1,30 @@ +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 + +fun ResolvedArtifactResult.source() = + when (val cid = id.componentIdentifier) { + 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() = 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 d933fc8..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 @@ -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 rootSelectorSpec: RootSelectorSpec = objectFactory.newInstance() + + fun roots(configure: Action) { + configure.execute(rootSelectorSpec) + } 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..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,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) { @@ -72,7 +76,7 @@ abstract class ExpediterExtension( } for (sourceSet in spec.sourceSets) { - files.from(project.sourceSet(sourceSet).java.classesDirectory) + sourceSet(project.sourceSet(sourceSet).java) } } @@ -122,6 +126,8 @@ abstract class ExpediterExtension( report = project.layout.buildDirectory.file("${key.reportName}.json").get().asFile failOnIssues = spec.failOnIssues + + 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/service/ApplicationTypeCache.kt b/plugin/src/main/kotlin/com/toasttab/expediter/gradle/service/ApplicationTypeCache.kt index 0b0ba16..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,15 +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.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() + 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..3bd6e23 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 @@ -205,6 +207,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/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..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,6 +20,8 @@ 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 @@ -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 { ClassfileSource(File(it), ClassfileSourceType.UNKNOWN, it) }) val p = Expediter(Ignore.NOTHING, scanner, PlatformClassloaderTypeProvider).findIssues() expectThat(p).containsExactlyInAnyOrder(