Skip to content

BadImport: SourcePositionException building the suggested fix for a record component with a TYPE_USE annotation #6074

Description

@markusbaumg

What version of Error Prone are you using?

2.50.0

Does this issue reproduce with the latest release?

Yes. 2.50.0 is the latest release at time of writing, and the crash is present in every release
back to 2.36.0 — see the version table below. It is not a regression.

What did you do?

BadImport crashes with SourcePositionException (aborting the whole compilation) when the
bad-named nested class it wants to qualify appears as the type of a record component that also
carries an annotation whose @Target includes ElementType.TYPE_USE
.

This reproduces with default Error Prone settingsBadImport is on by default, so no
configuration is needed to hit it.

Three files, no dependencies beyond Error Prone itself:

repro/model/Outer.java

package repro.model;

public class Outer {
  public enum Key {
    A
  }
}

repro/TypeUse.java

package repro;

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

@Target(ElementType.TYPE_USE)
public @interface TypeUse {}

repro/Repro.java

package repro;

import repro.model.Outer.Key;

public record Repro(@TypeUse Key key) {}

Compile (JDK 25; $EP_CP is the error_prone_core:2.50.0 runtime classpath):

javac \
  -J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
  -J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
  -J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED \
  -J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED \
  -J--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
  -J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED \
  -J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
  -J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \
  -J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED \
  -J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED \
  -XDcompilePolicy=simple \
  --should-stop=ifError=FLOW \
  -processorpath "$EP_CP" \
  '-Xplugin:ErrorProne' \
  -d out \
  repro/model/Outer.java repro/TypeUse.java repro/Repro.java

What did you expect to see?

The ordinary BadImport finding with a working suggested fix, exactly as produced when the
annotation is removed:

Repro.java:5: warning: [BadImport] Importing nested classes/static methods/static fields with
commonly-used names can make code harder to read [...] Here we recommend using qualified class:
Outer.
public record Repro(Key key) {}
                    ^
    (see https://errorprone.info/bugpattern/BadImport)
  Did you mean 'public record Repro(Outer.Key key) {}'?

What did you see instead?

Repro.java:5: error: An unhandled exception was thrown by the Error Prone static analysis plugin.
public record Repro(@TypeUse Key key) {}
                    ^
     error-prone version: 2.50.0
     BugPattern: BadImport
     Stack Trace:
     com.google.errorprone.SourcePositionException: invalid source position: [67, -1)
        at com.google.errorprone.fixes.Replacement.create(Replacement.java:39)
        at com.google.errorprone.fixes.SuggestedFix$ReplacementFix.getReplacement(SuggestedFix.java:477)
        at com.google.errorprone.fixes.SuggestedFix.getReplacements(SuggestedFix.java:95)
        at com.google.errorprone.fixes.AppliedFix.apply(AppliedFix.java:45)
        at com.google.errorprone.JavacErrorDescriptionListener.lambda$new$0(JavacErrorDescriptionListener.java:94)
        at java.base/java.util.HashMap.computeIfAbsent(HashMap.java:1229)
        at com.google.errorprone.JavacErrorDescriptionListener.lambda$new$1(JavacErrorDescriptionListener.java:90)
        at com.google.errorprone.JavacErrorDescriptionListener.lambda$onDescribed$3(JavacErrorDescriptionListener.java:109)
        at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:214)
        at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:197)
        at java.base/java.util.Collections$2.tryAdvance(Collections.java:5182)
        at java.base/java.util.Collections$2.forEachRemaining(Collections.java:5190)
        at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:570)
        at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:560)
        at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)
        at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:265)
        at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:723)
        at com.google.errorprone.JavacErrorDescriptionListener.onDescribed(JavacErrorDescriptionListener.java:111)
        at com.google.errorprone.ErrorProneAnalyzer.lambda$finished$3(ErrorProneAnalyzer.java:216)
        at com.google.errorprone.VisitorState.reportMatch(VisitorState.java:248)
        at com.google.errorprone.scanner.Scanner.reportMatch(Scanner.java:126)
        at com.google.errorprone.scanner.ErrorProneScanner.processMatchers(ErrorProneScanner.java:540)
        at com.google.errorprone.scanner.ErrorProneScanner.visitImport(ErrorProneScanner.java:804)
        at com.google.errorprone.scanner.ErrorProneScanner.visitImport(ErrorProneScanner.java:180)
        at jdk.compiler/com.sun.tools.javac.tree.JCTree$JCImportBase.accept(JCTree.java:678)
        at jdk.compiler/com.sun.source.util.TreePathScanner.scan(TreePathScanner.java:92)
        at com.google.errorprone.scanner.Scanner.scan(Scanner.java:74)
        at com.google.errorprone.scanner.Scanner.scan(Scanner.java:48)
        at jdk.compiler/com.sun.source.util.TreeScanner.scan(TreeScanner.java:110)
        at jdk.compiler/com.sun.source.util.TreeScanner.scanAndReduce(TreeScanner.java:118)
        at jdk.compiler/com.sun.source.util.TreeScanner.visitCompilationUnit(TreeScanner.java:150)
        at com.google.errorprone.scanner.ErrorProneScanner.visitCompilationUnit(ErrorProneScanner.java:663)
        at com.google.errorprone.scanner.ErrorProneScanner.visitCompilationUnit(ErrorProneScanner.java:180)
        at jdk.compiler/com.sun.tools.javac.tree.JCTree$JCCompilationUnit.accept(JCTree.java:627)
        at jdk.compiler/com.sun.source.util.TreePathScanner.scan(TreePathScanner.java:66)
        at com.google.errorprone.scanner.Scanner.scan(Scanner.java:58)
        at com.google.errorprone.scanner.ErrorProneScannerTransformer.apply(ErrorProneScannerTransformer.java:43)
        at com.google.errorprone.ErrorProneAnalyzer.finished(ErrorProneAnalyzer.java:231)
        at jdk.compiler/com.sun.tools.javac.api.MultiTaskListener.finished(MultiTaskListener.java:133)
        at jdk.compiler/com.sun.tools.javac.main.JavaCompiler.flow(JavaCompiler.java:1423)
        at jdk.compiler/com.sun.tools.javac.main.JavaCompiler.flow(JavaCompiler.java:1370)
        at jdk.compiler/com.sun.tools.javac.main.JavaCompiler.compile(JavaCompiler.java:955)
        at jdk.compiler/com.sun.tools.javac.main.Main.compile(Main.java:319)
        at jdk.compiler/com.sun.tools.javac.main.Main.compile(Main.java:178)
        at jdk.compiler/com.sun.tools.javac.Main.compile(Main.java:66)
        at jdk.compiler/com.sun.tools.javac.Main.main(Main.java:52)
1 error

Narrowing

Only the combination record component + TYPE_USE-targeted annotation crashes. Every other
shape of the same declaration produces a correct suggested fix:

declaration result
record component + TYPE_USE-targeted annotation CRASH
record component, no annotation finding only, fix is clean
record component + FIELD / PARAMETER / METHOD / RECORD_COMPONENT-only annotation finding only, fix is clean
class field / method parameter / return type / local + TYPE_USE annotation finding only, fix is clean

Further observations:

  • Severity is irrelevant: it crashes at default severity, at :WARN, and at :ERROR.
  • The generic wrapper is irrelevant: @TypeUse Key key and @TypeUse Map<Key, String> key
    crash identically.
  • The reported start offset (67 above) is the offset of the @ of @TypeUse, not of the
    Key identifier at offset 76. So the replacement region is being computed over the
    AnnotatedType node wrapping the component's type rather than over the identifier itself, and
    that node has no end position recorded (-1) — presumably because the record component's type
    tree is reached through a synthesized member rather than the original source tree.

Is this a regression?

No — it is not a regression. It reproduces on every Error Prone release I could run on JDK 25:
2.36.0 through 2.50.0, 15 consecutive releases spanning 2024-11-19 to 2026-06-10.

Error Prone result
2.50.0 (latest) CRASHSourcePositionException: invalid source position: [67, -1)
2.49.0 CRASHSourcePositionException
2.48.0 CRASHSourcePositionException
2.47.0 CRASHIllegalArgumentException: invalid replacement: [67, -1)
2.46.0 CRASHIllegalArgumentException
2.45.0 CRASHIllegalArgumentException
2.44.0 CRASHIllegalArgumentException
2.43.0 CRASHIllegalArgumentException
2.42.0 CRASHIllegalArgumentException
2.41.0 CRASHIllegalArgumentException
2.40.0 CRASHIllegalArgumentException
2.39.0 CRASHIllegalArgumentException
2.38.0 CRASHIllegalArgumentException
2.37.0 CRASHIllegalArgumentException
2.36.0 CRASHIllegalArgumentException
2.35.1 and older not testable — Error Prone does not run on JDK 25 (NoSuchFieldError: Class com.sun.tools.javac.code.TypeTag does not have member field 'UNKNOWN' from ASTHelpers.<clinit>)

Two notes on reading that table:

  • The change of exception type at 2.48.0 is cosmetic, not behavioural: both are thrown from the
    same Replacement.create call site with the identical offsets [67, -1).
    SourcePositionException is a wrapper introduced around the pre-existing
    Preconditions.checkArgument failure.
  • The floor at 2.35.1 is a JDK 25 incompatibility in Error Prone itself, unrelated to this bug.
    I had only JDK 25 available, so I cannot say whether the crash predates 2.36.0 — only that it has
    been present in every release since.

Consequently there is no "pin to an older version" workaround; disabling BadImport is the only
option.

Impact

Because an annotation with a TYPE_USE target is enough, this is easy to hit in ordinary code.
jakarta.validation.constraints.@NotNull (and the rest of Bean Validation) lists TYPE_USE among
its targets, so any validated record DTO whose component type is an imported nested class named
Builder / Key / Type / Id / Factory / … aborts the build. In our case a single such DTO
made BadImport unusable repository-wide, and there is no per-site suppression that helps because
the crash happens while rendering the fix for the import statement.

Version history

Not a regression — it crashes in every release I tested, back to 2.30.0. Only the failure mode
changed: 2.36.0 introduced SourcePositionException, before that the same bad region surfaced as
IllegalArgumentException, and 2.35.1 and older report it as a generic unhandled plugin exception.

Error Prone Result Reported as
2.50.0 - 2.48.0 CRASH SourcePositionException: invalid source position: [67, -1)
2.47.0 - 2.36.0 CRASH IllegalArgumentException: invalid replacement: [67, -1)
2.35.1 - 2.30.0 CRASH unhandled exception thrown by the Error Prone plugin

Suggested direction

Either skip the fix (emit the finding without a SuggestedFix) when the end position of the
usage's enclosing tree is unavailable, or resolve the replacement region from the identifier node
rather than from its enclosing AnnotatedType.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions