From 7c3dcff733b372204d83c07cf7d291c51d0951a8 Mon Sep 17 00:00:00 2001 From: Daniel Espendiller Date: Fri, 29 May 2020 21:49:01 +0200 Subject: [PATCH] Add property suggestions from Doctrine @Attribute annotations #112 (https://www.doctrine-project.org/projects/doctrine-annotations/en/latest/custom.html#attribute-types) --- README.md | 20 +++++ .../AnnotationCompletionContributor.java | 22 ++--- .../dict/AnnotationPropertyEnum.java | 24 +++++- .../dict/PhpDocTagAnnotationNonReference.java | 80 +++++++++++++++++++ .../AnnotationGoToDeclarationHandler.java | 10 ++- .../php/annotation/util/AnnotationUtil.java | 42 +++++++++- .../AnnotationCompletionContributorTest.java | 10 +++ .../tests/completion/fixtures/classes.php | 9 +++ .../AnnotationGoToDeclarationHandlerTest.java | 27 +++++++ .../tests/navigation/fixtures/classes.php | 9 +++ .../tests/util/AnnotationUtilTest.java | 33 +++++++- 11 files changed, 265 insertions(+), 21 deletions(-) create mode 100644 src/main/java/de/espend/idea/php/annotation/dict/PhpDocTagAnnotationNonReference.java diff --git a/README.md b/README.md index 6d509738..314a0ccc 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,26 @@ class NotBlank extends Constraint { } ``` +https://www.doctrine-project.org/projects/doctrine-annotations/en/latest/custom.html#attribute-types + +```php +/** + * @Annotation + * @Attributes({ + * @Attribute("stringProperty", type = "string"), + * @Attribute("annotProperty", type = "bool"), + * }) + */ +/** + * @Annotation + * @Attributes( + * @Attribute("stringProperty", type = "string"), + * @Attribute("annotProperty", type = "bool"), + * ) + */ + +```php + ### Annotation Target Detection `@Target` is used to attach annotation, if non provided its added to "ALL list" diff --git a/src/main/java/de/espend/idea/php/annotation/completion/AnnotationCompletionContributor.java b/src/main/java/de/espend/idea/php/annotation/completion/AnnotationCompletionContributor.java index 7b0ac00c..f64dac34 100644 --- a/src/main/java/de/espend/idea/php/annotation/completion/AnnotationCompletionContributor.java +++ b/src/main/java/de/espend/idea/php/annotation/completion/AnnotationCompletionContributor.java @@ -24,7 +24,9 @@ import de.espend.idea.php.annotation.extension.parameter.AnnotationPropertyParameter; import de.espend.idea.php.annotation.extension.parameter.AnnotationVirtualPropertyCompletionParameter; import de.espend.idea.php.annotation.pattern.AnnotationPattern; -import de.espend.idea.php.annotation.util.*; +import de.espend.idea.php.annotation.util.AnnotationUtil; +import de.espend.idea.php.annotation.util.PhpElementsUtil; +import de.espend.idea.php.annotation.util.PhpIndexUtil; import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.NotNull; @@ -160,7 +162,7 @@ protected void addCompletions(@NotNull CompletionParameters parameters, Processi private static class PhpDocAttributeList extends CompletionProvider { @Override - protected void addCompletions(@NotNull CompletionParameters completionParameters, ProcessingContext processingContext, @NotNull CompletionResultSet completionResultSet) { + protected void addCompletions(@NotNull CompletionParameters completionParameters, @NotNull ProcessingContext processingContext, @NotNull CompletionResultSet completionResultSet) { PsiElement psiElement = completionParameters.getOriginalPosition(); if(psiElement == null) { return; @@ -176,11 +178,15 @@ protected void addCompletions(@NotNull CompletionParameters completionParameters return; } - for(Field field: phpClass.getFields()) { - if (field.getModifier().isPublic()) { - attachLookupElement(completionResultSet, field); + AnnotationUtil.visitAttributes(phpClass, (s, s2, psiElement1) -> { + if (psiElement1 instanceof Field) { + attachLookupElement(completionResultSet, (Field) psiElement1); + } else { + completionResultSet.addElement(new PhpAnnotationPropertyLookupElement(new AnnotationProperty(s, AnnotationPropertyEnum.fromString(s)))); } - } + + return null; + }); // extension point for virtual properties AnnotationVirtualPropertyCompletionParameter virtualPropertyParameter = null; @@ -208,10 +214,6 @@ protected void addCompletions(@NotNull CompletionParameters completionParameters } private void attachLookupElement(CompletionResultSet completionResultSet, Field field) { - if(field.isConstant()) { - return; - } - String propertyName = field.getName(); // private $isNillable = false; diff --git a/src/main/java/de/espend/idea/php/annotation/dict/AnnotationPropertyEnum.java b/src/main/java/de/espend/idea/php/annotation/dict/AnnotationPropertyEnum.java index ef6141bb..88dde80d 100644 --- a/src/main/java/de/espend/idea/php/annotation/dict/AnnotationPropertyEnum.java +++ b/src/main/java/de/espend/idea/php/annotation/dict/AnnotationPropertyEnum.java @@ -1,8 +1,30 @@ package de.espend.idea.php.annotation.dict; +import org.jetbrains.annotations.NotNull; + /** * @author Daniel Espendiller */ public enum AnnotationPropertyEnum { - ARRAY, STRING, INTEGER, BOOLEAN + ARRAY, STRING, INTEGER, BOOLEAN, UNKNOWN; + + public static AnnotationPropertyEnum fromString(@NotNull String value) { + if (value.equalsIgnoreCase("string")) { + return STRING; + } + + if (value.equalsIgnoreCase("array")) { + return ARRAY; + } + + if (value.equalsIgnoreCase("integer") || value.equalsIgnoreCase("int")) { + return INTEGER; + } + + if (value.equalsIgnoreCase("boolean") || value.equalsIgnoreCase("bool")) { + return BOOLEAN; + } + + return UNKNOWN; + } } diff --git a/src/main/java/de/espend/idea/php/annotation/dict/PhpDocTagAnnotationNonReference.java b/src/main/java/de/espend/idea/php/annotation/dict/PhpDocTagAnnotationNonReference.java new file mode 100644 index 00000000..d8e6fa3a --- /dev/null +++ b/src/main/java/de/espend/idea/php/annotation/dict/PhpDocTagAnnotationNonReference.java @@ -0,0 +1,80 @@ +package de.espend.idea.php.annotation.dict; + +import com.jetbrains.php.lang.documentation.phpdoc.parser.PhpDocElementTypes; +import com.jetbrains.php.lang.documentation.phpdoc.psi.tags.PhpDocTag; +import com.jetbrains.php.lang.psi.elements.PhpPsiElement; +import com.jetbrains.php.lang.psi.elements.StringLiteralExpression; +import de.espend.idea.php.annotation.pattern.AnnotationPattern; +import de.espend.idea.php.annotation.util.PhpElementsUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Daniel Espendiller + */ +public class PhpDocTagAnnotationNonReference { + final private PhpDocTag phpDocTag; + + public PhpDocTagAnnotationNonReference(@NotNull PhpDocTag phpDocTag) { + this.phpDocTag = phpDocTag; + } + + @NotNull + public PhpDocTag getPhpDocTag() { + return phpDocTag; + } + + /** + * Get property Value from "@Template(template="foo"); + * + * @param propertyName property name template="" + * @return Property value + */ + @Nullable + public String getPropertyValue(String propertyName) { + StringLiteralExpression literalExpression = getPropertyValuePsi(propertyName); + if(literalExpression != null) { + return literalExpression.getContents(); + } + + return null; + } + + /** + * Get property psi element + * + * @param propertyName property name template="" + * @return Property value + */ + @Nullable + public StringLiteralExpression getPropertyValuePsi(String propertyName) { + PhpPsiElement docAttrList = phpDocTag.getFirstPsiChild(); + if(docAttrList != null) { + return PhpElementsUtil.getChildrenOnPatternMatch(docAttrList, AnnotationPattern.getPropertyIdentifierValue(propertyName)); + } + + return null; + } + + /** + * Get default property value from annotation "@Template("foo"); + * + * @return Content of property value literal + */ + @Nullable + public String getDefaultPropertyValue() { + PhpPsiElement phpDocAttrList = phpDocTag.getFirstPsiChild(); + + if(phpDocAttrList != null) { + if(phpDocAttrList.getNode().getElementType() == PhpDocElementTypes.phpDocAttributeList) { + PhpPsiElement phpPsiElement = phpDocAttrList.getFirstPsiChild(); + if(phpPsiElement instanceof StringLiteralExpression) { + return ((StringLiteralExpression) phpPsiElement).getContents(); + } + } + } + + return null; + } + +} diff --git a/src/main/java/de/espend/idea/php/annotation/navigation/AnnotationGoToDeclarationHandler.java b/src/main/java/de/espend/idea/php/annotation/navigation/AnnotationGoToDeclarationHandler.java index 38763888..af06f918 100644 --- a/src/main/java/de/espend/idea/php/annotation/navigation/AnnotationGoToDeclarationHandler.java +++ b/src/main/java/de/espend/idea/php/annotation/navigation/AnnotationGoToDeclarationHandler.java @@ -107,11 +107,13 @@ private void addPropertyGoto(PsiElement psiElement, List targets) { return; } - for(Field field: phpClass.getFields()) { - if(field.getName().equals(property)) { - targets.add(field); + AnnotationUtil.visitAttributes(phpClass, (s, s2, psiElement1) -> { + if(s.equals(property)) { + targets.add(psiElement1); } - } + + return null; + }); // extension point to provide virtual properties / fields targets AnnotationVirtualPropertyTargetsParameter parameter = null; diff --git a/src/main/java/de/espend/idea/php/annotation/util/AnnotationUtil.java b/src/main/java/de/espend/idea/php/annotation/util/AnnotationUtil.java index 63ad45f1..9c0370b4 100644 --- a/src/main/java/de/espend/idea/php/annotation/util/AnnotationUtil.java +++ b/src/main/java/de/espend/idea/php/annotation/util/AnnotationUtil.java @@ -6,13 +6,11 @@ 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.*; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.Processor; +import com.intellij.util.TripleFunction; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.indexing.FileBasedIndex; import com.intellij.util.indexing.FileContent; @@ -670,6 +668,42 @@ public static Collection getActiveImportsAliasesFromSettings() { return ContainerUtil.filter(useAliasOptions, UseAliasOption::isEnabled); } + /** + * Get attributes for @Foo("test", ATTRIBUTE) + * + * "@Attributes(@Attribute("accessControl", type="string"))" + * "@Attributes({@Attribute("accessControl", type="string")})" + * "class foo { public $foo; }" + */ + public static void visitAttributes(@NotNull PhpClass phpClass, TripleFunction fn) { + for (Field field : phpClass.getFields()) { + if(field.getModifier().isPublic() && !field.isConstant()) { + fn.fun(field.getName(), null, field); + } + } + + PhpDocComment docComment = phpClass.getDocComment(); + if (docComment != null) { + for (PhpDocTag phpDocTag : docComment.getTagElementsByName("@Attributes")) { + for (PhpDocTag docTag : PsiTreeUtil.collectElementsOfType(phpDocTag, PhpDocTag.class)) { + String name = docTag.getName(); + if (!"@Attribute".equals(name)) { + continue; + } + + PhpDocTagAnnotationNonReference phpDocAnnotationContainer = new PhpDocTagAnnotationNonReference(docTag); + String defaultPropertyValue = phpDocAnnotationContainer.getDefaultPropertyValue(); + if (defaultPropertyValue == null || StringUtils.isBlank(defaultPropertyValue)) { + continue; + } + + fn.fun(defaultPropertyValue, phpDocAnnotationContainer.getPropertyValue("type"), docTag); + } + } + } + } + + /** * matches "@Callback(propertyName="")" */ diff --git a/src/test/java/de/espend/idea/php/annotation/tests/completion/AnnotationCompletionContributorTest.java b/src/test/java/de/espend/idea/php/annotation/tests/completion/AnnotationCompletionContributorTest.java index 531562a9..a42e7780 100644 --- a/src/test/java/de/espend/idea/php/annotation/tests/completion/AnnotationCompletionContributorTest.java +++ b/src/test/java/de/espend/idea/php/annotation/tests/completion/AnnotationCompletionContributorTest.java @@ -58,6 +58,16 @@ public void testCompletionForProperty() { ); } + public void testCompletionForPropertyInsideAnnotationAttributes() { + assertCompletionContains(PhpFileType.INSTANCE, ")" + + "*/" + + "class Foo {}", + "accessControl", "annotProperty" + ); + } + public void testDocTagCompletionInClassPropertyScope() { assertCompletionContains(PhpFileType.INSTANCE, "o=\"test\") */" + + "}\n", + PlatformPatterns.psiElement(Field.class).withName("foo") + ); + + assertNavigationMatch(PhpFileType.INSTANCE, "Control=\"test\") */" + + "}\n", + PlatformPatterns.psiElement(PhpDocTag.class) + ); + } } diff --git a/src/test/java/de/espend/idea/php/annotation/tests/navigation/fixtures/classes.php b/src/test/java/de/espend/idea/php/annotation/tests/navigation/fixtures/classes.php index c6f18adb..6614e3e7 100644 --- a/src/test/java/de/espend/idea/php/annotation/tests/navigation/fixtures/classes.php +++ b/src/test/java/de/espend/idea/php/annotation/tests/navigation/fixtures/classes.php @@ -4,6 +4,15 @@ { /** * @Annotation + * + * @Attributes({ + * @Attribute("stringProperty", type = "string"), + * @Attribute("annotProperty", type = "SomeAnnotationClass"), + * }) + * + * @Attributes( + * @Attribute("accessControl", type="string"), + * ) */ class Bar { diff --git a/src/test/java/de/espend/idea/php/annotation/tests/util/AnnotationUtilTest.java b/src/test/java/de/espend/idea/php/annotation/tests/util/AnnotationUtilTest.java index ad505f42..756cbac4 100644 --- a/src/test/java/de/espend/idea/php/annotation/tests/util/AnnotationUtilTest.java +++ b/src/test/java/de/espend/idea/php/annotation/tests/util/AnnotationUtilTest.java @@ -12,8 +12,6 @@ import de.espend.idea.php.annotation.util.AnnotationUtil; import java.util.*; -import java.util.function.Function; -import java.util.stream.Collectors; /** * @author Daniel Espendiller @@ -232,4 +230,35 @@ public void testThatImportForClassIsSuggestedForAliasImportClass() { Map possibleImportClasses = AnnotationUtil.getPossibleImportClasses(phpDocTag); assertEquals("ORM", possibleImportClasses.get("\\Doctrine\\ORM\\Mapping")); } + + public void testAttributeVisitingForAnnotationClass() { + myFixture.copyFileToProject("doctrine.php"); + + PhpClass phpClass = PhpPsiElementFactory.createFromText(getProject(), PhpClass.class, " attributes = new HashSet<>(); + + AnnotationUtil.visitAttributes(phpClass, (s, s2, psiElement) -> { + attributes.add(s); + return null; + }); + + assertContainsElements(attributes, "accessControl", "accessControl2", "array", "array2", "foo"); + } }