Skip to content

Commit 4875f40

Browse files
committed
Merge branch 'main' of https://github.com/spring-projects/spring-tools into main
2 parents 522acf1 + 652592f commit 4875f40

File tree

6 files changed

+300
-1
lines changed

6 files changed

+300
-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
@@ -47,6 +47,7 @@
4747
import org.springframework.ide.vscode.boot.java.reconcilers.ClasspathResourceTypeReconciler;
4848
import org.springframework.ide.vscode.boot.java.reconcilers.EntityIdForRepoReconciler;
4949
import org.springframework.ide.vscode.boot.java.reconcilers.FeignClientReconciler;
50+
import org.springframework.ide.vscode.boot.java.reconcilers.FinalAutowiredFieldReconciler;
5051
import org.springframework.ide.vscode.boot.java.reconcilers.HttpSecurityLambdaDslReconciler;
5152
import org.springframework.ide.vscode.boot.java.reconcilers.ImplicitWebAnnotationNamesReconciler;
5253
import org.springframework.ide.vscode.boot.java.reconcilers.ModulithTypeReferenceViolationReconciler;
@@ -99,6 +100,10 @@ public class JdtConfig {
99100
return new AutowiredFieldIntoConstructorParameterReconciler(server.getQuickfixRegistry());
100101
}
101102

103+
@Bean FinalAutowiredFieldReconciler finalAutowiredFieldReconciler() {
104+
return new FinalAutowiredFieldReconciler();
105+
}
106+
102107
@Bean Boot3NotSupportedTypeReconciler boot3NotSupportedTypeReconciler() {
103108
return new Boot3NotSupportedTypeReconciler();
104109
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ public enum Boot2JavaProblemType implements ProblemType {
3737
WEB_ANNOTATION_NAMES(HINT, "Web annotation names are unnecessary when it is the same as method parameter name", "Implicit web annotations names", List.of(DiagnosticTag.Unnecessary)),
3838
VALUE_CLASSPATH_RESOURCE_TYPE(WARNING, "Type is not compatible with classpath resource injection", "Invalid type for classpath resource in `@Value`"),
3939
WEB_CONFIGURER_CONFIGURATION(WARNING, "Class implementing a web configurer interface should be annotated with '@Configuration'", "Missing '@Configuration' on web configurer"),
40-
MISSING_VALIDATED_ANNOTATION(WARNING, "Component class using bean validation annotations should be annotated with '@Validated'", "Missing '@Validated' on component");
40+
MISSING_VALIDATED_ANNOTATION(WARNING, "Component class using bean validation annotations should be annotated with '@Validated'", "Missing '@Validated' on component"),
41+
JAVA_FINAL_AUTOWIRED_FIELD(WARNING, "`@Autowired` field should not be `final`", "`final` `@Autowired` field");
4142

4243
private final ProblemSeverity defaultSeverity;
4344
private final String description;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2026 Broadcom
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 - 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.FieldDeclaration;
23+
import org.eclipse.jdt.core.dom.Modifier;
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.commons.java.IClasspathUtil;
29+
import org.springframework.ide.vscode.commons.java.IJavaProject;
30+
import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType;
31+
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl;
32+
33+
public class FinalAutowiredFieldReconciler implements JdtAstReconciler {
34+
35+
private static final String LABEL = "`@Autowired` field should not be `final`";
36+
37+
@Override
38+
public boolean isApplicable(IJavaProject project) {
39+
return springBootVersionGreaterOrEqual(2, 0, 0).test(project);
40+
}
41+
42+
@Override
43+
public ProblemType getProblemType() {
44+
return Boot2JavaProblemType.JAVA_FINAL_AUTOWIRED_FIELD;
45+
}
46+
47+
@Override
48+
public ASTVisitor createVisitor(IJavaProject project, URI docUri, CompilationUnit cu, ReconcilingContext context) {
49+
Path sourceFile = Paths.get(docUri);
50+
// Check if source file belongs to non-test java sources folder
51+
if (IClasspathUtil.getProjectJavaSourceFoldersWithoutTests(project.getClasspath())
52+
.anyMatch(f -> sourceFile.startsWith(f.toPath()))) {
53+
final AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(cu);
54+
55+
return new ASTVisitor() {
56+
57+
@Override
58+
public boolean visit(FieldDeclaration field) {
59+
Annotation annotation = ReconcileUtils.findAnnotation(annotationHierarchies, field,
60+
Annotations.AUTOWIRED, false);
61+
if (annotation != null && field.getParent() instanceof TypeDeclaration) {
62+
if (isFinal(field)) {
63+
ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), LABEL,
64+
field.getStartPosition(), field.getLength());
65+
context.getProblemCollector().accept(problem);
66+
}
67+
}
68+
return true;
69+
}
70+
71+
};
72+
} else {
73+
return null;
74+
}
75+
}
76+
77+
private static boolean isFinal(FieldDeclaration field) {
78+
for (Object modifier : field.modifiers()) {
79+
if (modifier instanceof Modifier && ((Modifier) modifier).isFinal()) {
80+
return true;
81+
}
82+
}
83+
return false;
84+
}
85+
86+
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,12 @@
109109
"label": "Missing '@Validated' on component",
110110
"description": "Component class using bean validation annotations should be annotated with '@Validated'",
111111
"defaultSeverity": "WARNING"
112+
},
113+
{
114+
"code": "JAVA_FINAL_AUTOWIRED_FIELD",
115+
"label": "`final` `@Autowired` field",
116+
"description": "`@Autowired` field should not be `final`",
117+
"defaultSeverity": "WARNING"
112118
}
113119
]
114120
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2026 Broadcom
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 - 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.FinalAutowiredFieldReconciler;
22+
import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler;
23+
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem;
24+
25+
public class FinalAutowiredFieldReconcilerTest extends BaseReconcilerTest {
26+
27+
@Override
28+
protected String getFolder() {
29+
return "finalautowiredfieldtest";
30+
}
31+
32+
@Override
33+
protected String getProjectName() {
34+
return "test-spring-indexing";
35+
}
36+
37+
protected JdtAstReconciler getReconciler() {
38+
return new FinalAutowiredFieldReconciler();
39+
}
40+
41+
@BeforeEach
42+
void setup() throws Exception {
43+
super.setup();
44+
}
45+
46+
@AfterEach
47+
void tearDown() throws Exception {
48+
super.tearDown();
49+
}
50+
51+
@Test
52+
void finalAutowiredField() throws Exception {
53+
String source = """
54+
package example.demo;
55+
56+
import org.springframework.beans.factory.annotation.Autowired;
57+
58+
class A {
59+
60+
@Autowired
61+
final String a = "";
62+
63+
}
64+
""";
65+
List<ReconcileProblem> problems = reconcile("A.java", source, false);
66+
67+
assertEquals(1, problems.size());
68+
69+
ReconcileProblem problem = problems.get(0);
70+
71+
assertEquals(Boot2JavaProblemType.JAVA_FINAL_AUTOWIRED_FIELD, problem.getType());
72+
73+
String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength());
74+
assertEquals("@Autowired\n\tfinal String a = \"\";", markedStr);
75+
}
76+
77+
@Test
78+
void nonFinalAutowiredField() throws Exception {
79+
String source = """
80+
package example.demo;
81+
82+
import org.springframework.beans.factory.annotation.Autowired;
83+
84+
class A {
85+
86+
@Autowired
87+
String a;
88+
89+
}
90+
""";
91+
List<ReconcileProblem> problems = reconcile("A.java", source, false);
92+
93+
assertEquals(0, problems.size());
94+
}
95+
96+
@Test
97+
void noAutowiredAnnotation() throws Exception {
98+
String source = """
99+
package example.demo;
100+
101+
class A {
102+
103+
final String a = "";
104+
105+
}
106+
""";
107+
List<ReconcileProblem> problems = reconcile("A.java", source, false);
108+
109+
assertEquals(0, problems.size());
110+
}
111+
112+
@Test
113+
void finalAutowiredFieldWithPrivateModifier() throws Exception {
114+
String source = """
115+
package example.demo;
116+
117+
import org.springframework.beans.factory.annotation.Autowired;
118+
119+
class A {
120+
121+
@Autowired
122+
private final String a = "";
123+
124+
}
125+
""";
126+
List<ReconcileProblem> problems = reconcile("A.java", source, false);
127+
128+
assertEquals(1, problems.size());
129+
130+
ReconcileProblem problem = problems.get(0);
131+
132+
assertEquals(Boot2JavaProblemType.JAVA_FINAL_AUTOWIRED_FIELD, problem.getType());
133+
}
134+
135+
@Test
136+
void multipleFinalAutowiredFields() throws Exception {
137+
String source = """
138+
package example.demo;
139+
140+
import org.springframework.beans.factory.annotation.Autowired;
141+
142+
class A {
143+
144+
@Autowired
145+
final String a = "";
146+
147+
@Autowired
148+
final String b = "";
149+
150+
}
151+
""";
152+
List<ReconcileProblem> problems = reconcile("A.java", source, false);
153+
154+
assertEquals(2, problems.size());
155+
156+
assertEquals(Boot2JavaProblemType.JAVA_FINAL_AUTOWIRED_FIELD, problems.get(0).getType());
157+
assertEquals(Boot2JavaProblemType.JAVA_FINAL_AUTOWIRED_FIELD, problems.get(1).getType());
158+
}
159+
160+
@Test
161+
void mixedFinalAndNonFinalAutowiredFields() throws Exception {
162+
String source = """
163+
package example.demo;
164+
165+
import org.springframework.beans.factory.annotation.Autowired;
166+
167+
class A {
168+
169+
@Autowired
170+
final String a = "";
171+
172+
@Autowired
173+
String b;
174+
175+
}
176+
""";
177+
List<ReconcileProblem> problems = reconcile("A.java", source, false);
178+
179+
assertEquals(1, problems.size());
180+
181+
ReconcileProblem problem = problems.get(0);
182+
183+
assertEquals(Boot2JavaProblemType.JAVA_FINAL_AUTOWIRED_FIELD, problem.getType());
184+
185+
String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength());
186+
assertEquals("@Autowired\n\tfinal String a = \"\";", markedStr);
187+
}
188+
189+
}

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,18 @@
755755
"HINT",
756756
"ERROR"
757757
]
758+
},
759+
"spring-boot.ls.problem.boot2.JAVA_FINAL_AUTOWIRED_FIELD": {
760+
"type": "string",
761+
"default": "WARNING",
762+
"description": "`@Autowired` field should not be `final`",
763+
"enum": [
764+
"IGNORE",
765+
"INFO",
766+
"WARNING",
767+
"HINT",
768+
"ERROR"
769+
]
758770
}
759771
}
760772
},

0 commit comments

Comments
 (0)