Skip to content

Commit

Permalink
Merge pull request #2400 from Haehnchen/feature/twig-types
Browse files Browse the repository at this point in the history
#2396 add experimental support for extracting Twig "types" variables with types
  • Loading branch information
Haehnchen authored Sep 17, 2024
2 parents e1dd5ae + 6276151 commit e27713e
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package fr.adrienbrault.idea.symfony2plugin.templating.util;

import com.intellij.lang.ASTNode;
import com.intellij.openapi.extensions.ExtensionPointName;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.PsiComment;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.formatter.FormatterUtil;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PsiTreeUtil;
import com.jetbrains.php.PhpIndex;
Expand Down Expand Up @@ -35,6 +37,7 @@
import fr.adrienbrault.idea.symfony2plugin.util.yaml.YamlHelper;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;
import java.util.regex.Matcher;
Expand Down Expand Up @@ -182,7 +185,10 @@ private static Map<String, String> findInlineStatementVariableDocBlock(@NotNull
return variables;
}

Map<String, String> inlineCommentDocsVars = getInlineCommentDocsVars(twigCompositeElement);
Map<String, String> inlineCommentDocsVars = new HashMap<>() {{
putAll(getInlineCommentDocsVars(twigCompositeElement));
putAll(getTypesTagVars(twigCompositeElement));
}};

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

/**
* Find file related doc blocks:
* Find file related doc blocks or "types" tags:
*
* "@var foo \Foo"
* - "@var foo \Foo"
* - "{% types {...} %}"
*/
public static Map<String, String> findFileVariableDocBlock(@NotNull TwigFile twigFile) {
return getInlineCommentDocsVars(twigFile);
return new HashMap<>() {{
putAll(getInlineCommentDocsVars(twigFile));
putAll(getTypesTagVars(twigFile));
}};
}

/**
* "@var foo \Foo"
*/
private static Map<String, String> getInlineCommentDocsVars(@NotNull PsiElement twigCompositeElement) {
Map<String, String> variables = new HashMap<>();

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

/**
* {% types {...} %}
*/
private static Map<String, String> getTypesTagVars(@NotNull PsiElement twigFile) {
Map<String, String> variables = new HashMap<>();

for (PsiElement psiComment: YamlHelper.getChildrenFix(twigFile)) {
if (!(psiComment instanceof TwigCompositeElement) || psiComment.getNode().getElementType() != TwigElementTypes.TAG) {
continue;
}

PsiElement firstChild = psiComment.getFirstChild();
if (firstChild == null) {
continue;
}

PsiElement tagName = PsiElementUtils.getNextSiblingAndSkip(firstChild, TwigTokenTypes.TAG_NAME);
if (tagName == null || !"types".equals(tagName.getText())) {
continue;
}

ASTNode lbraceCurlPsi = FormatterUtil.getNextNonWhitespaceLeaf(tagName.getNode());
if (lbraceCurlPsi == null || lbraceCurlPsi.getElementType() != TwigTokenTypes.LBRACE_CURL) {
continue;
}

ASTNode variableNamePsi = FormatterUtil.getNextNonWhitespaceLeaf(lbraceCurlPsi);
if (variableNamePsi == null) {
continue;
}

if (variableNamePsi.getElementType() == TwigTokenTypes.IDENTIFIER) {
String variableName = variableNamePsi.getText();
if (!variableName.isBlank()) {
variables.put(variableName, getTypesTagVarValue(variableNamePsi.getPsi()));
}
}

for (PsiElement commaPsi : PsiElementUtils.getNextSiblingOfTypes(variableNamePsi.getPsi(), PlatformPatterns.psiElement().withElementType(TwigTokenTypes.COMMA))) {
ASTNode commaPsiNext = FormatterUtil.getNextNonWhitespaceLeaf(commaPsi.getNode());
if (commaPsiNext != null && commaPsiNext.getElementType() == TwigTokenTypes.IDENTIFIER) {
String variableName = commaPsiNext.getText();
if (!variableName.isBlank()) {
variables.put(variableName, getTypesTagVarValue(commaPsiNext.getPsi()));
}
}
}
}

return variables;
}

/**
* Find value tarting scope key:
* - : 'foo'
* - : "foo"
*/
@Nullable
private static String getTypesTagVarValue(@NotNull PsiElement psiColon) {
PsiElement filter = PsiElementUtils.getNextSiblingAndSkip(psiColon, TwigTokenTypes.STRING_TEXT, TwigTokenTypes.SINGLE_QUOTE, TwigTokenTypes.COLON, TwigTokenTypes.DOUBLE_QUOTE, TwigTokenTypes.QUESTION);
if (filter == null) {
return null;
}

String type = PsiElementUtils.trimQuote(filter.getText());
if (type.isBlank()) {
return null;
}

// secure value
Matcher matcher = Pattern.compile("^(?<class>[\\w\\\\\\[\\]]+)$").matcher(type);
if (matcher.find()) {
// unescape: see also for Twig 4: https://github.com/twigphp/Twig/pull/4199
return matcher.group("class").replace("\\\\", "\\");
}

// unknown
return "\\mixed";
}

@NotNull
public static Map<String, PsiVariable> collectScopeVariables(@NotNull PsiElement psiElement) {
return collectScopeVariables(psiElement, new HashSet<>());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,16 @@ public void collect(@NotNull TwigFileVariableCollectorParameter parameter, @NotN
variables.putAll(convertHashMapToTypeSet(TwigTypeResolveUtil.findFileVariableDocBlock((TwigFile) parameter.getElement().getContainingFile())));
}

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

for(final Map.Entry<String, String> entry: hashMap.entrySet()) {
globalVars.put(entry.getKey(), new HashSet<>(Collections.singletonList(entry.getValue())));
String value = entry.getValue();
if (value != null) {
globalVars.put(entry.getKey(), new HashSet<>(Collections.singletonList(value)));
} else {
globalVars.put(entry.getKey(), new HashSet<>());
}
}

return globalVars;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,39 @@ public void testFindFileVariableDocBlock() {
assertEquals("\\AppBundle\\Entity\\MeterValueDTO", fileVariableDocBlock.get("foo_6"));
}

/**
* @see TwigTypeResolveUtil#findFileVariableDocBlock
*/
public void testFindFileTypeTag() {
PsiFile fileFromText = PsiFileFactory.getInstance(getProject()).createFileFromText(TwigLanguage.INSTANCE, "" +
"{% types {\n" +
" is_correct: 'bool',\n" +
" score: 'int',\n" +
" foobar_1: 'array<int, App\\\\User>',\n" +
" foobar_2?: '\\\\App\\\\User'," +
" foobar_3: '\\\\App\\\\User[]'," +
" foobar_4: '\\\\App\\\\User[]|\\User'," +
" foobar_5: '',foobar_6: ''\r\n,\n\tfoobar_7:''\n\t\r," +
"} %}" +
"\n"
);

Map<String, String> fileVariableDocBlock = TwigTypeResolveUtil.findFileVariableDocBlock((TwigFile) fileFromText);
assertEquals("bool", fileVariableDocBlock.get("is_correct"));
assertEquals("int", fileVariableDocBlock.get("score"));
assertNull(fileVariableDocBlock.get("foobar_5"));

assertEquals("\\App\\User", fileVariableDocBlock.get("foobar_2"));
assertEquals("\\App\\User[]", fileVariableDocBlock.get("foobar_3"));

// maybe resolve this
assertEquals("\\mixed", fileVariableDocBlock.get("foobar_1"));
assertEquals("\\mixed", fileVariableDocBlock.get("foobar_4"));

assertNull(fileVariableDocBlock.get("foobar_6"));
assertNull(fileVariableDocBlock.get("foobar_7"));
}

public void testReqExForInlineDocVariables() {
assertMatches("@var foo_1 \\AppBundle\\Entity\\MeterValueDTO", TwigTypeResolveUtil.DOC_TYPE_PATTERN_SINGLE);
assertMatches("@var \\AppBundle\\Entity\\MeterValueDTO foo_1", TwigTypeResolveUtil.DOC_TYPE_PATTERN_SINGLE);
Expand Down

0 comments on commit e27713e

Please sign in to comment.