Skip to content

Commit

Permalink
Merge pull request #1982 from Haehnchen/feature/907-link-data-class-form
Browse files Browse the repository at this point in the history
 #907 linemarker for linking a data_class to related forms
  • Loading branch information
Haehnchen committed Jul 2, 2022
2 parents bb9a86c + 8479264 commit a91c959
Show file tree
Hide file tree
Showing 7 changed files with 274 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,22 @@
import com.intellij.codeInsight.daemon.LineMarkerProvider;
import com.intellij.codeInsight.navigation.NavigationGutterIconBuilder;
import com.intellij.psi.PsiElement;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.util.indexing.FileBasedIndex;
import com.jetbrains.php.lang.psi.elements.PhpClass;
import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons;
import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent;
import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.FormDataClassStubIndex;
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
* @author Daniel Espendiller <daniel@espendiller.net>
Expand All @@ -33,13 +40,48 @@ public void collectSlowLineMarkers(@NotNull List<? extends PsiElement> psiElemen
for (PsiElement psiElement : psiElements) {
if (PhpElementsUtil.getClassNamePattern().accepts(psiElement)) {
attachFormDataClass(lineMarkerInfos, psiElement);
attachPhpClassToFormDataClass(lineMarkerInfos, psiElement);
}
}
}

private void attachPhpClassToFormDataClass(@NotNull Collection<? super LineMarkerInfo<?>> lineMarkerInfos, @NotNull PsiElement leaf) {
PsiElement phpClassContext = leaf.getContext();
if(!(phpClassContext instanceof PhpClass)) {
return;
}

String fqn = ((PhpClass) phpClassContext).getFQN();

Set<String> classes = FileBasedIndex.getInstance().getValues(FormDataClassStubIndex.KEY, "\\" + StringUtils.stripStart(fqn, "\\"), GlobalSearchScope.allScope(leaf.getProject()))
.stream()
.flatMap(Set::stream)
.collect(Collectors.toSet());

Collection<PhpClass> phpClasses = new HashSet<>();
for (String clazz: classes) {
phpClasses.addAll(PhpElementsUtil.getClassesInterface(leaf.getProject(), clazz));
}

if (!phpClasses.isEmpty()) {
NavigationGutterIconBuilder<PsiElement> builder = NavigationGutterIconBuilder.create(Symfony2Icons.FORM_TYPE_LINE_MARKER)
.setTargets(phpClasses)
.setTooltipText("Navigate to form");

lineMarkerInfos.add(builder.createLineMarkerInfo(leaf));
}
}

private void attachFormDataClass(@NotNull Collection<? super LineMarkerInfo<?>> lineMarkerInfos, @NotNull PsiElement leaf) {
PsiElement phpClassContext = leaf.getContext();
if(!(phpClassContext instanceof PhpClass) || !PhpElementsUtil.isInstanceOf((PhpClass) phpClassContext, "\\Symfony\\Component\\Form\\FormTypeInterface")) {
if (phpClassContext == null) {
return;
}

boolean isFormTypeInstance = PhpElementsUtil.isInstanceOf((PhpClass) phpClassContext, "\\Symfony\\Component\\Form\\FormTypeInterface")
|| PhpElementsUtil.isInstanceOf((PhpClass) phpClassContext, "\\Symfony\\Component\\Form\\FormExtensionInterface");

if (!isFormTypeInstance) {
return;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package fr.adrienbrault.idea.symfony2plugin.stubs.indexes;

import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiRecursiveElementVisitor;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.indexing.*;
import com.intellij.util.io.DataExternalizer;
import com.intellij.util.io.EnumeratorStringDescriptor;
import com.intellij.util.io.KeyDescriptor;
import com.jetbrains.php.lang.PhpFileType;
import com.jetbrains.php.lang.psi.PhpFile;
import com.jetbrains.php.lang.psi.elements.*;
import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.externalizer.StringSetDataExternalizer;
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
import gnu.trove.THashMap;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
* public function configureOptions(OptionsResolver $resolver)
* {
* $resolver->setDefaults(['data_class' => XXX]);
* $resolver->setDefault('data_class', XXX);
* }
*
* @author Daniel Espendiller <daniel@espendiller.net>
*/
public class FormDataClassStubIndex extends FileBasedIndexExtension<String, Set<String>> {
public static final ID<String, Set<String>> KEY = ID.create("fr.adrienbrault.idea.symfony2plugin.form_data_class");
private final KeyDescriptor<String> myKeyDescriptor = new EnumeratorStringDescriptor();

@NotNull
@Override
public ID<String, Set<String>> getName() {
return KEY;
}

@NotNull
@Override
public DataIndexer<String, Set<String>, FileContent> getIndexer() {
return inputData -> {
Map<String, Set<String>> map = new THashMap<>();

PsiFile psiFile = inputData.getPsiFile();
if(!(psiFile instanceof PhpFile)) {
return map;
}

psiFile.accept(new PsiRecursiveElementVisitor() {
@Override
public void visitElement(@NotNull PsiElement element) {
if (element instanceof MethodReference) {
String phpClassFqn = null;

String name = ((MethodReference) element).getName();
if ("setDefault".equals(name) && ((MethodReference) element).getType().getTypes().stream().anyMatch(s -> s.toLowerCase().contains("optionsresolver"))) {
// $resolver->setDefault('data_class', XXX);

ParameterList parameterList = ((MethodReference) element).getParameterList();
if (parameterList != null) {
PsiElement parameter = parameterList.getParameter(0);
if (parameter instanceof StringLiteralExpression) {
String contents = ((StringLiteralExpression) parameter).getContents();
if ("data_class".equals(contents)) {
PsiElement parameter1 = parameterList.getParameter(1);
if (parameter1 != null) {
phpClassFqn = getString(parameter1);
}
}
}
}
} else if ("setDefaults".equals(name) && ((MethodReference) element).getType().getTypes().stream().anyMatch(s -> s.toLowerCase().contains("optionsresolver"))) {
// $resolver->setDefaults(['data_class' => XXX]);

ParameterList parameterList = ((MethodReference) element).getParameterList();
if (parameterList != null) {
PsiElement parameter = parameterList.getParameter(0);
if (parameter instanceof ArrayCreationExpression) {
PhpPsiElement dataClassPsiElement = PhpElementsUtil.getArrayValue((ArrayCreationExpression) parameter, "data_class");
if (dataClassPsiElement != null) {
phpClassFqn = getString(dataClassPsiElement);
}
}
}
}

if (phpClassFqn != null) {
Method methodScope = PsiTreeUtil.getParentOfType(element, Method.class);
if (methodScope != null) {
PhpClass parentOfType = methodScope.getContainingClass();
if (parentOfType != null) {
map.putIfAbsent(phpClassFqn, new HashSet<>());
map.get(phpClassFqn).add(parentOfType.getFQN());
}
}
}
}

super.visitElement(element);
}

@Nullable
private String getString(@NotNull PsiElement parameter) {
if (parameter instanceof ClassConstantReference) {
String classConstantPhpFqn = PhpElementsUtil.getClassConstantPhpFqn((ClassConstantReference) parameter);
if (StringUtils.isNotBlank(classConstantPhpFqn)) {
return "\\" + StringUtils.stripStart(classConstantPhpFqn, "\\");
}
} else if (parameter instanceof StringLiteralExpression) {
String contents1 = ((StringLiteralExpression) parameter).getContents();
if (StringUtils.isNotBlank(contents1)) {
return "\\" + StringUtils.stripStart(contents1, "\\");
}
}

return null;
}
});

return map;
};

}

@NotNull
@Override
public KeyDescriptor<String> getKeyDescriptor() {
return this.myKeyDescriptor;
}

@NotNull
@Override
public DataExternalizer<Set<String>> getValueExternalizer() {
return new StringSetDataExternalizer();
}

@NotNull
@Override
public FileBasedIndex.InputFilter getInputFilter() {
return file -> file.getFileType() == PhpFileType.INSTANCE;
}

@Override
public boolean dependsOnFileContent() {
return true;
}

@Override
public int getVersion() {
return 1;
}
}
1 change: 1 addition & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@
<fileBasedIndex implementation="fr.adrienbrault.idea.symfony2plugin.stubs.indexes.ContainerIdUsagesStubIndex"/>
<fileBasedIndex implementation="fr.adrienbrault.idea.symfony2plugin.stubs.indexes.TwigBlockIndexExtension"/>
<fileBasedIndex implementation="fr.adrienbrault.idea.symfony2plugin.stubs.indexes.TwigControllerStubIndex"/>
<fileBasedIndex implementation="fr.adrienbrault.idea.symfony2plugin.stubs.indexes.FormDataClassStubIndex"/>

<codeInsight.lineMarkerProvider language="PHP" implementationClass="fr.adrienbrault.idea.symfony2plugin.config.ServiceLineMarkerProvider"/>
<codeInsight.lineMarkerProvider language="PHP" implementationClass="fr.adrienbrault.idea.symfony2plugin.dic.ControllerMethodLineMarkerProvider"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ protected String getTestDataPath() {
return "src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/form/fixtures";
}

public void testThatRouteLineMarkerForControllerIsGiven() {
public void testThatFormCanNavigateToDataClass() {
assertLineMarker(
myFixture.configureByText(
PhpFileType.INSTANCE,
Expand All @@ -38,6 +38,17 @@ public void testThatRouteLineMarkerForControllerIsGiven() {
),
new LineMarker.ToolTipEqualsAssert("Navigate to data class")
);
}

public void testThatDataClassCanNavigateToForm() {
assertLineMarker(
myFixture.configureByText(
PhpFileType.INSTANCE,
"<?php\n" +
"namespace App;\n" +
"class FoobarDataClass {}\n"
),
new LineMarker.ToolTipEqualsAssert("Navigate to form")
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,16 @@ public function getExtendedType() {
}

}

class FormTypeDataClass
{
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => \App\FoobarDataClass::class,
));
}
}
}

namespace Form\FormType {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package fr.adrienbrault.idea.symfony2plugin.tests.stubs.indexes;

import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.FormDataClassStubIndex;
import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase;

/**
* @author Daniel Espendiller <daniel@espendiller.net>
*
* @see fr.adrienbrault.idea.symfony2plugin.stubs.indexes.FormDataClassStubIndex
*/
public class FormDataClassStubIndexTest extends SymfonyLightCodeInsightFixtureTestCase {

public void setUp() throws Exception {
super.setUp();

myFixture.configureFromExistingVirtualFile(myFixture.copyFileToProject("FormDataClassStubIndex.php"));
}

public String getTestDataPath() {
return "src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/stubs/indexes/fixtures";
}

public void testTemplateIncludeIndexer() {
assertIndexContains(FormDataClassStubIndex.KEY, "\\App\\FooDataClass1", "\\App\\FooDataClass2", "\\App\\FooDataClass3");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace App {

use Symfony\Component\OptionsResolver\OptionsResolver;

class FooDataClass1 {}
class FooDataClass2 {}
class FooDataClass3 {}

class AutoFarmType
{
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => FooDataClass1::class,
]);

$resolver->setDefault('data_class', FooDataClass2::class);

$resolver->setDefault('data_class', 'App\FooDataClass3');
}
}
}

0 comments on commit a91c959

Please sign in to comment.