-
Notifications
You must be signed in to change notification settings - Fork 365
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Don't remove referenced annotations from record parameters #4474
base: main
Are you sure you want to change the base?
Conversation
Currently, the test fails with:
|
Thanks for getting this reduced and started @mvitz ! Looks like an oversight in the parser after the addition of records, which would then cause the annotation to not be parsed as such, but as "whitespace". The fix is then likely in ReloadableJava17ParserVisitor. |
Thanks for the hint. |
Hmm; thanks for diving in! It looks like the annotation is still present at this point in the parsing: rewrite/rewrite-java-17/src/main/java/org/openrewrite/java/isolated/ReloadableJava17Parser.java Lines 193 to 194 in a98d83d
But it's gone when we exit that |
Sorry, my fault. I added a more accurate testcase where the annotation contains |
Note to myself, if I change Lines 411 to 415 into
The assertion failure is still there, but now marks another non-whitespace:
Within After thinking during the night and checking some examples today annotations on records opens a rabbit hole. All the following are valid annotations for a record component:
However, in the generated class (after compilation) they appear on different locations:
Therefore, the first initial example with |
Thanks for the detailed look! Problems sure have a way of looking deceptively simple don't they? 🙃 |
///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS org.openrewrite:rewrite-java:8.44.1
//DEPS org.openrewrite:rewrite-java-21:8.44.1
//DEPS org.openrewrite:rewrite-test:8.44.1
//DEPS org.junit.jupiter:junit-jupiter:5.11.4
//DEPS org.junit.platform:junit-platform-launcher:1.11.4
//DEPS org.springframework:spring-web:6.1.3
//DEPS org.projectlombok:lombok:1.18.36
import org.openrewrite.java.tree.J;
import org.openrewrite.ExecutionContext;
import org.openrewrite.InMemoryExecutionContext;
import org.openrewrite.java.JavaParser;
import org.openrewrite.Tree;
import org.openrewrite.SourceFile;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.java.RemoveUnusedImports;
import org.openrewrite.java.tree.JavaType;
import org.openrewrite.java.tree.TypeUtils;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.tree.J;
import org.openrewrite.ExecutionContext;
import org.openrewrite.InMemoryExecutionContext;
import java.util.*;
public class DemoRemoveUnusedImports {
public static void main(String[] args) {
String sourceCode = """
package com.example;
import org.springframework.web.bind.annotation.RequestParam; // This should not be removed
import java.util.List; // This should be removed
import lombok.Builder;
@Builder
public record TestParams(
@RequestParam(required = false)
Integer limit
) {}
""";
JavaParser jp = JavaParser.fromJavaVersion()
.classpath("spring-web", "lombok")
.build();
System.out.println("=== Testing RemoveUnusedImports ===");
testRecipe(new RemoveUnusedImports(), jp, sourceCode);
jp.reset();
System.out.println("\n=== Testing PatchedRemoveUnusedImports ===");
testRecipe(new PatchedRemoveUnusedImports(), jp, sourceCode);
}
private static void testRecipe(Recipe recipe, JavaParser jp, String sourceCode) {
try {
List<SourceFile> sourceFiles = jp.parse(sourceCode).toList();
if (sourceFiles.isEmpty()) {
System.out.println("No source files parsed.");
return;
}
J.CompilationUnit cu = (J.CompilationUnit) sourceFiles.get(0);
System.out.println("--- Original Source Code ---");
System.out.println(sourceCode);
System.out.println("---");
Tree result = recipe.getVisitor().visit(cu, new InMemoryExecutionContext());
System.out.println("--- Modified Source Code ---");
System.out.println(((J.CompilationUnit) result).print());
System.out.println("---");
} catch (Exception e) {
System.out.println("Error occurred:");
e.printStackTrace(System.out);
}
}
}
class PatchedRemoveUnusedImports extends Recipe {
@Override
public String getDisplayName() {
return "Remove unused imports (record-aware for older Rewrite)";
}
@Override
public String getDescription() {
return "Removes imports for types that are not referenced, scanning canonical constructor parameters for record annotations.";
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new JavaIsoVisitor<ExecutionContext>() {
private final Set<String> usedTypes = new HashSet<>();
@Override
public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) {
// Normal AST traversal first
super.visitCompilationUnit(cu, ctx);
// Also gather from getTypesInUse()
cu.getTypesInUse().getTypesInUse().forEach(t -> {
if (t instanceof JavaType.FullyQualified) {
usedTypes.add(((JavaType.FullyQualified) t).getFullyQualifiedName());
}
});
// Filter out unused imports
List<J.Import> retainedImports = new ArrayList<>();
for (J.Import anImport : cu.getImports()) {
JavaType importType = anImport.getQualid().getType();
if (importType instanceof JavaType.FullyQualified fq) {
if (usedTypes.contains(fq.getFullyQualifiedName())) {
retainedImports.add(anImport);
}
}
}
return cu.withImports(retainedImports);
}
@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
boolean isRecord = classDecl.getModifiers().stream()
.anyMatch(m ->
m.getType() == J.Modifier.Type.LanguageExtension
&& "record".equalsIgnoreCase(m.getKeyword())
);
if (isRecord) {
for (Object bodyStmt : classDecl.getBody().getStatements()) {
if (bodyStmt instanceof J.MethodDeclaration md) {
boolean constructorMatchesClassName =
md.isConstructor()
&& md.getSimpleName().equals(classDecl.getSimpleName());
if (constructorMatchesClassName) {
for (Object param : md.getParameters()) {
if (param instanceof J.VariableDeclarations varDec) {
for (J.Annotation ann : varDec.getLeadingAnnotations()) {
recordAnnotationUsage(ann);
}
}
}
}
}
}
}
for (J.Annotation ann : classDecl.getLeadingAnnotations()) {
recordAnnotationUsage(ann);
}
return super.visitClassDeclaration(classDecl, ctx);
}
@Override
public J.Annotation visitAnnotation(J.Annotation annotation, ExecutionContext ctx) {
recordAnnotationUsage(annotation);
return super.visitAnnotation(annotation, ctx);
}
private void recordAnnotationUsage(J.Annotation ann) {
JavaType.FullyQualified fq = TypeUtils.asFullyQualified(ann.getType());
if (fq != null) {
usedTypes.add(fq.getFullyQualifiedName());
}
}
};
}
}
} output === Testing RemoveUnusedImports ===
--- Original Source Code ---
package com.example;
import org.springframework.web.bind.annotation.RequestParam; // This should not be removed
import java.util.List; // This should be removed
import lombok.Builder;
@Builder
public record TestParams(
@RequestParam(required = false)
Integer limit
) {}
---
--- Modified Source Code ---
package com.example; // This should be removed
import lombok.Builder;
@Builder
public record TestParams(
@RequestParam(required = false)
Integer limit
) {}
---
=== Testing PatchedRemoveUnusedImports ===
--- Original Source Code ---
package com.example;
import org.springframework.web.bind.annotation.RequestParam; // This should not be removed
import java.util.List; // This should be removed
import lombok.Builder;
@Builder
public record TestParams(
@RequestParam(required = false)
Integer limit
) {}
---
--- Modified Source Code ---
package com.example; // This should be removed
import lombok.Builder;
@Builder
public record TestParams(
@RequestParam(required = false)
Integer limit
) {}
---
Long story short i didn't quite understand the best introspection approach in this context but if you want a reproducer this jbang does the job of before/after. |
What's changed?
What's your motivation?
RemoveUnusedImports
removes used imports for annotated method parameters #4473Anything in particular you'd like reviewers to focus on?
Anyone you would like to review specifically?
Have you considered any alternatives or workarounds?
Any additional context
Checklist