Skip to content

Commit 5b9e200

Browse files
committed
GH-1330: Explain AOP annotations with copilot
1 parent fb072bf commit 5b9e200

File tree

14 files changed

+372
-47
lines changed

14 files changed

+372
-47
lines changed

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

Lines changed: 12 additions & 0 deletions
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/handlers/QueryCodeLensProvider.java

Lines changed: 101 additions & 31 deletions
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;
@@ -66,7 +72,7 @@ public class QueryCodeLensProvider implements CodeLensProvider {
6672
private SpelSemanticTokens spelSemanticTokens;
6773

6874
private static boolean showCodeLenses;
69-
75+
7076
public QueryCodeLensProvider(JavaProjectFinder projectFinder, SimpleLanguageServer server, SpelSemanticTokens spelSemanticTokens) {
7177
this.projectFinder = projectFinder;
7278
this.spelSemanticTokens = spelSemanticTokens;
@@ -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 what it does, focusing on its 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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ public enum QueryType {
44
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"),
55
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"),
66
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+
AOP("Explain AOP using Copilot", "Explain the following AOP annotation with a clear summary first, followed by a detailed contextual explanation of its usage and any parameters it includes: \n\n"),
78
DEFAULT("Explain Query using Copilot", "Explain the following query with a clear summary first, followed by a detailed explanation: \n\n");
89

910
private final String title;

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

Lines changed: 97 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public class QueryCodeLensProviderTest {
7979
@BeforeEach
8080
public void setup() throws Exception {
8181
harness.intialize(null);
82-
directory = new File(ProjectsHarness.class.getResource("/test-projects/test-spel-query-codelense/").toURI());
82+
directory = new File(ProjectsHarness.class.getResource("/test-projects/test-spel-query-aop-codelenses/").toURI());
8383
String projectDir = directory.toURI().toString();
8484
server = mock(SimpleLanguageServer.class);
8585
commandHandlerCaptor = ArgumentCaptor.forClass(ExecuteCommandHandler.class);
@@ -107,9 +107,9 @@ public void testShowCodeLensesTrueForQuery() throws Exception {
107107

108108
assertEquals(3, codeLenses.size());
109109

110-
assertTrue(containsCodeLens(codeLenses.get(0), QueryType.DEFAULT.getTitle(), 9, 8, 9, 108));
111-
assertTrue(containsCodeLens(codeLenses.get(1), QueryType.DEFAULT.getTitle(), 13, 8, 13, 39));
112-
assertTrue(containsCodeLens(codeLenses.get(2), QueryType.DEFAULT.getTitle(), 17, 14, 17, 92));
110+
assertTrue(containsCodeLens(codeLenses.get(0), QueryType.DEFAULT.getTitle(), 9, 1, 9, 109));
111+
assertTrue(containsCodeLens(codeLenses.get(1), QueryType.DEFAULT.getTitle(), 13, 1, 13, 40));
112+
assertTrue(containsCodeLens(codeLenses.get(2), QueryType.DEFAULT.getTitle(), 17, 1, 17, 93));
113113
}
114114

115115
@Test
@@ -194,6 +194,85 @@ public static String concat(String str1,String str2){
194194
assertEquals(expectedPrompt, actualPrompt);
195195
}
196196

197+
@Test
198+
public void testShowCodeLensesTrueForAOP() throws Exception {
199+
200+
setCommandParamsHandler(true);
201+
202+
String docUri = directory.toPath().resolve("src/main/java/org/test/MyAspect.java").toUri().toString();
203+
TextDocumentInfo doc = harness.getOrReadFile(new File(new URI(docUri)), LanguageId.JAVA.getId());
204+
TextDocumentInfo openedDoc = harness.openDocument(doc);
205+
206+
List<? extends CodeLens> codeLenses = harness.getCodeLenses(openedDoc);
207+
208+
assertEquals(7, codeLenses.size());
209+
210+
assertTrue(containsCodeLens(codeLenses.get(0), QueryType.AOP.getTitle(), 9, 1, 9, 53));
211+
assertTrue(containsCodeLens(codeLenses.get(1), QueryType.AOP.getTitle(), 14, 1, 14, 24));
212+
assertTrue(containsCodeLens(codeLenses.get(2), QueryType.AOP.getTitle(), 19, 1, 19, 51));
213+
assertTrue(containsCodeLens(codeLenses.get(3), QueryType.AOP.getTitle(), 27, 1, 27, 50));
214+
assertTrue(containsCodeLens(codeLenses.get(4), QueryType.AOP.getTitle(), 32, 1, 32, 92));
215+
assertTrue(containsCodeLens(codeLenses.get(5), QueryType.AOP.getTitle(), 37, 1, 37, 86));
216+
assertTrue(containsCodeLens(codeLenses.get(6), QueryType.AOP.getTitle(), 42, 1, 42, 65));
217+
}
218+
219+
@Test
220+
public void testShowCodeLensesTrueForAopPointcutExamples() throws Exception {
221+
222+
setCommandParamsHandler(true);
223+
224+
String docUri = directory.toPath().resolve("src/main/java/org/test/PointcutExamples.java").toUri().toString();
225+
TextDocumentInfo doc = harness.getOrReadFile(new File(new URI(docUri)), LanguageId.JAVA.getId());
226+
TextDocumentInfo openedDoc = harness.openDocument(doc);
227+
228+
String expectedPrompt = """
229+
Explain the following AOP annotation with a clear summary first, followed by a detailed contextual explanation of its usage and any parameters it includes: \n
230+
@Pointcut("cflow(execution(* com.example..*.*(..)))")
231+
232+
""";
233+
234+
String expectedPromptWithContext = """
235+
Explain the following AOP annotation with a clear summary first, followed by a detailed contextual explanation of its usage and any parameters it includes: \n
236+
@AfterReturning(pointcut="targetService()",returning="result")
237+
238+
This is the pointcut definition referenced in the above annotation. \n
239+
@Pointcut("target(com.example.service.MyService)") public void targetService(){
240+
}
241+
\n
242+
Provide a brief summary of what it does, focusing on its role within the annotation.
243+
Avoid detailed implementation steps and avoid repeating information covered earlier.
244+
""";
245+
String expectedPromptWithMultiPointcutRef = """
246+
Explain the following AOP annotation with a clear summary first, followed by a detailed contextual explanation of its usage and any parameters it includes: \n
247+
@Pointcut("serviceLayer() || repositoryLayer()")
248+
249+
This is the pointcut definition referenced in the above annotation. \n
250+
@Pointcut("within(com.example.repository..*)") public void repositoryLayer(){
251+
}
252+
@Pointcut("execution(* com.example.service.*.*(..))") public void serviceLayer(){
253+
}
254+
\n
255+
Provide a brief summary of what it does, focusing on its role within the annotation.
256+
Avoid detailed implementation steps and avoid repeating information covered earlier.
257+
""";
258+
259+
List<? extends CodeLens> codeLenses = harness.getCodeLenses(openedDoc);
260+
261+
assertEquals(8, codeLenses.size());
262+
263+
assertTrue(containsCodeLens(codeLenses.get(0), QueryType.AOP.getTitle(), 4, 1, 4, 54));
264+
assertTrue(containsCodeLens(codeLenses.get(3), QueryType.AOP.getTitle(), 15, 1, 15, 64));
265+
266+
String actualPrompt = codeLenses.get(0).getCommand().getArguments().get(0).toString();
267+
String actualPromptWithContext = codeLenses.get(3).getCommand().getArguments().get(0).toString();
268+
String actualPromptWithMultiPointcutRef = codeLenses.get(7).getCommand().getArguments().get(0).toString();
269+
270+
assertEquals(expectedPrompt, actualPrompt);
271+
assertEquals(expectedPromptWithContext, actualPromptWithContext);
272+
assertEquals(expectedPromptWithMultiPointcutRef, actualPromptWithMultiPointcutRef);
273+
274+
}
275+
197276
@Test
198277
public void testShowCodeLensesFalseForQuery() throws Exception {
199278

@@ -222,6 +301,20 @@ public void testShowCodeLensesFalseForSpel() throws Exception {
222301
assertEquals(0, codeLenses.size());
223302
}
224303

304+
@Test
305+
public void testShowCodeLensesFalseForAOP() throws Exception {
306+
307+
setCommandParamsHandler(false);
308+
309+
String docUri = directory.toPath().resolve("src/main/java/org/test/MyAspect.java").toUri().toString();
310+
TextDocumentInfo doc = harness.getOrReadFile(new File(new URI(docUri)), LanguageId.JAVA.getId());
311+
TextDocumentInfo openedDoc = harness.openDocument(doc);
312+
313+
List<? extends CodeLens> codeLenses = harness.getCodeLenses(openedDoc);
314+
315+
assertEquals(0, codeLenses.size());
316+
}
317+
225318
private void setCommandParamsHandler(boolean value) throws InterruptedException, ExecutionException {
226319
ExecuteCommandHandler handler = commandHandlerCaptor.getValue();
227320
ExecuteCommandParams params = new ExecuteCommandParams();
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)