Skip to content

Commit 9f7af60

Browse files
committed
Optimize annotation and attribute lookup paths with index-backed caches
1 parent 7ce1aad commit 9f7af60

10 files changed

Lines changed: 343 additions & 108 deletions

File tree

src/main/java/de/espend/idea/php/annotation/AnnotationStubIndex.java

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import com.intellij.util.io.DataExternalizer;
66
import com.intellij.util.io.EnumeratorStringDescriptor;
77
import com.intellij.util.io.KeyDescriptor;
8-
import com.intellij.util.io.VoidDataExternalizer;
98
import com.jetbrains.php.lang.PhpFileType;
109
import com.jetbrains.php.lang.psi.PhpFile;
1110
import com.jetbrains.php.lang.psi.elements.PhpClass;
@@ -19,21 +18,21 @@
1918
/**
2019
* @author Daniel Espendiller <daniel@espendiller.net>
2120
*/
22-
public class AnnotationStubIndex extends FileBasedIndexExtension<String, Void> {
23-
public static final ID<String, Void> KEY = ID.create("espend.php.annotation.classes");
21+
public class AnnotationStubIndex extends FileBasedIndexExtension<String, String> {
22+
public static final ID<String, String> KEY = ID.create("espend.php.annotation.classes");
2423
private final KeyDescriptor<String> myKeyDescriptor = new EnumeratorStringDescriptor();
2524

2625
@NotNull
2726
@Override
28-
public ID<String, Void> getName() {
27+
public ID<String, String> getName() {
2928
return KEY;
3029
}
3130

3231
@NotNull
3332
@Override
34-
public DataIndexer<String, Void, FileContent> getIndexer() {
33+
public DataIndexer<String, String, FileContent> getIndexer() {
3534
return inputData -> {
36-
final Map<String, Void> map = new HashMap<>();
35+
final Map<String, String> map = new HashMap<>();
3736

3837
PsiFile psiFile = inputData.getPsiFile();
3938
if (!(psiFile instanceof PhpFile)) {
@@ -54,8 +53,11 @@ public DataIndexer<String, Void, FileContent> getIndexer() {
5453
// doctrine has many tests: Doctrine\Tests\Common\Annotations\Fixtures
5554
// we are on index process, project is not fully loaded here, so filter name based tests
5655
// e.g. PhpUnitUtil.isTestClass not possible
57-
if (!fqn.contains("\\Tests\\") && !fqn.contains("\\Fixtures\\") && AnnotationUtil.isAnnotationClass(phpClass)) {
58-
map.put(fqn, null);
56+
if (!fqn.contains("\\Tests\\") && !fqn.contains("\\Fixtures\\")) {
57+
String serializedTargets = AnnotationUtil.getSerializedAnnotationTargets(phpClass);
58+
if (serializedTargets != null) {
59+
map.put(fqn, serializedTargets);
60+
}
5961
}
6062
}
6163
}
@@ -72,8 +74,8 @@ public KeyDescriptor<String> getKeyDescriptor() {
7274

7375
@NotNull
7476
@Override
75-
public DataExternalizer<Void> getValueExternalizer() {
76-
return VoidDataExternalizer.INSTANCE;
77+
public DataExternalizer<String> getValueExternalizer() {
78+
return EnumeratorStringDescriptor.INSTANCE;
7779
}
7880

7981
@NotNull
@@ -89,6 +91,6 @@ public boolean dependsOnFileContent() {
8991

9092
@Override
9193
public int getVersion() {
92-
return 2;
94+
return 3;
9395
}
9496
}

src/main/java/de/espend/idea/php/annotation/completion/AnnotationCompletionContributor.java

Lines changed: 62 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33
import com.intellij.codeInsight.completion.*;
44
import com.intellij.codeInsight.lookup.LookupElementBuilder;
55
import com.intellij.openapi.project.Project;
6+
import com.intellij.openapi.util.Key;
67
import com.intellij.patterns.ElementPattern;
78
import com.intellij.patterns.PlatformPatterns;
89
import com.intellij.psi.PsiElement;
910
import com.intellij.psi.PsiWhiteSpace;
11+
import com.intellij.psi.util.CachedValue;
12+
import com.intellij.psi.util.CachedValueProvider;
13+
import com.intellij.psi.util.CachedValuesManager;
1014
import com.intellij.psi.util.PsiTreeUtil;
1115
import com.intellij.util.ProcessingContext;
1216
import com.intellij.util.indexing.FileBasedIndex;
@@ -46,6 +50,7 @@
4650
* @author Daniel Espendiller <daniel@espendiller.net>
4751
*/
4852
public class AnnotationCompletionContributor extends CompletionContributor {
53+
private static final Key<CachedValue<Map<String, Collection<String>>>> ATTRIBUTE_FQNS_BY_NAMESPACE_CACHE = new Key<>("ATTRIBUTE_FQNS_BY_NAMESPACE_CACHE");
4954

5055
private static final ElementPattern<PsiElement> DOC_IDENTIFIER_PATTERN =
5156
PlatformPatterns.psiElement(PhpDocTokenTypes.DOC_IDENTIFIER);
@@ -411,36 +416,73 @@ private void attachLookupElements(@NotNull Project project, @NotNull PhpAttribut
411416

412417
items.putAll(getUseAsMap(phpAttributesList));
413418

414-
for (String fqnClass: FileBasedIndex.getInstance().getAllKeys(PhpAttributesFQNsIndex.KEY, project)) {
415-
if(!fqnClass.startsWith("\\")) {
416-
fqnClass = "\\" + fqnClass;
419+
Map<String, Collection<String>> attributesByNamespace = getAttributeFqnsByNamespace(project);
420+
421+
for (Map.Entry<String, String> aliasFqn : items.entrySet()) {
422+
String namespace = "\\" + StringUtils.stripStart(aliasFqn.getValue(), "\\");
423+
Collection<String> fqns = attributesByNamespace.get(namespace);
424+
if (fqns == null) {
425+
continue;
417426
}
418427

419-
// attach class also "@ORM\Entity" if there is not import but an alias via settings
420-
for (Map.Entry<String, String> aliasFqn : items.entrySet()) {
421-
String className = "\\" + StringUtils.stripStart(aliasFqn.getValue(), "\\") + "\\";
428+
for (String fqnClass : fqns) {
429+
String substring = fqnClass.substring(namespace.length() + 1);
430+
String lookupString = aliasFqn.getKey() + "\\" + substring;
422431

423-
if (!fqnClass.startsWith(className)) {
432+
PhpClass underlyingClass = PhpElementsUtil.getClassInterface(project, fqnClass);
433+
if (underlyingClass == null) {
424434
continue;
425435
}
426436

427-
String substring = fqnClass.substring(className.length());
437+
// check if Attribute is target allowed for context
438+
// @see com.jetbrains.php.completion.PhpCompletionContributor.PhpClassRefCompletionProvider.shouldAddElement
439+
List<PhpAttribute> rootAttributes = PhpClassCantBeUsedAsAttributeInspection.rootAttributes(underlyingClass).collect(Collectors.toList());
440+
if (!rootAttributes.isEmpty() && PhpInapplicableAttributeTargetDeclarationInspection.getInapplicableDeclarationName(phpAttributesList.getParent(), rootAttributes) == null) {
441+
PhpClassAnnotationLookupElement phpClassAnnotationLookupElement = new PhpClassAnnotationLookupElement(underlyingClass, new UseAliasOption(aliasFqn.getValue(), aliasFqn.getKey(), true), lookupString);
442+
phpClassAnnotationLookupElement.withInsertHandler(AttributeAliasInsertHandler.getInstance());
443+
completionResultSet.addElement(underlyingClass.isDeprecated() ? PrioritizedLookupElement.withPriority(phpClassAnnotationLookupElement, -1000) : phpClassAnnotationLookupElement);
444+
}
445+
}
446+
}
447+
}
428448

429-
String lookupString = aliasFqn.getKey() + "\\" + substring;
449+
/**
450+
* Cache attribute FQNs by namespace prefix so alias completion can query only relevant branches.
451+
*
452+
* Example key: "\Doctrine\ORM\Mapping" => ["\Doctrine\ORM\Mapping\Entity", ...]
453+
*/
454+
@NotNull
455+
private static Map<String, Collection<String>> getAttributeFqnsByNamespace(@NotNull Project project) {
456+
return CachedValuesManager.getManager(project).getCachedValue(
457+
project,
458+
ATTRIBUTE_FQNS_BY_NAMESPACE_CACHE,
459+
() -> {
460+
Map<String, Collection<String>> items = new HashMap<>();
461+
462+
for (String fqnClass : FileBasedIndex.getInstance().getAllKeys(PhpAttributesFQNsIndex.KEY, project)) {
463+
if (!fqnClass.startsWith("\\")) {
464+
fqnClass = "\\" + fqnClass;
465+
}
430466

431-
PhpClass underlyingClass = PhpElementsUtil.getClassInterface(project, fqnClass);
432-
if (underlyingClass != null) {
433-
// check if Attribute is target allowed for context
434-
// @see com.jetbrains.php.completion.PhpCompletionContributor.PhpClassRefCompletionProvider.shouldAddElement
435-
List<PhpAttribute> rootAttributes = PhpClassCantBeUsedAsAttributeInspection.rootAttributes(underlyingClass).collect(Collectors.toList());
436-
if (!rootAttributes.isEmpty() && PhpInapplicableAttributeTargetDeclarationInspection.getInapplicableDeclarationName(phpAttributesList.getParent(), rootAttributes) == null) {
437-
PhpClassAnnotationLookupElement phpClassAnnotationLookupElement = new PhpClassAnnotationLookupElement(underlyingClass, new UseAliasOption(aliasFqn.getValue(), aliasFqn.getKey(), true), lookupString);
438-
phpClassAnnotationLookupElement.withInsertHandler(AttributeAliasInsertHandler.getInstance());
439-
completionResultSet.addElement(underlyingClass.isDeprecated() ? PrioritizedLookupElement.withPriority(phpClassAnnotationLookupElement, -1000) : phpClassAnnotationLookupElement);
467+
int index = fqnClass.indexOf("\\", 1);
468+
while (index > 0) {
469+
items.computeIfAbsent(fqnClass.substring(0, index), key -> new ArrayList<>()).add(fqnClass);
470+
index = fqnClass.indexOf("\\", index + 1);
440471
}
441472
}
442-
}
443-
}
473+
474+
Map<String, Collection<String>> immutableItems = new HashMap<>();
475+
for (Map.Entry<String, Collection<String>> entry : items.entrySet()) {
476+
immutableItems.put(entry.getKey(), List.copyOf(entry.getValue()));
477+
}
478+
479+
return CachedValueProvider.Result.create(
480+
Collections.unmodifiableMap(immutableItems),
481+
AnnotationUtil.getModificationTrackerForIndexId(project, PhpAttributesFQNsIndex.KEY)
482+
);
483+
},
484+
false
485+
);
444486
}
445487

446488
private static Map<String, String> getUseAsMap(@NotNull PsiElement phpDocComment) {

src/main/java/de/espend/idea/php/annotation/navigation/AnnotationUsageLineMarkerProvider.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public void collectSlowLineMarkers(@NotNull List<? extends PsiElement> psiElemen
5151
|| !phpClass.getAttributes("\\Attribute").isEmpty();
5252

5353
if (!isAnnotationOrAttribute) {
54-
return;
54+
continue;
5555
}
5656

5757
String fqn = StringUtils.stripStart(phpClass.getFQN(), "\\");
@@ -92,4 +92,4 @@ public void collectSlowLineMarkers(@NotNull List<? extends PsiElement> psiElemen
9292
private static PsiElementPattern.Capture<PsiElement> getClassNamePattern() {
9393
return CLASS_NAME_PATTERN;
9494
}
95-
}
95+
}

0 commit comments

Comments
 (0)