Skip to content

Commit 2faab55

Browse files
committed
add Parameter Hints for YAML and XML service arguments #896
1 parent a23b7ce commit 2faab55

File tree

7 files changed

+324
-8
lines changed

7 files changed

+324
-8
lines changed

META-INF/plugin.xml

+2
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,8 @@
396396
<directoryProjectGenerator implementation="fr.adrienbrault.idea.symfony2plugin.installer.SymfonyInstallerProjectGenerator"/>
397397
<projectTemplatesFactory implementation="fr.adrienbrault.idea.symfony2plugin.installer.SymfonyInstallerTemplatesFactory"/>
398398

399+
<codeInsight.parameterNameHints language="XML" implementationClass="fr.adrienbrault.idea.symfony2plugin.dic.ServiceArgumentParameterHintsProvider"/>
400+
399401
<localInspection groupPath="Symfony" shortName="Symfony2PhpRouteMissingInspection" displayName="Route Missing"
400402
groupName="Route"
401403
enabledByDefault="true" level="WARNING"

src/fr/adrienbrault/idea/symfony2plugin/config/xml/XmlServiceContainerAnnotator.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ private void attachMethodInstances(PsiElement target, String serviceName, Method
133133

134134
}
135135

136-
public static int getArgumentIndex(XmlTag xmlTag) {
136+
public static int getArgumentIndex(@NotNull XmlTag xmlTag) {
137137

138138
PsiElement psiElement = xmlTag;
139139
int index = 0;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package fr.adrienbrault.idea.symfony2plugin.dic;
2+
3+
import com.intellij.codeInsight.hints.HintInfo;
4+
import com.intellij.codeInsight.hints.InlayInfo;
5+
import com.intellij.codeInsight.hints.InlayParameterHintsProvider;
6+
import com.intellij.openapi.util.Pair;
7+
import com.intellij.psi.PsiElement;
8+
import com.intellij.psi.xml.XmlAttribute;
9+
import com.intellij.psi.xml.XmlAttributeValue;
10+
import com.intellij.psi.xml.XmlTag;
11+
import com.intellij.psi.xml.XmlText;
12+
import com.jetbrains.php.lang.psi.elements.Function;
13+
import com.jetbrains.php.lang.psi.elements.Method;
14+
import com.jetbrains.php.lang.psi.elements.Parameter;
15+
import com.jetbrains.php.lang.psi.elements.PhpClass;
16+
import fr.adrienbrault.idea.symfony2plugin.config.xml.XmlServiceContainerAnnotator;
17+
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
18+
import fr.adrienbrault.idea.symfony2plugin.util.dict.ServiceUtil;
19+
import org.apache.commons.lang.StringUtils;
20+
import org.jetbrains.annotations.NotNull;
21+
import org.jetbrains.annotations.Nullable;
22+
23+
import java.util.ArrayList;
24+
import java.util.Collections;
25+
import java.util.List;
26+
import java.util.Set;
27+
28+
/**
29+
* @author Daniel Espendiller <daniel@espendiller.net>
30+
*/
31+
public class ServiceArgumentParameterHintsProvider implements InlayParameterHintsProvider {
32+
@NotNull
33+
@Override
34+
public List<InlayInfo> getParameterHints(PsiElement psiElement) {
35+
List<InlayInfo> inlays = new ArrayList<>();
36+
37+
Match typeHint = getTypeHint(psiElement);
38+
if(typeHint != null) {
39+
inlays.add(new InlayInfo(typeHint.parameter, typeHint.targetOffset));
40+
}
41+
42+
return inlays;
43+
}
44+
45+
@Nullable
46+
@Override
47+
public HintInfo getHintInfo(PsiElement psiElement) {
48+
Match typeHint = getTypeHint(psiElement);
49+
if(typeHint == null) {
50+
return null;
51+
}
52+
53+
return new HintInfo.MethodInfo(typeHint.method != null ? typeHint.method.getFQN() : typeHint.parameter, Collections.emptyList());
54+
}
55+
56+
@NotNull
57+
@Override
58+
public Set<String> getDefaultBlackList() {
59+
return Collections.emptySet();
60+
}
61+
62+
@Nullable
63+
private Match getTypeHint(@NotNull PsiElement psiElement) {
64+
if(psiElement instanceof XmlAttributeValue) {
65+
// <service><argument type="service" id="a<caret>a"></service>
66+
PsiElement xmlAttribute = psiElement.getParent();
67+
if(xmlAttribute instanceof XmlAttribute && "id".equalsIgnoreCase(((XmlAttribute) xmlAttribute).getName())) {
68+
PsiElement argumentTag = xmlAttribute.getParent();
69+
70+
if (argumentTag instanceof XmlTag) {
71+
if ("service".equalsIgnoreCase(((XmlTag) argumentTag).getName())) {
72+
// <service type="alias" id="a<caret>a"/>
73+
String alias = ((XmlTag) argumentTag).getAttributeValue("alias");
74+
if (alias != null && StringUtils.isNotBlank(alias)) {
75+
PhpClass serviceClass = ServiceUtil.getServiceClass(psiElement.getProject(), alias);
76+
if (serviceClass != null) {
77+
PsiElement firstChild = psiElement.getFirstChild();
78+
if (firstChild != null) {
79+
return new Match(serviceClass.getName(), null, firstChild.getTextRange().getEndOffset());
80+
}
81+
}
82+
}
83+
} else if ("argument".equalsIgnoreCase(((XmlTag) argumentTag).getName())) {
84+
// <service><argument type="service" id="a<caret>a"></service>
85+
PsiElement serviceTag = argumentTag.getParent();
86+
if (serviceTag instanceof XmlTag && "service".equals(((XmlTag) serviceTag).getName())) {
87+
Pair<String, Method> parameterHint = findMethodParameterHint((XmlTag) argumentTag);
88+
if (parameterHint != null) {
89+
PsiElement firstChild = psiElement.getFirstChild();
90+
if (firstChild != null) {
91+
return new Match(parameterHint.getFirst(), parameterHint.getSecond(), firstChild.getTextRange().getEndOffset());
92+
}
93+
}
94+
}
95+
}
96+
}
97+
}
98+
} else if(psiElement instanceof XmlText) {
99+
// <service><argument>%a<caret>a%</argument></service>
100+
PsiElement argumentTag = psiElement.getParent();
101+
102+
// match only: <argument>%a<caret>a%</argument>
103+
// ignore: <argument>\n<caret><argument></argument></argument>
104+
if(argumentTag instanceof XmlTag && "argument".equals(((XmlTag) argumentTag).getName()) && ((XmlTag) argumentTag).getSubTags().length == 0) {
105+
PsiElement serviceTag = argumentTag.getParent();
106+
if(serviceTag instanceof XmlTag && "service".equals(((XmlTag) serviceTag).getName())) {
107+
Pair<String, Method> parameterHint = findMethodParameterHint((XmlTag) argumentTag);
108+
if(parameterHint != null) {
109+
PsiElement firstChild = psiElement.getFirstChild();
110+
if(firstChild != null) {
111+
return new Match(parameterHint.getFirst(), parameterHint.getSecond(), firstChild.getTextRange().getStartOffset());
112+
}
113+
}
114+
}
115+
}
116+
}
117+
118+
return null;
119+
}
120+
121+
@Nullable
122+
private Pair<String, Method> findMethodParameterHint(@NotNull XmlTag argumentTag) {
123+
PsiElement serviceTag = argumentTag.getParent();
124+
if("service".equalsIgnoreCase(((XmlTag) serviceTag).getName())) {
125+
String aClass = ((XmlTag) serviceTag).getAttributeValue("class");
126+
if(StringUtils.isNotBlank(aClass)) {
127+
PhpClass phpClass = ServiceUtil.getResolvedClassDefinition(serviceTag.getProject(), aClass);
128+
if(phpClass != null) {
129+
Method constructor = phpClass.getConstructor();
130+
if(constructor != null) {
131+
int argumentIndex = XmlServiceContainerAnnotator.getArgumentIndex(argumentTag);
132+
133+
String s = attachMethodInstances(constructor, argumentIndex);
134+
if(s == null) {
135+
return null;
136+
}
137+
138+
return Pair.create(s, constructor);
139+
}
140+
}
141+
}
142+
}
143+
144+
return null;
145+
}
146+
147+
@Nullable
148+
private String attachMethodInstances(@NotNull Function function, int parameterIndex) {
149+
Parameter[] constructorParameter = function.getParameters();
150+
if(parameterIndex >= constructorParameter.length) {
151+
return null;
152+
}
153+
154+
Parameter parameter = constructorParameter[parameterIndex];
155+
156+
String className = parameter.getDeclaredType().toString();
157+
PhpClass expectedClass = PhpElementsUtil.getClassInterface(function.getProject(), className);
158+
if(expectedClass != null) {
159+
return expectedClass.getName();
160+
}
161+
162+
return parameter.getName();
163+
}
164+
165+
private static class Match {
166+
@NotNull
167+
private final String parameter;
168+
169+
@Nullable
170+
private final Method method;
171+
172+
private final int targetOffset;
173+
174+
Match(@NotNull String parameter, @Nullable Method method, int targetOffset) {
175+
this.parameter = parameter;
176+
this.method = method;
177+
this.targetOffset = targetOffset;
178+
}
179+
}
180+
}

symfony2-plugin.iml

+7-7
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,18 @@
55
<exclude-output />
66
<content url="file://$MODULE_DIR$">
77
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
8+
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
89
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
9-
<sourceFolder url="file://$MODULE_DIR$/resources" isTestSource="false" />
1010
</content>
1111
<orderEntry type="inheritedJdk" />
1212
<orderEntry type="sourceFolder" forTests="false" />
13-
<orderEntry type="library" scope="PROVIDED" name="php-openapi" level="project" />
14-
<orderEntry type="library" scope="PROVIDED" name="php" level="project" />
15-
<orderEntry type="library" scope="TEST" name="junit:junit:4.9" level="project" />
16-
<orderEntry type="library" scope="TEST" name="css-openapi" level="project" />
17-
<orderEntry type="library" scope="TEST" name="css" level="project" />
18-
<orderEntry type="library" scope="TEST" name="java-i18n" level="project" />
1913
<orderEntry type="library" scope="TEST" name="properties" level="project" />
14+
<orderEntry type="library" scope="TEST" name="css-openapi" level="project" />
15+
<orderEntry type="library" scope="TEST" name="lib" level="project" />
16+
<orderEntry type="library" scope="TEST" name="php" level="project" />
17+
<orderEntry type="library" scope="TEST" name="twig" level="project" />
18+
<orderEntry type="library" scope="TEST" name="webDeployment" level="project" />
2019
<orderEntry type="library" scope="TEST" name="yaml" level="project" />
20+
<orderEntry type="library" scope="TEST" name="java-i18n" level="project" />
2121
</component>
2222
</module>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package fr.adrienbrault.idea.symfony2plugin.tests.dic;
2+
3+
import com.intellij.codeInsight.hints.InlayInfo;
4+
import com.intellij.ide.highlighter.XmlFileType;
5+
import com.intellij.psi.PsiElement;
6+
import com.intellij.util.containers.ContainerUtil;
7+
import fr.adrienbrault.idea.symfony2plugin.dic.ServiceArgumentParameterHintsProvider;
8+
import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase;
9+
import org.jetbrains.annotations.NotNull;
10+
11+
import java.io.File;
12+
import java.util.List;
13+
14+
/**
15+
* @author Daniel Espendiller <daniel@espendiller.net>
16+
*
17+
* @see ServiceArgumentParameterHintsProvider
18+
*/
19+
public class ServiceArgumentParameterHintsProviderTest extends SymfonyLightCodeInsightFixtureTestCase {
20+
public void setUp() throws Exception {
21+
super.setUp();
22+
myFixture.copyFileToProject("ServiceArgumentParameterHintsProvider.php");
23+
myFixture.copyFileToProject("ServiceArgumentParameterHintsProvider.xml");
24+
}
25+
26+
public String getTestDataPath() {
27+
return new File(this.getClass().getResource("fixtures").getFile()).getAbsolutePath();
28+
}
29+
30+
public void testXmlParameterTypeHintForIdAttribute() {
31+
List<InlayInfo> parameterHints = getInlayInfo("" +
32+
"<container>\n" +
33+
" <services>\n" +
34+
" <service class=\"Foobar\\MyFoobar\">\n" +
35+
" <argument type=\"service\" id=\"a<caret>a\">\n" +
36+
" </service>\n" +
37+
" </services>\n" +
38+
"</container>\n"
39+
);
40+
41+
assertNotNull(ContainerUtil.find(parameterHints, inlayInfo -> "FooInterface".equals(inlayInfo.getText())));
42+
}
43+
44+
public void testXmlParameterTypeHintForXmlText() {
45+
List<InlayInfo> parameterHints = getInlayInfo("" +
46+
"<container>\n" +
47+
" <services>\n" +
48+
" <service class=\"Foobar\\MyFoobar\">\n" +
49+
" <argument>%a<caret>a%</argument>\n" +
50+
" </service>\n" +
51+
" </services>\n" +
52+
"</container>\n"
53+
);
54+
55+
assertNotNull(ContainerUtil.find(parameterHints, inlayInfo -> "FooInterface".equals(inlayInfo.getText())));
56+
}
57+
58+
public void testXmlParameterTypeHintWithoutTypeHintMustFallbackToParameterName() {
59+
List<InlayInfo> parameterHints = getInlayInfo("" +
60+
"<container>\n" +
61+
" <services>\n" +
62+
" <service class=\"Foobar\\MyFoobar\">\n" +
63+
" <argument>a</argument>\n" +
64+
" <argument>%a<caret>a%</argument>\n" +
65+
" </service>\n" +
66+
" </services>\n" +
67+
"</container>\n"
68+
);
69+
70+
assertNotNull(ContainerUtil.find(parameterHints, inlayInfo -> "foobar".equals(inlayInfo.getText())));
71+
}
72+
73+
public void testXmlParameterTypeForNestedArgumentsMustNotProvideHint() {
74+
List<InlayInfo> parameterHints = getInlayInfo("" +
75+
"<container>\n" +
76+
" <services>\n" +
77+
" <service class=\"Foobar\\MyFoobar\">\n" +
78+
" <argument type=\"collection\">\n" +
79+
" <caret>\n" +
80+
" <argument/>" +
81+
" </argument>\n" +
82+
" </service>\n" +
83+
" </services>\n" +
84+
"</container>\n"
85+
);
86+
87+
assertSize(0, parameterHints);
88+
}
89+
90+
public void testXmlParameterTypeForId() {
91+
List<InlayInfo> parameterHints = getInlayInfo("" +
92+
"<container>\n" +
93+
" <services>\n" +
94+
" <service id=\"my_<caret>foo\" alias=\"foo_alias_hint\"/>\n" +
95+
" </services>\n" +
96+
"</container>\n"
97+
);
98+
99+
assertNotNull(ContainerUtil.find(parameterHints, inlayInfo -> "MyFoobar".equals(inlayInfo.getText())));
100+
}
101+
102+
@NotNull
103+
private List<InlayInfo> getInlayInfo(@NotNull String content) {
104+
myFixture.configureByText(XmlFileType.INSTANCE, content);
105+
106+
PsiElement psiElement = myFixture.getFile().findElementAt(myFixture.getCaretOffset());
107+
assertNotNull(psiElement);
108+
109+
PsiElement parent = psiElement.getParent();
110+
111+
return new ServiceArgumentParameterHintsProvider().getParameterHints(parent);
112+
}
113+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace Foobar
4+
{
5+
interface FooInterface
6+
{
7+
}
8+
9+
class MyFoobar
10+
{
11+
public function __construct(FooInterface $foo, $foobar)
12+
{
13+
}
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<container>
3+
<services>
4+
<service id="foo_alias_hint" class="Foobar\MyFoobar"/>
5+
</services>
6+
</container>

0 commit comments

Comments
 (0)