From e3f0cc3c24706ee9d7a4825c6ce5ae66e7933426 Mon Sep 17 00:00:00 2001 From: Daniel Espendiller Date: Mon, 27 Apr 2026 12:20:00 +0200 Subject: [PATCH] Decouple form resolvers from PhpClass PSI elements --- .../templating/util/TwigTypeResolveUtil.java | 16 ++-- .../variable/resolver/FormFieldResolver.java | 17 ++-- .../variable/resolver/FormVarsResolver.java | 12 +-- .../variable/resolver/TwigTypeResolver.java | 4 +- .../resolver/FormFieldResolverTest.java | 24 +++++- .../resolver/FormVarsResolverTest.java | 81 +++++++++++++++++++ 6 files changed, 127 insertions(+), 27 deletions(-) create mode 100644 src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/variable/resolver/FormVarsResolverTest.java 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 140f1e454..cdd965967 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 @@ -143,22 +143,24 @@ public static Collection resolveTwigMethodName(@NotNull PsiEl String rootType = types.iterator().next(); Collection rootVariables = getRootVariableByName(psiElement, rootType); - if(types.size() == 1) { - Collection twigTypeContainers = TwigTypeContainer.fromCollection(psiElement.getProject(), rootVariables); + if (types.size() == 1) { + Project project = psiElement.getProject(); + Collection twigTypeContainers = TwigTypeContainer.fromCollection(project, rootVariables); for(TwigTypeResolver twigTypeResolver: TWIG_TYPE_RESOLVERS) { - twigTypeResolver.resolve(twigTypeContainers, twigTypeContainers, rootType, new ArrayList<>(), rootVariables); + twigTypeResolver.resolve(project, twigTypeContainers, twigTypeContainers, rootType, new ArrayList<>(), rootVariables); } return twigTypeContainers; } - Collection type = TwigTypeContainer.fromCollection(psiElement.getProject(), rootVariables); + Project project = psiElement.getProject(); + Collection type = TwigTypeContainer.fromCollection(project, rootVariables); Collection> previousElements = new ArrayList<>(); previousElements.add(new ArrayList<>(type)); String[] typeNames = types.toArray(new String[0]); for (int i = 1; i <= typeNames.length - 1; i++ ) { - type = resolveTwigMethodName(type, typeNames[i], previousElements); + type = resolveTwigMethodName(project, type, typeNames[i], previousElements); previousElements.add(new ArrayList<>(type)); // we can stop on empty list @@ -538,7 +540,7 @@ private static Collection getRootVariableByName(@NotNull PsiElement return phpNamedElements; } - private static Collection resolveTwigMethodName(Collection previousElement, String typeName, Collection> twigTypeContainer) { + private static Collection resolveTwigMethodName(@NotNull Project project, Collection previousElement, String typeName, Collection> twigTypeContainer) { ArrayList phpNamedElements = new ArrayList<>(); @@ -565,7 +567,7 @@ private static Collection resolveTwigMethodName(Collection targets, Collection previousElement, String typeName, Collection> previousElements, @Nullable Collection psiVariables) { + public void resolve(@NotNull Project project, Collection targets, Collection previousElement, String typeName, Collection> previousElements, @Nullable Collection psiVariables) { if (targets.isEmpty() || previousElements == null || !previousElements.isEmpty()) { return; } TwigTypeContainer twigTypeContainer = targets.iterator().next(); - if ( - twigTypeContainer.getPhpNamedElement() instanceof PhpClass phpClass && - isFormView(phpClass) && - twigTypeContainer.getFormViewDataHolder() instanceof FormViewDataHolder formViewDataHolder && - !formViewDataHolder.formTypeFqns().isEmpty() - ) { - visitFormFields(phpClass.getProject(), formViewDataHolder.formTypeFqns(), field -> targets.add(toTwigTypeContainer(field))); + FormViewDataHolder formViewDataHolder = twigTypeContainer.getFormViewDataHolder(); + if (formViewDataHolder == null || formViewDataHolder.formTypeFqns().isEmpty()) { + return; } - } - public static boolean isFormView(@NotNull PhpClass phpClass) { - return PhpElementsUtil.isInstanceOf(phpClass, "\\Symfony\\Component\\Form\\FormView") || - PhpElementsUtil.isInstanceOf(phpClass, "\\Symfony\\Component\\Form\\FormInterface"); // form view is create converting by Symfony on template render + visitFormFields(project, formViewDataHolder.formTypeFqns(), field -> targets.add(toTwigTypeContainer(field))); } public static boolean isFormView(@NotNull PhpType phpType) { diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/resolver/FormVarsResolver.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/resolver/FormVarsResolver.java index 6b8b0c41c..b21b61e32 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/resolver/FormVarsResolver.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/resolver/FormVarsResolver.java @@ -1,10 +1,11 @@ package fr.adrienbrault.idea.symfony2plugin.templating.variable.resolver; import com.intellij.openapi.project.Project; -import com.jetbrains.php.lang.psi.elements.PhpClass; import fr.adrienbrault.idea.symfony2plugin.form.util.FormOptionsUtil; 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.holder.FormViewDataHolder; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Collection; @@ -14,7 +15,7 @@ * @author Daniel Espendiller */ public class FormVarsResolver implements TwigTypeResolver { - public void resolve(Collection targets, Collection previousElement, String typeName, Collection> previousElements, @Nullable Collection psiVariables) { + public void resolve(@NotNull Project project, Collection targets, Collection previousElement, String typeName, Collection> previousElements, @Nullable Collection psiVariables) { if(!"vars".equals(typeName) || previousElements.isEmpty()) { return; } @@ -25,10 +26,9 @@ public void resolve(Collection targets, Collection */ public interface TwigTypeResolver { - void resolve(Collection targets, @Nullable Collection previousElement, String typeName, Collection> previousElements, @Nullable Collection psiVariables); + void resolve(@NotNull Project project, Collection targets, @Nullable Collection previousElement, String typeName, Collection> previousElements, @Nullable Collection psiVariables); } diff --git a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/variable/resolver/FormFieldResolverTest.java b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/variable/resolver/FormFieldResolverTest.java index a86276e2f..29d0d5fc7 100644 --- a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/variable/resolver/FormFieldResolverTest.java +++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/variable/resolver/FormFieldResolverTest.java @@ -104,7 +104,7 @@ public void testResolveUsesPrimitiveFormTypeFqnsWithoutPsiElement() { assertNotNull(rootFormDataHolder); assertContainsElements(rootFormDataHolder.formTypeFqns(), "\\App\\Form\\ProductType"); - new FormFieldResolver().resolve(targets, targets, "form", new ArrayList<>(), null); + new FormFieldResolver().resolve(getProject(), targets, targets, "form", new ArrayList<>(), null); TwigTypeContainer title = targets.stream() .filter(twigTypeContainer -> "title".equals(twigTypeContainer.getStringElement())) @@ -117,6 +117,28 @@ public void testResolveUsesPrimitiveFormTypeFqnsWithoutPsiElement() { assertEquals("\\App\\Form\\ProductType", formFieldDataHolder.ownerFormTypeFqn()); } + public void testResolveDoesNotUseFormViewClassWithoutFormViewDataHolder() { + myFixture.configureByText(PhpFileType.INSTANCE, "add('title');\n" + + " }\n" + + " }\n" + + "}\n" + ); + + Collection targets = TwigTypeContainer.fromCollection( + getProject(), + Collections.singleton(new PsiVariable("\\Symfony\\Component\\Form\\FormView")) + ); + + new FormFieldResolver().resolve(getProject(), targets, targets, "form", new ArrayList<>(), null); + + assertFalse(targets.stream().anyMatch(twigTypeContainer -> "title".equals(twigTypeContainer.getStringElement()))); + } + @NotNull private MethodReference findMethodReference(@NotNull String name) { for (PsiElement psiElement : PsiTreeUtil.collectElementsOfType(myFixture.getFile(), MethodReference.class)) { 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 new file mode 100644 index 000000000..74810efcd --- /dev/null +++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/variable/resolver/FormVarsResolverTest.java @@ -0,0 +1,81 @@ +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.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; + +/** + * @author Daniel Espendiller + */ +public class FormVarsResolverTest extends SymfonyLightCodeInsightFixtureTestCase { + public void testResolveAttachesFormVarsForFormViewDataHolder() { + configureFormViewVarsFixture(); + + ArrayList targets = new ArrayList<>(); + new FormVarsResolver().resolve( + getProject(), + targets, + null, + "vars", + Collections.singletonList(Collections.singletonList(createRootFormViewContainer(true))), + null + ); + + assertContainsElements( + targets.stream().map(TwigTypeContainer::getStringElement).toList(), + "compound", + "form_attr" + ); + } + + public void testResolveDoesNotUseFormViewClassWithoutFormViewDataHolder() { + configureFormViewVarsFixture(); + + ArrayList targets = new ArrayList<>(); + new FormVarsResolver().resolve( + getProject(), + targets, + null, + "vars", + Collections.singletonList(Collections.singletonList(createRootFormViewContainer(false))), + null + ); + + assertEmpty(targets); + } + + private void configureFormViewVarsFixture() { + myFixture.configureByText(PhpFileType.INSTANCE, "vars['form_attr'] = true;\n" + + " $view->vars = array_replace($view->vars, ['compound' => true]);\n" + + " }\n" + + " }\n" + + "}\n" + ); + } + + private TwigTypeContainer createRootFormViewContainer(boolean withFormViewDataHolder) { + PhpClass phpClass = PhpElementsUtil.getClass(getProject(), "\\Symfony\\Component\\Form\\FormView"); + assertNotNull(phpClass); + + return new TwigTypeContainer( + phpClass, + withFormViewDataHolder ? new FormViewDataHolder(Collections.singleton("\\App\\Form\\ProductType")) : null + ); + } +}