From 9d0a74c006aba9e69b37cabf62806124a2c49b22 Mon Sep 17 00:00:00 2001 From: Daniel Espendiller Date: Thu, 12 Jan 2017 15:14:40 +0100 Subject: [PATCH] Provide Annotation class usage linemarker #79, [API] Provide an index with annotated elements stubs #53 --- META-INF/plugin.xml | 3 + README.md | 24 ++++ .../php/annotation/AnnotationUsageIndex.java | 111 ++++++++++++++++++ .../ClassCompletionProviderAbstract.java | 1 - .../AnnotationUsageLineMarkerProvider.java | 106 +++++++++++++++++ .../php/annotation/util/AnnotationUtil.java | 54 +++++++++ ...otationRecursiveElementWalkingVisitor.java | 82 +++++++++++++ .../tests/AnnotationUsageIndexTest.java | 25 ++++ .../php/annotation/tests/fixtures/usages.php | 36 ++++++ ...AnnotationUsageLineMarkerProviderTest.java | 49 ++++++++ .../AnnotationUsageLineMarkerProvider.php | 12 ++ 11 files changed, 502 insertions(+), 1 deletion(-) create mode 100644 src/de/espend/idea/php/annotation/AnnotationUsageIndex.java create mode 100644 src/de/espend/idea/php/annotation/navigation/AnnotationUsageLineMarkerProvider.java create mode 100644 src/de/espend/idea/php/annotation/util/PhpDocTagAnnotationRecursiveElementWalkingVisitor.java create mode 100644 tests/de/espend/idea/php/annotation/tests/AnnotationUsageIndexTest.java create mode 100644 tests/de/espend/idea/php/annotation/tests/fixtures/usages.php create mode 100644 tests/de/espend/idea/php/annotation/tests/navigation/AnnotationUsageLineMarkerProviderTest.java create mode 100644 tests/de/espend/idea/php/annotation/tests/navigation/fixtures/AnnotationUsageLineMarkerProvider.php diff --git a/META-INF/plugin.xml b/META-INF/plugin.xml index cc6424c2..3139a39c 100644 --- a/META-INF/plugin.xml +++ b/META-INF/plugin.xml @@ -164,6 +164,7 @@ + @@ -176,6 +177,8 @@ + + + */ +public class AnnotationUsageIndex extends FileBasedIndexExtension> { + public static final ID> KEY = ID.create("espend.php.annotation.usage"); + private final KeyDescriptor myKeyDescriptor = new EnumeratorStringDescriptor(); + private static StringSetDataExternalizer EXTERNALIZER = new StringSetDataExternalizer(); + + @NotNull + @Override + public ID> getName() { + return KEY; + } + + @NotNull + @Override + public DataIndexer, FileContent> getIndexer() { + return inputData -> { + final Map> map = new THashMap<>(); + + PsiFile psiFile = inputData.getPsiFile(); + if(!(psiFile instanceof PhpFile)) { + return map; + } + + if(!AnnotationUtil.isValidForIndex(inputData)) { + return map; + } + + psiFile.accept(new PhpDocTagAnnotationRecursiveElementWalkingVisitor(pair -> { + map.put(pair.getFirst(), new HashSet<>()); + return true; + })); + + return map; + }; + } + + @NotNull + @Override + public KeyDescriptor getKeyDescriptor() { + return this.myKeyDescriptor; + } + + @NotNull + @Override + public DataExternalizer> getValueExternalizer() { + return EXTERNALIZER; + } + + @NotNull + @Override + public FileBasedIndex.InputFilter getInputFilter() { + return virtualFile -> virtualFile.getFileType() == PhpFileType.INSTANCE; + } + + @Override + public boolean dependsOnFileContent() { + return true; + } + + @Override + public int getVersion() { + return 1; + } + + private static class StringSetDataExternalizer implements DataExternalizer> { + public synchronized void save(@NotNull DataOutput out, Set value) throws IOException { + out.writeInt(value.size()); + Iterator var = value.iterator(); + + while(var.hasNext()) { + String s = (String)var.next(); + EnumeratorStringDescriptor.INSTANCE.save(out, s); + } + } + + public synchronized Set read(@NotNull DataInput in) throws IOException { + Set set = new THashSet<>(); + + for(int r = in.readInt(); r > 0; --r) { + set.add(EnumeratorStringDescriptor.INSTANCE.read(in)); + } + + return set; + } + } +} diff --git a/src/de/espend/idea/php/annotation/doctrine/reference/ClassCompletionProviderAbstract.java b/src/de/espend/idea/php/annotation/doctrine/reference/ClassCompletionProviderAbstract.java index 1edfb662..dcb982f2 100644 --- a/src/de/espend/idea/php/annotation/doctrine/reference/ClassCompletionProviderAbstract.java +++ b/src/de/espend/idea/php/annotation/doctrine/reference/ClassCompletionProviderAbstract.java @@ -4,7 +4,6 @@ import com.intellij.psi.PsiReference; import com.jetbrains.php.PhpIndex; import com.jetbrains.php.completion.PhpCompletionUtil; -import com.jetbrains.php.lang.PhpLangUtil; import com.jetbrains.php.lang.psi.elements.StringLiteralExpression; import de.espend.idea.php.annotation.extension.PhpAnnotationCompletionProvider; import de.espend.idea.php.annotation.extension.PhpAnnotationReferenceProvider; diff --git a/src/de/espend/idea/php/annotation/navigation/AnnotationUsageLineMarkerProvider.java b/src/de/espend/idea/php/annotation/navigation/AnnotationUsageLineMarkerProvider.java new file mode 100644 index 00000000..ee6ad31b --- /dev/null +++ b/src/de/espend/idea/php/annotation/navigation/AnnotationUsageLineMarkerProvider.java @@ -0,0 +1,106 @@ +package de.espend.idea.php.annotation.navigation; + +import com.intellij.codeInsight.daemon.LineMarkerInfo; +import com.intellij.codeInsight.daemon.LineMarkerProvider; +import com.intellij.codeInsight.navigation.NavigationGutterIconBuilder; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.NotNullLazyValue; +import com.intellij.patterns.PlatformPatterns; +import com.intellij.patterns.PsiElementPattern; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiWhiteSpace; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.util.indexing.FileBasedIndex; +import com.jetbrains.php.PhpIcons; +import com.jetbrains.php.lang.PhpFileType; +import com.jetbrains.php.lang.PhpLanguage; +import com.jetbrains.php.lang.lexer.PhpTokenTypes; +import com.jetbrains.php.lang.psi.elements.PhpClass; +import de.espend.idea.php.annotation.AnnotationUsageIndex; +import de.espend.idea.php.annotation.util.AnnotationUtil; +import org.apache.commons.lang.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; + +/** + * @author Daniel Espendiller + */ +public class AnnotationUsageLineMarkerProvider implements LineMarkerProvider { + @Nullable + @Override + public LineMarkerInfo getLineMarkerInfo(@NotNull PsiElement psiElement) { + return null; + } + + @Override + public void collectSlowLineMarkers(@NotNull List psiElements, @NotNull Collection results) { + for(PsiElement psiElement: psiElements) { + if(!getClassNamePattern().accepts(psiElement)) { + continue; + } + + PsiElement phpClass = psiElement.getContext(); + if(!(phpClass instanceof PhpClass) || !AnnotationUtil.isAnnotationClass((PhpClass) phpClass)) { + return; + } + + String fqn = StringUtils.stripStart(((PhpClass) phpClass).getFQN(), "\\"); + + // find one index annotation class and stop processing on first match + final boolean[] processed = {false}; + FileBasedIndex.getInstance().getFilesWithKey(AnnotationUsageIndex.KEY, new HashSet<>(Collections.singletonList(fqn)), virtualFile -> { + processed[0] = true; + + // stop on first match + return false; + }, GlobalSearchScope.getScopeRestrictedByFileTypes(GlobalSearchScope.allScope(psiElement.getProject()), PhpFileType.INSTANCE)); + + // we found at least one target to provide lazy target linemarker + if(processed[0]) { + NavigationGutterIconBuilder builder = NavigationGutterIconBuilder.create(PhpIcons.IMPLEMENTS) + .setTargets(new CollectionNotNullLazyValue(psiElement.getProject(), fqn)) + .setTooltipText("Navigate to implementations"); + + results.add(builder.createLineMarkerInfo(psiElement)); + } + } + } + + /** + * class "Foo" extends + */ + private static PsiElementPattern.Capture getClassNamePattern() { + return PlatformPatterns + .psiElement(PhpTokenTypes.IDENTIFIER) + .afterLeafSkipping( + PlatformPatterns.psiElement(PsiWhiteSpace.class), + PlatformPatterns.psiElement(PhpTokenTypes.kwCLASS) + ) + .withParent(PhpClass.class) + .withLanguage(PhpLanguage.INSTANCE); + } + + private static class CollectionNotNullLazyValue extends NotNullLazyValue> { + @NotNull + private final Project project; + + @NotNull + private final String fqn; + + CollectionNotNullLazyValue(@NotNull Project project, @NotNull String fqn) { + this.project = project; + this.fqn = fqn; + } + + @NotNull + @Override + protected Collection compute() { + return AnnotationUtil.getImplementationsForAnnotation(project, fqn); + } + } +} \ No newline at end of file diff --git a/src/de/espend/idea/php/annotation/util/AnnotationUtil.java b/src/de/espend/idea/php/annotation/util/AnnotationUtil.java index f91af583..d7a9e80e 100644 --- a/src/de/espend/idea/php/annotation/util/AnnotationUtil.java +++ b/src/de/espend/idea/php/annotation/util/AnnotationUtil.java @@ -3,9 +3,12 @@ import com.intellij.openapi.extensions.ExtensionPointName; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VfsUtil; +import com.intellij.openapi.vfs.VirtualFile; import com.intellij.patterns.PlatformPatterns; import com.intellij.patterns.PsiElementPattern; import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiManager; import com.intellij.psi.PsiWhiteSpace; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.util.PsiTreeUtil; @@ -14,6 +17,7 @@ import com.intellij.util.indexing.FileContent; import com.intellij.util.indexing.ID; import com.jetbrains.php.codeInsight.PhpCodeInsightUtil; +import com.jetbrains.php.lang.PhpFileType; import com.jetbrains.php.lang.documentation.phpdoc.PhpDocUtil; import com.jetbrains.php.lang.documentation.phpdoc.lexer.PhpDocTokenTypes; import com.jetbrains.php.lang.documentation.phpdoc.parser.PhpDocElementTypes; @@ -22,6 +26,7 @@ import com.jetbrains.php.lang.parser.PhpElementTypes; import com.jetbrains.php.lang.psi.elements.*; import de.espend.idea.php.annotation.AnnotationStubIndex; +import de.espend.idea.php.annotation.AnnotationUsageIndex; import de.espend.idea.php.annotation.dict.AnnotationTarget; import de.espend.idea.php.annotation.dict.PhpAnnotation; import de.espend.idea.php.annotation.dict.PhpDocCommentAnnotation; @@ -493,6 +498,55 @@ public static StringLiteralExpression getPropertyValueAsPsiElement(@NotNull PhpD return null; } + @NotNull + private static Collection getFilesImplementingAnnotation(@NotNull Project project, @NotNull String phpClassName) { + Collection files = new HashSet<>(); + + FileBasedIndex.getInstance().getFilesWithKey(AnnotationUsageIndex.KEY, new HashSet<>(Collections.singletonList(phpClassName)), virtualFile -> { + files.add(virtualFile); + return true; + }, GlobalSearchScope.getScopeRestrictedByFileTypes(GlobalSearchScope.allScope(project), PhpFileType.INSTANCE)); + + Collection elements = new ArrayList<>(); + + for (VirtualFile file : files) { + PsiFile psiFile = PsiManager.getInstance(project).findFile(file); + + if(psiFile == null) { + continue; + } + + elements.add(psiFile); + } + + return elements; + } + + /** + * Find all annotation usages for given class name + * + * Doctrine\ORM\Mapping\Entity => ORM\Entity(), Entity() + * + * @param project current Project + * @param fqnClassName Foobar\ClassName + * @return targets + */ + public static Collection getImplementationsForAnnotation(@NotNull Project project, @NotNull String fqnClassName) { + Collection psiElements = new HashSet<>(); + + for (PsiFile psiFile : getFilesImplementingAnnotation(project, fqnClassName)) { + psiFile.accept(new PhpDocTagAnnotationRecursiveElementWalkingVisitor(pair -> { + if(StringUtils.stripStart(pair.getFirst(), "\\").equalsIgnoreCase(StringUtils.stripStart(fqnClassName, "\\"))) { + psiElements.add(pair.getSecond()); + } + + return false; + })); + } + + return psiElements; + } + /** * matches "@Callback(propertyName="")" */ diff --git a/src/de/espend/idea/php/annotation/util/PhpDocTagAnnotationRecursiveElementWalkingVisitor.java b/src/de/espend/idea/php/annotation/util/PhpDocTagAnnotationRecursiveElementWalkingVisitor.java new file mode 100644 index 00000000..814f409f --- /dev/null +++ b/src/de/espend/idea/php/annotation/util/PhpDocTagAnnotationRecursiveElementWalkingVisitor.java @@ -0,0 +1,82 @@ +package de.espend.idea.php.annotation.util; + +import com.intellij.openapi.util.Pair; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiRecursiveElementWalkingVisitor; +import com.intellij.util.Processor; +import com.jetbrains.php.lang.documentation.phpdoc.psi.tags.PhpDocTag; +import org.apache.commons.lang.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; + +/** + * @author Daniel Espendiller + */ +public class PhpDocTagAnnotationRecursiveElementWalkingVisitor extends PsiRecursiveElementWalkingVisitor { + @NotNull + private final Processor> processor; + + public PhpDocTagAnnotationRecursiveElementWalkingVisitor(@NotNull Processor> processor) { + this.processor = processor; + } + + @Override + public void visitElement(PsiElement element) { + if ((element instanceof PhpDocTag)) { + visitPhpDocTag((PhpDocTag) element); + } + + super.visitElement(element); + } + + private void visitPhpDocTag(@NotNull PhpDocTag phpDocTag) { + // "@var" and user non related tags dont need an action + if(AnnotationUtil.NON_ANNOTATION_TAGS.contains(phpDocTag.getName())) { + return; + } + + String annotationFqnName = StringUtils.stripStart(getClassNameReference(phpDocTag, AnnotationUtil.getUseImportMap(phpDocTag)), "\\"); + + if(annotationFqnName != null && StringUtils.isNotBlank(annotationFqnName)) { + this.processor.process(Pair.create(annotationFqnName, phpDocTag)); + } + } + + @Nullable + private static String getClassNameReference(@NotNull PhpDocTag phpDocTag, @NotNull Map useImports) { + + if(useImports.size() == 0) { + return null; + } + + String annotationName = phpDocTag.getName(); + if(StringUtils.isBlank(annotationName)) { + return null; + } + + if(annotationName.startsWith("@")) { + annotationName = annotationName.substring(1); + } + + String className = annotationName; + String subNamespaceName = ""; + if(className.contains("\\")) { + className = className.substring(0, className.indexOf("\\")); + subNamespaceName = annotationName.substring(className.length()); + } + + if(!useImports.containsKey(className)) { + return null; + } + + // normalize name + String annotationFqnName = useImports.get(className) + subNamespaceName; + if(!annotationFqnName.startsWith("\\")) { + annotationFqnName = "\\" + annotationFqnName; + } + + return annotationFqnName; + } +} \ No newline at end of file diff --git a/tests/de/espend/idea/php/annotation/tests/AnnotationUsageIndexTest.java b/tests/de/espend/idea/php/annotation/tests/AnnotationUsageIndexTest.java new file mode 100644 index 00000000..9bd1f23b --- /dev/null +++ b/tests/de/espend/idea/php/annotation/tests/AnnotationUsageIndexTest.java @@ -0,0 +1,25 @@ +package de.espend.idea.php.annotation.tests; + +import de.espend.idea.php.annotation.AnnotationUsageIndex; + +import java.io.File; + +/** + * @author Daniel Espendiller + * @see de.espend.idea.php.annotation.AnnotationUsageIndex + */ +public class AnnotationUsageIndexTest extends AnnotationLightCodeInsightFixtureTestCase { + public void setUp() throws Exception { + super.setUp(); + myFixture.copyFileToProject("classes.php"); + myFixture.copyFileToProject("usages.php"); + } + + public String getTestDataPath() { + return new File(this.getClass().getResource("fixtures").getFile()).getAbsolutePath(); + } + + public void testThatUsagesAreInIndex() { + assertIndexContains(AnnotationUsageIndex.KEY, "Doctrine\\ORM\\Mapping\\Embedded"); + } +} diff --git a/tests/de/espend/idea/php/annotation/tests/fixtures/usages.php b/tests/de/espend/idea/php/annotation/tests/fixtures/usages.php new file mode 100644 index 00000000..90923063 --- /dev/null +++ b/tests/de/espend/idea/php/annotation/tests/fixtures/usages.php @@ -0,0 +1,36 @@ + + */ +public class AnnotationUsageLineMarkerProviderTest extends AnnotationLightCodeInsightFixtureTestCase { + public void setUp() throws Exception { + super.setUp(); + myFixture.copyFileToProject("AnnotationUsageLineMarkerProvider.php"); + } + + public String getTestDataPath() { + return new File(this.getClass().getResource("fixtures").getFile()).getAbsolutePath(); + } + + public void testThatLineMarkerIsProvidedForAnnotationClass() { + assertLineMarker(PhpPsiElementFactory.createPsiFileFromText(getProject(), "