From 0567cd3a622b5d1e509d1aed20e716e07ea9b0ad Mon Sep 17 00:00:00 2001 From: Daniel Espendiller Date: Wed, 17 Dec 2025 18:31:39 +0100 Subject: [PATCH] Replace `getClassInterface` with `hasClassOrInterface` for improved caching and performance of class and interface lookups. --- .../action/NewFileActionUtil.java | 6 ++-- .../action/NewTwigExtensionAction.java | 2 +- .../PhpAttributeCompletionContributor.java | 12 ++++---- .../completion/ServicePropertyInsertUtil.java | 2 +- .../dic/inspection/YamlClassInspection.java | 2 +- .../php/AddRouteAttributeIntention.java | 2 +- .../php/CommandToInvokableIntention.java | 2 +- .../symfony2plugin/util/PhpElementsUtil.java | 29 +++++++++++++++++++ 8 files changed, 43 insertions(+), 14 deletions(-) diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/action/NewFileActionUtil.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/action/NewFileActionUtil.java index 4e444f338..ca16d0f07 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/action/NewFileActionUtil.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/action/NewFileActionUtil.java @@ -66,7 +66,7 @@ public static boolean isInGivenDirectoryScope(@NotNull AnActionEvent event, @Not public static String guessCommandTemplateType(@NotNull Project project, @NotNull String namespace) { // Check if InvokableCommand is available (Symfony 7.3+) - if (PhpElementsUtil.getClassInterface(project, "\\Symfony\\Component\\Console\\Command\\InvokableCommand") != null) { + if (PhpElementsUtil.hasClassOrInterface(project, "\\Symfony\\Component\\Console\\Command\\InvokableCommand")) { String normalizedNamespace = "\\" + org.apache.commons.lang3.StringUtils.strip(namespace, "\\") + "\\"; Collection commandClasses = PhpIndexUtil.getPhpClassInsideNamespace(project, normalizedNamespace); @@ -90,7 +90,7 @@ public static String guessCommandTemplateType(@NotNull Project project, @NotNull } } - if (PhpElementsUtil.getClassInterface(project, "\\Symfony\\Component\\Console\\Attribute\\AsCommand") != null) { + if (PhpElementsUtil.hasClassOrInterface(project, "\\Symfony\\Component\\Console\\Attribute\\AsCommand")) { return "command_attributes"; } @@ -106,7 +106,7 @@ public static String guessCommandTemplateType(@NotNull Project project, @NotNull } public static String guessControllerTemplateType(@NotNull Project project) { - if (PhpElementsUtil.getClassInterface(project, "\\Symfony\\Component\\Routing\\Attribute\\Route") != null) { + if (PhpElementsUtil.hasClassOrInterface(project, "\\Symfony\\Component\\Routing\\Attribute\\Route")) { return "controller_attributes"; } diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/action/NewTwigExtensionAction.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/action/NewTwigExtensionAction.java index c137615c6..5d824ce89 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/action/NewTwigExtensionAction.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/action/NewTwigExtensionAction.java @@ -89,7 +89,7 @@ public void actionPerformed(@NotNull AnActionEvent event) { @NotNull private static String detectTemplate(@NotNull Project project) { // If attributes are available, use the new attribute-based template - if (PhpElementsUtil.getClassInterface(project, "\\Twig\\Attribute\\AsTwigFunction") != null) { + if (PhpElementsUtil.hasClassOrInterface(project, "\\Twig\\Attribute\\AsTwigFunction")) { return "twig_extension_function_attribute"; } diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/completion/PhpAttributeCompletionContributor.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/completion/PhpAttributeCompletionContributor.java index 5cf228e8b..254efffce 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/completion/PhpAttributeCompletionContributor.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/completion/PhpAttributeCompletionContributor.java @@ -95,7 +95,7 @@ private Collection getControllerCompletions(@NotNull Project proj Collection lookupElements = new ArrayList<>(); // Add Route attribute completion - if (PhpElementsUtil.getClassInterface(project, ROUTE_ATTRIBUTE_FQN) != null) { + if (PhpElementsUtil.hasClassOrInterface(project, ROUTE_ATTRIBUTE_FQN)) { LookupElement routeLookupElement = LookupElementBuilder .create("#[Route]") .withIcon(Symfony2Icons.SYMFONY_ATTRIBUTE) @@ -107,7 +107,7 @@ private Collection getControllerCompletions(@NotNull Project proj } // Add IsGranted attribute completion - if (PhpElementsUtil.getClassInterface(project, IS_GRANTED_ATTRIBUTE_FQN) != null) { + if (PhpElementsUtil.hasClassOrInterface(project, IS_GRANTED_ATTRIBUTE_FQN)) { LookupElement isGrantedLookupElement = LookupElementBuilder .create("#[IsGranted]") .withIcon(Symfony2Icons.SYMFONY_ATTRIBUTE) @@ -119,7 +119,7 @@ private Collection getControllerCompletions(@NotNull Project proj } // Add Cache attribute completion - if (PhpElementsUtil.getClassInterface(project, CACHE_ATTRIBUTE_FQN) != null) { + if (PhpElementsUtil.hasClassOrInterface(project, CACHE_ATTRIBUTE_FQN)) { LookupElement cacheLookupElement = LookupElementBuilder .create("#[Cache]") .withIcon(Symfony2Icons.SYMFONY_ATTRIBUTE) @@ -137,7 +137,7 @@ private Collection getTwigExtensionCompletions(@NotNull Project p Collection lookupElements = new ArrayList<>(); // Add AsTwigFilter attribute completion - if (PhpElementsUtil.getClassInterface(project, AS_TWIG_FILTER_ATTRIBUTE_FQN) != null) { + if (PhpElementsUtil.hasClassOrInterface(project, AS_TWIG_FILTER_ATTRIBUTE_FQN)) { LookupElement lookupElement = LookupElementBuilder .create("#[AsTwigFilter]") .withIcon(Symfony2Icons.SYMFONY_ATTRIBUTE) @@ -149,7 +149,7 @@ private Collection getTwigExtensionCompletions(@NotNull Project p } // Add AsTwigFunction attribute completion - if (PhpElementsUtil.getClassInterface(project, AS_TWIG_FUNCTION_ATTRIBUTE_FQN) != null) { + if (PhpElementsUtil.hasClassOrInterface(project, AS_TWIG_FUNCTION_ATTRIBUTE_FQN)) { LookupElement lookupElement = LookupElementBuilder .create("#[AsTwigFunction]") .withIcon(Symfony2Icons.SYMFONY_ATTRIBUTE) @@ -161,7 +161,7 @@ private Collection getTwigExtensionCompletions(@NotNull Project p } // Add AsTwigTest attribute completion - if (PhpElementsUtil.getClassInterface(project, AS_TWIG_TEST_ATTRIBUTE_FQN) != null) { + if (PhpElementsUtil.hasClassOrInterface(project, AS_TWIG_TEST_ATTRIBUTE_FQN)) { LookupElement lookupElement = LookupElementBuilder .create("#[AsTwigTest]") .withIcon(Symfony2Icons.SYMFONY_ATTRIBUTE) diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/completion/ServicePropertyInsertUtil.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/completion/ServicePropertyInsertUtil.java index 86cceb1ee..44b6be110 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/completion/ServicePropertyInsertUtil.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/completion/ServicePropertyInsertUtil.java @@ -62,7 +62,7 @@ public static List getInjectionService(@NotNull Project project, @NotNul for (String property : propertyNameFind) { if (alias.containsKey(property.toLowerCase())) { String key = property.toLowerCase(); - if (!PhpIndex.getInstance(project).getAnyByFQN(alias.get(key)).isEmpty()) { + if (PhpElementsUtil.hasClassOrInterface(project, alias.get(key))) { String fqn = alias.get(key); servicesMatch.put(fqn, new Match(fqn, 4)); } diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/dic/inspection/YamlClassInspection.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/dic/inspection/YamlClassInspection.java index 75d6b9bc0..d03726f2d 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/dic/inspection/YamlClassInspection.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/dic/inspection/YamlClassInspection.java @@ -66,7 +66,7 @@ private void invoke(@NotNull final PsiElement psiElement, @NotNull ProblemsHolde if (YamlHelper.isValidParameterName(className)) { String resolvedParameter = ContainerCollectionResolver.resolveParameter(project, className); - if (resolvedParameter != null && !PhpIndex.getInstance(project).getAnyByFQN(resolvedParameter).isEmpty()) { + if (resolvedParameter != null && PhpElementsUtil.hasClassOrInterface(project, resolvedParameter)) { return; } } diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/intentions/php/AddRouteAttributeIntention.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/intentions/php/AddRouteAttributeIntention.java index 65516d02c..fa59b1cf1 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/intentions/php/AddRouteAttributeIntention.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/intentions/php/AddRouteAttributeIntention.java @@ -110,7 +110,7 @@ public boolean isAvailable(@NotNull Project project, Editor editor, @NotNull Psi return false; } - if (PhpElementsUtil.getClassInterface(project, ROUTE_ATTRIBUTE_CLASS) == null) { + if (!PhpElementsUtil.hasClassOrInterface(project, ROUTE_ATTRIBUTE_CLASS)) { return false; } diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/intentions/php/CommandToInvokableIntention.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/intentions/php/CommandToInvokableIntention.java index 2fa788ed6..9a3b9c348 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/intentions/php/CommandToInvokableIntention.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/intentions/php/CommandToInvokableIntention.java @@ -163,7 +163,7 @@ public boolean isAvailable(@NotNull Project project, Editor editor, @NotNull Psi } // check if feature exists - if (PhpElementsUtil.getClassInterface(project, "Symfony\\Component\\Console\\Command\\InvokableCommand") == null) { + if (!PhpElementsUtil.hasClassOrInterface(project, "\\Symfony\\Component\\Console\\Command\\InvokableCommand")) { return false; } diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/util/PhpElementsUtil.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/util/PhpElementsUtil.java index 94f393c26..a759732c6 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/util/PhpElementsUtil.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/util/PhpElementsUtil.java @@ -4,6 +4,7 @@ import com.intellij.lang.ASTNode; import com.intellij.openapi.editor.Document; import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Key; import com.intellij.openapi.util.Ref; import com.intellij.patterns.ElementPattern; import com.intellij.patterns.PatternCondition; @@ -12,6 +13,10 @@ import com.intellij.psi.*; import com.intellij.psi.codeStyle.CodeStyleManager; import com.intellij.psi.formatter.FormatterUtil; +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.psi.util.PsiTreeUtil; import com.intellij.util.ProcessingContext; import com.intellij.util.Processor; @@ -55,6 +60,7 @@ import org.jetbrains.annotations.Nullable; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; /** @@ -62,6 +68,11 @@ */ public class PhpElementsUtil { + /** + * Cache for class/interface existence checks to avoid repeated PhpIndex queries + */ + private static final Key>> CLASS_EXISTS_CACHE = new Key<>("SYMFONY_PHP_CLASS_EXISTS_CACHE"); + /** * Only parameter on first index or named: "a('caret'), a(test: 'caret')" */ @@ -859,6 +870,24 @@ static public PhpClass getClassInterface(Project project, @NotNull String classN return phpClasses.isEmpty() ? null : phpClasses.iterator().next(); } + static public boolean hasClassOrInterface(@NotNull Project project, @NotNull String classFqnName) { + // Get or create the cached map + Map cache = CachedValuesManager.getManager(project).getCachedValue( + project, + CLASS_EXISTS_CACHE, + () -> CachedValueProvider.Result.create( + new ConcurrentHashMap<>(), + PsiModificationTracker.MODIFICATION_COUNT + ), + false + ); + + // Check the cache first, compute and store if missing + return cache.computeIfAbsent(classFqnName, fqn -> + !PhpIndex.getInstance(project).getAnyByFQN(fqn).isEmpty() + ); + } + /** * @param subjectClass eg DateTime * @param expectedClass eg DateTimeInterface