ResolutionFacade for synthetic files should be invalidated on changes in these synthetic files

Fixed EA-77017
This commit is contained in:
Valentin Kipyatkov
2016-03-01 13:56:22 +03:00
parent 28e85517eb
commit 05dc4c1c48
8 changed files with 108 additions and 151 deletions
@@ -16,15 +16,16 @@
package org.jetbrains.kotlin.asJava
import com.intellij.openapi.diagnostic.Logger
import com.intellij.psi.PsiClass
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.pom.PomManager
import com.intellij.pom.PomModelAspect
import com.intellij.pom.event.PomModelEvent
import com.intellij.pom.event.PomModelListener
import com.intellij.pom.tree.TreeAspect
import com.intellij.pom.tree.events.TreeChangeEvent
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
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 org.jetbrains.kotlin.psi.psiUtil.isAncestor
@@ -33,84 +34,40 @@ import org.jetbrains.kotlin.psi.psiUtil.parents
/**
* Tested in OutOfBlockModificationTestGenerated
*/
class KotlinCodeBlockModificationListener(modificationTracker: PsiModificationTracker) : PsiTreeChangePreprocessor {
private val myModificationTracker = modificationTracker as PsiModificationTrackerImpl
override fun treeChanged(event: PsiTreeChangeEventImpl) {
if (event.file !is KtFile) return
when (event.code) {
BEFORE_CHILDREN_CHANGE,
BEFORE_PROPERTY_CHANGE,
BEFORE_CHILD_MOVEMENT,
BEFORE_CHILD_REPLACEMENT,
BEFORE_CHILD_ADDITION,
BEFORE_CHILD_REMOVAL -> {
// skip
class KotlinCodeBlockModificationListener(
modificationTracker: PsiModificationTracker,
private val project: Project,
private val treeAspect: TreeAspect
) {
init {
val model = PomManager.getModel(project)
@Suppress("NAME_SHADOWING")
val modificationTracker = modificationTracker as PsiModificationTrackerImpl
model.addModelListener(object: PomModelListener {
override fun isAspectChangeInteresting(aspect: PomModelAspect): Boolean {
return aspect == treeAspect
}
CHILD_ADDED,
CHILD_REMOVED,
CHILD_REPLACED -> {
processChange(event.parent, event.oldChild, event.child)
}
CHILDREN_CHANGED -> {
if (!event.isGenericChange) {
processChange(event.parent, event.parent, null)
override fun modelChanged(event: PomModelEvent) {
val changeSet = event.getChangeSet(treeAspect) as TreeChangeEvent? ?: return
val file = changeSet.rootElement.psi.containingFile as? KtFile ?: return
if (changeSet.changedElements.any { !isInsideCodeBlock(it.psi) }) {
if (file.isPhysical) {
modificationTracker.incCounter()
}
incOutOfBlockModificationCount(file)
}
}
CHILD_MOVED,
PROPERTY_CHANGED -> {
myModificationTracker.incCounter()
}
else -> LOG.error("Unknown code:" + event.code)
}
}
private fun processChange(parent: PsiElement?, child1: PsiElement?, child2: PsiElement?) {
try {
if (!isInsideCodeBlock(parent)) {
if (parent != null && parent.containingFile is KtFile) {
myModificationTracker.incCounter()
}
else {
myModificationTracker.incOutOfCodeBlockModificationCounter()
}
return
}
if (containsClassesInside(child1) || (child2 != child1 && containsClassesInside(child2))) {
myModificationTracker.incCounter()
}
}
catch (e: PsiInvalidElementAccessException) {
myModificationTracker.incCounter() // Shall not happen actually, just a pre-release paranoia
}
})
}
companion object {
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.firstChild
while (child != null) {
if (containsClassesInside(child)) return true
child = child.nextSibling
}
return false
private fun incOutOfBlockModificationCount(file: KtFile) {
val count = file.getUserData(FILE_OUT_OF_BLOCK_MODIFICATION_COUNT) ?: 0
file.putUserData(FILE_OUT_OF_BLOCK_MODIFICATION_COUNT, count + 1)
}
fun isInsideCodeBlock(element: PsiElement?): Boolean {
if (element is PsiFileSystemItem) return false
if (element == null || element.parent == null) return true
private fun isInsideCodeBlock(element: PsiElement): Boolean {
//TODO: other types
val blockDeclaration = KtPsiUtil.getTopmostParentOfTypes(element, *BLOCK_DECLARATION_TYPES) ?: return false
if (blockDeclaration.parents.any { it !is KtClassBody && it !is KtClassOrObject && it !is KtFile }) return false // should not be local declaration
@@ -149,3 +106,8 @@ class KotlinCodeBlockModificationListener(modificationTracker: PsiModificationTr
)
}
}
private val FILE_OUT_OF_BLOCK_MODIFICATION_COUNT = Key<Long>("FILE_OUT_OF_BLOCK_MODIFICATION_COUNT")
val KtFile.outOfBlockModificationCount: Long
get() = getUserData(FILE_OUT_OF_BLOCK_MODIFICATION_COUNT) ?: 0
@@ -112,3 +112,11 @@ inline fun <T, R : Any> Iterable<T>.firstNotNullResult(transform: (T) -> R?): R?
}
return null
}
inline fun <T> Iterable<T>.sumByLong(selector: (T) -> Long): Long {
var sum: Long = 0
for (element in this) {
sum += selector(element)
}
return sum
}
@@ -19,12 +19,14 @@ package org.jetbrains.kotlin.idea.caches.resolve
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectRootModificationTracker
import com.intellij.openapi.util.ModificationTracker
import com.intellij.psi.util.CachedValue
import com.intellij.psi.util.CachedValueProvider
import com.intellij.psi.util.CachedValuesManager
import com.intellij.psi.util.PsiModificationTracker
import com.intellij.util.containers.SLRUCache
import org.jetbrains.kotlin.analyzer.EmptyResolverForProject
import org.jetbrains.kotlin.asJava.outOfBlockModificationCount
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.caches.resolve.KotlinCacheService
import org.jetbrains.kotlin.container.getService
@@ -40,6 +42,7 @@ import org.jetbrains.kotlin.resolve.TargetPlatform
import org.jetbrains.kotlin.resolve.diagnostics.KotlinSuppressCache
import org.jetbrains.kotlin.resolve.jvm.platform.JvmPlatform
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
import org.jetbrains.kotlin.utils.addToStdlib.sumByLong
import org.jetbrains.kotlin.utils.keysToMap
internal val LOG = Logger.getInstance(KotlinCacheService::class.java)
@@ -92,10 +95,10 @@ class KotlinCacheServiceImpl(val project: Project) : KotlinCacheService {
// we assume that all files come from the same module
val targetPlatform = files.map { TargetPlatformDetector.getPlatform(it) }.toSet().single()
val syntheticFileModule = files.map { it.getModuleInfo() }.toSet().single()
val dependenciesForSyntheticFileCache = listOf(
PsiModificationTracker.OUT_OF_CODE_BLOCK_MODIFICATION_COUNT,
KotlinOutOfBlockCompletionModificationTracker.getInstance(project)
)
val filesModificationTracker = ModificationTracker {
files.sumByLong { it.outOfBlockModificationCount }
}
val dependenciesForSyntheticFileCache = listOf(PsiModificationTracker.OUT_OF_CODE_BLOCK_MODIFICATION_COUNT, filesModificationTracker)
val debugName = "completion/highlighting in $syntheticFileModule for files ${files.joinToString { it.name }} for platform $targetPlatform"
return when {
syntheticFileModule is ModuleSourceInfo -> {
@@ -179,19 +182,17 @@ class KotlinCacheServiceImpl(val project: Project) : KotlinCacheService {
private val syntheticFileCachesLock = Any()
private val slruCacheProvider = CachedValueProvider {
private val syntheticFilesCacheProvider = CachedValueProvider {
CachedValueProvider.Result(object : SLRUCache<Set<KtFile>, ProjectResolutionFacade>(2, 3) {
override fun createValue(files: Set<KtFile>): ProjectResolutionFacade {
return createFacadeForSyntheticFiles(files)
}
override fun createValue(files: Set<KtFile>) = createFacadeForSyntheticFiles(files)
}, LibraryModificationTracker.getInstance(project), ProjectRootModificationTracker.getInstance(project))
}
private fun getFacadeForSyntheticFiles(files: Set<KtFile>): ProjectResolutionFacade {
return synchronized(syntheticFileCachesLock) {
synchronized(syntheticFileCachesLock) {
//NOTE: computations inside createCacheForSyntheticFiles depend on project root structure
// so we additionally drop the whole slru cache on change
CachedValuesManager.getManager(project).getCachedValue(project, slruCacheProvider).get(files)
return CachedValuesManager.getManager(project).getCachedValue(project, syntheticFilesCacheProvider).get(files)
}
}
@@ -1,48 +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.components.ServiceManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.SimpleModificationTracker
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.asJava.KotlinCodeBlockModificationListener
// NOTE: Sadly we must track out of block completion and drop sessions for synthetic files after the completion happens.
// Synthetic file for completion can be modified without sending tree changed events and sequence of completions can lead to inconsistent
// resolve session being cached for such a file otherwise.
// This code is not tested. See KT-6216 for an example.
class KotlinOutOfBlockCompletionModificationTracker() : SimpleModificationTracker() {
companion object {
fun getInstance(project: Project): KotlinOutOfBlockCompletionModificationTracker
= ServiceManager.getService(project, KotlinOutOfBlockCompletionModificationTracker::class.java)!!
}
}
fun performCompletionWithOutOfBlockTracking(completionPosition: PsiElement, body: () -> Unit) {
if (KotlinCodeBlockModificationListener.isInsideCodeBlock(completionPosition)) {
body()
return
}
try {
body()
}
finally {
KotlinOutOfBlockCompletionModificationTracker.getInstance(completionPosition.project).incModificationCount()
}
}
@@ -34,7 +34,6 @@ import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.ClassKind
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade
import org.jetbrains.kotlin.idea.caches.resolve.performCompletionWithOutOfBlockTracking
import org.jetbrains.kotlin.idea.completion.smart.SmartCompletion
import org.jetbrains.kotlin.idea.completion.smart.SmartCompletionSession
import org.jetbrains.kotlin.lexer.KtTokens
@@ -254,17 +253,13 @@ class KotlinCompletionContributor : CompletionContributor() {
val context = position.getUserData(CompletionContext.COMPLETION_CONTEXT_KEY)!!
val correctedOffset = context.offsetMap.getOffset(STRING_TEMPLATE_AFTER_DOT_REAL_START_OFFSET)
val correctedParameters = parameters.withPosition(correctedPosition, correctedOffset)
performCompletionWithOutOfBlockTracking(position) {
doComplete(correctedParameters, toFromOriginalFileMapper, result,
lookupElementPostProcessor = { wrapLookupElementForStringTemplateAfterDotCompletion(it) })
}
doComplete(correctedParameters, toFromOriginalFileMapper, result,
lookupElementPostProcessor = { wrapLookupElementForStringTemplateAfterDotCompletion(it) })
return
}
}
performCompletionWithOutOfBlockTracking(position) {
doComplete(parameters, toFromOriginalFileMapper, result)
}
doComplete(parameters, toFromOriginalFileMapper, result)
}
private fun doComplete(
+3 -5
View File
@@ -31,6 +31,9 @@
<component>
<implementation-class>org.jetbrains.kotlin.idea.completion.LookupCancelWatcher</implementation-class>
</component>
<component>
<implementation-class>org.jetbrains.kotlin.asJava.KotlinCodeBlockModificationListener</implementation-class>
</component>
</project-components>
<application-components>
@@ -264,9 +267,6 @@
<projectService serviceInterface="org.jetbrains.kotlin.idea.caches.resolve.LibraryModificationTracker"
serviceImplementation="org.jetbrains.kotlin.idea.caches.resolve.LibraryModificationTracker"/>
<projectService serviceInterface="org.jetbrains.kotlin.idea.caches.resolve.KotlinOutOfBlockCompletionModificationTracker"
serviceImplementation="org.jetbrains.kotlin.idea.caches.resolve.KotlinOutOfBlockCompletionModificationTracker"/>
<projectService serviceInterface="org.jetbrains.kotlin.resolve.jvm.KotlinJavaPsiFacade"
serviceImplementation="org.jetbrains.kotlin.resolve.jvm.KotlinJavaPsiFacade"/>
@@ -583,8 +583,6 @@
<editorNotificationProvider implementation="org.jetbrains.kotlin.idea.versions.UnsupportedAbiVersionNotificationPanelProvider"/>
<editorNotificationProvider implementation="org.jetbrains.kotlin.idea.configuration.KotlinSetupEnvironmentNotificationProvider"/>
<psi.treeChangePreprocessor implementation="org.jetbrains.kotlin.asJava.KotlinCodeBlockModificationListener"/>
<referencesSearch implementation="org.jetbrains.kotlin.idea.search.ideaExtensions.KotlinReferencesSearcher"/>
<directClassInheritorsSearch implementation="org.jetbrains.kotlin.idea.search.ideaExtensions.KotlinDirectInheritorsSearcher"/>
<overridingMethodsSearch implementation="org.jetbrains.kotlin.idea.search.ideaExtensions.KotlinOverridingMethodsWithGenericsSearcher"/>
@@ -22,7 +22,6 @@ import com.intellij.psi.PsiElement
import com.intellij.psi.PsiRecursiveElementVisitor
import com.intellij.psi.codeStyle.CodeStyleManager
import com.intellij.util.SmartList
import org.jetbrains.kotlin.idea.caches.resolve.KotlinOutOfBlockCompletionModificationTracker
import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade
import org.jetbrains.kotlin.idea.caches.resolve.resolveImportReference
import org.jetbrains.kotlin.idea.conversion.copy.range
@@ -66,9 +65,6 @@ class J2kPostProcessor(private val formatCode: Boolean) : PostProcessor {
if (modificationStamp == file.modificationStamp) break
//TODO: it's a hack!
KotlinOutOfBlockCompletionModificationTracker.getInstance(file.project).incModificationCount()
elementToActions = collectAvailableActions(file, rangeMarker)
}
@@ -0,0 +1,45 @@
/*
* Copyright 2010-2016 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
import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase
import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade
import org.jetbrains.kotlin.idea.test.KotlinWithJdkAndRuntimeLightProjectDescriptor
import org.jetbrains.kotlin.idea.util.getFileResolutionScope
import org.jetbrains.kotlin.incremental.components.NoLookupLocation
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.KtPsiFactory
import org.jetbrains.kotlin.resolve.scopes.utils.findClassifier
class DummyFileResolveCachingTest : LightCodeInsightFixtureTestCase() {
override fun getProjectDescriptor() = KotlinWithJdkAndRuntimeLightProjectDescriptor.INSTANCE
fun test() {
myFixture.configureByText(KotlinFileType.INSTANCE, "")
val dummyFileText = "import java.util.ArrayList"
val dummyFile = KtPsiFactory(project).createAnalyzableFile("Dummy.kt", dummyFileText, file)
dummyFile.getResolutionFacade().getFileResolutionScope(dummyFile)
dummyFile.importDirectives.single().delete()
val resolutionScope = dummyFile.getResolutionFacade().getFileResolutionScope(dummyFile)
val classifier = resolutionScope.findClassifier(Name.identifier("ArrayList"), NoLookupLocation.FROM_IDE)
assertNull(classifier)
}
}