diff --git a/compiler/light-classes/src/org/jetbrains/kotlin/asJava/KotlinCodeBlockModificationListener.kt b/compiler/light-classes/src/org/jetbrains/kotlin/asJava/KotlinCodeBlockModificationListener.kt index 41cb8ad942a..26eaa24e608 100644 --- a/compiler/light-classes/src/org/jetbrains/kotlin/asJava/KotlinCodeBlockModificationListener.kt +++ b/compiler/light-classes/src/org/jetbrains/kotlin/asJava/KotlinCodeBlockModificationListener.kt @@ -17,14 +17,17 @@ package org.jetbrains.kotlin.asJava import com.intellij.openapi.diagnostic.Logger -import com.intellij.psi.* +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFileSystemItem +import com.intellij.psi.PsiInvalidElementAccessException import com.intellij.psi.impl.PsiModificationTrackerImpl -import com.intellij.psi.impl.PsiTreeChangeEventImpl.PsiEventType.* import com.intellij.psi.impl.PsiTreeChangeEventImpl +import com.intellij.psi.impl.PsiTreeChangeEventImpl.PsiEventType.* import com.intellij.psi.impl.PsiTreeChangePreprocessor import com.intellij.psi.util.PsiModificationTracker import org.jetbrains.kotlin.psi.* -import com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.kotlin.psi.psiUtil.isAncestor /** * Tested in OutOfBlockModificationTestGenerated @@ -33,9 +36,9 @@ public class KotlinCodeBlockModificationListener(modificationTracker: PsiModific private val myModificationTracker = modificationTracker as PsiModificationTrackerImpl override fun treeChanged(event: PsiTreeChangeEventImpl) { - if (event.getFile() !is JetFile) return + if (event.file !is JetFile) return - when (event.getCode()) { + when (event.code) { BEFORE_CHILDREN_CHANGE, BEFORE_PROPERTY_CHANGE, BEFORE_CHILD_MOVEMENT, @@ -48,12 +51,12 @@ public class KotlinCodeBlockModificationListener(modificationTracker: PsiModific CHILD_ADDED, CHILD_REMOVED, CHILD_REPLACED -> { - processChange(event.getParent(), event.getOldChild(), event.getChild()) + processChange(event.parent, event.oldChild, event.child) } CHILDREN_CHANGED -> { - if (!event.isGenericChange()) { - processChange(event.getParent(), event.getParent(), null) + if (!event.isGenericChange) { + processChange(event.parent, event.parent, null) } } @@ -62,14 +65,14 @@ public class KotlinCodeBlockModificationListener(modificationTracker: PsiModific myModificationTracker.incCounter() } - else -> LOG.error("Unknown code:" + event.getCode()) + else -> LOG.error("Unknown code:" + event.code) } } private fun processChange(parent: PsiElement?, child1: PsiElement?, child2: PsiElement?) { try { if (!isInsideCodeBlock(parent)) { - if (parent != null && parent.getContainingFile() is JetFile) { + if (parent != null && parent.containingFile is JetFile) { myModificationTracker.incCounter() } else { @@ -88,16 +91,16 @@ public class KotlinCodeBlockModificationListener(modificationTracker: PsiModific } companion object { - private val LOG = Logger.getInstance("#org.jetbrains.kotlin.asJava.JetCodeBlockModificationListener") + private val LOG = Logger.getInstance("#org.jetbrains.kotlin.asJava.KotlinCodeBlockModificationListener") private fun containsClassesInside(element: PsiElement?): Boolean { if (element == null) return false if (element is PsiClass) return true - var child = element.getFirstChild() + var child = element.firstChild while (child != null) { if (containsClassesInside(child)) return true - child = child.getNextSibling() + child = child.nextSibling } return false @@ -105,38 +108,42 @@ public class KotlinCodeBlockModificationListener(modificationTracker: PsiModific public fun isInsideCodeBlock(element: PsiElement?): Boolean { if (element is PsiFileSystemItem) return false - if (element == null || element.getParent() == null) return true + if (element == null || element.parent == null) return true - val blockDeclarationCandidate = JetPsiUtil.getTopmostParentOfTypes( - element, - javaClass(), - javaClass()) + //TODO: other types + val blockDeclaration = JetPsiUtil.getTopmostParentOfTypes(element, *BLOCK_DECLARATION_TYPES) ?: return false - when (blockDeclarationCandidate) { + when (blockDeclaration) { is JetNamedFunction -> { - val function = blockDeclarationCandidate : JetNamedFunction - if (function.hasBlockBody() && function.getBodyExpression().isAncestorOf(element)) { - return true + if (blockDeclaration.hasBlockBody()) { + return blockDeclaration.bodyExpression.isAncestor(element) } - - if (function.hasDeclaredReturnType() && function.getInitializer().isAncestorOf(element)) { - return true + else if (blockDeclaration.hasDeclaredReturnType()) { + return blockDeclaration.initializer.isAncestor(element) } } + is JetProperty -> { - val property = blockDeclarationCandidate : JetProperty - for (accessor in property.getAccessors()) { - when { - accessor.getInitializer().isAncestorOf(element) -> return true - accessor.getBodyExpression().isAncestorOf(element) -> return true + for (accessor in blockDeclaration.accessors) { + if (accessor.initializer.isAncestor(element) || accessor.bodyExpression.isAncestor(element)) { + return true } } } + + else -> throw IllegalStateException() } return false } - private fun PsiElement?.isAncestorOf(element: PsiElement) = PsiTreeUtil.isAncestor(this, element, false) + public fun isBlockDeclaration(declaration: JetDeclaration): Boolean { + return BLOCK_DECLARATION_TYPES.any { it.isInstance(declaration) } + } + + private val BLOCK_DECLARATION_TYPES = arrayOf>( + javaClass(), + javaClass() + ) } } 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 new file mode 100644 index 00000000000..a35678d0848 --- /dev/null +++ b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/caches/resolve/SubtreeModificationCountUpdater.kt @@ -0,0 +1,81 @@ +/* + * 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 ede6e1c6dfa..b5b13514f51 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 @@ -22,6 +22,9 @@ import com.intellij.psi.util.CachedValueProvider 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.* @@ -32,13 +35,24 @@ import org.jetbrains.kotlin.resolve.TargetPlatform import org.jetbrains.kotlin.resolve.lazy.* public class ResolveElementCache(resolveSession: ResolveSession, private val project: Project) : ElementResolver(resolveSession), BodyResolveCache { - // Recreate internal cache after change of modification count - private val fullResolveCache: CachedValue> = CachedValuesManager.getManager(project).createCachedValue( - object : CachedValueProvider> { - override fun compute(): CachedValueProvider.Result> { - return CachedValueProvider.Result.create(ContainerUtil.createConcurrentSoftValueMap(), - PsiModificationTracker.MODIFICATION_COUNT, - resolveSession.getExceptionTracker()) + private class CachedResolve(val bindingContext: BindingContext, resolveElement: JetElement) { + private val elementModificationCount: Int? = + if (resolveElement is JetDeclaration && KotlinCodeBlockModificationListener.isBlockDeclaration(resolveElement)) + SubtreeModificationCountUpdater.getModificationCount(resolveElement) + else + null + + fun isUpToDate(resolveElement: JetElement) + = elementModificationCount == null || elementModificationCount == SubtreeModificationCountUpdater.getModificationCount(resolveElement) + } + + // 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(), + PsiModificationTracker.OUT_OF_CODE_BLOCK_MODIFICATION_COUNT, + resolveSession.exceptionTracker) } }, false) @@ -48,19 +62,28 @@ public class ResolveElementCache(resolveSession: ResolveSession, private val pro override fun compute(): CachedValueProvider.Result> { return CachedValueProvider.Result.create(ContainerUtil.createConcurrentSoftValueMap(), PsiModificationTracker.MODIFICATION_COUNT, - resolveSession.getExceptionTracker()) + resolveSession.exceptionTracker) } }, false) override fun getElementAdditionalResolve(resolveElement: JetElement, contextElement: JetElement, bodyResolveMode: BodyResolveMode): BindingContext { - val fullResolveMap = fullResolveCache.getValue() - fullResolveMap[resolveElement]?.let { return it } // check if full additional resolve already performed + // check if full additional resolve already performed and is up-to-date + val fullResolveMap = fullResolveCache.value + val cachedFullResolve = fullResolveMap[resolveElement] + if (cachedFullResolve != null) { + if (cachedFullResolve.isUpToDate(resolveElement)) { + return cachedFullResolve.bindingContext + } + else { + fullResolveMap.remove(resolveElement) // remove outdated cache entry + } + } when (bodyResolveMode) { BodyResolveMode.FULL -> { val bindingContext = performElementAdditionalResolve(resolveElement, resolveElement, BodyResolveMode.FULL).first - fullResolveMap[resolveElement] = bindingContext + fullResolveMap[resolveElement] = CachedResolve(bindingContext, resolveElement) return bindingContext } @@ -70,14 +93,13 @@ public class ResolveElementCache(resolveSession: ResolveSession, private val pro } val statementToResolve = PartialBodyResolveFilter.findStatementToResolve(contextElement, resolveElement) - val partialResolveMap = partialBodyResolveCache.getValue() + val partialResolveMap = partialBodyResolveCache.value partialResolveMap[statementToResolve ?: resolveElement]?.let { return it } // 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] = bindingContext + if (statementFilter == StatementFilter.NONE) { // partial resolve is not supported for the given declaration - full resolve performed instead + fullResolveMap[resolveElement] = CachedResolve(bindingContext, resolveElement) return bindingContext } diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index 2f40efd8c80..16ac926fb03 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -498,6 +498,7 @@ + diff --git a/idea/tests/org/jetbrains/kotlin/idea/ResolveElementCacheTest.kt b/idea/tests/org/jetbrains/kotlin/idea/ResolveElementCacheTest.kt index 69355a35836..5ea90111215 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/ResolveElementCacheTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/ResolveElementCacheTest.kt @@ -16,11 +16,13 @@ package org.jetbrains.kotlin.idea +import com.intellij.psi.PsiDocumentManager import org.jetbrains.kotlin.idea.caches.resolve.analyze import org.jetbrains.kotlin.idea.test.JetLightCodeInsightFixtureTestCase import org.jetbrains.kotlin.idea.test.JetLightProjectDescriptor import org.jetbrains.kotlin.idea.util.application.executeWriteCommand import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.startOffset import org.jetbrains.kotlin.resolve.BindingContext import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode @@ -65,6 +67,7 @@ class C(param1: String = "", param2: Int = 0) { public fun testResolveCaching() { doTest { + // resolve statements in "a()" val statement1 = statements[0] val statement2 = statements[1] val bindingContext1 = statement1.analyze(BodyResolveMode.FULL) @@ -74,15 +77,60 @@ class C(param1: String = "", param2: Int = 0) { val bindingContext3 = statement1.analyze(BodyResolveMode.FULL) assert(bindingContext3 === bindingContext1) - file.add(factory.createFunction("fun foo(){}")) + // modify body of "b()" + val bFun = members[1] as JetNamedFunction + val bBody = bFun.bodyExpression as JetBlockExpression + bBody.addAfter(factory.createExpression("x()"), bBody.lBrace) val bindingContext4 = statement1.analyze(BodyResolveMode.FULL) - assert(bindingContext4 !== bindingContext1) + assert(bindingContext4 === bindingContext1) // change in other function's body should not affect resolve of other one - statement1.getParent().addAfter(factory.createExpression("x()"), statement1) + // add parameter to "b()" this should invalidate all resolve + bFun.valueParameterList!!.addParameter(factory.createParameter("p: Int")) val bindingContext5 = statement1.analyze(BodyResolveMode.FULL) - assert(bindingContext5 !== bindingContext4) + assert(bindingContext5 !== bindingContext1) + + statement1.parent.addAfter(factory.createExpression("x()"), statement1) + + val bindingContext6 = statement1.analyze(BodyResolveMode.FULL) + assert(bindingContext6 !== bindingContext5) + } + } + + public fun testResolveSurvivesTypingInCodeBlock() { + doTest { + val statement = statements[0] + val bindingContext1 = statement.analyze(BodyResolveMode.FULL) + + val classConstructorParamTypeRef = klass.getPrimaryConstructor()!!.valueParameters.first().typeReference!! + val bindingContext2 = classConstructorParamTypeRef.analyze(BodyResolveMode.FULL) + + val documentManager = PsiDocumentManager.getInstance(getProject()) + val document = documentManager.getDocument(file)!! + documentManager.doPostponedOperationsAndUnblockDocument(document) + + // modify body of "b()" via document + val bFun = members[1] as JetNamedFunction + val bBody = bFun.bodyExpression as JetBlockExpression + document.insertString(bBody.lBrace!!.startOffset + 1, "x()") + documentManager.commitAllDocuments() + + val bindingContext3 = statement.analyze(BodyResolveMode.FULL) + assert(bindingContext3 === bindingContext1) + + val bindingContext4 = classConstructorParamTypeRef.analyze(BodyResolveMode.FULL) + assert(bindingContext4 === bindingContext2) + + // insert statement in "a()" + document.insertString(statement.startOffset, "x()\n") + documentManager.commitAllDocuments() + + val bindingContext5 = (members[0] as JetNamedFunction).bodyExpression!!.analyze(BodyResolveMode.FULL) + assert(bindingContext5 !== bindingContext1) + + val bindingContext6 = classConstructorParamTypeRef.analyze(BodyResolveMode.FULL) + assert(bindingContext6 === bindingContext2) } }