[Analysis API] Perform out-of-block for import changes in code fragments ^KT-65600 Fixed
diff --git a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/file/structure/LLFirDeclarationModificationService.kt b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/file/structure/LLFirDeclarationModificationService.kt index 8fc163d..2dc1bfe 100644 --- a/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/file/structure/LLFirDeclarationModificationService.kt +++ b/analysis/low-level-api-fir/src/org/jetbrains/kotlin/analysis/low/level/api/fir/file/structure/LLFirDeclarationModificationService.kt
@@ -185,8 +185,12 @@ val inBlockModificationOwner = nonLocalDeclarationForLocalChange(element) ?: return ChangeType.OutOfBlock if (inBlockModificationOwner is KtCodeFragment) { - // All code fragment content is local - return ChangeType.InBlock(inBlockModificationOwner, project) + return if (handleCodeFragmentImportsChange(inBlockModificationOwner)) { + ChangeType.OutOfBlock + } else { + // All changes inside the code fragment are local + ChangeType.InBlock(inBlockModificationOwner, project) + } } val isOutOfBlockChange = element.isNewDirectChildOf(inBlockModificationOwner, modificationType) @@ -199,6 +203,33 @@ } /** + * The function changes whether new import directives were added to the code fragment since the last invalidation event. + * While changes in the code fragment itself are local as only the body of 'FirCodeFragment' is changed, + * imports are projected to 'FirImports' which are outside the 'FirCodeFragment', and those additional imports won't be added to + * the 'FirFile' unless an out-of-block modification happens, causing re-creation of the whole 'FirFile'. + * + * Warning: the function is not pure, as it manipulates the import counter. + * Even if it returned 'true' once, consequent calls will most likely return 'false'. + * + * Note: because of the implementation, the first modification event in a code fragment will trigger the out-of-block event, + * as the user data is not set. Additional API in the 'KtCodeFragment' itself is needed to fix this. + * + * @return true if the imports changed in a code fragment since the last invalidation event. + */ + private fun handleCodeFragmentImportsChange(codeFragment: KtCodeFragment): Boolean { + val currentImportCount = codeFragment.importDirectives.size + val lastImportCount = codeFragment.getUserData(cachedCodeFragmentImportCountKey) + + /** In [KtCodeFragment], imports can only be added, and not removed/changed. */ + if (lastImportCount == null || lastImportCount != currentImportCount) { + codeFragment.putUserData(cachedCodeFragmentImportCountKey, currentImportCount) + return true + } + + return false + } + + /** * This check covers cases such as a new body that was added to a function, which should cause an out-of-block modification. */ private fun PsiElement.isNewDirectChildOf(inBlockModificationOwner: KtAnnotated, modificationType: ModificationType): Boolean = @@ -345,6 +376,8 @@ } } +private val cachedCodeFragmentImportCountKey = Key.create<Int>("CACHED_CODE_FRAGMENT_IMPORTS") + /** * The purpose of this property as user data is to avoid FIR building in case the [KtAnnotated] * doesn't have an associated FIR with body.