Skip to content

Commit 7198389

Browse files
committed
GH-1707: added codd lens to ask AI assistant to convert route definitions to builder pattern
1 parent 449f038 commit 7198389

File tree

9 files changed

+429
-18
lines changed

9 files changed

+429
-18
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import org.springframework.ide.vscode.boot.java.handlers.CodeLensProvider;
4747
import org.springframework.ide.vscode.boot.java.handlers.CopilotCodeLensProvider;
4848
import org.springframework.ide.vscode.boot.java.handlers.HighlightProvider;
49+
import org.springframework.ide.vscode.boot.java.handlers.RouterFunctionCodeLensProvider;
4950
import org.springframework.ide.vscode.boot.java.handlers.HoverProvider;
5051
import org.springframework.ide.vscode.boot.java.handlers.ReferenceProvider;
5152
import org.springframework.ide.vscode.boot.java.links.SourceLinks;
@@ -325,6 +326,7 @@ protected BootJavaCodeLensEngine createCodeLensEngine(SpringMetamodelIndex sprin
325326
Collection<CodeLensProvider> codeLensProvider = new ArrayList<>();
326327
codeLensProvider.add(new WebfluxHandlerCodeLensProvider(springIndex));
327328
codeLensProvider.add(new CopilotCodeLensProvider(projectFinder, server, spelSemanticTokens));
329+
codeLensProvider.add(new RouterFunctionCodeLensProvider());
328330
codeLensProvider.add(new DataRepositoryAotMetadataCodeLensProvider(projectFinder, repositoryAotMetadataService, refactorings, config));
329331
codeLensProvider.add(new WebConfigCodeLensProvider(projectFinder, springIndex, config));
330332

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

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,23 +61,23 @@ public class CopilotCodeLensProvider implements CodeLensProvider {
6161

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

64+
public static final String CMD_SEND_TO_AI_ASSISTANT = "vscode-spring-boot.query.explain";
6465
public static final String CMD_ENABLE_COPILOT_FEATURES = "sts/enable/copilot/features";
6566

6667
private static final String QUERY = "Query";
6768
private static final String FQN_QUERY = "org.springframework.data.jpa.repository." + QUERY;
68-
private static final String CMD = "vscode-spring-boot.query.explain";
69+
6970

7071
private final AnnotationParamSpelExtractor[] spelExtractors = AnnotationParamSpelExtractor.SPEL_EXTRACTORS;
71-
7272
private final JavaProjectFinder projectFinder;
73-
74-
private SpelSemanticTokens spelSemanticTokens;
73+
private final SpelSemanticTokens spelSemanticTokens;
7574

7675
private static boolean showCodeLenses;
7776

7877
public CopilotCodeLensProvider(JavaProjectFinder projectFinder, SimpleLanguageServer server, SpelSemanticTokens spelSemanticTokens) {
7978
this.projectFinder = projectFinder;
8079
this.spelSemanticTokens = spelSemanticTokens;
80+
8181
server.onCommand(CMD_ENABLE_COPILOT_FEATURES, params -> {
8282
if (params.getArguments().get(0) instanceof JsonPrimitive) {
8383
CopilotCodeLensProvider.showCodeLenses = ((JsonPrimitive) params.getArguments().get(0)).getAsBoolean();
@@ -174,7 +174,7 @@ protected void provideCodeLensForSpelExpression(CancelChecker cancelToken, Annot
174174

175175
Command cmd = new Command();
176176
cmd.setTitle(QueryType.SPEL.getTitle());
177-
cmd.setCommand(CMD);
177+
cmd.setCommand(CMD_SEND_TO_AI_ASSISTANT);
178178
cmd.setArguments(ImmutableList.of(QueryType.SPEL.getPrompt() + snippet.getText() + "\n\n" + context));
179179
codeLens.setCommand(cmd);
180180

@@ -203,7 +203,7 @@ protected void provideCodeLensForExpression(CancelChecker cancelToken, Annotatio
203203

204204
Command cmd = new Command();
205205
cmd.setTitle(queryType.getTitle());
206-
cmd.setCommand(CMD);
206+
cmd.setCommand(CMD_SEND_TO_AI_ASSISTANT);
207207
cmd.setArguments(ImmutableList.of(queryType.getPrompt() + node.toString() + "\n\n" +context));
208208
codeLens.setCommand(cmd);
209209

@@ -321,5 +321,15 @@ private String extractPointcutReference(org.eclipse.jdt.core.dom.Expression expr
321321
}
322322
return null;
323323
}
324+
325+
/**
326+
* Returns whether copilot code lenses should be shown.
327+
* This is used by other code lens providers that depend on the same configuration.
328+
*
329+
* @return true if code lenses should be shown, false otherwise
330+
*/
331+
public static boolean isShowCodeLenses() {
332+
return showCodeLenses;
333+
}
324334

325335
}

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,17 @@ public enum QueryType {
66
HQL("Explain Query with AI", "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"),
77
MONGODB("Explain Query with AI", "Explain the following MongoDB 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"),
88
AOP("Explain AOP annotation with AI", "Explain the following AOP annotation with a clear summary first, followed by a detailed contextual explanation of annotation and its purpose: \n\n"),
9-
DEFAULT("Explain Query with AI", "Explain the following query with a clear summary first, followed by a detailed explanation: \n\n");
9+
DEFAULT("Explain Query with AI", "Explain the following query with a clear summary first, followed by a detailed explanation: \n\n"),
10+
11+
ROUTER_CONVERSION("Convert to Router Builder Pattern with AI",
12+
"""
13+
Convert the Spring WebFlux/WebMVC functional router method $method_name$ at line $line_no$ from using static imports (RouterFunctions.route(), RequestPredicates.GET(), etc.)
14+
to the modern builder pattern (RouterFunctions.route().GET().POST().build()).
15+
16+
Provide the complete refactored method with the same functionality.
17+
""")
18+
19+
;
1020

1121
private final String title;
1222
private final String prompt;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Broadcom, Inc.
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v1.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-v10.html
7+
*
8+
* Contributors:
9+
* Broadcom, Inc. - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.ide.vscode.boot.java.handlers;
12+
13+
import java.util.List;
14+
import java.util.concurrent.atomic.AtomicBoolean;
15+
16+
import org.eclipse.jdt.core.dom.ASTVisitor;
17+
import org.eclipse.jdt.core.dom.Block;
18+
import org.eclipse.jdt.core.dom.CompilationUnit;
19+
import org.eclipse.jdt.core.dom.IMethodBinding;
20+
import org.eclipse.jdt.core.dom.MethodDeclaration;
21+
import org.eclipse.jdt.core.dom.MethodInvocation;
22+
import org.eclipse.jdt.core.dom.SimpleName;
23+
import org.eclipse.lsp4j.CodeLens;
24+
import org.eclipse.lsp4j.Command;
25+
import org.eclipse.lsp4j.jsonrpc.CancelChecker;
26+
import org.slf4j.Logger;
27+
import org.slf4j.LoggerFactory;
28+
import org.springframework.ide.vscode.boot.java.requestmapping.WebfluxUtils;
29+
import org.springframework.ide.vscode.commons.util.BadLocationException;
30+
import org.springframework.ide.vscode.commons.util.text.TextDocument;
31+
32+
import com.google.common.collect.ImmutableList;
33+
34+
/**
35+
* Code lens provider for functional router methods that use static imports.
36+
* Provides AI-assisted conversion to the modern builder pattern.
37+
*
38+
* Reuses the command and configuration from {@link CopilotCodeLensProvider}.
39+
*
40+
* @author Martin Lippert
41+
*/
42+
public class RouterFunctionCodeLensProvider implements CodeLensProvider {
43+
44+
protected static Logger logger = LoggerFactory.getLogger(RouterFunctionCodeLensProvider.class);
45+
46+
@Override
47+
public void provideCodeLenses(CancelChecker cancelToken, TextDocument document, CompilationUnit cu, List<CodeLens> resultAccumulator) {
48+
if (!CopilotCodeLensProvider.isShowCodeLenses()) {
49+
return;
50+
}
51+
52+
cu.accept(new ASTVisitor() {
53+
@Override
54+
public boolean visit(MethodDeclaration node) {
55+
cancelToken.checkCanceled();
56+
57+
// Check if this is a functional web router bean (WebFlux or WebMVC)
58+
if (WebfluxUtils.isFunctionalWebRouterBean(node)) {
59+
// Check if the method uses static imports (old style) instead of builder pattern
60+
if (usesStaticImports(node)) {
61+
provideCodeLensForRouterMethod(cancelToken, node, document, resultAccumulator);
62+
}
63+
}
64+
65+
return super.visit(node);
66+
}
67+
});
68+
}
69+
70+
/**
71+
* Checks if the router method uses static imports (RouterFunctions.route(), RequestPredicates.GET(), etc.)
72+
* instead of the builder pattern (RouterFunctions.route().GET().build()).
73+
*
74+
* The detection looks for:
75+
* - RouterFunctions.route() with 2 parameters (predicate, handler) - old style
76+
* - RouterFunction.andRoute() method calls - old style
77+
*
78+
* If we find RouterFunctions.route() with no parameters, that's the builder pattern, so we return false.
79+
*/
80+
private boolean usesStaticImports(MethodDeclaration method) {
81+
Block methodBody = method.getBody();
82+
if (methodBody == null) {
83+
return false;
84+
}
85+
86+
AtomicBoolean foundStaticImportStyle = new AtomicBoolean(false);
87+
AtomicBoolean foundBuilderStyle = new AtomicBoolean(false);
88+
89+
methodBody.accept(new ASTVisitor() {
90+
@Override
91+
public boolean visit(MethodInvocation node) {
92+
IMethodBinding methodBinding = node.resolveMethodBinding();
93+
94+
if (methodBinding != null) {
95+
String declaringClassName = methodBinding.getDeclaringClass().getBinaryName();
96+
String methodName = methodBinding.getName();
97+
98+
// Check for static import style
99+
if (isStaticImportStyle(declaringClassName, methodName, node)) {
100+
foundStaticImportStyle.set(true);
101+
}
102+
103+
// Check for builder style
104+
if (isBuilderStyle(declaringClassName, methodName, node)) {
105+
foundBuilderStyle.set(true);
106+
}
107+
}
108+
109+
return super.visit(node);
110+
}
111+
});
112+
113+
// Only show code lens if using static import style and NOT using builder style
114+
return foundStaticImportStyle.get() && !foundBuilderStyle.get();
115+
}
116+
117+
/**
118+
* Checks if a method invocation is using the static import style.
119+
* This includes:
120+
* - RouterFunctions.route() with 2+ parameters (predicate, handler)
121+
* - RouterFunction.andRoute() with parameters
122+
*/
123+
private boolean isStaticImportStyle(String declaringClassName, String methodName, MethodInvocation node) {
124+
if ((WebfluxUtils.ROUTER_FUNCTIONS_TYPE.equals(declaringClassName) ||
125+
WebfluxUtils.MVC_ROUTER_FUNCTIONS_TYPE.equals(declaringClassName))) {
126+
if ("route".equals(methodName)) {
127+
// route() with parameters = static style, route() with no parameters = builder style
128+
List<?> arguments = node.arguments();
129+
return arguments != null && arguments.size() >= 2;
130+
}
131+
}
132+
133+
if ((WebfluxUtils.ROUTER_FUNCTION_TYPE.equals(declaringClassName) ||
134+
WebfluxUtils.MVC_ROUTER_FUNCTION_TYPE.equals(declaringClassName))) {
135+
if ("andRoute".equals(methodName)) {
136+
// andRoute() is only available in static import style
137+
return true;
138+
}
139+
}
140+
141+
return false;
142+
}
143+
144+
/**
145+
* Checks if a method invocation is using the builder pattern.
146+
* This includes:
147+
* - RouterFunctions.route() with no parameters
148+
* - HTTP method calls like GET(), POST(), etc. on RouterFunction.Builder
149+
* - build() call at the end
150+
*/
151+
private boolean isBuilderStyle(String declaringClassName, String methodName, MethodInvocation node) {
152+
// Check for RouterFunctions.route() with no parameters
153+
if ((WebfluxUtils.ROUTER_FUNCTIONS_TYPE.equals(declaringClassName) ||
154+
WebfluxUtils.MVC_ROUTER_FUNCTIONS_TYPE.equals(declaringClassName))) {
155+
if ("route".equals(methodName)) {
156+
List<?> arguments = node.arguments();
157+
return arguments == null || arguments.isEmpty();
158+
}
159+
}
160+
161+
// Check for builder-specific methods
162+
String builderType = WebfluxUtils.ROUTER_FUNCTIONS_TYPE + "$Builder";
163+
String mvcBuilderType = WebfluxUtils.MVC_ROUTER_FUNCTIONS_TYPE + "$Builder";
164+
165+
if (builderType.equals(declaringClassName) || mvcBuilderType.equals(declaringClassName)) {
166+
// Any method on the builder indicates builder pattern
167+
return true;
168+
}
169+
170+
return false;
171+
}
172+
173+
private void provideCodeLensForRouterMethod(CancelChecker cancelToken, MethodDeclaration node,
174+
TextDocument document, List<CodeLens> resultAccumulator) {
175+
cancelToken.checkCanceled();
176+
177+
if (node != null) {
178+
try {
179+
CodeLens codeLens = new CodeLens();
180+
codeLens.setRange(document.toRange(node.getStartPosition(), node.getLength()));
181+
182+
// Get method name and line number
183+
SimpleName nameNode = node.getName();
184+
185+
String methodName = nameNode.getIdentifier();
186+
int lineNumber = document.toPosition(nameNode.getStartPosition()).getLine() + 1; // Convert to 1-based line number
187+
188+
// Replace placeholders in the prompt
189+
String prompt = QueryType.ROUTER_CONVERSION.getPrompt()
190+
.replace("$method_name$", methodName)
191+
.replace("$line_no$", String.valueOf(lineNumber));
192+
193+
Command cmd = new Command();
194+
cmd.setTitle(QueryType.ROUTER_CONVERSION.getTitle());
195+
cmd.setCommand(CopilotCodeLensProvider.CMD_SEND_TO_AI_ASSISTANT);
196+
cmd.setArguments(ImmutableList.of(prompt));
197+
codeLens.setCommand(cmd);
198+
199+
resultAccumulator.add(codeLens);
200+
} catch (BadLocationException e) {
201+
logger.error("Error providing code lens for router function: " + e.getMessage());
202+
}
203+
}
204+
}
205+
}
206+

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ public class WebfluxUtils {
3535
public static final String REQUEST_PREDICATES_TYPE = "org.springframework.web.reactive.function.server.RequestPredicates";
3636

3737
public static final String MVC_ROUTER_FUNCTION_TYPE = "org.springframework.web.servlet.function.RouterFunction";
38+
public static final String MVC_ROUTER_FUNCTIONS_TYPE = "org.springframework.web.servlet.function.RouterFunctions";
39+
3840
public static final String MVC_REQUEST_PREDICATES_TYPE = "org.springframework.web.servlet.function.RequestPredicates";
3941

4042
public static final String REQUEST_PREDICATE_PATH_METHOD = "path";

0 commit comments

Comments
 (0)