From 2c5addd7f6b2aaf21c0ad5414569d58b9c88e0d9 Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Wed, 29 Apr 2026 18:15:48 -0700 Subject: [PATCH 1/2] Remove annotation and import if unused via JDT Signed-off-by: BoykoAlex # 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 --- .../ChangeMethodVisibilityRefactoring.java | 25 ++- .../jdt/refactoring/JdtRefactorUtils.java | 144 ++++++++++++ .../RemoveAnnotationRefactoring.java | 84 +++++++ .../BeanMethodNotPublicReconciler.java | 9 +- .../NoAutowiredOnConstructorReconciler.java | 24 +- .../NoRepoAnnotationReconciler.java | 62 ++++-- .../vscode/boot/bootiful/IndexerTestConf.java | 6 + ...ChangeMethodVisibilityRefactoringTest.java | 33 ++- .../RemoveAnnotationRefactoringTest.java | 188 ++++++++++++++++ ...oAutowiredOnConstructorReconcilerTest.java | 184 ++++++++------- .../test/NoRepoAnnotationReconcilerTest.java | 210 ++++++++++-------- 11 files changed, 753 insertions(+), 216 deletions(-) create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/jdt/refactoring/RemoveAnnotationRefactoring.java create mode 100644 headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/jdt/refactoring/RemoveAnnotationRefactoringTest.java diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/jdt/refactoring/ChangeMethodVisibilityRefactoring.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/jdt/refactoring/ChangeMethodVisibilityRefactoring.java index 326b254758..929e37ebfd 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/jdt/refactoring/ChangeMethodVisibilityRefactoring.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/jdt/refactoring/ChangeMethodVisibilityRefactoring.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2026 Broadcom + * Copyright (c) 2026 Broadcom, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -11,11 +11,12 @@ package org.springframework.ide.vscode.boot.java.jdt.refactoring; import org.eclipse.jdt.core.dom.AST; -import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.MethodDeclaration; import org.eclipse.jdt.core.dom.Modifier; import org.eclipse.jdt.core.dom.Modifier.ModifierKeyword; +import org.eclipse.jdt.core.dom.NodeFinder; import org.eclipse.jdt.core.dom.rewrite.ASTRewrite; import org.eclipse.jdt.core.dom.rewrite.ListRewrite; @@ -82,16 +83,18 @@ private ModifierKeyword getModifierKeyword(Visibility visibility) { } private MethodDeclaration findMethodAtOffset(CompilationUnit cu, int offset) { - MethodDeclaration[] result = new MethodDeclaration[1]; - cu.accept(new ASTVisitor() { - @Override - public boolean visit(MethodDeclaration node) { - if (node.getStartPosition() == offset) { - result[0] = node; + ASTNode node = NodeFinder.perform(cu, offset, 0); + while (node != null) { + if (node instanceof MethodDeclaration m) { + int start = m.getName().getStartPosition(); + int end = start + m.getName().getLength(); + if (offset >= start && offset <= end) { + return m; } - return result[0] == null; // stop visiting if found + return null; } - }); - return result[0]; + node = node.getParent(); + } + return null; } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/jdt/refactoring/JdtRefactorUtils.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/jdt/refactoring/JdtRefactorUtils.java index c667b1110f..78ad8dc79a 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/jdt/refactoring/JdtRefactorUtils.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/jdt/refactoring/JdtRefactorUtils.java @@ -11,11 +11,24 @@ package org.springframework.ide.vscode.boot.java.jdt.refactoring; import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import org.eclipse.jdt.core.dom.AST; +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.ChildListPropertyDescriptor; import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.IBinding; +import org.eclipse.jdt.core.dom.ITypeBinding; import org.eclipse.jdt.core.dom.ImportDeclaration; +import org.eclipse.jdt.core.dom.Name; +import org.eclipse.jdt.core.dom.QualifiedName; +import org.eclipse.jdt.core.dom.SimpleName; +import org.eclipse.jdt.core.dom.StructuralPropertyDescriptor; import org.eclipse.jdt.core.dom.rewrite.ASTRewrite; import org.eclipse.jdt.core.dom.rewrite.ListRewrite; import org.eclipse.lsp4j.TextDocumentEdit; @@ -35,6 +48,137 @@ */ public final class JdtRefactorUtils { + public static void removeImports(CompilationUnit cu, ASTRewrite rewrite, String... fqns) { + Set fqnsToCheck = new HashSet<>(); + Map> fqnToImports = new HashMap<>(); + + for (String fqn : fqns) { + for (Object importObj : cu.imports()) { + ImportDeclaration imp = (ImportDeclaration) importObj; + if (!imp.isOnDemand() && imp.getName().getFullyQualifiedName().equals(fqn)) { + fqnsToCheck.add(fqn); + fqnToImports.computeIfAbsent(fqn, k -> new ArrayList<>()).add(imp); + } + } + } + + if (fqnsToCheck.isEmpty()) { + return; + } + + Set usedFqns = getUsedTypes(cu, rewrite, fqnsToCheck); + + ListRewrite importsRewrite = null; + for (String fqn : fqnsToCheck) { + if (!usedFqns.contains(fqn)) { + if (importsRewrite == null) { + importsRewrite = rewrite.getListRewrite(cu, CompilationUnit.IMPORTS_PROPERTY); + } + for (ImportDeclaration imp : fqnToImports.get(fqn)) { + importsRewrite.remove(imp, null); + } + } + } + } + + private static Set getUsedTypes(CompilationUnit cu, ASTRewrite rewrite, Set fqnsToCheck) { + Set usedFqns = new HashSet<>(); + + cu.accept(new ASTVisitor() { + + private void checkTypeRef(Name node) { + if (usedFqns.size() == fqnsToCheck.size()) return; // All found + + if (node == null) return; + + // Get the leftmost qualifier + while (node.isQualifiedName()) { + node = ((QualifiedName) node).getQualifier(); + } + + IBinding binding = node.resolveBinding(); + String fqn = null; + + if (binding instanceof ITypeBinding) { + fqn = ((ITypeBinding) binding).getErasure().getQualifiedName(); + } + + if (fqn != null && fqnsToCheck.contains(fqn) && !usedFqns.contains(fqn)) { + if (survivesRewrite(node, rewrite)) { + usedFqns.add(fqn); + } + } + } + + @Override + public boolean visit(SimpleName node) { + // If we get here directly, it might be a static reference + if (!isInsideImport(node)) { + checkTypeRef(node); + } + return true; + } + + }); + return usedFqns; + } + + private static boolean isInsideImport(ASTNode node) { + ASTNode current = node; + while (current != null) { + if (current instanceof ImportDeclaration) { + return true; + } + current = current.getParent(); + } + return false; + } + + private static boolean survivesRewrite(ASTNode node, ASTRewrite rewrite) { + ASTNode current = node; + while (current != null) { + ASTNode parent = current.getParent(); + if (parent != null) { + StructuralPropertyDescriptor prop = current.getLocationInParent(); + if (prop != null) { + if (prop.isChildListProperty()) { + ListRewrite listRewrite = rewrite.getListRewrite(parent, (ChildListPropertyDescriptor) prop); + + List originalList = listRewrite.getOriginalList(); + List rewrittenList = listRewrite.getRewrittenList(); + + boolean inOriginal = false; + for (Object o : originalList) { + if (o == current) { + inOriginal = true; + break; + } + } + + boolean inRewritten = false; + for (Object o : rewrittenList) { + if (o == current) { + inRewritten = true; + break; + } + } + + if (inOriginal && !inRewritten) { + return false; // Removed from list + } + } else { + Object rewrittenNode = rewrite.get(parent, prop); + if (rewrittenNode != current) { + return false; // Replaced or removed + } + } + } + } + current = parent; + } + return true; + } + /** * Add an import for the given {@link ClassType} to the compilation unit, unless * the import is unnecessary. diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/jdt/refactoring/RemoveAnnotationRefactoring.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/jdt/refactoring/RemoveAnnotationRefactoring.java new file mode 100644 index 0000000000..10b5773976 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/jdt/refactoring/RemoveAnnotationRefactoring.java @@ -0,0 +1,84 @@ +/******************************************************************************* + * Copyright (c) 2026 Broadcom, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * VMware, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.jdt.refactoring; + +import java.util.HashSet; +import java.util.Set; + +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.Annotation; +import org.eclipse.jdt.core.dom.ChildListPropertyDescriptor; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.NodeFinder; +import org.eclipse.jdt.core.dom.rewrite.ASTRewrite; +import org.eclipse.jdt.core.dom.rewrite.ListRewrite; + +/** + * A JDT-based refactoring that removes annotations identified by their start positions. + *

+ * Pass one or more annotation offsets to remove specific annotations. + * When used with a single offset this corresponds to a node-scoped quickfix. + * When used with multiple offsets (all occurrences in a file) this corresponds + * to a file-scoped "fix all" quickfix. + */ +public class RemoveAnnotationRefactoring implements JdtRefactoring { + + private final int[] annotationOffsets; + + /** + * @param annotationOffsets start positions of the annotation nodes to remove + */ + public RemoveAnnotationRefactoring(int... annotationOffsets) { + this.annotationOffsets = annotationOffsets; + } + + @Override + public void apply(ASTRewrite rewrite, CompilationUnit cu) { + Set fqnsToCheck = new HashSet<>(); + + for (int offset : annotationOffsets) { + Annotation annotation = findAnnotationAtOffset(cu, offset); + if (annotation != null) { + ASTNode parent = annotation.getParent(); + ChildListPropertyDescriptor property = (ChildListPropertyDescriptor) annotation.getLocationInParent(); + ListRewrite modifiersRewrite = rewrite.getListRewrite(parent, property); + modifiersRewrite.remove(annotation, null); + + ITypeBinding binding = annotation.resolveTypeBinding(); + if (binding != null) { + fqnsToCheck.add(binding.getErasure().getQualifiedName()); + } + } + } + + if (!fqnsToCheck.isEmpty()) { + JdtRefactorUtils.removeImports(cu, rewrite, fqnsToCheck.toArray(new String[fqnsToCheck.size()])); + } + } + + private static Annotation findAnnotationAtOffset(CompilationUnit cu, int offset) { + ASTNode node = NodeFinder.perform(cu, offset, 0); + while (node != null) { + if (node instanceof Annotation a) { + int start = a.getStartPosition(); + int end = a.getTypeName().getStartPosition() + a.getTypeName().getLength(); + if (offset >= start && offset <= end) { + return a; + } + return null; + } + node = node.getParent(); + } + return null; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/BeanMethodNotPublicReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/BeanMethodNotPublicReconciler.java index 7c0495ea81..e10c5785de 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/BeanMethodNotPublicReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/BeanMethodNotPublicReconciler.java @@ -12,6 +12,7 @@ import java.lang.reflect.Field; import java.net.URI; +import java.util.ArrayList; import java.util.List; import org.eclipse.jdt.core.dom.ASTVisitor; @@ -69,8 +70,8 @@ public ASTVisitor createVisitor(IJavaProject project, URI docUri, CompilationUni return new ASTVisitor() { - private final List problemOffsets = new java.util.ArrayList<>(); - private final List problems = new java.util.ArrayList<>(); + private final List problemOffsets = new ArrayList<>(); + private final List problems = new ArrayList<>(); @Override public boolean visit(SingleMemberAnnotation node) { @@ -148,7 +149,7 @@ private void visitAnnotation(IJavaProject project, CompilationUnit cu, URI docUr method.getName().getStartPosition(), method.getName().getLength())); addQuickFixes(cu, docUri, problem, method); - problemOffsets.add(method.getStartPosition()); + problemOffsets.add(method.getName().getStartPosition()); problems.add(problem); problemCollector.accept(problem); @@ -176,7 +177,7 @@ private void addQuickFixes(CompilationUnit cu, URI docUri, ReconcileProblemImpl if (quickfixType != null) { String uri = docUri.toASCIIString(); JdtFixDescriptor descriptor = new JdtFixDescriptor( - new ChangeMethodVisibilityRefactoring(Visibility.PACKAGE_PRIVATE, method.getStartPosition()), + new ChangeMethodVisibilityRefactoring(Visibility.PACKAGE_PRIVATE, method.getName().getStartPosition()), List.of(uri), LABEL); problem.addQuickfix(new QuickfixData<>(quickfixType, descriptor, LABEL, true)); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NoAutowiredOnConstructorReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NoAutowiredOnConstructorReconciler.java index 0a4dfd2bb4..267b66a037 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NoAutowiredOnConstructorReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NoAutowiredOnConstructorReconciler.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023, 2025 VMware, Inc. + * Copyright (c) 2023, 2026 VMware, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -22,25 +22,27 @@ import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.MethodDeclaration; import org.eclipse.jdt.core.dom.TypeDeclaration; -import org.openrewrite.java.spring.NoAutowiredOnConstructor; import org.springframework.ide.vscode.boot.java.Annotations; import org.springframework.ide.vscode.boot.java.Boot2JavaProblemType; import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies; +import org.springframework.ide.vscode.boot.java.jdt.refactoring.JdtFixDescriptor; +import org.springframework.ide.vscode.boot.java.jdt.refactoring.JdtRefactorings; +import org.springframework.ide.vscode.boot.java.jdt.refactoring.RemoveAnnotationRefactoring; import org.springframework.ide.vscode.boot.java.utils.ASTUtils; import org.springframework.ide.vscode.commons.java.IClasspathUtil; import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.languageserver.quickfix.Quickfix.QuickfixData; import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry; +import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixType; import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType; import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl; -import org.springframework.ide.vscode.commons.rewrite.config.RecipeScope; -import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor; public class NoAutowiredOnConstructorReconciler implements JdtAstReconciler { private static final String PROBLEM_LABEL = "Unnecessary `@Autowired` annotation"; private static final String FIX_LABEL = "Remove unnecessary `@Autowired` annotation"; - private QuickfixRegistry registry; + private final QuickfixRegistry registry; public NoAutowiredOnConstructorReconciler(QuickfixRegistry registry) { this.registry = registry; @@ -97,10 +99,14 @@ public boolean visit(TypeDeclaration typeDecl) { if (autowiredAnnotation != null) { ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), PROBLEM_LABEL, autowiredAnnotation.getStartPosition(), autowiredAnnotation.getLength()); - ReconcileUtils.setRewriteFixes(registry, problem, - List.of(new FixDescriptor(NoAutowiredOnConstructor.class.getName(), List.of(docUri.toASCIIString()), FIX_LABEL) - .withRecipeScope(RecipeScope.NODE) - .withRangeScope(ReconcileUtils.createOpenRewriteRange(cu, typeDecl, null)))); + QuickfixType quickfixType = registry.getQuickfixType(JdtRefactorings.JDT_QUICKFIX); + if (quickfixType != null) { + JdtFixDescriptor fix = new JdtFixDescriptor( + new RemoveAnnotationRefactoring(autowiredAnnotation.getStartPosition()), + List.of(docUri.toASCIIString()), + FIX_LABEL); + problem.addQuickfix(new QuickfixData<>(quickfixType, fix, FIX_LABEL, true)); + } context.getProblemCollector().accept(problem); } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NoRepoAnnotationReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NoRepoAnnotationReconciler.java index a9e602fc10..109fb4892a 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NoRepoAnnotationReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NoRepoAnnotationReconciler.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023, 2025 VMware, Inc. + * Copyright (c) 2023, 2026 VMware, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -13,6 +13,7 @@ import static org.springframework.ide.vscode.commons.java.SpringProjectUtil.springBootVersionGreaterOrEqual; import java.net.URI; +import java.util.ArrayList; import java.util.List; import org.eclipse.jdt.core.dom.ASTVisitor; @@ -22,22 +23,25 @@ import org.eclipse.jdt.core.dom.MarkerAnnotation; import org.eclipse.jdt.core.dom.NormalAnnotation; import org.eclipse.jdt.core.dom.TypeDeclaration; -import org.openrewrite.java.spring.NoRepoAnnotationOnRepoInterface; import org.springframework.ide.vscode.boot.java.Annotations; import org.springframework.ide.vscode.boot.java.Boot2JavaProblemType; +import org.springframework.ide.vscode.boot.java.jdt.refactoring.JdtFixDescriptor; +import org.springframework.ide.vscode.boot.java.jdt.refactoring.JdtRefactorings; +import org.springframework.ide.vscode.boot.java.jdt.refactoring.RemoveAnnotationRefactoring; import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.languageserver.quickfix.Quickfix.QuickfixData; import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry; +import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixType; import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl; -import org.springframework.ide.vscode.commons.rewrite.config.RecipeScope; -import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor; public class NoRepoAnnotationReconciler implements JdtAstReconciler { private static final String PROBLEM_LABEL = "Unnecessary @Repository"; private static final String FIX_LABEL = "Remove Unnecessary @Repository"; + private static final String FIX_ALL_LABEL = "Remove all unnecessary @Repository in file"; private static final String INTERFACE_REPOSITORY = "org.springframework.data.repository.Repository"; - private QuickfixRegistry registry; + private final QuickfixRegistry registry; public NoRepoAnnotationReconciler(QuickfixRegistry registry) { this.registry = registry; @@ -56,31 +60,30 @@ public Boot2JavaProblemType getProblemType() { @Override public ASTVisitor createVisitor(IJavaProject project, URI docUri, CompilationUnit cu, ReconcilingContext context) { + List problemOffsets = new ArrayList<>(); + List problems = new ArrayList<>(); + return new ASTVisitor() { @Override public boolean visit(TypeDeclaration typeDecl) { if (typeDecl.isInterface()) { for (Object o : typeDecl.modifiers()) { - if (o instanceof Annotation) { - Annotation a = (Annotation) o; + if (o instanceof Annotation a) { if (isApplicableRepoAnnotation(a)) { ITypeBinding type = typeDecl.resolveBinding(); if (type != null && isRepo(type)) { ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), PROBLEM_LABEL, a.getStartPosition(), a.getLength()); - String uri = docUri.toASCIIString(); - String id = NoRepoAnnotationOnRepoInterface.class.getName(); - ReconcileUtils.setRewriteFixes(registry, problem, List.of( -// new FixDescriptor(ID, List.of(uri), FIX_LABEL) -// .withRangeScope(RewriteQuickFixUtils.createOpenRewriteRange(cu, typeDecl)) -// .withRecipeScope(RecipeScope.NODE), - new FixDescriptor(id, List.of(uri), - ReconcileUtils.buildLabel(FIX_LABEL, RecipeScope.FILE)) - .withRecipeScope(RecipeScope.FILE), - new FixDescriptor(id, List.of(uri), - ReconcileUtils.buildLabel(FIX_LABEL, RecipeScope.PROJECT)) - .withRecipeScope(RecipeScope.PROJECT) - )); + QuickfixType quickfixType = registry.getQuickfixType(JdtRefactorings.JDT_QUICKFIX); + if (quickfixType != null) { + JdtFixDescriptor fix = new JdtFixDescriptor( + new RemoveAnnotationRefactoring(a.getStartPosition()), + List.of(docUri.toASCIIString()), + FIX_LABEL); + problem.addQuickfix(new QuickfixData<>(quickfixType, fix, FIX_LABEL, true)); + } + problemOffsets.add(a.getStartPosition()); + problems.add(problem); context.getProblemCollector().accept(problem); } } @@ -89,6 +92,22 @@ public boolean visit(TypeDeclaration typeDecl) { } return super.visit(typeDecl); } + + @Override + public void endVisit(CompilationUnit node) { + if (!problemOffsets.isEmpty()) { + QuickfixType quickfixType = registry.getQuickfixType(JdtRefactorings.JDT_QUICKFIX); + if (quickfixType != null) { + JdtFixDescriptor fixAll = new JdtFixDescriptor( + new RemoveAnnotationRefactoring(problemOffsets.stream().mapToInt(i -> i).toArray()), + List.of(docUri.toASCIIString()), + FIX_ALL_LABEL); + for (ReconcileProblemImpl problem : problems) { + problem.addQuickfix(new QuickfixData<>(quickfixType, fixAll, FIX_ALL_LABEL, false)); + } + } + } + } }; } @@ -108,7 +127,8 @@ private static boolean isApplicableRepoAnnotation(Annotation a) { } private static boolean isRepo(ITypeBinding t) { - if (INTERFACE_REPOSITORY.equals(t.getQualifiedName())) { + if (t == null) return false; + if (INTERFACE_REPOSITORY.equals(t.getErasure().getQualifiedName())) { return true; } else { for (ITypeBinding st : t.getInterfaces()) { diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/bootiful/IndexerTestConf.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/bootiful/IndexerTestConf.java index 035fc312c0..1b55df01c1 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/bootiful/IndexerTestConf.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/bootiful/IndexerTestConf.java @@ -13,7 +13,9 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.ide.vscode.boot.app.BootJavaConfig; import org.springframework.ide.vscode.boot.app.BootLanguageServerParams; +import org.springframework.ide.vscode.boot.app.ProblemParameterProvider; import org.springframework.ide.vscode.boot.editor.harness.PropertyIndexHarness; import org.springframework.ide.vscode.boot.index.cache.IndexCache; import org.springframework.ide.vscode.boot.index.cache.IndexCacheVoid; @@ -58,4 +60,8 @@ public class IndexerTestConf { return SourceLinkFactory.NO_SOURCE_LINKS; } + @Bean ProblemParameterProvider problemParameterProvider(SimpleLanguageServer server) { + return new ProblemParameterProvider(new BootJavaConfig(server)); + } + } \ No newline at end of file diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/jdt/refactoring/ChangeMethodVisibilityRefactoringTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/jdt/refactoring/ChangeMethodVisibilityRefactoringTest.java index 73acfe7a55..1557144d9c 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/jdt/refactoring/ChangeMethodVisibilityRefactoringTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/jdt/refactoring/ChangeMethodVisibilityRefactoringTest.java @@ -67,7 +67,7 @@ private static int offsetOf(String source, String substring) { } @Test - void publicToPackagePrivate() throws Exception { + void publicToPackagePrivate_offsetInsideMethodName() throws Exception { String source = """ package com.example; @@ -77,7 +77,8 @@ public void test() { } """; - String result = applyRefactoring(source, Visibility.PACKAGE_PRIVATE, offsetOf(source, "public void test")); + // Offset inside the method name "test" + String result = applyRefactoring(source, Visibility.PACKAGE_PRIVATE, offsetOf(source, "test") + 1); assertEquals(""" package com.example; @@ -89,6 +90,24 @@ void test() { """, result); } + @Test + void ignoreOffsetOutsideMethodName() throws Exception { + String source = """ + package com.example; + + class TestClass { + public void test() { + } + } + """; + + // Offset inside the return type "void" + String result = applyRefactoring(source, Visibility.PACKAGE_PRIVATE, offsetOf(source, "void")); + + // Should not modify the source + assertEquals(source, result); + } + @Test void privateToPublic() throws Exception { String source = """ @@ -100,7 +119,7 @@ private void test() { } """; - String result = applyRefactoring(source, Visibility.PUBLIC, offsetOf(source, "private void test")); + String result = applyRefactoring(source, Visibility.PUBLIC, offsetOf(source, "test") + 1); assertEquals(""" package com.example; @@ -123,7 +142,7 @@ void test() { } """; - String result = applyRefactoring(source, Visibility.PROTECTED, offsetOf(source, "void test")); + String result = applyRefactoring(source, Visibility.PROTECTED, offsetOf(source, "test") + 1); assertEquals(""" package com.example; @@ -147,7 +166,7 @@ public void test() { } """; - String result = applyRefactoring(source, Visibility.PRIVATE, offsetOf(source, "@Override")); + String result = applyRefactoring(source, Visibility.PRIVATE, offsetOf(source, "test") + 1); assertEquals(""" package com.example; @@ -175,8 +194,8 @@ protected void test2() { """; String result = applyRefactoring(source, Visibility.PACKAGE_PRIVATE, - offsetOf(source, "public void test1"), - offsetOf(source, "protected void test2")); + offsetOf(source, "test1") + 1, + offsetOf(source, "test2") + 1); assertEquals(""" package com.example; diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/jdt/refactoring/RemoveAnnotationRefactoringTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/jdt/refactoring/RemoveAnnotationRefactoringTest.java new file mode 100644 index 0000000000..4000ee8b4b --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/jdt/refactoring/RemoveAnnotationRefactoringTest.java @@ -0,0 +1,188 @@ +/******************************************************************************* + * Copyright (c) 2026 VMware, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * VMware, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.jdt.refactoring; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Map; + +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.dom.AST; +import org.eclipse.jdt.core.dom.ASTParser; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.rewrite.ASTRewrite; +import org.eclipse.jface.text.Document; +import org.eclipse.text.edits.TextEdit; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link RemoveAnnotationRefactoring}. + */ +class RemoveAnnotationRefactoringTest { + + private static CompilationUnit parseSource(String source) { + ASTParser parser = ASTParser.newParser(AST.JLS25); + parser.setSource(source.toCharArray()); + parser.setKind(ASTParser.K_COMPILATION_UNIT); + parser.setResolveBindings(true); + parser.setEnvironment(new String[0], new String[0], null, true); + parser.setUnitName("Test.java"); + Map options = JavaCore.getOptions(); + JavaCore.setComplianceOptions(JavaCore.VERSION_21, options); + parser.setCompilerOptions(options); + return (CompilationUnit) parser.createAST(null); + } + + private static String applyRefactoring(String source, int... offsets) throws Exception { + CompilationUnit cu = parseSource(source); + ASTRewrite rewrite = ASTRewrite.create(cu.getAST()); + new RemoveAnnotationRefactoring(offsets).apply(rewrite, cu); + Document doc = new Document(source); + TextEdit edit = rewrite.rewriteAST(doc, JavaCore.getOptions()); + edit.apply(doc); + return doc.get(); + } + + private static int offsetOf(String source, String substring) { + return source.indexOf(substring); + } + + @Test + void removeMarkerAnnotationFromConstructorAndImport() throws Exception { + String source = """ + package com.example; + + import java.lang.annotation.Documented; + + class MyService { + @Documented + MyService() {} + } + """; + + // Offset inside the annotation name + String result = applyRefactoring(source, offsetOf(source, "@Documented") + 2); + + assertEquals(""" + package com.example; + + class MyService { + MyService() {} + } + """, result); + } + + @Test + void removeMarkerAnnotationButKeepImportIfUsed() throws Exception { + String source = """ + package com.example; + + import java.lang.annotation.Documented; + + class MyService { + @Documented + MyService() {} + + @Documented + MyService(int x) {} + } + """; + + // Remove only the first annotation + String result = applyRefactoring(source, offsetOf(source, "@Documented") + 2); + + assertEquals(""" + package com.example; + + import java.lang.annotation.Documented; + + class MyService { + MyService() {} + + @Documented + MyService(int x) {} + } + """, result); + } + + @Test + void removeMarkerAnnotationFromTypeDeclaration() throws Exception { + String source = """ + package com.example; + + import java.lang.annotation.Documented; + import java.io.Serializable; + + @Documented + interface PersonRepo extends Serializable {} + """; + + // Offset at the start of the annotation name + String result = applyRefactoring(source, offsetOf(source, "@Documented") + 1); + + assertEquals(""" + package com.example; + + import java.io.Serializable; + + interface PersonRepo extends Serializable {} + """, result); + } + + @Test + void removeBatchAnnotations() throws Exception { + String source = """ + package com.example; + + import java.lang.annotation.Documented; + import java.io.Serializable; + + @Documented + interface Repo1 extends Serializable {} + + @Documented + interface Repo2 extends Serializable {} + """; + + String result = applyRefactoring(source, + offsetOf(source, "@Documented\ninterface Repo1") + 1, + offsetOf(source, "@Documented\ninterface Repo2") + 2); + + assertEquals(""" + package com.example; + + import java.io.Serializable; + + interface Repo1 extends Serializable {} + + interface Repo2 extends Serializable {} + """, result); + } + + @Test + void ignoreOffsetOutsideAnnotationName() throws Exception { + String source = """ + package com.example; + + import java.lang.annotation.Documented; + + @Documented + interface Repo1 {} + """; + + // Offset inside the parentheses, which is outside the name node + String result = applyRefactoring(source, offsetOf(source, "Repo1")); + + // Should not modify the source + assertEquals(source, result); + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/NoAutowiredOnConstructorReconcilerTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/NoAutowiredOnConstructorReconcilerTest.java index 42eea76a72..098f2138f6 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/NoAutowiredOnConstructorReconcilerTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/NoAutowiredOnConstructorReconcilerTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023, 2025 VMware, Inc. + * Copyright (c) 2023, 2026 VMware, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -11,114 +11,146 @@ package org.springframework.ide.vscode.boot.java.reconcilers.test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import java.io.File; import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; -import org.junit.jupiter.api.AfterEach; +import org.eclipse.lsp4j.Diagnostic; +import org.eclipse.lsp4j.TextDocumentIdentifier; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.ide.vscode.boot.app.SpringSymbolIndex; +import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest; +import org.springframework.ide.vscode.boot.bootiful.IndexerTestConf; import org.springframework.ide.vscode.boot.java.Boot2JavaProblemType; -import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler; -import org.springframework.ide.vscode.boot.java.reconcilers.NoAutowiredOnConstructorReconciler; -import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry; -import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem; +import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.util.text.LanguageId; +import org.springframework.ide.vscode.languageserver.testharness.CodeAction; +import org.springframework.ide.vscode.languageserver.testharness.Editor; +import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness; +import org.springframework.ide.vscode.project.harness.ProjectsHarness; +import org.springframework.test.context.junit.jupiter.SpringExtension; -public class NoAutowiredOnConstructorReconcilerTest extends BaseReconcilerTest { +@ExtendWith(SpringExtension.class) +@BootLanguageServerTest +@Import(IndexerTestConf.class) +public class NoAutowiredOnConstructorReconcilerTest { - @Override - protected String getFolder() { - return "noautowiredonconstructor"; - } + @Autowired private BootLanguageServerHarness harness; + @Autowired private JavaProjectFinder projectFinder; + @Autowired private SpringSymbolIndex indexer; - @Override - protected String getProjectName() { - return "test-spring-validations"; - } - - @Override - protected JdtAstReconciler getReconciler() { - return new NoAutowiredOnConstructorReconciler(new QuickfixRegistry()); - } + private File directory; @BeforeEach - void setup() throws Exception { - super.setup(); - } - - @AfterEach - void tearDown() throws Exception { - super.tearDown(); + public void setup() throws Exception { + harness.intialize(null); + harness.changeConfiguration("{\"boot-java\": {\"validation\": {\"java\": { \"reconcilers\": true}}}}"); + directory = new File(ProjectsHarness.class.getResource("/test-projects/test-spring-validations/").toURI()); + String projectDir = directory.toURI().toString(); + projectFinder.find(new TextDocumentIdentifier(projectDir)).get(); + CompletableFuture initProject = indexer.waitOperation(); + initProject.get(5, TimeUnit.SECONDS); } - + @Test void singleConstructors() throws Exception { - String source = """ - package example.demo; - + String docUri = directory.toPath() + .resolve("src/main/java/org/test/WithAutowiredConstructor.java").toUri().toString(); + + Editor editor = harness.newEditor(LanguageId.JAVA, """ + package org.test; + import org.springframework.beans.factory.annotation.Autowired; - - class A { - + + public class WithAutowiredConstructor { + + private final String value; + @Autowired - A() {}; - + public WithAutowiredConstructor(String value) { + this.value = value; + } + } - """; - List problems = reconcile("A.java", source, false); - - assertEquals(1, problems.size()); - - ReconcileProblem problem = problems.get(0); - - assertEquals(Boot2JavaProblemType.JAVA_AUTOWIRED_CONSTRUCTOR, problem.getType()); - - String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); - assertEquals("@Autowired", markedStr); - - assertEquals(1, problem.getQuickfixes().size()); - + """, docUri); + + Diagnostic problem = editor.assertProblem("@Autowired"); + assertNotNull(problem); + assertEquals(Boot2JavaProblemType.JAVA_AUTOWIRED_CONSTRUCTOR.getCode(), problem.getCode().getLeft()); + + List codeActions = editor.getCodeActions(problem); + assertEquals(1, codeActions.size()); + + harness.executeCommand(codeActions.get(0).getCommand()); + + assertEquals(""" + package org.test; + + public class WithAutowiredConstructor { + + private final String value; + + public WithAutowiredConstructor(String value) { + this.value = value; + } + + } + """, editor.getRawText()); + + editor.assertProblems(); } @Test void multipleConstructors() throws Exception { - String source = """ - package example.demo; - + String docUri = directory.toPath() + .resolve("src/main/java/org/test/MultipleConstructors.java").toUri().toString(); + + Editor editor = harness.newEditor(LanguageId.JAVA, """ + package org.test; + import org.springframework.beans.factory.annotation.Autowired; - - class A { - + + class MultipleConstructors { + @Autowired - A() {}; - - A(int k) {} - + MultipleConstructors() {} + + MultipleConstructors(int k) {} + } - """; - List problems = reconcile("A.java", source, false); - - assertEquals(0, problems.size()); + """, docUri); + + editor.assertProblems(); } @Test void multipleConstructorsViaLombok() throws Exception { - String source = """ - package example.demo; - + String docUri = directory.toPath() + .resolve("src/main/java/org/test/LombokConstructors.java").toUri().toString(); + + Editor editor = harness.newEditor(LanguageId.JAVA, """ + package org.test; + import org.springframework.beans.factory.annotation.Autowired; import lombok.RequiredArgsConstructor; - + @RequiredArgsConstructor - class A { - + class LombokConstructors { + @Autowired - A() {}; - + LombokConstructors() {} + } - """; - List problems = reconcile("A.java", source, false); - - assertEquals(0, problems.size()); + """, docUri); + + editor.assertProblems(); } } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/NoRepoAnnotationReconcilerTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/NoRepoAnnotationReconcilerTest.java index a25f1a0f86..a26802df93 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/NoRepoAnnotationReconcilerTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/NoRepoAnnotationReconcilerTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 VMware, Inc. + * Copyright (c) 2023, 2026 VMware, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -11,124 +11,158 @@ package org.springframework.ide.vscode.boot.java.reconcilers.test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import java.io.File; import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; -import org.junit.jupiter.api.AfterEach; +import org.eclipse.lsp4j.Diagnostic; +import org.eclipse.lsp4j.TextDocumentIdentifier; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.ide.vscode.boot.app.SpringSymbolIndex; +import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest; +import org.springframework.ide.vscode.boot.bootiful.IndexerTestConf; import org.springframework.ide.vscode.boot.java.Boot2JavaProblemType; -import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler; -import org.springframework.ide.vscode.boot.java.reconcilers.NoRepoAnnotationReconciler; -import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry; -import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem; +import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.util.text.LanguageId; +import org.springframework.ide.vscode.languageserver.testharness.CodeAction; +import org.springframework.ide.vscode.languageserver.testharness.Editor; +import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness; +import org.springframework.ide.vscode.project.harness.ProjectsHarness; +import org.springframework.test.context.junit.jupiter.SpringExtension; -public class NoRepoAnnotationReconcilerTest extends BaseReconcilerTest { +@ExtendWith(SpringExtension.class) +@BootLanguageServerTest +@Import(IndexerTestConf.class) +public class NoRepoAnnotationReconcilerTest { - @Override - protected String getFolder() { - return "norepoannotation"; - } - - @Override - protected String getProjectName() { - return "test-spring-validations"; - } + @Autowired private BootLanguageServerHarness harness; + @Autowired private JavaProjectFinder projectFinder; + @Autowired private SpringSymbolIndex indexer; - @Override - protected JdtAstReconciler getReconciler() { - return new NoRepoAnnotationReconciler(new QuickfixRegistry()); - } + private File directory; @BeforeEach - void setup() throws Exception { - super.setup(); - } - - @AfterEach - void tearDown() throws Exception { - super.tearDown(); + public void setup() throws Exception { + harness.intialize(null); + harness.changeConfiguration("{\"boot-java\": {\"validation\": {\"java\": { \"reconcilers\": true}}}}"); + directory = new File(ProjectsHarness.class.getResource("/test-projects/test-spring-validations/").toURI()); + String projectDir = directory.toURI().toString(); + projectFinder.find(new TextDocumentIdentifier(projectDir)).get(); + CompletableFuture initProject = indexer.waitOperation(); + initProject.get(5, TimeUnit.SECONDS); } - + @Test void sanityTest() throws Exception { - String source = """ - package example.demo; - + String docUri = directory.toPath() + .resolve("src/main/java/org/test/RepoWithUnnecessaryAnnotation.java").toUri().toString(); + + Editor editor = harness.newEditor(LanguageId.JAVA, """ + package org.test; + import org.springframework.data.repository.Repository; - + @org.springframework.stereotype.Repository interface A extends Repository { - } - """; - List problems = reconcile("A.java", source, false); - - assertEquals(1, problems.size()); - - ReconcileProblem problem = problems.get(0); - - assertEquals(Boot2JavaProblemType.JAVA_REPOSITORY, problem.getType()); - - String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); - assertEquals("@org.springframework.stereotype.Repository", markedStr); - - assertEquals(2, problem.getQuickfixes().size()); - + """, docUri); + + Diagnostic problem = editor.assertProblem("@org.springframework.stereotype.Repository"); + assertNotNull(problem); + assertEquals(Boot2JavaProblemType.JAVA_REPOSITORY.getCode(), problem.getCode().getLeft()); + + List codeActions = editor.getCodeActions(problem); + assertEquals(1, codeActions.size()); + + harness.executeCommand(codeActions.get(0).getCommand()); + + assertEquals(""" + package org.test; + + import org.springframework.data.repository.Repository; + + interface A extends Repository { + } + """, editor.getRawText()); + + editor.assertProblems(); } - + @Test void inverseSanityTest() throws Exception { - String source = """ - package example.demo; - + String docUri = directory.toPath() + .resolve("src/main/java/org/test/InverseSanity.java").toUri().toString(); + + Editor editor = harness.newEditor(LanguageId.JAVA, """ + package org.test; + import org.springframework.stereotype.Repository; - + @Repository interface A extends org.springframework.data.repository.Repository { - } - """; - List problems = reconcile("A.java", source, false); - - assertEquals(1, problems.size()); - - ReconcileProblem problem = problems.get(0); - - assertEquals(Boot2JavaProblemType.JAVA_REPOSITORY, problem.getType()); - - String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); - assertEquals("@Repository", markedStr); - - assertEquals(2, problem.getQuickfixes().size()); - + """, docUri); + + Diagnostic problem = editor.assertProblem("@Repository"); + assertNotNull(problem); + assertEquals(Boot2JavaProblemType.JAVA_REPOSITORY.getCode(), problem.getCode().getLeft()); + + List codeActions = editor.getCodeActions(problem); + assertEquals(1, codeActions.size()); + + harness.executeCommand(codeActions.get(0).getCommand()); + + assertEquals(""" + package org.test; + + interface A extends org.springframework.data.repository.Repository { + } + """, editor.getRawText()); + + editor.assertProblems(); } - + @Test void emptyRepoAnnotation() throws Exception { - String source = """ - package example.demo; - + String docUri = directory.toPath() + .resolve("src/main/java/org/test/EmptyRepoAnnotation.java").toUri().toString(); + + Editor editor = harness.newEditor(LanguageId.JAVA, """ + package org.test; + import org.springframework.data.repository.Repository; - - @org.springframework.stereotype.Repository() + + @org.springframework.stereotype.Repository interface A extends Repository { - } - """; - List problems = reconcile("A.java", source, false); - - assertEquals(1, problems.size()); - - ReconcileProblem problem = problems.get(0); - - assertEquals(Boot2JavaProblemType.JAVA_REPOSITORY, problem.getType()); - - String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); - assertEquals("@org.springframework.stereotype.Repository()", markedStr); - - assertEquals(2, problem.getQuickfixes().size()); - + """, docUri); + + Diagnostic problem = editor.assertProblem("@org.springframework.stereotype.Repository"); + assertNotNull(problem); + assertEquals(Boot2JavaProblemType.JAVA_REPOSITORY.getCode(), problem.getCode().getLeft()); + + List codeActions = editor.getCodeActions(problem); + assertEquals(1, codeActions.size()); + + harness.executeCommand(codeActions.get(0).getCommand()); + + assertEquals(""" + package org.test; + + import org.springframework.data.repository.Repository; + + interface A extends Repository { + } + """, editor.getRawText()); + + editor.assertProblems(); } } From d4ba2d11f766f3fcaa1959b5953583fcac19c170 Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Thu, 30 Apr 2026 10:33:31 -0700 Subject: [PATCH 2/2] Polish and tests Signed-off-by: BoykoAlex --- .../java/spring/NoAutowiredOnConstructor.java | 89 --- .../NoRepoAnnotationOnRepoInterface.java | 70 --- .../spring/NoAutowiredOnConstructorTest.java | 588 ------------------ .../NoRepoAnnotationOnRepoInterfaceTest.java | 255 -------- .../jdt/refactoring/JdtRefactorUtilsTest.java | 175 +++++- 5 files changed, 174 insertions(+), 1003 deletions(-) delete mode 100644 headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/java/spring/NoAutowiredOnConstructor.java delete mode 100644 headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/java/spring/NoRepoAnnotationOnRepoInterface.java delete mode 100644 headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/java/spring/NoAutowiredOnConstructorTest.java delete mode 100644 headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/java/spring/NoRepoAnnotationOnRepoInterfaceTest.java diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/java/spring/NoAutowiredOnConstructor.java b/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/java/spring/NoAutowiredOnConstructor.java deleted file mode 100644 index 24a8bf6f9e..0000000000 --- a/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/java/spring/NoAutowiredOnConstructor.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2021 the original author or authors. - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * https://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.openrewrite.java.spring; - -import org.openrewrite.ExecutionContext; -import org.openrewrite.Preconditions; -import org.openrewrite.Recipe; -import org.openrewrite.TreeVisitor; -import org.openrewrite.internal.ListUtils; -import org.openrewrite.java.AnnotationMatcher; -import org.openrewrite.java.JavaIsoVisitor; -import org.openrewrite.java.RemoveAnnotationVisitor; -import org.openrewrite.java.search.FindAnnotations; -import org.openrewrite.java.search.UsesType; -import org.openrewrite.java.tree.J; -import org.openrewrite.java.tree.Statement; - -public class NoAutowiredOnConstructor extends Recipe { - private static final AnnotationMatcher AUTOWIRED_ANNOTATION_MATCHER = - new AnnotationMatcher("@org.springframework.beans.factory.annotation.Autowired(true)"); - - @Override - public String getDisplayName() { - return "Remove the `@Autowired` annotation on inferred constructor"; - } - - @Override - public String getDescription() { - return "Spring can infer an autowired constructor when there is a single constructor on the bean. " + - "This recipe removes unneeded `@Autowired` annotations on constructors."; - } - - @Override - public TreeVisitor getVisitor() { - return Preconditions.check(new UsesType<>("org.springframework.beans.factory.annotation.Autowired", false), new JavaIsoVisitor() { - @Override - public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) { - J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx); - - int constructorCount = 0; - for (Statement s : cd.getBody().getStatements()) { - if (isConstructor(s)) { - constructorCount++; - if (constructorCount > 1) { - return cd; - } - } - } - - // Lombok can also provide a constructor, so keep `@Autowired` on constructors if found - if (!FindAnnotations.find(cd, "@lombok.*Constructor").isEmpty()) { - return cd; - } - - // `@ConfigurationProperties` classes usually use field injection, so keep `@Autowired` on constructors - if (!FindAnnotations.find(cd, "@org.springframework.boot.context.properties.ConfigurationProperties").isEmpty()) { - return cd; - } - - return cd.withBody(cd.getBody().withStatements( - ListUtils.map(cd.getBody().getStatements(), s -> { - if (!isConstructor(s)) { - return s; - } - maybeRemoveImport("org.springframework.beans.factory.annotation.Autowired"); - return (Statement) new RemoveAnnotationVisitor(AUTOWIRED_ANNOTATION_MATCHER).visit(s, ctx, getCursor()); - }) - )); - } - }); - } - - private static boolean isConstructor(Statement s) { - return s instanceof J.MethodDeclaration && ((J.MethodDeclaration) s).isConstructor(); - } -} diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/java/spring/NoRepoAnnotationOnRepoInterface.java b/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/java/spring/NoRepoAnnotationOnRepoInterface.java deleted file mode 100644 index 7a3534a86a..0000000000 --- a/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/java/spring/NoRepoAnnotationOnRepoInterface.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2022 the original author or authors. - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * https://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.openrewrite.java.spring; - -import org.openrewrite.ExecutionContext; -import org.openrewrite.Preconditions; -import org.openrewrite.Recipe; -import org.openrewrite.TreeVisitor; -import org.openrewrite.java.AnnotationMatcher; -import org.openrewrite.java.JavaIsoVisitor; -import org.openrewrite.java.RemoveAnnotationVisitor; -import org.openrewrite.java.search.UsesType; -import org.openrewrite.java.tree.J; -import org.openrewrite.java.tree.JavaType; -import org.openrewrite.java.tree.TypeUtils; - -public class NoRepoAnnotationOnRepoInterface extends Recipe { - - private static final String INTERFACE_REPOSITORY = "org.springframework.data.repository.Repository"; - private static final String ANNOTATION_REPOSITORY = "org.springframework.stereotype.Repository"; - - @Override - public String getDisplayName() { - return "Remove unnecessary `@Repository` annotation from Spring Data `Repository` sub-interface"; - } - - @Override - public String getDescription() { - return "Removes superfluous `@Repository` annotation from Spring Data `Repository` sub-interfaces."; - } - - @Override - public TreeVisitor getVisitor() { - return Preconditions.check(new UsesType<>(ANNOTATION_REPOSITORY, false), new JavaIsoVisitor() { - @Override - public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) { - J.ClassDeclaration c = super.visitClassDeclaration(classDecl, ctx); - if (c.getKind() == J.ClassDeclaration.Kind.Type.Interface) { - boolean hasRepoAnnotation = c.getLeadingAnnotations().stream().anyMatch(annotation -> { - if (annotation.getArguments() == null || annotation.getArguments().isEmpty() || - annotation.getArguments().get(0) instanceof J.Empty) { - JavaType.FullyQualified type = TypeUtils.asFullyQualified(annotation.getType()); - return type != null && ANNOTATION_REPOSITORY.equals(type.getFullyQualifiedName()); - } - return false; - }); - if (hasRepoAnnotation && TypeUtils.isAssignableTo(INTERFACE_REPOSITORY, c.getType())) { - maybeRemoveImport(ANNOTATION_REPOSITORY); - return (J.ClassDeclaration) new RemoveAnnotationVisitor(new AnnotationMatcher("@" + ANNOTATION_REPOSITORY)) - .visit(c, ctx, getCursor().getParentOrThrow()); - } - } - return c; - } - }); - } -} diff --git a/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/java/spring/NoAutowiredOnConstructorTest.java b/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/java/spring/NoAutowiredOnConstructorTest.java deleted file mode 100644 index aa29076768..0000000000 --- a/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/java/spring/NoAutowiredOnConstructorTest.java +++ /dev/null @@ -1,588 +0,0 @@ -/* - * Copyright 2021 the original author or authors. - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * https://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.openrewrite.java.spring; - -import org.junit.jupiter.api.Test; -import org.openrewrite.Issue; -import org.openrewrite.java.JavaParser; -import org.openrewrite.test.RecipeSpec; -import org.openrewrite.test.RewriteTest; - -import static org.openrewrite.java.Assertions.java; - -class NoAutowiredOnConstructorTest implements RewriteTest { - - @Override - public void defaults(RecipeSpec spec) { - spec.recipe(new NoAutowiredOnConstructor()) - .parser(JavaParser.fromJavaVersion().classpath("spring-beans", "spring-boot", "spring-context", "spring-core")); - } - - @Issue("https://github.com/openrewrite/rewrite-spring/issues/78") - @Test - void removeLeadingAutowiredAnnotation() { - //language=java - rewriteRun( - java("@org.springframework.stereotype.Component public class TestSourceA {}"), - java("@org.springframework.stereotype.Component public class TestSourceB {}"), - java("@org.springframework.stereotype.Component public class TestSourceC {}"), - java( - """ - import org.springframework.beans.factory.annotation.Autowired; - - @Autowired - public class TestConfiguration { - private final TestSourceA testSourceA; - private TestSourceB testSourceB; - - @Autowired - private TestSourceC testSourceC; - - @Autowired - public TestConfiguration(TestSourceA testSourceA) { - this.testSourceA = testSourceA; - } - - @Autowired - public void setTestSourceB(TestSourceB testSourceB) { - this.testSourceB = testSourceB; - } - } - """, - """ - import org.springframework.beans.factory.annotation.Autowired; - - @Autowired - public class TestConfiguration { - private final TestSourceA testSourceA; - private TestSourceB testSourceB; - - @Autowired - private TestSourceC testSourceC; - - public TestConfiguration(TestSourceA testSourceA) { - this.testSourceA = testSourceA; - } - - @Autowired - public void setTestSourceB(TestSourceB testSourceB) { - this.testSourceB = testSourceB; - } - } - """ - ) - ); - } - - @Issue("https://github.com/openrewrite/rewrite-spring/issues/78") - @Test - void removeLeadingAutowiredAnnotationNoModifiers() { - //language=java - rewriteRun( - java("@org.springframework.stereotype.Component public class TestSourceA {}"), - java("@org.springframework.stereotype.Component public class TestSourceB {}"), - java("@org.springframework.stereotype.Component public class TestSourceC {}"), - java( - """ - import org.springframework.beans.factory.annotation.Autowired; - - public class TestConfiguration { - private final TestSourceA testSourceA; - private TestSourceB testSourceB; - - @Autowired - private TestSourceC testSourceC; - - @Autowired - TestConfiguration(TestSourceA testSourceA) { - this.testSourceA = testSourceA; - } - - @Autowired - public void setTestSourceB(TestSourceB testSourceB) { - this.testSourceB = testSourceB; - } - } - """, - """ - import org.springframework.beans.factory.annotation.Autowired; - - public class TestConfiguration { - private final TestSourceA testSourceA; - private TestSourceB testSourceB; - - @Autowired - private TestSourceC testSourceC; - - TestConfiguration(TestSourceA testSourceA) { - this.testSourceA = testSourceA; - } - - @Autowired - public void setTestSourceB(TestSourceB testSourceB) { - this.testSourceB = testSourceB; - } - } - """ - ) - ); - } - - @Issue("https://github.com/openrewrite/rewrite-spring/issues/78") - @Test - void removeAutowiredWithMultipleAnnotation() { - //language=java - rewriteRun( - java("@org.springframework.stereotype.Component public class TestSourceA {}"), - java( - """ - import org.springframework.beans.factory.annotation.Autowired; - import org.springframework.beans.factory.annotation.Qualifier; - - public class AnnotationPos1 { - private final TestSourceA testSourceA; - - @Autowired - @Deprecated - @Qualifier - public AnnotationPos1(TestSourceA testSourceA) { - this.testSourceA = testSourceA; - } - } - """, - """ - import org.springframework.beans.factory.annotation.Qualifier; - - public class AnnotationPos1 { - private final TestSourceA testSourceA; - - @Deprecated - @Qualifier - public AnnotationPos1(TestSourceA testSourceA) { - this.testSourceA = testSourceA; - } - } - """ - ), - java( - """ - import org.springframework.beans.factory.annotation.Autowired; - import org.springframework.beans.factory.annotation.Qualifier; - - public class AnnotationPos2 { - private final TestSourceA testSourceA; - - @Deprecated - @Autowired - @Qualifier - public AnnotationPos2(TestSourceA testSourceA) { - this.testSourceA = testSourceA; - } - } - """, - """ - import org.springframework.beans.factory.annotation.Qualifier; - - public class AnnotationPos2 { - private final TestSourceA testSourceA; - - @Deprecated - @Qualifier - public AnnotationPos2(TestSourceA testSourceA) { - this.testSourceA = testSourceA; - } - } - """ - ), - java( - """ - import org.springframework.beans.factory.annotation.Autowired; - import org.springframework.beans.factory.annotation.Qualifier; - - public class AnnotationPos3 { - private final TestSourceA testSourceA; - - @Deprecated - @Qualifier - @Autowired - public AnnotationPos3(TestSourceA testSourceA) { - this.testSourceA = testSourceA; - } - } - """, - """ - import org.springframework.beans.factory.annotation.Qualifier; - - public class AnnotationPos3 { - private final TestSourceA testSourceA; - - @Deprecated - @Qualifier - public AnnotationPos3(TestSourceA testSourceA) { - this.testSourceA = testSourceA; - } - } - """ - ) - ); - } - - @Issue("https://github.com/openrewrite/rewrite-spring/issues/78") - @Test - void removeAutowiredWithMultipleInLineAnnotation() { - //language=java - rewriteRun( - java("@org.springframework.stereotype.Component public class TestSourceA {}"), - java( - """ - import org.springframework.beans.factory.annotation.Autowired; - import org.springframework.beans.factory.annotation.Qualifier; - - public class AnnotationPos1 { - private final TestSourceA testSourceA; - - @Autowired @Deprecated @Qualifier - public AnnotationPos1(TestSourceA testSourceA) { - this.testSourceA = testSourceA; - } - } - """, - """ - import org.springframework.beans.factory.annotation.Qualifier; - - public class AnnotationPos1 { - private final TestSourceA testSourceA; - - @Deprecated @Qualifier - public AnnotationPos1(TestSourceA testSourceA) { - this.testSourceA = testSourceA; - } - } - """ - ), - java( - """ - import org.springframework.beans.factory.annotation.Autowired; - import org.springframework.beans.factory.annotation.Qualifier; - - public class AnnotationPos2 { - private final TestSourceA testSourceA; - - @Deprecated @Autowired @Qualifier - public AnnotationPos2(TestSourceA testSourceA) { - this.testSourceA = testSourceA; - } - } - """, - """ - import org.springframework.beans.factory.annotation.Qualifier; - - public class AnnotationPos2 { - private final TestSourceA testSourceA; - - @Deprecated @Qualifier - public AnnotationPos2(TestSourceA testSourceA) { - this.testSourceA = testSourceA; - } - } - """ - ), - java( - """ - import org.springframework.beans.factory.annotation.Autowired; - import org.springframework.beans.factory.annotation.Qualifier; - - public class AnnotationPos3 { - private final TestSourceA testSourceA; - - @Deprecated @Qualifier @Autowired - public AnnotationPos3(TestSourceA testSourceA) { - this.testSourceA = testSourceA; - } - } - """, - """ - import org.springframework.beans.factory.annotation.Qualifier; - - public class AnnotationPos3 { - private final TestSourceA testSourceA; - - @Deprecated @Qualifier - public AnnotationPos3(TestSourceA testSourceA) { - this.testSourceA = testSourceA; - } - } - """ - ) - ); - } - - @Issue("https://github.com/openrewrite/rewrite-spring/issues/78") - @Test - void oneNamePrefixAnnotation() { - //language=java - rewriteRun( - java( - """ - import javax.sql.DataSource; - import org.springframework.beans.factory.annotation.Autowired; - - public class DatabaseConfiguration { - private final DataSource dataSource; - - public @Autowired DatabaseConfiguration(DataSource dataSource) { - } - } - """, - """ - import javax.sql.DataSource; - - public class DatabaseConfiguration { - private final DataSource dataSource; - - public DatabaseConfiguration(DataSource dataSource) { - } - } - """ - ) - ); - } - - @Issue("https://github.com/openrewrite/rewrite-spring/issues/78") - @Test - void multipleNamePrefixAnnotationsPos1() { - //language=java - rewriteRun( - java( - """ - import javax.sql.DataSource; - import org.springframework.beans.factory.annotation.Autowired; - - public class DatabaseConfiguration { - private final DataSource dataSource; - - public @Autowired @Deprecated DatabaseConfiguration(DataSource dataSource) { - } - } - """, - """ - import javax.sql.DataSource; - - public class DatabaseConfiguration { - private final DataSource dataSource; - - public @Deprecated DatabaseConfiguration(DataSource dataSource) { - } - } - """ - ) - ); - } - - @Issue("https://github.com/openrewrite/rewrite-spring/issues/78") - @Test - void multipleNamePrefixAnnotationsPos2() { - //language=java - rewriteRun( - java( - """ - import javax.sql.DataSource; - import org.springframework.beans.factory.annotation.Autowired; - - public class DatabaseConfiguration { - private final DataSource dataSource; - - public @SuppressWarnings("") @Autowired @Deprecated DatabaseConfiguration(DataSource dataSource) { - } - } - """, - """ - import javax.sql.DataSource; - - public class DatabaseConfiguration { - private final DataSource dataSource; - - public @SuppressWarnings("") @Deprecated DatabaseConfiguration(DataSource dataSource) { - } - } - """ - ) - ); - } - - @Issue("https://github.com/openrewrite/rewrite-spring/issues/78") - @Test - void multipleNamePrefixAnnotationsPos3() { - //language=java - rewriteRun( - java( - """ - import javax.sql.DataSource; - import org.springframework.beans.factory.annotation.Autowired; - - public class DatabaseConfiguration { - private final DataSource dataSource; - - public @SuppressWarnings("") @Deprecated @Autowired DatabaseConfiguration(DataSource dataSource) { - } - } - """, - """ - import javax.sql.DataSource; - - public class DatabaseConfiguration { - private final DataSource dataSource; - - public @SuppressWarnings("") @Deprecated DatabaseConfiguration(DataSource dataSource) { - } - } - """ - ) - ); - } - - @Issue("https://github.com/openrewrite/rewrite-spring/issues/78") - @Test - void keepAutowiredAnnotationsWhenMultipleConstructorsExist() { - //language=java - rewriteRun( - java( - """ - import org.springframework.beans.factory.annotation.Autowired; - import org.springframework.core.io.Resource; - import java.io.PrintStream; - - public class MyAppResourceService { - private final Resource someResource; - private final PrintStream printStream; - - public MyAppResourceService(Resource someResource) { - this.someResource = someResource; - this.printStream = System.out; - } - - @Autowired - public MyAppResourceService(Resource someResource, PrintStream printStream) { - this.someResource = someResource; - this.printStream = printStream; - } - } - """ - ) - ); - } - - @Test - void optionalAutowiredAnnotations() { - //language=java - rewriteRun( - java( - """ - import org.springframework.beans.factory.annotation.Autowired; - import javax.sql.DataSource; - - public class DatabaseConfiguration { - private final DataSource dataSource; - - public DatabaseConfiguration(@Autowired(required = false) DataSource dataSource) { - } - } - """ - ) - ); - } - - @Test - void noAutowiredAnnotations() { - //language=java - rewriteRun( - java( - """ - import org.springframework.context.annotation.Primary; - import javax.sql.DataSource; - - public class DatabaseConfiguration { - private final DataSource dataSource; - - @Primary - public DatabaseConfiguration(DataSource dataSource) { - } - } - """ - ) - ); - } - - @Test - void ignoreConfigurationProperties() { - //language=java - rewriteRun( - java( - """ - import org.springframework.beans.factory.annotation.Autowired; - import org.springframework.boot.context.properties.ConfigurationProperties; - import org.springframework.core.env.Environment; - @ConfigurationProperties - public class ArchivingWorkflowListenerProperties { - private final Environment environment; - @Autowired - public ArchivingWorkflowListenerProperties(Environment environment) { - this.environment = environment; - } - } - """ - ) - ); - } - - @Test - @Issue("https://github.com/openrewrite/rewrite-spring/issues/479") - void ignoreLombokConstructors() { - //language=java - rewriteRun( - java( - """ - package lombok; - - import java.lang.annotation.ElementType; - import java.lang.annotation.Retention; - import java.lang.annotation.RetentionPolicy; - import java.lang.annotation.Target; - - @Target({ElementType.TYPE}) - @Retention(RetentionPolicy.SOURCE) - public @interface NoArgsConstructor { - } - """ - ), - java( - """ - import lombok.NoArgsConstructor; - import org.springframework.beans.factory.annotation.Autowired; - import org.springframework.core.env.Environment; - @NoArgsConstructor - public class ArchivingWorkflowListenerProperties { - private final Environment environment; - @Autowired - public ArchivingWorkflowListenerProperties(Environment environment) { - this.environment = environment; - } - } - """ - ) - ); - } -} diff --git a/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/java/spring/NoRepoAnnotationOnRepoInterfaceTest.java b/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/java/spring/NoRepoAnnotationOnRepoInterfaceTest.java deleted file mode 100644 index 0d4058aa64..0000000000 --- a/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/java/spring/NoRepoAnnotationOnRepoInterfaceTest.java +++ /dev/null @@ -1,255 +0,0 @@ -/* - * Copyright 2022 the original author or authors. - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * https://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.openrewrite.java.spring; - -import org.junit.jupiter.api.Test; -import org.openrewrite.DocumentExample; -import org.openrewrite.java.JavaParser; -import org.openrewrite.test.RecipeSpec; -import org.openrewrite.test.RewriteTest; - -import static org.openrewrite.java.Assertions.java; - -class NoRepoAnnotationOnRepoInterfaceTest implements RewriteTest { - - @Override - public void defaults(RecipeSpec spec) { - spec.recipe(new NoRepoAnnotationOnRepoInterface()) - .parser(JavaParser.fromJavaVersion().classpath("spring-context", "spring-beans", "spring-data")); - } - - @DocumentExample - @Test - void simpleCase() { - //language=java - rewriteRun( - java( - """ - import org.springframework.stereotype.Repository; - - @Repository - public interface MyRepo extends org.springframework.data.repository.Repository { - } - """, - """ - - public interface MyRepo extends org.springframework.data.repository.Repository { - } - """ - ) - ); - } - - @Test - void simpleCaseWithNoParameters() { - //language=java - rewriteRun( - java( - """ - import org.springframework.stereotype.Repository; - - @Repository( ) - public interface MyRepo extends org.springframework.data.repository.Repository { - } - """, - """ - - public interface MyRepo extends org.springframework.data.repository.Repository { - } - """ - ) - ); - } - - @Test - void crudRepoClass() { - //language=java - rewriteRun( - java( - """ - import java.util.Optional; - - import org.springframework.data.repository.CrudRepository; - import org.springframework.stereotype.Repository; - - @Repository - public class MyRepo implements CrudRepository { - - @Override - public S save(S entity) { - return null; - } - - @Override - public Iterable saveAll(Iterable entities) { - return null; - } - - @Override - public Optional findById(String id) { - return Optional.empty(); - } - - @Override - public boolean existsById(String id) { - return false; - } - - @Override - public Iterable findAll() { - return null; - } - - @Override - public Iterable findAllById(Iterable ids) { - return null; - } - - @Override - public long count() { - return 0; - } - - @Override - public void deleteById(String id) { - } - - @Override - public void delete(String entity) { - } - - @Override - public void deleteAllById(Iterable ids) { - } - - @Override - public void deleteAll(Iterable entities) { - } - - @Override - public void deleteAll() { - } - - } - """ - ) - ); - } - - @Test - void crudRepoInterface() { - //language=java - rewriteRun( - java( - """ - import org.springframework.data.repository.CrudRepository; - import org.springframework.stereotype.Repository; - - @Repository - public interface MyRepo extends CrudRepository { - - } - """, - """ - import org.springframework.data.repository.CrudRepository; - - public interface MyRepo extends CrudRepository { - - } - """ - ) - ); - } - - @Test - void crudRepoInterfaceWithMultipleAnnotations() { - //language=java - rewriteRun( - java( - """ - import org.springframework.data.repository.CrudRepository; - import org.springframework.stereotype.Repository; - - @Repository - @Deprecated - public interface MyRepo extends CrudRepository { - - } - """, - """ - import org.springframework.data.repository.CrudRepository; - - @Deprecated - public interface MyRepo extends CrudRepository { - - } - """ - ) - ); - } - - @Test - void repoAnnotationWithParameters() { - //language=java - rewriteRun( - java( - """ - import org.springframework.data.repository.CrudRepository; - import org.springframework.stereotype.Repository; - - @Repository("myRepoBean") - public interface MyRepo extends CrudRepository { - - } - """ - ) - ); - } - - @Test - void noRepoSubclass() { - //language=java - rewriteRun( - java( - """ - import java.util.List; - - import org.springframework.stereotype.Repository; - - @Repository - public interface MyRepo extends List { - } - """ - ) - ); - } - - @Test - void noRepoAnnotation() { - //language=java - rewriteRun( - java( - """ - import org.springframework.data.repository.CrudRepository; - - public interface MyRepo extends CrudRepository { - } - """ - ) - ); - } - -} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/jdt/refactoring/JdtRefactorUtilsTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/jdt/refactoring/JdtRefactorUtilsTest.java index 439eb43aed..53a3d7a3d2 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/jdt/refactoring/JdtRefactorUtilsTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/jdt/refactoring/JdtRefactorUtilsTest.java @@ -50,7 +50,9 @@ private static CompilationUnit parseSource(String source) { ASTParser parser = ASTParser.newParser(AST.JLS25); parser.setSource(source.toCharArray()); parser.setKind(ASTParser.K_COMPILATION_UNIT); - parser.setResolveBindings(false); + parser.setResolveBindings(true); + parser.setEnvironment(new String[0], new String[0], null, true); + parser.setUnitName("Test.java"); Map options = JavaCore.getOptions(); JavaCore.setComplianceOptions(JavaCore.VERSION_21, options); parser.setCompilerOptions(options); @@ -68,6 +70,16 @@ private static String applyAddImport(String source, String fqn) throws Exception return doc.get(); } + private static String applyRemoveImports(String source, String... fqns) throws Exception { + CompilationUnit cu = parseSource(source); + ASTRewrite rewrite = ASTRewrite.create(cu.getAST()); + JdtRefactorUtils.removeImports(cu, rewrite, fqns); + Document doc = new Document(source); + TextEdit edit = rewrite.rewriteAST(doc, defaultFormatterOptions()); + edit.apply(doc); + return doc.get(); + } + // ========== extractSimpleName ========== @Test @@ -398,4 +410,165 @@ void toLspTextDocumentEdit_crossLineEdit_correctLineAndCharacter() throws Except assertEquals(0, lspEdit.getRange().getStart().getCharacter()); } + // ========== removeImports ========== + + @Test + void removeImports_unusedImport_removed() throws Exception { + String source = """ + package com.example; + + import java.util.List; + import java.util.ArrayList; + + class Foo { + List list; + } + """; + + String result = applyRemoveImports(source, "java.util.ArrayList"); + + assertEquals(""" + package com.example; + + import java.util.List; + + class Foo { + List list; + } + """, result); + } + + @Test + void removeImports_usedImport_kept() throws Exception { + String source = """ + package com.example; + + import java.util.List; + import java.util.ArrayList; + + class Foo { + List list = new ArrayList<>(); + } + """; + + String result = applyRemoveImports(source, "java.util.ArrayList"); + + assertEquals(source, result); + } + + @Test + void removeImports_multipleUnusedImports_removed() throws Exception { + String source = """ + package com.example; + + import java.util.List; + import java.util.ArrayList; + import java.util.Map; + import java.util.HashMap; + + class Foo { + } + """; + + String result = applyRemoveImports(source, "java.util.List", "java.util.ArrayList", "java.util.Map"); + + assertEquals(""" + package com.example; + + import java.util.HashMap; + + class Foo { + } + """, result); + } + + @Test + void removeImports_usedInAnnotation_kept() throws Exception { + String source = """ + package com.example; + + import java.lang.annotation.Documented; + + @Documented + class Foo { + } + """; + + String result = applyRemoveImports(source, "java.lang.annotation.Documented"); + + assertEquals(source, result); + } + + @Test + void removeImports_usedAsStaticMethod_kept() throws Exception { + String source = """ + package com.example; + + import java.util.Collections; + + class Foo { + void test() { + Collections.emptyList(); + } + } + """; + + String result = applyRemoveImports(source, "java.util.Collections"); + + assertEquals(source, result); + } + + @Test + void removeImports_onDemandImport_ignored() throws Exception { + String source = """ + package com.example; + + import java.util.*; + + class Foo { + } + """; + + String result = applyRemoveImports(source, "java.util.List"); + + assertEquals(source, result); + } + + @Test + void removeImports_survivesRewrite_removedIfNodeRemoved() throws Exception { + String source = """ + package com.example; + + import java.util.List; + import java.util.ArrayList; + + class Foo { + List list; + } + """; + + CompilationUnit cu = parseSource(source); + ASTRewrite rewrite = ASTRewrite.create(cu.getAST()); + + // Remove the 'List list;' field declaration + org.eclipse.jdt.core.dom.TypeDeclaration typeDecl = (org.eclipse.jdt.core.dom.TypeDeclaration) cu.types().get(0); + org.eclipse.jdt.core.dom.FieldDeclaration fieldDecl = typeDecl.getFields()[0]; + rewrite.remove(fieldDecl, null); + + JdtRefactorUtils.removeImports(cu, rewrite, "java.util.List"); + + Document doc = new Document(source); + TextEdit edit = rewrite.rewriteAST(doc, defaultFormatterOptions()); + edit.apply(doc); + + assertEquals(""" + package com.example; + + import java.util.ArrayList; + + class Foo { + } + """, doc.get()); + } + }