diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/form/util/FormUtil.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/form/util/FormUtil.java index bc377d00c..d66b5d02d 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/form/util/FormUtil.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/form/util/FormUtil.java @@ -251,21 +251,40 @@ public static Method resolveFormGetterCallMethod(MethodReference methodReference */ @Nullable public static PhpClass getFormTypeClassOnParameter(@NotNull PsiElement psiElement) { - - if (psiElement instanceof StringLiteralExpression) { - return getFormTypeToClass(psiElement.getProject(), ((StringLiteralExpression) psiElement).getContents()); + switch (psiElement) { + case StringLiteralExpression stringLiteralExpression -> { + return getFormTypeToClass(psiElement.getProject(), stringLiteralExpression.getContents()); + } + case ClassConstantReference classConstantReference -> { + return PhpElementsUtil.getClassConstantPhpClass(classConstantReference); + } + case PhpTypedElement phpTypedElement -> { + String typeName = phpTypedElement.getType().toString(); + return getFormTypeToClass(psiElement.getProject(), typeName); + } + default -> { + } } - if (psiElement instanceof ClassConstantReference) { - return PhpElementsUtil.getClassConstantPhpClass((ClassConstantReference) psiElement); - } + return null; + } - if (psiElement instanceof PhpTypedElement) { - String typeName = ((PhpTypedElement) psiElement).getType().toString(); - return getFormTypeToClass(psiElement.getProject(), typeName); + /** + * Resolves a Symfony form type parameter to a normalized FQN string without exposing the resolved {@link PhpClass}. + * + *

Supported inputs match {@link #getFormTypeClassOnParameter(PsiElement)}: string literals, class constants, + * and typed expressions such as {@code new FooType()}.

+ * + * @return normalized class FQN with a leading backslash, or {@code null} when the parameter cannot be resolved + */ + @Nullable + public static String getFormTypeFqnOnParameter(@NotNull PsiElement psiElement) { + PhpClass phpClass = getFormTypeClassOnParameter(psiElement); + if (phpClass == null) { + return null; } - return null; + return phpClass.getFQN(); } @NotNull diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigLineMarkerProvider.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigLineMarkerProvider.java index e83f17c6b..1bd3ad835 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigLineMarkerProvider.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigLineMarkerProvider.java @@ -333,8 +333,11 @@ private LineMarkerInfo attachFormType(@NotNull PsiElement psiElement) { for (TwigTypeContainer twigTypeContainer : twigTypeContainers) { Object dataHolder = twigTypeContainer.getDataHolder(); - if (dataHolder instanceof FormDataHolder && PhpElementsUtil.isInstanceOf(((FormDataHolder) dataHolder).getFormType(), "\\Symfony\\Component\\Form\\FormTypeInterface")) { - phpClasses.add(((FormDataHolder) dataHolder).getFormType()); + if (dataHolder instanceof FormDataHolder formDataHolder) { + PhpClass phpClass = PhpElementsUtil.getClassInterface(psiElement.getProject(), formDataHolder.ownerFormTypeFqn()); + if (phpClass != null && PhpElementsUtil.isInstanceOf(phpClass, "\\Symfony\\Component\\Form\\FormTypeInterface")) { + phpClasses.add(phpClass); + } } } 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 dfbb07235..fbebbf601 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateCompletionContributor.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateCompletionContributor.java @@ -57,6 +57,7 @@ import org.apache.commons.lang3.StringUtils; import org.intellij.lang.annotations.RegExp; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.*; import java.util.function.Function; @@ -755,11 +756,8 @@ protected void addCompletions(@NotNull CompletionParameters parameters, @NotNull // form Object dataHolder = twigTypeContainer.getDataHolder(); - if (dataHolder instanceof FormDataHolder) { - lookupElement = lookupElement.withIcon(Symfony2Icons.FORM_TYPE); - - lookupElement = lookupElement.withTypeText(((FormDataHolder) dataHolder).getPhpClass().getName()); - lookupElement = lookupElement.withTailText("(" + ((FormDataHolder) dataHolder).getFormType().getName() + ")", true); + if (dataHolder instanceof FormDataHolder formDataHolder) { + lookupElement = decorateFormFieldLookupElement(lookupElement, formDataHolder); } resultSet.addElement(lookupElement); @@ -976,7 +974,7 @@ public boolean accepts(@NotNull String s, ProcessingContext processingContext) { String typeText = null; if (twigTypeContainers.getDataHolder() instanceof FormDataHolder formDataHolder) { - typeText = formDataHolder.getPhpClass().getName(); + typeText = getFormTypeShortName(formDataHolder.fieldTypeFqn()); } for (String s : new String[]{"form_row", "form_widget", "form_label", "form_errors", "form_help"}) { @@ -992,6 +990,28 @@ public boolean accepts(@NotNull String s, ProcessingContext processingContext) { } } + @Nullable + private static String getFormTypeShortName(@Nullable String fqn) { + if (fqn == null || fqn.isBlank()) { + return null; + } + + int index = fqn.lastIndexOf('\\'); + return index >= 0 ? fqn.substring(index + 1) : fqn; + } + + @NotNull + public static LookupElementBuilder decorateFormFieldLookupElement(@NotNull LookupElementBuilder lookupElement, @NotNull FormDataHolder formDataHolder) { + lookupElement = lookupElement.withIcon(Symfony2Icons.FORM_TYPE); + + String fieldTypeShortName = getFormTypeShortName(formDataHolder.fieldTypeFqn()); + if (fieldTypeShortName != null) { + lookupElement = lookupElement.withTypeText(fieldTypeShortName); + } + + return lookupElement.withTailText("(" + getFormTypeShortName(formDataHolder.ownerFormTypeFqn()) + ")", true); + } + private class IncompleteFormPrintBlockCompletionProvider extends CompletionProvider { @Override protected void addCompletions(@NotNull CompletionParameters completionParameters, @NotNull ProcessingContext processingContext, @NotNull CompletionResultSet resultSet) { @@ -1028,9 +1048,9 @@ public boolean accepts(@NotNull String s, ProcessingContext processingContext) { } String typeText = null; - Collection formTypeFromFormFactory = FormFieldResolver.getFormTypeFromFormFactory(element); - if (!formTypeFromFormFactory.isEmpty()) { - typeText = StringUtils.stripStart(formTypeFromFormFactory.iterator().next().getFQN(), "\\"); + Set formTypeFqnsFromFormFactory = FormFieldResolver.getFormTypeFqnsFromFormFactory(element); + if (!formTypeFqnsFromFormFactory.isEmpty()) { + typeText = StringUtils.stripStart(formTypeFqnsFromFormFactory.iterator().next(), "\\"); } for (String s : new String[]{"form_start", "form_rest", "form_end", "form_errors"}) { @@ -1082,9 +1102,9 @@ public boolean accepts(@NotNull String s, ProcessingContext processingContext) { } String typeText = null; - Collection formTypeFromFormFactory = FormFieldResolver.getFormTypeFromFormFactory(element); - if (!formTypeFromFormFactory.isEmpty()) { - typeText = StringUtils.stripStart(formTypeFromFormFactory.iterator().next().getFQN(), "\\"); + Set formTypeFqnsFromFormFactory = FormFieldResolver.getFormTypeFqnsFromFormFactory(element); + if (!formTypeFqnsFromFormFactory.isEmpty()) { + typeText = StringUtils.stripStart(formTypeFqnsFromFormFactory.iterator().next(), "\\"); } if (orderedList == null) { 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 3a9d40a2e..b2418cb7d 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateGoToDeclarationHandler.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateGoToDeclarationHandler.java @@ -539,9 +539,12 @@ public static Collection getTypeGoto(@NotNull PsiElement psiElement) // @TODO: provide extension if (text.equals(twigTypeContainer.getStringElement())) { Object dataHolder = twigTypeContainer.getDataHolder(); - if (dataHolder instanceof FormDataHolder) { + if (dataHolder instanceof FormDataHolder formDataHolder) { // @TODO: resolve the to field itself - targetPsiElements.add(((FormDataHolder) dataHolder).getFormType()); + PhpClass phpClass = PhpElementsUtil.getClassInterface(psiElement.getProject(), formDataHolder.ownerFormTypeFqn()); + if (phpClass != null) { + targetPsiElements.add(phpClass); + } } } } diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/resolver/FormFieldResolver.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/resolver/FormFieldResolver.java index 9b18c1f5a..59e15c15a 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/resolver/FormFieldResolver.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/resolver/FormFieldResolver.java @@ -125,6 +125,23 @@ public static Collection getFormTypeFromFormFactory(@NotNull PsiElemen return phpClasses; } + /** + * Resolves form type FQNs for a form reference such as {@code $form->createView()}. + * + *

This is the primitive counterpart of {@link #getFormTypeFromFormFactory(PsiElement)}. It may still use PSI + * internally, but it returns only normalized FQN strings with a leading backslash.

+ */ + @NotNull + public static Set getFormTypeFqnsFromFormFactory(@NotNull PsiElement formReference) { + Set formTypeFqns = new LinkedHashSet<>(); + + for (PhpClass phpClass : getFormTypeFromFormFactory(formReference)) { + formTypeFqns.add(phpClass.getFQN()); + } + + return formTypeFqns; + } + @Nullable private static PhpClass resolveCall(@NotNull MethodReference methodReference) { int index = -1; @@ -157,49 +174,55 @@ private static PhpClass resolveCall(@NotNull MethodReference methodReference) { } @NotNull - private static List getTwigTypeContainer(@NotNull Method method, @NotNull PhpClass formTypClass) { - List twigTypeContainers = new ArrayList<>(); + private static List getTwigFormFields(@NotNull Method method, @NotNull PhpClass formTypeClass) { + List twigFormFields = new ArrayList<>(); for(MethodReference methodReference: FormUtil.getFormBuilderTypes(method)) { String fieldName = PsiElementUtils.getMethodParameterAt(methodReference, 0); + if (fieldName == null) { + continue; + } + PsiElement psiElement = PsiElementUtils.getMethodParameterPsiElementAt(methodReference, 1); - TwigTypeContainer twigTypeContainer = new TwigTypeContainer(fieldName); + String fieldTypeFqn = null; // find form field type if(psiElement != null) { - PhpClass fieldType = FormUtil.getFormTypeClassOnParameter(psiElement); - if(fieldType != null) { - twigTypeContainer.withDataHolder(new FormDataHolder(fieldType, formTypClass)); - } + fieldTypeFqn = FormUtil.getFormTypeFqnOnParameter(psiElement); } - twigTypeContainers.add(twigTypeContainer); + twigFormFields.add(new TwigFormField(fieldName, fieldTypeFqn, formTypeClass.getFQN())); } - return twigTypeContainers; + return twigFormFields; } /** * Search and resolve: "$form->createView()" to its PhpClass which is a form type */ public static void visitFormReferencesFields(@NotNull PsiElement formReference, @NotNull Consumer consumer) { - visitFormReferencesFields(formReference.getProject(), getFormTypeFromFormFactory(formReference), consumer); + visitFormFields(formReference.getProject(), getFormTypeFqnsFromFormFactory(formReference), field -> consumer.accept(toTwigTypeContainer(field))); } /** * Visit all form fields in given PhpClass which are already a form type */ public static void visitFormReferencesFields(@NotNull PhpClass phpClass, @NotNull Consumer consumer) { - visitFormReferencesFields(phpClass.getProject(), Collections.singleton(phpClass), consumer); + visitFormFields(phpClass.getProject(), Collections.singleton(phpClass.getFQN()), field -> consumer.accept(toTwigTypeContainer(field))); } - private static void visitFormReferencesFields(@NotNull Project project, @NotNull Collection phpClasses, @NotNull Consumer consumer) { + public static void visitFormFields(@NotNull Project project, @NotNull Collection formTypeFqns, @NotNull Consumer consumer) { FormUtil.FormTypeCollector collector = null; Collection methods = new HashSet<>(); - for (PhpClass phpClass : phpClasses) { + for (String formTypeFqn : formTypeFqns) { + PhpClass phpClass = PhpElementsUtil.getClassInterface(project, formTypeFqn); + if (phpClass == null) { + continue; + } + Method method = phpClass.findMethodByName("buildForm"); if (method != null) { methods.add(method); @@ -225,14 +248,19 @@ private static void visitFormReferencesFields(@NotNull Project project, @NotNull } } - private static void consumeFieldType(@NotNull PhpClass phpClass, @NotNull Consumer consumer) { + @NotNull + private static TwigTypeContainer toTwigTypeContainer(@NotNull TwigFormField field) { + return new TwigTypeContainer(field.name()).withDataHolder(new FormDataHolder(field.fieldTypeFqn(), field.ownerFormTypeFqn())); + } + + private static void consumeFieldType(@NotNull PhpClass phpClass, @NotNull Consumer consumer) { Method method = phpClass.findMethodByName("buildForm"); if (method == null) { return; } - for (TwigTypeContainer twigTypeContainer : getTwigTypeContainer(method, phpClass)) { - consumer.accept(twigTypeContainer); + for (TwigFormField twigFormField : getTwigFormFields(method, phpClass)) { + consumer.accept(twigFormField); } } } diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/resolver/TwigFormField.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/resolver/TwigFormField.java new file mode 100644 index 000000000..0fff71e18 --- /dev/null +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/resolver/TwigFormField.java @@ -0,0 +1,20 @@ +package fr.adrienbrault.idea.symfony2plugin.templating.variable.resolver; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Primitive metadata for a Symfony form field found in a form type. + * + * @param name field name passed to the form builder + * @param fieldTypeFqn explicit field form type FQN, or {@code null} when the builder call has no type parameter + * @param ownerFormTypeFqn FQN of the form type whose {@code buildForm()} contributed this field + * + * @author Daniel Espendiller + */ +public record TwigFormField( + @NotNull String name, + @Nullable String fieldTypeFqn, + @NotNull String ownerFormTypeFqn +) { +} diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/resolver/holder/FormDataHolder.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/resolver/holder/FormDataHolder.java index 6b9027969..4f8039ebe 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/resolver/holder/FormDataHolder.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/resolver/holder/FormDataHolder.java @@ -1,30 +1,27 @@ package fr.adrienbrault.idea.symfony2plugin.templating.variable.resolver.holder; -import com.jetbrains.php.lang.psi.elements.PhpClass; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; /** + * UI metadata for a Symfony form field without holding PSI objects. + * + * @param fieldTypeFqn explicit normalized field form type FQN, or {@code null} when the builder call has no type parameter + * @param ownerFormTypeFqn normalized FQN of the form type whose {@code buildForm()} contributed this field + * * @author Daniel Espendiller */ -public class FormDataHolder { - @NotNull - private final PhpClass phpClass; +public record FormDataHolder( + @Nullable String fieldTypeFqn, + @NotNull String ownerFormTypeFqn +) { + public FormDataHolder { + if (fieldTypeFqn != null && !fieldTypeFqn.startsWith("\\")) { + throw new IllegalArgumentException("fieldTypeFqn must be normalized with a leading backslash"); + } - @NotNull - private final PhpClass formType; - - public FormDataHolder(@NotNull PhpClass phpClass, @NotNull PhpClass formType) { - this.phpClass = phpClass; - this.formType = formType; - } - - @NotNull - public PhpClass getPhpClass() { - return phpClass; - } - - @NotNull - public PhpClass getFormType() { - return formType; + if (!ownerFormTypeFqn.startsWith("\\")) { + throw new IllegalArgumentException("ownerFormTypeFqn must be normalized with a leading backslash"); + } } } diff --git a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/SymfonyLightCodeInsightFixtureTestCase.java b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/SymfonyLightCodeInsightFixtureTestCase.java index 8cebcb610..c60a6b31d 100644 --- a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/SymfonyLightCodeInsightFixtureTestCase.java +++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/SymfonyLightCodeInsightFixtureTestCase.java @@ -267,7 +267,7 @@ private void assertNavigationIsEmpty() { } } - private void assertNavigationMatch(ElementPattern pattern) { + public void assertNavigationMatch(ElementPattern pattern) { PsiElement psiElement = myFixture.getFile().findElementAt(myFixture.getCaretOffset()); diff --git a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/form/util/FormUtilTest.java b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/form/util/FormUtilTest.java index 52f86bdf4..fd00652ec 100644 --- a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/form/util/FormUtilTest.java +++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/form/util/FormUtilTest.java @@ -49,6 +49,24 @@ public void testGetFormTypeClassOnParameter() { ).getFQN()); } + public void testGetFormTypeFqnOnParameter() { + assertEquals("\\Form\\FormType\\Foo", FormUtil.getFormTypeFqnOnParameter( + PhpPsiElementFactory.createPhpPsiFromText(getProject(), PhpTypedElementImpl.class, " formTypeClasses = FormUtil.getFormTypeClasses(getProject()); diff --git a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/TwigLineMarkerProviderTest.java b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/TwigLineMarkerProviderTest.java index 6ab3d37a6..93b55f6f0 100644 --- a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/TwigLineMarkerProviderTest.java +++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/TwigLineMarkerProviderTest.java @@ -13,6 +13,7 @@ import com.intellij.util.ProcessingContext; import com.jetbrains.php.lang.PhpFileType; import com.jetbrains.php.lang.psi.elements.Method; +import com.jetbrains.php.lang.psi.elements.PhpClass; import com.jetbrains.twig.TwigLanguage; import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase; import org.jetbrains.annotations.NotNull; @@ -168,6 +169,53 @@ public void testNoLineMarkerForStandaloneTemplate() { assertFalse("Should not have extends/include navigation marker", hasNavigationMarker); } + public void testFormStartLineMarkerNavigatesToFormType() { + myFixture.addFileToProject( + "src/Controller/ProductController.php", + "add('title');\n" + + " }\n" + + " }\n" + + "}\n" + + "namespace App\\Controller {\n" + + " class ProductController extends \\Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController {\n" + + " public function index() {\n" + + " $form = $this->createForm(\\App\\Form\\ProductType::class);\n" + + " return $this->render('form/line_marker.html.twig', ['form' => $form->createView()]);\n" + + " }\n" + + " }\n" + + "}\n" + ); + + PsiFile templateFile = myFixture.addFileToProject( + "templates/form/line_marker.html.twig", + "{{ form_start(form) }}" + ); + + assertLineMarker( + templateFile, + new LineMarker.TargetAcceptsPattern( + "Navigate to Form", + PlatformPatterns.psiElement(PhpClass.class).withName("ProductType") + ) + ); + } + /** * Test embed line marker. */ diff --git a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/TwigTemplateCompletionContributorTest.java b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/TwigTemplateCompletionContributorTest.java index 8c873565d..93abeeb63 100644 --- a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/TwigTemplateCompletionContributorTest.java +++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/TwigTemplateCompletionContributorTest.java @@ -1,7 +1,12 @@ package fr.adrienbrault.idea.symfony2plugin.tests.templating; +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.codeInsight.lookup.LookupElementBuilder; +import com.intellij.codeInsight.lookup.LookupElementPresentation; import com.intellij.patterns.PlatformPatterns; import com.jetbrains.twig.TwigFileType; +import fr.adrienbrault.idea.symfony2plugin.templating.TwigTemplateCompletionContributor; +import fr.adrienbrault.idea.symfony2plugin.templating.variable.resolver.holder.FormDataHolder; import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase; /** @@ -60,6 +65,20 @@ public void testThatTypesTagProvidesIncompleteForStatementCompletion() { ); } + public void testFormFieldCompletionUsesPrimitiveFormDataHolderPresentation() { + LookupElement lookupElement = TwigTemplateCompletionContributor.decorateFormFieldLookupElement( + LookupElementBuilder.create("title"), + new FormDataHolder("\\Symfony\\Component\\Form\\Extension\\Core\\Type\\TextType", "\\App\\Form\\ProductType") + ); + + LookupElementPresentation presentation = new LookupElementPresentation(); + lookupElement.renderElement(presentation); + + assertEquals("title", lookupElement.getLookupString()); + assertEquals("TextType", presentation.getTypeText()); + assertEquals("(ProductType)", presentation.getTailText()); + } + public void testThatConstantProvidesCompletionForClassConstant() { assertCompletionContains(TwigFileType.INSTANCE, "{{ constant('') }}", "CONST_FOO"); assertCompletionContains(TwigFileType.INSTANCE, "{{ constant('') }}", "FooConst::CAR", "FooEnum::FOOBAR"); diff --git a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/TwigTemplateGoToDeclarationHandlerTest.java b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/TwigTemplateGoToDeclarationHandlerTest.java index eb0661348..ee6bb23bd 100644 --- a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/TwigTemplateGoToDeclarationHandlerTest.java +++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/TwigTemplateGoToDeclarationHandlerTest.java @@ -2,6 +2,7 @@ import com.intellij.patterns.PlatformPatterns; import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; import com.jetbrains.php.lang.psi.elements.Field; import com.jetbrains.php.lang.psi.elements.Function; import com.jetbrains.php.lang.psi.elements.Method; @@ -115,6 +116,49 @@ public void testTypeGotoIgnoresCoreTwigReferenceResolutionForPropertyAccess() { assertContainsElements(methodNames, "getFoobar"); } + public void testFormFieldGotoUsesPrimitiveFormDataHolderOwnerFormTypeFqn() { + myFixture.copyFileToProject("ide-twig.json"); + + myFixture.addFileToProject( + "src/Controller/FormGotoController.php", + "add('title', \\Symfony\\Component\\Form\\Extension\\Core\\Type\\TextType::class);\n" + + " }\n" + + " }\n" + + "}\n" + + "namespace App\\Controller {\n" + + " class FormGotoController extends \\Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController {\n" + + " public function index() {\n" + + " $form = $this->createForm(\\App\\Form\\ProductType::class);\n" + + " return $this->render('form/goto.html.twig', ['form' => $form->createView()]);\n" + + " }\n" + + " }\n" + + "}\n" + ); + + PsiFile templateFile = myFixture.addFileToProject("templates/form/goto.html.twig", "{{ form.title }}"); + myFixture.configureFromExistingVirtualFile(templateFile.getVirtualFile()); + myFixture.getEditor().getCaretModel().moveToOffset("{{ form.ti".length()); + + assertNavigationMatch(PlatformPatterns.psiElement(PhpClass.class).withName("ProductType")); + } + public void testSeeTagGoto() { assertNavigationMatch(TwigFileType.INSTANCE, "{# @see \\DateTime #}", PlatformPatterns.psiElement(PhpClass.class)); assertNavigationMatch(TwigFileType.INSTANCE, "{# @see DateTime #}", PlatformPatterns.psiElement(PhpClass.class)); 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 new file mode 100644 index 000000000..a0cb3edc4 --- /dev/null +++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/variable/resolver/FormFieldResolverTest.java @@ -0,0 +1,91 @@ +package fr.adrienbrault.idea.symfony2plugin.tests.templating.variable.resolver; + +import com.intellij.psi.PsiElement; +import com.intellij.psi.util.PsiTreeUtil; +import com.jetbrains.php.lang.PhpFileType; +import com.jetbrains.php.lang.psi.elements.MethodReference; +import fr.adrienbrault.idea.symfony2plugin.templating.variable.resolver.FormFieldResolver; +import fr.adrienbrault.idea.symfony2plugin.templating.variable.resolver.TwigFormField; +import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author Daniel Espendiller + */ +public class FormFieldResolverTest extends SymfonyLightCodeInsightFixtureTestCase { + public void testGetFormTypeFqnsFromFormFactory() { + myFixture.configureByText(PhpFileType.INSTANCE, "createForm(\\App\\Form\\ProductType::class);\n" + + " return ['form' => $form->createView()];\n" + + " }\n" + + " }\n" + + "}\n" + ); + + Set formTypeFqns = FormFieldResolver.getFormTypeFqnsFromFormFactory(findMethodReference("createView")); + + assertContainsElements(formTypeFqns, "\\App\\Form\\ProductType"); + } + + public void testVisitFormFieldsProvidesPrimitiveFieldMetadata() { + myFixture.configureByText(PhpFileType.INSTANCE, "add('title', \\Symfony\\Component\\Form\\Extension\\Core\\Type\\TextType::class);\n" + + " $builder->add('plain');\n" + + " }\n" + + " }\n" + + "}\n" + ); + + Collection fields = new ArrayList<>(); + FormFieldResolver.visitFormFields(getProject(), Collections.singleton("\\App\\Form\\ProductType"), fields::add); + + Set fieldNames = fields.stream().map(TwigFormField::name).collect(Collectors.toSet()); + assertContainsElements(fieldNames, "title", "plain"); + + TwigFormField title = fields.stream() + .filter(field -> "title".equals(field.name())) + .findFirst() + .orElseThrow(); + + assertEquals("\\Symfony\\Component\\Form\\Extension\\Core\\Type\\TextType", title.fieldTypeFqn()); + assertEquals("\\App\\Form\\ProductType", title.ownerFormTypeFqn()); + + TwigFormField plain = fields.stream() + .filter(field -> "plain".equals(field.name())) + .findFirst() + .orElseThrow(); + + assertNull(plain.fieldTypeFqn()); + assertEquals("\\App\\Form\\ProductType", plain.ownerFormTypeFqn()); + } + + @NotNull + private MethodReference findMethodReference(@NotNull String name) { + for (PsiElement psiElement : PsiTreeUtil.collectElementsOfType(myFixture.getFile(), MethodReference.class)) { + MethodReference methodReference = (MethodReference) psiElement; + if (name.equals(methodReference.getName())) { + return methodReference; + } + } + + fail("Method reference not found: " + name); + throw new IllegalStateException(name); + } +}