[AA] Allow specifying REx file contents in testdata.

Tests can now specify the code generated by a resolve extension from
within the test's testdata. Module-level directives control whether
resolve extensions are enabled for that module, as well as package names
and source shadowing regexes. File-level directives allow a `// FILE:`
block within the testdata to be converted into a KtResolveExtensionFile
and removed from the module as a whole. (This requires a new
`ModuleStructureTransformer`, because we need to be able to entirely
remove the files in question.)

Any test can add support for these directives by calling
`KtResolveExtensionTestSupport.configure` from within their
`configureTest` stanza. This allows this functionality to be used in
conjuction with any test base class.

^KT-59329
This commit is contained in:
Justin Paupore
2023-06-14 23:49:03 -07:00
committed by Ilya Kirillov
parent 7a69f13fd6
commit 734b87b97e
41 changed files with 666 additions and 565 deletions
@@ -1,24 +0,0 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.analysis.api.impl.base.test.cases.references
import com.intellij.psi.search.GlobalSearchScope
import org.jetbrains.kotlin.analysis.api.impl.base.test.util.KtMultiModuleResolveExtensionProviderForTest
import org.jetbrains.kotlin.analysis.api.resolve.extensions.KtResolveExtensionFile
import org.jetbrains.kotlin.analysis.api.resolve.extensions.KtResolveExtensionProvider
import org.jetbrains.kotlin.analysis.project.structure.KtSourceModule
import org.jetbrains.kotlin.name.FqName
abstract class AbstractMultiModuleReferenceResolveWithResolveExtensionTest : AbstractReferenceResolveWithResolveExtensionTest() {
override fun createResolveExtensionProvider(
files: List<KtResolveExtensionFile>,
packages: Set<FqName>,
shadowedScope: GlobalSearchScope,
): KtResolveExtensionProvider =
KtMultiModuleResolveExtensionProviderForTest(files, packages, shadowedScope) { module ->
module is KtSourceModule && module.moduleName == "extendedModule"
}
}
@@ -5,74 +5,12 @@
package org.jetbrains.kotlin.analysis.api.impl.base.test.cases.references
import com.intellij.openapi.module.Module
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.search.GlobalSearchScope
import org.jetbrains.kotlin.analysis.api.impl.base.test.util.KtResolveExtensionFileForTests
import org.jetbrains.kotlin.analysis.api.impl.base.test.util.KtResolveExtensionProviderForTestPreAnalysisHandler
import org.jetbrains.kotlin.analysis.api.resolve.extensions.KtResolveExtensionFile
import org.jetbrains.kotlin.analysis.api.resolve.extensions.KtResolveExtensionProvider
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.test.bind
import org.jetbrains.kotlin.analysis.api.impl.base.test.cases.resolve.extensions.KtResolveExtensionTestSupport
import org.jetbrains.kotlin.test.builders.TestConfigurationBuilder
abstract class AbstractReferenceResolveWithResolveExtensionTest : AbstractReferenceResolveTest() {
abstract fun createResolveExtensionProvider(
files: List<KtResolveExtensionFile>,
packages: Set<FqName>,
shadowedScope: GlobalSearchScope,
): KtResolveExtensionProvider
override fun configureTest(builder: TestConfigurationBuilder) {
super.configureTest(builder)
val provider = createResolveExtensionProvider(
files = listOf(
KtResolveExtensionFileForTests(
"extension1.kt",
packageName = FqName("generated"),
topLevelClassifiersNames = setOf("GeneratedClass1"),
topLevelCallableNames = setOf(
"generatedTopLevelFunction1",
"generatedTopLevelExtensionFunction1",
"generatedOverloadedExtensionFunction",
),
fileText = """|package generated
|
|class GeneratedClass1 {
| fun generatedClassMember1(): Int
|}
|
|fun generatedTopLevelFunction1(): GeneratedClass2
|
|fun String.generatedTopLevelExtensionFunction1(boolean: Boolean): Int
|
|fun Any.generatedOverloadedExtensionFunction(): Int
""".trimMargin()
),
KtResolveExtensionFileForTests(
"extension2.kt",
packageName = FqName("generated"),
topLevelClassifiersNames = setOf("GeneratedClass2"),
topLevelCallableNames = setOf(),
fileText = """|package generated
|
|class GeneratedClass2 {
| fun generatedClassMember2(): GeneratedClass1
|}
""".trimMargin(),
)
),
packages = setOf(FqName("generated")),
shadowedScope = object : GlobalSearchScope() {
override fun contains(file: VirtualFile): Boolean = ".hidden." in file.name
override fun isSearchInModuleContent(aModule: Module): Boolean = false
override fun isSearchInLibraries(): Boolean = false
}
)
with(builder) {
usePreAnalysisHandlers(::KtResolveExtensionProviderForTestPreAnalysisHandler.bind(listOf(provider)))
}
KtResolveExtensionTestSupport.configure(builder)
}
}
@@ -1,21 +0,0 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.analysis.api.impl.base.test.cases.references
import com.intellij.psi.search.GlobalSearchScope
import org.jetbrains.kotlin.analysis.api.impl.base.test.util.KtSingleModuleResolveExtensionProviderForTest
import org.jetbrains.kotlin.analysis.api.resolve.extensions.KtResolveExtensionFile
import org.jetbrains.kotlin.analysis.api.resolve.extensions.KtResolveExtensionProvider
import org.jetbrains.kotlin.name.FqName
abstract class AbstractSingleModuleReferenceResolveWithResolveExtensionTest : AbstractReferenceResolveWithResolveExtensionTest() {
override fun createResolveExtensionProvider(
files: List<KtResolveExtensionFile>,
packages: Set<FqName>,
shadowedScope: GlobalSearchScope,
): KtResolveExtensionProvider =
KtSingleModuleResolveExtensionProviderForTest(files, packages, shadowedScope)
}
@@ -3,7 +3,7 @@
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.analysis.api.impl.base.test.util
package org.jetbrains.kotlin.analysis.api.impl.base.test.cases.resolve.extensions
import com.intellij.mock.MockProject
import com.intellij.openapi.util.ModificationTracker
@@ -19,46 +19,26 @@ import org.jetbrains.kotlin.analysis.project.structure.KtModule
import org.jetbrains.kotlin.analysis.test.framework.services.environmentManager
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.test.services.PreAnalysisHandler
import org.jetbrains.kotlin.test.services.TestModuleStructure
import org.jetbrains.kotlin.test.services.TestServices
class KtSingleModuleResolveExtensionProviderForTest(
class KtResolveExtensionProviderForTest(
private val files: List<KtResolveExtensionFile>,
private val packages: Set<FqName>,
private val shadowedScope: GlobalSearchScope,
) : KtResolveExtensionProvider() {
override fun provideExtensionsFor(module: KtModule): List<KtResolveExtension> {
return listOf(KtResolveExtensionForTest(files, packages, shadowedScope))
}
}
class KtMultiModuleResolveExtensionProviderForTest(
private val files: List<KtResolveExtensionFile>,
private val packages: Set<FqName>,
private val shadowedScope: GlobalSearchScope,
private val hasResolveExtension: (KtModule) -> Boolean,
private val hasResolveExtension: (KtModule) -> Boolean = { true }
) : KtResolveExtensionProvider() {
override fun provideExtensionsFor(module: KtModule): List<KtResolveExtension> {
if (!hasResolveExtension(module)) return emptyList()
return listOf(KtResolveExtensionForTest(files, packages, shadowedScope))
}
}
class KtResolveExtensionProviderForTestPreAnalysisHandler(
testServices: TestServices,
private val providers: List<KtResolveExtensionProvider>,
) : PreAnalysisHandler(testServices) {
override fun preprocessModuleStructure(moduleStructure: TestModuleStructure) {
fun register(testServices: TestServices) {
val project = testServices.environmentManager.getProject() as MockProject
val extensionPoint = project.extensionArea.getExtensionPoint(KtResolveExtensionProvider.EP_NAME)
for (provider in providers) {
extensionPoint.registerExtension(provider, project)
}
val extensionPoint = project.extensionArea.getExtensionPoint(EP_NAME)
extensionPoint.registerExtension(this, project)
}
}
class KtResolveExtensionForTest(
private val files: List<KtResolveExtensionFile>,
private val packages: Set<FqName>,
@@ -76,6 +56,7 @@ class KtResolveExtensionFileForTests(
topLevelClassifiersNames: Set<String>,
topLevelCallableNames: Set<String>,
private val fileText: String,
private val psiTargetProvider: KtResolveExtensionReferencePsiTargetsProvider? = null
) : KtResolveExtensionFile() {
private val topLevelClassifiersNames: Set<Name> = topLevelClassifiersNames.mapTo(mutableSetOf()) { Name.identifier(it) }
@@ -87,11 +68,12 @@ class KtResolveExtensionFileForTests(
override fun buildFileText(): String = fileText
override fun createPsiTargetsProvider(): KtResolveExtensionReferencePsiTargetsProvider {
return object : KtResolveExtensionReferencePsiTargetsProvider() {
override fun KtAnalysisSession.getReferenceTargetsForSymbol(symbol: KtSymbol): Collection<PsiElement> {
return emptyList()
}
}
private object EmptyReferencePsiTargetsProvider : KtResolveExtensionReferencePsiTargetsProvider() {
override fun KtAnalysisSession.getReferenceTargetsForSymbol(symbol: KtSymbol): Collection<PsiElement> =
emptyList()
}
}
override fun createPsiTargetsProvider(): KtResolveExtensionReferencePsiTargetsProvider =
psiTargetProvider ?: EmptyReferencePsiTargetsProvider
}
@@ -0,0 +1,141 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.analysis.api.impl.base.test.cases.resolve.extensions
import com.intellij.openapi.module.Module
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.search.GlobalSearchScope
import org.jetbrains.kotlin.analysis.api.resolve.extensions.KtResolveExtensionFile
import org.jetbrains.kotlin.analysis.project.structure.KtSourceModule
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.test.TestInfrastructureInternals
import org.jetbrains.kotlin.test.builders.TestConfigurationBuilder
import org.jetbrains.kotlin.test.directives.ConfigurationDirectives
import org.jetbrains.kotlin.test.directives.model.DirectiveApplicability
import org.jetbrains.kotlin.test.directives.model.SimpleDirectivesContainer
import org.jetbrains.kotlin.test.model.TestFile
import org.jetbrains.kotlin.test.model.TestModule
import org.jetbrains.kotlin.test.services.ModuleStructureTransformer
import org.jetbrains.kotlin.test.services.TestModuleStructure
import org.jetbrains.kotlin.test.services.TestServices
import org.jetbrains.kotlin.test.services.impl.TestModuleStructureImpl
object KtResolveExtensionTestSupport {
object Directives : SimpleDirectivesContainer() {
val WITH_RESOLVE_EXTENSION by directive(
"Enable resolve extension for this module",
DirectiveApplicability.Module,
)
val RESOLVE_EXTENSION_PACKAGE by valueDirective(
"Package name for resolve extension or resolve extension file",
DirectiveApplicability.Any,
::FqName,
)
val RESOLVE_EXTENSION_SHADOWED by valueDirective(
"File name regex(es) to shadow in resolve extension",
DirectiveApplicability.Module,
::Regex,
)
val RESOLVE_EXTENSION_FILE by directive(
"Treat this file as a KtResolveExtensionFile and remove it from the module",
DirectiveApplicability.File,
)
val RESOLVE_EXTENSION_CLASSIFIER by stringDirective(
"Name(s) of top-level classifier(s) in this resolve extension file",
DirectiveApplicability.File,
)
val RESOLVE_EXTENSION_CALLABLE by stringDirective(
"Names of top-level callables in this resolve extension file",
DirectiveApplicability.File,
)
}
@OptIn(TestInfrastructureInternals::class)
private class ResolveExtensionDirectiveModuleStructureTransformer(
private val testServices: TestServices,
) : ModuleStructureTransformer() {
override fun transformModuleStructure(moduleStructure: TestModuleStructure): TestModuleStructure {
check(Directives.WITH_RESOLVE_EXTENSION in moduleStructure.allDirectives) {
"configureResolveExtensions() was called, but no modules specify WITH_RESOLVE_EXTENSION."
}
return TestModuleStructureImpl(
moduleStructure.modules.map { it.transformAndRegisterResolveExtension() },
moduleStructure.originalTestDataFiles,
)
}
private fun TestModule.transformAndRegisterResolveExtension(): TestModule {
val (resolveExtensionTestFiles, regularFiles) = files.partition { Directives.RESOLVE_EXTENSION_FILE in it.directives }
if (Directives.WITH_RESOLVE_EXTENSION !in directives) {
check(resolveExtensionTestFiles.isEmpty()) {
"Module $name has resolve extension files, but does not specify WITH_RESOLVE_EXTENSION"
}
return this
}
val packageNames = directives[Directives.RESOLVE_EXTENSION_PACKAGE].toSet()
check(packageNames.isNotEmpty()) {
"Module $name does not specify any RESOLVE_EXTENSION_PACKAGE"
}
val shadowedPatterns = directives[Directives.RESOLVE_EXTENSION_SHADOWED]
val shadowedScope = if (shadowedPatterns.isNotEmpty()) {
object : GlobalSearchScope() {
override fun contains(file: VirtualFile): Boolean =
shadowedPatterns.any { it.containsMatchIn(file.name) }
override fun isSearchInModuleContent(module: Module): Boolean = true
override fun isSearchInLibraries(): Boolean = true
}
} else {
GlobalSearchScope.EMPTY_SCOPE
}
val singleModulePackageName = packageNames.singleOrNull()
val ktResolveExtensionFiles =
resolveExtensionTestFiles.map { it.toKtResolveExtensionFile(singleModulePackageName) }
val provider = KtResolveExtensionProviderForTest(ktResolveExtensionFiles, packageNames, shadowedScope) {
it is KtSourceModule && it.moduleName == name
}
provider.register(testServices)
return this.copy(files = regularFiles)
}
private fun TestFile.toKtResolveExtensionFile(moduleSinglePackageName: FqName?): KtResolveExtensionFile {
val packageName = directives[Directives.RESOLVE_EXTENSION_PACKAGE].singleOrNull()
?: moduleSinglePackageName
?: error("Extension file $name must specify exactly one RESOLVE_EXTENSION_PACKAGE")
val classifierNames = directives[Directives.RESOLVE_EXTENSION_CLASSIFIER].toSet()
val callableNames = directives[Directives.RESOLVE_EXTENSION_CALLABLE].toSet()
check(classifierNames.isNotEmpty() || callableNames.isNotEmpty()) {
"Extension file $name does not specify any RESOLVE_EXTENSION_CLASSIFIER or RESOLVE_EXTENSION_CALLABLE"
}
return KtResolveExtensionFileForTests(
fileName = name,
packageName = packageName,
topLevelClassifiersNames = classifierNames,
topLevelCallableNames = callableNames,
fileText = originalContent,
)
}
}
@OptIn(TestInfrastructureInternals::class)
fun configure(builder: TestConfigurationBuilder) = with(builder) {
defaultDirectives {
+ConfigurationDirectives.WITH_STDLIB
}
useDirectives(Directives)
useModuleStructureTransformers(KtResolveExtensionTestSupport::ResolveExtensionDirectiveModuleStructureTransformer)
}
}