From f169366478cee788aab972830b094eaa8f0aad65 Mon Sep 17 00:00:00 2001 From: Vladimir Dolzhenko Date: Wed, 20 Oct 2021 15:50:28 +0200 Subject: [PATCH] [kotlin] Fix incremental analysis for inner class bodies #KTIJ-18388 Fixed GitOrigin-RevId: 635fbea4ce7ed1cf6cc535aaca04c2c3331eb35f --- ...PureKotlinCodeBlockModificationListener.kt | 52 ++++++++++++++++--- .../AbstractOutOfBlockModificationTest.kt | 6 ++- .../OutOfBlockModificationTestGenerated.java | 5 ++ .../outOfBlock/InnerClassInnerMethod.kt | 11 ++++ .../idea/test/DirectiveBasedActionUtils.kt | 42 +++++++++++++++ 5 files changed, 106 insertions(+), 10 deletions(-) create mode 100644 plugins/kotlin/idea/tests/testData/codeInsight/outOfBlock/InnerClassInnerMethod.kt diff --git a/plugins/kotlin/frontend-independent/src/org/jetbrains/kotlin/idea/caches/trackers/PureKotlinCodeBlockModificationListener.kt b/plugins/kotlin/frontend-independent/src/org/jetbrains/kotlin/idea/caches/trackers/PureKotlinCodeBlockModificationListener.kt index 03c114e762b3..bd897096c84e 100644 --- a/plugins/kotlin/frontend-independent/src/org/jetbrains/kotlin/idea/caches/trackers/PureKotlinCodeBlockModificationListener.kt +++ b/plugins/kotlin/frontend-independent/src/org/jetbrains/kotlin/idea/caches/trackers/PureKotlinCodeBlockModificationListener.kt @@ -20,7 +20,6 @@ import com.intellij.pom.tree.events.impl.ChangeInfoImpl import com.intellij.psi.PsiComment import com.intellij.psi.PsiElement import com.intellij.psi.PsiWhiteSpace -import com.intellij.psi.impl.source.tree.SharedImplUtil import com.intellij.psi.util.PsiTreeUtil import com.intellij.psi.util.findTopmostParentInFile import com.intellij.psi.util.findTopmostParentOfType @@ -111,6 +110,19 @@ class PureKotlinCodeBlockModificationListener(project: Project) : Disposable { if (KtPsiUtil.isLocal(blockDeclaration)) return null + val directParentClassOrObject = PsiTreeUtil.getParentOfType(blockDeclaration, KtClassOrObject::class.java) + val parentClassOrObject = directParentClassOrObject + ?.takeIf { !it.isTopLevel() && it.hasModifier(KtTokens.INNER_KEYWORD) }?.let { + var e: KtClassOrObject? = it + while (e != null) { + e = PsiTreeUtil.getParentOfType(e, KtClassOrObject::class.java) + if (e?.hasModifier(KtTokens.INNER_KEYWORD) == false) { + break + } + } + e + } ?: directParentClassOrObject + when (blockDeclaration) { is KtNamedFunction -> { // if (blockDeclaration.visibilityModifierType()?.toVisibility() == Visibilities.PRIVATE) { @@ -122,12 +134,24 @@ class PureKotlinCodeBlockModificationListener(project: Project) : Disposable { // case like `fun foo(): String {......}` return blockDeclaration.bodyExpression ?.takeIf { it.isAncestor(element) } - ?.let { BlockModificationScopeElement(blockDeclaration, it) } + ?.let { + if (parentClassOrObject == directParentClassOrObject) { + BlockModificationScopeElement(blockDeclaration, it) + } else if (parentClassOrObject != null) { + BlockModificationScopeElement(parentClassOrObject, it) + } else null + } } else if (blockDeclaration.hasDeclaredReturnType()) { // case like `fun foo(): String = blabla` return blockDeclaration.initializer ?.takeIf { it.isAncestor(element) } - ?.let { BlockModificationScopeElement(blockDeclaration, it) } + ?.let { + if (parentClassOrObject == directParentClassOrObject) { + BlockModificationScopeElement(blockDeclaration, it) + } else if (parentClassOrObject != null) { + BlockModificationScopeElement(parentClassOrObject, it) + } else null + } } } @@ -155,7 +179,11 @@ class PureKotlinCodeBlockModificationListener(project: Project) : Disposable { blockDeclaration.findTopmostParentOfType() as? KtElement if (declaration != null) { - return BlockModificationScopeElement(declaration, properExpression) + return if (parentClassOrObject == directParentClassOrObject) { + BlockModificationScopeElement(declaration, properExpression) + } else if (parentClassOrObject != null) { + BlockModificationScopeElement(parentClassOrObject, properExpression) + } else null } } } @@ -175,8 +203,12 @@ class PureKotlinCodeBlockModificationListener(project: Project) : Disposable { blockDeclaration .takeIf { it.isAncestor(element) } ?.let { ktClassInitializer -> - (PsiTreeUtil.getParentOfType(blockDeclaration, KtClassOrObject::class.java))?.let { - return BlockModificationScopeElement(it, ktClassInitializer) + parentClassOrObject?.let { + return if (parentClassOrObject == directParentClassOrObject) { + BlockModificationScopeElement(it, ktClassInitializer) + } else { + BlockModificationScopeElement(parentClassOrObject, ktClassInitializer) + } } } } @@ -185,8 +217,12 @@ class PureKotlinCodeBlockModificationListener(project: Project) : Disposable { blockDeclaration.takeIf { it.bodyExpression?.isAncestor(element) ?: false || it.getDelegationCallOrNull()?.isAncestor(element) ?: false }?.let { ktConstructor -> - PsiTreeUtil.getParentOfType(blockDeclaration, KtClassOrObject::class.java)?.let { - return BlockModificationScopeElement(it, ktConstructor) + parentClassOrObject?.let { + return if (parentClassOrObject == directParentClassOrObject) { + BlockModificationScopeElement(it, ktConstructor) + } else { + BlockModificationScopeElement(parentClassOrObject, ktConstructor) + } } } } diff --git a/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/codeInsight/AbstractOutOfBlockModificationTest.kt b/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/codeInsight/AbstractOutOfBlockModificationTest.kt index 23fb9c9d2872..6480b3419c54 100644 --- a/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/codeInsight/AbstractOutOfBlockModificationTest.kt +++ b/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/codeInsight/AbstractOutOfBlockModificationTest.kt @@ -36,8 +36,9 @@ abstract class AbstractOutOfBlockModificationTest : KotlinLightCodeInsightFixtur ktFile.text, SKIP_ANALYZE_CHECK_DIRECTIVE ) + val project = myFixture.project val tracker = - PsiManager.getInstance(myFixture.project).modificationTracker as PsiModificationTrackerImpl + PsiManager.getInstance(project).modificationTracker as PsiModificationTrackerImpl val element = ktFile.findElementAt(myFixture.caretOffset) assertNotNull("Should be valid element", element) val oobBeforeType = ktFile.outOfBlockModificationCount @@ -47,7 +48,7 @@ abstract class AbstractOutOfBlockModificationTest : KotlinLightCodeInsightFixtur ktFile.analyzeWithAllCompilerChecks() myFixture.type(stringToType) - PsiDocumentManager.getInstance(myFixture.project).commitDocument(myFixture.getDocument(myFixture.file)) + PsiDocumentManager.getInstance(project).commitDocument(myFixture.getDocument(myFixture.file)) val oobAfterCount = ktFile.outOfBlockModificationCount val modificationCountAfterType = tracker.modificationCount assertTrue( @@ -60,6 +61,7 @@ abstract class AbstractOutOfBlockModificationTest : KotlinLightCodeInsightFixtur expectedOutOfBlock, oobBeforeType != oobAfterCount ) checkForUnexpectedErrors(ktFile) + DirectiveBasedActionUtils.inspectionChecks(name, ktFile) if (!isSkipCheckDefined) { checkOOBWithDescriptorsResolve(expectedOutOfBlock) diff --git a/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/codeInsight/OutOfBlockModificationTestGenerated.java b/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/codeInsight/OutOfBlockModificationTestGenerated.java index 201d0bd8a786..a68246fe6127 100644 --- a/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/codeInsight/OutOfBlockModificationTestGenerated.java +++ b/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/codeInsight/OutOfBlockModificationTestGenerated.java @@ -368,6 +368,11 @@ public class OutOfBlockModificationTestGenerated extends AbstractOutOfBlockModif runTest("testData/codeInsight/outOfBlock/InitBlockInObject.kt"); } + @TestMetadata("InnerClassInnerMethod.kt") + public void testInnerClassInnerMethod() throws Exception { + runTest("testData/codeInsight/outOfBlock/InnerClassInnerMethod.kt"); + } + @TestMetadata("LocalFunWithBody.kt") public void testLocalFunWithBody() throws Exception { runTest("testData/codeInsight/outOfBlock/LocalFunWithBody.kt"); diff --git a/plugins/kotlin/idea/tests/testData/codeInsight/outOfBlock/InnerClassInnerMethod.kt b/plugins/kotlin/idea/tests/testData/codeInsight/outOfBlock/InnerClassInnerMethod.kt new file mode 100644 index 000000000000..d104b289e141 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/codeInsight/outOfBlock/InnerClassInnerMethod.kt @@ -0,0 +1,11 @@ +// OUT_OF_CODE_BLOCK: FALSE +// TYPE: '\b"a"' +// INSPECTION-CLASS: org.jetbrains.kotlin.idea.inspections.RedundantInnerClassModifierInspection +// INSPECTION: [LIKE_UNUSED_SYMBOL:6] Redundant 'inner' modifier +class RedundantInner(private val s: String) { + private inner class D { + fun f(): Int { + return s.length + } + } +} \ No newline at end of file diff --git a/plugins/kotlin/test-framework/test/org/jetbrains/kotlin/idea/test/DirectiveBasedActionUtils.kt b/plugins/kotlin/test-framework/test/org/jetbrains/kotlin/idea/test/DirectiveBasedActionUtils.kt index 26c3034eadb8..5cc450d5355e 100644 --- a/plugins/kotlin/test-framework/test/org/jetbrains/kotlin/idea/test/DirectiveBasedActionUtils.kt +++ b/plugins/kotlin/test-framework/test/org/jetbrains/kotlin/idea/test/DirectiveBasedActionUtils.kt @@ -2,12 +2,17 @@ package org.jetbrains.kotlin.idea.test +import com.intellij.codeInsight.daemon.impl.DaemonProgressIndicator import com.intellij.codeInsight.intention.IntentionAction +import com.intellij.codeInspection.InspectionManager +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.openapi.progress.ProgressManager import com.intellij.psi.PsiFile import com.intellij.testFramework.UsefulTestCase import org.jetbrains.kotlin.diagnostics.Severity import org.jetbrains.kotlin.diagnostics.rendering.DefaultErrorMessages import org.jetbrains.kotlin.idea.caches.resolve.analyzeWithContent +import org.jetbrains.kotlin.idea.inspections.AbstractKotlinInspection import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.resolve.diagnostics.Diagnostics import org.jetbrains.kotlin.test.InTextDirectivesUtils @@ -66,6 +71,43 @@ object DirectiveBasedActionUtils { ) } + fun inspectionChecks(name: String, file: PsiFile) { + InTextDirectivesUtils.findLinesWithPrefixesRemoved(file.text, "// INSPECTION-CLASS:").takeIf { it.isNotEmpty() }?.let { inspectionNames -> + val inspectionManager = InspectionManager.getInstance(file.project) + val inspections = inspectionNames.map { Class.forName(it).getDeclaredConstructor().newInstance() as AbstractKotlinInspection } + + val problems = mutableListOf() + ProgressManager.getInstance().executeProcessUnderProgress( + { + for (inspection in inspections) { + problems += inspection.processFile( + file, + inspectionManager + ) + } + }, DaemonProgressIndicator() + ) + val directive = "// INSPECTION:" + val expected = InTextDirectivesUtils.findLinesWithPrefixesRemoved(file.text, directive) + .sorted() + .map { "$directive $it" } + + val actual = problems + // lineNumber is 0-based + .map { "$directive [${it.highlightType.name}:${it.lineNumber + 1}] $it" } + .sorted() + + if (actual.isEmpty() && expected.isEmpty()) return + + KotlinLightCodeInsightFixtureTestCaseBase.assertOrderedEquals( + "All actual $name should be mentioned in test data with '$directive' directive. " + + "But no unnecessary $name should be me mentioned, file:\n${file.text}", + actual, + expected + ) + } + } + fun checkAvailableActionsAreExpected(file: PsiFile, availableActions: Collection) { val expectedActions = InTextDirectivesUtils.findLinesWithPrefixesRemoved(file.text, "// ACTION:").sorted()