Skip to content

Commit 1d6ee6b

Browse files
committed
GH-1582: validation to detect potential wrong path definitions in rest controller annotation
1 parent bf1ce1b commit 1d6ee6b

File tree

7 files changed

+245
-1
lines changed

7 files changed

+245
-1
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import org.springframework.ide.vscode.boot.java.reconcilers.NoRepoAnnotationReconciler;
5050
import org.springframework.ide.vscode.boot.java.reconcilers.NoRequestMappingAnnotationReconciler;
5151
import org.springframework.ide.vscode.boot.java.reconcilers.NotRegisteredBeansReconciler;
52+
import org.springframework.ide.vscode.boot.java.reconcilers.PathInControllerAnnotationReconciler;
5253
import org.springframework.ide.vscode.boot.java.reconcilers.PreciseBeanTypeReconciler;
5354
import org.springframework.ide.vscode.boot.java.reconcilers.ServerHttpSecurityLambdaDslReconciler;
5455
import org.springframework.ide.vscode.boot.java.reconcilers.UnnecessarySpringExtensionReconciler;
@@ -95,6 +96,10 @@ public class JdtConfig {
9596
return new NoAutowiredOnConstructorReconciler(server.getQuickfixRegistry());
9697
}
9798

99+
@Bean PathInControllerAnnotationReconciler pathInControllerAnnotationReconciler() {
100+
return new PathInControllerAnnotationReconciler();
101+
}
102+
98103
@Bean WebSecurityConfigurerAdapterReconciler webSecurityConfigurerAdapterReconciler(SimpleLanguageServer server) {
99104
return new WebSecurityConfigurerAdapterReconciler(server.getQuickfixRegistry());
100105
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public class Annotations {
3030
public static final String IMPORT = "org.springframework.context.annotation.Import";
3131

3232
public static final String CONTROLLER = "org.springframework.stereotype.Controller";
33+
public static final String REST_CONTROLLER = "org.springframework.web.bind.annotation.RestController";
3334

3435
public static final String CONFIGURATION_PROPERTIES = "org.springframework.boot.context.properties.ConfigurationProperties";
3536

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
public enum Boot2JavaProblemType implements ProblemType {
2424

25+
PATH_IN_CONTROLLER_ANNOTATION(WARNING, "Controller annotation default attribute might contain a path", "Controller annotation default attribute might contain a path"),
2526
JAVA_AUTOWIRED_CONSTRUCTOR(WARNING, "Unnecessary `@Autowired` over the only constructor", "Unnecessary `@Autowired`", List.of(DiagnosticTag.Unnecessary)),
2627
JAVA_PUBLIC_BEAN_METHOD(HINT, "Public modifier on `@Bean` method. They no longer have to be public visibility to be usable by Spring.", "public `@Bean` method", List.of(DiagnosticTag.Unnecessary)),
2728
JAVA_TEST_SPRING_EXTENSION(WARNING, "`@SpringBootTest` and all test slice annotations already applies `@SpringExtension` as of Spring Boot 2.1.0.", "Unnecessary `@SpringExtension`", List.of(DiagnosticTag.Unnecessary)),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2023, 2025 VMware, 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+
* VMware, Inc. - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.ide.vscode.boot.java.reconcilers;
12+
13+
import static org.springframework.ide.vscode.commons.java.SpringProjectUtil.springBootVersionGreaterOrEqual;
14+
15+
import java.net.URI;
16+
import java.nio.file.Path;
17+
import java.nio.file.Paths;
18+
19+
import org.eclipse.jdt.core.dom.ASTVisitor;
20+
import org.eclipse.jdt.core.dom.Annotation;
21+
import org.eclipse.jdt.core.dom.CompilationUnit;
22+
import org.eclipse.jdt.core.dom.Expression;
23+
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
24+
import org.eclipse.jdt.core.dom.TypeDeclaration;
25+
import org.springframework.ide.vscode.boot.java.Annotations;
26+
import org.springframework.ide.vscode.boot.java.Boot2JavaProblemType;
27+
import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies;
28+
import org.springframework.ide.vscode.boot.java.utils.ASTUtils;
29+
import org.springframework.ide.vscode.commons.java.IClasspathUtil;
30+
import org.springframework.ide.vscode.commons.java.IJavaProject;
31+
import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType;
32+
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl;
33+
34+
public class PathInControllerAnnotationReconciler implements JdtAstReconciler {
35+
36+
private static final String PROBLEM_LABEL = "attribute refers to the component name, but looks like a path definition";
37+
38+
public PathInControllerAnnotationReconciler() {
39+
}
40+
41+
@Override
42+
public boolean isApplicable(IJavaProject project) {
43+
return springBootVersionGreaterOrEqual(2, 0, 0).test(project);
44+
}
45+
46+
@Override
47+
public ProblemType getProblemType() {
48+
return Boot2JavaProblemType.PATH_IN_CONTROLLER_ANNOTATION;
49+
}
50+
51+
@Override
52+
public ASTVisitor createVisitor(IJavaProject project, URI docUri, CompilationUnit cu, ReconcilingContext context) {
53+
AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(cu);
54+
55+
Path sourceFile = Paths.get(docUri);
56+
boolean insideOfSourceFolders = IClasspathUtil.getProjectJavaSourceFoldersWithoutTests(project.getClasspath())
57+
.anyMatch(f -> sourceFile.startsWith(f.toPath()));
58+
59+
return new ASTVisitor() {
60+
61+
@Override
62+
public boolean visit(TypeDeclaration typeDecl) {
63+
64+
if (!insideOfSourceFolders) {
65+
return super.visit(typeDecl);
66+
}
67+
68+
boolean isRestController = annotationHierarchies.isAnnotatedWith(typeDecl.resolveBinding(), Annotations.REST_CONTROLLER);
69+
if (!isRestController) {
70+
return super.visit(typeDecl);
71+
}
72+
73+
Annotation controllerAnnotation = ReconcileUtils.findAnnotation(annotationHierarchies, typeDecl, Annotations.REST_CONTROLLER, false);
74+
if (controllerAnnotation.isSingleMemberAnnotation()) {
75+
SingleMemberAnnotation sma = (SingleMemberAnnotation) controllerAnnotation;
76+
Expression value = sma.getValue();
77+
String stringValue = ASTUtils.getExpressionValueAsString(value, (t) -> {});
78+
79+
if (stringValue.contains("/")) {
80+
81+
ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), PROBLEM_LABEL,
82+
value.getStartPosition(), value.getLength());
83+
84+
context.getProblemCollector().accept(problem);
85+
}
86+
}
87+
88+
return super.visit(typeDecl);
89+
}
90+
91+
};
92+
}
93+
94+
}

headless-services/spring-boot-language-server/src/main/resources/problem-types.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,13 @@
8585
"label": "Implicit web annotations names",
8686
"description": "Web annotation names are unnecessary when it is the same as method parameter name",
8787
"defaultSeverity": "HINT"
88-
}
88+
},
89+
{
90+
"code": "WEB_ANNOTATION_NAMES",
91+
"label": "Controller annotation default attribute might contain a path",
92+
"description": "Controller annotation default attribute might contain a path - whereas the default attribute is the component name",
93+
"defaultSeverity": "WARNING"
94+
}
8995
]
9096
},
9197
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2023, 2025 VMware, 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+
* VMware, Inc. - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.ide.vscode.boot.java.reconcilers.test;
12+
13+
import static org.junit.jupiter.api.Assertions.assertEquals;
14+
15+
import java.util.List;
16+
17+
import org.junit.jupiter.api.AfterEach;
18+
import org.junit.jupiter.api.BeforeEach;
19+
import org.junit.jupiter.api.Test;
20+
import org.springframework.ide.vscode.boot.java.Boot2JavaProblemType;
21+
import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler;
22+
import org.springframework.ide.vscode.boot.java.reconcilers.PathInControllerAnnotationReconciler;
23+
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem;
24+
25+
public class PathInControllerAnnotationReconcilerTest extends BaseReconcilerTest {
26+
27+
@Override
28+
protected String getFolder() {
29+
return "pathincontrollerannotation";
30+
}
31+
32+
@Override
33+
protected String getProjectName() {
34+
return "test-spring-validations";
35+
}
36+
37+
@Override
38+
protected JdtAstReconciler getReconciler() {
39+
return new PathInControllerAnnotationReconciler();
40+
}
41+
42+
@BeforeEach
43+
void setup() throws Exception {
44+
super.setup();
45+
}
46+
47+
@AfterEach
48+
void tearDown() throws Exception {
49+
super.tearDown();
50+
}
51+
52+
@Test
53+
void restControllerWithPath() throws Exception {
54+
String source = """
55+
package example.demo;
56+
57+
import org.springframework.web.bind.annotation.RestController;
58+
59+
@RestController("/mypath")
60+
public class A {
61+
}
62+
""";
63+
List<ReconcileProblem> problems = reconcile("A.java", source, false);
64+
65+
assertEquals(1, problems.size());
66+
67+
ReconcileProblem problem = problems.get(0);
68+
69+
assertEquals(Boot2JavaProblemType.PATH_IN_CONTROLLER_ANNOTATION, problem.getType());
70+
71+
String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength());
72+
assertEquals("\"/mypath\"", markedStr);
73+
74+
assertEquals(0, problem.getQuickfixes().size());
75+
}
76+
77+
@Test
78+
void restControllerWithoutPath() throws Exception {
79+
String source = """
80+
package example.demo;
81+
82+
import org.springframework.web.bind.annotation.RestController;
83+
84+
@RestController("mypath")
85+
public class A {
86+
}
87+
""";
88+
List<ReconcileProblem> problems = reconcile("A.java", source, false);
89+
90+
assertEquals(0, problems.size());
91+
}
92+
93+
@Test
94+
void restControllerWithoutAnything() throws Exception {
95+
String source = """
96+
package example.demo;
97+
98+
import org.springframework.web.bind.annotation.RestController;
99+
100+
@RestController
101+
public class A {
102+
}
103+
""";
104+
List<ReconcileProblem> problems = reconcile("A.java", source, false);
105+
106+
assertEquals(0, problems.size());
107+
}
108+
109+
@Test
110+
void restSimpleControllerWithoutAnything() throws Exception {
111+
String source = """
112+
package example.demo;
113+
114+
import org.springframework.stereotype.Controller;
115+
116+
@Controller
117+
public class A {
118+
}
119+
""";
120+
List<ReconcileProblem> problems = reconcile("A.java", source, false);
121+
122+
assertEquals(0, problems.size());
123+
}
124+
125+
}

vscode-extensions/vscode-spring-boot/package.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,18 @@
706706
"HINT",
707707
"ERROR"
708708
]
709+
},
710+
"spring-boot.ls.problem.boot2.PATH_IN_CONTROLLER_ANNOTATION": {
711+
"type": "string",
712+
"default": "WARNING",
713+
"description": "Controller annotation default attribute might contain a path - whereas the default attribute is the component name",
714+
"enum": [
715+
"IGNORE",
716+
"INFO",
717+
"WARNING",
718+
"HINT",
719+
"ERROR"
720+
]
709721
}
710722
}
711723
},

0 commit comments

Comments
 (0)