diff --git a/compiler/frontend/src/org/jetbrains/kotlin/psi/JetDeclarationStub.java b/compiler/frontend/src/org/jetbrains/kotlin/psi/JetDeclarationStub.java index d4cacf3dc63..b1027d8251c 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/psi/JetDeclarationStub.java +++ b/compiler/frontend/src/org/jetbrains/kotlin/psi/JetDeclarationStub.java @@ -28,6 +28,8 @@ import org.jetbrains.kotlin.psi.findDocComment.FindDocCommentPackage; import org.jetbrains.kotlin.psi.stubs.KotlinClassOrObjectStub; abstract class JetDeclarationStub> extends JetModifierListOwnerStub implements JetDeclaration { + private long modificationStamp = 0; + public JetDeclarationStub(@NotNull T stub, @NotNull IStubElementType nodeType) { super(stub, nodeType); } @@ -36,6 +38,16 @@ abstract class JetDeclarationStub> extends JetModifierL super(node); } + @Override + public void subtreeChanged() { + super.subtreeChanged(); + modificationStamp++; + } + + public long getModificationStamp() { + return modificationStamp; + } + @Nullable @Override public KDoc getDocComment() { diff --git a/compiler/frontend/src/org/jetbrains/kotlin/psi/JetElement.kt b/compiler/frontend/src/org/jetbrains/kotlin/psi/JetElement.kt index 2a727701013..fefb3bab1a3 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/psi/JetElement.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/psi/JetElement.kt @@ -29,3 +29,11 @@ public interface JetElement : NavigatablePsiElement { @deprecated("Don't use getReference() on JetElement for the choice is unpredictable") override fun getReference(): PsiReference? } + +public fun JetElement.getModificationStamp(): Long { + return when (this) { + is JetFile -> this.modificationStamp + is JetDeclarationStub<*> -> this.modificationStamp + else -> (parent as JetElement).getModificationStamp() + } +} diff --git a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/SubtreeModificationCountUpdater.kt b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/SubtreeModificationCountUpdater.kt deleted file mode 100644 index a35678d0848..00000000000 --- a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/SubtreeModificationCountUpdater.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2010-2015 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.jetbrains.kotlin.idea.caches.resolve - -import com.intellij.openapi.util.Key -import com.intellij.psi.PsiElement -import com.intellij.psi.impl.PsiTreeChangeEventImpl -import com.intellij.psi.impl.PsiTreeChangePreprocessor -import org.jetbrains.kotlin.psi.JetFile -import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf - -public class SubtreeModificationCountUpdater : PsiTreeChangePreprocessor { - override fun treeChanged(event: PsiTreeChangeEventImpl) { - if (event.file !is JetFile) return - - when (event.code!!) { - PsiTreeChangeEventImpl.PsiEventType.BEFORE_CHILDREN_CHANGE, - PsiTreeChangeEventImpl.PsiEventType.BEFORE_PROPERTY_CHANGE, - PsiTreeChangeEventImpl.PsiEventType.BEFORE_CHILD_MOVEMENT, - PsiTreeChangeEventImpl.PsiEventType.BEFORE_CHILD_REPLACEMENT, - PsiTreeChangeEventImpl.PsiEventType.BEFORE_CHILD_ADDITION, - PsiTreeChangeEventImpl.PsiEventType.BEFORE_CHILD_REMOVAL, - PsiTreeChangeEventImpl.PsiEventType.PROPERTY_CHANGED -> { - // skip - } - - PsiTreeChangeEventImpl.PsiEventType.CHILD_ADDED, - PsiTreeChangeEventImpl.PsiEventType.CHILD_REMOVED, - PsiTreeChangeEventImpl.PsiEventType.CHILD_REPLACED -> { - incCounters(event.parent) - } - - PsiTreeChangeEventImpl.PsiEventType.CHILDREN_CHANGED -> { - if (!event.isGenericChange) { - incCounters(event.parent) - } - } - - PsiTreeChangeEventImpl.PsiEventType.CHILD_MOVED -> { - incCounters(event.oldParent) - incCounters(event.newParent) - } - - else -> error("Unknown code:${event.code}") - } - } - - private fun incCounters(element: PsiElement?) { - element?.parentsWithSelf?.forEach { - val count = it.getUserData(MODIFICATION_COUNT_KEY) - if (count != null) { - it.putUserData(MODIFICATION_COUNT_KEY, count + 1) - } - } - } - - companion object { - private val MODIFICATION_COUNT_KEY = Key("SubtreeModificationCountUpdater.MODIFICATION_COUNT_KEY") - - public fun getModificationCount(element: PsiElement): Int { - element.getUserData(MODIFICATION_COUNT_KEY)?.let { return it } - - element.putUserData(MODIFICATION_COUNT_KEY, 0) - return 0 - } - } -} \ No newline at end of file diff --git a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/project/ResolveElementCache.kt b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/project/ResolveElementCache.kt index b5b13514f51..c93752b0177 100644 --- a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/project/ResolveElementCache.kt +++ b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/project/ResolveElementCache.kt @@ -23,8 +23,6 @@ import com.intellij.psi.util.CachedValuesManager import com.intellij.psi.util.PsiModificationTracker import com.intellij.util.containers.ContainerUtil import org.jetbrains.kotlin.asJava.KotlinCodeBlockModificationListener -import org.jetbrains.kotlin.descriptors.ModuleDescriptor -import org.jetbrains.kotlin.idea.caches.resolve.SubtreeModificationCountUpdater import org.jetbrains.kotlin.idea.stubindex.JetProbablyNothingFunctionShortNameIndex import org.jetbrains.kotlin.idea.stubindex.JetProbablyNothingPropertyShortNameIndex import org.jetbrains.kotlin.psi.* @@ -33,34 +31,53 @@ import org.jetbrains.kotlin.resolve.BodyResolveCache import org.jetbrains.kotlin.resolve.StatementFilter import org.jetbrains.kotlin.resolve.TargetPlatform import org.jetbrains.kotlin.resolve.lazy.* +import org.jetbrains.kotlin.utils.addToStdlib.check public class ResolveElementCache(resolveSession: ResolveSession, private val project: Project) : ElementResolver(resolveSession), BodyResolveCache { - private class CachedResolve(val bindingContext: BindingContext, resolveElement: JetElement) { - private val elementModificationCount: Int? = - if (resolveElement is JetDeclaration && KotlinCodeBlockModificationListener.isBlockDeclaration(resolveElement)) - SubtreeModificationCountUpdater.getModificationCount(resolveElement) - else - null + private class CachedFullResolve(val bindingContext: BindingContext, resolveElement: JetElement) { + private val modificationStamp: Long? = modificationStamp(resolveElement) - fun isUpToDate(resolveElement: JetElement) - = elementModificationCount == null || elementModificationCount == SubtreeModificationCountUpdater.getModificationCount(resolveElement) + fun isUpToDate(resolveElement: JetElement) = modificationStamp == modificationStamp(resolveElement) + + private fun modificationStamp(resolveElement: JetElement): Long? { + val file = resolveElement.containingFile + return if (!file.isPhysical) // for non-physical file we don't get OUT_OF_CODE_BLOCK_MODIFICATION_COUNT increased and must reset data on any modification of the file + file.modificationStamp + else if (resolveElement is JetDeclaration && KotlinCodeBlockModificationListener.isBlockDeclaration(resolveElement)) + resolveElement.getModificationStamp() + else + null + } } // drop whole cache after change "out of code block" - private val fullResolveCache: CachedValue> = CachedValuesManager.getManager(project).createCachedValue( - object : CachedValueProvider> { - override fun compute(): CachedValueProvider.Result> { - return CachedValueProvider.Result.create(ContainerUtil.createConcurrentSoftValueMap(), + private val fullResolveCache: CachedValue> = CachedValuesManager.getManager(project).createCachedValue( + object : CachedValueProvider> { + override fun compute(): CachedValueProvider.Result> { + return CachedValueProvider.Result.create(ContainerUtil.createConcurrentSoftValueMap(), PsiModificationTracker.OUT_OF_CODE_BLOCK_MODIFICATION_COUNT, resolveSession.exceptionTracker) } }, false) - private val partialBodyResolveCache: CachedValue> = CachedValuesManager.getManager(project).createCachedValue( - object : CachedValueProvider> { - override fun compute(): CachedValueProvider.Result> { - return CachedValueProvider.Result.create(ContainerUtil.createConcurrentSoftValueMap(), + private class CachedPartialResolve(val bindingContext: BindingContext, file: JetFile) { + private val modificationStamp: Long? = modificationStamp(file) + + fun isUpToDate(file: JetFile) = modificationStamp == modificationStamp(file) + + private fun modificationStamp(file: JetFile): Long? { + return if (!file.isPhysical) // for non-physical file we don't get MODIFICATION_COUNT increased and must reset data on any modification of the file + file.modificationStamp + else + null + } + } + + private val partialBodyResolveCache: CachedValue> = CachedValuesManager.getManager(project).createCachedValue( + object : CachedValueProvider> { + override fun compute(): CachedValueProvider.Result> { + return CachedValueProvider.Result.create(ContainerUtil.createConcurrentSoftValueMap(), PsiModificationTracker.MODIFICATION_COUNT, resolveSession.exceptionTracker) } @@ -83,7 +100,7 @@ public class ResolveElementCache(resolveSession: ResolveSession, private val pro when (bodyResolveMode) { BodyResolveMode.FULL -> { val bindingContext = performElementAdditionalResolve(resolveElement, resolveElement, BodyResolveMode.FULL).first - fullResolveMap[resolveElement] = CachedResolve(bindingContext, resolveElement) + fullResolveMap[resolveElement] = CachedFullResolve(bindingContext, resolveElement) return bindingContext } @@ -92,23 +109,28 @@ public class ResolveElementCache(resolveSession: ResolveSession, private val pro return getElementAdditionalResolve(resolveElement, contextElement, BodyResolveMode.FULL) } + val file = resolveElement.getContainingJetFile() val statementToResolve = PartialBodyResolveFilter.findStatementToResolve(contextElement, resolveElement) val partialResolveMap = partialBodyResolveCache.value - partialResolveMap[statementToResolve ?: resolveElement]?.let { return it } // partial resolve is already cached for this statement + partialResolveMap[statementToResolve ?: resolveElement] + ?.check { it.isUpToDate(file) } + ?.let { return it.bindingContext } // partial resolve is already cached for this statement val (bindingContext, statementFilter) = performElementAdditionalResolve(resolveElement, contextElement, BodyResolveMode.PARTIAL) if (statementFilter == StatementFilter.NONE) { // partial resolve is not supported for the given declaration - full resolve performed instead - fullResolveMap[resolveElement] = CachedResolve(bindingContext, resolveElement) + fullResolveMap[resolveElement] = CachedFullResolve(bindingContext, resolveElement) return bindingContext } + val resolveToCache = CachedPartialResolve(bindingContext, file) + for (statement in (statementFilter as PartialBodyResolveFilter).allStatementsToResolve) { if (!partialResolveMap.containsKey(statement) && bindingContext[BindingContext.PROCESSED, statement] == true) { - partialResolveMap[statement] = bindingContext + partialResolveMap[statement] = resolveToCache } } - partialResolveMap[resolveElement] = bindingContext // we use the whole declaration key in the map to obtain resolve not inside any block (e.g. default parameter values) + partialResolveMap[resolveElement] = resolveToCache // we use the whole declaration key in the map to obtain resolve not inside any block (e.g. default parameter values) return bindingContext } @@ -136,3 +158,4 @@ public class ResolveElementCache(resolveSession: ResolveSession, private val pro override fun resolveFunctionBody(function: JetNamedFunction) = getElementAdditionalResolve(function, function, BodyResolveMode.FULL) } + diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index bf2660530b5..c15ae5c9456 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -503,7 +503,6 @@ - diff --git a/idea/tests/org/jetbrains/kotlin/idea/ResolveElementCacheTest.kt b/idea/tests/org/jetbrains/kotlin/idea/ResolveElementCacheTest.kt index 5ea90111215..789f48fafc9 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/ResolveElementCacheTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/ResolveElementCacheTest.kt @@ -39,6 +39,7 @@ class C(param1: String = "", param2: Int = 0) { } fun b() { + x(1) } fun c() { @@ -56,45 +57,83 @@ class C(param1: String = "", param2: Int = 0) { private fun doTest(handler: Data.() -> Unit) { val file = myFixture.configureByText("Test.kt", FILE_TEXT) as JetFile - val klass = file.getDeclarations().single() as JetClass - val members = klass.getDeclarations() - val function = members.first() as JetNamedFunction - val statements = (function.getBodyExpression() as JetBlockExpression).getStatements() - myFixture.getProject().executeWriteCommand("") { - Data(file, klass, members, statements, JetPsiFactory(getProject())).handler() - } + val data = extractData(file) + myFixture.project.executeWriteCommand("") { data.handler() } } - public fun testResolveCaching() { + private fun extractData(file: JetFile): Data { + val klass = file.declarations.single() as JetClass + val members = klass.declarations + val function = members.first() as JetNamedFunction + val statements = (function.bodyExpression as JetBlockExpression).statements + return Data(file, klass, members, statements, JetPsiFactory(getProject())) + } + + public fun testFullResolveCaching() { + doTest { this.testResolveCaching() } + } + + private fun Data.testResolveCaching() { + // resolve statements in "a()" + val statement1 = statements[0] + val statement2 = statements[1] + val aFunBodyContext1 = statement1.analyze(BodyResolveMode.FULL) + val aFunBodyContext2 = statement2.analyze(BodyResolveMode.FULL) + assert(aFunBodyContext1 === aFunBodyContext2) + + val aFunBodyContext3 = statement1.analyze(BodyResolveMode.FULL) + assert(aFunBodyContext3 === aFunBodyContext1) + + val bFun = members[1] as JetNamedFunction + val bBody = bFun.bodyExpression as JetBlockExpression + val bStatement = bBody.statements[0] + val bFunBodyContext = bStatement.analyze(BodyResolveMode.FULL) + + // modify body of "b()" + bBody.addAfter(factory.createExpression("x()"), bBody.lBrace) + val bFunBodyContextAfterChange1 = bStatement.analyze(BodyResolveMode.FULL) + assert(bFunBodyContext !== bFunBodyContextAfterChange1) + + if (file.isPhysical) { // for non-physical files we reset caches for whole file + val aFunBodyContextAfterChange1 = statement1.analyze(BodyResolveMode.FULL) + assert(aFunBodyContextAfterChange1 === aFunBodyContext1) // change in other function's body should not affect resolve of other one + } + + // add parameter to "b()" this should invalidate all resolve + bFun.valueParameterList!!.addParameter(factory.createParameter("p: Int")) + + val aFunBodyContextAfterChange2 = statement1.analyze(BodyResolveMode.FULL) + assert(aFunBodyContextAfterChange2 !== aFunBodyContext1) + + val bFunBodyContextAfterChange2 = bStatement.analyze(BodyResolveMode.FULL) + assert(bFunBodyContextAfterChange2 !== bFunBodyContextAfterChange1) + } + + public fun testNonPhysicalFileFullResolveCaching() { doTest { - // resolve statements in "a()" - val statement1 = statements[0] - val statement2 = statements[1] - val bindingContext1 = statement1.analyze(BodyResolveMode.FULL) - val bindingContext2 = statement2.analyze(BodyResolveMode.FULL) - assert(bindingContext1 === bindingContext2) + val nonPhysicalFile = JetPsiFactory(getProject()).createAnalyzableFile("NonPhysical.kt", FILE_TEXT, file) + val nonPhysicalData = extractData(nonPhysicalFile) + nonPhysicalData.testResolveCaching() - val bindingContext3 = statement1.analyze(BodyResolveMode.FULL) - assert(bindingContext3 === bindingContext1) + // now check how changes in the physical file affect non-physical one + val statement = nonPhysicalData.statements[0] + val nonPhysicalContext1 = statement.analyze(BodyResolveMode.FULL) - // modify body of "b()" - val bFun = members[1] as JetNamedFunction - val bBody = bFun.bodyExpression as JetBlockExpression - bBody.addAfter(factory.createExpression("x()"), bBody.lBrace) + statements[0].delete() - val bindingContext4 = statement1.analyze(BodyResolveMode.FULL) - assert(bindingContext4 === bindingContext1) // change in other function's body should not affect resolve of other one + val nonPhysicalContext2 = statement.analyze(BodyResolveMode.FULL) + assert(nonPhysicalContext2 === nonPhysicalContext1) // change inside function's body should not affect other files - // add parameter to "b()" this should invalidate all resolve - bFun.valueParameterList!!.addParameter(factory.createParameter("p: Int")) + members[2].delete() - val bindingContext5 = statement1.analyze(BodyResolveMode.FULL) - assert(bindingContext5 !== bindingContext1) + val nonPhysicalContext3 = statement.analyze(BodyResolveMode.FULL) + assert(nonPhysicalContext3 !== nonPhysicalContext1) - statement1.parent.addAfter(factory.createExpression("x()"), statement1) - - val bindingContext6 = statement1.analyze(BodyResolveMode.FULL) - assert(bindingContext6 !== bindingContext5) + // and now check that non-physical changes do not affect physical world + val physicalContext1 = statements[1].analyze(BodyResolveMode.FULL) + nonPhysicalData.members[0].delete() + val physicalContext2 = statements[1].analyze(BodyResolveMode.FULL) + assert(physicalContext1 === physicalContext2) } } @@ -149,27 +188,37 @@ class C(param1: String = "", param2: Int = 0) { } public fun testPartialResolveCaching() { + doTest { this.testPartialResolveCaching() } + } + + private fun Data.testPartialResolveCaching() { + val statement1 = statements[0] + val statement2 = statements[1] + val bindingContext1 = statement1.analyze(BodyResolveMode.PARTIAL) + val bindingContext2 = statement2.analyze(BodyResolveMode.PARTIAL) + assert(bindingContext1 !== bindingContext2) + + val bindingContext3 = statement1.analyze(BodyResolveMode.PARTIAL) + val bindingContext4 = statement2.analyze(BodyResolveMode.PARTIAL) + assert(bindingContext3 === bindingContext1) + assert(bindingContext4 === bindingContext2) + + file.add(factory.createFunction("fun foo(){}")) + + val bindingContext5 = statement1.analyze(BodyResolveMode.PARTIAL) + assert(bindingContext5 !== bindingContext1) + + statement1.getParent().addAfter(factory.createExpression("x()"), statement1) + + val bindingContext6 = statement1.analyze(BodyResolveMode.PARTIAL) + assert(bindingContext6 !== bindingContext5) + } + + public fun testNonPhysicalFilePartialResolveCaching() { doTest { - val statement1 = statements[0] - val statement2 = statements[1] - val bindingContext1 = statement1.analyze(BodyResolveMode.PARTIAL) - val bindingContext2 = statement2.analyze(BodyResolveMode.PARTIAL) - assert(bindingContext1 !== bindingContext2) - - val bindingContext3 = statement1.analyze(BodyResolveMode.PARTIAL) - val bindingContext4 = statement2.analyze(BodyResolveMode.PARTIAL) - assert(bindingContext3 === bindingContext1) - assert(bindingContext4 === bindingContext2) - - file.add(factory.createFunction("fun foo(){}")) - - val bindingContext5 = statement1.analyze(BodyResolveMode.PARTIAL) - assert(bindingContext5 !== bindingContext1) - - statement1.getParent().addAfter(factory.createExpression("x()"), statement1) - - val bindingContext6 = statement1.analyze(BodyResolveMode.PARTIAL) - assert(bindingContext6 !== bindingContext5) + val nonPhysicalFile = JetPsiFactory(getProject()).createAnalyzableFile("NonPhysical.kt", FILE_TEXT, file) + val nonPhysicalData = extractData(nonPhysicalFile) + nonPhysicalData.testPartialResolveCaching() } }