Skip to content

Commit 01d2d9a

Browse files
authored
Merge pull request #2452 from Haehnchen/feature/general-attr-index
provide general attribute indexer. "AsCommand" classes are now indexed
2 parents 118d426 + 94981c7 commit 01d2d9a

File tree

11 files changed

+502
-148
lines changed

11 files changed

+502
-148
lines changed
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package fr.adrienbrault.idea.symfony2plugin.stubs.indexes;
2+
3+
import com.intellij.util.indexing.*;
4+
import com.intellij.util.io.DataExternalizer;
5+
import com.intellij.util.io.EnumeratorStringDescriptor;
6+
import com.intellij.util.io.KeyDescriptor;
7+
import com.jetbrains.php.lang.PhpFileType;
8+
import com.jetbrains.php.lang.psi.PhpFile;
9+
import com.jetbrains.php.lang.psi.PhpPsiUtil;
10+
import com.jetbrains.php.lang.psi.elements.*;
11+
import com.jetbrains.php.lang.psi.stubs.indexes.expectedArguments.PhpExpectedFunctionArgument;
12+
import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.externalizer.StringListDataExternalizer;
13+
import org.apache.commons.lang3.StringUtils;
14+
import org.jetbrains.annotations.NotNull;
15+
16+
import java.util.*;
17+
18+
/**
19+
* Generalized index for PHP attributes on classes and methods
20+
*
21+
* Maps attribute FQNs to their targets with additional data:
22+
* - Key: Attribute FQN (e.g., "\Twig\Attribute\AsTwigFilter", "\Symfony\Component\Console\Attribute\AsCommand")
23+
* - Value: List<String> where:
24+
* [0] = Class FQN (e.g., "App\Twig\AppExtension")
25+
* [1] = Method name (for method-level attributes) or attribute parameter (e.g., filter name)
26+
* [2+] = Additional data (extensible for future use)
27+
*
28+
* Examples:
29+
* - AsTwigFilter: ["App\Twig\AppExtension", "formatProductNumber", "product_number_filter"]
30+
* - AsCommand: ["App\Command\CreateUserCommand"]
31+
*
32+
* @author Daniel Espendiller <daniel@espendiller.net>
33+
*/
34+
public class PhpAttributeIndex extends FileBasedIndexExtension<String, List<String>> {
35+
public static final ID<String, List<String>> KEY = ID.create("fr.adrienbrault.idea.symfony2plugin.php_attribute.index");
36+
37+
@Override
38+
public @NotNull ID<String, List<String>> getName() {
39+
return KEY;
40+
}
41+
42+
@Override
43+
public @NotNull DataIndexer<String, List<String>, FileContent> getIndexer() {
44+
return new PhpAttributeIndexer();
45+
}
46+
47+
@Override
48+
public @NotNull KeyDescriptor<String> getKeyDescriptor() {
49+
return EnumeratorStringDescriptor.INSTANCE;
50+
}
51+
52+
@Override
53+
public @NotNull DataExternalizer<List<String>> getValueExternalizer() {
54+
return StringListDataExternalizer.INSTANCE;
55+
}
56+
57+
@Override
58+
public @NotNull FileBasedIndex.InputFilter getInputFilter() {
59+
return virtualFile -> virtualFile.getFileType() == PhpFileType.INSTANCE;
60+
}
61+
62+
@Override
63+
public boolean dependsOnFileContent() {
64+
return true;
65+
}
66+
67+
@Override
68+
public int getVersion() {
69+
return 6;
70+
}
71+
72+
public static class PhpAttributeIndexer implements DataIndexer<String, List<String>, FileContent> {
73+
// Twig attributes on methods
74+
private static final Set<String> TWIG_METHOD_ATTRIBUTES = Set.of(
75+
"\\Twig\\Attribute\\AsTwigFilter",
76+
"\\Twig\\Attribute\\AsTwigFunction",
77+
"\\Twig\\Attribute\\AsTwigTest"
78+
);
79+
80+
// Symfony console command attributes on classes
81+
private static final String AS_COMMAND_ATTRIBUTE = "\\Symfony\\Component\\Console\\Attribute\\AsCommand";
82+
83+
@Override
84+
public @NotNull Map<String, List<String>> map(@NotNull FileContent inputData) {
85+
Map<String, List<String>> result = new HashMap<>();
86+
if (!(inputData.getPsiFile() instanceof PhpFile phpFile)) {
87+
return result;
88+
}
89+
90+
for (PhpClass phpClass : PhpPsiUtil.findAllClasses(phpFile)) {
91+
// Process class-level attributes
92+
processClassAttributes(phpClass, result);
93+
94+
// Process method-level attributes
95+
for (Method method : phpClass.getOwnMethods()) {
96+
processMethodAttributes(phpClass, method, result);
97+
}
98+
}
99+
100+
return result;
101+
}
102+
103+
/**
104+
* Process attributes on class level (e.g., AsCommand on Command classes)
105+
*/
106+
private void processClassAttributes(@NotNull PhpClass phpClass, @NotNull Map<String, List<String>> result) {
107+
for (PhpAttribute attribute : phpClass.getAttributes()) {
108+
String attributeFqn = attribute.getFQN();
109+
if (attributeFqn == null) {
110+
continue;
111+
}
112+
113+
// Index AsCommand attribute on class
114+
// Key: Attribute FQN
115+
// Value: [class FQN]
116+
if (AS_COMMAND_ATTRIBUTE.equals(attributeFqn)) {
117+
String classFqn = StringUtils.stripStart(phpClass.getFQN(), "\\");
118+
result.put(attributeFqn, List.of(classFqn));
119+
}
120+
}
121+
}
122+
123+
/**
124+
* Process attributes on method level (Twig attributes)
125+
*/
126+
private void processMethodAttributes(@NotNull PhpClass phpClass, @NotNull Method method, @NotNull Map<String, List<String>> result) {
127+
for (PhpAttribute attribute : method.getAttributes()) {
128+
String attributeFqn = attribute.getFQN();
129+
if (attributeFqn == null) {
130+
continue;
131+
}
132+
133+
// Index Twig attributes on methods
134+
// Key: Attribute FQN
135+
// Value: [class FQN, method name, filter/function/test name]
136+
if (TWIG_METHOD_ATTRIBUTES.contains(attributeFqn)) {
137+
String nameAttribute = extractFirstAttributeParameter(attribute);
138+
if (nameAttribute != null) {
139+
String classFqn = StringUtils.stripStart(phpClass.getFQN(), "\\");
140+
result.put(attributeFqn, List.of(classFqn, method.getName(), nameAttribute));
141+
}
142+
}
143+
}
144+
}
145+
146+
/**
147+
* Extract the first parameter from a PHP attribute during indexing
148+
* We can't use PhpPsiAttributesUtil because it doesn't work reliably during indexing
149+
*/
150+
private String extractFirstAttributeParameter(@NotNull PhpAttribute attribute) {
151+
for (PhpAttribute.PhpAttributeArgument argument : attribute.getArguments()) {
152+
PhpExpectedFunctionArgument funcArg = argument.getArgument();
153+
if (funcArg != null && funcArg.getArgumentIndex() == 0) {
154+
// Try to get the value as a string
155+
String value = funcArg.getValue();
156+
if (value != null) {
157+
// Remove quotes if present
158+
return value.replaceAll("^['\"]|['\"]$", "");
159+
}
160+
}
161+
}
162+
return null;
163+
}
164+
}
165+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package fr.adrienbrault.idea.symfony2plugin.stubs.indexes;
2+
3+
import com.intellij.openapi.project.Project;
4+
import com.intellij.psi.search.GlobalSearchScope;
5+
import com.intellij.util.indexing.FileBasedIndex;
6+
import com.jetbrains.php.PhpIndex;
7+
import com.jetbrains.php.lang.psi.elements.PhpClass;
8+
import org.jetbrains.annotations.NotNull;
9+
10+
import java.util.ArrayList;
11+
import java.util.Collection;
12+
import java.util.List;
13+
14+
/**
15+
* Utility class for querying PhpAttributeIndex
16+
*
17+
* Index structure:
18+
* - Key: Attribute FQN (e.g., "\Twig\Attribute\AsTwigFilter")
19+
* - Value: List<String> where [0] = class FQN, [1+] = additional data
20+
*
21+
* @author Daniel Espendiller <daniel@espendiller.net>
22+
*/
23+
public class PhpAttributeIndexUtil {
24+
/**
25+
* Get all indexed data for the given attribute FQN
26+
*
27+
* @param project The project
28+
* @param attributeFqn The attribute FQN (e.g., "\Twig\Attribute\AsTwigFilter")
29+
* @return Collection of List<String> where [0] = class FQN, [1+] = additional data
30+
*/
31+
@NotNull
32+
public static Collection<List<String>> getAttributeData(@NotNull Project project, @NotNull String attributeFqn) {
33+
return FileBasedIndex.getInstance().getValues(
34+
PhpAttributeIndex.KEY,
35+
attributeFqn,
36+
GlobalSearchScope.allScope(project)
37+
);
38+
}
39+
40+
/**
41+
* Get all PHP classes that have the given attribute
42+
*
43+
* @param project The project
44+
* @param attributeFqn The attribute FQN
45+
* @return Collection of PhpClass instances
46+
*/
47+
@NotNull
48+
public static Collection<PhpClass> getClassesWithAttribute(@NotNull Project project, @NotNull String attributeFqn) {
49+
Collection<PhpClass> classes = new ArrayList<>();
50+
PhpIndex phpIndex = PhpIndex.getInstance(project);
51+
52+
for (List<String> data : getAttributeData(project, attributeFqn)) {
53+
if (!data.isEmpty()) {
54+
String classFqn = "\\" + data.get(0);
55+
Collection<PhpClass> foundClasses = phpIndex.getAnyByFQN(classFqn);
56+
if (!foundClasses.isEmpty()) {
57+
classes.add(foundClasses.iterator().next());
58+
}
59+
}
60+
}
61+
62+
return classes;
63+
}
64+
}

src/main/java/fr/adrienbrault/idea/symfony2plugin/stubs/indexes/TwigAttributeIndex.java

Lines changed: 0 additions & 102 deletions
This file was deleted.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package fr.adrienbrault.idea.symfony2plugin.stubs.indexes.externalizer;
2+
3+
import com.intellij.util.io.DataExternalizer;
4+
import com.intellij.util.io.EnumeratorStringDescriptor;
5+
import org.jetbrains.annotations.NotNull;
6+
7+
import java.io.DataInput;
8+
import java.io.DataOutput;
9+
import java.io.IOException;
10+
import java.util.ArrayList;
11+
import java.util.List;
12+
13+
/**
14+
* DataExternalizer for List<String> with proper equals/hashCode support
15+
*
16+
* @author Daniel Espendiller <daniel@espendiller.net>
17+
*/
18+
public class StringListDataExternalizer implements DataExternalizer<List<String>> {
19+
20+
public static final StringListDataExternalizer INSTANCE = new StringListDataExternalizer();
21+
22+
@Override
23+
public synchronized void save(@NotNull DataOutput out, List<String> value) throws IOException {
24+
out.writeInt(value.size());
25+
26+
for (String s : value) {
27+
EnumeratorStringDescriptor.INSTANCE.save(out, s);
28+
}
29+
}
30+
31+
@Override
32+
public synchronized List<String> read(@NotNull DataInput in) throws IOException {
33+
List<String> list = new ArrayList<>();
34+
35+
for (int r = in.readInt(); r > 0; --r) {
36+
list.add(EnumeratorStringDescriptor.INSTANCE.read(in));
37+
}
38+
39+
return list;
40+
}
41+
}

src/main/java/fr/adrienbrault/idea/symfony2plugin/stubs/util/IndexUtil.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public static void forceReindex() {
3131
TwigMacroFunctionStubIndex.KEY,
3232
TranslationStubIndex.KEY,
3333
TwigBlockIndexExtension.KEY,
34-
TwigAttributeIndex.KEY
34+
PhpAttributeIndex.KEY
3535
};
3636

3737
for(ID<?,?> id: indexIds) {

0 commit comments

Comments
 (0)