Skip to content

Commit 64e7084

Browse files
vudayanimartinlippert
authored andcommitted
GH-1330: Explain AOP annotations with copilot
1 parent fb072bf commit 64e7084

File tree

15 files changed

+387
-62
lines changed

15 files changed

+387
-62
lines changed

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java

+12
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
*******************************************************************************/
1111
package org.springframework.ide.vscode.boot.java;
1212

13+
import java.util.Map;
14+
1315
/**
1416
* Constants containing various fully-qualified annotation names.
1517
*
@@ -85,6 +87,16 @@ public class Annotations {
8587

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

90+
public static final Map<String, String> AOP_ANNOTATIONS = Map.of(
91+
"org.aspectj.lang.annotation.Pointcut", "Pointcut",
92+
"org.aspectj.lang.annotation.Before", "Before",
93+
"org.aspectj.lang.annotation.Around", "Around",
94+
"org.aspectj.lang.annotation.After", "After",
95+
"org.aspectj.lang.annotation.AfterReturning", "AfterReturning",
96+
"org.aspectj.lang.annotation.AfterThrowing", "AfterThrowing",
97+
"org.aspectj.lang.annotation.DeclareParents", "DeclareParents"
98+
);
99+
88100

89101

90102
}

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
import org.springframework.ide.vscode.boot.java.handlers.CodeLensProvider;
4141
import org.springframework.ide.vscode.boot.java.handlers.HighlightProvider;
4242
import org.springframework.ide.vscode.boot.java.handlers.HoverProvider;
43-
import org.springframework.ide.vscode.boot.java.handlers.QueryCodeLensProvider;
43+
import org.springframework.ide.vscode.boot.java.handlers.CopilotCodeLensProvider;
4444
import org.springframework.ide.vscode.boot.java.handlers.ReferenceProvider;
4545
import org.springframework.ide.vscode.boot.java.links.SourceLinks;
4646
import org.springframework.ide.vscode.boot.java.livehover.ActiveProfilesProvider;
@@ -319,7 +319,7 @@ protected ReferencesHandler createReferenceHandler(SimpleLanguageServer server,
319319
protected BootJavaCodeLensEngine createCodeLensEngine(SpringSymbolIndex index, JavaProjectFinder projectFinder, SimpleLanguageServer server, SpelSemanticTokens spelSemanticTokens) {
320320
Collection<CodeLensProvider> codeLensProvider = new ArrayList<>();
321321
codeLensProvider.add(new WebfluxHandlerCodeLensProvider(index));
322-
codeLensProvider.add(new QueryCodeLensProvider(projectFinder, server, spelSemanticTokens));
322+
codeLensProvider.add(new CopilotCodeLensProvider(projectFinder, server, spelSemanticTokens));
323323

324324
return new BootJavaCodeLensEngine(this, codeLensProvider);
325325
}
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
import java.util.ArrayList;
1414
import java.util.Arrays;
1515
import java.util.Collections;
16+
import java.util.HashMap;
1617
import java.util.List;
18+
import java.util.Map;
1719
import java.util.Optional;
1820
import java.util.Set;
1921
import java.util.concurrent.CompletableFuture;
@@ -25,13 +27,17 @@
2527
import org.eclipse.jdt.core.dom.Expression;
2628
import org.eclipse.jdt.core.dom.MemberValuePair;
2729
import org.eclipse.jdt.core.dom.MethodDeclaration;
30+
import org.eclipse.jdt.core.dom.MethodInvocation;
2831
import org.eclipse.jdt.core.dom.NormalAnnotation;
32+
import org.eclipse.jdt.core.dom.SimpleName;
2933
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
34+
import org.eclipse.jdt.core.dom.StringLiteral;
3035
import org.eclipse.lsp4j.CodeLens;
3136
import org.eclipse.lsp4j.Command;
3237
import org.eclipse.lsp4j.jsonrpc.CancelChecker;
3338
import org.slf4j.Logger;
3439
import org.slf4j.LoggerFactory;
40+
import org.springframework.ide.vscode.boot.java.Annotations;
3541
import org.springframework.ide.vscode.boot.java.spel.AnnotationParamSpelExtractor;
3642
import org.springframework.ide.vscode.boot.java.spel.AnnotationParamSpelExtractor.Snippet;
3743
import org.springframework.ide.vscode.boot.java.spel.SpelSemanticTokens;
@@ -49,9 +55,9 @@
4955
/**
5056
* @author Udayani V
5157
*/
52-
public class QueryCodeLensProvider implements CodeLensProvider {
58+
public class CopilotCodeLensProvider implements CodeLensProvider {
5359

54-
protected static Logger logger = LoggerFactory.getLogger(QueryCodeLensProvider.class);
60+
protected static Logger logger = LoggerFactory.getLogger(CopilotCodeLensProvider.class);
5561

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

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

6874
private static boolean showCodeLenses;
69-
70-
public QueryCodeLensProvider(JavaProjectFinder projectFinder, SimpleLanguageServer server, SpelSemanticTokens spelSemanticTokens) {
75+
76+
public CopilotCodeLensProvider(JavaProjectFinder projectFinder, SimpleLanguageServer server, SpelSemanticTokens spelSemanticTokens) {
7177
this.projectFinder = projectFinder;
7278
this.spelSemanticTokens = spelSemanticTokens;
7379
server.onCommand(CMD_ENABLE_COPILOT_FEATURES, params -> {
7480
if (params.getArguments().get(0) instanceof JsonPrimitive) {
75-
QueryCodeLensProvider.showCodeLenses = ((JsonPrimitive) params.getArguments().get(0)).getAsBoolean();
81+
CopilotCodeLensProvider.showCodeLenses = ((JsonPrimitive) params.getArguments().get(0)).getAsBoolean();
7682
}
7783
return CompletableFuture.completedFuture(showCodeLenses);
7884
});
@@ -84,53 +90,66 @@ public void provideCodeLenses(CancelChecker cancelToken, TextDocument document,
8490
if (!showCodeLenses) {
8591
return;
8692
}
93+
94+
Map<String, String> pointcutMap = findPointcuts(cu);
95+
8796
cu.accept(new ASTVisitor() {
8897

8998
@Override
9099
public boolean visit(SingleMemberAnnotation node) {
91100
Arrays.stream(spelExtractors).map(e -> e.getSpelRegion(node)).filter(o -> o.isPresent())
92101
.map(o -> o.get()).forEach(snippet -> {
93102
String additionalContext = parseSpelAndFetchContext(cu, snippet.text());
94-
provideCodeLensForSpelExpression(cancelToken, node, document, snippet,
95-
additionalContext, resultAccumulator);
103+
provideCodeLensForSpelExpression(cancelToken, node, document, snippet, additionalContext, resultAccumulator);
96104
});
97105

98106
if (isQueryAnnotation(node)) {
99-
String queryPrompt = determineQueryPrompt(document);
100-
provideCodeLensForQuery(cancelToken, node, document, node.getValue(), queryPrompt,
101-
resultAccumulator);
107+
QueryType queryType = determineQueryType(document);
108+
provideCodeLensForExpression(cancelToken, node, document, queryType, "", resultAccumulator);
109+
} else if (isAopAnnotation(node)) {
110+
String additionalPointcutContext = extractPointcutReference(node.getValue(), pointcutMap);
111+
provideCodeLensForExpression(cancelToken, node, document, QueryType.AOP, additionalPointcutContext, resultAccumulator);
102112
}
103113

104114
return super.visit(node);
105115
}
106116

107117
@Override
108118
public boolean visit(NormalAnnotation node) {
109-
110119

111120
Arrays.stream(spelExtractors).map(e -> e.getSpelRegion(node)).filter(o -> o.isPresent())
112121
.map(o -> o.get()).forEach(snippet -> {
113122
String additionalContext = parseSpelAndFetchContext(cu, snippet.text());
114-
provideCodeLensForSpelExpression(cancelToken, node, document, snippet, additionalContext,
115-
resultAccumulator);
123+
provideCodeLensForSpelExpression(cancelToken, node, document, snippet, additionalContext, resultAccumulator);
116124
});
117125

118126
if (isQueryAnnotation(node)) {
119-
String queryPrompt = determineQueryPrompt(document);
120-
for (Object value : node.values()) {
121-
if (value instanceof MemberValuePair) {
122-
MemberValuePair pair = (MemberValuePair) value;
123-
if ("value".equals(pair.getName().getIdentifier())) {
124-
provideCodeLensForQuery(cancelToken, node, document, pair.getValue(), queryPrompt,
125-
resultAccumulator);
126-
break;
127-
}
128-
}
127+
QueryType queryType = determineQueryType(document);
128+
provideCodeLensForExpression(cancelToken, node, document, queryType, "", resultAccumulator);
129+
} else if (isAopAnnotation(node)) {
130+
Expression value = getMemberValue(node);
131+
String additionalPointcutContext = null;
132+
if (value != null) {
133+
additionalPointcutContext = extractPointcutReference(value, pointcutMap);
129134
}
135+
provideCodeLensForExpression(cancelToken, node, document, QueryType.AOP, additionalPointcutContext, resultAccumulator);
130136
}
131137

132138
return super.visit(node);
133139
}
140+
141+
private Expression getMemberValue(NormalAnnotation node) {
142+
for (Object value : node.values()) {
143+
if (value instanceof MemberValuePair) {
144+
MemberValuePair pair = (MemberValuePair) value;
145+
if ("pointcut".equals(pair.getName().getIdentifier())) {
146+
return pair.getValue();
147+
}
148+
}
149+
}
150+
return null;
151+
}
152+
134153
});
135154
}
136155

@@ -163,20 +182,26 @@ protected void provideCodeLensForSpelExpression(CancelChecker cancelToken, Annot
163182
}
164183
}
165184

166-
protected void provideCodeLensForQuery(CancelChecker cancelToken, Annotation node, TextDocument document,
167-
Expression valueExp, String query, List<CodeLens> resultAccumulator) {
185+
protected void provideCodeLensForExpression(CancelChecker cancelToken, Annotation node, TextDocument document,
186+
QueryType queryType, String additionalContext, List<CodeLens> resultAccumulator) {
168187
cancelToken.checkCanceled();
169188

170-
if (valueExp != null) {
189+
if (node != null) {
171190
try {
172-
191+
192+
String context = additionalContext != null && !additionalContext.isEmpty() ? String.format(
193+
"""
194+
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.
195+
Avoid detailed implementation steps and avoid repeating information covered earlier.
196+
""",additionalContext) : "";
197+
173198
CodeLens codeLens = new CodeLens();
174-
codeLens.setRange(document.toRange(valueExp.getStartPosition(), valueExp.getLength()));
199+
codeLens.setRange(document.toRange(node.getStartPosition(), node.getLength()));
175200

176201
Command cmd = new Command();
177-
cmd.setTitle(QueryType.DEFAULT.getTitle());
202+
cmd.setTitle(queryType.getTitle());
178203
cmd.setCommand(CMD);
179-
cmd.setArguments(ImmutableList.of(query + valueExp.toString()));
204+
cmd.setArguments(ImmutableList.of(queryType.getPrompt() + node.toString() + "\n\n" +context));
180205
codeLens.setCommand(cmd);
181206

182207
resultAccumulator.add(codeLens);
@@ -190,15 +215,15 @@ private static boolean isQueryAnnotation(Annotation a) {
190215
return FQN_QUERY.equals(a.getTypeName().getFullyQualifiedName())
191216
|| QUERY.equals(a.getTypeName().getFullyQualifiedName());
192217
}
193-
194-
private String determineQueryPrompt(TextDocument document) {
218+
219+
private QueryType determineQueryType(TextDocument document) {
195220
Optional<IJavaProject> optProject = projectFinder.find(document.getId());
196221
if (optProject.isPresent()) {
197222
IJavaProject jp = optProject.get();
198-
return SpringProjectUtil.hasDependencyStartingWith(jp, "hibernate-core", null) ? QueryType.HQL.getPrompt()
199-
: QueryType.JPQL.getPrompt();
223+
return SpringProjectUtil.hasDependencyStartingWith(jp, "hibernate-core", null) ? QueryType.HQL
224+
: QueryType.JPQL;
200225
}
201-
return QueryType.DEFAULT.getPrompt();
226+
return QueryType.DEFAULT;
202227
}
203228

204229
private String parseSpelAndFetchContext(CompilationUnit cu, String spelExpression) {
@@ -238,4 +263,49 @@ public boolean visit(MethodDeclaration node) {
238263
return methodContext;
239264
}
240265

266+
private boolean isAopAnnotation(Annotation a) {
267+
String annotationFQN = a.getTypeName().getFullyQualifiedName();
268+
return Annotations.AOP_ANNOTATIONS.containsKey(annotationFQN)
269+
|| Annotations.AOP_ANNOTATIONS.containsValue(annotationFQN);
270+
}
271+
272+
private Map<String, String> findPointcuts(CompilationUnit cu) {
273+
Map<String, String> pointcutMap = new HashMap<>();
274+
cu.accept(new ASTVisitor() {
275+
@Override
276+
public boolean visit(MethodDeclaration node) {
277+
for (Object modifierObj : node.modifiers()) {
278+
if (modifierObj instanceof Annotation) {
279+
Annotation annotation = (Annotation) modifierObj;
280+
if ("Pointcut".equals(annotation.getTypeName().getFullyQualifiedName())) {
281+
String methodName = node.getName().getIdentifier();
282+
pointcutMap.put(methodName, node.toString());
283+
}
284+
}
285+
}
286+
return super.visit(node);
287+
}
288+
});
289+
return pointcutMap;
290+
291+
}
292+
293+
private String extractPointcutReference(org.eclipse.jdt.core.dom.Expression expression, Map<String, String> pointcutMap) {
294+
if (expression instanceof MethodInvocation) {
295+
return ((MethodInvocation) expression).getName().getIdentifier();
296+
} else if (expression instanceof SimpleName) {
297+
return ((SimpleName) expression).getIdentifier();
298+
} else if (expression instanceof StringLiteral) {
299+
String literalValue = ((StringLiteral) expression).getLiteralValue();
300+
StringBuilder pointcuts = new StringBuilder();
301+
for (Map.Entry<String, String> entry : pointcutMap.entrySet()) {
302+
if (literalValue.contains(entry.getKey())) {
303+
pointcuts.append(entry.getValue());
304+
}
305+
}
306+
return pointcuts.toString();
307+
}
308+
return null;
309+
}
310+
241311
}

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/QueryType.java

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
package org.springframework.ide.vscode.boot.java.handlers;
22

33
public enum QueryType {
4-
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"),
5-
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"),
6-
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"),
7-
DEFAULT("Explain Query using Copilot", "Explain the following query with a clear summary first, followed by a detailed explanation: \n\n");
4+
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"),
5+
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"),
6+
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"),
7+
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"),
8+
DEFAULT("Explain Query with Copilot", "Explain the following query with a clear summary first, followed by a detailed explanation: \n\n");
89

910
private final String title;
1011
private final String prompt;

0 commit comments

Comments
 (0)