[LL API] Fix code fragment compilation in library sources (KT-61383)
In complex projects, there might be several library copies (with the same or different versions). As there is no way to build a reliable dependency graph between libraries, a project library depends on all other libraries. As a result, there might be several declarations in the classpath with the same name and signature. Normally, K2 issues a 'resolution ambiguity' error on calls to such libraries. It is acceptable for resolution, as resolution errors are never shown in the library code. However, the backend, to which 'evaluate expression' needs to pass FIR afterwards, is not designed for compiling ambiguous (and non-completed) calls.
This commit is contained in:
+6
@@ -85,6 +85,12 @@ public class FirIdeNormalAnalysisLibrarySourceModuleCompilerFacilityTestGenerate
|
||||
runTest("analysis/analysis-api/testData/components/compilerFacility/compilation/codeFragments/customName.kt");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestMetadata("duplicateLibrary.kt")
|
||||
public void testDuplicateLibrary() throws Exception {
|
||||
runTest("analysis/analysis-api/testData/components/compilerFacility/compilation/codeFragments/duplicateLibrary.kt");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestMetadata("imports.kt")
|
||||
public void testImports() throws Exception {
|
||||
|
||||
+6
@@ -85,6 +85,12 @@ public class FirIdeNormalAnalysisSourceModuleCompilerFacilityTestGenerated exten
|
||||
runTest("analysis/analysis-api/testData/components/compilerFacility/compilation/codeFragments/customName.kt");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestMetadata("duplicateLibrary.kt")
|
||||
public void testDuplicateLibrary() throws Exception {
|
||||
runTest("analysis/analysis-api/testData/components/compilerFacility/compilation/codeFragments/duplicateLibrary.kt");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestMetadata("imports.kt")
|
||||
public void testImports() throws Exception {
|
||||
|
||||
+14
-2
@@ -17,12 +17,14 @@ import org.jetbrains.kotlin.analysis.test.framework.base.AbstractAnalysisApiBase
|
||||
import org.jetbrains.kotlin.analysis.test.framework.services.expressionMarkerProvider
|
||||
import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
|
||||
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
|
||||
import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
|
||||
import org.jetbrains.kotlin.cli.jvm.config.JvmClasspathRoot
|
||||
import org.jetbrains.kotlin.codegen.BytecodeListingTextCollectingVisitor
|
||||
import org.jetbrains.kotlin.codegen.ClassBuilderFactories
|
||||
import org.jetbrains.kotlin.codegen.forTestCompile.ForTestCompileRuntime
|
||||
import org.jetbrains.kotlin.config.CommonConfigurationKeys
|
||||
import org.jetbrains.kotlin.config.CompilerConfiguration
|
||||
import org.jetbrains.kotlin.config.JVMConfigurationKeys
|
||||
import org.jetbrains.kotlin.fileClasses.javaFileFacadeFqName
|
||||
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors
|
||||
import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
|
||||
import org.jetbrains.kotlin.ir.util.DumpIrTreeOptions
|
||||
@@ -33,6 +35,7 @@ import org.jetbrains.kotlin.test.directives.ConfigurationDirectives
|
||||
import org.jetbrains.kotlin.test.directives.JvmEnvironmentConfigurationDirectives
|
||||
import org.jetbrains.kotlin.test.directives.model.SimpleDirectivesContainer
|
||||
import org.jetbrains.kotlin.test.model.TestModule
|
||||
import org.jetbrains.kotlin.test.services.EnvironmentConfigurator
|
||||
import org.jetbrains.kotlin.test.services.TestServices
|
||||
import org.jetbrains.kotlin.test.services.assertions
|
||||
import org.jetbrains.org.objectweb.asm.ClassReader
|
||||
@@ -110,7 +113,7 @@ abstract class AbstractCompilerFacilityTest : AbstractAnalysisApiBasedSingleModu
|
||||
super.configureTest(builder)
|
||||
with(builder) {
|
||||
useDirectives(Directives)
|
||||
useConfigurators()
|
||||
useConfigurators(::CompilerFacilityEnvironmentConfigurator)
|
||||
defaultDirectives {
|
||||
+ConfigurationDirectives.WITH_STDLIB
|
||||
+JvmEnvironmentConfigurationDirectives.FULL_JDK
|
||||
@@ -168,6 +171,15 @@ abstract class AbstractCompilerFacilityTest : AbstractAnalysisApiBasedSingleModu
|
||||
val CODE_FRAGMENT_IMPORT by stringDirective("Import directive for a code fragment")
|
||||
val CODE_FRAGMENT_CLASS_NAME by stringDirective("Short name of a code fragment class")
|
||||
val CODE_FRAGMENT_METHOD_NAME by stringDirective("Name of a code fragment facade method")
|
||||
val ATTACH_DUPLICATE_STDLIB by directive("Attach the 'stdlib-jvm-minimal-for-test' library to simulate duplicate stdlib dependency")
|
||||
}
|
||||
}
|
||||
|
||||
private class CompilerFacilityEnvironmentConfigurator(testServices: TestServices) : EnvironmentConfigurator(testServices) {
|
||||
override fun configureCompilerConfiguration(configuration: CompilerConfiguration, module: TestModule) {
|
||||
if (module.directives.contains(AbstractCompilerFacilityTest.Directives.ATTACH_DUPLICATE_STDLIB)) {
|
||||
configuration.add(CLIConfigurationKeys.CONTENT_ROOTS, JvmClasspathRoot(ForTestCompileRuntime.minimalRuntimeJarForTests()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
text.takeUnless { it.isEmpty() }?.length
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
MODULE_FRAGMENT
|
||||
FILE fqName:<root> fileName:fragment.kt
|
||||
CLASS CLASS name:CodeFragment modality:FINAL visibility:public superTypes:[kotlin.Any]
|
||||
$this: VALUE_PARAMETER INSTANCE_RECEIVER name:<this> type:<root>.CodeFragment
|
||||
CONSTRUCTOR visibility:public <> () returnType:<root>.CodeFragment [primary]
|
||||
BLOCK_BODY
|
||||
DELEGATING_CONSTRUCTOR_CALL 'public constructor <init> () [primary] declared in kotlin.Any'
|
||||
FUN name:run visibility:public modality:FINAL <> (p0:kotlin.String) returnType:kotlin.Int?
|
||||
VALUE_PARAMETER name:p0 index:0 type:kotlin.String
|
||||
EXPRESSION_BODY
|
||||
BLOCK type=kotlin.Int? origin=null
|
||||
BLOCK type=kotlin.Int? origin=SAFE_CALL
|
||||
VAR IR_TEMPORARY_VARIABLE name:tmp_0 type:kotlin.String? [val]
|
||||
CALL 'public final fun takeUnless <T> (predicate: kotlin.Function1<T of kotlin.takeUnless, kotlin.Boolean>): T of kotlin.takeUnless? [inline] declared in kotlin' type=kotlin.String? origin=null
|
||||
<T>: kotlin.String
|
||||
$receiver: GET_VAR 'p0: kotlin.String declared in <root>.CodeFragment.run' type=kotlin.String origin=null
|
||||
predicate: FUN_EXPR type=kotlin.Function1<kotlin.String, kotlin.Boolean> origin=LAMBDA
|
||||
FUN LOCAL_FUNCTION_FOR_LAMBDA name:<anonymous> visibility:local modality:FINAL <> (it:kotlin.String) returnType:kotlin.Boolean
|
||||
VALUE_PARAMETER name:it index:0 type:kotlin.String
|
||||
BLOCK_BODY
|
||||
RETURN type=kotlin.Nothing from='local final fun <anonymous> (it: kotlin.String): kotlin.Boolean declared in <root>.CodeFragment.run'
|
||||
CALL 'public final fun isEmpty (): kotlin.Boolean [inline] declared in kotlin.text' type=kotlin.Boolean origin=null
|
||||
$receiver: GET_VAR 'it: kotlin.String declared in <root>.CodeFragment.run.<anonymous>' type=kotlin.String origin=null
|
||||
WHEN type=kotlin.Int? origin=null
|
||||
BRANCH
|
||||
if: CALL 'public final fun EQEQ (arg0: kotlin.Any?, arg1: kotlin.Any?): kotlin.Boolean declared in kotlin.internal.ir' type=kotlin.Boolean origin=EQEQ
|
||||
arg0: GET_VAR 'val tmp_0: kotlin.String? [val] declared in <root>.CodeFragment.run' type=kotlin.String? origin=null
|
||||
arg1: CONST Null type=kotlin.Nothing? value=null
|
||||
then: CONST Null type=kotlin.Nothing? value=null
|
||||
BRANCH
|
||||
if: CONST Boolean type=kotlin.Boolean value=true
|
||||
then: CALL 'public open fun <get-length> (): kotlin.Int declared in kotlin.String' type=kotlin.Int origin=GET_PROPERTY
|
||||
$this: GET_VAR 'val tmp_0: kotlin.String? [val] declared in <root>.CodeFragment.run' type=kotlin.String? origin=null
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// ATTACH_DUPLICATE_STDLIB
|
||||
|
||||
fun test(text: String) {
|
||||
<caret>consume(text)
|
||||
}
|
||||
|
||||
fun consume(text: String) {}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
public final class CodeFragment {
|
||||
// source: 'fragment.kt'
|
||||
public method <init>(): void
|
||||
public final static method run(p0: java.lang.String): java.lang.Integer
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* 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.low.level.api.fir.resolve.extensions
|
||||
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import org.jetbrains.kotlin.fir.psi
|
||||
import org.jetbrains.kotlin.fir.resolve.BodyResolveComponents
|
||||
import org.jetbrains.kotlin.fir.resolve.calls.Candidate
|
||||
import org.jetbrains.kotlin.fir.resolve.calls.ConeCallConflictResolver
|
||||
import org.jetbrains.kotlin.fir.resolve.calls.ConeCallConflictResolverFactory
|
||||
import org.jetbrains.kotlin.fir.resolve.inference.InferenceComponents
|
||||
import org.jetbrains.kotlin.fir.symbols.impl.FirCallableSymbol
|
||||
import org.jetbrains.kotlin.name.CallableId
|
||||
import org.jetbrains.kotlin.name.FqName
|
||||
import org.jetbrains.kotlin.psi.KtCodeFragment
|
||||
import org.jetbrains.kotlin.psi.KtFile
|
||||
import org.jetbrains.kotlin.resolve.calls.results.TypeSpecificityComparator
|
||||
|
||||
internal class LLLibraryScopeAwareCallConflictResolverFactory(
|
||||
private val delegateFactory: ConeCallConflictResolverFactory
|
||||
) : ConeCallConflictResolverFactory() {
|
||||
override fun create(
|
||||
typeSpecificityComparator: TypeSpecificityComparator,
|
||||
components: InferenceComponents,
|
||||
transformerComponents: BodyResolveComponents,
|
||||
): ConeCallConflictResolver {
|
||||
val delegate = delegateFactory.create(typeSpecificityComparator, components, transformerComponents)
|
||||
return LLLibraryScopeAwareConeCallConflictResolver(delegate, transformerComponents)
|
||||
}
|
||||
}
|
||||
|
||||
private class LLLibraryScopeAwareConeCallConflictResolver(
|
||||
private val delegate: ConeCallConflictResolver,
|
||||
private val bodyResolveComponents: BodyResolveComponents
|
||||
) : ConeCallConflictResolver() {
|
||||
override fun chooseMaximallySpecificCandidates(candidates: Set<Candidate>, discriminateAbstracts: Boolean): Set<Candidate> {
|
||||
if (candidates.size > 1 && bodyResolveComponents.file.psi is KtCodeFragment) {
|
||||
val filteredCandidates = filterCodeFragmentCandidates(candidates)
|
||||
return delegate.chooseMaximallySpecificCandidates(filteredCandidates, discriminateAbstracts)
|
||||
}
|
||||
|
||||
return delegate.chooseMaximallySpecificCandidates(candidates, discriminateAbstracts)
|
||||
}
|
||||
|
||||
// In complex projects, there might be several library copies (with the same or different versions).
|
||||
// As there is no way to build a reliable dependency graph between libraries, a project library depends on all other libraries.
|
||||
// As a result, there might be several declarations in the classpath with the same name and signature.
|
||||
// Normally, K2 issues a 'resolution ambiguity' error on calls to such libraries. It is sort of acceptable for resolution, as
|
||||
// resolution errors are never shown in the library code. However, the backend, to which 'evaluate expression' needs to pass FIR
|
||||
// afterwards, is not designed for compiling ambiguous (and non-completed) calls.
|
||||
// The code below scans for declaration duplicates, and chooses a set of them from a single class input (a JAR or a directory).
|
||||
// The logic is not ideal, as, in theory, versions might differ non-trivially: each artifact may have unique declarations.
|
||||
// However, such cases should be relatively rare, and to provide the candidate list more precisely, one would need to also compare
|
||||
// signatures of each declaration.
|
||||
private fun filterCodeFragmentCandidates(candidates: Set<Candidate>): Set<Candidate> {
|
||||
val binaryCallableCandidates = LinkedHashMap<CallableId, MutableMap<VirtualFile, MutableList<Candidate>>>()
|
||||
val otherCandidates = ArrayList<Candidate>()
|
||||
|
||||
for (candidate in candidates) {
|
||||
val symbol = candidate.symbol
|
||||
|
||||
if (symbol is FirCallableSymbol<*> && symbol.callableId.className == null) {
|
||||
val callableId = symbol.callableId
|
||||
|
||||
val symbolFile = symbol.fir.psi?.containingFile
|
||||
val symbolVirtualFile = symbolFile?.virtualFile
|
||||
if (symbolFile is KtFile && symbolFile.isCompiled && symbolVirtualFile != null) {
|
||||
val symbolRootVirtualFile = getSymbolRootFile(symbolVirtualFile, symbolFile.packageFqName)
|
||||
if (symbolRootVirtualFile != null) {
|
||||
binaryCallableCandidates
|
||||
.getOrPut(callableId, ::LinkedHashMap)
|
||||
.getOrPut(symbolRootVirtualFile, ::ArrayList)
|
||||
.add(candidate)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
otherCandidates.add(candidate)
|
||||
}
|
||||
|
||||
if (binaryCallableCandidates.isNotEmpty()) {
|
||||
return buildSet {
|
||||
addAll(otherCandidates)
|
||||
for (binaryCallableCandidateGroup in binaryCallableCandidates.values) {
|
||||
val chosenGroupKey = binaryCallableCandidateGroup.keys.maxBy { it.path }
|
||||
addAll(binaryCallableCandidateGroup[chosenGroupKey].orEmpty())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return candidates
|
||||
}
|
||||
|
||||
private fun getSymbolRootFile(virtualFile: VirtualFile, packageFqName: FqName): VirtualFile? {
|
||||
val nestingLevel = packageFqName.pathSegments().size
|
||||
return generateSequence(virtualFile) { it.parent }.drop(nestingLevel + 1).firstOrNull()
|
||||
}
|
||||
}
|
||||
+10
@@ -9,14 +9,18 @@ import com.intellij.openapi.project.Project
|
||||
import com.intellij.psi.search.GlobalSearchScope
|
||||
import org.jetbrains.kotlin.analysis.low.level.api.fir.project.structure.*
|
||||
import org.jetbrains.kotlin.analysis.low.level.api.fir.providers.*
|
||||
import org.jetbrains.kotlin.analysis.low.level.api.fir.resolve.extensions.LLLibraryScopeAwareCallConflictResolverFactory
|
||||
import org.jetbrains.kotlin.analysis.project.structure.*
|
||||
import org.jetbrains.kotlin.analysis.providers.createPackagePartProvider
|
||||
import org.jetbrains.kotlin.fir.BuiltinTypes
|
||||
import org.jetbrains.kotlin.fir.FirSession
|
||||
import org.jetbrains.kotlin.fir.SessionConfiguration
|
||||
import org.jetbrains.kotlin.fir.backend.jvm.FirJvmTypeMapper
|
||||
import org.jetbrains.kotlin.fir.deserialization.SingleModuleDataProvider
|
||||
import org.jetbrains.kotlin.fir.java.JavaSymbolProvider
|
||||
import org.jetbrains.kotlin.fir.java.deserialization.OptionalAnnotationClassesProvider
|
||||
import org.jetbrains.kotlin.fir.resolve.calls.ConeCallConflictResolverFactory
|
||||
import org.jetbrains.kotlin.fir.resolve.calls.callConflictResolverFactory
|
||||
import org.jetbrains.kotlin.fir.resolve.providers.FirSymbolProvider
|
||||
import org.jetbrains.kotlin.fir.resolve.scopes.wrapScopeWithJvmMapped
|
||||
import org.jetbrains.kotlin.fir.scopes.FirKotlinScopeProvider
|
||||
@@ -71,6 +75,7 @@ internal class LLFirJvmSessionFactory(project: Project) : LLFirAbstractSessionFa
|
||||
)
|
||||
register(JavaSymbolProvider::class, javaSymbolProvider)
|
||||
register(FirJvmTypeMapper::class, FirJvmTypeMapper(this))
|
||||
registerLibraryScopeAwareCallConflictResolverFactory()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,9 +84,14 @@ internal class LLFirJvmSessionFactory(project: Project) : LLFirAbstractSessionFa
|
||||
registerCommonJavaComponents(JavaModuleResolver.getInstance(project))
|
||||
registerJavaSpecificResolveComponents()
|
||||
register(FirJvmTypeMapper::class, FirJvmTypeMapper(this))
|
||||
registerLibraryScopeAwareCallConflictResolverFactory()
|
||||
}
|
||||
}
|
||||
|
||||
private fun FirSession.registerLibraryScopeAwareCallConflictResolverFactory() {
|
||||
register(ConeCallConflictResolverFactory::class, LLLibraryScopeAwareCallConflictResolverFactory(callConflictResolverFactory))
|
||||
}
|
||||
|
||||
override fun createProjectLibraryProvidersForScope(
|
||||
session: LLFirSession,
|
||||
moduleData: LLFirModuleData,
|
||||
|
||||
Reference in New Issue
Block a user