Skip to content

GH-1330: Explain AOP annotations with copilot #1338

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

Merged
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 @@ -10,6 +10,8 @@
*******************************************************************************/
package org.springframework.ide.vscode.boot.java;

import java.util.Map;

/**
* Constants containing various fully-qualified annotation names.
*
Expand Down Expand Up @@ -85,6 +87,16 @@ public class Annotations {

public static final String SCHEDULED = "org.springframework.scheduling.annotation.Scheduled";

public static final Map<String, String> AOP_ANNOTATIONS = Map.of(
"org.aspectj.lang.annotation.Pointcut", "Pointcut",
"org.aspectj.lang.annotation.Before", "Before",
"org.aspectj.lang.annotation.Around", "Around",
"org.aspectj.lang.annotation.After", "After",
"org.aspectj.lang.annotation.AfterReturning", "AfterReturning",
"org.aspectj.lang.annotation.AfterThrowing", "AfterThrowing",
"org.aspectj.lang.annotation.DeclareParents", "DeclareParents"
);



}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
import org.springframework.ide.vscode.boot.java.handlers.CodeLensProvider;
import org.springframework.ide.vscode.boot.java.handlers.HighlightProvider;
import org.springframework.ide.vscode.boot.java.handlers.HoverProvider;
import org.springframework.ide.vscode.boot.java.handlers.QueryCodeLensProvider;
import org.springframework.ide.vscode.boot.java.handlers.CopilotCodeLensProvider;
import org.springframework.ide.vscode.boot.java.handlers.ReferenceProvider;
import org.springframework.ide.vscode.boot.java.links.SourceLinks;
import org.springframework.ide.vscode.boot.java.livehover.ActiveProfilesProvider;
Expand Down Expand Up @@ -319,7 +319,7 @@ protected ReferencesHandler createReferenceHandler(SimpleLanguageServer server,
protected BootJavaCodeLensEngine createCodeLensEngine(SpringSymbolIndex index, JavaProjectFinder projectFinder, SimpleLanguageServer server, SpelSemanticTokens spelSemanticTokens) {
Collection<CodeLensProvider> codeLensProvider = new ArrayList<>();
codeLensProvider.add(new WebfluxHandlerCodeLensProvider(index));
codeLensProvider.add(new QueryCodeLensProvider(projectFinder, server, spelSemanticTokens));
codeLensProvider.add(new CopilotCodeLensProvider(projectFinder, server, spelSemanticTokens));

return new BootJavaCodeLensEngine(this, codeLensProvider);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
Expand All @@ -25,13 +27,17 @@
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.MemberValuePair;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.lsp4j.CodeLens;
import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.jsonrpc.CancelChecker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.boot.java.Annotations;
import org.springframework.ide.vscode.boot.java.spel.AnnotationParamSpelExtractor;
import org.springframework.ide.vscode.boot.java.spel.AnnotationParamSpelExtractor.Snippet;
import org.springframework.ide.vscode.boot.java.spel.SpelSemanticTokens;
Expand All @@ -49,9 +55,9 @@
/**
* @author Udayani V
*/
public class QueryCodeLensProvider implements CodeLensProvider {
public class CopilotCodeLensProvider implements CodeLensProvider {

protected static Logger logger = LoggerFactory.getLogger(QueryCodeLensProvider.class);
protected static Logger logger = LoggerFactory.getLogger(CopilotCodeLensProvider.class);

public static final String CMD_ENABLE_COPILOT_FEATURES = "sts/enable/copilot/features";

Expand All @@ -66,13 +72,13 @@ public class QueryCodeLensProvider implements CodeLensProvider {
private SpelSemanticTokens spelSemanticTokens;

private static boolean showCodeLenses;

public QueryCodeLensProvider(JavaProjectFinder projectFinder, SimpleLanguageServer server, SpelSemanticTokens spelSemanticTokens) {
public CopilotCodeLensProvider(JavaProjectFinder projectFinder, SimpleLanguageServer server, SpelSemanticTokens spelSemanticTokens) {
this.projectFinder = projectFinder;
this.spelSemanticTokens = spelSemanticTokens;
server.onCommand(CMD_ENABLE_COPILOT_FEATURES, params -> {
if (params.getArguments().get(0) instanceof JsonPrimitive) {
QueryCodeLensProvider.showCodeLenses = ((JsonPrimitive) params.getArguments().get(0)).getAsBoolean();
CopilotCodeLensProvider.showCodeLenses = ((JsonPrimitive) params.getArguments().get(0)).getAsBoolean();
}
return CompletableFuture.completedFuture(showCodeLenses);
});
Expand All @@ -84,53 +90,66 @@ public void provideCodeLenses(CancelChecker cancelToken, TextDocument document,
if (!showCodeLenses) {
return;
}

Map<String, String> pointcutMap = findPointcuts(cu);

cu.accept(new ASTVisitor() {

@Override
public boolean visit(SingleMemberAnnotation node) {
Arrays.stream(spelExtractors).map(e -> e.getSpelRegion(node)).filter(o -> o.isPresent())
.map(o -> o.get()).forEach(snippet -> {
String additionalContext = parseSpelAndFetchContext(cu, snippet.text());
provideCodeLensForSpelExpression(cancelToken, node, document, snippet,
additionalContext, resultAccumulator);
provideCodeLensForSpelExpression(cancelToken, node, document, snippet, additionalContext, resultAccumulator);
});

if (isQueryAnnotation(node)) {
String queryPrompt = determineQueryPrompt(document);
provideCodeLensForQuery(cancelToken, node, document, node.getValue(), queryPrompt,
resultAccumulator);
QueryType queryType = determineQueryType(document);
provideCodeLensForExpression(cancelToken, node, document, queryType, "", resultAccumulator);
} else if (isAopAnnotation(node)) {
String additionalPointcutContext = extractPointcutReference(node.getValue(), pointcutMap);
provideCodeLensForExpression(cancelToken, node, document, QueryType.AOP, additionalPointcutContext, resultAccumulator);
}

return super.visit(node);
}

@Override
public boolean visit(NormalAnnotation node) {


Arrays.stream(spelExtractors).map(e -> e.getSpelRegion(node)).filter(o -> o.isPresent())
.map(o -> o.get()).forEach(snippet -> {
String additionalContext = parseSpelAndFetchContext(cu, snippet.text());
provideCodeLensForSpelExpression(cancelToken, node, document, snippet, additionalContext,
resultAccumulator);
provideCodeLensForSpelExpression(cancelToken, node, document, snippet, additionalContext, resultAccumulator);
});

if (isQueryAnnotation(node)) {
String queryPrompt = determineQueryPrompt(document);
for (Object value : node.values()) {
if (value instanceof MemberValuePair) {
MemberValuePair pair = (MemberValuePair) value;
if ("value".equals(pair.getName().getIdentifier())) {
provideCodeLensForQuery(cancelToken, node, document, pair.getValue(), queryPrompt,
resultAccumulator);
break;
}
}
QueryType queryType = determineQueryType(document);
provideCodeLensForExpression(cancelToken, node, document, queryType, "", resultAccumulator);
} else if (isAopAnnotation(node)) {
Expression value = getMemberValue(node);
String additionalPointcutContext = null;
if (value != null) {
additionalPointcutContext = extractPointcutReference(value, pointcutMap);
}
provideCodeLensForExpression(cancelToken, node, document, QueryType.AOP, additionalPointcutContext, resultAccumulator);
}

return super.visit(node);
}

private Expression getMemberValue(NormalAnnotation node) {
for (Object value : node.values()) {
if (value instanceof MemberValuePair) {
MemberValuePair pair = (MemberValuePair) value;
if ("pointcut".equals(pair.getName().getIdentifier())) {
return pair.getValue();
}
}
}
return null;
}

});
}

Expand Down Expand Up @@ -163,20 +182,26 @@ protected void provideCodeLensForSpelExpression(CancelChecker cancelToken, Annot
}
}

protected void provideCodeLensForQuery(CancelChecker cancelToken, Annotation node, TextDocument document,
Expression valueExp, String query, List<CodeLens> resultAccumulator) {
protected void provideCodeLensForExpression(CancelChecker cancelToken, Annotation node, TextDocument document,
QueryType queryType, String additionalContext, List<CodeLens> resultAccumulator) {
cancelToken.checkCanceled();

if (valueExp != null) {
if (node != null) {
try {


String context = additionalContext != null && !additionalContext.isEmpty() ? String.format(
"""
This is the pointcut definition referenced in the above annotation. \n\n %s \n\nProvide a brief summary of the pointcut's role within the annotation.
Avoid detailed implementation steps and avoid repeating information covered earlier.
""",additionalContext) : "";

CodeLens codeLens = new CodeLens();
codeLens.setRange(document.toRange(valueExp.getStartPosition(), valueExp.getLength()));
codeLens.setRange(document.toRange(node.getStartPosition(), node.getLength()));

Command cmd = new Command();
cmd.setTitle(QueryType.DEFAULT.getTitle());
cmd.setTitle(queryType.getTitle());
cmd.setCommand(CMD);
cmd.setArguments(ImmutableList.of(query + valueExp.toString()));
cmd.setArguments(ImmutableList.of(queryType.getPrompt() + node.toString() + "\n\n" +context));
codeLens.setCommand(cmd);

resultAccumulator.add(codeLens);
Expand All @@ -190,15 +215,15 @@ private static boolean isQueryAnnotation(Annotation a) {
return FQN_QUERY.equals(a.getTypeName().getFullyQualifiedName())
|| QUERY.equals(a.getTypeName().getFullyQualifiedName());
}

private String determineQueryPrompt(TextDocument document) {
private QueryType determineQueryType(TextDocument document) {
Optional<IJavaProject> optProject = projectFinder.find(document.getId());
if (optProject.isPresent()) {
IJavaProject jp = optProject.get();
return SpringProjectUtil.hasDependencyStartingWith(jp, "hibernate-core", null) ? QueryType.HQL.getPrompt()
: QueryType.JPQL.getPrompt();
return SpringProjectUtil.hasDependencyStartingWith(jp, "hibernate-core", null) ? QueryType.HQL
: QueryType.JPQL;
}
return QueryType.DEFAULT.getPrompt();
return QueryType.DEFAULT;
}

private String parseSpelAndFetchContext(CompilationUnit cu, String spelExpression) {
Expand Down Expand Up @@ -238,4 +263,49 @@ public boolean visit(MethodDeclaration node) {
return methodContext;
}

private boolean isAopAnnotation(Annotation a) {
String annotationFQN = a.getTypeName().getFullyQualifiedName();
return Annotations.AOP_ANNOTATIONS.containsKey(annotationFQN)
|| Annotations.AOP_ANNOTATIONS.containsValue(annotationFQN);
}

private Map<String, String> findPointcuts(CompilationUnit cu) {
Map<String, String> pointcutMap = new HashMap<>();
cu.accept(new ASTVisitor() {
@Override
public boolean visit(MethodDeclaration node) {
for (Object modifierObj : node.modifiers()) {
if (modifierObj instanceof Annotation) {
Annotation annotation = (Annotation) modifierObj;
if ("Pointcut".equals(annotation.getTypeName().getFullyQualifiedName())) {
String methodName = node.getName().getIdentifier();
pointcutMap.put(methodName, node.toString());
}
}
}
return super.visit(node);
}
});
return pointcutMap;

}

private String extractPointcutReference(org.eclipse.jdt.core.dom.Expression expression, Map<String, String> pointcutMap) {
if (expression instanceof MethodInvocation) {
return ((MethodInvocation) expression).getName().getIdentifier();
} else if (expression instanceof SimpleName) {
return ((SimpleName) expression).getIdentifier();
} else if (expression instanceof StringLiteral) {
String literalValue = ((StringLiteral) expression).getLiteralValue();
StringBuilder pointcuts = new StringBuilder();
for (Map.Entry<String, String> entry : pointcutMap.entrySet()) {
if (literalValue.contains(entry.getKey())) {
pointcuts.append(entry.getValue());
}
}
return pointcuts.toString();
}
return null;
}

}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package org.springframework.ide.vscode.boot.java.handlers;

public enum QueryType {
SPEL("Explain SpEL Expression using Copilot", "Explain the following SpEL Expression with a clear summary first, followed by a breakdown of the expression with details: \n\n"),
JPQL("Explain Query using Copilot", "Explain the following JPQL query with a clear summary first, followed by a detailed explanation. If the query contains any SpEL expressions, explain those parts as well: \n\n"),
HQL("Explain Query using Copilot", "Explain the following HQL query with a clear summary first, followed by a detailed explanation. If the query contains any SpEL expressions, explain those parts as well: \n\n"),
DEFAULT("Explain Query using Copilot", "Explain the following query with a clear summary first, followed by a detailed explanation: \n\n");
SPEL("Explain SpEL Expression with Copilot", "Explain the following SpEL Expression with a clear summary first, followed by a breakdown of the expression with details: \n\n"),
JPQL("Explain Query with Copilot", "Explain the following JPQL query with a clear summary first, followed by a detailed explanation. If the query contains any SpEL expressions, explain those parts as well: \n\n"),
HQL("Explain Query with Copilot", "Explain the following HQL query with a clear summary first, followed by a detailed explanation. If the query contains any SpEL expressions, explain those parts as well: \n\n"),
AOP("Explain AOP annotation with Copilot", "Explain the following AOP annotation with a clear summary first, followed by a detailed contextual explanation of annotation and its purpose: \n\n"),
DEFAULT("Explain Query with Copilot", "Explain the following query with a clear summary first, followed by a detailed explanation: \n\n");

private final String title;
private final String prompt;
Expand Down
Loading