Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow annotation queries to happen in ANNOTATION location #107

Merged
merged 3 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading