[KT-58817] Implicit receiver for code completion

Prior to this commit code completion for .kts files was supported only
partially. Script has an implicit receiver - base class representing a
script itself, the point where its basic API resides. The knowledge of
this receiver was missing.

There are at least two context where this knowledge is crucial:
1. Code highlighting (worked fine)
2. Code completion (failed)

`FirScriptConfiguratorExtension` is responsible for filling
`FirScriptBuilder` with base script class (in addition to other
properties). See usages of [1] in its implementation.

The thing is that resolution during the completion works a bit
different. Instead of converting the entire `KtFile` it's interested
in `KtScript` only. See usages of [2].
`RawFirBuilder.Visitor.visitScript` is where implicit receivers were
missing.

Code completion and inspections applied to `.kts` files already have
`FirScript` and don't require its full reconstruction with expensive
`FirScriptConfiguratorExtension`. `RawFirBuilder.Visitor` was modified
to support sometimes already existing `FirElement`.

----------------------------------------------------------------
[1]: ScriptCompilationConfiguration.baseClass
[2]: RawFirBuilder.Visitor.convertScript
This commit is contained in:
Andrei Klunnyi
2023-05-17 15:21:28 +02:00
committed by Space Team
parent 2a1d4a42ae
commit 6535278bd3
5 changed files with 133 additions and 105 deletions
@@ -41,38 +41,16 @@ import kotlin.script.experimental.host.StringScriptSource
class FirScriptConfiguratorExtensionImpl(
session: FirSession,
// TODO: left here because it seems it will be needed soon, remove supression if used or remove the param if it is not the case
@Suppress("UNUSED_PARAMETER") hostConfiguration: ScriptingHostConfiguration
@Suppress("UNUSED_PARAMETER") hostConfiguration: ScriptingHostConfiguration,
) : FirScriptConfiguratorExtension(session) {
@OptIn(SymbolInternals::class)
override fun FirScriptBuilder.configure(fileBuilder: FirFileBuilder) {
val sourceFile = fileBuilder.sourceFile ?: return
// TODO: rewrite/extract decision logic for clarity
val compilationConfiguration = session.scriptDefinitionProviderService?.let { providerService ->
fileBuilder.sourceFile?.toSourceCode()?.let { script ->
val ktFile = (script as? KtFileScriptSource)?.ktFile ?: error("only PSI scripts are supported at the moment")
providerService.configurationProvider?.getScriptConfigurationResult(ktFile)?.valueOrNull()?.configuration
?: providerService.definitionProvider?.findDefinition(script)?.compilationConfiguration
} ?: providerService.definitionProvider?.getDefaultDefinition()?.compilationConfiguration
}
if (compilationConfiguration != null) {
compilationConfiguration[ScriptCompilationConfiguration.defaultImports]?.forEach { defaultImport ->
val trimmed = defaultImport.trim()
val endsWithStar = trimmed.endsWith("*")
val stripped = if (endsWithStar) trimmed.substring(0, trimmed.length - 2) else trimmed
val fqName = FqName.fromSegments(stripped.split("."))
fileBuilder.imports += buildImport {
fileBuilder.sourceFile?.project()?.let {
val dummyElement = KtPsiFactory(it, markGenerated = true).createColon()
source = KtFakeSourceElement(dummyElement, KtFakeSourceElementKind.ImplicitImport)
}
importedFqName = fqName
isAllUnder = endsWithStar
}
}
compilationConfiguration[ScriptCompilationConfiguration.baseClass]?.let { baseClass ->
withConfigurationIfAny(sourceFile) { configuration ->
// TODO: rewrite/extract decision logic for clarity
configuration[ScriptCompilationConfiguration.baseClass]?.let { baseClass ->
val baseClassFqn = FqName.fromSegments(baseClass.typeName.split("."))
contextReceivers.add(buildContextReceiverWithFqName(baseClassFqn))
@@ -88,8 +66,8 @@ class FirScriptConfiguratorExtensionImpl(
origin = FirDeclarationOrigin.ScriptCustomization
// TODO: copy type parameters?
returnTypeRef = baseCtorParameter.returnTypeRef
this.name = baseCtorParameter.name
this.symbol = FirPropertySymbol(this.name)
name = baseCtorParameter.name
symbol = FirPropertySymbol(name)
status = FirDeclarationStatusImpl(Visibilities.Local, Modality.FINAL)
isLocal = true
isVar = false
@@ -98,12 +76,10 @@ class FirScriptConfiguratorExtensionImpl(
}
}
}
compilationConfiguration[ScriptCompilationConfiguration.implicitReceivers]?.forEach { implicitReceiver ->
configuration[ScriptCompilationConfiguration.implicitReceivers]?.forEach { implicitReceiver ->
contextReceivers.add(buildContextReceiverWithFqName(FqName.fromSegments(implicitReceiver.typeName.split("."))))
}
compilationConfiguration[ScriptCompilationConfiguration.providedProperties]?.forEach { propertyName, propertyType ->
configuration[ScriptCompilationConfiguration.providedProperties]?.forEach { propertyName, propertyType ->
val typeRef = buildUserTypeRef {
isMarkedNullable = propertyType.isNullable
propertyType.typeName.split(".").forEach {
@@ -115,21 +91,53 @@ class FirScriptConfiguratorExtensionImpl(
moduleData = session.moduleData
origin = FirDeclarationOrigin.ScriptCustomization
returnTypeRef = typeRef
this.name = Name.identifier(propertyName)
this.symbol = FirPropertySymbol(this.name)
name = Name.identifier(propertyName)
symbol = FirPropertySymbol(name)
status = FirDeclarationStatusImpl(Visibilities.Local, Modality.FINAL)
isLocal = true
isVar = false
}
)
}
configuration[ScriptCompilationConfiguration.annotationsForSamWithReceivers]?.forEach {
_knownAnnotationsForSamWithReceiver.add(it.typeName)
}
compilationConfiguration[ScriptCompilationConfiguration.annotationsForSamWithReceivers]?.forEach {
configuration[ScriptCompilationConfiguration.defaultImports]?.forEach { defaultImport ->
val trimmed = defaultImport.trim()
val endsWithStar = trimmed.endsWith("*")
val stripped = if (endsWithStar) trimmed.substring(0, trimmed.length - 2) else trimmed
val fqName = FqName.fromSegments(stripped.split("."))
fileBuilder.imports += buildImport {
fileBuilder.sourceFile?.project()?.let {
val dummyElement = KtPsiFactory(it, markGenerated = true).createColon()
source = KtFakeSourceElement(dummyElement, KtFakeSourceElementKind.ImplicitImport)
}
importedFqName = fqName
isAllUnder = endsWithStar
}
}
configuration[ScriptCompilationConfiguration.annotationsForSamWithReceivers]?.forEach {
_knownAnnotationsForSamWithReceiver.add(it.typeName)
}
}
}
private fun withConfigurationIfAny(file: KtSourceFile, body: (ScriptCompilationConfiguration) -> Unit) {
val configuration = session.scriptDefinitionProviderService?.let { providerService ->
val sourceCode = file.toSourceCode()
val ktFile = sourceCode?.originalKtFile()
with(providerService) {
ktFile?.let { configurationFor(it) }
?: sourceCode?.let { configurationFor(it) }
?: defaultConfiguration()
}
}
configuration?.let { body.invoke(it) }
}
private fun buildContextReceiverWithFqName(baseClassFqn: FqName) =
buildContextReceiver {
typeRef = buildUserTypeRef {
@@ -156,6 +164,19 @@ class FirScriptConfiguratorExtensionImpl(
private fun KtSourceFile.project(): Project? = (toSourceCode() as? KtFileScriptSource)?.ktFile?.project
private fun SourceCode.originalKtFile(): KtFile =
(this as? KtFileScriptSource)?.ktFile?.originalFile as? KtFile
?: error("only PSI scripts are supported at the moment")
private fun FirScriptDefinitionProviderService.configurationFor(file: KtFile): ScriptCompilationConfiguration? =
configurationProvider?.getScriptConfigurationResult(file)?.valueOrNull()?.configuration
private fun FirScriptDefinitionProviderService.configurationFor(sourceCode: SourceCode): ScriptCompilationConfiguration? =
definitionProvider?.findDefinition(sourceCode)?.compilationConfiguration
private fun FirScriptDefinitionProviderService.defaultConfiguration(): ScriptCompilationConfiguration? =
definitionProvider?.getDefaultDefinition()?.compilationConfiguration
fun KtSourceFile.toSourceCode(): SourceCode? = when (this) {
is KtPsiSourceFile -> (psiFile as? KtFile)?.let(::KtFileScriptSource) ?: VirtualFileScriptSource(psiFile.virtualFile)
is KtVirtualFileSourceFile -> VirtualFileScriptSource(virtualFile)