Skip to content

Commit e27713e

Browse files
authored
Merge pull request #2400 from Haehnchen/feature/twig-types
#2396 add experimental support for extracting Twig "types" variables with types
2 parents e1dd5ae + 6276151 commit e27713e

File tree

3 files changed

+137
-6
lines changed

3 files changed

+137
-6
lines changed

src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/util/TwigTypeResolveUtil.java

+97-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package fr.adrienbrault.idea.symfony2plugin.templating.util;
22

3+
import com.intellij.lang.ASTNode;
34
import com.intellij.openapi.extensions.ExtensionPointName;
45
import com.intellij.openapi.project.Project;
56
import com.intellij.openapi.vfs.VirtualFile;
67
import com.intellij.patterns.PlatformPatterns;
78
import com.intellij.psi.PsiComment;
89
import com.intellij.psi.PsiElement;
910
import com.intellij.psi.PsiWhiteSpace;
11+
import com.intellij.psi.formatter.FormatterUtil;
1012
import com.intellij.psi.tree.IElementType;
1113
import com.intellij.psi.util.PsiTreeUtil;
1214
import com.jetbrains.php.PhpIndex;
@@ -35,6 +37,7 @@
3537
import fr.adrienbrault.idea.symfony2plugin.util.yaml.YamlHelper;
3638
import org.apache.commons.lang3.StringUtils;
3739
import org.jetbrains.annotations.NotNull;
40+
import org.jetbrains.annotations.Nullable;
3841

3942
import java.util.*;
4043
import java.util.regex.Matcher;
@@ -182,7 +185,10 @@ private static Map<String, String> findInlineStatementVariableDocBlock(@NotNull
182185
return variables;
183186
}
184187

185-
Map<String, String> inlineCommentDocsVars = getInlineCommentDocsVars(twigCompositeElement);
188+
Map<String, String> inlineCommentDocsVars = new HashMap<>() {{
189+
putAll(getInlineCommentDocsVars(twigCompositeElement));
190+
putAll(getTypesTagVars(twigCompositeElement));
191+
}};
186192

187193
// visit parent elements for extending scope
188194
if(nextParent) {
@@ -196,14 +202,21 @@ private static Map<String, String> findInlineStatementVariableDocBlock(@NotNull
196202
}
197203

198204
/**
199-
* Find file related doc blocks:
205+
* Find file related doc blocks or "types" tags:
200206
*
201-
* "@var foo \Foo"
207+
* - "@var foo \Foo"
208+
* - "{% types {...} %}"
202209
*/
203210
public static Map<String, String> findFileVariableDocBlock(@NotNull TwigFile twigFile) {
204-
return getInlineCommentDocsVars(twigFile);
211+
return new HashMap<>() {{
212+
putAll(getInlineCommentDocsVars(twigFile));
213+
putAll(getTypesTagVars(twigFile));
214+
}};
205215
}
206216

217+
/**
218+
* "@var foo \Foo"
219+
*/
207220
private static Map<String, String> getInlineCommentDocsVars(@NotNull PsiElement twigCompositeElement) {
208221
Map<String, String> variables = new HashMap<>();
209222

@@ -228,6 +241,86 @@ private static Map<String, String> getInlineCommentDocsVars(@NotNull PsiElement
228241
return variables;
229242
}
230243

244+
/**
245+
* {% types {...} %}
246+
*/
247+
private static Map<String, String> getTypesTagVars(@NotNull PsiElement twigFile) {
248+
Map<String, String> variables = new HashMap<>();
249+
250+
for (PsiElement psiComment: YamlHelper.getChildrenFix(twigFile)) {
251+
if (!(psiComment instanceof TwigCompositeElement) || psiComment.getNode().getElementType() != TwigElementTypes.TAG) {
252+
continue;
253+
}
254+
255+
PsiElement firstChild = psiComment.getFirstChild();
256+
if (firstChild == null) {
257+
continue;
258+
}
259+
260+
PsiElement tagName = PsiElementUtils.getNextSiblingAndSkip(firstChild, TwigTokenTypes.TAG_NAME);
261+
if (tagName == null || !"types".equals(tagName.getText())) {
262+
continue;
263+
}
264+
265+
ASTNode lbraceCurlPsi = FormatterUtil.getNextNonWhitespaceLeaf(tagName.getNode());
266+
if (lbraceCurlPsi == null || lbraceCurlPsi.getElementType() != TwigTokenTypes.LBRACE_CURL) {
267+
continue;
268+
}
269+
270+
ASTNode variableNamePsi = FormatterUtil.getNextNonWhitespaceLeaf(lbraceCurlPsi);
271+
if (variableNamePsi == null) {
272+
continue;
273+
}
274+
275+
if (variableNamePsi.getElementType() == TwigTokenTypes.IDENTIFIER) {
276+
String variableName = variableNamePsi.getText();
277+
if (!variableName.isBlank()) {
278+
variables.put(variableName, getTypesTagVarValue(variableNamePsi.getPsi()));
279+
}
280+
}
281+
282+
for (PsiElement commaPsi : PsiElementUtils.getNextSiblingOfTypes(variableNamePsi.getPsi(), PlatformPatterns.psiElement().withElementType(TwigTokenTypes.COMMA))) {
283+
ASTNode commaPsiNext = FormatterUtil.getNextNonWhitespaceLeaf(commaPsi.getNode());
284+
if (commaPsiNext != null && commaPsiNext.getElementType() == TwigTokenTypes.IDENTIFIER) {
285+
String variableName = commaPsiNext.getText();
286+
if (!variableName.isBlank()) {
287+
variables.put(variableName, getTypesTagVarValue(commaPsiNext.getPsi()));
288+
}
289+
}
290+
}
291+
}
292+
293+
return variables;
294+
}
295+
296+
/**
297+
* Find value tarting scope key:
298+
* - : 'foo'
299+
* - : "foo"
300+
*/
301+
@Nullable
302+
private static String getTypesTagVarValue(@NotNull PsiElement psiColon) {
303+
PsiElement filter = PsiElementUtils.getNextSiblingAndSkip(psiColon, TwigTokenTypes.STRING_TEXT, TwigTokenTypes.SINGLE_QUOTE, TwigTokenTypes.COLON, TwigTokenTypes.DOUBLE_QUOTE, TwigTokenTypes.QUESTION);
304+
if (filter == null) {
305+
return null;
306+
}
307+
308+
String type = PsiElementUtils.trimQuote(filter.getText());
309+
if (type.isBlank()) {
310+
return null;
311+
}
312+
313+
// secure value
314+
Matcher matcher = Pattern.compile("^(?<class>[\\w\\\\\\[\\]]+)$").matcher(type);
315+
if (matcher.find()) {
316+
// unescape: see also for Twig 4: https://github.com/twigphp/Twig/pull/4199
317+
return matcher.group("class").replace("\\\\", "\\");
318+
}
319+
320+
// unknown
321+
return "\\mixed";
322+
}
323+
231324
@NotNull
232325
public static Map<String, PsiVariable> collectScopeVariables(@NotNull PsiElement psiElement) {
233326
return collectScopeVariables(psiElement, new HashSet<>());

src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/collector/FileDocVariableCollector.java

+7-2
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,16 @@ public void collect(@NotNull TwigFileVariableCollectorParameter parameter, @NotN
2020
variables.putAll(convertHashMapToTypeSet(TwigTypeResolveUtil.findFileVariableDocBlock((TwigFile) parameter.getElement().getContainingFile())));
2121
}
2222

23-
private static Map<String, Set<String>> convertHashMapToTypeSet(Map<String, String> hashMap) {
23+
private static Map<String, Set<String>> convertHashMapToTypeSet(@NotNull Map<String, String> hashMap) {
2424
HashMap<String, Set<String>> globalVars = new HashMap<>();
2525

2626
for(final Map.Entry<String, String> entry: hashMap.entrySet()) {
27-
globalVars.put(entry.getKey(), new HashSet<>(Collections.singletonList(entry.getValue())));
27+
String value = entry.getValue();
28+
if (value != null) {
29+
globalVars.put(entry.getKey(), new HashSet<>(Collections.singletonList(value)));
30+
} else {
31+
globalVars.put(entry.getKey(), new HashSet<>());
32+
}
2833
}
2934

3035
return globalVars;

src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/util/TwigTypeResolveUtilTest.java

+33
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,39 @@ public void testFindFileVariableDocBlock() {
5353
assertEquals("\\AppBundle\\Entity\\MeterValueDTO", fileVariableDocBlock.get("foo_6"));
5454
}
5555

56+
/**
57+
* @see TwigTypeResolveUtil#findFileVariableDocBlock
58+
*/
59+
public void testFindFileTypeTag() {
60+
PsiFile fileFromText = PsiFileFactory.getInstance(getProject()).createFileFromText(TwigLanguage.INSTANCE, "" +
61+
"{% types {\n" +
62+
" is_correct: 'bool',\n" +
63+
" score: 'int',\n" +
64+
" foobar_1: 'array<int, App\\\\User>',\n" +
65+
" foobar_2?: '\\\\App\\\\User'," +
66+
" foobar_3: '\\\\App\\\\User[]'," +
67+
" foobar_4: '\\\\App\\\\User[]|\\User'," +
68+
" foobar_5: '',foobar_6: ''\r\n,\n\tfoobar_7:''\n\t\r," +
69+
"} %}" +
70+
"\n"
71+
);
72+
73+
Map<String, String> fileVariableDocBlock = TwigTypeResolveUtil.findFileVariableDocBlock((TwigFile) fileFromText);
74+
assertEquals("bool", fileVariableDocBlock.get("is_correct"));
75+
assertEquals("int", fileVariableDocBlock.get("score"));
76+
assertNull(fileVariableDocBlock.get("foobar_5"));
77+
78+
assertEquals("\\App\\User", fileVariableDocBlock.get("foobar_2"));
79+
assertEquals("\\App\\User[]", fileVariableDocBlock.get("foobar_3"));
80+
81+
// maybe resolve this
82+
assertEquals("\\mixed", fileVariableDocBlock.get("foobar_1"));
83+
assertEquals("\\mixed", fileVariableDocBlock.get("foobar_4"));
84+
85+
assertNull(fileVariableDocBlock.get("foobar_6"));
86+
assertNull(fileVariableDocBlock.get("foobar_7"));
87+
}
88+
5689
public void testReqExForInlineDocVariables() {
5790
assertMatches("@var foo_1 \\AppBundle\\Entity\\MeterValueDTO", TwigTypeResolveUtil.DOC_TYPE_PATTERN_SINGLE);
5891
assertMatches("@var \\AppBundle\\Entity\\MeterValueDTO foo_1", TwigTypeResolveUtil.DOC_TYPE_PATTERN_SINGLE);

0 commit comments

Comments
 (0)