From d47409d5cdcc91255e213e6a990726d4fbb452a5 Mon Sep 17 00:00:00 2001 From: Daniel Espendiller Date: Mon, 27 Apr 2026 13:51:10 +0200 Subject: [PATCH] Refactor Twig type containers to store cacheable type strings instead of PSI elements --- .../TwigTemplateCompletionContributor.java | 7 +-- .../TwigTemplateGoToDeclarationHandler.java | 8 +-- .../TwigVariableDeprecatedInspection.java | 27 +++++---- .../TwigVariablePathInspection.java | 36 +++++------- .../templating/util/TwigTypeResolveUtil.java | 55 +++++++++++++------ .../variable/TwigTypeContainer.java | 44 +++++++-------- .../TwigMethodReferencesSearchExecutor.kt | 38 ++++++------- .../resolver/FormFieldResolverTest.java | 7 +-- .../resolver/FormVarsResolverTest.java | 15 ++--- 9 files changed, 116 insertions(+), 121 deletions(-) diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateCompletionContributor.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateCompletionContributor.java index 57f0b980e..135c6aab1 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateCompletionContributor.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateCompletionContributor.java @@ -735,15 +735,14 @@ protected void addCompletions(@NotNull CompletionParameters parameters, @NotNull // find core function for that for(TwigTypeContainer twigTypeContainer: TwigTypeResolveUtil.resolveTwigMethodName(psiElement, possibleTypes)) { - if(twigTypeContainer.getPhpNamedElement() instanceof PhpClass) { - - for(Method method: ((PhpClass) twigTypeContainer.getPhpNamedElement()).getMethods()) { + for (PhpClass phpClass : TwigTypeResolveUtil.resolveTwigTypeClasses(psiElement.getProject(), twigTypeContainer)) { + for(Method method: phpClass.getMethods()) { if(TwigTypeResolveUtil.isTwigAccessibleMethod(method)) { resultSet.addElement(new PhpTwigMethodLookupElement(method)); } } - for(Field field: ((PhpClass) twigTypeContainer.getPhpNamedElement()).getFields()) { + for(Field field: phpClass.getFields()) { if(field.getModifier().isPublic()) { resultSet.addElement(new PhpTwigMethodLookupElement(field)); } diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateGoToDeclarationHandler.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateGoToDeclarationHandler.java index 6c94df099..c42852a4f 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateGoToDeclarationHandler.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateGoToDeclarationHandler.java @@ -520,9 +520,7 @@ public static Collection getTypeGoto(@NotNull PsiElement psiElement) if(beforeLeaf.isEmpty()) { Collection twigTypeContainers = TwigTypeResolveUtil.resolveTwigMethodName(psiElement, TwigTypeResolveUtil.formatPsiTypeNameWithCurrent(psiElement)); for(TwigTypeContainer twigTypeContainer: twigTypeContainers) { - if(twigTypeContainer.getPhpNamedElement() != null) { - targetPsiElements.add(twigTypeContainer.getPhpNamedElement()); - } + targetPsiElements.addAll(TwigTypeResolveUtil.resolveTwigTypeClasses(psiElement.getProject(), twigTypeContainer)); } } else { @@ -531,9 +529,7 @@ public static Collection getTypeGoto(@NotNull PsiElement psiElement) if(StringUtils.isNotBlank(text)) { // provide method / field goto for(TwigTypeContainer twigTypeContainer: types) { - if(twigTypeContainer.getPhpNamedElement() != null) { - targetPsiElements.addAll(TwigTypeResolveUtil.getTwigPhpNameTargets(twigTypeContainer.getPhpNamedElement(), text)); - } + targetPsiElements.addAll(TwigTypeResolveUtil.getTwigPhpNameTargets(psiElement.getProject(), twigTypeContainer, text)); // form // @TODO: provide extension diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/inspection/TwigVariableDeprecatedInspection.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/inspection/TwigVariableDeprecatedInspection.java index dd9eff746..b9ec9d145 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/inspection/TwigVariableDeprecatedInspection.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/inspection/TwigVariableDeprecatedInspection.java @@ -15,6 +15,7 @@ import fr.adrienbrault.idea.symfony2plugin.templating.variable.TwigTypeContainer; import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil; import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.NonNull; import java.util.Collection; @@ -43,35 +44,33 @@ private static class MyPsiElementVisitor extends PsiElementVisitor { } @Override - public void visitElement(PsiElement element) { - if(getTypeCompletionPattern().accepts(element)) { + public void visitElement(@NonNull PsiElement element) { + if (getTypeCompletionPattern().accepts(element)) { visit(element); } + super.visitElement(element); } private void visit(@NotNull PsiElement element) { Collection beforeLeaf = TwigTypeResolveUtil.formatPsiTypeName(element); - if(beforeLeaf.isEmpty()) { + if (beforeLeaf.isEmpty()) { return; } Collection types = TwigTypeResolveUtil.resolveTwigMethodName(element, beforeLeaf); - if(types.isEmpty()) { + if (types.isEmpty()) { return; } - for(TwigTypeContainer twigTypeContainer: types) { - PhpNamedElement phpClass = twigTypeContainer.getPhpNamedElement(); - if(!(phpClass instanceof PhpClass)) { - continue; - } - - String text = element.getText(); + for (TwigTypeContainer twigTypeContainer: types) { + for (PhpClass phpClass : TwigTypeResolveUtil.resolveTwigTypeClasses(element.getProject(), twigTypeContainer)) { + String text = element.getText(); - for (PhpNamedElement namedElement : TwigTypeResolveUtil.getTwigPhpNameTargets(phpClass, text)) { - if(namedElement instanceof Method method && PhpElementsUtil.isClassOrFunctionDeprecated(method)) { - this.holder.registerProblem(element, String.format("Method '%s::%s' is deprecated", phpClass.getName(), namedElement.getName()), ProblemHighlightType.LIKE_DEPRECATED); + for (PhpNamedElement namedElement : TwigTypeResolveUtil.getTwigPhpNameTargets(phpClass, text)) { + if (namedElement instanceof Method method && PhpElementsUtil.isClassOrFunctionDeprecated(method)) { + this.holder.registerProblem(element, String.format("Method '%s::%s' is deprecated", phpClass.getName(), namedElement.getName()), ProblemHighlightType.LIKE_DEPRECATED); + } } } } diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/inspection/TwigVariablePathInspection.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/inspection/TwigVariablePathInspection.java index c7a750108..6bc3bedc8 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/inspection/TwigVariablePathInspection.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/inspection/TwigVariablePathInspection.java @@ -7,13 +7,12 @@ import com.intellij.psi.PsiElement; import com.intellij.psi.PsiElementVisitor; import com.jetbrains.php.lang.psi.elements.PhpClass; -import com.jetbrains.php.lang.psi.elements.PhpNamedElement; import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent; import fr.adrienbrault.idea.symfony2plugin.templating.TwigPattern; import fr.adrienbrault.idea.symfony2plugin.templating.util.TwigTypeResolveUtil; import fr.adrienbrault.idea.symfony2plugin.templating.variable.TwigTypeContainer; -import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil; import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.NonNull; import java.util.Collection; @@ -42,7 +41,7 @@ private static class MyPsiElementVisitor extends PsiElementVisitor { } @Override - public void visitElement(PsiElement element) { + public void visitElement(@NonNull PsiElement element) { if(getTypeCompletionPattern().accepts(element)) { visit(element); } @@ -56,36 +55,31 @@ private void visit(@NotNull PsiElement element) { } Collection types = TwigTypeResolveUtil.resolveTwigMethodName(element, beforeLeaf); - if(types.isEmpty()) { + if (types.isEmpty()) { return; } - for(TwigTypeContainer twigTypeContainer: types) { - PhpNamedElement phpNamedElement = twigTypeContainer.getPhpNamedElement(); - if(phpNamedElement == null) { - continue; - } - - if(isWeakPhpClass(phpNamedElement)) { + for (TwigTypeContainer twigTypeContainer: types) { + String text = element.getText(); + Collection phpClasses = TwigTypeResolveUtil.resolveTwigTypeClasses(element.getProject(), twigTypeContainer); + if (phpClasses.isEmpty()) { return; } - String text = element.getText(); - if(!TwigTypeResolveUtil.getTwigPhpNameTargets(phpNamedElement, text).isEmpty()) { - return; + for (PhpClass phpClass : phpClasses) { + if(TwigTypeResolveUtil.isWeakCollectionLikeClass(phpClass)) { + return; + } + + if (!TwigTypeResolveUtil.getTwigPhpNameTargets(phpClass, text).isEmpty()) { + return; + } } } this.holder.registerProblem(element, "Field or method not found", ProblemHighlightType.GENERIC_ERROR_OR_WARNING); } - private boolean isWeakPhpClass(PhpNamedElement phpNamedElement) { - return phpNamedElement instanceof PhpClass && ( - PhpElementsUtil.isInstanceOf((PhpClass) phpNamedElement, "ArrayAccess") || - PhpElementsUtil.isInstanceOf((PhpClass) phpNamedElement, "Iterator") - ); - } - private ElementPattern getTypeCompletionPattern() { return typeCompletionPattern != null ? typeCompletionPattern : (typeCompletionPattern = TwigPattern.getTypeCompletionPattern()); } diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/util/TwigTypeResolveUtil.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/util/TwigTypeResolveUtil.java index cdd965967..9086cdbcb 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/util/TwigTypeResolveUtil.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/util/TwigTypeResolveUtil.java @@ -145,7 +145,7 @@ public static Collection resolveTwigMethodName(@NotNull PsiEl Collection rootVariables = getRootVariableByName(psiElement, rootType); if (types.size() == 1) { Project project = psiElement.getProject(); - Collection twigTypeContainers = TwigTypeContainer.fromCollection(project, rootVariables); + Collection twigTypeContainers = TwigTypeContainer.fromCollection(rootVariables); for(TwigTypeResolver twigTypeResolver: TWIG_TYPE_RESOLVERS) { twigTypeResolver.resolve(project, twigTypeContainers, twigTypeContainers, rootType, new ArrayList<>(), rootVariables); } @@ -154,7 +154,7 @@ public static Collection resolveTwigMethodName(@NotNull PsiEl } Project project = psiElement.getProject(); - Collection type = TwigTypeContainer.fromCollection(project, rootVariables); + Collection type = TwigTypeContainer.fromCollection(rootVariables); Collection> previousElements = new ArrayList<>(); previousElements.add(new ArrayList<>(type)); @@ -546,23 +546,18 @@ private static Collection resolveTwigMethodName(@NotNull Proj for(TwigTypeContainer phpNamedElement: previousElement) { - if(phpNamedElement.getPhpNamedElement() != null) { - for(PhpNamedElement target : getTwigPhpNameTargets(phpNamedElement.getPhpNamedElement(), typeName)) { - PhpType phpType = target.getType(); + for(PhpNamedElement target : getTwigPhpNameTargets(project, phpNamedElement, typeName)) { + PhpType phpType = target.getType(); - // @TODO: provide extension - // custom resolving for Twig here: "app.user" => can also be a general solution just support the "getToken()->getUser()" - if (target instanceof Method && StaticVariableCollector.isUserMethod((Method) target)) { - phpNamedElements.addAll(getApplicationUserImplementations(target.getProject())); - } + // @TODO: provide extension + // custom resolving for Twig here: "app.user" => can also be a general solution just support the "getToken()->getUser()" + if (target instanceof Method && StaticVariableCollector.isUserMethod((Method) target)) { + phpNamedElements.addAll(getApplicationUserImplementations(project)); + } - // @TODO: use full resolving for object, that would allow using TypeProviders and core PhpStorm feature - for (String typeString: phpType.filterPrimitives().getTypes()) { - PhpClass phpClass = PhpElementsUtil.getClassInterface(phpNamedElement.getPhpNamedElement().getProject(), typeString); - if(phpClass != null) { - phpNamedElements.add(new TwigTypeContainer(phpClass)); - } - } + Set types = phpType.filterPrimitives().getTypes(); + if (!types.isEmpty()) { + phpNamedElements.add(new TwigTypeContainer(types)); } } @@ -584,7 +579,7 @@ private static Collection getApplicationUserImplementations(@ .getAllSubclasses(project, "\\Symfony\\Component\\Security\\Core\\User\\UserInterface") .stream() .filter(phpClass -> !phpClass.isInterface()) // filter out implementation like AdvancedUserInterface - .map(TwigTypeContainer::new) + .map(phpClass -> new TwigTypeContainer(Collections.singleton(phpClass.getFQN()))) .collect(Collectors.toList()); } @@ -635,6 +630,26 @@ public static Collection getTwigPhpNameTargets(PhpNam return targets; } + @NotNull + public static Collection resolveTwigTypeClasses(@NotNull Project project, @NotNull TwigTypeContainer twigTypeContainer) { + if (twigTypeContainer.getTypes().isEmpty()) { + return Collections.emptyList(); + } + + return PhpElementsUtil.getClassFromPhpTypeSet(project, twigTypeContainer.getTypes()); + } + + @NotNull + public static Collection getTwigPhpNameTargets(@NotNull Project project, @NotNull TwigTypeContainer twigTypeContainer, @NotNull String variableName) { + Collection targets = new ArrayList<>(); + + for (PhpClass phpClass : resolveTwigTypeClasses(project, twigTypeContainer)) { + targets.addAll(getTwigPhpNameTargets(phpClass, variableName)); + } + + return targets; + } + public static String getTypeDisplayName(Project project, Set types) { @@ -685,6 +700,10 @@ public static boolean isTwigAccessibleMethod(@NotNull Method method) { return !name.startsWith("set") && !name.startsWith("__"); } + public static boolean isWeakCollectionLikeClass(@NotNull PhpClass phpClass) { + return PhpElementsUtil.isInstanceOf(phpClass, "ArrayAccess") || PhpElementsUtil.isInstanceOf(phpClass, "Iterator"); + } + public static boolean isPropertyShortcutMethod(String methodName) { for (String shortcut: PROPERTY_SHORTCUTS) { if (methodName.startsWith(shortcut) && methodName.length() > shortcut.length()) { diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/TwigTypeContainer.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/TwigTypeContainer.java index cb935f2b0..a6b0b76d4 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/TwigTypeContainer.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/TwigTypeContainer.java @@ -1,34 +1,34 @@ package fr.adrienbrault.idea.symfony2plugin.templating.variable; -import com.intellij.openapi.project.Project; -import com.jetbrains.php.lang.psi.elements.PhpClass; -import com.jetbrains.php.lang.psi.elements.PhpNamedElement; import fr.adrienbrault.idea.symfony2plugin.templating.variable.dict.PsiVariable; import fr.adrienbrault.idea.symfony2plugin.templating.variable.resolver.holder.FormFieldDataHolder; import fr.adrienbrault.idea.symfony2plugin.templating.variable.resolver.holder.FormViewDataHolder; -import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * @author Daniel Espendiller */ public class TwigTypeContainer { - private final PhpNamedElement phpNamedElement; + private final Set types = new HashSet<>(); private final String stringElement; private final FormFieldDataHolder formFieldDataHolder; private final FormViewDataHolder formViewDataHolder; - public TwigTypeContainer(PhpNamedElement phpNamedElement) { - this(phpNamedElement, null); + public TwigTypeContainer(@NotNull Collection types) { + this(types, null); } - public TwigTypeContainer(PhpNamedElement phpNamedElement, @Nullable FormViewDataHolder formViewDataHolder) { - this.phpNamedElement = phpNamedElement; + private TwigTypeContainer(@NotNull Collection types, @Nullable FormViewDataHolder formViewDataHolder) { + this.types.addAll(types); this.stringElement = null; this.formFieldDataHolder = null; this.formViewDataHolder = formViewDataHolder; @@ -39,15 +39,14 @@ public TwigTypeContainer(String stringElement) { } public TwigTypeContainer(String stringElement, @Nullable FormFieldDataHolder formFieldDataHolder) { - this.phpNamedElement = null; this.stringElement = stringElement; this.formFieldDataHolder = formFieldDataHolder; this.formViewDataHolder = null; } - @Nullable - public PhpNamedElement getPhpNamedElement() { - return phpNamedElement; + @NotNull + public Set getTypes() { + return Collections.unmodifiableSet(types); } @Nullable @@ -55,19 +54,20 @@ public String getStringElement() { return stringElement; } - public static Collection fromCollection(Project project, Collection psiVariables) { + public static Collection fromCollection(Collection psiVariables) { List twigTypeContainerList = new ArrayList<>(); - for(PsiVariable phpNamedElement :psiVariables) { - Collection phpClass = PhpElementsUtil.getClassFromPhpTypeSet(project, phpNamedElement.getTypes()); - if(!phpClass.isEmpty()) { - FormViewDataHolder formViewDataHolder = phpNamedElement.getFormTypeFqns().isEmpty() - ? null - : new FormViewDataHolder(phpNamedElement.getFormTypeFqns()); - - twigTypeContainerList.add(new TwigTypeContainer(phpClass.iterator().next(), formViewDataHolder)); + for(PsiVariable psiVariable : psiVariables) { + if (psiVariable.getTypes().isEmpty()) { + continue; } + + FormViewDataHolder formViewDataHolder = psiVariable.getFormTypeFqns().isEmpty() + ? null + : new FormViewDataHolder(psiVariable.getFormTypeFqns()); + + twigTypeContainerList.add(new TwigTypeContainer(psiVariable.getTypes(), formViewDataHolder)); } return twigTypeContainerList; diff --git a/src/main/kotlin/fr/adrienbrault/idea/symfony2plugin/templating/usages/TwigMethodReferencesSearchExecutor.kt b/src/main/kotlin/fr/adrienbrault/idea/symfony2plugin/templating/usages/TwigMethodReferencesSearchExecutor.kt index 26e3af8ea..6274bbb9e 100644 --- a/src/main/kotlin/fr/adrienbrault/idea/symfony2plugin/templating/usages/TwigMethodReferencesSearchExecutor.kt +++ b/src/main/kotlin/fr/adrienbrault/idea/symfony2plugin/templating/usages/TwigMethodReferencesSearchExecutor.kt @@ -24,7 +24,6 @@ import com.jetbrains.twig.TwigFile import com.jetbrains.twig.TwigFileType import fr.adrienbrault.idea.symfony2plugin.templating.TwigPattern import fr.adrienbrault.idea.symfony2plugin.templating.util.TwigTypeResolveUtil -import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil /** * Adds Twig usages to Find Usages for PHP symbols that are referenced from Twig. @@ -438,14 +437,15 @@ class TwigMethodReferencesSearchExecutor : QueryExecutor targets = TwigTypeContainer.fromCollection(getProject(), Collections.singleton(rootVariable)); + Collection targets = TwigTypeContainer.fromCollection(Collections.singleton(rootVariable)); assertSize(1, targets); TwigTypeContainer rootContainer = targets.iterator().next(); @@ -129,10 +129,7 @@ public void testResolveDoesNotUseFormViewClassWithoutFormViewDataHolder() { "}\n" ); - Collection targets = TwigTypeContainer.fromCollection( - getProject(), - Collections.singleton(new PsiVariable("\\Symfony\\Component\\Form\\FormView")) - ); + Collection targets = TwigTypeContainer.fromCollection(Collections.singleton(new PsiVariable("\\Symfony\\Component\\Form\\FormView"))); new FormFieldResolver().resolve(getProject(), targets, targets, "form", new ArrayList<>(), null); diff --git a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/variable/resolver/FormVarsResolverTest.java b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/variable/resolver/FormVarsResolverTest.java index 74810efcd..e19c72a33 100644 --- a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/variable/resolver/FormVarsResolverTest.java +++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/variable/resolver/FormVarsResolverTest.java @@ -1,12 +1,10 @@ package fr.adrienbrault.idea.symfony2plugin.tests.templating.variable.resolver; import com.jetbrains.php.lang.PhpFileType; -import com.jetbrains.php.lang.psi.elements.PhpClass; import fr.adrienbrault.idea.symfony2plugin.templating.variable.TwigTypeContainer; +import fr.adrienbrault.idea.symfony2plugin.templating.variable.dict.PsiVariable; import fr.adrienbrault.idea.symfony2plugin.templating.variable.resolver.FormVarsResolver; -import fr.adrienbrault.idea.symfony2plugin.templating.variable.resolver.holder.FormViewDataHolder; import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase; -import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil; import java.util.ArrayList; import java.util.Collections; @@ -70,12 +68,11 @@ private void configureFormViewVarsFixture() { } private TwigTypeContainer createRootFormViewContainer(boolean withFormViewDataHolder) { - PhpClass phpClass = PhpElementsUtil.getClass(getProject(), "\\Symfony\\Component\\Form\\FormView"); - assertNotNull(phpClass); + PsiVariable rootVariable = new PsiVariable("\\Symfony\\Component\\Form\\FormView"); + if (withFormViewDataHolder) { + rootVariable.addFormTypeFqns(Collections.singleton("\\App\\Form\\ProductType")); + } - return new TwigTypeContainer( - phpClass, - withFormViewDataHolder ? new FormViewDataHolder(Collections.singleton("\\App\\Form\\ProductType")) : null - ); + return TwigTypeContainer.fromCollection(Collections.singleton(rootVariable)).iterator().next(); } }