Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expression Language support #1654

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
51481c8
Impl. Expression Language Parser
adamwojs Jul 3, 2021
88983da
Impl. Syntax Highlighter for Expression Language
adamwojs Jul 3, 2021
e8a2df7
Impl. Brace Matcher for Expression Language
adamwojs Jul 3, 2021
17407b9
Impl. Quote Handler for Expression Language
adamwojs Jul 3, 2021
aac5182
Impl. Expression Language Injections for XML
adamwojs Jul 3, 2021
7195971
Impl. Expression Language Injections for YAML
adamwojs Jul 3, 2021
717ac6e
Impl. Expression Language Injections for PHP
adamwojs Jul 3, 2021
d8d37b5
Registered extensions in plugin.xml
adamwojs Jul 3, 2021
e405591
Impl. Expression Language Parser (tests)
adamwojs Jul 3, 2021
378ade2
Impl. Expression Language Injections for XML (tests)
adamwojs Jul 3, 2021
16d3204
Impl. Expression Language Injections for YAML (tests)
adamwojs Jul 3, 2021
4488c35
Impl. Expression Language Injections for PHP (tests)
adamwojs Jul 3, 2021
0f1d4a5
Deprecated ParameterLanguageInjector in favour of StringLiteralLangua…
adamwojs Jul 3, 2021
91b70a9
fixup! Impl. Expression Language Injections for PHP
adamwojs Jul 8, 2021
61906bb
fixup! Impl. Expression Language Injections for PHP (tests)
adamwojs Jul 8, 2021
ba45a7c
Added null check in YamlHelper.{isRoutingFile,isConfigFile,isServices…
adamwojs Sep 18, 2021
daca2db
Extracted literals to own PSI nodes
adamwojs Sep 18, 2021
e5b4001
Rebased and fixed conflicts
adamwojs May 22, 2022
1d37466
Removed unused EOL macro
adamwojs May 22, 2022
a8df36c
fixup! Rebased and fixed conflicts
adamwojs May 22, 2022
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ out/
/.idea/misc.xml
/.gradle
/build
/src/main/gen
27 changes: 27 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ plugins {
id("org.jetbrains.changelog") version "1.3.1"
// Gradle Qodana Plugin
id("org.jetbrains.qodana") version "0.1.13"
// Gradle Grammar-Kit Plugin
id("org.jetbrains.grammarkit") version "2021.2.2"
}

group = properties("pluginGroup")
Expand Down Expand Up @@ -112,4 +114,29 @@ tasks {
includeEngines("junit-vintage")
}
}

generateLexer {
source.set("src/main/java/fr/adrienbrault/idea/symfony2plugin/expressionLanguage/ExpressionLanguage.flex")
targetDir.set("src/main/gen/fr/adrienbrault/idea/symfony2plugin/expressionLanguage/")
targetClass.set("ExpressionLanguageLexer")
purgeOldFiles.set(true)
}

generateParser {
source.set("src/main/java/fr/adrienbrault/idea/symfony2plugin/expressionLanguage/ExpressionLanguage.bnf")
targetRoot.set("src/main/gen")
pathToParser.set("fr/adrienbrault/idea/symfony2plugin/expressionLanguage/ExpressionLanguageParser.java")
pathToPsiRoot.set("fr/adrienbrault/idea/symfony2plugin/expressionLanguage/psi")
purgeOldFiles.set(true)
}

compileJava {
dependsOn("generateLexer")
dependsOn("generateParser")
}
}

java.sourceSets["main"].java {
// Include the generated files in the source set
srcDir("src/main/gen")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package fr.adrienbrault.idea.symfony2plugin.config.xml;

import com.intellij.lang.injection.MultiHostInjector;
import com.intellij.lang.injection.MultiHostRegistrar;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.patterns.XmlElementPattern;
import com.intellij.patterns.XmlPatterns;
import com.intellij.psi.ElementManipulators;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiLanguageInjectionHost;
import com.intellij.psi.xml.XmlText;
import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent;
import fr.adrienbrault.idea.symfony2plugin.expressionLanguage.ExpressionLanguage;
import org.jetbrains.annotations.NotNull;

import java.util.Collections;
import java.util.List;

public class XmlLanguageInjector implements MultiHostInjector {

@Override
public void getLanguagesToInject(@NotNull MultiHostRegistrar registrar, @NotNull PsiElement context) {
if (!Symfony2ProjectComponent.isEnabled(context.getProject())) {
return;
}

if (isExpressionLanguageString(context)) {
registrar
.startInjecting(ExpressionLanguage.INSTANCE)
.addPlace(null, null, (PsiLanguageInjectionHost) context, ElementManipulators.getValueTextRange(context))
.doneInjecting();
}
}

@Override
public @NotNull List<? extends Class<? extends PsiElement>> elementsToInjectIn() {
return Collections.singletonList(XmlText.class);
}

private boolean isExpressionLanguageString(@NotNull PsiElement element) {
return PlatformPatterns.or(
getExpressionArgumentPattern(),
getRouteConditionPattern()
).accepts(element);
}

/**
* <argument type="expression">container.get('service_id')</argument>
*/
private XmlElementPattern.XmlTextPattern getExpressionArgumentPattern() {
return XmlPatterns
.xmlText()
.withParent(XmlPatterns
.xmlTag()
.withName("argument")
.withAttributeValue("type", "expression")
)
.inside(
XmlHelper.getInsideTagPattern("services")
).inFile(XmlHelper.getXmlFilePattern());
}

/**
* <routes ...>
* <route ...>
* <condition>context.getMethod() in ['GET', 'HEAD']</condition>
* </route>
* </routes>
*/
private XmlElementPattern.XmlTextPattern getRouteConditionPattern() {
return XmlPatterns
.xmlText()
.withParent(XmlPatterns
.xmlTag()
.withName("condition")
.withParent(XmlPatterns
.xmlTag()
.withName("route")
.withParent(XmlPatterns
.xmlTag()
.withName("routes")
)
)
)
.inFile(XmlHelper.getXmlFilePattern());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package fr.adrienbrault.idea.symfony2plugin.config.yaml;

import com.intellij.lang.injection.MultiHostInjector;
import com.intellij.lang.injection.MultiHostRegistrar;
import com.intellij.openapi.util.TextRange;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.ElementManipulators;
import com.intellij.psi.PsiElement;
import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent;
import fr.adrienbrault.idea.symfony2plugin.expressionLanguage.ExpressionLanguage;
import fr.adrienbrault.idea.symfony2plugin.util.yaml.YamlHelper;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.yaml.psi.YAMLPsiElement;
import org.jetbrains.yaml.psi.YAMLQuotedText;

import java.util.Collections;
import java.util.List;

public class YamlLanguageInjector implements MultiHostInjector {

private static final String EXPRESSION_LANGUAGE_PREFIX = "@=";

@Override
public void getLanguagesToInject(@NotNull MultiHostRegistrar registrar, @NotNull PsiElement context) {
if (!Symfony2ProjectComponent.isEnabled(context.getProject())) {
return;
}

if (!(context instanceof YAMLQuotedText)) {
return;
}

var file = context.getContainingFile();
if (file == null) {
return;
}

var element = (YAMLQuotedText) context;
var value = element.getTextValue();

if (YamlHelper.isServicesFile(file) && isExpressionLanguageString(value) && isExpressionLanguageStringAllowed(element)) {
registrar
.startInjecting(ExpressionLanguage.INSTANCE)
.addPlace(null, null, element, getExpressionLanguageTextRange(value))
.doneInjecting();
} else if (YamlHelper.isRoutingFile(file) && isInsideRouteConditionKey(element)) {
registrar
.startInjecting(ExpressionLanguage.INSTANCE)
.addPlace(null, null, element, ElementManipulators.getValueTextRange(element))
.doneInjecting();
}
}

@NotNull
@Override
public List<? extends Class<? extends PsiElement>> elementsToInjectIn() {
return Collections.singletonList(YAMLQuotedText.class);
}

private boolean isExpressionLanguageString(@NotNull String str) {
return str.startsWith(EXPRESSION_LANGUAGE_PREFIX) && str.length() > EXPRESSION_LANGUAGE_PREFIX.length();
}

private boolean isExpressionLanguageStringAllowed(@NotNull YAMLPsiElement element) {
return PlatformPatterns.and(
YamlElementPatternHelper.getInsideKeyValue("services"),
YamlElementPatternHelper.getInsideKeyValue("arguments", "properties", "calls", "configurator")
).accepts(element);
}

@NotNull
private TextRange getExpressionLanguageTextRange(@NotNull String str) {
return new TextRange(EXPRESSION_LANGUAGE_PREFIX.length() + 1, str.length() + 1);
}

private boolean isInsideRouteConditionKey(@NotNull YAMLPsiElement element) {
return YamlElementPatternHelper.getInsideKeyValue("condition").accepts(element);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
{
parserClass="fr.adrienbrault.idea.symfony2plugin.expressionLanguage.ExpressionLanguageParser"

extends="com.intellij.extapi.psi.ASTWrapperPsiElement"

psiClassPrefix="ExpressionLanguage"
psiImplClassSuffix="Impl"
psiPackage="fr.adrienbrault.idea.symfony2plugin.expressionLanguage.psi"
psiImplPackage="fr.adrienbrault.idea.symfony2plugin.expressionLanguage.psi.impl"

elementTypeHolderClass="fr.adrienbrault.idea.symfony2plugin.expressionLanguage.psi.ExpressionLanguageTypes"
elementTypeClass="fr.adrienbrault.idea.symfony2plugin.expressionLanguage.psi.ExpressionLanguageElementType"
tokenTypeClass="fr.adrienbrault.idea.symfony2plugin.expressionLanguage.psi.ExpressionLanguageTokenType"

extends(".*expr")=expr
tokens=[
space='regexp:\s+'
number='regexp:\d+(\.\d*)?([Ee][+\-]\d+)?'
null="regexp:NULL|null"
true="regexp:TRUE|true"
false="regexp:FALSE|false"
id='regexp:\p{Alpha}\w*'
string="regexp:('([^'\\]|\\.)*'|\"([^\"\\]|\\.)*\")"
OP_OR="||"
OP_OR_KW="or"
OP_AND="&&"
OP_AND_KW="and"
OP_BIT_OR="|"
OP_BIT_XOR="^"
OP_BIT_AND="&"
OP_IDENTICAL="==="
OP_EQ="=="
OP_NOT_IDENTICAL="!=="
OP_NEQ="!="
OP_LT="<"
OP_GT=">"
OP_GTE=">="
OP_LTE="<="
OP_NOT_IN="not in"
OP_IN="in"
OP_MATCHES="matches"
OP_RANGE=".."
OP_PLUS="+"
OP_MINUS="-"
OP_CONCAT="~"
OP_MUL="*"
OP_DIV="/"
OP_MOD="%"
OP_POW="**"
OP_NOT='!'
OP_NOT_KW='not'

L_ROUND_BRACKET="("
R_ROUND_BRACKET=")"
L_CURLY_BRACKET="{"
R_CURLY_BRACKET="}"
L_SQUARE_BRACKET="["
R_SQUARE_BRACKET="]"

syntax='regexp:[?:.,]'
]
}

root ::= expr

expr ::= ternary_group
| or_expr
| and_expr
| bit_or_expr
| bit_xor_expr
| bit_and_expr
| rel_group
| range_expr
| add_group
| concat_expr
| mul_group
| not_expr
| exp_expr
| sign_group
| qualification_expr
| array_access_expr
| primary_group

private sign_group ::= unary_plus_expr | unary_min_expr
private mul_group ::= mul_expr | div_expr | mod_expr
private add_group ::= plus_expr | minus_expr
private ternary_group ::= elvis_expr | ternary_expr

private rel_group ::= identical_expr
| eq_expr
| not_identical_expr
| neq_expr
| gte_expr
| gt_expr
| lt_expr
| lte_expr
| not_in_expr
| in_expr
| matches_expr

private primary_group ::= simple_ref_expr | literal_expr | array_expr | hash_expr | call_expr | paren_expr

not_expr ::= (OP_NOT|OP_NOT_KW) expr
unary_min_expr ::= OP_MINUS expr
unary_plus_expr ::= OP_PLUS expr
div_expr ::= expr OP_DIV expr
mul_expr ::= expr OP_MUL expr
mod_expr ::= expr OP_MOD expr
concat_expr ::= expr OP_CONCAT expr
minus_expr ::= expr OP_MINUS expr
plus_expr ::= expr OP_PLUS expr
range_expr ::= expr OP_RANGE expr
identical_expr ::= expr OP_IDENTICAL expr
not_identical_expr ::= expr OP_NOT_IDENTICAL expr
eq_expr ::= expr OP_EQ expr
neq_expr ::= expr OP_NEQ expr
gt_expr ::= expr OP_GT expr
gte_expr ::= expr OP_GTE expr
lt_expr ::= expr OP_LT expr
lte_expr ::= expr OP_LTE expr
not_in_expr ::= expr OP_NOT_IN expr
in_expr ::= expr OP_IN expr
matches_expr ::= expr OP_MATCHES expr
or_expr ::= expr (OP_OR|OP_OR_KW) expr
and_expr ::= expr (OP_AND|OP_AND_KW) expr
bit_and_expr ::= expr OP_BIT_AND expr
bit_or_expr ::= expr OP_BIT_OR expr
bit_xor_expr ::= expr OP_BIT_XOR expr
exp_expr ::= expr (OP_POW expr)+
paren_expr ::= L_ROUND_BRACKET expr R_ROUND_BRACKET
ternary_expr ::= expr '?' expr (':' expr)?
elvis_expr ::= expr '?:' expr

fake ref_expr ::= expr? '.' identifier
simple_ref_expr ::= identifier {extends=ref_expr elementType=ref_expr}
qualification_expr ::= expr '.' identifier {extends=ref_expr elementType=ref_expr}
array_access_expr ::= expr L_SQUARE_BRACKET expr R_SQUARE_BRACKET {extends=ref_expr elementType=ref_expr}

literal_expr ::= number_literal | string_literal | boolean_literal | null_literal
string_literal ::= string
number_literal ::= number
boolean_literal ::= true | false
null_literal ::= null

array_expr ::= L_SQUARE_BRACKET expr_list? R_SQUARE_BRACKET
hash_expr ::= L_CURLY_BRACKET hash_entries? R_CURLY_BRACKET

call_expr ::= expr L_ROUND_BRACKET expr_list? R_ROUND_BRACKET

private hash_entries ::= hash_entry (',' hash_entry)*
private hash_entry ::= identifier ':' expr
private expr_list ::= expr (',' expr_list)*

identifier ::= id
Loading