[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
@@ -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,