From a71342dae98228c6986388633d77c0d628440488 Mon Sep 17 00:00:00 2001 From: adityaanikam Date: Tue, 1 Sep 2026 00:20:13 +0530 Subject: [PATCH] Guard BadImport's type-annotation move against unpositioned trees BadImport.moveTypeAnnotations() unconditionally does builder.delete(annotation) for every TYPE_USE-targeted annotation on the qualified type, then reinserts it before the qualified type. That delete requires a valid start/end source position for the annotation tree. A record component's type-use annotation can be reached through javac's synthesized members (the desugared field/accessor/canonical constructor), whose copy of the annotation tree has no recorded end position. Building a Replacement from that position throws SourcePositionException (IllegalArgumentException before 2.48.0), aborting the whole compilation -- BadImport is on by default, so no special configuration is needed to hit it. Skip the move for annotations without an explicit source position, using ASTHelpers.hasExplicitSource() (the same helper already used elsewhere in the codebase for this exact check). The identifier is still qualified by the prefixWith() call in the caller; only the annotation relocation is skipped, so the fix stays useful instead of being dropped entirely. Fixes #6074 --- .../errorprone/bugpatterns/BadImport.java | 10 ++++ .../errorprone/bugpatterns/BadImportTest.java | 47 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/BadImport.java b/core/src/main/java/com/google/errorprone/bugpatterns/BadImport.java index 2a12892cc36..2dd5a2520b7 100644 --- a/core/src/main/java/com/google/errorprone/bugpatterns/BadImport.java +++ b/core/src/main/java/com/google/errorprone/bugpatterns/BadImport.java @@ -20,6 +20,7 @@ import static com.google.errorprone.matchers.Matchers.annotations; import static com.google.errorprone.matchers.ProtobufMatchers.MESSAGE_LITE_TYPE; import static com.google.errorprone.util.ASTHelpers.getSymbol; +import static com.google.errorprone.util.ASTHelpers.hasExplicitSource; import static com.google.errorprone.util.ASTHelpers.isSubtype; import com.google.common.collect.ImmutableSet; @@ -275,6 +276,15 @@ private void moveTypeAnnotations( SuggestedFix.Builder builder) { for (AnnotationTree annotation : HAS_TYPE_USE_ANNOTATION.multiMatchResult(annotationHolder, state).matchingNodes()) { + if (!hasExplicitSource(annotation, state)) { + // A record component's type-use annotation can be reached through javac's + // synthesized members (field/accessor/canonical constructor), whose copy of the + // annotation tree has no recorded end position. Deleting and reinserting it would + // build an invalid Replacement and crash the whole compilation (#6074). Leave the + // annotation where it is -- the node is still qualified by the prefixWith() call + // above, it just won't be relocated ahead of the qualified type. + continue; + } builder.delete(annotation); builder.prefixWith(node, state.getSourceForNode(annotation) + " "); } diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/BadImportTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/BadImportTest.java index 8d82cb10e11..1d4a6d87bf6 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/BadImportTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/BadImportTest.java @@ -582,6 +582,53 @@ abstract class Test { .doTest(); } + + @Test + public void recordComponentWithTypeUseAnnotation_doesNotCrash() { + refactoringTestHelper + .addInputLines( + "input/TypeUseAnnotation.java", + """ + package test; + + import java.lang.annotation.ElementType; + import java.lang.annotation.Target; + + @Target(ElementType.TYPE_USE) + @interface TypeUseAnnotation {} + """) + .expectUnchanged() + .addInputLines( + "input/SomeClass.java", + """ + package test; + + class SomeClass { + enum Builder { + A + } + } + """) + .expectUnchanged() + .addInputLines( + "input/Test.java", + """ + package test; + + import test.SomeClass.Builder; + + record Test(@TypeUseAnnotation Builder builder) {} + """) + .addOutputLines( + "output/Test.java", + """ + package test; + + record Test(@TypeUseAnnotation SomeClass.Builder builder) {} + """) + .doTest(); + } + @Test public void negative_truth8AssertThatFalseFlag() { compilationTestHelper