Skip to content

Commit

Permalink
Provide Annotation class usage linemarker #79, [API] Provide an index…
Browse files Browse the repository at this point in the history
… with annotated elements stubs #53
  • Loading branch information
Haehnchen committed Sep 15, 2017
1 parent 34acbf2 commit 1ec3eea
Show file tree
Hide file tree
Showing 8 changed files with 406 additions and 1 deletion.
3 changes: 3 additions & 0 deletions META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@

<projectService serviceImplementation="de.espend.idea.php.annotation.Settings"/>
<fileBasedIndex implementation="de.espend.idea.php.annotation.AnnotationStubIndex"/>
<fileBasedIndex implementation="de.espend.idea.php.annotation.AnnotationUsageIndex"/>

<gotoDeclarationHandler implementation="de.espend.idea.php.annotation.navigation.AnnotationGoToDeclarationHandler"/>
<completion.contributor language="PHP" implementationClass="de.espend.idea.php.annotation.completion.AnnotationCompletionContributor"/>
Expand All @@ -176,6 +177,8 @@

<annotator language="PHP" implementationClass="de.espend.idea.php.annotation.annotator.AnnotationDocTagAnnotator"/>

<codeInsight.lineMarkerProvider language="PHP" implementationClass="de.espend.idea.php.annotation.navigation.AnnotationUsageLineMarkerProvider"/>

<localInspection groupPath="PHP" shortName="AnnotationMissingUseInspection" displayName="Missing Import"
groupName="Annotation"
enabledByDefault="true"
Expand Down
174 changes: 174 additions & 0 deletions src/de/espend/idea/php/annotation/AnnotationUsageIndex.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package de.espend.idea.php.annotation;

import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiRecursiveElementWalkingVisitor;
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.documentation.phpdoc.psi.tags.PhpDocTag;
import com.jetbrains.php.lang.psi.PhpFile;
import de.espend.idea.php.annotation.util.AnnotationUtil;
import gnu.trove.THashMap;
import gnu.trove.THashSet;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

/**
* @author Daniel Espendiller <daniel@espendiller.net>
*/
public class AnnotationUsageIndex extends FileBasedIndexExtension<String, Set<String>> {
public static final ID<String, Set<String>> KEY = ID.create("espend.php.annotation.usage");
private final KeyDescriptor<String> myKeyDescriptor = new EnumeratorStringDescriptor();
private static StringSetDataExternalizer EXTERNALIZER = new StringSetDataExternalizer();

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

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

PsiFile psiFile = inputData.getPsiFile();
if(!(psiFile instanceof PhpFile)) {
return map;
}

if(!AnnotationUtil.isValidForIndex(inputData)) {
return map;
}

psiFile.accept(new PsiRecursiveElementWalkingVisitor() {
@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)) {
map.put(annotationFqnName, new HashSet<>());
}
}
});

return map;
}
};
}

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

@NotNull
@Override
public DataExternalizer<Set<String>> 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;
}

@Nullable
private static String getClassNameReference(@NotNull PhpDocTag phpDocTag, @NotNull Map<String, String> 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;
}

private static class StringSetDataExternalizer implements DataExternalizer<Set<String>> {
public synchronized void save(@NotNull DataOutput out, Set<String> 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<String> read(@NotNull DataInput in) throws IOException {
Set<String> set = new THashSet<>();

for(int r = in.readInt(); r > 0; --r) {
set.add(EnumeratorStringDescriptor.INSTANCE.read(in));
}

return set;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
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.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.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 org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;

/**
* @author Daniel Espendiller <daniel@espendiller.net>
*/
public class AnnotationUsageLineMarkerProvider implements LineMarkerProvider {
@Nullable
@Override
public LineMarkerInfo getLineMarkerInfo(@NotNull PsiElement psiElement) {
return null;
}

@Override
public void collectSlowLineMarkers(@NotNull List<PsiElement> psiElements, @NotNull Collection<LineMarkerInfo> results) {
for(PsiElement psiElement: psiElements) {
if(!getClassNamePattern().accepts(psiElement)) {
continue;
}

PsiElement phpClass = psiElement.getContext();
if(!(phpClass instanceof PhpClass)) {
return;
}

String fqn = StringUtils.stripStart(((PhpClass) phpClass).getFQN(), "\\");

Collection<VirtualFile> files = new HashSet<>();

FileBasedIndex.getInstance().getFilesWithKey(AnnotationUsageIndex.KEY, new HashSet<>(Collections.singletonList(fqn)), virtualFile -> {
files.add(virtualFile);
return true;
}, GlobalSearchScope.getScopeRestrictedByFileTypes(GlobalSearchScope.allScope(psiElement.getProject()), PhpFileType.INSTANCE));


if(files.size() > 0) {
NavigationGutterIconBuilder<PsiElement> 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<PsiElement> 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<Collection<? extends PsiElement>> {
@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<? extends PsiElement> compute() {
Collection<VirtualFile> files = new HashSet<>();

FileBasedIndex.getInstance().getFilesWithKey(AnnotationUsageIndex.KEY, new HashSet<>(Collections.singletonList(fqn)), virtualFile -> {
files.add(virtualFile);
return true;
}, GlobalSearchScope.getScopeRestrictedByFileTypes(GlobalSearchScope.allScope(project), PhpFileType.INSTANCE));

Collection<PsiElement> elements = new ArrayList<>();

for (VirtualFile file : files) {
PsiFile psiFile = PsiManager.getInstance(project).findFile(file);

if(psiFile == null) {
continue;
}

elements.add(psiFile);
}

return elements;
}
}
}
Original file line number Diff line number Diff line change
@@ -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 <daniel@espendiller.net>
* @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");
}
}
36 changes: 36 additions & 0 deletions tests/de/espend/idea/php/annotation/tests/fixtures/usages.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace Doctrine\ORM\Mapping
{
/**
* @Annotation
* @Target("PROPERTY")
*/
final class Embedded implements Annotation
{
}

/**
* @Annotation
* @Target("PROPERTY")
*/
final class CustomIdGenerator implements Annotation
{
}
}

namespace My\Model
{
use \Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Embedded
*/
class Item
{
/**
* @ORM\CustomIdGenerator
*/
private $foo;
}
}

0 comments on commit 1ec3eea

Please sign in to comment.