Skip to content

Improve code quality by enforcing more Error Prone checks - #77

Merged
ThoSap merged 9 commits into
mainfrom
promote-disabled-error-prone-checks
Oct 23, 2026
Merged

ThoSap merged 9 commits into
mainfrom
promote-disabled-error-prone-checks

Conversation

@ThoSap

@ThoSap ThoSap commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Follow-up to #66, #67, #68 and #69.
Error Prone 2.51.0 disables 151 checks by default. This PR measured every one of them on all three source sets and promotes 57 to ERROR.
148 checks now run at ERROR, up from 91.

48 checks had zero findings. The other 9 needed a small fix first, and each fix is its own commit before the promotion.
The fixes do not change the behavior of the operator.

Method

  • The list comes from the DISABLED_CHECKS set in BuiltInCheckerSuppliers of 2.51.0: 151 checks. errorprone.args already set 36 of them, and EmptyIfStatement is the same check as EmptyIf. That leaves 114 candidates.
  • All candidates ran at WARN with -XepAllDisabledChecksAsWarnings on operator main, operator test and generated. Then every candidate with findings was read finding by finding.
  • A check that needs a library the project does not have (Guava, Guice, Dagger, Truth, JUnit 4, Flogger, Android, ...) was not promoted, even with zero findings. Such a check cannot report anything here.
  • After the promotion, a temporary class with one violation per check proved that the checks fail the build.

The fixes

Commit Change For
707ff17 Remove the unreachable default -> throw new UnsupportedOperationException(...) from the exhaustive GrantObjectType switch in GrantService. MissingCasesInEnumSwitch now fails the build for a new enum value, instead of an exception at runtime UnnecessaryDefaultInEnumSwitch
019c567 One comment on the @SuppressWarnings("unchecked") cast in HelmTest SuppressWarningsWithoutExplanation
5ae0463 rescheduleAfter(60, TimeUnit.SECONDS) → rescheduleAfter(Duration.ofSeconds(60)), and the same for Awaitility atMost and pollInterval. 39 places, 22 in main and 17 in test. The fabric8 withTimeout(..., TimeUnit) calls stay, as they have no Duration overload PreferJavaTimeOverload
576dc63 8 private helper methods without instance state become static, 1 in main and 7 in test MethodCanBeStatic
94cf18c "string".equals(x) → Objects.equals(x, "string") at 4 places. This is the null-safe fix of the check. Boolean.TRUE became true because of BooleanLiteral YodaCondition
addeb02 The mock KubernetesClient in KubernetesServiceTest and PasswordFingerprintServiceTest is an instance field instead of static. See the review notes NonFinalStaticField
89329d6 12 × catch (Exception e) → catch (RuntimeException e) in the reconcilers and PostgreSQLInstanceReadinessCheck. The try blocks call only jOOQ and fabric8, which throw unchecked exceptions, so the same exceptions are caught as before. The 3 catches in PostgreSQLAuthenticationService stay, because they catch checked exceptions CatchingUnchecked
c1f1841 The private reconcile(DSLContext, Database, CRStatus) of DatabaseReconciler becomes reconcileWithoutTransaction. It overloaded the public reconcile(Database, Context). The other reconcilers call this step reconcileInTransaction, and the database step runs outside a transaction because PostgreSQL does not allow create database in one UngroupedOverloads, InconsistentOverloads

The 57 promoted checks

The groups are the groups of errorprone.args. Two groups are new: "Security and platform" and "Dependency injection".

Javadoc (1)

Check Why
UrlInSee A URL belongs in a Markdown link [text](url), not in @see

Imports (2)

Check Why
NonCanonicalStaticMemberImport Imports a static member from the class that declares it
UnnecessarilyFullyQualified It detects clashes, so @io.fabric8.generator.annotation.Nullable next to the JSpecify Nullable stays allowed

Naming and source layout (5)

Check Why
ClassNamedLikeTypeParameter A class named T or E2 reads like a type parameter
DifferentNameButSame One type written in two ways in one file, for example Foo.Bar next to Bar
NegativeBoolean A local boolean is not named no… or not…, so that conditions do not read as double negations
PublicApiNamedStreamShouldReturnStream A public stream() must return a stream
UngroupedOverloads Overloads stand next to each other

Blocks, statements and switches (2)

Check Why
ForEachIterable An enhanced for loop instead of a manual Iterator loop
UnnecessaryDefaultInEnumSwitch A default in an exhaustive enum switch hides a missing case from MissingCasesInEnumSwitch. MissingDefault exempts exhaustive enum switches, so the two agree

Class design (11)

Check Why
AvoidObjectArrays No object array in a public signature. It skips varargs, and the generated jOOQ arrays are suppressed. Its message names the Guava ImmutableList; List works the same here
BuilderReturnThis Protects the fluent test-data builders
FieldCanBeFinal Makes immutability visible. It skips @Inject fields (also jakarta) and fields that the code never assigns, such as @TempDir and @InjectMock
FieldCanBeLocal No hidden state in CDI singletons
FieldCanBeStatic One constant value per class, not per instance
InconsistentOverloads Overloads keep their shared parameters in the same order
MethodCanBeStatic A private method without instance state says so with static
NonFinalStaticField A mutable static field is shared global state
PreferredInterfaceType Narrows Iterable or Collection to the interface List or Set, and CharSequence to String, for final fields, locals and non-overridable return types
RecordComponentAccessorAnnotationConflict An explicit accessor silently drops an annotation of the record component
UnnecessarilyVisible A method or constructor with @Inject (also jakarta) is called by the framework only, so package-private is enough

Correctness (13)

Check Why
AssertFalse The JVM runs without -ea, so assert false does nothing
CatchingUnchecked A catch (Exception e) that can only catch unchecked exceptions becomes catch (RuntimeException e). If a later change adds a call with a checked exception, the compiler forces a decision instead of a silent catch-all
CheckedExceptionNotThrown A false throws forces callers to write dead catch blocks
ComparisonContractViolated Wrong sort results or an IllegalArgumentException in TimSort
EqualsBrokenForNull equals(null) must return false by contract
InterruptedExceptionSwallowed A lost interrupt prevents a clean operator shutdown
Interruption Future.cancel(true) can break JDBC connections
IterablePathParameter A Path is itself an Iterable<Path>. Relevant because of the file-based credentials
PreferJavaTimeOverload A Duration keeps the value and the unit together
StronglyTypeTime A Duration field instead of a long with a unit in its name
TimeUnitMismatch Seconds passed where nanoseconds are expected
UnsafeLocaleUsage Unsafe Locale constructors and toString give wrong locale tags
UseCorrectAssertInTests Without -ea, a Java assert in a test checks nothing. AssertJ always checks

Security and platform (6)

Check Why
BanClassLoader The operator holds cluster credentials. Loading code at runtime has no use here
BanSerializableRead CRs travel as JSON. Java deserialization is a known attack path
InsecureCryptoUsage The operator handles passwords and writes SCRAM verifiers
SunApi Internal sun.* APIs break in a later JDK. Also replaces the Checkstyle IllegalImport that #66 lost
SystemExitOutsideMain Quarkus controls the lifecycle. System.exit skips the shutdown hooks
SystemOut The operator logs through JBoss Logging. Console output skips log levels and the JSON format

Dependency injection (3)

All three know the jakarta.inject annotations that Quarkus uses.

Check Why
MissingRuntimeRetention Without RUNTIME retention, CDI ignores a qualifier or scope without an error
MoreThanOneQualifier Two qualifiers on one injection point make the resolution ambiguous
QualifierWithTypeUse CDI does not read a qualifier in a type-use position

Redundant code and house style (14)

Check Why
ConstantPatternCompile Compiles a constant regex once, not on every reconcile
DeduplicateConstants Uses the existing constant instead of repeating its literal
InitializeInline A declaration with its value, which fits var and Varifier
LambdaFunctionalInterface IntFunction instead of Function<Integer, …> in private code, without boxing
ParameterComment A /* name= */ comment must name the right parameter
RedundantOverride An override that only calls super is dead code
RedundantThrows throws IOException, FileNotFoundException declares one subtype too many
StringFormatWithLiteral format() with literal arguments only is one literal
SuppressWarningsWithoutExplanation Covers only deprecation, rawtypes and unchecked, the javac categories from #67. With -Werror, such a suppression hides a build failure, so it needs a reason
ThrowSpecificExceptions No throw new RuntimeException(...). The code uses IllegalStateException and similar types
TransientMisuse transient has no effect on a static field
UnnecessarilyUsedValue An ignorable result does not need an unused variable. Goes with CheckReturnValue and UnnamedVariable
UnnecessaryOptionalGet Uses the lambda parameter instead of opt.get() inside opt.map(...)
YodaCondition The variable comes first. The null-safe form is Objects.equals(x, CONSTANT)

The checks that stay off

With findings (13)
Check Findings Reason
UnnecessarySemicolon 1830 A false positive on Lombok @Getter/@Setter, and its fix removes the ; of real fields. Reported as google/error-prone#6233
Java8ApiChecker 211 The project uses Java 25
AddNullMarkedToClass 27 Requires @NullMarked directly on every top-level class. All 27 findings are generated jOOQ classes, which ignore the check's suppression. Not part of this PR; RequireExplicitNullMarking already requires explicit null marking
VarWithPrimitive 33 The project prefers var. Forbidding it for primitives is a style change with 33 edits
MissingJavadoc 32 Checks public types. This is an application, not a library
IdentifierName 24 Rejects PostgreSQL, SQL and CR in names. A fix would rename public classes such as CRStatus
CanIgnoreReturnValueSuggester 21 Needs @CanIgnoreReturnValue. error_prone_annotations is only on the test classpath
Var 14 Needs @Var from the same artifact on every reassigned variable
DefaultLocale 7 Its fix replaces "...".formatted(...) with String.format(Locale.ROOT, ...). Decided against that rewrite
BooleanParameter 3 Wants /* name= */ comments on boolean literals
ThrowsUncheckedException 2 throws DataAccessException in PostgreSQLContextFactory documents the jOOQ failure on purpose
TypeParameterNaming 1 Wants CRT instead of CR in BaseReconciler. CR is the domain term
PreferPreconditions 1 Its fix calls Guava, which is only a transitive test dependency
With zero findings, but a reason (10)
Check Reason
TraditionalJavadocToMarkdown False positives and a crash with Lombok. Reported as google/error-prone#6232 and projectlombok/lombok#4091
RedundantNullCheck CR objects come from JSON, so a non-null field can be null at runtime. The check would remove valid defensive checks
MockitoDoSetup Does not know the Quarkus @InjectSpy. For such a spy, its fix when(spy.m()) calls the real method
CannotMockFinalClass Assumes the old Mockito. Mockito 5 mocks final classes
CannotMockMethod Assumes the old Mockito. Mockito 5 mocks final methods
FieldMissingNullable NullAway in JSpecify mode does this, more strictly
ParameterMissingNullable NullAway does this. In the default conservative mode, this check turns its main rule off
ReturnMissingNullable NullAway in JSpecify mode does this, more strictly
ReturnsNullCollection NullAway in JSpecify mode does this, more strictly
VoidMissingNullable NullAway owns nullness
Not applicable (33)

Each check needs a library or platform that the project does not have.
JUnit 4, Truth, Dagger, AutoValue, Flogger, protobuf, JSR 305 and TestParameterInjector are on no compile classpath.
Guava and Guice are only transitive test dependencies of quarkus-junit, and no source file imports them.

Library or platform Checks
Android BinderIdentityRestoredDangerously, HardCodedSdCardPath, StaticOrDefaultInterfaceMethod
Guice, Dagger AssistedInjectAndInjectOnConstructors, BindingToUnqualifiedCommonType, MutableGuiceModule, PrivateConstructorForNoninstantiableModule, RefersToDaggerCodegen, ScopeOnModule
AutoValue, AutoFactory, auto-common AutoFactoryAtInject, ExtendsAutoValue, AnnotationMirrorToString, AnnotationValueToString
Flogger FloggerLogWithCause, FloggerMessageFormat, FloggerRedundantIsEnabled, FloggerRequiredModifiers, FloggerWithCause, FloggerWithoutCause
Guava, guava-testlib ImmutableMemberCollection, ImmutableSetForContains, UnnecessarySetDefault
JSR 305 ImmutableRefactoring, UsingJsr305CheckReturnValue
Truth ImplementAssertionWithChaining, TruthContainsExactlyElementsInUsage
JUnit 3 and 4 ExpectedExceptionChecker, TestExceptionChecker, UnnecessaryTestMethodPrefix, TryFailRefactoring
TestParameterInjector SimplifyTestParameter
protobuf StronglyTypeByteString
@NoAllocation NoAllocation

Review notes

  • The mock client in two tests is now an instance field. @EnableKubernetesMockClient writes a static field once per class in beforeAll, and an instance field in beforeEach, from a new mock server. It starts the per-test server in both cases, so the old setup started a server it never used. Each test now gets a fresh server at no extra cost. The @BeforeEach clearSecrets() methods are now redundant, but they stay to keep the change small.
  • DefaultLocale stays off. %d in formatted(...) uses the digits of the default locale, but its fix replaces "...".formatted(...) with String.format(Locale.ROOT, ...). We decided against that rewrite.
  • CatchingUnchecked changes no behavior today. The compiler proves that the 12 try blocks can only throw unchecked exceptions, and the project has no @SneakyThrows. The only difference would be a checked exception that a library throws in a hidden way: it would now leave the reconciler, and the Java Operator SDK would log it and retry with backoff. The CR status would not show ERROR, because the reconcilers do not implement ErrorStatusHandler.
  • MethodCanBeStatic, YodaCondition, PreferredInterfaceType and CatchingUnchecked were first classified as "keep off". A second look at their sources showed that the reasons were wrong: YodaCondition offers a null-safe fix, PreferredInterfaceType narrows to interfaces, not to implementation types, and the "hidden checked exception" against CatchingUnchecked does not occur in this project.

Test scope

  • ./gradlew --rerun-tasks :operator:compileJava :operator:compileTestJava :generated:compileJava - BUILD SUCCESSFUL, 0 warnings, 0 findings, with 148 checks at ERROR and -Werror.
  • Every commit builds on its own.
  • Severity proven live: temporary classes failed the build with [SystemOut], [StringFormatWithLiteral], [NonFinalStaticField], [PreferredInterfaceType], [MethodCanBeStatic], [YodaCondition], [CatchingUnchecked], [UngroupedOverloads], [InconsistentOverloads], [AvoidObjectArrays] and [NegativeBoolean].
  • KubernetesServiceTest and PasswordFingerprintServiceTest: 32 tests, 0 failures, after the instance-field change.
  • The full test suite runs in CI.

…itch

The switch handles every `GrantObjectType` value. Without the `default`,
`MissingCasesInEnumSwitch` fails the build when a new value is added,
instead of an `UnsupportedOperationException` at runtime.

Assisted-by: Claude:claude-opus-5-5
Assisted-by: Claude:claude-opus-5-5
…Interval`

Assisted-by: Claude:claude-opus-5-5
The extension of `@EnableKubernetesMockClient` writes a static field once
per class, and an instance field before each test, from a new mock server.
It starts the per-test server in both cases, so each test now gets a
fresh server at no extra cost.

Assisted-by: Claude:claude-opus-5-5
@ThoSap
ThoSap force-pushed the promote-disabled-error-prone-checks branch from fa961e7 to d02fd10 Compare October 22, 2026 16:44
@ThoSap ThoSap changed the title Promote 53 disabled Error Prone checks to ERROR Promote 52 disabled Error Prone checks to ERROR Oct 22, 2026
The try blocks call only jOOQ and fabric8, which throw unchecked
exceptions. If a later change adds a call with a checked exception,
the compiler now forces a decision instead of a silent catch-all.

Assisted-by: Claude:claude-opus-5-5
@ThoSap
ThoSap force-pushed the promote-disabled-error-prone-checks branch from d02fd10 to a0a01de Compare October 22, 2026 16:47
@ThoSap ThoSap changed the title Promote 52 disabled Error Prone checks to ERROR Promote 53 disabled Error Prone checks to ERROR Oct 22, 2026
…ithoutTransaction`

The other reconcilers call this step `reconcileInTransaction`. The
database step runs outside a transaction, because PostgreSQL does not
allow `create database` in one.

Assisted-by: Claude:claude-opus-5-5
Each check reports zero findings on all three source sets. Nine of them
needed the fixes of the previous commits: `UnnecessaryDefaultInEnumSwitch`,
`SuppressWarningsWithoutExplanation`, `PreferJavaTimeOverload`,
`MethodCanBeStatic`, `YodaCondition`, `NonFinalStaticField`,
`CatchingUnchecked`, `UngroupedOverloads` and `InconsistentOverloads`.

Assisted-by: Claude:claude-opus-5-5
@ThoSap
ThoSap force-pushed the promote-disabled-error-prone-checks branch from a0a01de to 1f83dfe Compare October 22, 2026 17:00
@ThoSap ThoSap changed the title Promote 53 disabled Error Prone checks to ERROR Promote 57 disabled Error Prone checks to ERROR Oct 22, 2026
@ThoSap ThoSap self-assigned this Oct 22, 2026
@ThoSap ThoSap changed the title Promote 57 disabled Error Prone checks to ERROR Improve code quality by enforcing more Error Prone checks Oct 23, 2026
@ThoSap
ThoSap merged commit 0a58cbc into main Oct 23, 2026
4 checks passed
@ThoSap
ThoSap deleted the promote-disabled-error-prone-checks branch October 23, 2026 20:47
@ThoSap ThoSap added the enhancement New feature or request label Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant