[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:
Yan Zhulanow
2023-08-22 16:48:28 +09:00
committed by Space Team
parent 4b80c2197f
commit b96c1a644f
9 changed files with 184 additions and 2 deletions
@@ -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 {
@@ -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 {
@@ -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()))
}
}
}
@@ -0,0 +1 @@
text.takeUnless { it.isEmpty() }?.length
@@ -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
@@ -0,0 +1,7 @@
// ATTACH_DUPLICATE_STDLIB
fun test(text: String) {
<caret>consume(text)
}
fun consume(text: String) {}
@@ -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
}
@@ -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()
}
}
@@ -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,