diff --git a/src/main/kotlin/io/bazel/kotlin/plugin/jdeps/JdepsGenExtension.kt b/src/main/kotlin/io/bazel/kotlin/plugin/jdeps/JdepsGenExtension.kt index b6343d3ee..17aa86200 100644 --- a/src/main/kotlin/io/bazel/kotlin/plugin/jdeps/JdepsGenExtension.kt +++ b/src/main/kotlin/io/bazel/kotlin/plugin/jdeps/JdepsGenExtension.kt @@ -20,6 +20,7 @@ import org.jetbrains.kotlin.descriptors.impl.LocalVariableDescriptor import org.jetbrains.kotlin.extensions.StorageComponentContainerContributor import org.jetbrains.kotlin.load.java.descriptors.JavaMethodDescriptor import org.jetbrains.kotlin.load.java.descriptors.JavaPropertyDescriptor +import org.jetbrains.kotlin.load.java.lazy.descriptors.LazyJavaClassDescriptor import org.jetbrains.kotlin.load.java.sources.JavaSourceElement import org.jetbrains.kotlin.load.java.structure.impl.classFiles.BinaryJavaClass import org.jetbrains.kotlin.load.java.structure.impl.classFiles.BinaryJavaField @@ -45,6 +46,7 @@ import org.jetbrains.kotlin.types.typeUtil.supertypes import java.io.BufferedOutputStream import java.io.File import java.nio.file.Paths +import java.util.* /** * Kotlin compiler extension that tracks classes (and corresponding classpath jars) needed to @@ -105,6 +107,18 @@ class JdepsGenExtension( ) } } + + fun getResourceName(descriptor: DeclarationDescriptorWithSource): String? { + if (descriptor.containingDeclaration is LazyJavaClassDescriptor) { + val fqName: String? = (descriptor.containingDeclaration as LazyJavaClassDescriptor)?.jClass?.fqName?.asString() + if (fqName != null) { + if (fqName.indexOf(".R.") > 0 || fqName.indexOf("R.") == 0) { + return fqName + "." + descriptor.name.asString() + } + } + } + return null + } } private val explicitClassesCanonicalPaths = mutableSetOf() @@ -143,8 +157,9 @@ class JdepsGenExtension( )?.let { explicitClassesCanonicalPaths.add(it) } } is FunctionDescriptor -> { + resultingDescriptor.returnType?.let { addImplicitDep(it) } resultingDescriptor.returnType?.let { - collectTypeReferences(it, isExplicit = false, collectTypeArguments = false) + collectTypeReferences(it, isExplicit = false) } resultingDescriptor.valueParameters.forEach { valueParameter -> collectTypeReferences(valueParameter.type, isExplicit = false) @@ -162,6 +177,7 @@ class JdepsGenExtension( } is JavaPropertyDescriptor -> { getClassCanonicalPath(resultingDescriptor)?.let { explicitClassesCanonicalPaths.add(it) } + getResourceName(resultingDescriptor)?.let { explicitClassesCanonicalPaths.add(it) } } is PropertyDescriptor -> { when (resultingDescriptor.containingDeclaration) { @@ -175,7 +191,7 @@ class JdepsGenExtension( explicitClassesCanonicalPaths.add(virtualFileClass.file.path) } } - collectTypeReferences(resultingDescriptor.type, isExplicit = false) + addImplicitDep(resultingDescriptor.type) } else -> return } @@ -191,6 +207,9 @@ class JdepsGenExtension( descriptor.typeConstructor.supertypes.forEach { collectTypeReferences(it) } + descriptor.annotations.forEach { annotation -> + collectTypeReferences(annotation.type) + } } is FunctionDescriptor -> { descriptor.returnType?.let { collectTypeReferences(it) } @@ -219,6 +238,14 @@ class JdepsGenExtension( } } + private fun addImplicitDep(it: KotlinType) { + getClassCanonicalPath(it.constructor)?.let { implicitClassesCanonicalPaths.add(it) } + } + + private fun addExplicitDep(it: KotlinType) { + getClassCanonicalPath(it.constructor)?.let { explicitClassesCanonicalPaths.add(it) } + } + /** * Records direct and indirect references for a given type. Direct references are explicitly * used in the code, e.g: a type declaration or a generic type declaration. Indirect references @@ -228,41 +255,35 @@ class JdepsGenExtension( private fun collectTypeReferences( kotlinType: KotlinType, isExplicit: Boolean = true, - collectTypeArguments: Boolean = true, - visitedKotlinTypes: MutableSet> = mutableSetOf(), ) { - val kotlintTypeAndIsExplicit = Pair(kotlinType, isExplicit) - if (!visitedKotlinTypes.contains(kotlintTypeAndIsExplicit)) { - visitedKotlinTypes.add(kotlintTypeAndIsExplicit) + if (isExplicit) { + addExplicitDep(kotlinType) + } else { + addImplicitDep(kotlinType) + } + kotlinType.supertypes().forEach { + addImplicitDep(it) + } + + collectTypeArguments(kotlinType, isExplicit) + } + + private fun collectTypeArguments( + kotlinType: KotlinType, + isExplicit: Boolean, + visitedKotlinTypes: MutableSet = mutableSetOf(), + ) { + visitedKotlinTypes.add(kotlinType) + kotlinType.arguments.map { it.type }.forEach { typeArgument -> if (isExplicit) { - getClassCanonicalPath(kotlinType.constructor)?.let { - explicitClassesCanonicalPaths.add(it) - } + addExplicitDep(typeArgument) } else { - getClassCanonicalPath(kotlinType.constructor)?.let { - implicitClassesCanonicalPaths.add(it) - } + addImplicitDep(typeArgument) } - - kotlinType.supertypes().forEach { supertype -> - collectTypeReferences( - supertype, - isExplicit = false, - collectTypeArguments = collectTypeArguments, - visitedKotlinTypes, - ) - } - - if (collectTypeArguments) { - kotlinType.arguments.map { it.type }.forEach { typeArgument -> - collectTypeReferences( - typeArgument, - isExplicit = isExplicit, - collectTypeArguments = true, - visitedKotlinTypes = visitedKotlinTypes, - ) - } + typeArgument.supertypes().forEach { addImplicitDep(it) } + if (!visitedKotlinTypes.contains(typeArgument)) { + collectTypeArguments(typeArgument, isExplicit, visitedKotlinTypes) } } } diff --git a/src/test/kotlin/io/bazel/kotlin/builder/tasks/jvm/KotlinBuilderJvmJdepsTest.kt b/src/test/kotlin/io/bazel/kotlin/builder/tasks/jvm/KotlinBuilderJvmJdepsTest.kt index c47ef7f80..ef5373db0 100644 --- a/src/test/kotlin/io/bazel/kotlin/builder/tasks/jvm/KotlinBuilderJvmJdepsTest.kt +++ b/src/test/kotlin/io/bazel/kotlin/builder/tasks/jvm/KotlinBuilderJvmJdepsTest.kt @@ -221,6 +221,138 @@ class KotlinBuilderJvmJdepsTest(private val enableK2Compiler: Boolean) { assertIncomplete(jdeps).isEmpty() } + @Test + fun `java annotation on class is an explict dep`() { + val dependingTarget = runJdepsCompileTask { c: KotlinJvmTestBuilder.TaskBuilder -> + c.addSource( + "AnotherClass.kt", + """ + package something + + @JavaAnnotation + class AnotherClass { + } + """ + ) + c.addDirectDependencies(TEST_FIXTURES_DEP) + } + val jdeps = depsProto(dependingTarget) + + assertThat(jdeps.ruleLabel).isEqualTo(dependingTarget.label()) + + assertExplicit(jdeps).contains(TEST_FIXTURES_DEP.singleCompileJar()) + assertImplicit(jdeps).isEmpty() + assertUnused(jdeps).isEmpty() + assertIncomplete(jdeps).isEmpty() + } + + @Test + fun `java annotation values on class are an explict dep`() { + val dependentTarget = runJdepsCompileTask { c: KotlinJvmTestBuilder.TaskBuilder -> + c.addSource( + "ClassToReference.kt", + """ + package something; + + @JavaAnnotation + class ClassToReference + """ + ) + c.addDirectDependencies(TEST_FIXTURES_DEP) + } + + val anotherDepTarget = runJdepsCompileTask { c: KotlinJvmTestBuilder.TaskBuilder -> + c.addSource( + "AnotherClassToReference.kt", + """ + package something; + + @JavaAnnotation(modules = [ClassToReference::class]) + class AnotherClassToReference + """ + ) + c.addDirectDependencies(dependentTarget) + c.addDirectDependencies(TEST_FIXTURES_DEP) + } + + val dependingTarget = runJdepsCompileTask { c: KotlinJvmTestBuilder.TaskBuilder -> + c.addSource( + "AnotherClass.kt", + """ + package something + + @JavaAnnotation(modules = [AnotherClassToReference::class]) + class AnotherClass { + } + """ + ) + c.addTransitiveDependencies(dependentTarget) + c.addDirectDependencies(anotherDepTarget) + c.addDirectDependencies(TEST_FIXTURES_DEP) + } + val jdeps = depsProto(dependingTarget) + + assertThat(jdeps.ruleLabel).isEqualTo(dependingTarget.label()) + + assertExplicit(jdeps).contains(TEST_FIXTURES_DEP.singleCompileJar()) + assertExplicit(jdeps).contains(anotherDepTarget.singleCompileJar()) + assertImplicit(jdeps).contains(dependentTarget.singleCompileJar()) + assertUnused(jdeps).isEmpty() + assertIncomplete(jdeps).isEmpty() + } + + @Test + fun `types referenced inside when statements are explicit deps`() { + val dependingTarget = runJdepsCompileTask { c: KotlinJvmTestBuilder.TaskBuilder -> + c.addSource( + "AnotherClass.kt", + """ + package something + + import pkg.assertion.ExampleException + + fun mapExceptions(exception: Exception) = when (exception) { + is ExampleException -> exception + else -> exception + } + """ + ) + c.addDirectDependencies(TEST_FIXTURES_DEP) + } + val jdeps = depsProto(dependingTarget) + + assertThat(jdeps.ruleLabel).isEqualTo(dependingTarget.label()) + + assertExplicit(jdeps).contains(TEST_FIXTURES_DEP.singleCompileJar()) + assertImplicit(jdeps).isEmpty() + assertUnused(jdeps).isEmpty() + assertIncomplete(jdeps).isEmpty() + } + + @Test + fun `java classes with inheritance`() { + val dependingTarget = runJdepsCompileTask { c: KotlinJvmTestBuilder.TaskBuilder -> + c.addSource( + "AnotherClass.kt", + """ + package something + + class SomeClass : SomeClassWithInheritance.BaseClassCallback { + } + """ + ) + c.addDirectDependencies(TEST_FIXTURES_DEP) + } + val jdeps = depsProto(dependingTarget) + + assertThat(jdeps.ruleLabel).isEqualTo(dependingTarget.label()) + + assertExplicit(jdeps).contains(TEST_FIXTURES_DEP.singleCompileJar()) + assertImplicit(jdeps).isEmpty() + assertUnused(jdeps).isEmpty() + assertIncomplete(jdeps).isEmpty() + } + @Test fun `java annotation on property is an explict dep`() { val dependingTarget = runJdepsCompileTask { c: KotlinJvmTestBuilder.TaskBuilder -> diff --git a/src/test/kotlin/io/bazel/kotlin/builder/tasks/testFixtures/BaseClass.java b/src/test/kotlin/io/bazel/kotlin/builder/tasks/testFixtures/BaseClass.java new file mode 100644 index 000000000..601fcccfd --- /dev/null +++ b/src/test/kotlin/io/bazel/kotlin/builder/tasks/testFixtures/BaseClass.java @@ -0,0 +1,4 @@ +package something; + +public abstract class BaseClass { +} diff --git a/src/test/kotlin/io/bazel/kotlin/builder/tasks/testFixtures/ExampleException.java b/src/test/kotlin/io/bazel/kotlin/builder/tasks/testFixtures/ExampleException.java new file mode 100644 index 000000000..ba145afbc --- /dev/null +++ b/src/test/kotlin/io/bazel/kotlin/builder/tasks/testFixtures/ExampleException.java @@ -0,0 +1,5 @@ +package pkg.assertion; + +public class ExampleException extends RuntimeException { + +} diff --git a/src/test/kotlin/io/bazel/kotlin/builder/tasks/testFixtures/JavaAnnotation.java b/src/test/kotlin/io/bazel/kotlin/builder/tasks/testFixtures/JavaAnnotation.java index 0b0875ec6..91b0cd369 100644 --- a/src/test/kotlin/io/bazel/kotlin/builder/tasks/testFixtures/JavaAnnotation.java +++ b/src/test/kotlin/io/bazel/kotlin/builder/tasks/testFixtures/JavaAnnotation.java @@ -2,7 +2,10 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Array; @Retention(RetentionPolicy.RUNTIME) public @interface JavaAnnotation { -} \ No newline at end of file + + Class[] modules() default {}; +} diff --git a/src/test/kotlin/io/bazel/kotlin/builder/tasks/testFixtures/SomeClassWithInheritance.java b/src/test/kotlin/io/bazel/kotlin/builder/tasks/testFixtures/SomeClassWithInheritance.java new file mode 100644 index 000000000..10a5d62f2 --- /dev/null +++ b/src/test/kotlin/io/bazel/kotlin/builder/tasks/testFixtures/SomeClassWithInheritance.java @@ -0,0 +1,9 @@ +package something; + +public class SomeClassWithInheritance extends BaseClass { + + + interface BaseClassCallback { + + } +}