Skip to content

Commit

Permalink
Allow annotation queries to happen in ANNOTATION location (#107)
Browse files Browse the repository at this point in the history
* Allow annotation queries to happen in ANNOTATION location

Signed-off-by: Juan Manuel Leflet Estrada <[email protected]>

* Annotation query can have empty pattern

Signed-off-by: Juan Manuel Leflet Estrada <[email protected]>

* Allow for inspection of annotations within annotations

Refactor and cleanup

Signed-off-by: Juan Manuel Leflet Estrada <[email protected]>

---------

Signed-off-by: Juan Manuel Leflet Estrada <[email protected]>
  • Loading branch information
jmle authored Sep 12, 2024
1 parent 602c3b0 commit 2261dca
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ public RuleEntryParams(final String commandId, final List<Object> arguments) {

this.projectName = (String) obj.get("project");
this.query = (String) obj.get("query");
this.annotationQuery = AnnotationQuery.fromMap((Map<String, Object>) obj.get("annotationQuery"));
this.location = Integer.parseInt((String) obj.get("location"));
this.annotationQuery = AnnotationQuery.fromMap(this.query, (Map<String, Object>) obj.get("annotationQuery"), location);
this.analysisMode = (String) obj.get("analysisMode");
this.includedPaths = (ArrayList<String>) obj.get("includedPaths");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

/**
* Represents additional query information to inspect annotations in annotated symbols.
Expand All @@ -14,14 +15,20 @@ public class AnnotationQuery {
*/
private String type;

/**
* Indicates whether this AnnotationQuery is done on an annotation (location == ANNOTATION)
*/
private boolean isOnAnnotation;

/**
* The elements within the annotation, ie, "value" in <code>@BeanAnnotation(value = "value")</code>
*/
private Map<String, String> elements;

public AnnotationQuery(String type, Map<String, String> elements) {
public AnnotationQuery(String type, Map<String, String> elements, boolean isOnAnnotation) {
this.type = type;
this.elements = elements;
this.isOnAnnotation = isOnAnnotation;
}

public String getType() {
Expand All @@ -32,20 +39,37 @@ public Map<String, String> getElements() {
return elements;
}

public static AnnotationQuery fromMap(Map<String, Object> query) {
if (query == null) {
public boolean isOnAnnotation() {
return this.isOnAnnotation;
}

/**
* Checks whether the query matches against a given annotation
*/
public boolean matchesAnnotation(String annotation) {
// If the annotation query is happening on an annotation, the annotation field in the annotation query can be null
if (isOnAnnotation() && getType() == null) {
return true;
} else {
return Pattern.matches(getType(), annotation);
}
}

public static AnnotationQuery fromMap(String query, Map<String, Object> annotationQuery, int location) {
if (annotationQuery == null) {
return null;
}

String typePattern = (String) query.get("pattern");
boolean isOnAnnotation = location == 4;
String typePattern = isOnAnnotation && annotationQuery.get("pattern").equals("") ? query : (String) annotationQuery.get("pattern");;
final Map<String, String> elements = new HashMap<>();
List<Map<String, String>> mapElements = (List<Map<String, String>>) query.get("elements");
List<Map<String, String>> mapElements = (List<Map<String, String>>) annotationQuery.get("elements");
for (int i = 0; mapElements != null && i < mapElements.size(); i++) {
String key = mapElements.get(i).get("name");
String value = mapElements.get(i).get("value");
elements.put(key, value);
}

return new AnnotationQuery(typePattern, elements);
return new AnnotationQuery(typePattern, elements, isOnAnnotation);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,22 @@
import java.util.ArrayList;
import java.util.List;

import io.konveyor.tackle.core.internal.query.AnnotationQuery;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jdt.core.IAnnotatable;
import org.eclipse.jdt.core.IAnnotation;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.search.SearchMatch;
import org.eclipse.jdt.internal.core.ResolvedSourceField;
import org.eclipse.jdt.internal.core.ResolvedSourceMethod;
import org.eclipse.jdt.internal.core.ResolvedSourceType;
import org.eclipse.jdt.internal.core.SourceRefElement;
import org.eclipse.lsp4j.SymbolInformation;

public class AnnotationSymbolProvider implements SymbolProvider {
public class AnnotationSymbolProvider implements SymbolProvider, WithAnnotationQuery {

private AnnotationQuery annotationQuery;

@Override
public List<SymbolInformation> get(SearchMatch match) throws CoreException {
List<SymbolInformation> symbols = new ArrayList<>();
Expand All @@ -25,12 +33,31 @@ public List<SymbolInformation> get(SearchMatch match) throws CoreException {
symbol.setKind(convertSymbolKind(element));
symbol.setContainerName(annotation.getParent().getElementName());
symbol.setLocation(getLocation(element, match));
symbols.add(symbol);

if (annotationQuery != null) {
List<Class<? extends SourceRefElement>> classes = new ArrayList<>();
classes.add(ResolvedSourceMethod.class);
classes.add(ResolvedSourceField.class);
classes.add(ResolvedSourceType.class);
if (matchesAnnotationQuery(match, classes)) {
symbols.add(symbol);
}
} else {
symbols.add(symbol);
}
}
return symbols;
} catch (Exception e) {
logInfo("unable to match for annotations: " + e);
return null;
}
}

public AnnotationQuery getAnnotationQuery() {
return annotationQuery;
}

public void setAnnotationQuery(AnnotationQuery annotationQuery) {
this.annotationQuery = annotationQuery;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@

import io.konveyor.tackle.core.internal.query.AnnotationQuery;
import org.eclipse.jdt.core.IAnnotation;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IImportDeclaration;
import org.eclipse.jdt.core.IMemberValuePair;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.search.SearchMatch;
import org.eclipse.jdt.internal.core.Annotation;
import org.eclipse.jdt.internal.core.CompilationUnit;
import org.eclipse.jdt.internal.core.SourceRefElement;

import java.util.Arrays;
Expand Down Expand Up @@ -51,34 +49,26 @@ default boolean matchesAnnotationQuery(SearchMatch match, List<Class<? extends S
for (IAnnotation annotation : annotations) {
// See if the annotation's name matches the pattern given in the query for the annotation
String fqn = getFQN(annotation);
if (Pattern.matches(getAnnotationQuery().getType(), fqn)) {
// If the query has annotation elements to check, iterate through the annotation's values and check
if (getAnnotationQuery().getElements() != null && !getAnnotationQuery().getElements().entrySet().isEmpty()) {
IMemberValuePair[] memberValuePairs = annotation.getMemberValuePairs();
Set<Map.Entry<String, String>> annotationElements = getAnnotationQuery().getElements().entrySet();
for (IMemberValuePair member : memberValuePairs) {
for (Map.Entry<String, String> annotationElement : annotationElements) {
if (annotationElement.getKey().equals(member.getMemberName())) {
// Member values can be arrays. In this case, lets iterate over it and compare:
if (member.getValue() instanceof Object[]) {
Object[] values = (Object[]) member.getValue();
// TODO: at the moment we are just toString()ing the values.
// We might want to make this more sophisticated, relying on
// member.getValueKind() to match on specific kinds.
return Arrays.stream(values).anyMatch(v -> Pattern.matches(annotationElement.getValue(), v.toString()));
} else {
if (Pattern.matches(annotationElement.getValue(), member.getValue().toString())) {
return true;
}
if (getAnnotationQuery().matchesAnnotation(fqn)) {
return doElementsMatch((Annotation) annotation);
} else {
// The LS doesn't seem to be able to match on annotations within annotations, but
// if the main annotation doesn't match, there might be some annotations inside:
for (IMemberValuePair member : annotation.getMemberValuePairs()) {
if (member.getValueKind() == IMemberValuePair.K_ANNOTATION) {
if (member.getValue() instanceof Object[]) {
Object[] objs = (Object[]) member.getValue();
for (int i = 0; i < objs.length; i++) {
Annotation innerAnnotation = (Annotation) objs[i];
fqn = getFQN(innerAnnotation);
if (getAnnotationQuery().matchesAnnotation(fqn)) {
return doElementsMatch(innerAnnotation);
}
}
}

}
} else {
// No annotation elements, but the annotation itself matches
return true;
}

}
}
return false;
Expand All @@ -91,6 +81,94 @@ default boolean matchesAnnotationQuery(SearchMatch match, List<Class<? extends S
return true;
}

/**
* This relatively complicated function checks whether the elements of the annotation query match
* the elements of the actual annotation being inspected.
*
* Java annotations can have all sorts of elements inside: Strings, Arrays, or even other annotations:
* <pre>
* {@code
* @DataSourceDefinitions({
* @DataSourceDefinition(
* name = "jdbc/multiple-ds-xa",
* className="com.example.MyDataSource",
* portNumber=6689,
* serverName="example.com",
* user="lance",
* password="secret"
* ),
* @DataSourceDefinition(
* name = "jdbc/multiple-ds-non-xa",
* className="com.example.MyDataSource",
* portNumber=6689,
* serverName="example.com",
* user="lance",
* password="secret",
* transactional = false
* ),
* })
* public class AnnotationMultipleDs {
* }
*
* </pre>
*
* If we have a query like this:
* <pre>
* when:
* java.referenced:
* location: ANNOTATION
* pattern: javax.annotation.sql.DataSourceDefinition
* annotated:
* elements:
* - name: transactional
* value: false
* </pre>
* we need to check if the annotation elements (the different config properties defined within the annotation)
* match the one(s) in the query. The query can define more than one element, as shown by the fact that the
* "elements" node is an array, therefore, if multiple elements are present in the query, all must be matched.
*
* @param annotation the annotation to inspect
* @return a boolean indicating whether the elements match
* @throws JavaModelException
*/
private boolean doElementsMatch(Annotation annotation) throws JavaModelException {
// If the query has annotation elements to check, iterate through the annotation's values and check
if (getAnnotationQuery().getElements() != null && !getAnnotationQuery().getElements().entrySet().isEmpty()) {
IMemberValuePair[] memberValuePairs = annotation.getMemberValuePairs();
Set<Map.Entry<String, String>> ruleAnnotationElems = getAnnotationQuery().getElements().entrySet();
boolean allElementsMatch = true;
boolean oneElementMatched = false;
// TODO: there is a problem with defaults: they don't appear in the memberValuePairs so they cannot be matched
for (int i = 0; i < memberValuePairs.length && allElementsMatch; i++) {
IMemberValuePair member = memberValuePairs[i];
for (Map.Entry<String, String> ruleAnnotationElem : ruleAnnotationElems) {
String ruleAnnotationElementName = ruleAnnotationElem.getKey();
if (ruleAnnotationElementName.equals(member.getMemberName())) {
// Member values can be arrays. In this case, lets iterate over it and compare:
if (member.getValue() instanceof Object[]) {
Object[] values = (Object[]) member.getValue();
// TODO: at the moment we are just toString()ing the values.
// We might want to make this more sophisticated, relying on
// member.getValueKind() to match on specific kinds. This however can match
boolean valueMatches = Arrays.stream(values).anyMatch(v -> Pattern.matches(ruleAnnotationElem.getValue(), v.toString()));
oneElementMatched |= valueMatches;
allElementsMatch &= valueMatches;
} else {
boolean valueMatches = Pattern.matches(ruleAnnotationElem.getValue(), member.getValue().toString());
oneElementMatched |= valueMatches;
allElementsMatch &= valueMatches;
}
}
}
}
return oneElementMatched && allElementsMatch;
}

// No annotation elements, but the annotation itself matches
return true;

}

private IAnnotation[] tryToGetAnnotations(SourceRefElement t) {
try {
return t.getAnnotations();
Expand Down

0 comments on commit 2261dca

Please sign in to comment.