Skip to content

Commit 2c5addd

Browse files
committed
Remove annotation and import if unused via JDT
Signed-off-by: BoykoAlex <alex.boyko@broadcom.com> # Conflicts: # headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/jdt/refactoring/ChangeMethodVisibilityRefactoring.java # headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/jdt/refactoring/ChangeMethodVisibilityRefactoringTest.java
1 parent 061d3b9 commit 2c5addd

11 files changed

Lines changed: 753 additions & 216 deletions

File tree

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

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2026 Broadcom
2+
* Copyright (c) 2026 Broadcom, Inc.
33
* All rights reserved. This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v1.0
55
* which accompanies this distribution, and is available at
@@ -11,11 +11,12 @@
1111
package org.springframework.ide.vscode.boot.java.jdt.refactoring;
1212

1313
import org.eclipse.jdt.core.dom.AST;
14-
import org.eclipse.jdt.core.dom.ASTVisitor;
14+
import org.eclipse.jdt.core.dom.ASTNode;
1515
import org.eclipse.jdt.core.dom.CompilationUnit;
1616
import org.eclipse.jdt.core.dom.MethodDeclaration;
1717
import org.eclipse.jdt.core.dom.Modifier;
1818
import org.eclipse.jdt.core.dom.Modifier.ModifierKeyword;
19+
import org.eclipse.jdt.core.dom.NodeFinder;
1920
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
2021
import org.eclipse.jdt.core.dom.rewrite.ListRewrite;
2122

@@ -82,16 +83,18 @@ private ModifierKeyword getModifierKeyword(Visibility visibility) {
8283
}
8384

8485
private MethodDeclaration findMethodAtOffset(CompilationUnit cu, int offset) {
85-
MethodDeclaration[] result = new MethodDeclaration[1];
86-
cu.accept(new ASTVisitor() {
87-
@Override
88-
public boolean visit(MethodDeclaration node) {
89-
if (node.getStartPosition() == offset) {
90-
result[0] = node;
86+
ASTNode node = NodeFinder.perform(cu, offset, 0);
87+
while (node != null) {
88+
if (node instanceof MethodDeclaration m) {
89+
int start = m.getName().getStartPosition();
90+
int end = start + m.getName().getLength();
91+
if (offset >= start && offset <= end) {
92+
return m;
9193
}
92-
return result[0] == null; // stop visiting if found
94+
return null;
9395
}
94-
});
95-
return result[0];
96+
node = node.getParent();
97+
}
98+
return null;
9699
}
97100
}

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

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,24 @@
1111
package org.springframework.ide.vscode.boot.java.jdt.refactoring;
1212

1313
import java.util.ArrayList;
14+
import java.util.HashMap;
15+
import java.util.HashSet;
1416
import java.util.List;
17+
import java.util.Map;
18+
import java.util.Set;
1519

1620
import org.eclipse.jdt.core.dom.AST;
21+
import org.eclipse.jdt.core.dom.ASTNode;
22+
import org.eclipse.jdt.core.dom.ASTVisitor;
23+
import org.eclipse.jdt.core.dom.ChildListPropertyDescriptor;
1724
import org.eclipse.jdt.core.dom.CompilationUnit;
25+
import org.eclipse.jdt.core.dom.IBinding;
26+
import org.eclipse.jdt.core.dom.ITypeBinding;
1827
import org.eclipse.jdt.core.dom.ImportDeclaration;
28+
import org.eclipse.jdt.core.dom.Name;
29+
import org.eclipse.jdt.core.dom.QualifiedName;
30+
import org.eclipse.jdt.core.dom.SimpleName;
31+
import org.eclipse.jdt.core.dom.StructuralPropertyDescriptor;
1932
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
2033
import org.eclipse.jdt.core.dom.rewrite.ListRewrite;
2134
import org.eclipse.lsp4j.TextDocumentEdit;
@@ -35,6 +48,137 @@
3548
*/
3649
public final class JdtRefactorUtils {
3750

51+
public static void removeImports(CompilationUnit cu, ASTRewrite rewrite, String... fqns) {
52+
Set<String> fqnsToCheck = new HashSet<>();
53+
Map<String, List<ImportDeclaration>> fqnToImports = new HashMap<>();
54+
55+
for (String fqn : fqns) {
56+
for (Object importObj : cu.imports()) {
57+
ImportDeclaration imp = (ImportDeclaration) importObj;
58+
if (!imp.isOnDemand() && imp.getName().getFullyQualifiedName().equals(fqn)) {
59+
fqnsToCheck.add(fqn);
60+
fqnToImports.computeIfAbsent(fqn, k -> new ArrayList<>()).add(imp);
61+
}
62+
}
63+
}
64+
65+
if (fqnsToCheck.isEmpty()) {
66+
return;
67+
}
68+
69+
Set<String> usedFqns = getUsedTypes(cu, rewrite, fqnsToCheck);
70+
71+
ListRewrite importsRewrite = null;
72+
for (String fqn : fqnsToCheck) {
73+
if (!usedFqns.contains(fqn)) {
74+
if (importsRewrite == null) {
75+
importsRewrite = rewrite.getListRewrite(cu, CompilationUnit.IMPORTS_PROPERTY);
76+
}
77+
for (ImportDeclaration imp : fqnToImports.get(fqn)) {
78+
importsRewrite.remove(imp, null);
79+
}
80+
}
81+
}
82+
}
83+
84+
private static Set<String> getUsedTypes(CompilationUnit cu, ASTRewrite rewrite, Set<String> fqnsToCheck) {
85+
Set<String> usedFqns = new HashSet<>();
86+
87+
cu.accept(new ASTVisitor() {
88+
89+
private void checkTypeRef(Name node) {
90+
if (usedFqns.size() == fqnsToCheck.size()) return; // All found
91+
92+
if (node == null) return;
93+
94+
// Get the leftmost qualifier
95+
while (node.isQualifiedName()) {
96+
node = ((QualifiedName) node).getQualifier();
97+
}
98+
99+
IBinding binding = node.resolveBinding();
100+
String fqn = null;
101+
102+
if (binding instanceof ITypeBinding) {
103+
fqn = ((ITypeBinding) binding).getErasure().getQualifiedName();
104+
}
105+
106+
if (fqn != null && fqnsToCheck.contains(fqn) && !usedFqns.contains(fqn)) {
107+
if (survivesRewrite(node, rewrite)) {
108+
usedFqns.add(fqn);
109+
}
110+
}
111+
}
112+
113+
@Override
114+
public boolean visit(SimpleName node) {
115+
// If we get here directly, it might be a static reference
116+
if (!isInsideImport(node)) {
117+
checkTypeRef(node);
118+
}
119+
return true;
120+
}
121+
122+
});
123+
return usedFqns;
124+
}
125+
126+
private static boolean isInsideImport(ASTNode node) {
127+
ASTNode current = node;
128+
while (current != null) {
129+
if (current instanceof ImportDeclaration) {
130+
return true;
131+
}
132+
current = current.getParent();
133+
}
134+
return false;
135+
}
136+
137+
private static boolean survivesRewrite(ASTNode node, ASTRewrite rewrite) {
138+
ASTNode current = node;
139+
while (current != null) {
140+
ASTNode parent = current.getParent();
141+
if (parent != null) {
142+
StructuralPropertyDescriptor prop = current.getLocationInParent();
143+
if (prop != null) {
144+
if (prop.isChildListProperty()) {
145+
ListRewrite listRewrite = rewrite.getListRewrite(parent, (ChildListPropertyDescriptor) prop);
146+
147+
List<?> originalList = listRewrite.getOriginalList();
148+
List<?> rewrittenList = listRewrite.getRewrittenList();
149+
150+
boolean inOriginal = false;
151+
for (Object o : originalList) {
152+
if (o == current) {
153+
inOriginal = true;
154+
break;
155+
}
156+
}
157+
158+
boolean inRewritten = false;
159+
for (Object o : rewrittenList) {
160+
if (o == current) {
161+
inRewritten = true;
162+
break;
163+
}
164+
}
165+
166+
if (inOriginal && !inRewritten) {
167+
return false; // Removed from list
168+
}
169+
} else {
170+
Object rewrittenNode = rewrite.get(parent, prop);
171+
if (rewrittenNode != current) {
172+
return false; // Replaced or removed
173+
}
174+
}
175+
}
176+
}
177+
current = parent;
178+
}
179+
return true;
180+
}
181+
38182
/**
39183
* Add an import for the given {@link ClassType} to the compilation unit, unless
40184
* the import is unnecessary.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2026 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+
* VMware, Inc. - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.ide.vscode.boot.java.jdt.refactoring;
12+
13+
import java.util.HashSet;
14+
import java.util.Set;
15+
16+
import org.eclipse.jdt.core.dom.ASTNode;
17+
import org.eclipse.jdt.core.dom.Annotation;
18+
import org.eclipse.jdt.core.dom.ChildListPropertyDescriptor;
19+
import org.eclipse.jdt.core.dom.CompilationUnit;
20+
import org.eclipse.jdt.core.dom.ITypeBinding;
21+
import org.eclipse.jdt.core.dom.NodeFinder;
22+
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
23+
import org.eclipse.jdt.core.dom.rewrite.ListRewrite;
24+
25+
/**
26+
* A JDT-based refactoring that removes annotations identified by their start positions.
27+
* <p>
28+
* Pass one or more annotation offsets to remove specific annotations.
29+
* When used with a single offset this corresponds to a node-scoped quickfix.
30+
* When used with multiple offsets (all occurrences in a file) this corresponds
31+
* to a file-scoped "fix all" quickfix.
32+
*/
33+
public class RemoveAnnotationRefactoring implements JdtRefactoring {
34+
35+
private final int[] annotationOffsets;
36+
37+
/**
38+
* @param annotationOffsets start positions of the annotation nodes to remove
39+
*/
40+
public RemoveAnnotationRefactoring(int... annotationOffsets) {
41+
this.annotationOffsets = annotationOffsets;
42+
}
43+
44+
@Override
45+
public void apply(ASTRewrite rewrite, CompilationUnit cu) {
46+
Set<String> fqnsToCheck = new HashSet<>();
47+
48+
for (int offset : annotationOffsets) {
49+
Annotation annotation = findAnnotationAtOffset(cu, offset);
50+
if (annotation != null) {
51+
ASTNode parent = annotation.getParent();
52+
ChildListPropertyDescriptor property = (ChildListPropertyDescriptor) annotation.getLocationInParent();
53+
ListRewrite modifiersRewrite = rewrite.getListRewrite(parent, property);
54+
modifiersRewrite.remove(annotation, null);
55+
56+
ITypeBinding binding = annotation.resolveTypeBinding();
57+
if (binding != null) {
58+
fqnsToCheck.add(binding.getErasure().getQualifiedName());
59+
}
60+
}
61+
}
62+
63+
if (!fqnsToCheck.isEmpty()) {
64+
JdtRefactorUtils.removeImports(cu, rewrite, fqnsToCheck.toArray(new String[fqnsToCheck.size()]));
65+
}
66+
}
67+
68+
private static Annotation findAnnotationAtOffset(CompilationUnit cu, int offset) {
69+
ASTNode node = NodeFinder.perform(cu, offset, 0);
70+
while (node != null) {
71+
if (node instanceof Annotation a) {
72+
int start = a.getStartPosition();
73+
int end = a.getTypeName().getStartPosition() + a.getTypeName().getLength();
74+
if (offset >= start && offset <= end) {
75+
return a;
76+
}
77+
return null;
78+
}
79+
node = node.getParent();
80+
}
81+
return null;
82+
}
83+
84+
}

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import java.lang.reflect.Field;
1414
import java.net.URI;
15+
import java.util.ArrayList;
1516
import java.util.List;
1617

1718
import org.eclipse.jdt.core.dom.ASTVisitor;
@@ -69,8 +70,8 @@ public ASTVisitor createVisitor(IJavaProject project, URI docUri, CompilationUni
6970

7071
return new ASTVisitor() {
7172

72-
private final List<Integer> problemOffsets = new java.util.ArrayList<>();
73-
private final List<ReconcileProblemImpl> problems = new java.util.ArrayList<>();
73+
private final List<Integer> problemOffsets = new ArrayList<>();
74+
private final List<ReconcileProblemImpl> problems = new ArrayList<>();
7475

7576
@Override
7677
public boolean visit(SingleMemberAnnotation node) {
@@ -148,7 +149,7 @@ private void visitAnnotation(IJavaProject project, CompilationUnit cu, URI docUr
148149
method.getName().getStartPosition(), method.getName().getLength()));
149150

150151
addQuickFixes(cu, docUri, problem, method);
151-
problemOffsets.add(method.getStartPosition());
152+
problemOffsets.add(method.getName().getStartPosition());
152153
problems.add(problem);
153154

154155
problemCollector.accept(problem);
@@ -176,7 +177,7 @@ private void addQuickFixes(CompilationUnit cu, URI docUri, ReconcileProblemImpl
176177
if (quickfixType != null) {
177178
String uri = docUri.toASCIIString();
178179
JdtFixDescriptor descriptor = new JdtFixDescriptor(
179-
new ChangeMethodVisibilityRefactoring(Visibility.PACKAGE_PRIVATE, method.getStartPosition()),
180+
new ChangeMethodVisibilityRefactoring(Visibility.PACKAGE_PRIVATE, method.getName().getStartPosition()),
180181
List.of(uri), LABEL);
181182
problem.addQuickfix(new QuickfixData<>(quickfixType, descriptor, LABEL, true));
182183
}

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

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2023, 2025 VMware, Inc.
2+
* Copyright (c) 2023, 2026 VMware, Inc.
33
* All rights reserved. This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v1.0
55
* which accompanies this distribution, and is available at
@@ -22,25 +22,27 @@
2222
import org.eclipse.jdt.core.dom.CompilationUnit;
2323
import org.eclipse.jdt.core.dom.MethodDeclaration;
2424
import org.eclipse.jdt.core.dom.TypeDeclaration;
25-
import org.openrewrite.java.spring.NoAutowiredOnConstructor;
2625
import org.springframework.ide.vscode.boot.java.Annotations;
2726
import org.springframework.ide.vscode.boot.java.Boot2JavaProblemType;
2827
import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies;
28+
import org.springframework.ide.vscode.boot.java.jdt.refactoring.JdtFixDescriptor;
29+
import org.springframework.ide.vscode.boot.java.jdt.refactoring.JdtRefactorings;
30+
import org.springframework.ide.vscode.boot.java.jdt.refactoring.RemoveAnnotationRefactoring;
2931
import org.springframework.ide.vscode.boot.java.utils.ASTUtils;
3032
import org.springframework.ide.vscode.commons.java.IClasspathUtil;
3133
import org.springframework.ide.vscode.commons.java.IJavaProject;
34+
import org.springframework.ide.vscode.commons.languageserver.quickfix.Quickfix.QuickfixData;
3235
import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry;
36+
import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixType;
3337
import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType;
3438
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl;
35-
import org.springframework.ide.vscode.commons.rewrite.config.RecipeScope;
36-
import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor;
3739

3840
public class NoAutowiredOnConstructorReconciler implements JdtAstReconciler {
3941

4042
private static final String PROBLEM_LABEL = "Unnecessary `@Autowired` annotation";
4143
private static final String FIX_LABEL = "Remove unnecessary `@Autowired` annotation";
4244

43-
private QuickfixRegistry registry;
45+
private final QuickfixRegistry registry;
4446

4547
public NoAutowiredOnConstructorReconciler(QuickfixRegistry registry) {
4648
this.registry = registry;
@@ -97,10 +99,14 @@ public boolean visit(TypeDeclaration typeDecl) {
9799
if (autowiredAnnotation != null) {
98100
ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), PROBLEM_LABEL,
99101
autowiredAnnotation.getStartPosition(), autowiredAnnotation.getLength());
100-
ReconcileUtils.setRewriteFixes(registry, problem,
101-
List.of(new FixDescriptor(NoAutowiredOnConstructor.class.getName(), List.of(docUri.toASCIIString()), FIX_LABEL)
102-
.withRecipeScope(RecipeScope.NODE)
103-
.withRangeScope(ReconcileUtils.createOpenRewriteRange(cu, typeDecl, null))));
102+
QuickfixType quickfixType = registry.getQuickfixType(JdtRefactorings.JDT_QUICKFIX);
103+
if (quickfixType != null) {
104+
JdtFixDescriptor fix = new JdtFixDescriptor(
105+
new RemoveAnnotationRefactoring(autowiredAnnotation.getStartPosition()),
106+
List.of(docUri.toASCIIString()),
107+
FIX_LABEL);
108+
problem.addQuickfix(new QuickfixData<>(quickfixType, fix, FIX_LABEL, true));
109+
}
104110
context.getProblemCollector().accept(problem);
105111
}
106112
}

0 commit comments

Comments
 (0)