Skip to content

Commit b189a8d

Browse files
authored
Merge pull request #1301 from King2500/feature/lang-injector
Language injection for CSS, XPath, JSON and DQL
2 parents 020089d + 3527ba7 commit b189a8d

File tree

7 files changed

+282
-2
lines changed

7 files changed

+282
-2
lines changed

.travis.yml

+3-2
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@ before_install:
1818
- "export ORG_GRADLE_PROJECT_ideaVersion=${IDEA_VERSION}"
1919
- "export ORG_GRADLE_PROJECT_phpPluginVersion=${PHP_PLUGIN_VERSION}"
2020
- "export ORG_GRADLE_PROJECT_twigPluginVersion=${TWIG_PLUGIN_VERSION}"
21+
- "export ORG_GRADLE_PROJECT_dqlPluginVersion=${DQL_PLUGIN_VERSION}"
2122
- "export ORG_GRADLE_PROJECT_toolboxPluginVersion=${TOOLBOX_PLUGIN_VERSION}"
2223
- "export ORG_GRADLE_PROJECT_annotationPluginVersion=${ANNOTATION_PLUGIN_VERSION}"
2324

2425
env:
25-
- PHPSTORM_ENV="skip incomplete" IDEA_VERSION="IU-191.6707.31-EAP-SNAPSHOT" PHP_PLUGIN_VERSION="191.6707.42" TWIG_PLUGIN_VERSION="191.6183.95" TOOLBOX_PLUGIN_VERSION="0.4.6" ANNOTATION_PLUGIN_VERSION="5.3"
26-
- PHPSTORM_ENV="skip incomplete" IDEA_VERSION="IU-2019.1" PHP_PLUGIN_VERSION="191.6183.95" TWIG_PLUGIN_VERSION="191.6183.95" TOOLBOX_PLUGIN_VERSION="0.4.6" ANNOTATION_PLUGIN_VERSION="5.3"
26+
- PHPSTORM_ENV="skip incomplete" IDEA_VERSION="IU-191.6707.31-EAP-SNAPSHOT" PHP_PLUGIN_VERSION="191.6707.42" TWIG_PLUGIN_VERSION="191.6183.95" DQL_PLUGIN_VERSION="191.5849.16" TOOLBOX_PLUGIN_VERSION="0.4.6" ANNOTATION_PLUGIN_VERSION="5.3"
27+
- PHPSTORM_ENV="skip incomplete" IDEA_VERSION="IU-2019.1" PHP_PLUGIN_VERSION="191.6183.95" TWIG_PLUGIN_VERSION="191.6183.95" DQL_PLUGIN_VERSION="191.5849.16" TOOLBOX_PLUGIN_VERSION="0.4.6" ANNOTATION_PLUGIN_VERSION="5.3"
2728

2829
script:
2930
- "./gradlew check verifyPlugin buildPlugin"

build.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,14 @@ intellij {
2828
plugins = [
2929
"com.jetbrains.php:${phpPluginVersion}",
3030
"com.jetbrains.twig:${twigPluginVersion}",
31+
"com.jetbrains.php.dql:${dqlPluginVersion}",
3132
"de.espend.idea.php.annotation:${annotationPluginVersion}",
3233
"de.espend.idea.php.toolbox:${toolboxPluginVersion}",
3334
'coverage',
3435
'webDeployment',
3536
'yaml',
3637
'CSS',
38+
'XPath',
3739
'java-i18n',
3840
'properties'
3941
]

gradle.properties

+4
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,28 @@
22
#ideaVersion = IU-191.6707.31-EAP-SNAPSHOT
33
#phpPluginVersion = 191.6707.42
44
#twigPluginVersion = 191.6183.95
5+
#dqlPluginVersion = 191.5849.16
56
#toolboxPluginVersion = 0.4.6
67
#annotationPluginVersion = 5.3
78

89
ideaVersion = IU-2019.1
910
phpPluginVersion = 191.6183.95
1011
twigPluginVersion = 191.6183.95
12+
dqlPluginVersion = 191.5849.16
1113
toolboxPluginVersion = 0.4.6
1214
annotationPluginVersion = 5.3
1315

1416
#ideaVersion = IU-2018.3.6
1517
#phpPluginVersion = 183.6156.29
1618
#twigPluginVersion = 183.3795.24
19+
#dqlPluginVersion = 183.4284.100
1720
#toolboxPluginVersion = 0.4.6
1821
#annotationPluginVersion = 5.3
1922

2023
#ideaVersion = IU-2018.3
2124
#phpPluginVersion = 183.4284.150
2225
#twigPluginVersion = 183.3795.24
26+
#dqlPluginVersion = 183.4284.100
2327
#toolboxPluginVersion = 0.4.6
2428
#annotationPluginVersion = 5.3
2529

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package fr.adrienbrault.idea.symfony2plugin.lang;
2+
3+
import com.intellij.lang.Language;
4+
import com.intellij.lang.injection.MultiHostInjector;
5+
import com.intellij.lang.injection.MultiHostRegistrar;
6+
import com.intellij.openapi.util.TextRange;
7+
import com.intellij.psi.PsiElement;
8+
import com.intellij.psi.PsiLanguageInjectionHost;
9+
import com.jetbrains.php.lang.psi.elements.*;
10+
import com.jetbrains.php.lang.psi.elements.impl.StringLiteralExpressionImpl;
11+
import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent;
12+
import fr.adrienbrault.idea.symfony2plugin.util.MethodMatcher;
13+
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
14+
import org.jetbrains.annotations.NotNull;
15+
16+
import java.util.Collections;
17+
import java.util.List;
18+
19+
public class ParameterLanguageInjector implements MultiHostInjector {
20+
21+
private static final MethodMatcher.CallToSignature[] CSS_SELECTOR_SIGNATURES = {
22+
new MethodMatcher.CallToSignature("\\Symfony\\Component\\DomCrawler\\Crawler", "filter"),
23+
new MethodMatcher.CallToSignature("\\Symfony\\Component\\DomCrawler\\Crawler", "children"),
24+
new MethodMatcher.CallToSignature("\\Symfony\\Component\\CssSelector\\CssSelectorConverter", "toXPath"),
25+
};
26+
27+
private static final MethodMatcher.CallToSignature[] XPATH_SIGNATURES = {
28+
new MethodMatcher.CallToSignature("\\Symfony\\Component\\DomCrawler\\Crawler", "filterXPath"),
29+
new MethodMatcher.CallToSignature("\\Symfony\\Component\\DomCrawler\\Crawler", "evaluate"),
30+
};
31+
32+
private static final MethodMatcher.CallToSignature[] JSON_SIGNATURES = {
33+
//new MethodMatcher.CallToSignature("\\Symfony\\Component\\HttpFoundation\\JsonResponse", "__construct"),
34+
new MethodMatcher.CallToSignature("\\Symfony\\Component\\HttpFoundation\\JsonResponse", "fromJsonString"),
35+
new MethodMatcher.CallToSignature("\\Symfony\\Component\\HttpFoundation\\JsonResponse", "setJson"),
36+
};
37+
38+
private static final MethodMatcher.CallToSignature[] DQL_SIGNATURES = {
39+
new MethodMatcher.CallToSignature("\\Doctrine\\ORM\\EntityManager", "createQuery"),
40+
new MethodMatcher.CallToSignature("\\Doctrine\\ORM\\Query", "setDQL"),
41+
};
42+
43+
private final MethodLanguageInjection[] LANGUAGE_INJECTIONS = {
44+
new MethodLanguageInjection(LANGUAGE_ID_CSS, "@media all { ", " }", CSS_SELECTOR_SIGNATURES),
45+
new MethodLanguageInjection(LANGUAGE_ID_XPATH, null, null, XPATH_SIGNATURES),
46+
new MethodLanguageInjection(LANGUAGE_ID_JSON, null, null, JSON_SIGNATURES),
47+
new MethodLanguageInjection(LANGUAGE_ID_DQL, null, null, DQL_SIGNATURES),
48+
};
49+
50+
public static final String LANGUAGE_ID_CSS = "CSS";
51+
public static final String LANGUAGE_ID_XPATH = "XPath";
52+
public static final String LANGUAGE_ID_JSON = "JSON";
53+
public static final String LANGUAGE_ID_DQL = "DQL";
54+
55+
private static final String DQL_VARIABLE_NAME = "dql";
56+
57+
public ParameterLanguageInjector() {
58+
}
59+
60+
@NotNull
61+
@Override
62+
public List<? extends Class<? extends PsiElement>> elementsToInjectIn() {
63+
return Collections.singletonList(StringLiteralExpressionImpl.class);
64+
}
65+
66+
@Override
67+
public void getLanguagesToInject(@NotNull MultiHostRegistrar registrar, @NotNull PsiElement element) {
68+
if (!(element instanceof StringLiteralExpression) || !((PsiLanguageInjectionHost) element).isValidHost()) {
69+
return;
70+
}
71+
if (!Symfony2ProjectComponent.isEnabled(element.getProject())) {
72+
return;
73+
}
74+
75+
final StringLiteralExpressionImpl expr = (StringLiteralExpressionImpl) element;
76+
77+
PsiElement parent = expr.getParent();
78+
79+
final boolean isParameter = parent instanceof ParameterList && expr.getPrevPsiSibling() == null; // 1st parameter
80+
final boolean isAssignment = parent instanceof AssignmentExpression;
81+
82+
if (!isParameter && !isAssignment) {
83+
return;
84+
}
85+
86+
if (isParameter) {
87+
parent = parent.getParent();
88+
}
89+
90+
for (MethodLanguageInjection languageInjection : LANGUAGE_INJECTIONS) {
91+
Language language = languageInjection.getLanguage();
92+
if (language == null) {
93+
continue;
94+
}
95+
// $crawler->filter('...')
96+
// $em->createQuery('...')
97+
// JsonResponse::fromJsonString('...')
98+
if (parent instanceof MethodReference) {
99+
if (PhpElementsUtil.isMethodReferenceInstanceOf((MethodReference) parent, languageInjection.getSignatures())) {
100+
injectLanguage(registrar, expr, language, languageInjection);
101+
return;
102+
}
103+
}
104+
// $dql = "...";
105+
else if (parent instanceof AssignmentExpression) {
106+
if (LANGUAGE_ID_DQL.equals(language.getID())) {
107+
PhpPsiElement variable = ((AssignmentExpression) parent).getVariable();
108+
if (variable instanceof Variable) {
109+
if (DQL_VARIABLE_NAME.equals(variable.getName())) {
110+
injectLanguage(registrar, expr, language, languageInjection);
111+
return;
112+
}
113+
}
114+
}
115+
}
116+
}
117+
118+
}
119+
120+
private void injectLanguage(@NotNull MultiHostRegistrar registrar, @NotNull StringLiteralExpressionImpl element, Language language, MethodLanguageInjection languageInjection) {
121+
final int length = ((StringLiteralExpression) element).getContents().length();
122+
final TextRange range = TextRange.create(1, length + 1);
123+
124+
registrar.startInjecting(language)
125+
.addPlace(languageInjection.getPrefix(), languageInjection.getSuffix(), element, range)
126+
.doneInjecting();
127+
}
128+
129+
private class MethodLanguageInjection {
130+
private final Language language;
131+
private final String prefix;
132+
private final String suffix;
133+
private final MethodMatcher.CallToSignature[] signatures;
134+
135+
MethodLanguageInjection(@NotNull String languageId, String prefix, String suffix, MethodMatcher.CallToSignature[] signatures) {
136+
137+
this.language = Language.findLanguageByID(languageId);
138+
this.prefix = prefix;
139+
this.suffix = suffix;
140+
this.signatures = signatures;
141+
}
142+
143+
public Language getLanguage() {
144+
return language;
145+
}
146+
147+
public String getPrefix() {
148+
return prefix;
149+
}
150+
151+
public String getSuffix() {
152+
return suffix;
153+
}
154+
155+
public MethodMatcher.CallToSignature[] getSignatures() {
156+
return signatures;
157+
}
158+
}
159+
}

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

+3
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,8 @@
254254
<codeInsight.parameterNameHints language="XML" implementationClass="fr.adrienbrault.idea.symfony2plugin.dic.ServiceArgumentParameterHintsProvider"/>
255255
<codeInsight.parameterNameHints language="yaml" implementationClass="fr.adrienbrault.idea.symfony2plugin.dic.ServiceArgumentParameterHintsProvider"/>
256256

257+
<multiHostInjector implementation="fr.adrienbrault.idea.symfony2plugin.lang.ParameterLanguageInjector"/>
258+
257259
<localInspection groupPath="Symfony" shortName="PhpRouteMissingInspection" displayName="Route Missing"
258260
groupName="Route"
259261
enabledByDefault="true" level="WARNING"
@@ -611,6 +613,7 @@
611613
<depends>org.jetbrains.plugins.yaml</depends>
612614
<depends>de.espend.idea.php.annotation</depends>
613615

616+
<depends optional="true">com.jetbrains.php.dql</depends>
614617
<depends optional="true">de.espend.idea.php.toolbox</depends>
615618
<depends optional="true" config-file="deployment-aware.xml">com.jetbrains.plugins.webDeployment</depends>
616619

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package fr.adrienbrault.idea.symfony2plugin.tests.lang;
2+
3+
import com.intellij.openapi.fileTypes.LanguageFileType;
4+
import com.intellij.testFramework.fixtures.InjectionTestFixture;
5+
import com.jetbrains.php.lang.PhpFileType;
6+
import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase;
7+
8+
import static fr.adrienbrault.idea.symfony2plugin.lang.ParameterLanguageInjector.*;
9+
10+
public class ParameterLanguageInjectorTest extends SymfonyLightCodeInsightFixtureTestCase {
11+
12+
private InjectionTestFixture injectionTestFixture;
13+
14+
@Override
15+
public void setUp() throws Exception {
16+
super.setUp();
17+
myFixture.copyFileToProject("classes.php");
18+
injectionTestFixture = new InjectionTestFixture(myFixture);
19+
}
20+
21+
public String getTestDataPath() {
22+
return "src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/lang/fixtures";
23+
}
24+
25+
public void testCssLanguageInjections() {
26+
String base = "<?php $c = new \\Symfony\\Component\\DomCrawler\\Crawler();\n";
27+
assertInjectedLangAtCaret(PhpFileType.INSTANCE, base + "$c->filter('html > bo<caret>dy');", LANGUAGE_ID_CSS);
28+
assertInjectedLangAtCaret(PhpFileType.INSTANCE, base + "$c->filter('<caret>');", LANGUAGE_ID_CSS);
29+
assertInjectedLangAtCaret(PhpFileType.INSTANCE, base + "$c->children('html > bo<caret>dy');", LANGUAGE_ID_CSS);
30+
assertInjectedLangAtCaret(PhpFileType.INSTANCE, base + "$c->children('<caret>');", LANGUAGE_ID_CSS);
31+
32+
base = "<?php $c = new \\Symfony\\Component\\CssSelector\\CssSelectorConverter();\n";
33+
assertInjectedLangAtCaret(PhpFileType.INSTANCE, base + "$c->toXPath('html > bo<caret>dy');", LANGUAGE_ID_CSS);
34+
assertInjectedLangAtCaret(PhpFileType.INSTANCE, base + "$c->toXPath('<caret>');", LANGUAGE_ID_CSS);
35+
}
36+
37+
public void testXPathLanguageInjections() {
38+
String base = "<?php $c = new \\Symfony\\Component\\DomCrawler\\Crawler();\n";
39+
assertInjectedLangAtCaret(PhpFileType.INSTANCE, base + "$c->filterXPath('//dum<caret>my');", LANGUAGE_ID_XPATH);
40+
assertInjectedLangAtCaret(PhpFileType.INSTANCE, base + "$c->filterXPath('<caret>');", LANGUAGE_ID_XPATH);
41+
assertInjectedLangAtCaret(PhpFileType.INSTANCE, base + "$c->evaluate('//dum<caret>my');", LANGUAGE_ID_XPATH);
42+
assertInjectedLangAtCaret(PhpFileType.INSTANCE, base + "$c->evaluate('<caret>');", LANGUAGE_ID_XPATH);
43+
}
44+
45+
public void testJsonLanguageInjections() {
46+
String base = "<?php \\Symfony\\Component\\HttpFoundation\\";
47+
assertInjectedLangAtCaret(PhpFileType.INSTANCE, base + "JsonResponse::fromJsonString('<caret>');", LANGUAGE_ID_JSON);
48+
assertInjectedLangAtCaret(PhpFileType.INSTANCE, base + "JsonResponse::fromJsonString('{\"foo\": <caret>}');", LANGUAGE_ID_JSON);
49+
50+
base = "<?php $r = new \\Symfony\\Component\\HttpFoundation\\JsonResponse();\n";
51+
assertInjectedLangAtCaret(PhpFileType.INSTANCE, base + "$r->setJson('<caret>');", LANGUAGE_ID_JSON);
52+
assertInjectedLangAtCaret(PhpFileType.INSTANCE, base + "$r->setJson('{\"foo\": <caret>}');", LANGUAGE_ID_JSON);
53+
}
54+
55+
public void testDqlLanguageInjections() {
56+
String base = "<?php $em = new \\Doctrine\\ORM\\EntityManager();\n";
57+
assertInjectedLangAtCaret(PhpFileType.INSTANCE, base + "$em->createQuery('SELECT b FR<caret>OM \\Foo\\Bar b');", LANGUAGE_ID_DQL);
58+
assertInjectedLangAtCaret(PhpFileType.INSTANCE, base + "$em->createQuery('<caret>');", LANGUAGE_ID_DQL);
59+
60+
base = "<?php $q = new \\Doctrine\\ORM\\Query();\n";
61+
assertInjectedLangAtCaret(PhpFileType.INSTANCE, base + "$q->setDQL('SELECT b FR<caret>OM \\Foo\\Bar b');", LANGUAGE_ID_DQL);
62+
assertInjectedLangAtCaret(PhpFileType.INSTANCE, base + "$q->setDQL('<caret>');", LANGUAGE_ID_DQL);
63+
64+
assertInjectedLangAtCaret(PhpFileType.INSTANCE, "<?php $dql = \"SELECT b FR<caret>OM \\Foo\\Bar b\");", LANGUAGE_ID_DQL);
65+
assertInjectedLangAtCaret(PhpFileType.INSTANCE, "<?php $dql = \"<caret>\");", LANGUAGE_ID_DQL);
66+
}
67+
68+
private void assertInjectedLangAtCaret(LanguageFileType fileType, String configureByText, String lang) {
69+
myFixture.configureByText(fileType, configureByText);
70+
injectionTestFixture.assertInjectedLangAtCaret(lang);
71+
}
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace Symfony\Component\DomCrawler {
4+
class Crawler
5+
{
6+
public function filter($str) {}
7+
public function children($str) {}
8+
public function filterXPath($str) {}
9+
public function evaluate($str) {}
10+
}
11+
}
12+
13+
namespace Symfony\Component\CssSelector {
14+
class CssSelectorConverter
15+
{
16+
public function toXPath($str) {}
17+
}
18+
}
19+
20+
namespace Symfony\Component\HttpFoundation {
21+
class JsonResponse {
22+
public static function fromJsonString() {}
23+
public function setJson() {}
24+
}
25+
}
26+
namespace Doctrine\ORM {
27+
class EntityManager {
28+
public function createQuery() {}
29+
}
30+
class Query {
31+
public function setDQL() {}
32+
}
33+
}
34+
35+
namespace Foo {
36+
class Bar
37+
{
38+
}
39+
}

0 commit comments

Comments
 (0)