Skip to content

Commit 6290651

Browse files
committed
#907 linemarker for linking a data_class to related forms
1 parent bb9a86c commit 6290651

File tree

7 files changed

+264
-2
lines changed

7 files changed

+264
-2
lines changed

src/main/java/fr/adrienbrault/idea/symfony2plugin/form/PhpLineMarkerProvider.java

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,22 @@
44
import com.intellij.codeInsight.daemon.LineMarkerProvider;
55
import com.intellij.codeInsight.navigation.NavigationGutterIconBuilder;
66
import com.intellij.psi.PsiElement;
7+
import com.intellij.psi.search.GlobalSearchScope;
8+
import com.intellij.util.indexing.FileBasedIndex;
79
import com.jetbrains.php.lang.psi.elements.PhpClass;
810
import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons;
911
import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent;
12+
import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.FormDataClassStubIndex;
1013
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
14+
import org.apache.commons.lang.StringUtils;
1115
import org.jetbrains.annotations.NotNull;
1216
import org.jetbrains.annotations.Nullable;
1317

1418
import java.util.Collection;
19+
import java.util.HashSet;
1520
import java.util.List;
21+
import java.util.Set;
22+
import java.util.stream.Collectors;
1623

1724
/**
1825
* @author Daniel Espendiller <daniel@espendiller.net>
@@ -33,13 +40,44 @@ public void collectSlowLineMarkers(@NotNull List<? extends PsiElement> psiElemen
3340
for (PsiElement psiElement : psiElements) {
3441
if (PhpElementsUtil.getClassNamePattern().accepts(psiElement)) {
3542
attachFormDataClass(lineMarkerInfos, psiElement);
43+
attachPhpClassToFormDataClass(lineMarkerInfos, psiElement);
3644
}
3745
}
3846
}
3947

48+
private void attachPhpClassToFormDataClass(@NotNull Collection<? super LineMarkerInfo<?>> lineMarkerInfos, @NotNull PsiElement leaf) {
49+
PsiElement phpClassContext = leaf.getContext();
50+
if(!(phpClassContext instanceof PhpClass)) {
51+
return;
52+
}
53+
54+
String fqn = ((PhpClass) phpClassContext).getFQN();
55+
56+
Set<String> classes = FileBasedIndex.getInstance().getValues(FormDataClassStubIndex.KEY, "\\" + StringUtils.stripStart(fqn, "\\"), GlobalSearchScope.allScope(leaf.getProject()))
57+
.stream()
58+
.flatMap(Set::stream)
59+
.collect(Collectors.toSet());
60+
61+
Collection<PhpClass> phpClasses = new HashSet<>();
62+
for (String clazz: classes) {
63+
phpClasses.addAll(PhpElementsUtil.getClassesInterface(leaf.getProject(), clazz));
64+
}
65+
66+
if (!phpClasses.isEmpty()) {
67+
NavigationGutterIconBuilder<PsiElement> builder = NavigationGutterIconBuilder.create(Symfony2Icons.FORM_TYPE_LINE_MARKER)
68+
.setTargets(phpClasses)
69+
.setTooltipText("Navigate to form");
70+
71+
lineMarkerInfos.add(builder.createLineMarkerInfo(leaf));
72+
}
73+
}
74+
4075
private void attachFormDataClass(@NotNull Collection<? super LineMarkerInfo<?>> lineMarkerInfos, @NotNull PsiElement leaf) {
4176
PsiElement phpClassContext = leaf.getContext();
42-
if(!(phpClassContext instanceof PhpClass) || !PhpElementsUtil.isInstanceOf((PhpClass) phpClassContext, "\\Symfony\\Component\\Form\\FormTypeInterface")) {
77+
boolean b = !(phpClassContext instanceof PhpClass)
78+
|| !(PhpElementsUtil.isInstanceOf((PhpClass) phpClassContext, "\\Symfony\\Component\\Form\\FormTypeInterface") || PhpElementsUtil.isInstanceOf((PhpClass) phpClassContext, "\\Symfony\\Component\\Form\\FormExtensionInterface"));
79+
80+
if (b) {
4381
return;
4482
}
4583

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package fr.adrienbrault.idea.symfony2plugin.stubs.indexes;
2+
3+
import com.intellij.psi.PsiElement;
4+
import com.intellij.psi.PsiFile;
5+
import com.intellij.psi.PsiRecursiveElementVisitor;
6+
import com.intellij.psi.util.PsiTreeUtil;
7+
import com.intellij.util.indexing.*;
8+
import com.intellij.util.io.DataExternalizer;
9+
import com.intellij.util.io.EnumeratorStringDescriptor;
10+
import com.intellij.util.io.KeyDescriptor;
11+
import com.jetbrains.php.lang.PhpFileType;
12+
import com.jetbrains.php.lang.psi.PhpFile;
13+
import com.jetbrains.php.lang.psi.elements.*;
14+
import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.externalizer.StringSetDataExternalizer;
15+
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
16+
import gnu.trove.THashMap;
17+
import org.apache.commons.lang.StringUtils;
18+
import org.jetbrains.annotations.NotNull;
19+
import org.jetbrains.annotations.Nullable;
20+
21+
import java.util.HashSet;
22+
import java.util.Map;
23+
import java.util.Set;
24+
25+
/**
26+
* @author Daniel Espendiller <daniel@espendiller.net>
27+
*/
28+
public class FormDataClassStubIndex extends FileBasedIndexExtension<String, Set<String>> {
29+
public static final ID<String, Set<String>> KEY = ID.create("fr.adrienbrault.idea.symfony2plugin.form_data_class");
30+
private final KeyDescriptor<String> myKeyDescriptor = new EnumeratorStringDescriptor();
31+
32+
@NotNull
33+
@Override
34+
public ID<String, Set<String>> getName() {
35+
return KEY;
36+
}
37+
38+
@NotNull
39+
@Override
40+
public DataIndexer<String, Set<String>, FileContent> getIndexer() {
41+
return inputData -> {
42+
Map<String, Set<String>> map = new THashMap<>();
43+
44+
PsiFile psiFile = inputData.getPsiFile();
45+
if(!(psiFile instanceof PhpFile)) {
46+
return map;
47+
}
48+
49+
psiFile.accept(new PsiRecursiveElementVisitor() {
50+
@Override
51+
public void visitElement(@NotNull PsiElement element) {
52+
if (element instanceof MethodReference) {
53+
String phpClassFqn = null;
54+
55+
String name = ((MethodReference) element).getName();
56+
if ("setDefault".equals(name) && ((MethodReference) element).getType().getTypes().stream().anyMatch(s -> s.toLowerCase().contains("optionsresolver"))) {
57+
// $resolver->setDefault('data_class', XXX);
58+
59+
ParameterList parameterList = ((MethodReference) element).getParameterList();
60+
if (parameterList != null) {
61+
PsiElement parameter = parameterList.getParameter(0);
62+
if (parameter instanceof StringLiteralExpression) {
63+
String contents = ((StringLiteralExpression) parameter).getContents();
64+
if ("data_class".equals(contents)) {
65+
PsiElement parameter1 = parameterList.getParameter(1);
66+
if (parameter1 != null) {
67+
phpClassFqn = getString(parameter1);
68+
}
69+
}
70+
}
71+
}
72+
} else if ("setDefaults".equals(name) && ((MethodReference) element).getType().getTypes().stream().anyMatch(s -> s.toLowerCase().contains("optionsresolver"))) {
73+
// $resolver->setDefaults(['data_class' => XXX]);
74+
75+
ParameterList parameterList = ((MethodReference) element).getParameterList();
76+
if (parameterList != null) {
77+
PsiElement parameter = parameterList.getParameter(0);
78+
if (parameter instanceof ArrayCreationExpression) {
79+
PhpPsiElement dataClassPsiElement = PhpElementsUtil.getArrayValue((ArrayCreationExpression) parameter, "data_class");
80+
if (dataClassPsiElement != null) {
81+
phpClassFqn = getString(dataClassPsiElement);
82+
}
83+
}
84+
}
85+
}
86+
87+
if (phpClassFqn != null) {
88+
Method methodScope = PsiTreeUtil.getParentOfType(element, Method.class);
89+
if (methodScope != null) {
90+
PhpClass parentOfType = methodScope.getContainingClass();
91+
if (parentOfType != null) {
92+
map.putIfAbsent(phpClassFqn, new HashSet<>());
93+
map.get(phpClassFqn).add(parentOfType.getFQN());
94+
}
95+
}
96+
}
97+
}
98+
99+
super.visitElement(element);
100+
}
101+
102+
@Nullable
103+
private String getString(@NotNull PsiElement parameter) {
104+
if (parameter instanceof ClassConstantReference) {
105+
String classConstantPhpFqn = PhpElementsUtil.getClassConstantPhpFqn((ClassConstantReference) parameter);
106+
if (StringUtils.isNotBlank(classConstantPhpFqn)) {
107+
return "\\" + StringUtils.stripStart(classConstantPhpFqn, "\\");
108+
}
109+
} else if (parameter instanceof StringLiteralExpression) {
110+
String contents1 = ((StringLiteralExpression) parameter).getContents();
111+
if (StringUtils.isNotBlank(contents1)) {
112+
return "\\" + StringUtils.stripStart(contents1, "\\");
113+
}
114+
}
115+
116+
return null;
117+
}
118+
});
119+
120+
return map;
121+
};
122+
123+
}
124+
125+
@NotNull
126+
@Override
127+
public KeyDescriptor<String> getKeyDescriptor() {
128+
return this.myKeyDescriptor;
129+
}
130+
131+
@NotNull
132+
@Override
133+
public DataExternalizer<Set<String>> getValueExternalizer() {
134+
return new StringSetDataExternalizer();
135+
}
136+
137+
@NotNull
138+
@Override
139+
public FileBasedIndex.InputFilter getInputFilter() {
140+
return file -> file.getFileType() == PhpFileType.INSTANCE;
141+
}
142+
143+
@Override
144+
public boolean dependsOnFileContent() {
145+
return true;
146+
}
147+
148+
@Override
149+
public int getVersion() {
150+
return 1;
151+
}
152+
}

src/main/resources/META-INF/plugin.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@
227227
<fileBasedIndex implementation="fr.adrienbrault.idea.symfony2plugin.stubs.indexes.ContainerIdUsagesStubIndex"/>
228228
<fileBasedIndex implementation="fr.adrienbrault.idea.symfony2plugin.stubs.indexes.TwigBlockIndexExtension"/>
229229
<fileBasedIndex implementation="fr.adrienbrault.idea.symfony2plugin.stubs.indexes.TwigControllerStubIndex"/>
230+
<fileBasedIndex implementation="fr.adrienbrault.idea.symfony2plugin.stubs.indexes.FormDataClassStubIndex"/>
230231

231232
<codeInsight.lineMarkerProvider language="PHP" implementationClass="fr.adrienbrault.idea.symfony2plugin.config.ServiceLineMarkerProvider"/>
232233
<codeInsight.lineMarkerProvider language="PHP" implementationClass="fr.adrienbrault.idea.symfony2plugin.dic.ControllerMethodLineMarkerProvider"/>

src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/form/PhpLineMarkerProviderTest.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ protected String getTestDataPath() {
1717
return "src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/form/fixtures";
1818
}
1919

20-
public void testThatRouteLineMarkerForControllerIsGiven() {
20+
public void testThatFormCanNavigateToDataClass() {
2121
assertLineMarker(
2222
myFixture.configureByText(
2323
PhpFileType.INSTANCE,
@@ -38,6 +38,17 @@ public void testThatRouteLineMarkerForControllerIsGiven() {
3838
),
3939
new LineMarker.ToolTipEqualsAssert("Navigate to data class")
4040
);
41+
}
4142

43+
public void testThatDataClassCanNavigateToForm() {
44+
assertLineMarker(
45+
myFixture.configureByText(
46+
PhpFileType.INSTANCE,
47+
"<?php\n" +
48+
"namespace App;\n" +
49+
"class FoobarDataClass {}\n"
50+
),
51+
new LineMarker.ToolTipEqualsAssert("Navigate to form")
52+
);
4253
}
4354
}

src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/form/fixtures/classes.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,16 @@ public function getExtendedType() {
7575
}
7676

7777
}
78+
79+
class FormTypeDataClass
80+
{
81+
public function configureOptions(OptionsResolver $resolver)
82+
{
83+
$resolver->setDefaults(array(
84+
'data_class' => \App\FoobarDataClass::class,
85+
));
86+
}
87+
}
7888
}
7989

8090
namespace Form\FormType {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package fr.adrienbrault.idea.symfony2plugin.tests.stubs.indexes;
2+
3+
import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.FormDataClassStubIndex;
4+
import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase;
5+
6+
/**
7+
* @author Daniel Espendiller <daniel@espendiller.net>
8+
*
9+
* @see fr.adrienbrault.idea.symfony2plugin.stubs.indexes.FormDataClassStubIndex
10+
*/
11+
public class FormDataClassStubIndexTest extends SymfonyLightCodeInsightFixtureTestCase {
12+
13+
public void setUp() throws Exception {
14+
super.setUp();
15+
16+
myFixture.configureFromExistingVirtualFile(myFixture.copyFileToProject("FormDataClassStubIndex.php"));
17+
}
18+
19+
public String getTestDataPath() {
20+
return "src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/stubs/indexes/fixtures";
21+
}
22+
23+
public void testTemplateIncludeIndexer() {
24+
assertIndexContains(FormDataClassStubIndex.KEY, "\\App\\FooDataClass1", "\\App\\FooDataClass2", "\\App\\FooDataClass3");
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace App {
4+
5+
use Symfony\Component\OptionsResolver\OptionsResolver;
6+
7+
class FooDataClass1 {}
8+
class FooDataClass2 {}
9+
class FooDataClass3 {}
10+
11+
class AutoFarmType
12+
{
13+
public function configureOptions(OptionsResolver $resolver)
14+
{
15+
$resolver->setDefaults([
16+
'data_class' => FooDataClass1::class,
17+
]);
18+
19+
$resolver->setDefault('data_class', FooDataClass2::class);
20+
21+
$resolver->setDefault('data_class', 'App\FooDataClass3');
22+
}
23+
}
24+
}

0 commit comments

Comments
 (0)