Skip to content

Fail the build on NullAway and ten more Error Prone checks - #201

Merged
abashev merged 24 commits into
21.X.Xfrom
abashev/Fix-error-prone-issues
Nov 8, 2026
Merged

abashev merged 24 commits into
21.X.Xfrom
abashev/Fix-error-prone-issues

Conversation

@abashev

@abashev abashev commented Nov 8, 2026 •

Copy link
Copy Markdown
Collaborator

Error Prone has run on every compile, but nothing it reported failed the build except two JUnit checks, and the warnings scrolled past. This is the first step towards making it fail the build: the suppressions go, nullness moves to JSpecify, and eleven checks become errors once their findings are fixed.

  • @SuppressWarnings. 105 of the 110 are gone. They hid almost nothing. 94 named their checks with palantir-baseline's for-rollout: prefix, which plain Error Prone does not take for a check's name, so NullAway and the rest reported those lines all along. Others named checks this build does not run (TooManyArguments, ImmutablesStyle), IntelliJ inspections, or javac's unchecked lint, which is off. Removing UnicodeEscape's brings back its one finding.
  • Immutable stays suppressed. The five suppressions on Doc's and Level's memoized suppliers remain. Immutable is an error by default, and the check is right: Suppliers.memoize caches its value, so Break, Token and NonBreakingSpace, which are @Immutable and extend Doc, are not immutable. Fixing that is a change of its own.
  • Four checks are errors, after their findings are fixed. Each one has its own commit, and every commit builds. All the checks that fail the build are now set in one block; the ones that were only listed as comments for later are gone.
    • UnusedVariable: ExecutableTransform's logger, StringWrapper's endLine, and the stderr that two MainTest tests read after the process had exited.
    • SameNameButDifferent: in PalantirJavaFormatSettings, State named both IntelliJ's @State and the settings bean. The bean is now StoredSettings. Its class name is not stored in .idea/open-java-format.xml, and PalantirJavaFormatSettingsTest loads files written before.
    • RefactorSwitch: four switch statements that returned from every case are now the switch expressions they return.
    • FormatStringConcatenation: seven calls in the tests pass a format string and its arguments to AssertJ's describedAs, hasMessage and isEqualTo and to Guava's checkState, instead of a concatenated string. In JavaCommentsHelperTest the expected comment stays written out like its input, and the check is suppressed on that test.
  • JSpecify. org.jspecify.annotations.Nullable replaces jsr305's javax.annotation.Nullable, and the jspecify dependency (1.0.1, the version Guava brings) replaces jsr305. JetBrains' @NotNull goes from the IntelliJ plugin, and Obs's jsr305 @CheckReturnValue is now Error Prone's. CLAUDE.md says where the type-use annotation goes on nested and array types.
  • NullAway is an error.
    • It runs in JSpecify mode. Outside that mode NullAway took Guava's @ParametricNullness for @Nullable, so every token read from a PeekingIterator<Token> counted as possibly null. That made 49 of its 102 findings; the mode reports 7 others that were real, which leaves 60. On javac 21 the mode needs -XDaddTypeAnnotationsToSymbol=true to see @Nullable on classes read from jars.
    • Most of the 60 real findings were a null the code meant, with no @Nullable where it went. A lookup that cannot miss now goes through checkNotNull, which fails as the dereference did. The commit message lists the few changes beyond annotations, such as the javadoc lexer's one null-valued newline token and JUnit 6's computeIfAbsent.
  • Three of Picnic's suggestions are errors, after their findings are fixed:
    • RedundantStringConversion: six toString() calls on values that concatenation or Path.of converts anyway.
    • CanonicalAnnotationSyntax: braces around a single annotation value, and a trailing comma in an annotation array.
    • DirectReturn: two methods that put their last value in a variable only to return it.
  • CollectorMutability is an error. The ten Collectors.toList() calls are Stream.toList(), as CLAUDE.md asks of new code, and StringWrapper's Collectors.toMap is toUnmodifiableMap. Nothing changed these collections after they were built, and none holds a null. A new test round-trips the IntelliJ settings' classpath, the one list that goes through a serializer.
  • StringConcatToTextBlock is an error. The Java sources that DiagnosticTest and FormatterTest feed the formatter, strings joined by + or Joiner, are text blocks.
  • EffectivelyPrivate is an error. Twelve members of private nested classes lose a public or protected that reached no further than the file.
  • Four of Picnic's suggestions are off:
    • LexicographicalAnnotationListing wants annotations in alphabetical order. Its 25 suggestions would move @Override and @Test below the other annotations.
    • LexicographicalAnnotationAttributeListing would sort annotation arrays, {false, true} in a dozen @ValueSource tests and a @CsvSource's rows.
    • StringJoining would replace String.format("%s:%s", a, b) with String.join and two String.valueOf calls.
    • IsInstanceLambdaUsage would replace doc -> doc instanceof Level with Level.class::isInstance.
  • UTF_8 is imported statically, as most of the code already had it: 36 uses of StandardCharsets.UTF_8.

Tests

./gradlew test passes on JDK 21: 1627 tests, 14 skipped. With an unused local added to CommandLineOptionsParser, :open-java-format:compileJava fails with error: [UnusedVariable]. With ModifierOrderer.compareTo dereferencing its @Nullable field again, it fails with error: [NullAway].

Not here

30 warnings remain: ConstantNaming 4, and 1 to 3 each for 17 more checks. There are also 49 suggestions: StaticImport 47 and NonStaticImport 2. Those checks, Immutable included, follow one at a time.

Of the 110 annotations 105 hid nothing. 94 named their checks with
palantir-baseline's for-rollout: prefix, which plain Error Prone does
not take for the check's name: NullAway and the rest reported those
lines all along. Others named checks this build does not run
(TooManyArguments, ImmutablesStyle), IntelliJ inspections, or javac's
unchecked lint, which is off. Without UnicodeEscape's, its one finding
shows.

The five on Doc's and Level's memoized suppliers stay: Immutable is an
error, and the suppliers are mutable caches that the check rightly
rejects. They need their own change.
ExecutableTransform's logger, StringWrapper's endLine and the stderr
two MainTest tests read after the process had exited were never read.
They are gone, and Error Prone now fails the build on the next one.
In PalantirJavaFormatSettings `State` named both IntelliJ's @State
annotation and the settings' own State class. The annotation is now
written out in full, which leaves the name to the class.
The switch statements in DimensionHelpers.extractDims,
CommandLineOptionsParser's two range parsers and SnippetFormatter's
snippetWrapper returned from every case. Each is now the switch
expression it returns, with yield where a case has statements before
its value.
Eight calls in the tests built a message or an expected string by
concatenation and handed it to a method that formats one itself:
AssertJ's describedAs, hasMessage and isEqualTo(String, Object...) and
Guava's checkState. They now pass a format string and its arguments.
The checks that fail the build join excludedPaths in a single block,
in alphabetical order. The checks that were only listed as comments
for later are gone; the findings still to fix are in the build output.
State also named IntelliJ's @State annotation on the same class, which
the previous commit wrote out in full to tell the two apart. The class
gets a name of its own and the annotation its import back.

The class name is not in .idea/open-java-format.xml: IntelliJ writes
the bean's fields into the component element, and
PalantirJavaFormatSettingsTest loads files written before.
…fTheRun

As a format string the expected comment no longer read like the input
it is rewritten from. The concatenation is back, with
FormatStringConcatenation suppressed on the test.
The 21.X.X merge brought in Signature, whose bundled field only went
into the serialized signature and carried a suppression. jars() now
picks the jars by it: the hash is there exactly when the build declared
none of its own, so the step formats with the same jars as before.
jsr305's javax.annotation.Nullable gives way to
org.jspecify.annotations.Nullable, and the jsr305 dependency to
jspecify 1.0.1, the version Guava already brings. Obs's
@CheckReturnValue, the other jsr305 annotation in use, is now Error
Prone's.

JetBrains' @NotNull goes from the IntelliJ plugin: what is not
@nullable is non-null, and NullAway checks it. The plugin does not
instrument its code, so the annotations added no runtime checks either.

JSpecify's @nullable is a type-use annotation. Where it stood above an
@OverRide it now follows it, next to the return type it annotates.
CLAUDE.md names the annotation and where it goes on nested and array
types.
Outside it NullAway takes Guava's @ParametricNullness for @nullable:
PeekingIterator.peek() and next(), Iterables.getLast and
getOnlyElement, Iterators.getLast. Every token taken from a
PeekingIterator<Token> was reported as possibly null. JSpecify mode
reads Guava's type parameters instead, and of NullAway's 102 findings
60 remain.

On javac 21 the mode needs -XDaddTypeAnnotationsToSymbol=true to see
type-use annotations, @nullable among them, on classes read from jars.
Picnic's check asks for annotations in alphabetical order, and gave 25
suggestions that would move @OverRide and @test below the rest. The
annotation that says what a member is stays first, as most of the code
has it. JavaCommentsHelperTest's @test is above its @SuppressWarnings
again.
Most of the 60 findings were a null the code meant, with no @nullable
where it went. Now marked @nullable:
- RawTok's and Tok's kind and string value, and a comment's text before
  it is first read;
- the parent of the root exploration node;
- the methods maybeGetMethod looks up, and the fields that keep them;
- the receiver of an unqualified call, and a dimension list's missing
  type;
- getLineEnding's result for a string without one;
- the three scan overrides, which javac calls with null for a missing
  tree.

Where a lookup cannot miss (a token at a position the input has, a
range the output built, a level's children), checkNotNull says so and
fails as the dereference did.

Besides that:
- variableFragmentDims no longer takes a missing type: its one caller
  that may have none maps the Optional instead.
- The javadoc lexer's newline after a `}` it moves has the "\n" of
  every other newline token rather than null. The formatter writes a
  line break for it and never reads the value.
- Newlines.LineIterator returns the line it reads instead of keeping it
  in a field.
- BootstrappingFormatterService runs the formatter without a working
  directory, rather than throwing, if the java binary has no parent
  directory.
- PalantirJavaFormatSettings.getState() is no longer @nullable: it
  never returns null.
- ParameterizedClass uses JUnit 6's computeIfAbsent, which never
  returns null, in place of the deprecated getOrComputeIfAbsent.
- FormatterIntegrationTest reads the expected output's line separator
  in the tests that use it.
- NullAway takes AssertJ's assertThat(x).isNotNull() in tests for the
  null check it is.
Its findings are fixed, and the next one now fails the build.
@abashev abashev changed the title Fail the build on four Error Prone checks Fail the build on NullAway and four more Error Prone checks Nov 8, 2026
The 36 uses of StandardCharsets.UTF_8 now read UTF_8, as the rest of
the code already wrote it, and the ten files no longer import
StandardCharsets. The import in a MainTest string is input to the
formatter and stays.
- LexicographicalAnnotationAttributeListing would sort the values in an
  annotation's arrays: {false, true} for {true, false} in a dozen
  @valuesource tests, and a @CsvSource's rows out of the order they are
  grouped in.
- StringJoining would turn String.format("%s:%s", lower, higher) into
  String.join(":", String.valueOf(lower), String.valueOf(higher)).
- IsInstanceLambdaUsage would turn doc -> doc instanceof Level into
  Level.class::isInstance.
Six toString() calls on what string concatenation or Path.of converts
anyway are gone: on the diagnostics Main prints, on the paths MainTest
expects, and on an environment variable, which is a String already.
An annotation attribute with one value no longer has braces around it
(@State's storages), and an array of them no trailing comma
(SpotlessExcludesTest's @valuesource, which then fits on one line).
Formatter.formatSourceAndFixImports and JavadocLexer.generateTokens
assigned their last value to a variable only to return it; they return
it directly.
@abashev abashev changed the title Fail the build on NullAway and four more Error Prone checks Fail the build on NullAway and seven more Error Prone checks Nov 8, 2026
The settings' tests wrote and read back the native binary settings but
never a classpath, the one list in them. The new test round-trips two
jars through IntelliJ's serializer.
@abashev abashev changed the title Fail the build on NullAway and seven more Error Prone checks Fail the build on NullAway and eight more Error Prone checks Nov 8, 2026
@abashev abashev changed the title Fail the build on NullAway and eight more Error Prone checks Fail the build on NullAway and ten more Error Prone checks Nov 8, 2026
The ten Collectors.toList() calls are now Stream.toList(), as CLAUDE.md
asks of new code, and StringWrapper's Collectors.toMap is
toUnmodifiableMap. Nothing changed any of these collections after they
were built, and none holds a null, so only their mutability differs:
the lists go to command lines, records and assertions, and the map is
only read. The previous commit's test round-trips the IntelliJ
settings' classpath, the one list that goes through a serializer.
The Java sources that DiagnosticTest and FormatterTest feed the
formatter were strings joined by + or by Joiner.on('\n'). They are text
blocks now, ending in \ where the source had no final newline, and
Joiner is no longer imported.
Twelve members of private nested classes were public or protected,
which reaches no further than the file a private class is in. They
have no access modifier now:
- the constructors of Obs's two nodes, of ReferenceScanner, and of the
  test's DelegatingFormatter;
- the IntelliJ formatting task's constructor and applyReplacements;
- SnippetWrapper's three methods and StringWrapper's
  getReflowReplacements;
- TypeNameClassifier's TyParseState.isSingleUnit and next(), with the
  five overrides of next() in its constants.
@abashev
abashev force-pushed the abashev/Fix-error-prone-issues branch from db6dfa4 to fbb6a7c Compare November 8, 2026 16:33
@abashev
abashev merged commit c7354ce into 21.X.X Nov 8, 2026
17 checks passed
@abashev
abashev deleted the abashev/Fix-error-prone-issues branch November 8, 2026 16:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant