diff --git a/detekt-test-utils/api/detekt-test-utils.api b/detekt-test-utils/api/detekt-test-utils.api index 6e61e268505..c102c1d78a6 100644 --- a/detekt-test-utils/api/detekt-test-utils.api +++ b/detekt-test-utils/api/detekt-test-utils.api @@ -48,6 +48,7 @@ public final class io/github/detekt/test/utils/StringPrintStream : java/io/Print public abstract interface annotation class io/gitlab/arturbosch/detekt/rules/KotlinCoreEnvironmentTest : java/lang/annotation/Annotation { public abstract fun additionalJavaSourcePaths ()[Ljava/lang/String; + public abstract fun additionalTypes ()[Ljava/lang/Class; } public final class io/gitlab/arturbosch/detekt/rules/KotlinEnvironmentTestSetupKt { diff --git a/detekt-test-utils/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/KotlinEnvironmentTestSetup.kt b/detekt-test-utils/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/KotlinEnvironmentTestSetup.kt index 97f59ef5e36..4dc1f41a008 100644 --- a/detekt-test-utils/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/KotlinEnvironmentTestSetup.kt +++ b/detekt-test-utils/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/KotlinEnvironmentTestSetup.kt @@ -12,6 +12,7 @@ import org.spekframework.spek2.dsl.Root import org.spekframework.spek2.lifecycle.CachingMode import java.io.File import java.nio.file.Path +import kotlin.reflect.KClass @Deprecated( "This is specific to Spek and will be removed in a future release. Documentation has been updated to " + @@ -33,6 +34,7 @@ fun Root.setupKotlinEnvironment(additionalJavaSourceRootPath: Path? = null) { @Target(AnnotationTarget.CLASS) @ExtendWith(KotlinEnvironmentResolver::class) annotation class KotlinCoreEnvironmentTest( + val additionalTypes: Array> = [], val additionalJavaSourcePaths: Array = [] ) @@ -48,7 +50,10 @@ internal class KotlinEnvironmentResolver : ParameterResolver { override fun resolveParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Any { val closeableWrapper = extensionContext.wrapper ?: CloseableWrapper( - createEnvironment(additionalJavaSourceRootPaths = extensionContext.additionalJavaSourcePaths()) + createEnvironment( + additionalJavaSourceRootPaths = extensionContext.additionalJavaSourcePaths(), + additionalRootPaths = extensionContext.additionalRootPaths().toList() + ) ).also { extensionContext.wrapper = it } return closeableWrapper.wrapper.env } @@ -61,6 +66,12 @@ internal class KotlinEnvironmentResolver : ParameterResolver { .find { it is KotlinCoreEnvironmentTest } as? KotlinCoreEnvironmentTest ?: return emptyList() return annotation.additionalJavaSourcePaths.map { resourceAsPath(it).toFile() } } + + private fun ExtensionContext.additionalRootPaths(): Set { + val annotation = requiredTestClass.annotations + .find { it is KotlinCoreEnvironmentTest } as? KotlinCoreEnvironmentTest ?: return emptySet() + return annotation.additionalTypes.map { File(it.java.protectionDomain.codeSource.location.path) }.toSet() + } } private class CloseableWrapper(val wrapper: KotlinCoreEnvironmentWrapper) : diff --git a/detekt-test-utils/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/KotlinCoreEnvironmentTestSpec.kt b/detekt-test-utils/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/KotlinCoreEnvironmentTestSpec.kt new file mode 100644 index 00000000000..1518c3ebac9 --- /dev/null +++ b/detekt-test-utils/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/KotlinCoreEnvironmentTestSpec.kt @@ -0,0 +1,86 @@ +package io.gitlab.arturbosch.detekt.rules + +import io.github.detekt.test.utils.compileContentForTest +import org.assertj.core.api.AbstractAssert +import org.assertj.core.api.Assertions.assertThat +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment +import org.jetbrains.kotlin.cli.jvm.compiler.NoScopeRecordCliBindingTrace +import org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM +import org.jetbrains.kotlin.psi.KtClass +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameOrNull +import org.jetbrains.kotlin.resolve.descriptorUtil.getAllSuperclassesWithoutAny +import org.jetbrains.kotlin.resolve.lazy.declarations.FileBasedDeclarationProviderFactory +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +internal class KotlinCoreEnvironmentTestSpec { + + private val code = """ + import org.assertj.core.api.AbstractAssert + + class CustomAssert(actual: String) : AbstractAssert(actual, CustomAssert::class.java) + """.trimIndent() + + @Nested + @KotlinCoreEnvironmentTest + inner class `Without additional types`(private val env: KotlinCoreEnvironment) { + @Test + fun `no assertj api are available on classpath`() { + val actual = code.compileAndGetSuperTypes(env) + + assertThat(actual).isEmpty() + } + } + + @Nested + inner class `With additional types` { + val expected = listOf("org.assertj.core.api.AbstractAssert") + + @Nested + @KotlinCoreEnvironmentTest(additionalTypes = [AbstractAssert::class]) + inner class `Only addiontial types`(private val env: KotlinCoreEnvironment) { + @Test + fun `types from detekt api are available on classpath`() { + val actual = code.compileAndGetSuperTypes(env) + + assertThat(actual).isEqualTo(expected) + } + } + + @Nested + @KotlinCoreEnvironmentTest(additionalTypes = [AbstractAssert::class, CharRange::class]) + inner class `Also types that are already available`(private val env: KotlinCoreEnvironment) { + @Test + fun `no conflict if types are added multiple times`() { + val actual = code.compileAndGetSuperTypes(env) + + assertThat(actual).isEqualTo(expected) + } + } + } + + private fun String.compileAndGetSuperTypes(env: KotlinCoreEnvironment): List { + val ktFile = compileContentForTest(this) + val ktClass = ktFile.findChildByClass(KtClass::class.java)!! + val bindingContext = env.getContextForPaths(listOf(ktFile)) + + return bindingContext[BindingContext.CLASS, ktClass] + ?.getAllSuperclassesWithoutAny() + ?.map { checkNotNull(it.fqNameOrNull()).toString() } + .orEmpty() + } +} + +// copied from KotlinCoreEnvironmentExtensions.kt +// this is not ideal +private fun KotlinCoreEnvironment.getContextForPaths(paths: List): BindingContext = + TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration( + this.project, + paths, + NoScopeRecordCliBindingTrace(), + this.configuration, + this::createPackagePartProvider, + ::FileBasedDeclarationProviderFactory + ).bindingContext