Skip to content

Commit

Permalink
Add property suggestions from Doctrine @Attribute annotations #112 (h…
Browse files Browse the repository at this point in the history
  • Loading branch information
Haehnchen committed May 29, 2020
1 parent a299719 commit 7c3dcff
Show file tree
Hide file tree
Showing 11 changed files with 265 additions and 21 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -160,7 +162,7 @@ protected void addCompletions(@NotNull CompletionParameters parameters, Processi
private static class PhpDocAttributeList extends CompletionProvider<CompletionParameters> {

@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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,30 @@
package de.espend.idea.php.annotation.dict;

import org.jetbrains.annotations.NotNull;

/**
* @author Daniel Espendiller <daniel@espendiller.net>
*/
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;
}
}
Original file line number Diff line number Diff line change
@@ -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 <daniel@espendiller.net>
*/
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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,13 @@ private void addPropertyGoto(PsiElement psiElement, List<PsiElement> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -670,6 +668,42 @@ public static Collection<UseAliasOption> getActiveImportsAliasesFromSettings() {
return ContainerUtil.filter(useAliasOptions, UseAliasOption::isEnabled);
}

/**
* Get attributes for @Foo("test", ATTR<caret>IBUTE)
*
* "@Attributes(@Attribute("accessControl", type="string"))"
* "@Attributes({@Attribute("accessControl", type="string")})"
* "class foo { public $foo; }"
*/
public static void visitAttributes(@NotNull PhpClass phpClass, TripleFunction<String, String, PsiElement, Void> 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="<value>")"
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ public void testCompletionForProperty() {
);
}

public void testCompletionForPropertyInsideAnnotationAttributes() {
assertCompletionContains(PhpFileType.INSTANCE, "<?php\n" +
"/**" +
"* @\\My\\Annotations\\All(\"a\",<caret>)" +
"*/" +
"class Foo {}",
"accessControl", "annotProperty"
);
}

public void testDocTagCompletionInClassPropertyScope() {
assertCompletionContains(PhpFileType.INSTANCE, "<?php\n" +
"class Foo {\n" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@
/**
* @Annotation
* @Target("ALL")
*
* @Attributes({
* @Attribute("stringProperty", type = "string"),
* @Attribute("annotProperty", type = "SomeAnnotationClass"),
* })
*
* @Attributes(
* @Attribute("accessControl", type="string"),
* )
*/
class All
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.intellij.patterns.PlatformPatterns;
import com.jetbrains.php.lang.PhpFileType;
import com.jetbrains.php.lang.documentation.phpdoc.psi.tags.PhpDocTag;
import com.jetbrains.php.lang.psi.elements.Field;
import com.jetbrains.php.lang.psi.elements.PhpClass;
import de.espend.idea.php.annotation.tests.AnnotationLightCodeInsightFixtureTestCase;
Expand Down Expand Up @@ -179,4 +180,30 @@ public void testThatClassContainsProvidesNavigation() {
PlatformPatterns.psiElement(PhpClass.class).withName("Bar")
);
}

public void testNavigationForPropertyInsideAnnotationAttributes() {
assertNavigationMatch(PhpFileType.INSTANCE, "<?php\n" +
"namespace Bar;\n" +
"\n" +
"use Foo\\Bar;\n" +
"\n" +
"class Foo\n" +
"{\n" +
" /** @Bar(fo<caret>o=\"test\") */" +
"}\n",
PlatformPatterns.psiElement(Field.class).withName("foo")
);

assertNavigationMatch(PhpFileType.INSTANCE, "<?php\n" +
"namespace Bar;\n" +
"\n" +
"use Foo\\Bar;\n" +
"\n" +
"class Foo\n" +
"{\n" +
" /** @Bar(access<caret>Control=\"test\") */" +
"}\n",
PlatformPatterns.psiElement(PhpDocTag.class)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@
{
/**
* @Annotation
*
* @Attributes({
* @Attribute("stringProperty", type = "string"),
* @Attribute("annotProperty", type = "SomeAnnotationClass"),
* })
*
* @Attributes(
* @Attribute("accessControl", type="string"),
* )
*/
class Bar
{
Expand Down
Loading

0 comments on commit 7c3dcff

Please sign in to comment.