Skip to content
Merged
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
Expand Up @@ -5,7 +5,6 @@
import com.intellij.util.io.DataExternalizer;
import com.intellij.util.io.EnumeratorStringDescriptor;
import com.intellij.util.io.KeyDescriptor;
import com.intellij.util.io.VoidDataExternalizer;
import com.jetbrains.php.lang.PhpFileType;
import com.jetbrains.php.lang.psi.PhpFile;
import com.jetbrains.php.lang.psi.elements.PhpClass;
Expand All @@ -19,21 +18,21 @@
/**
* @author Daniel Espendiller <daniel@espendiller.net>
*/
public class AnnotationStubIndex extends FileBasedIndexExtension<String, Void> {
public static final ID<String, Void> KEY = ID.create("espend.php.annotation.classes");
public class AnnotationStubIndex extends FileBasedIndexExtension<String, String> {
public static final ID<String, String> KEY = ID.create("espend.php.annotation.classes");
private final KeyDescriptor<String> myKeyDescriptor = new EnumeratorStringDescriptor();

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

@NotNull
@Override
public DataIndexer<String, Void, FileContent> getIndexer() {
public DataIndexer<String, String, FileContent> getIndexer() {
return inputData -> {
final Map<String, Void> map = new HashMap<>();
final Map<String, String> map = new HashMap<>();

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

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

@NotNull
Expand All @@ -89,6 +91,6 @@ public boolean dependsOnFileContent() {

@Override
public int getVersion() {
return 2;
return 3;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@
import com.intellij.codeInsight.completion.*;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.patterns.ElementPattern;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.util.CachedValue;
import com.intellij.psi.util.CachedValueProvider;
import com.intellij.psi.util.CachedValuesManager;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.ProcessingContext;
import com.intellij.util.indexing.FileBasedIndex;
Expand Down Expand Up @@ -46,6 +50,7 @@
* @author Daniel Espendiller <daniel@espendiller.net>
*/
public class AnnotationCompletionContributor extends CompletionContributor {
private static final Key<CachedValue<Map<String, Collection<String>>>> ATTRIBUTE_FQNS_BY_NAMESPACE_CACHE = new Key<>("ATTRIBUTE_FQNS_BY_NAMESPACE_CACHE");

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

items.putAll(getUseAsMap(phpAttributesList));

for (String fqnClass: FileBasedIndex.getInstance().getAllKeys(PhpAttributesFQNsIndex.KEY, project)) {
if(!fqnClass.startsWith("\\")) {
fqnClass = "\\" + fqnClass;
Map<String, Collection<String>> attributesByNamespace = getAttributeFqnsByNamespace(project);

for (Map.Entry<String, String> aliasFqn : items.entrySet()) {
String namespace = "\\" + StringUtils.stripStart(aliasFqn.getValue(), "\\");
Collection<String> fqns = attributesByNamespace.get(namespace);
if (fqns == null) {
continue;
}

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

if (!fqnClass.startsWith(className)) {
PhpClass underlyingClass = PhpElementsUtil.getClassInterface(project, fqnClass);
if (underlyingClass == null) {
continue;
}

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

String lookupString = aliasFqn.getKey() + "\\" + substring;
/**
* Cache attribute FQNs by namespace prefix so alias completion can query only relevant branches.
*
* Example key: "\Doctrine\ORM\Mapping" => ["\Doctrine\ORM\Mapping\Entity", ...]
*/
@NotNull
private static Map<String, Collection<String>> getAttributeFqnsByNamespace(@NotNull Project project) {
return CachedValuesManager.getManager(project).getCachedValue(
project,
ATTRIBUTE_FQNS_BY_NAMESPACE_CACHE,
() -> {
Map<String, Collection<String>> items = new HashMap<>();

for (String fqnClass : FileBasedIndex.getInstance().getAllKeys(PhpAttributesFQNsIndex.KEY, project)) {
if (!fqnClass.startsWith("\\")) {
fqnClass = "\\" + fqnClass;
}

PhpClass underlyingClass = PhpElementsUtil.getClassInterface(project, fqnClass);
if (underlyingClass != null) {
// check if Attribute is target allowed for context
// @see com.jetbrains.php.completion.PhpCompletionContributor.PhpClassRefCompletionProvider.shouldAddElement
List<PhpAttribute> rootAttributes = PhpClassCantBeUsedAsAttributeInspection.rootAttributes(underlyingClass).collect(Collectors.toList());
if (!rootAttributes.isEmpty() && PhpInapplicableAttributeTargetDeclarationInspection.getInapplicableDeclarationName(phpAttributesList.getParent(), rootAttributes) == null) {
PhpClassAnnotationLookupElement phpClassAnnotationLookupElement = new PhpClassAnnotationLookupElement(underlyingClass, new UseAliasOption(aliasFqn.getValue(), aliasFqn.getKey(), true), lookupString);
phpClassAnnotationLookupElement.withInsertHandler(AttributeAliasInsertHandler.getInstance());
completionResultSet.addElement(underlyingClass.isDeprecated() ? PrioritizedLookupElement.withPriority(phpClassAnnotationLookupElement, -1000) : phpClassAnnotationLookupElement);
int index = fqnClass.indexOf("\\", 1);
while (index > 0) {
items.computeIfAbsent(fqnClass.substring(0, index), key -> new ArrayList<>()).add(fqnClass);
index = fqnClass.indexOf("\\", index + 1);
}
}
}
}

Map<String, Collection<String>> immutableItems = new HashMap<>();
for (Map.Entry<String, Collection<String>> entry : items.entrySet()) {
immutableItems.put(entry.getKey(), List.copyOf(entry.getValue()));
}

return CachedValueProvider.Result.create(
Collections.unmodifiableMap(immutableItems),
AnnotationUtil.getModificationTrackerForIndexId(project, PhpAttributesFQNsIndex.KEY)
);
},
false
);
}

private static Map<String, String> getUseAsMap(@NotNull PsiElement phpDocComment) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public void collectSlowLineMarkers(@NotNull List<? extends PsiElement> psiElemen
|| !phpClass.getAttributes("\\Attribute").isEmpty();

if (!isAnnotationOrAttribute) {
return;
continue;
}

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