From 0f6f22d76bc39000d496b4e961991f4c7aab93c8 Mon Sep 17 00:00:00 2001 From: Yan Zhulanow Date: Wed, 5 Jul 2023 22:45:54 +0900 Subject: [PATCH] [Analysis API] Add API for code compilation The API replaces 'KotlinCompilerIde' in the IntelliJ IDEA plugin. Code moved to Analysis API as its K2 implementation severely depends on the internal compiler API (FIR). The API is going to be used in the JVM debugger evaluator, and in the Bytecode Tool Window. In this commit, only ordinary Kotlin are supported. In later commits, there will be also support for 'KtCodeFragment' files, as well as some test coverage. --- analysis/analysis-api-fe10/build.gradle.kts | 3 +- .../api/descriptors/CliFe10AnalysisFacade.kt | 3 +- .../api/descriptors/Fe10AnalysisFacade.kt | 6 +- .../api/descriptors/KtFe10AnalysisSession.kt | 1 + .../components/KtFe10CompilerFacility.kt | 197 +++++++++++++++ .../utils/InlineFunctionAnalyzer.kt | 131 ++++++++++ analysis/analysis-api-fir/build.gradle.kts | 5 + .../analysis/api/fir/KtFirAnalysisSession.kt | 2 + .../fir/components/KtFirCompilerFacility.kt | 232 ++++++++++++++++++ .../analysis-api-impl-base/build.gradle.kts | 1 + .../base/util/KtCompiledFileForOutputFile.kt | 21 ++ analysis/analysis-api/build.gradle.kts | 1 + .../kotlin/analysis/api/KtAnalysisSession.kt | 6 +- .../api/components/KtCompilerFacility.kt | 102 ++++++++ .../api/fir/api/targets/LLFirResolveTarget.kt | 18 ++ .../fir/compile/CompilationPeerCollector.kt | 168 +++++++++++++ .../testdata/compilationPeers/functionCall.kt | 23 ++ .../compilationPeers/functionCall.txt | 2 + .../compilationPeers/functionCallDeep.kt | 27 ++ .../compilationPeers/functionCallDeep.txt | 3 + .../compilationPeers/libraryFunctionCall.kt | 14 ++ .../compilationPeers/libraryFunctionCall.txt | 1 + .../testdata/compilationPeers/noInline.kt | 18 ++ .../testdata/compilationPeers/noInline.txt | 1 + .../testdata/compilationPeers/propertyCall.kt | 19 ++ .../compilationPeers/propertyCall.txt | 2 + .../AbstractCompilationPeerAnalysisTest.kt | 40 +++ .../CompilationPeerAnalysisTestGenerated.java | 56 +++++ .../tests/analysis/api/firLowLevel.kt | 4 + 29 files changed, 1103 insertions(+), 4 deletions(-) create mode 100644 analysis/analysis-api-fe10/src/org/jetbrains/kotlin/analysis/api/descriptors/components/KtFe10CompilerFacility.kt create mode 100644 analysis/analysis-api-fe10/src/org/jetbrains/kotlin/analysis/api/descriptors/utils/InlineFunctionAnalyzer.kt create mode 100644 analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/components/KtFirCompilerFacility.kt create mode 100644 analysis/analysis-api-impl-base/src/org/jetbrains/kotlin/analysis/api/impl/base/util/KtCompiledFileForOutputFile.kt create mode 100644 analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/components/KtCompilerFacility.kt create mode 100644 analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/compile/CompilationPeerCollector.kt create mode 100644 analysis/low-level-api-fir/testdata/compilationPeers/functionCall.kt create mode 100644 analysis/low-level-api-fir/testdata/compilationPeers/functionCall.txt create mode 100644 analysis/low-level-api-fir/testdata/compilationPeers/functionCallDeep.kt create mode 100644 analysis/low-level-api-fir/testdata/compilationPeers/functionCallDeep.txt create mode 100644 analysis/low-level-api-fir/testdata/compilationPeers/libraryFunctionCall.kt create mode 100644 analysis/low-level-api-fir/testdata/compilationPeers/libraryFunctionCall.txt create mode 100644 analysis/low-level-api-fir/testdata/compilationPeers/noInline.kt create mode 100644 analysis/low-level-api-fir/testdata/compilationPeers/noInline.txt create mode 100644 analysis/low-level-api-fir/testdata/compilationPeers/propertyCall.kt create mode 100644 analysis/low-level-api-fir/testdata/compilationPeers/propertyCall.txt create mode 100644 analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/AbstractCompilationPeerAnalysisTest.kt create mode 100644 analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/CompilationPeerAnalysisTestGenerated.java diff --git a/analysis/analysis-api-fe10/build.gradle.kts b/analysis/analysis-api-fe10/build.gradle.kts index 8e39a7656a8..94b697f046d 100644 --- a/analysis/analysis-api-fe10/build.gradle.kts +++ b/analysis/analysis-api-fe10/build.gradle.kts @@ -13,9 +13,10 @@ dependencies { implementation(project(":analysis:kt-references:kt-references-fe10")) implementation(project(":compiler:backend")) - implementation(project(":compiler:backend.jvm")) implementation(project(":compiler:backend-common")) implementation(project(":compiler:backend.common.jvm")) + implementation(project(":compiler:backend.jvm")) + implementation(project(":compiler:backend.jvm.entrypoint")) testApiJUnit5() testImplementation(project(":analysis:analysis-api-providers")) diff --git a/analysis/analysis-api-fe10/src/org/jetbrains/kotlin/analysis/api/descriptors/CliFe10AnalysisFacade.kt b/analysis/analysis-api-fe10/src/org/jetbrains/kotlin/analysis/api/descriptors/CliFe10AnalysisFacade.kt index 6d8f36fd5cb..b74b3e5456c 100644 --- a/analysis/analysis-api-fe10/src/org/jetbrains/kotlin/analysis/api/descriptors/CliFe10AnalysisFacade.kt +++ b/analysis/analysis-api-fe10/src/org/jetbrains/kotlin/analysis/api/descriptors/CliFe10AnalysisFacade.kt @@ -59,7 +59,8 @@ class CliFe10AnalysisFacade : Fe10AnalysisFacade { return getHandler(element).kotlinTypeRefiner ?: error("Resolution is not performed") } - override fun analyze(element: KtElement, mode: Fe10AnalysisFacade.AnalysisMode): BindingContext { + override fun analyze(elements: List, mode: Fe10AnalysisFacade.AnalysisMode): BindingContext { + val element = elements.firstOrNull() ?: return BindingContext.EMPTY return getResolveSession(element).bindingContext } diff --git a/analysis/analysis-api-fe10/src/org/jetbrains/kotlin/analysis/api/descriptors/Fe10AnalysisFacade.kt b/analysis/analysis-api-fe10/src/org/jetbrains/kotlin/analysis/api/descriptors/Fe10AnalysisFacade.kt index 38e1ee5fc41..b550351333b 100644 --- a/analysis/analysis-api-fe10/src/org/jetbrains/kotlin/analysis/api/descriptors/Fe10AnalysisFacade.kt +++ b/analysis/analysis-api-fe10/src/org/jetbrains/kotlin/analysis/api/descriptors/Fe10AnalysisFacade.kt @@ -35,7 +35,11 @@ interface Fe10AnalysisFacade { fun getOverloadingConflictResolver(element: KtElement): OverloadingConflictResolver> fun getKotlinTypeRefiner(element: KtElement): KotlinTypeRefiner - fun analyze(element: KtElement, mode: AnalysisMode = AnalysisMode.FULL): BindingContext + fun analyze(elements: List, mode: AnalysisMode = AnalysisMode.FULL): BindingContext + + fun analyze(element: KtElement, mode: AnalysisMode = AnalysisMode.FULL): BindingContext { + return analyze(listOf(element), mode) + } fun getOrigin(file: VirtualFile): KtSymbolOrigin diff --git a/analysis/analysis-api-fe10/src/org/jetbrains/kotlin/analysis/api/descriptors/KtFe10AnalysisSession.kt b/analysis/analysis-api-fe10/src/org/jetbrains/kotlin/analysis/api/descriptors/KtFe10AnalysisSession.kt index b87f81378a4..b115baa479e 100644 --- a/analysis/analysis-api-fe10/src/org/jetbrains/kotlin/analysis/api/descriptors/KtFe10AnalysisSession.kt +++ b/analysis/analysis-api-fe10/src/org/jetbrains/kotlin/analysis/api/descriptors/KtFe10AnalysisSession.kt @@ -70,6 +70,7 @@ class KtFe10AnalysisSession( override val substitutorFactoryImpl: KtSubstitutorFactory = KtFe10SubstitutorFactory(this) override val symbolProviderByJavaPsiImpl: KtSymbolProviderByJavaPsi = KtFe10SymbolProviderByJavaPsi(this) override val resolveExtensionInfoProviderImpl: KtResolveExtensionInfoProvider = KtFe10ResolveExtensionInfoProvider(this) + override val compilerFacilityImpl: KtCompilerFacility = KtFe10CompilerFacility(this) override fun createContextDependentCopy(originalKtFile: KtFile, elementToReanalyze: KtElement): KtAnalysisSession = withValidityAssertion { diff --git a/analysis/analysis-api-fe10/src/org/jetbrains/kotlin/analysis/api/descriptors/components/KtFe10CompilerFacility.kt b/analysis/analysis-api-fe10/src/org/jetbrains/kotlin/analysis/api/descriptors/components/KtFe10CompilerFacility.kt new file mode 100644 index 00000000000..42c487ccf14 --- /dev/null +++ b/analysis/analysis-api-fe10/src/org/jetbrains/kotlin/analysis/api/descriptors/components/KtFe10CompilerFacility.kt @@ -0,0 +1,197 @@ +/* + * 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.descriptors.components + +import org.jetbrains.kotlin.analysis.api.components.KtCompilerFacility +import org.jetbrains.kotlin.analysis.api.components.KtCompilationResult +import org.jetbrains.kotlin.analysis.api.components.KtCompilerTarget +import org.jetbrains.kotlin.analysis.api.descriptors.Fe10AnalysisFacade.AnalysisMode +import org.jetbrains.kotlin.analysis.api.descriptors.KtFe10AnalysisSession +import org.jetbrains.kotlin.analysis.api.descriptors.components.base.Fe10KtAnalysisSessionComponent +import org.jetbrains.kotlin.analysis.api.descriptors.utils.InlineFunctionAnalyzer +import org.jetbrains.kotlin.analysis.api.diagnostics.KtDiagnostic +import org.jetbrains.kotlin.analysis.api.impl.base.util.KtCompiledFileForOutputFile +import org.jetbrains.kotlin.backend.common.phaser.PhaseConfig +import org.jetbrains.kotlin.backend.jvm.FacadeClassSourceShimForFragmentCompilation +import org.jetbrains.kotlin.backend.jvm.JvmGeneratorExtensionsImpl +import org.jetbrains.kotlin.backend.jvm.JvmIrCodegenFactory +import org.jetbrains.kotlin.backend.jvm.jvmPhases +import org.jetbrains.kotlin.codegen.DefaultCodegenFactory +import org.jetbrains.kotlin.codegen.KotlinCodegenFacade +import org.jetbrains.kotlin.codegen.state.GenerationState +import org.jetbrains.kotlin.config.* +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.diagnostics.Severity +import org.jetbrains.kotlin.load.kotlin.toSourceElement +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.resolve.diagnostics.Diagnostics +import org.jetbrains.kotlin.resolve.source.PsiSourceFile +import org.jetbrains.kotlin.serialization.deserialization.descriptors.DeserializedContainerSource + +/** + * Whether unbound IR symbols should be stubbed instead of linked. + * This should be enabled if the compiled file could refer to symbols defined in another file of the same module. + * Such symbols are not compiled (only the file is passed to the backend) and so they cannot be linked from a dependency. + * + * The option only has an effect when the IR backend is active. + */ +val STUB_UNBOUND_IR_SYMBOLS: CompilerConfigurationKey = CompilerConfigurationKey("stub unbound IR symbols") + +internal class KtFe10CompilerFacility( + override val analysisSession: KtFe10AnalysisSession +) : KtCompilerFacility(), Fe10KtAnalysisSessionComponent { + override fun compile(file: KtFile, configuration: CompilerConfiguration, target: KtCompilerTarget): KtCompilationResult { + if (file is KtCodeFragment) { + throw UnsupportedOperationException("Code fragments are not supported in K1 implementation") + } + + val classBuilderFactory = when (target) { + is KtCompilerTarget.Jvm -> target.classBuilderFactory + } + + val effectiveConfiguration = configuration + .copy() + .apply { + put(JVMConfigurationKeys.DO_NOT_CLEAR_BINDING_CONTEXT, true) + } + + val useIrBackend = effectiveConfiguration.getBoolean(JVMConfigurationKeys.IR) + val disableInline = effectiveConfiguration.getBoolean(CommonConfigurationKeys.DISABLE_INLINE) + + // The binding context needs to be built from all files with reachable inline functions, as such files may contain classes whose + // descriptors must be available in the binding context for the IR backend. Note that the full bytecode is only generated for + // `file` because of filtering in `generateClassFilter`, while only select declarations from other files are generated if needed + // by the backend. + val inlineAnalyzer = InlineFunctionAnalyzer(analysisContext, analyzeOnlyReifiedInlineFunctions = disableInline) + inlineAnalyzer.analyze(file) + + val filesToCompile = inlineAnalyzer.allFiles() + val bindingContext = analysisContext.analyze(filesToCompile, AnalysisMode.ALL_COMPILER_CHECKS) + + val frontendErrors = computeErrors(bindingContext.diagnostics, effectiveConfiguration) + if (frontendErrors.isNotEmpty()) { + return KtCompilationResult.Failure(frontendErrors) + } + + // The IR backend will try to regenerate object literals defined in inline functions from generated class files during inlining. + // Hence, we need to be aware of which object declarations are defined in the relevant inline functions. + val inlineObjectDeclarations = when { + useIrBackend -> inlineAnalyzer.inlineObjectDeclarations() + else -> setOf() + } + + val inlineObjectDeclarationFiles = inlineObjectDeclarations.mapTo(mutableSetOf()) { it.containingKtFile } + + class GenerateClassFilter : GenerationState.GenerateClassFilter() { + override fun shouldGeneratePackagePart(ktFile: KtFile): Boolean { + return file === ktFile || inlineObjectDeclarationFiles.contains(ktFile) + } + + override fun shouldAnnotateClass(processingClassOrObject: KtClassOrObject): Boolean { + return true + } + + override fun shouldGenerateClass(processingClassOrObject: KtClassOrObject): Boolean { + return processingClassOrObject.containingKtFile === file || + processingClassOrObject is KtObjectDeclaration && inlineObjectDeclarations.contains(processingClassOrObject) + } + + override fun shouldGenerateScript(script: KtScript): Boolean { + return script.containingKtFile === file + } + + override fun shouldGenerateCodeFragment(script: KtCodeFragment) = false + } + + val generateClassFilter = GenerateClassFilter() + + val codegenFactory = when { + useIrBackend -> createJvmIrCodegenFactory(effectiveConfiguration) + else -> DefaultCodegenFactory + } + + val state = GenerationState.Builder( + file.project, + classBuilderFactory, + analysisContext.resolveSession.moduleDescriptor, + bindingContext, + filesToCompile, + effectiveConfiguration, + ).generateDeclaredClassFilter(generateClassFilter) + .codegenFactory(codegenFactory) + .build() + + try { + KotlinCodegenFacade.compileCorrectFiles(state) + + val backendErrors = computeErrors(state.collectedExtraJvmDiagnostics, effectiveConfiguration) + if (backendErrors.isNotEmpty()) { + return KtCompilationResult.Failure(backendErrors) + } + + val outputFiles = state.factory.asList().map(::KtCompiledFileForOutputFile) + return KtCompilationResult.Success(outputFiles) + } finally { + state.destroy() + } + } + + private fun computeErrors(diagnostics: Diagnostics, configuration: CompilerConfiguration): List { + val allowedErrorFilter = configuration[ALLOWED_ERROR_FILTER] ?: { true } + + return buildList { + for (diagnostic in diagnostics.all()) { + if (diagnostic.severity == Severity.ERROR) { + val ktDiagnostic = KtFe10Diagnostic(diagnostic, token) + if (!allowedErrorFilter(ktDiagnostic)) { + add(ktDiagnostic) + } + } + } + } + } + + private fun createJvmIrCodegenFactory(configuration: CompilerConfiguration): JvmIrCodegenFactory { + val stubUnboundIrSymbols = configuration[STUB_UNBOUND_IR_SYMBOLS] ?: false + + val jvmGeneratorExtensions = if (stubUnboundIrSymbols) { + object : JvmGeneratorExtensionsImpl(configuration) { + override fun getContainerSource(descriptor: DeclarationDescriptor): DeserializedContainerSource? { + // Stubbed top-level function IR symbols (from other source files in the module) require a parent facade class to be + // generated, which requires a container source to be provided. Without a facade class, function IR symbols will have + // an `IrExternalPackageFragment` parent, which trips up code generation during IR lowering. + val psiSourceFile = + descriptor.toSourceElement.containingFile as? PsiSourceFile ?: return super.getContainerSource(descriptor) + return FacadeClassSourceShimForFragmentCompilation(psiSourceFile) + } + } + } else { + JvmGeneratorExtensionsImpl(configuration) + } + + val ideCodegenSettings = JvmIrCodegenFactory.IdeCodegenSettings( + shouldStubAndNotLinkUnboundSymbols = stubUnboundIrSymbols, + shouldDeduplicateBuiltInSymbols = stubUnboundIrSymbols, + + // Because the file to compile may be contained in a "common" multiplatform module, an `expect` declaration doesn't necessarily + // have an obvious associated `actual` symbol. `shouldStubOrphanedExpectSymbols` generates stubs for such `expect` declarations. + shouldStubOrphanedExpectSymbols = true, + + // Likewise, the file to compile may be contained in a "platform" multiplatform module, where the `actual` declaration is + // referenced in the symbol table automatically, but not its `expect` counterpart, because it isn't contained in the files to + // compile. `shouldReferenceUndiscoveredExpectSymbols` references such `expect` symbols in the symbol table so that they can + // subsequently be stubbed. + shouldReferenceUndiscoveredExpectSymbols = true, + ) + + return JvmIrCodegenFactory( + configuration, + PhaseConfig(jvmPhases), + jvmGeneratorExtensions = jvmGeneratorExtensions, + ideCodegenSettings = ideCodegenSettings, + ) + } +} \ No newline at end of file diff --git a/analysis/analysis-api-fe10/src/org/jetbrains/kotlin/analysis/api/descriptors/utils/InlineFunctionAnalyzer.kt b/analysis/analysis-api-fe10/src/org/jetbrains/kotlin/analysis/api/descriptors/utils/InlineFunctionAnalyzer.kt new file mode 100644 index 00000000000..0e64837804e --- /dev/null +++ b/analysis/analysis-api-fe10/src/org/jetbrains/kotlin/analysis/api/descriptors/utils/InlineFunctionAnalyzer.kt @@ -0,0 +1,131 @@ +/* + * 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.descriptors.utils + +import org.jetbrains.kotlin.analysis.api.descriptors.Fe10AnalysisContext +import org.jetbrains.kotlin.descriptors.CallableDescriptor +import org.jetbrains.kotlin.descriptors.PropertyDescriptor +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.references.fe10.util.DescriptorToSourceUtilsIde +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall +import org.jetbrains.kotlin.resolve.inline.InlineUtil +import org.jetbrains.kotlin.serialization.deserialization.descriptors.DeserializedSimpleFunctionDescriptor + +class InlineFunctionAnalyzer( + private val analysisContext: Fe10AnalysisContext, + private val analyzeOnlyReifiedInlineFunctions: Boolean, +) { + private val analyzedElements: MutableSet = mutableSetOf() + private val inlineFunctionsWithBody: MutableSet = mutableSetOf() + + /** + * Collects all inline function calls in an [element] (usually a file) and follows each transitively. + */ + fun analyze(element: KtElement) { + val project = element.project + val nextInlineFunctions = HashSet() + + element.accept(object : KtTreeVisitorVoid() { + override fun visitExpression(expression: KtExpression) { + super.visitExpression(expression) + + val bindingContext = analysisContext.analyze(expression) + val call = bindingContext.get(BindingContext.CALL, expression) ?: return + val resolvedCall = bindingContext.get(BindingContext.RESOLVED_CALL, call) + checkResolveCall(resolvedCall) + } + + override fun visitDestructuringDeclaration(destructuringDeclaration: KtDestructuringDeclaration) { + super.visitDestructuringDeclaration(destructuringDeclaration) + + val bindingContext = analysisContext.analyze(destructuringDeclaration) + + for (entry in destructuringDeclaration.entries) { + val resolvedCall = bindingContext.get(BindingContext.COMPONENT_RESOLVED_CALL, entry) + checkResolveCall(resolvedCall) + } + } + + override fun visitForExpression(expression: KtForExpression) { + super.visitForExpression(expression) + + val bindingContext = analysisContext.analyze(expression) + + checkResolveCall(bindingContext.get(BindingContext.LOOP_RANGE_ITERATOR_RESOLVED_CALL, expression.loopRange)) + checkResolveCall(bindingContext.get(BindingContext.LOOP_RANGE_HAS_NEXT_RESOLVED_CALL, expression.loopRange)) + checkResolveCall(bindingContext.get(BindingContext.LOOP_RANGE_NEXT_RESOLVED_CALL, expression.loopRange)) + } + + private fun checkResolveCall(resolvedCall: ResolvedCall<*>?) { + if (resolvedCall == null) return + + val descriptor = resolvedCall.resultingDescriptor + if (descriptor is DeserializedSimpleFunctionDescriptor) return + + analyzeNextIfInline(descriptor) + + if (descriptor is PropertyDescriptor) { + for (accessor in descriptor.accessors) { + analyzeNextIfInline(accessor) + } + } + } + + private fun analyzeNextIfInline(descriptor: CallableDescriptor) { + if (!InlineUtil.isInline(descriptor) || analyzeOnlyReifiedInlineFunctions && !hasReifiedTypeParameters(descriptor)) { + return + } + + val declaration = DescriptorToSourceUtilsIde.getAnyDeclaration(project, descriptor) + if (declaration != null && declaration is KtDeclarationWithBody && !analyzedElements.contains(declaration)) { + nextInlineFunctions.add(declaration) + return + } + } + }) + + analyzedElements.add(element) + + if (nextInlineFunctions.isNotEmpty()) { + for (inlineFunction in nextInlineFunctions) { + if (inlineFunction.bodyExpression != null) { + inlineFunctionsWithBody.add(inlineFunction) + analyze(inlineFunction) + } + } + analyzedElements.addAll(nextInlineFunctions) + } + } + + private fun hasReifiedTypeParameters(descriptor: CallableDescriptor): Boolean { + return descriptor.typeParameters.any { it.isReified } + } + + /** + * Returns the list of files that contain all reached inline functions. + */ + fun allFiles(): List = analyzedElements.mapTo(mutableSetOf()) { it.containingKtFile }.toList() + + /** + * Returns the set of [KtObjectDeclaration]s which are defined as an object literal in one of the reached inline functions. + */ + fun inlineObjectDeclarations(): Set { + val results = mutableSetOf() + + inlineFunctionsWithBody.forEach { inlineFunction -> + val body = inlineFunction.bodyExpression ?: return@forEach + body.accept(object : KtTreeVisitorVoid() { + override fun visitObjectLiteralExpression(expression: KtObjectLiteralExpression) { + super.visitObjectLiteralExpression(expression) + results.add(expression.objectDeclaration) + } + }) + } + + return results + } +} \ No newline at end of file diff --git a/analysis/analysis-api-fir/build.gradle.kts b/analysis/analysis-api-fir/build.gradle.kts index bc269aff0c3..16e718df8b0 100644 --- a/analysis/analysis-api-fir/build.gradle.kts +++ b/analysis/analysis-api-fir/build.gradle.kts @@ -21,6 +21,11 @@ dependencies { api(project(":analysis:analysis-api-impl-base")) api(project(":analysis:light-classes-base")) api(project(":compiler:backend.common.jvm")) + implementation(project(":compiler:cli-base")) + implementation(project(":compiler:backend")) + implementation(project(":compiler:backend.jvm.entrypoint")) + implementation(project(":compiler:ir.backend.common")) + implementation(project(":compiler:ir.serialization.jvm")) api(intellijCore()) implementation(project(":analysis:analysis-api-providers")) implementation(project(":analysis:analysis-internal-utils")) diff --git a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/KtFirAnalysisSession.kt b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/KtFirAnalysisSession.kt index 008e4f66204..7c4ae8fd042 100644 --- a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/KtFirAnalysisSession.kt +++ b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/KtFirAnalysisSession.kt @@ -128,6 +128,8 @@ private constructor( override val resolveExtensionInfoProviderImpl: KtResolveExtensionInfoProvider = KtFirResolveExtensionInfoProvider(this) + override val compilerFacilityImpl: KtCompilerFacility = KtFirCompilerFacility(this) + @Suppress("AnalysisApiMissingLifetimeCheck") override fun createContextDependentCopy(originalKtFile: KtFile, elementToReanalyze: KtElement): KtAnalysisSession { check(mode == AnalysisSessionMode.REGULAR) { diff --git a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/components/KtFirCompilerFacility.kt b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/components/KtFirCompilerFacility.kt new file mode 100644 index 00000000000..1a2a4faab9b --- /dev/null +++ b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/components/KtFirCompilerFacility.kt @@ -0,0 +1,232 @@ +/* + * 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.fir.components + +import com.intellij.openapi.progress.ProgressManager +import org.jetbrains.kotlin.analysis.api.components.KtCompilationResult +import org.jetbrains.kotlin.analysis.api.components.KtCompilerFacility +import org.jetbrains.kotlin.analysis.api.components.KtCompilerTarget +import org.jetbrains.kotlin.analysis.api.diagnostics.KtDiagnostic +import org.jetbrains.kotlin.analysis.api.fir.KtFirAnalysisSession +import org.jetbrains.kotlin.analysis.api.impl.base.util.KtCompiledFileForOutputFile +import org.jetbrains.kotlin.analysis.low.level.api.fir.api.DiagnosticCheckerFilter +import org.jetbrains.kotlin.analysis.low.level.api.fir.api.collectDiagnosticsForFile +import org.jetbrains.kotlin.analysis.low.level.api.fir.api.getOrBuildFirFile +import org.jetbrains.kotlin.analysis.low.level.api.fir.api.targets.LLFirWholeFileResolveTarget +import org.jetbrains.kotlin.analysis.low.level.api.fir.api.targets.resolve +import org.jetbrains.kotlin.analysis.low.level.api.fir.compile.CompilationPeerCollector +import org.jetbrains.kotlin.backend.common.phaser.PhaseConfig +import org.jetbrains.kotlin.backend.jvm.* +import org.jetbrains.kotlin.builtins.DefaultBuiltIns +import org.jetbrains.kotlin.cli.jvm.compiler.NoScopeRecordCliBindingTrace +import org.jetbrains.kotlin.codegen.CodegenFactory +import org.jetbrains.kotlin.codegen.state.GenerationState +import org.jetbrains.kotlin.config.* +import org.jetbrains.kotlin.constant.EvaluatedConstTracker +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.diagnostics.DiagnosticMarker +import org.jetbrains.kotlin.diagnostics.KtPsiDiagnostic +import org.jetbrains.kotlin.diagnostics.Severity +import org.jetbrains.kotlin.fir.backend.Fir2IrCommonMemberStorage +import org.jetbrains.kotlin.fir.backend.Fir2IrConfiguration +import org.jetbrains.kotlin.fir.backend.Fir2IrConverter +import org.jetbrains.kotlin.fir.backend.jvm.* +import org.jetbrains.kotlin.fir.declarations.FirFile +import org.jetbrains.kotlin.fir.declarations.FirResolvePhase +import org.jetbrains.kotlin.fir.pipeline.signatureComposerForJvmFir2Ir +import org.jetbrains.kotlin.ir.backend.jvm.serialization.JvmIrMangler +import org.jetbrains.kotlin.ir.declarations.impl.IrFactoryImpl +import org.jetbrains.kotlin.load.kotlin.toSourceElement +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.resolve.source.PsiSourceFile +import org.jetbrains.kotlin.serialization.deserialization.descriptors.DeserializedContainerSource + +internal class KtFirCompilerFacility( + override val analysisSession: KtFirAnalysisSession +) : KtCompilerFacility(), KtFirAnalysisSessionComponent { + override fun compile(file: KtFile, configuration: CompilerConfiguration, target: KtCompilerTarget): KtCompilationResult { + val classBuilderFactory = when (target) { + is KtCompilerTarget.Jvm -> target.classBuilderFactory + } + + val effectiveConfiguration = configuration + .copy() + .apply { + put(CommonConfigurationKeys.USE_FIR, true) + } + + val mainFirFile = getFullyResolvedFirFile(file) + + val frontendDiagnostics = file.collectDiagnosticsForFile(firResolveSession, DiagnosticCheckerFilter.ONLY_COMMON_CHECKERS) + val frontendErrors = computeErrors(frontendDiagnostics, effectiveConfiguration) + + if (frontendErrors.isNotEmpty()) { + return KtCompilationResult.Failure(frontendErrors) + } + + val compilationPeerData = CompilationPeerCollector.process(mainFirFile) + + val filesToCompile = compilationPeerData.files + + val firFilesToCompile = filesToCompile.map(::getFullyResolvedFirFile) + + val generateClassFilter = SingleFileGenerateClassFilter(file, compilationPeerData.inlinedClasses) + val fir2IrExtensions = JvmFir2IrExtensions(effectiveConfiguration, JvmIrDeserializerImpl(), JvmIrMangler) + + val fir2IrConfiguration = Fir2IrConfiguration( + effectiveConfiguration.languageVersionSettings, + linkViaSignatures = false, + effectiveConfiguration[CommonConfigurationKeys.EVALUATED_CONST_TRACKER] ?: EvaluatedConstTracker.create(), + effectiveConfiguration[CommonConfigurationKeys.INLINE_CONST_TRACKER] + ) + + val fir2IrResult = Fir2IrConverter.createModuleFragmentWithSignaturesIfNeeded( + rootModuleSession, + firResolveSession.getScopeSessionFor(rootModuleSession), + firFilesToCompile, + fir2IrExtensions, + fir2IrConfiguration, + JvmIrMangler, + IrFactoryImpl, + FirJvmVisibilityConverter, + Fir2IrJvmSpecialAnnotationSymbolProvider(), + DefaultBuiltIns.Instance, + Fir2IrCommonMemberStorage(signatureComposerForJvmFir2Ir(false), FirJvmKotlinMangler()), + initializedIrBuiltIns = null + ) + + ProgressManager.checkCanceled() + + val bindingContext = NoScopeRecordCliBindingTrace().bindingContext + val codegenFactory = createJvmIrCodegenFactory(effectiveConfiguration) + + val generationState = GenerationState.Builder( + project, + classBuilderFactory, + fir2IrResult.irModuleFragment.descriptor, + bindingContext, + filesToCompile, + effectiveConfiguration, + ).generateDeclaredClassFilter(generateClassFilter) + .codegenFactory(codegenFactory) + .build() + + try { + generationState.beforeCompile() + + ProgressManager.checkCanceled() + + codegenFactory.generateModuleInFrontendIRMode( + generationState, + fir2IrResult.irModuleFragment, + fir2IrResult.components.symbolTable, + fir2IrResult.components.irProviders, + JvmFir2IrExtensions(effectiveConfiguration, JvmIrDeserializerImpl(), JvmIrMangler), + FirJvmBackendExtension(fir2IrResult.components, null), + fir2IrResult.pluginContext + ) + + CodegenFactory.doCheckCancelled(generationState) + generationState.factory.done() + + val backendDiagnostics = generationState.collectedExtraJvmDiagnostics.all() + val backendErrors = computeErrors(backendDiagnostics, effectiveConfiguration) + + if (backendErrors.isNotEmpty()) { + return KtCompilationResult.Failure(backendErrors) + } + + val outputFiles = generationState.factory.asList().map(::KtCompiledFileForOutputFile) + return KtCompilationResult.Success(outputFiles) + } finally { + generationState.destroy() + } + } + + private fun getFullyResolvedFirFile(file: KtFile): FirFile { + val firFile = file.getOrBuildFirFile(firResolveSession) + LLFirWholeFileResolveTarget(firFile).resolve(FirResolvePhase.BODY_RESOLVE) + return firFile + } + + private fun computeErrors(diagnostics: Collection, configuration: CompilerConfiguration): List { + val allowedErrorFilter = configuration[ALLOWED_ERROR_FILTER] ?: { true } + + return buildList { + for (diagnostic in diagnostics) { + require(diagnostic is KtPsiDiagnostic) + + if (diagnostic.severity == Severity.ERROR) { + val ktDiagnostic = diagnostic.asKtDiagnostic() + if (!allowedErrorFilter(ktDiagnostic)) { + add(ktDiagnostic) + } + } + } + } + } + + private class SingleFileGenerateClassFilter( + private val file: KtFile, + private val inlinedClasses: Set + ) : GenerationState.GenerateClassFilter() { + private val filesWithInlinedClasses = inlinedClasses.mapTo(mutableSetOf()) { it.containingKtFile } + + override fun shouldGeneratePackagePart(ktFile: KtFile): Boolean { + return file === ktFile || ktFile in filesWithInlinedClasses + } + + override fun shouldAnnotateClass(processingClassOrObject: KtClassOrObject): Boolean { + return true + } + + override fun shouldGenerateClass(processingClassOrObject: KtClassOrObject): Boolean { + return processingClassOrObject.containingKtFile === file || + processingClassOrObject is KtObjectDeclaration && processingClassOrObject in inlinedClasses + } + + override fun shouldGenerateScript(script: KtScript): Boolean { + return script.containingKtFile === file + } + + override fun shouldGenerateCodeFragment(script: KtCodeFragment) = false + } + + private fun createJvmIrCodegenFactory(configuration: CompilerConfiguration): JvmIrCodegenFactory { + val jvmGeneratorExtensions = object : JvmGeneratorExtensionsImpl(configuration) { + override fun getContainerSource(descriptor: DeclarationDescriptor): DeserializedContainerSource? { + // Stubbed top-level function IR symbols (from other source files in the module) require a parent facade class to be + // generated, which requires a container source to be provided. Without a facade class, function IR symbols will have + // an `IrExternalPackageFragment` parent, which trips up code generation during IR lowering. + val psiSourceFile = + descriptor.toSourceElement.containingFile as? PsiSourceFile ?: return super.getContainerSource(descriptor) + return FacadeClassSourceShimForFragmentCompilation(psiSourceFile) + } + } + + val ideCodegenSettings = JvmIrCodegenFactory.IdeCodegenSettings( + shouldStubAndNotLinkUnboundSymbols = true, + shouldDeduplicateBuiltInSymbols = false, + + // Because the file to compile may be contained in a "common" multiplatform module, an `expect` declaration doesn't necessarily + // have an obvious associated `actual` symbol. `shouldStubOrphanedExpectSymbols` generates stubs for such `expect` declarations. + shouldStubOrphanedExpectSymbols = true, + + // Likewise, the file to compile may be contained in a "platform" multiplatform module, where the `actual` declaration is + // referenced in the symbol table automatically, but not its `expect` counterpart, because it isn't contained in the files to + // compile. `shouldReferenceUndiscoveredExpectSymbols` references such `expect` symbols in the symbol table so that they can + // subsequently be stubbed. + shouldReferenceUndiscoveredExpectSymbols = false, // TODO it was true + ) + + return JvmIrCodegenFactory( + configuration, + PhaseConfig(jvmPhases), + jvmGeneratorExtensions = jvmGeneratorExtensions, + ideCodegenSettings = ideCodegenSettings, + ) + } +} \ No newline at end of file diff --git a/analysis/analysis-api-impl-base/build.gradle.kts b/analysis/analysis-api-impl-base/build.gradle.kts index 111d977c4bb..8d844132a70 100644 --- a/analysis/analysis-api-impl-base/build.gradle.kts +++ b/analysis/analysis-api-impl-base/build.gradle.kts @@ -9,6 +9,7 @@ dependencies { api(project(":analysis:analysis-api-impl-barebone")) api(project(":analysis:kt-references")) api(project(":compiler:resolution.common.jvm")) + implementation(project(":compiler:backend-common")) api(intellijCore()) implementation(project(":analysis:analysis-internal-utils")) diff --git a/analysis/analysis-api-impl-base/src/org/jetbrains/kotlin/analysis/api/impl/base/util/KtCompiledFileForOutputFile.kt b/analysis/analysis-api-impl-base/src/org/jetbrains/kotlin/analysis/api/impl/base/util/KtCompiledFileForOutputFile.kt new file mode 100644 index 00000000000..95404722389 --- /dev/null +++ b/analysis/analysis-api-impl-base/src/org/jetbrains/kotlin/analysis/api/impl/base/util/KtCompiledFileForOutputFile.kt @@ -0,0 +1,21 @@ +/* + * 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.util + +import org.jetbrains.kotlin.analysis.api.components.KtCompiledFile +import org.jetbrains.kotlin.backend.common.output.OutputFile +import java.io.File + +class KtCompiledFileForOutputFile(private val outputFile: OutputFile) : KtCompiledFile { + override val path: String + get() = outputFile.relativePath + + override val sourceFiles: List + get() = outputFile.sourceFiles + + override val content: ByteArray + get() = outputFile.asByteArray() +} \ No newline at end of file diff --git a/analysis/analysis-api/build.gradle.kts b/analysis/analysis-api/build.gradle.kts index f9ba36ba9e1..18bbc28e10d 100644 --- a/analysis/analysis-api/build.gradle.kts +++ b/analysis/analysis-api/build.gradle.kts @@ -12,6 +12,7 @@ dependencies { compileOnly(commonDependency("org.jetbrains.kotlin:kotlin-reflect")) { isTransitive = false } compileOnly(project(":compiler:psi")) + implementation(project(":compiler:backend")) compileOnly(project(":core:compiler.common")) compileOnly(project(":core:compiler.common.jvm")) compileOnly(project(":core:compiler.common.js")) diff --git a/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/KtAnalysisSession.kt b/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/KtAnalysisSession.kt index 1d32c54e847..390de152a79 100644 --- a/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/KtAnalysisSession.kt +++ b/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/KtAnalysisSession.kt @@ -64,7 +64,8 @@ public abstract class KtAnalysisSession(final override val token: KtLifetimeToke KtSignatureSubstitutorMixIn, KtScopeSubstitutionMixIn, KtSymbolProviderByJavaPsiMixIn, - KtResolveExtensionInfoProviderMixIn { + KtResolveExtensionInfoProviderMixIn, + KtCompilerFacilityMixIn { public abstract val useSiteModule: KtModule @@ -165,6 +166,9 @@ public abstract class KtAnalysisSession(final override val token: KtLifetimeToke internal val resolveExtensionInfoProvider: KtResolveExtensionInfoProvider get() = resolveExtensionInfoProviderImpl protected abstract val resolveExtensionInfoProviderImpl: KtResolveExtensionInfoProvider + internal val compilerFacility: KtCompilerFacility get() = compilerFacilityImpl + protected abstract val compilerFacilityImpl: KtCompilerFacility + @KtAnalysisApiInternals public val substitutorFactory: KtSubstitutorFactory get() = substitutorFactoryImpl protected abstract val substitutorFactoryImpl: KtSubstitutorFactory diff --git a/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/components/KtCompilerFacility.kt b/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/components/KtCompilerFacility.kt new file mode 100644 index 00000000000..c84f5b51426 --- /dev/null +++ b/analysis/analysis-api/src/org/jetbrains/kotlin/analysis/api/components/KtCompilerFacility.kt @@ -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.api.components + +import org.jetbrains.kotlin.analysis.api.diagnostics.KtDiagnostic +import org.jetbrains.kotlin.analysis.api.lifetime.withValidityAssertion +import org.jetbrains.kotlin.codegen.ClassBuilderFactory +import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.config.CommonConfigurationKeys +import org.jetbrains.kotlin.config.CompilerConfigurationKey +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtCodeFragment +import java.io.File + +/** + * Compilation result. + */ +public sealed class KtCompilationResult { + /** + * Successful compilation result. + * + * @property output Output files produced by the compiler. For the JVM target, these are class files and '.kotlin_module'. + */ + public class Success(public val output: List) : KtCompilationResult() + + /** + * Failed compilation result. + * + * @property errors Non-recoverable errors either during code analysis, or during code generation. + */ + public class Failure(public val errors: List) : KtCompilationResult() +} + +public interface KtCompiledFile { + /** + * Path of the compiled file relative to the root of the output directory. + */ + public val path: String + + /** + * Source files that were compiled to produce this file. + */ + public val sourceFiles: List + + /** + * Content of the compiled file. + */ + public val content: ByteArray +} + +/** + * `true` if the compiled file is a Java class file. + */ +public val KtCompiledFile.isClassFile: Boolean + get() = path.endsWith(".class", ignoreCase = true) + +/** + * Compilation target platform. + */ +public sealed class KtCompilerTarget { + /** JVM target (produces '.class' files). */ + public class Jvm(public val classBuilderFactory: ClassBuilderFactory) : KtCompilerTarget() +} + +public abstract class KtCompilerFacility : KtAnalysisSessionComponent() { + public companion object { + /** Filter for the allowed errors. Compilation will be aborted if there are errors that this filter rejects. */ + public val ALLOWED_ERROR_FILTER: CompilerConfigurationKey<(KtDiagnostic) -> Boolean> = + CompilerConfigurationKey<(KtDiagnostic) -> Boolean>("error filter") + } + + public abstract fun compile(file: KtFile, configuration: CompilerConfiguration, target: KtCompilerTarget): KtCompilationResult +} + +public interface KtCompilerFacilityMixIn : KtAnalysisSessionMixIn { + /** + * Compile the given [file]. + * + * @param file A file to compile. + * The file must be either a source module file, or a [KtCodeFragment]. + * For a [KtCodeFragment], a source module context, a compiled library source context, or an empty context(`null`) are supported. + * + * @param configuration Compiler configuration. + * It is recommended to submit at least the module name ([CommonConfigurationKeys.MODULE_NAME]) + * and language version settings ([CommonConfigurationKeys.LANGUAGE_VERSION_SETTINGS]). + * + * @param target Compilation target platform. + * + * @return Compilation result. + * + * The function _does not_ wrap unchecked exceptions from the compiler. + * The implementation should wrap the `compile()` call into a `try`/`catch` block if necessary. + */ + public fun compile(file: KtFile, configuration: CompilerConfiguration, target: KtCompilerTarget): KtCompilationResult { + return withValidityAssertion { + analysisSession.compilerFacility.compile(file, configuration, target) + } + } +} \ No newline at end of file diff --git a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/api/targets/LLFirResolveTarget.kt b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/api/targets/LLFirResolveTarget.kt index 4983a937084..6709c3cb042 100644 --- a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/api/targets/LLFirResolveTarget.kt +++ b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/api/targets/LLFirResolveTarget.kt @@ -5,9 +5,13 @@ package org.jetbrains.kotlin.analysis.low.level.api.fir.api.targets +import org.jetbrains.kotlin.analysis.low.level.api.fir.sessions.llFirResolvableSession +import org.jetbrains.kotlin.analysis.low.level.api.fir.sessions.llFirSession import org.jetbrains.kotlin.fir.FirElementWithResolveState import org.jetbrains.kotlin.fir.declarations.FirFile import org.jetbrains.kotlin.fir.declarations.FirRegularClass +import org.jetbrains.kotlin.fir.declarations.FirResolvePhase +import org.jetbrains.kotlin.utils.exceptions.errorWithAttachment /** @@ -47,3 +51,17 @@ sealed class LLFirResolveTarget { protected abstract fun toStringForTarget(): String } + +/** + * Resolves the target to the specified [phase]. + * The owning session must be a resolvable one. + */ +fun LLFirResolveTarget.resolve(phase: FirResolvePhase) { + val session = firFile.llFirResolvableSession + ?: errorWithAttachment("Resolvable session expected, got '${firFile.llFirSession::class.java}'") { + withEntry("firSession", firFile.llFirSession) { it.toString() } + } + + val lazyDeclarationResolver = session.moduleComponents.firModuleLazyDeclarationResolver + lazyDeclarationResolver.lazyResolveTarget(this, phase, towerDataContextCollector = null) +} \ No newline at end of file diff --git a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/compile/CompilationPeerCollector.kt b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/compile/CompilationPeerCollector.kt new file mode 100644 index 00000000000..f15ea9c7d60 --- /dev/null +++ b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/compile/CompilationPeerCollector.kt @@ -0,0 +1,168 @@ +/* + * 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.compile + +import com.intellij.openapi.progress.ProgressManager +import org.jetbrains.kotlin.codegen.state.GenerationState +import org.jetbrains.kotlin.fir.FirElement +import org.jetbrains.kotlin.fir.contracts.FirContractDescription +import org.jetbrains.kotlin.fir.declarations.* +import org.jetbrains.kotlin.fir.declarations.utils.hasBody +import org.jetbrains.kotlin.fir.declarations.utils.isInline +import org.jetbrains.kotlin.fir.expressions.FirBlock +import org.jetbrains.kotlin.fir.expressions.FirResolvable +import org.jetbrains.kotlin.fir.expressions.impl.FirContractCallBlock +import org.jetbrains.kotlin.fir.psi +import org.jetbrains.kotlin.fir.references.FirResolvedNamedReference +import org.jetbrains.kotlin.fir.symbols.SymbolInternals +import org.jetbrains.kotlin.fir.symbols.impl.FirCallableSymbol +import org.jetbrains.kotlin.fir.symbols.lazyResolveToPhase +import org.jetbrains.kotlin.fir.unwrapSubstitutionOverrides +import org.jetbrains.kotlin.fir.visitors.FirDefaultVisitorVoid +import org.jetbrains.kotlin.psi.KtClassOrObject +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.utils.addIfNotNull + +/** + * Processes the declaration, collecting files that would need to be submitted to the backend (or handled specially) + * in case if the declaration is compiled. + * + * Besides the file that owns the declaration, the visitor also recursively collects source files with called inline functions. + * In addition, the visitor collects a list of inlined local classes. Such a list might be useful in [GenerationState.GenerateClassFilter] + * to filter out class files unrelated to the current compilation. + * + * Note that compiled declarations are not analyzed, as the backend can inline them natively. + */ +object CompilationPeerCollector { + fun process(declaration: FirDeclaration): CompilationPeerData { + val visitor = CompilationPeerCollectingVisitor() + visitor.process(declaration) + return CompilationPeerData(visitor.files, visitor.inlinedClasses) + } +} + +class CompilationPeerData( + /** File with the original declaration and all files with called inline functions. */ + val files: List, + + /** Local classes inlined as a part of inline functions. */ + val inlinedClasses: Set +) + +private class CompilationPeerCollectingVisitor : FirDefaultVisitorVoid() { + private val processed = mutableSetOf() + private val queue = ArrayDeque() + + private val collectedFiles = mutableSetOf() + private val collectedInlinedClasses = mutableSetOf() + private var isInlineFunctionContext = false + + val files: List + get() = collectedFiles.toList() + + val inlinedClasses: Set + get() = collectedInlinedClasses + + fun process(declaration: FirDeclaration) { + processSingle(declaration) + + while (queue.isNotEmpty()) { + processSingle(queue.removeFirst()) + } + } + + private fun processSingle(declaration: FirDeclaration) { + ProgressManager.checkCanceled() + + if (processed.add(declaration)) { + val containingFile = declaration.psi?.containingFile + if (containingFile is KtFile && !containingFile.isCompiled) { + collectedFiles.add(containingFile) + declaration.accept(this) + } + } + } + + override fun visitElement(element: FirElement) { + if (element is FirResolvable) { + processResolvable(element) + } + + element.acceptChildren(this) + } + + override fun visitBlock(block: FirBlock) { + if (block !is FirContractCallBlock) { + super.visitBlock(block) + } + } + + override fun visitContractDescription(contractDescription: FirContractDescription) { + // Skip contract description. + // Contract blocks are skipped in BE, so we would never need to inline contract DSL calls. + } + + override fun visitConstructor(constructor: FirConstructor) { + constructor.lazyResolveToPhase(FirResolvePhase.BODY_RESOLVE) + + super.visitConstructor(constructor) + } + + override fun visitSimpleFunction(simpleFunction: FirSimpleFunction) { + simpleFunction.lazyResolveToPhase(FirResolvePhase.BODY_RESOLVE) + + val oldIsInlineFunctionContext = isInlineFunctionContext + try { + isInlineFunctionContext = simpleFunction.isInline + super.visitFunction(simpleFunction) + } finally { + isInlineFunctionContext = oldIsInlineFunctionContext + } + } + + override fun visitProperty(property: FirProperty) { + property.lazyResolveToPhase(FirResolvePhase.BODY_RESOLVE) + + super.visitProperty(property) + } + + override fun visitClass(klass: FirClass) { + super.visitClass(klass) + + if (isInlineFunctionContext) { + collectedInlinedClasses.addIfNotNull(klass.psi as? KtClassOrObject) + } + } + + @OptIn(SymbolInternals::class) + private fun processResolvable(element: FirResolvable) { + fun addToQueue(function: FirFunction?) { + val original = function?.unwrapSubstitutionOverrides() ?: return + if (original.isInline && original.hasBody) { + queue.add(function) + } + } + + val reference = element.calleeReference + if (reference !is FirResolvedNamedReference) { + return + } + + val symbol = reference.resolvedSymbol + if (symbol is FirCallableSymbol<*>) { + when (val fir = symbol.fir) { + is FirFunction -> { + addToQueue(fir) + } + is FirProperty -> { + addToQueue(fir.getter) + addToQueue(fir.setter) + } + else -> {} + } + } + } +} \ No newline at end of file diff --git a/analysis/low-level-api-fir/testdata/compilationPeers/functionCall.kt b/analysis/low-level-api-fir/testdata/compilationPeers/functionCall.kt new file mode 100644 index 00000000000..0f6afaf1b15 --- /dev/null +++ b/analysis/low-level-api-fir/testdata/compilationPeers/functionCall.kt @@ -0,0 +1,23 @@ +/* + * 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. + */ + +// FILE: lib.kt +package lib + +inline fun lib() {} + +// FILE: unrelated.kt +package lib + +fun unrelated() {} + +// FILE: main.kt +package test + +import lib.* + +fun foo() { + lib() +} \ No newline at end of file diff --git a/analysis/low-level-api-fir/testdata/compilationPeers/functionCall.txt b/analysis/low-level-api-fir/testdata/compilationPeers/functionCall.txt new file mode 100644 index 00000000000..f2264c4c63f --- /dev/null +++ b/analysis/low-level-api-fir/testdata/compilationPeers/functionCall.txt @@ -0,0 +1,2 @@ +File lib.kt +File main.kt \ No newline at end of file diff --git a/analysis/low-level-api-fir/testdata/compilationPeers/functionCallDeep.kt b/analysis/low-level-api-fir/testdata/compilationPeers/functionCallDeep.kt new file mode 100644 index 00000000000..cb2c22fdeb5 --- /dev/null +++ b/analysis/low-level-api-fir/testdata/compilationPeers/functionCallDeep.kt @@ -0,0 +1,27 @@ +/* + * 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. + */ + +// FILE: base.kt +package base + +inline fun base() {} + +// FILE: lib.kt +package lib + +import base.* + +inline fun lib() { + base() +} + +// FILE: main.kt +package test + +import lib.* + +fun foo() { + lib() +} \ No newline at end of file diff --git a/analysis/low-level-api-fir/testdata/compilationPeers/functionCallDeep.txt b/analysis/low-level-api-fir/testdata/compilationPeers/functionCallDeep.txt new file mode 100644 index 00000000000..a437a987faf --- /dev/null +++ b/analysis/low-level-api-fir/testdata/compilationPeers/functionCallDeep.txt @@ -0,0 +1,3 @@ +File base.kt +File lib.kt +File main.kt \ No newline at end of file diff --git a/analysis/low-level-api-fir/testdata/compilationPeers/libraryFunctionCall.kt b/analysis/low-level-api-fir/testdata/compilationPeers/libraryFunctionCall.kt new file mode 100644 index 00000000000..5380efa2288 --- /dev/null +++ b/analysis/low-level-api-fir/testdata/compilationPeers/libraryFunctionCall.kt @@ -0,0 +1,14 @@ +/* + * 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. + */ + +// WITH_STDLIB + +package test + +import lib.* + +fun foo() { + println("foo") +} \ No newline at end of file diff --git a/analysis/low-level-api-fir/testdata/compilationPeers/libraryFunctionCall.txt b/analysis/low-level-api-fir/testdata/compilationPeers/libraryFunctionCall.txt new file mode 100644 index 00000000000..b12b535e165 --- /dev/null +++ b/analysis/low-level-api-fir/testdata/compilationPeers/libraryFunctionCall.txt @@ -0,0 +1 @@ +File libraryFunctionCall.kt \ No newline at end of file diff --git a/analysis/low-level-api-fir/testdata/compilationPeers/noInline.kt b/analysis/low-level-api-fir/testdata/compilationPeers/noInline.kt new file mode 100644 index 00000000000..892dfbb7f51 --- /dev/null +++ b/analysis/low-level-api-fir/testdata/compilationPeers/noInline.kt @@ -0,0 +1,18 @@ +/* + * 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. + */ + +// FILE: lib.kt +package lib + +fun lib() {} + +// FILE: main.kt +package test + +import lib.* + +fun foo() { + lib() +} \ No newline at end of file diff --git a/analysis/low-level-api-fir/testdata/compilationPeers/noInline.txt b/analysis/low-level-api-fir/testdata/compilationPeers/noInline.txt new file mode 100644 index 00000000000..c78ab3e563b --- /dev/null +++ b/analysis/low-level-api-fir/testdata/compilationPeers/noInline.txt @@ -0,0 +1 @@ +File main.kt \ No newline at end of file diff --git a/analysis/low-level-api-fir/testdata/compilationPeers/propertyCall.kt b/analysis/low-level-api-fir/testdata/compilationPeers/propertyCall.kt new file mode 100644 index 00000000000..ee80206a357 --- /dev/null +++ b/analysis/low-level-api-fir/testdata/compilationPeers/propertyCall.kt @@ -0,0 +1,19 @@ +/* + * 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. + */ + +// FILE: lib.kt +package lib + +inline val lib: String + get() = "lib" + +// FILE: main.kt +package test + +import lib.* + +fun foo() { + lib.length +} \ No newline at end of file diff --git a/analysis/low-level-api-fir/testdata/compilationPeers/propertyCall.txt b/analysis/low-level-api-fir/testdata/compilationPeers/propertyCall.txt new file mode 100644 index 00000000000..f2264c4c63f --- /dev/null +++ b/analysis/low-level-api-fir/testdata/compilationPeers/propertyCall.txt @@ -0,0 +1,2 @@ +File lib.kt +File main.kt \ No newline at end of file diff --git a/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/AbstractCompilationPeerAnalysisTest.kt b/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/AbstractCompilationPeerAnalysisTest.kt new file mode 100644 index 00000000000..1ed85ab0460 --- /dev/null +++ b/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/AbstractCompilationPeerAnalysisTest.kt @@ -0,0 +1,40 @@ +/* + * 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 + +import org.jetbrains.kotlin.analysis.low.level.api.fir.api.getFirResolveSession +import org.jetbrains.kotlin.analysis.low.level.api.fir.api.getOrBuildFirFile +import org.jetbrains.kotlin.analysis.low.level.api.fir.compile.CompilationPeerCollector +import org.jetbrains.kotlin.analysis.low.level.api.fir.test.configurators.AnalysisApiFirSourceTestConfigurator +import org.jetbrains.kotlin.analysis.project.structure.ProjectStructureProvider +import org.jetbrains.kotlin.analysis.test.framework.base.AbstractAnalysisApiBasedSingleModuleTest +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.test.model.TestModule +import org.jetbrains.kotlin.test.services.TestServices +import org.jetbrains.kotlin.test.services.assertions + +abstract class AbstractCompilationPeerAnalysisTest : AbstractAnalysisApiBasedSingleModuleTest() { + override val configurator = AnalysisApiFirSourceTestConfigurator(analyseInDependentSession = false) + + override fun doTestByFileStructure(ktFiles: List, module: TestModule, testServices: TestServices) { + val mainKtFile = ktFiles.singleOrNull() ?: ktFiles.single { it.name == "main.kt" } + + val project = mainKtFile.project + val sourceModule = ProjectStructureProvider.getModule(project, mainKtFile, contextualModule = null) + + val resolveSession = sourceModule.getFirResolveSession(project) + val firFile = mainKtFile.getOrBuildFirFile(resolveSession) + + val compilationPeerData = CompilationPeerCollector.process(firFile) + + val actualItems = compilationPeerData.files.map { "File " + it.name }.sorted() + + compilationPeerData.inlinedClasses.map { "Class " + it.name } + + val actualText = actualItems.joinToString(separator = "\n") + + testServices.assertions.assertEqualsToTestDataFileSibling(actual = actualText) + } +} \ No newline at end of file diff --git a/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/CompilationPeerAnalysisTestGenerated.java b/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/CompilationPeerAnalysisTestGenerated.java new file mode 100644 index 00000000000..6d672110abb --- /dev/null +++ b/analysis/low-level-api-fir/tests/org/jetbrains/kotlin/analysis/low/level/api/fir/CompilationPeerAnalysisTestGenerated.java @@ -0,0 +1,56 @@ +/* + * 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; + +import com.intellij.testFramework.TestDataPath; +import org.jetbrains.kotlin.test.util.KtTestUtil; +import org.jetbrains.kotlin.test.TestMetadata; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.regex.Pattern; + +/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.analysis.api.GenerateAnalysisApiTestsKt}. DO NOT MODIFY MANUALLY */ +@SuppressWarnings("all") +@TestMetadata("analysis/low-level-api-fir/testdata/compilationPeers") +@TestDataPath("$PROJECT_ROOT") +public class CompilationPeerAnalysisTestGenerated extends AbstractCompilationPeerAnalysisTest { + @Test + public void testAllFilesPresentInCompilationPeers() throws Exception { + KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("analysis/low-level-api-fir/testdata/compilationPeers"), Pattern.compile("^(.+)\\.kt$"), null, true); + } + + @Test + @TestMetadata("functionCall.kt") + public void testFunctionCall() throws Exception { + runTest("analysis/low-level-api-fir/testdata/compilationPeers/functionCall.kt"); + } + + @Test + @TestMetadata("functionCallDeep.kt") + public void testFunctionCallDeep() throws Exception { + runTest("analysis/low-level-api-fir/testdata/compilationPeers/functionCallDeep.kt"); + } + + @Test + @TestMetadata("libraryFunctionCall.kt") + public void testLibraryFunctionCall() throws Exception { + runTest("analysis/low-level-api-fir/testdata/compilationPeers/libraryFunctionCall.kt"); + } + + @Test + @TestMetadata("noInline.kt") + public void testNoInline() throws Exception { + runTest("analysis/low-level-api-fir/testdata/compilationPeers/noInline.kt"); + } + + @Test + @TestMetadata("propertyCall.kt") + public void testPropertyCall() throws Exception { + runTest("analysis/low-level-api-fir/testdata/compilationPeers/propertyCall.kt"); + } +} diff --git a/generators/analysis-api-generator/tests/org/jetbrains/kotlin/generators/tests/analysis/api/firLowLevel.kt b/generators/analysis-api-generator/tests/org/jetbrains/kotlin/generators/tests/analysis/api/firLowLevel.kt index 319f949ad9e..a696b219747 100644 --- a/generators/analysis-api-generator/tests/org/jetbrains/kotlin/generators/tests/analysis/api/firLowLevel.kt +++ b/generators/analysis-api-generator/tests/org/jetbrains/kotlin/generators/tests/analysis/api/firLowLevel.kt @@ -130,6 +130,10 @@ internal fun TestGroupSuite.generateFirLowLevelApiTests() { testClass { model("classId") } + + testClass { + model("compilationPeers") + } } testGroup(