[frameworks] IDEA-302600 UAST variable reference search during reference contribution slows down highlighting

GitOrigin-RevId: 31d76d98a28d9db626b91e900ceb89d6f3822a31
This commit is contained in:
Yuriy Artamonov
2022-09-28 17:48:43 +02:00
committed by intellij-monorepo-bot
parent 7b60305ad9
commit e7c4768fe0
5 changed files with 208 additions and 90 deletions

View File

@@ -14,6 +14,7 @@ import com.intellij.util.ArrayUtilRt;
import com.intellij.util.ProcessingContext;
import com.intellij.util.SmartList;
import com.intellij.util.containers.ConcurrentFactoryMap;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -32,20 +33,20 @@ public class PsiReferenceRegistrarImpl extends PsiReferenceRegistrar {
private final List<Disposable> myCleanupDisposables = new ArrayList<>();
PsiReferenceRegistrarImpl() {
myBindingCache = ConcurrentFactoryMap.createMap(key-> {
List<ProviderBinding> result = new SmartList<>();
for (Class<?> bindingClass : myBindingsMap.keySet()) {
if (bindingClass.isAssignableFrom(key)) {
result.add(myBindingsMap.get(bindingClass));
}
}
for (Class<?> bindingClass : myNamedBindingsMap.keySet()) {
if (bindingClass.isAssignableFrom(key)) {
result.add(myNamedBindingsMap.get(bindingClass));
}
}
return result.toArray(new ProviderBinding[0]);
}
myBindingCache = ConcurrentFactoryMap.createMap(key -> {
List<ProviderBinding> result = new SmartList<>();
for (Class<?> bindingClass : myBindingsMap.keySet()) {
if (bindingClass.isAssignableFrom(key)) {
result.add(myBindingsMap.get(bindingClass));
}
}
for (Class<?> bindingClass : myNamedBindingsMap.keySet()) {
if (bindingClass.isAssignableFrom(key)) {
result.add(myNamedBindingsMap.get(bindingClass));
}
}
return result.toArray(new ProviderBinding[0]);
}
);
}
@@ -86,7 +87,8 @@ public class PsiReferenceRegistrarImpl extends PsiReferenceRegistrar {
for (PatternCondition<? super String> condition1 : conditions1) {
if (condition1 instanceof ValuePatternCondition) {
final Collection<String> strings = ((ValuePatternCondition)condition1).getValues();
registerNamedReferenceProvider(ArrayUtilRt.toStringArray(strings), nameCondition, scope, true, provider, priority, pattern, parentDisposable);
registerNamedReferenceProvider(ArrayUtilRt.toStringArray(strings), nameCondition, scope, true, provider, priority, pattern,
parentDisposable);
return;
}
if (condition1 instanceof CaseInsensitiveValuePatternCondition) {
@@ -174,9 +176,9 @@ public class PsiReferenceRegistrarImpl extends PsiReferenceRegistrar {
}
}
@NotNull
List<ProviderBinding.ProviderInfo<ProcessingContext>> getPairsByElement(@NotNull PsiElement element,
@NotNull PsiReferenceService.Hints hints) {
@ApiStatus.Internal
public @NotNull List<ProviderBinding.ProviderInfo<ProcessingContext>> getPairsByElement(@NotNull PsiElement element,
@NotNull PsiReferenceService.Hints hints) {
final ProviderBinding[] bindings = myBindingCache.get(element.getClass());
if (bindings.length == 0) return Collections.emptyList();

View File

@@ -978,7 +978,7 @@ uast.evaluator.loop.iteration.limit=20
uast.evaluator.loop.iteration.limit.description=Maximal allowed iteration count for loop evaluation in the Uast TreeBasedEvaluator visitor
uast.references.by.usage=true
uast.references.by.usage.description=Enables Uast reference contributors that searches for usages of variables in the opened file and its module
uast.references.by.usage.description=Enables Uast reference contributors that searches for usages of variables in the opened file
idea.report.nullity.missing.in.generated.overriders=true
idea.report.nullity.missing.in.generated.overriders.description=Whether "@NotNull/@Nullable problems" inspection should complain about overriding methods or parameters missing @NotNull, which occur in generated code

View File

@@ -2,48 +2,46 @@
package com.intellij.psi
import com.intellij.codeInsight.completion.CompletionUtilCoreImpl
import com.intellij.lang.jvm.JvmModifier
import com.intellij.lang.Language
import com.intellij.model.search.SearchService
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.progress.impl.CancellationCheck
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.util.Key
import com.intellij.patterns.ElementPattern
import com.intellij.patterns.uast.UExpressionPattern
import com.intellij.patterns.uast.injectionHostUExpression
import com.intellij.psi.impl.cache.CacheManager
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.UastPatternAdapter.Companion.getOrCreateCachedElement
import com.intellij.psi.impl.source.resolve.reference.PsiReferenceRegistrarImpl
import com.intellij.psi.impl.source.resolve.reference.ReferenceProvidersRegistry
import com.intellij.psi.search.LocalSearchScope
import com.intellij.psi.search.PsiSearchHelper
import com.intellij.psi.search.PsiSearchHelper.SearchCostResult
import com.intellij.psi.search.UsageSearchContext
import com.intellij.psi.util.CachedValueProvider
import com.intellij.psi.util.CachedValueProvider.Result
import com.intellij.psi.util.CachedValuesManager
import com.intellij.psi.util.PsiModificationTracker
import com.intellij.util.ProcessingContext
import com.intellij.util.containers.ContainerUtil
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.uast.*
import org.jetbrains.uast.expressions.UInjectionHost
internal class UastReferenceByUsageAdapter(private val usagePattern: ElementPattern<out UElement>,
private val provider: UastReferenceProvider) : UastReferenceProvider(UExpression::class.java) {
class UastReferenceByUsageAdapter(
val expressionPattern: ElementPattern<out UElement>,
val usagePattern: ElementPattern<out UElement>,
val provider: UastReferenceProvider
) : PsiReferenceProvider() {
val supportedUElementTypes: List<Class<UExpression>> = listOf(UExpression::class.java)
override fun acceptsTarget(target: PsiElement): Boolean {
return provider.acceptsTarget(target)
}
override fun getReferencesByElement(element: UElement, context: ProcessingContext): Array<PsiReference> {
val parentVariable = when (val uastParent = getOriginalUastParent(element)) {
is UVariable -> uastParent
is UPolyadicExpression -> uastParent.uastParent as? UVariable // support .withUastParentOrSelf() patterns
else -> null
}
override fun getReferencesByElement(element: PsiElement, context: ProcessingContext): Array<PsiReference> {
val uElement = getOrCreateCachedElement(element, context, supportedUElementTypes) ?: return PsiReference.EMPTY_ARRAY
return CancellationCheck.runWithCancellationCheck { getReferencesByElement(uElement, context) }
}
if (parentVariable == null
|| parentVariable.name.isNullOrEmpty()
|| !parentVariable.type.equalsToText(CommonClassNames.JAVA_LANG_STRING)) {
return PsiReference.EMPTY_ARRAY
}
internal fun getReferencesByElement(element: UElement, context: ProcessingContext): Array<PsiReference> {
val parentVariable = getUsageVariableTargetForInitializer(element) ?: return PsiReference.EMPTY_ARRAY
val usage = getDirectVariableUsages(parentVariable).find { usage ->
val refExpression = getUsageReferenceExpressionWithCache(usage, context)
@@ -58,11 +56,81 @@ internal class UastReferenceByUsageAdapter(private val usagePattern: ElementPatt
override fun toString(): String = "uastReferenceByUsageAdapter($provider)"
}
private fun getUsageVariableTargetForInitializer(element: UElement): UVariable? {
val parentVariable = when (val uastParent = getOriginalUastParent(element)) {
is UVariable -> uastParent
is UPolyadicExpression -> uastParent.uastParent as? UVariable // support .withUastParentOrSelf() patterns
else -> null
}
if (parentVariable == null
|| parentVariable.name.isNullOrEmpty()
|| !parentVariable.type.equalsToText(CommonClassNames.JAVA_LANG_STRING)) {
return null
}
return parentVariable
}
/**
* Find usages of variable or constant in the scope of module.
*
* @param element initializer of variable, String literal
* @param targetHint target hint, usually POM target element
* @return references that are supposed to be created in usage places
*/
@ApiStatus.Internal
fun getReferencesForDirectUsagesOfVariable(element: PsiElement, targetHint: PsiElement?): Array<PsiReference> {
val uElement = element.toUElementOfType<UInjectionHost>() ?: return PsiReference.EMPTY_ARRAY
val originalElement = CompletionUtilCoreImpl.getOriginalElement(element) ?: element
val uParentVariable = getUsageVariableTargetForInitializer(uElement) ?: return PsiReference.EMPTY_ARRAY
val registrar = ReferenceProvidersRegistry.getInstance().getRegistrar(Language.findLanguageByID("UAST")!!)
val providerInfos = (registrar as PsiReferenceRegistrarImpl).getPairsByElement(originalElement, PsiReferenceService.Hints(targetHint, null))
// by-usage providers must implement acceptsTarget correctly, we rely on fact that they accept targetHint
val suitableProviders = providerInfos.asSequence()
.map { it.provider }
.filterIsInstance<UastReferenceByUsageAdapter>()
.toList()
val usages = getDirectVariableUsagesWithNonLocal(uParentVariable)
for (usage in usages) {
val refExpression = usage.toUElementOfType<UReferenceExpression>()
if (refExpression != null) {
val context = ProcessingContext()
context.put(USAGE_PSI_ELEMENT, usage)
context.put(REQUESTED_PSI_ELEMENT, originalElement)
for (provider in suitableProviders) {
if (provider.usagePattern.accepts(refExpression, context)) {
val references = provider.provider.getReferencesByElement(refExpression, context)
if (references.isNotEmpty()) {
return references
}
}
}
}
}
return PsiReference.EMPTY_ARRAY
}
internal val STRICT_CONSTANT_NAME_PATTERN: Regex = Regex("[\\p{Upper}_0-9]+")
@ApiStatus.Experimental
fun uInjectionHostInVariable(): UExpressionPattern<*, *> = injectionHostUExpression().filter {
it.uastParent is UVariable
}
@ApiStatus.Experimental
fun uInjectionHostInStrictConstant(): UExpressionPattern<*, *> = injectionHostUExpression().filter {
val uastParent = it.uastParent
uastParent is UVariable && uastParent.name?.matches(STRICT_CONSTANT_NAME_PATTERN) == true
}
@ApiStatus.Experimental
fun uExpressionInVariable(): UExpressionPattern<*, *> = injectionHostUExpression().filter {
val parent = it.uastParent
@@ -96,61 +164,22 @@ private fun getDirectVariableUsages(uVar: UVariable): Sequence<PsiElement> {
if (DumbService.isDumb(project)) return emptySequence() // do not try to search in dumb mode
val cachedValue = CachedValuesManager.getManager(project).getCachedValue(variablePsi, CachedValueProvider {
val anchors = findDirectVariableUsages(variablePsi).map(PsiAnchor::create)
val anchors = findLocalDirectVariableUsages(variablePsi).map(PsiAnchor::create)
Result.createSingleDependency(anchors, PsiModificationTracker.MODIFICATION_COUNT)
})
return cachedValue.asSequence().mapNotNull(PsiAnchor::retrieve)
}
private const val MAX_FILES_TO_FIND_USAGES: Int = 5
private val STRICT_CONSTANT_NAME_PATTERN: Regex = Regex("[\\p{Upper}_0-9]+")
private fun findDirectVariableUsages(variablePsi: PsiElement): Iterable<PsiElement> {
private fun findLocalDirectVariableUsages(variablePsi: PsiElement): Iterable<PsiElement> {
val uVariable = variablePsi.toUElementOfType<UVariable>()
val variableName = uVariable?.name
if (variableName.isNullOrEmpty()) return emptyList()
val currentFile = variablePsi.containingFile ?: return emptyList()
val localUsages = findVariableUsages(variablePsi, variableName, arrayOf(currentFile))
// non-local searches are limited for real-life use cases, we do not try to find all possible usages
if (uVariable is ULocalVariable
|| (variablePsi is PsiModifierListOwner && variablePsi.hasModifier(JvmModifier.PRIVATE))
|| !STRICT_CONSTANT_NAME_PATTERN.matches(variableName)) {
return localUsages
}
val module = ModuleUtilCore.findModuleForPsiElement(variablePsi) ?: return localUsages
val uastScope = getUastScope(module.moduleScope)
val searchHelper = PsiSearchHelper.getInstance(module.project)
if (searchHelper.isCheapEnoughToSearch(variableName, uastScope, currentFile, null) != SearchCostResult.FEW_OCCURRENCES) {
return localUsages
}
val cacheManager = CacheManager.getInstance(variablePsi.project)
val containingFiles = cacheManager.getVirtualFilesWithWord(
variableName,
UsageSearchContext.IN_CODE,
uastScope,
true)
val useScope = variablePsi.useScope
val psiManager = PsiManager.getInstance(module.project)
val filesToSearch = containingFiles.asSequence()
.filter { useScope.contains(it) && it != currentFile.virtualFile }
.mapNotNull { psiManager.findFile(it) }
.sortedBy { it.virtualFile.canonicalPath }
.take(MAX_FILES_TO_FIND_USAGES)
.toList()
.toTypedArray()
val nonLocalUsages = findVariableUsages(variablePsi, variableName, filesToSearch)
return ContainerUtil.concat(localUsages, nonLocalUsages)
return findReferencedVariableUsages(variablePsi, variableName, arrayOf(currentFile))
}
private fun findVariableUsages(variablePsi: PsiElement, variableName: String, files: Array<PsiFile>): List<PsiElement> {
internal fun findReferencedVariableUsages(variablePsi: PsiElement, variableName: String, files: Array<PsiFile>): List<PsiElement> {
if (files.isEmpty()) return emptyList()
return SearchService.getInstance()
@@ -170,9 +199,4 @@ private fun findVariableUsages(variablePsi: PsiElement, variableName: String, fi
}
.findAll()
.sortedWith(compareBy({ it.containingFile?.virtualFile?.canonicalPath ?: "" }, { it.textOffset }))
}
private fun getUastScope(originalScope: GlobalSearchScope): GlobalSearchScope {
val fileTypes = UastLanguagePlugin.getInstances().map { it.language.associatedFileType }.toTypedArray()
return GlobalSearchScope.getScopeRestrictedByFileTypes(originalScope, *fileTypes)
}

View File

@@ -0,0 +1,91 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@file:JvmName("UastReferenceByUsageResolver")
package com.intellij.psi
import com.intellij.lang.jvm.JvmModifier
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.project.DumbService
import com.intellij.psi.impl.cache.CacheManager
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.PsiSearchHelper
import com.intellij.psi.search.UsageSearchContext
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.annotations.ApiStatus
import org.jetbrains.uast.ULocalVariable
import org.jetbrains.uast.UVariable
import org.jetbrains.uast.UastLanguagePlugin
import org.jetbrains.uast.toUElementOfType
private const val MAX_FILES_TO_FIND_USAGES: Int = 5
/**
* Searches for direct variable usages not only in the same file but also in the module.
* Must be used only from [PsiReference.resolve], do not call from [PsiReferenceProvider.getReferencesByElement].
*/
@ApiStatus.Experimental
internal fun getDirectVariableUsagesWithNonLocal(uVar: UVariable): Sequence<PsiElement> {
val variablePsi = uVar.sourcePsi ?: return emptySequence()
val project = variablePsi.project
if (DumbService.isDumb(project)) return emptySequence() // do not try to search in dumb mode
val cachedValue = CachedValuesManager.getManager(project).getCachedValue(variablePsi, CachedValueProvider {
val anchors = findAllDirectVariableUsages(variablePsi).map(PsiAnchor::create)
CachedValueProvider.Result.createSingleDependency(anchors, PsiModificationTracker.MODIFICATION_COUNT)
})
return cachedValue.asSequence().mapNotNull(PsiAnchor::retrieve)
}
private fun findAllDirectVariableUsages(variablePsi: PsiElement): Iterable<PsiElement> {
val uVariable = variablePsi.toUElementOfType<UVariable>()
val variableName = uVariable?.name
if (variableName.isNullOrEmpty()) return emptyList()
val currentFile = variablePsi.containingFile ?: return emptyList()
val localUsages = findReferencedVariableUsages(variablePsi, variableName, arrayOf(currentFile))
// non-local searches are limited for real-life use cases, we do not try to find all possible usages
if (uVariable is ULocalVariable
|| (variablePsi is PsiModifierListOwner && variablePsi.hasModifier(JvmModifier.PRIVATE))
|| !STRICT_CONSTANT_NAME_PATTERN.matches(variableName)) {
return localUsages
}
val module = ModuleUtilCore.findModuleForPsiElement(variablePsi) ?: return localUsages
val uastScope = getUastScope(module.moduleScope)
val searchHelper = PsiSearchHelper.getInstance(module.project)
if (searchHelper.isCheapEnoughToSearch(variableName, uastScope, currentFile, null) != PsiSearchHelper.SearchCostResult.FEW_OCCURRENCES) {
return localUsages
}
val cacheManager = CacheManager.getInstance(variablePsi.project)
val containingFiles = cacheManager.getVirtualFilesWithWord(
variableName,
UsageSearchContext.IN_CODE,
uastScope,
true)
val useScope = variablePsi.useScope
val psiManager = PsiManager.getInstance(module.project)
val filesToSearch = containingFiles.asSequence()
.filter { useScope.contains(it) && it != currentFile.virtualFile }
.mapNotNull { psiManager.findFile(it) }
.sortedBy { it.virtualFile.canonicalPath }
.take(MAX_FILES_TO_FIND_USAGES)
.toList()
.toTypedArray()
val nonLocalUsages = findReferencedVariableUsages(variablePsi, variableName, filesToSearch)
return ContainerUtil.concat(localUsages, nonLocalUsages)
}
private fun getUastScope(originalScope: GlobalSearchScope): GlobalSearchScope {
val fileTypes = UastLanguagePlugin.getInstances().map { it.language.associatedFileType }.toTypedArray()
return GlobalSearchScope.getScopeRestrictedByFileTypes(originalScope, *fileTypes)
}

View File

@@ -122,6 +122,7 @@ fun uastReferenceProviderByUsage(targetClass: Class<out PsiElement>?,
* Consider using [uastReferenceProviderByUsage] if you need to obtain additional context from a usage place.
*/
@ApiStatus.Experimental
@JvmOverloads
fun PsiReferenceRegistrar.registerReferenceProviderByUsage(expressionPattern: ElementPattern<out UElement>,
usagePattern: ElementPattern<out UElement>,
provider: UastReferenceProvider,
@@ -129,7 +130,8 @@ fun PsiReferenceRegistrar.registerReferenceProviderByUsage(expressionPattern: El
this.registerUastReferenceProvider(usagePattern, provider, priority)
if (Registry.`is`("uast.references.by.usage", true)) {
this.registerUastReferenceProvider(expressionPattern, UastReferenceByUsageAdapter(usagePattern, provider), priority)
val adapter = UastReferenceByUsageAdapter(expressionPattern, usagePattern, provider)
this.registerReferenceProvider(adaptPattern(expressionPattern::accepts, adapter.supportedUElementTypes), adapter, priority)
}
}
@@ -139,4 +141,3 @@ fun PsiReferenceRegistrar.registerReferenceProviderByUsage(usagePattern: Element
priority: Double = PsiReferenceRegistrar.DEFAULT_PRIORITY) {
registerReferenceProviderByUsage(uExpressionInVariable(), usagePattern, provider, priority)
}