From 4fa4e954ebc2320d0300e9cf000fab5c2d7f5410 Mon Sep 17 00:00:00 2001 From: Daniel Espendiller Date: Wed, 12 Aug 2020 18:14:49 +0200 Subject: [PATCH] provide Twig type for "loop" variable inside "foreach" --- .../templating/util/TwigTypeResolveUtil.java | 4 ++ .../variable/TwigTypeContainer.java | 5 +++ .../resolver/ForEachLoopResolver.java | 34 ++++++++++++++++ .../util/TwigTypeResolveUtilTest.java | 39 +++++++++++++++++++ 4 files changed, 82 insertions(+) create mode 100644 src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/resolver/ForEachLoopResolver.java diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/util/TwigTypeResolveUtil.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/util/TwigTypeResolveUtil.java index e4943d0f6..5c17b220f 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/util/TwigTypeResolveUtil.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/util/TwigTypeResolveUtil.java @@ -25,6 +25,7 @@ import fr.adrienbrault.idea.symfony2plugin.templating.variable.TwigTypeContainer; import fr.adrienbrault.idea.symfony2plugin.templating.variable.collector.StaticVariableCollector; import fr.adrienbrault.idea.symfony2plugin.templating.variable.dict.PsiVariable; +import fr.adrienbrault.idea.symfony2plugin.templating.variable.resolver.ForEachLoopResolver; import fr.adrienbrault.idea.symfony2plugin.templating.variable.resolver.FormFieldResolver; import fr.adrienbrault.idea.symfony2plugin.templating.variable.resolver.FormVarsResolver; import fr.adrienbrault.idea.symfony2plugin.templating.variable.resolver.TwigTypeResolver; @@ -83,6 +84,7 @@ public class TwigTypeResolveUtil { private static TwigTypeResolver[] TWIG_TYPE_RESOLVERS = new TwigTypeResolver[] { new FormVarsResolver(), new FormFieldResolver(), + new ForEachLoopResolver(), }; @NotNull @@ -366,6 +368,8 @@ private static void collectForArrayScopeVariables(@NotNull PsiElement psiElement return; } + globalVars.put("loop", new PsiVariable("\\loop")); + // {% for user in "users" %} PsiElement forTag = twigCompositeElement.getFirstChild(); PsiElement inVariable = PsiElementUtils.getChildrenOfType(forTag, TwigPattern.getForTagInVariablePattern()); diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/TwigTypeContainer.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/TwigTypeContainer.java index 772580820..7202649b5 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/TwigTypeContainer.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/TwigTypeContainer.java @@ -47,6 +47,11 @@ public static Collection fromCollection(Project project, Coll if(phpClass.size() > 0) { twigTypeContainerList.add(new TwigTypeContainer(phpClass.iterator().next())); } + + // inside {% for ... %}{% endfor %} we have a "loop" var; fake an internal type here + if (phpNamedElement.getTypes().contains("\\loop")) { + twigTypeContainerList.add(new TwigTypeContainer("\\loop")); + } } return twigTypeContainerList; diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/resolver/ForEachLoopResolver.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/resolver/ForEachLoopResolver.java new file mode 100644 index 000000000..fe28e3996 --- /dev/null +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/resolver/ForEachLoopResolver.java @@ -0,0 +1,34 @@ +package fr.adrienbrault.idea.symfony2plugin.templating.variable.resolver; + +import fr.adrienbrault.idea.symfony2plugin.templating.variable.TwigTypeContainer; +import fr.adrienbrault.idea.symfony2plugin.templating.variable.dict.PsiVariable; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.List; + +/** + * Provide the loop.XXX types inside foreach block if previous element is named loop with the type "\loop" {% for ... %}{% endfor %} + * + * @author Daniel Espendiller + */ +public class ForEachLoopResolver implements TwigTypeResolver { + + /** + * @link https://twig.symfony.com/doc/3.x/tags/for.html#the-loop-variable + */ + public static final String[] LOOP_VARIABLES = { + "index", "index0", "revindex", "revindex0", "first", "last", "length", "parent" + }; + + @Override + public void resolve(Collection targets, @Nullable Collection previousElement, String typeName, Collection> previousElements, @Nullable Collection psiVariables) { + if (previousElement == null || previousElement.stream().noneMatch(p -> "\\loop".equals(p.getStringElement()))) { + return; + } + + for(String string: LOOP_VARIABLES) { + targets.add(new TwigTypeContainer(string)); + } + } +} diff --git a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/util/TwigTypeResolveUtilTest.java b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/util/TwigTypeResolveUtilTest.java index 78e22401e..761cfec23 100644 --- a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/util/TwigTypeResolveUtilTest.java +++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/util/TwigTypeResolveUtilTest.java @@ -9,11 +9,15 @@ import com.jetbrains.twig.TwigFileType; import com.jetbrains.twig.TwigLanguage; import fr.adrienbrault.idea.symfony2plugin.templating.util.TwigTypeResolveUtil; +import fr.adrienbrault.idea.symfony2plugin.templating.variable.TwigTypeContainer; import fr.adrienbrault.idea.symfony2plugin.templating.variable.dict.PsiVariable; import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase; import org.jetbrains.annotations.NotNull; +import java.util.Collection; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; /** * @author Daniel Espendiller @@ -84,6 +88,41 @@ public void testCollectScopeVariables() { assertContainsElements(stringPsiVariableMap.get("b").getTypes(), "\\Foo\\Bar"); } + /** + * @see TwigTypeResolveUtil#collectScopeVariables + */ + public void testForeachMustProvideLoopVariable() { + myFixture.configureByText(TwigFileType.INSTANCE, + "{% for foo in foo %}" + + "{{ }}" + + "{% endfor %}" + ); + + PsiElement psiElement = myFixture.getFile().findElementAt(myFixture.getCaretOffset()); + + Map stringPsiVariableMap = TwigTypeResolveUtil.collectScopeVariables(psiElement); + assertContainsElements(stringPsiVariableMap.keySet(), "loop"); + } + + /** + * @see TwigTypeResolveUtil#resolveTwigMethodName + */ + public void testForeachMustProvideLoopTypeVariable() { + myFixture.configureByText(TwigFileType.INSTANCE, + "{% for foo in foo %}" + + "{{ loop.i }}" + + "{% endfor %}" + ); + + PsiElement psiElement = myFixture.getFile().findElementAt(myFixture.getCaretOffset()); + Collection beforeLeaf = TwigTypeResolveUtil.formatPsiTypeName(psiElement); + + Collection stringPsiVariableMap = TwigTypeResolveUtil.resolveTwigMethodName(psiElement.getPrevSibling(), beforeLeaf); + + Set types = stringPsiVariableMap.stream().map(TwigTypeContainer::getStringElement).collect(Collectors.toSet()); + assertContainsElements(types, "last", "index", "index0"); + } + private void assertMatches(@NotNull String content, @NotNull String... regularExpressions) { for (String regularExpression : regularExpressions) { if(content.matches(regularExpression)) {