Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
*
* <p>Supported inputs match {@link #getFormTypeClassOnParameter(PsiElement)}: string literals, class constants,
* and typed expressions such as {@code new FooType()}.</p>
*
* @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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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"}) {
Expand All @@ -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<CompletionParameters> {
@Override
protected void addCompletions(@NotNull CompletionParameters completionParameters, @NotNull ProcessingContext processingContext, @NotNull CompletionResultSet resultSet) {
Expand Down Expand Up @@ -1028,9 +1048,9 @@ public boolean accepts(@NotNull String s, ProcessingContext processingContext) {
}

String typeText = null;
Collection<PhpClass> formTypeFromFormFactory = FormFieldResolver.getFormTypeFromFormFactory(element);
if (!formTypeFromFormFactory.isEmpty()) {
typeText = StringUtils.stripStart(formTypeFromFormFactory.iterator().next().getFQN(), "\\");
Set<String> 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"}) {
Expand Down Expand Up @@ -1082,9 +1102,9 @@ public boolean accepts(@NotNull String s, ProcessingContext processingContext) {
}

String typeText = null;
Collection<PhpClass> formTypeFromFormFactory = FormFieldResolver.getFormTypeFromFormFactory(element);
if (!formTypeFromFormFactory.isEmpty()) {
typeText = StringUtils.stripStart(formTypeFromFormFactory.iterator().next().getFQN(), "\\");
Set<String> formTypeFqnsFromFormFactory = FormFieldResolver.getFormTypeFqnsFromFormFactory(element);
if (!formTypeFqnsFromFormFactory.isEmpty()) {
typeText = StringUtils.stripStart(formTypeFqnsFromFormFactory.iterator().next(), "\\");
}

if (orderedList == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -539,9 +539,12 @@ public static Collection<PsiElement> 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);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,23 @@ public static Collection<PhpClass> getFormTypeFromFormFactory(@NotNull PsiElemen
return phpClasses;
}

/**
* Resolves form type FQNs for a form reference such as {@code $form->createView()}.
*
* <p>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.</p>
*/
@NotNull
public static Set<String> getFormTypeFqnsFromFormFactory(@NotNull PsiElement formReference) {
Set<String> 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;
Expand Down Expand Up @@ -157,49 +174,55 @@ private static PhpClass resolveCall(@NotNull MethodReference methodReference) {
}

@NotNull
private static List<TwigTypeContainer> getTwigTypeContainer(@NotNull Method method, @NotNull PhpClass formTypClass) {
List<TwigTypeContainer> twigTypeContainers = new ArrayList<>();
private static List<TwigFormField> getTwigFormFields(@NotNull Method method, @NotNull PhpClass formTypeClass) {
List<TwigFormField> 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<TwigTypeContainer> 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<TwigTypeContainer> 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<PhpClass> phpClasses, @NotNull Consumer<TwigTypeContainer> consumer) {
public static void visitFormFields(@NotNull Project project, @NotNull Collection<String> formTypeFqns, @NotNull Consumer<TwigFormField> consumer) {
FormUtil.FormTypeCollector collector = null;

Collection<Method> 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);
Expand All @@ -225,14 +248,19 @@ private static void visitFormReferencesFields(@NotNull Project project, @NotNull
}
}

private static void consumeFieldType(@NotNull PhpClass phpClass, @NotNull Consumer<TwigTypeContainer> 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<TwigFormField> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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 <daniel@espendiller.net>
*/
public record TwigFormField(
@NotNull String name,
@Nullable String fieldTypeFqn,
@NotNull String ownerFormTypeFqn
) {
}
Original file line number Diff line number Diff line change
@@ -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 <daniel@espendiller.net>
*/
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");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ private void assertNavigationIsEmpty() {
}
}

private void assertNavigationMatch(ElementPattern<?> pattern) {
public void assertNavigationMatch(ElementPattern<?> pattern) {

PsiElement psiElement = myFixture.getFile().findElementAt(myFixture.getCaretOffset());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,24 @@ public void testGetFormTypeClassOnParameter() {
).getFQN());
}

public void testGetFormTypeFqnOnParameter() {
assertEquals("\\Form\\FormType\\Foo", FormUtil.getFormTypeFqnOnParameter(
PhpPsiElementFactory.createPhpPsiFromText(getProject(), PhpTypedElementImpl.class, "<?php new \\Form\\FormType\\Foo();")
));

assertEquals("\\Form\\FormType\\Foo", FormUtil.getFormTypeFqnOnParameter(
PhpPsiElementFactory.createPhpPsiFromText(getProject(), StringLiteralExpressionImpl.class, "<?php '\\Form\\FormType\\Foo'")
));

assertEquals("\\Form\\FormType\\Foo", FormUtil.getFormTypeFqnOnParameter(
PhpPsiElementFactory.createPhpPsiFromText(getProject(), StringLiteralExpressionImpl.class, "<?php 'Form\\FormType\\Foo'")
));

assertEquals("\\Form\\FormType\\Foo", FormUtil.getFormTypeFqnOnParameter(
PhpPsiElementFactory.createPhpPsiFromText(getProject(), ClassConstantReferenceImpl.class, "<?php Form\\FormType\\Foo::class")
));
}

@SuppressWarnings({"ConstantConditions"})
public void testGetFormTypeClasses() {
Map<String, FormTypeClass> formTypeClasses = FormUtil.getFormTypeClasses(getProject());
Expand Down
Loading
Loading