Skip to content

add support for Twig extension registered via Attributes #2433

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package fr.adrienbrault.idea.symfony2plugin.stubs.indexes;

import com.intellij.util.indexing.*;
import com.intellij.util.io.DataExternalizer;
import com.intellij.util.io.EnumeratorStringDescriptor;
import com.intellij.util.io.KeyDescriptor;
import com.jetbrains.php.lang.PhpFileType;
import com.jetbrains.php.lang.psi.PhpFile;
import com.jetbrains.php.lang.psi.PhpPsiUtil;
import com.jetbrains.php.lang.psi.elements.*;
import fr.adrienbrault.idea.symfony2plugin.util.PhpPsiAttributesUtil;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;

import java.util.HashMap;
import java.util.Map;

/**
* @author Daniel Espendiller <[email protected]>
*/
public class TwigAttributeIndex extends FileBasedIndexExtension<String, String> {
public static final ID<String, String> KEY = ID.create("fr.adrienbrault.idea.symfony2plugin.twig_attribute_extension.index");

@Override
public @NotNull ID<String, String> getName() {
return KEY;
}

@Override
public @NotNull DataIndexer<String, String, FileContent> getIndexer() {
return new TwigAttributeIndexer();
}

@Override
public @NotNull KeyDescriptor<String> getKeyDescriptor() {
return EnumeratorStringDescriptor.INSTANCE;
}

@Override
public @NotNull DataExternalizer<String> getValueExternalizer() {
return EnumeratorStringDescriptor.INSTANCE;
}

@Override
public @NotNull FileBasedIndex.InputFilter getInputFilter() {
return virtualFile -> virtualFile.getFileType() == PhpFileType.INSTANCE;
}

@Override
public boolean dependsOnFileContent() {
return true;
}

@Override
public int getVersion() {
return 1;
}

public static class TwigAttributeIndexer implements DataIndexer<String, String, FileContent> {
private static final String[] SUPPORTED_ATTRIBUTES = {
"\\Twig\\Attribute\\AsTwigFilter",
"\\Twig\\Attribute\\AsTwigFunction",
"\\Twig\\Attribute\\AsTwigTest",
};

@Override
public @NotNull Map<String, String> map(@NotNull FileContent inputData) {
Map<String, String> result = new HashMap<>();
if (!(inputData.getPsiFile() instanceof PhpFile phpFile)) {
return result;
}

for (PhpClass phpClass : PhpPsiUtil.findAllClasses(phpFile)) {
for (Method method : phpClass.getMethods()) {
processMethodAttributes(phpClass, method, result);
}
}

return result;
}

private void processMethodAttributes(@NotNull PhpClass phpClass, @NotNull Method method, @NotNull Map<String, String> result) {
for (PhpAttribute attribute : method.getAttributes()) {
String attributeFqn = getAttributeFqn(attribute);
if (attributeFqn == null) {
continue;
}

for (String supportedAttr : SUPPORTED_ATTRIBUTES) {
if (supportedAttr.equals(attributeFqn)) {
String nameAttribute = PhpPsiAttributesUtil.getAttributeValueByNameAsString(attribute, 0, "name");
if (nameAttribute != null) {
result.put(nameAttribute, String.format("#M#C\\%s.%s", StringUtils.stripStart(phpClass.getFQN(), "\\"), method.getName()));
}

break;
}
}
}
}

private String getAttributeFqn(@NotNull PhpAttribute attribute) {
ClassReference reference = attribute.getClassReference();
return reference != null ? reference.getFQN() : null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ public static void forceReindex() {
TwigIncludeStubIndex.KEY,
TwigMacroFunctionStubIndex.KEY,
TranslationStubIndex.KEY,
TwigBlockIndexExtension.KEY
TwigBlockIndexExtension.KEY,
TwigAttributeIndex.KEY
};

for(ID<?,?> id: indexIds) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@
import com.intellij.openapi.util.Key;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.PsiElement;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.*;
import com.jetbrains.php.PhpIcons;
import com.jetbrains.php.PhpIndex;
import com.jetbrains.php.lang.PhpFileType;
import com.jetbrains.php.lang.parser.PhpElementTypes;
import com.jetbrains.php.lang.psi.PhpPsiUtil;
import com.jetbrains.php.lang.psi.elements.*;
import fr.adrienbrault.idea.symfony2plugin.stubs.cache.FileIndexCaches;
import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.TwigAttributeIndex;
import fr.adrienbrault.idea.symfony2plugin.templating.dict.TwigExtension;
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils;
Expand All @@ -30,6 +34,15 @@ public class TwigExtensionParser {
private static final Key<CachedValue<Map<String, TwigExtension>>> FILTERS_CACHE = new Key<>("TWIG_EXTENSIONS_FILTERS");
private static final Key<CachedValue<Map<String, TwigExtension>>> OPERATORS_CACHE = new Key<>("TWIG_EXTENSIONS_OPERATORS");

private static final Key<CachedValue<Map<String, List<String>>>> TWIG_ATTRIBUTE_FUNCTION_INDEX = new Key<>("TWIG_ATTRIBUTE_FUNCTION_INDEX");
private static final Key<CachedValue<Set<String>>> TWIG_ATTRIBUTE_FUNCTION_INDEX_NAMES = new Key<>("TWIG_ATTRIBUTE_FUNCTION_INDEX_NAMES");

private static final Key<CachedValue<Map<String, List<String>>>> TWIG_ATTRIBUTE_FILTER_INDEX = new Key<>("TWIG_ATTRIBUTE_FILTER_INDEX");
private static final Key<CachedValue<Set<String>>> TWIG_ATTRIBUTE_FILTER_NAMES = new Key<>("TWIG_ATTRIBUTE_FILTER_NAMES");

private static final Key<CachedValue<Map<String, List<String>>>> TWIG_ATTRIBUTE_TEST_INDEX = new Key<>("TWIG_ATTRIBUTE_TEST_INDEX");
private static final Key<CachedValue<Set<String>>> TWIG_ATTRIBUTE_TEST_NAMES = new Key<>("TWIG_ATTRIBUTE_FUNCTION_FILTER_NAMES");

public enum TwigExtensionType {
FUNCTION_METHOD, FUNCTION_NODE, SIMPLE_FUNCTION, FILTER, SIMPLE_TEST, OPERATOR
}
Expand All @@ -39,7 +52,7 @@ public static Map<String, TwigExtension> getFunctions(@NotNull Project project)
return CachedValuesManager.getManager(project).getCachedValue(
project,
FUNCTION_CACHE,
() -> CachedValueProvider.Result.create(parseFunctions(TwigUtil.getTwigExtensionClasses(project)), PsiModificationTracker.MODIFICATION_COUNT),
() -> CachedValueProvider.Result.create(parseFunctions(project, TwigUtil.getTwigExtensionClasses(project)), PsiModificationTracker.MODIFICATION_COUNT),
false
);
}
Expand All @@ -49,7 +62,7 @@ public static Map<String, TwigExtension> getFilters(@NotNull Project project) {
return CachedValuesManager.getManager(project).getCachedValue(
project,
FILTERS_CACHE,
() -> CachedValueProvider.Result.create(parseFilters(TwigUtil.getTwigExtensionClasses(project)), PsiModificationTracker.MODIFICATION_COUNT),
() -> CachedValueProvider.Result.create(parseFilters(project, TwigUtil.getTwigExtensionClasses(project)), PsiModificationTracker.MODIFICATION_COUNT),
false
);
}
Expand All @@ -59,7 +72,7 @@ public static Map<String, TwigExtension> getSimpleTest(@NotNull Project project)
return CachedValuesManager.getManager(project).getCachedValue(
project,
TEST_CACHE,
() -> CachedValueProvider.Result.create(parseTests(TwigUtil.getTwigExtensionClasses(project)), PsiModificationTracker.MODIFICATION_COUNT),
() -> CachedValueProvider.Result.create(parseTests(project, TwigUtil.getTwigExtensionClasses(project)), PsiModificationTracker.MODIFICATION_COUNT),
false
);
}
Expand All @@ -75,7 +88,7 @@ public static Map<String, TwigExtension> getOperators(@NotNull Project project)
}

@NotNull
private static Map<String, TwigExtension> parseFilters(@NotNull Collection<PhpClass> phpClasses) {
private static Map<String, TwigExtension> parseFilters(@NotNull Project project, @NotNull Collection<PhpClass> phpClasses) {
Map<String, TwigExtension> extensions = new HashMap<>();

for (PhpClass phpClass : phpClasses) {
Expand All @@ -92,11 +105,15 @@ private static Map<String, TwigExtension> parseFilters(@NotNull Collection<PhpCl
}
}

for (Map.Entry<String, List<String>> entry : FileIndexCaches.getStringDataCache(project, TWIG_ATTRIBUTE_FILTER_INDEX, TWIG_ATTRIBUTE_FILTER_NAMES, TwigAttributeIndex.KEY, GlobalSearchScope.getScopeRestrictedByFileTypes(GlobalSearchScope.allScope(project), PhpFileType.INSTANCE)).entrySet()) {
extensions.put(entry.getKey(), new TwigExtension(TwigExtensionType.FILTER, entry.getValue().getFirst()));
}

return Collections.unmodifiableMap(extensions);
}

@NotNull
private static Map<String, TwigExtension> parseFunctions(@NotNull Collection<PhpClass> phpClasses) {
private static Map<String, TwigExtension> parseFunctions(@NotNull Project project, @NotNull Collection<PhpClass> phpClasses) {
Map<String, TwigExtension> extensions = new HashMap<>();

for (PhpClass phpClass : phpClasses) {
Expand All @@ -113,11 +130,15 @@ private static Map<String, TwigExtension> parseFunctions(@NotNull Collection<Php
}
}

for (Map.Entry<String, List<String>> entry : FileIndexCaches.getStringDataCache(project, TWIG_ATTRIBUTE_FUNCTION_INDEX, TWIG_ATTRIBUTE_FUNCTION_INDEX_NAMES, TwigAttributeIndex.KEY, GlobalSearchScope.getScopeRestrictedByFileTypes(GlobalSearchScope.allScope(project), PhpFileType.INSTANCE)).entrySet()) {
extensions.put(entry.getKey(), new TwigExtension(TwigExtensionType.FUNCTION_METHOD, entry.getValue().getFirst()));
}

return Collections.unmodifiableMap(extensions);
}

@NotNull
private static Map<String, TwigExtension> parseTests(@NotNull Collection<PhpClass> phpClasses) {
private static Map<String, TwigExtension> parseTests(@NotNull Project project, @NotNull Collection<PhpClass> phpClasses) {
Map<String, TwigExtension> extensions = new HashMap<>();

for (PhpClass phpClass : phpClasses) {
Expand All @@ -129,6 +150,10 @@ private static Map<String, TwigExtension> parseTests(@NotNull Collection<PhpClas
}
}

for (Map.Entry<String, List<String>> entry : FileIndexCaches.getStringDataCache(project, TWIG_ATTRIBUTE_TEST_INDEX, TWIG_ATTRIBUTE_TEST_NAMES, TwigAttributeIndex.KEY, GlobalSearchScope.getScopeRestrictedByFileTypes(GlobalSearchScope.allScope(project), PhpFileType.INSTANCE)).entrySet()) {
extensions.put(entry.getKey(), new TwigExtension(TwigExtensionType.SIMPLE_TEST, entry.getValue().getFirst()));
}

return Collections.unmodifiableMap(extensions);
}

Expand Down
1 change: 1 addition & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@
<fileBasedIndex implementation="fr.adrienbrault.idea.symfony2plugin.stubs.indexes.UxTemplateStubIndex"/>
<fileBasedIndex implementation="fr.adrienbrault.idea.symfony2plugin.stubs.indexes.TwigBlockEmbedIndex"/>
<fileBasedIndex implementation="fr.adrienbrault.idea.symfony2plugin.stubs.indexes.ConfigStubIndex"/>
<fileBasedIndex implementation="fr.adrienbrault.idea.symfony2plugin.stubs.indexes.TwigAttributeIndex"/>

<codeInsight.lineMarkerProvider language="PHP" implementationClass="fr.adrienbrault.idea.symfony2plugin.config.ServiceLineMarkerProvider"/>
<codeInsight.lineMarkerProvider language="PHP" implementationClass="fr.adrienbrault.idea.symfony2plugin.dic.ControllerMethodLineMarkerProvider"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package fr.adrienbrault.idea.symfony2plugin.tests.stubs.indexes;

import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.TwigAttributeIndex;
import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase;

/**
* @author Daniel Espendiller <[email protected]>
* @see fr.adrienbrault.idea.symfony2plugin.stubs.indexes.TwigAttributeIndex
*/
public class TwigAttributeIndexTest extends SymfonyLightCodeInsightFixtureTestCase {
public void setUp() throws Exception {
super.setUp();

myFixture.copyFileToProject("TwigAttributeIndex.php");
}

public void testThatValuesAreInIndex() {
assertIndexContainsKeyWithValue(TwigAttributeIndex.KEY, "product_number_filter", "#M#C\\App\\Twig\\AppExtension.formatProductNumberFilter");
assertIndexContainsKeyWithValue(TwigAttributeIndex.KEY, "product_number_function", "#M#C\\App\\Twig\\AppExtension.formatProductNumberFunction");
assertIndexContainsKeyWithValue(TwigAttributeIndex.KEY, "product_number_test", "#M#C\\App\\Twig\\AppExtension.formatProductNumberTest");
}

public String getTestDataPath() {
return "src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/stubs/indexes/fixtures";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace App\Twig;

use Twig\Attribute\AsTwigFilter;
use Twig\Attribute\AsTwigFunction;
use Twig\Attribute\AsTwigTest;

class AppExtension
{
#[AsTwigFilter('product_number_filter')]
public function formatProductNumberFilter(string $number): string
{
}

#[AsTwigFunction('product_number_function')]
public function formatProductNumberFunction(string $number): string
{
}

#[AsTwigTest('product_number_test')]
public function formatProductNumberTest(string $number): string
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,21 @@ public void testExtensionAreCollected() {
"#Fmax",
TwigExtensionParser.getFunctions(getProject()).get("conditional_return").getSignature()
);

assertEquals(
"#M#C\\App\\Twig\\AppExtension.formatProductNumberFilter",
TwigExtensionParser.getFilters(getProject()).get("product_number_filter").getSignature()
);

assertEquals(
"#M#C\\App\\Twig\\AppExtension.formatProductNumberFunction",
TwigExtensionParser.getFunctions(getProject()).get("product_number_function").getSignature()
);

assertEquals(
"#M#C\\App\\\\Twig\\AppExtension.formatProductNumberTest",
TwigExtensionParser.getSimpleTest(getProject()).get("product_number_test").getSignature()
);
}

public void testExtensionAreCollectedForDeprecated() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,28 @@ public function foobar()
namespace Twig\Extension {
interface ExtensionInterface {}
class AbstractExtension implements ExtensionInterface {}
}

namespace App\Twig {
use Twig\Attribute\AsTwigFilter;
use Twig\Attribute\AsTwigFunction;
use Twig\Attribute\AsTwigTest;

class AppExtension
{
#[AsTwigFilter('product_number_filter')]
public function formatProductNumberFilter(string $number): string
{
}

#[AsTwigFunction('product_number_function')]
public function formatProductNumberFunction(string $number): string
{
}

#[AsTwigTest('product_number_test')]
public function formatProductNumberTest(string $number): string
{
}
}
}
Loading