Repository navigation
Conversation
1c9cb2f to
c229c67
Compare
The comment in `generated/build.gradle.kts` and the section in `README.md` claimed the annotation positions are correct because the generated code uses no arrays. Six generated columns are arrays. Java applies a `TYPE_USE` annotation written before an array type to the element type, and NullAway drops array-dimension annotations in JSpecify mode. So `@Nullable String[] getNspacl()` reads as a non-null array of nullable strings, while the column is a nullable array of non-null strings. The six accessors are `getNspacl`, `getDatacl`, `getRelacl`, `getReloptions`, `getSetconfig` and `getDefaclacl`. No operator code calls them today, because the operator uses the table field constants and `Routines.aclexplode` instead. The README also claimed NullAway reads the real nullness of every generated method. The generator annotates column accessors only. `Routines.java` and the classes in the `routines` package carry no annotation, so a routine such as `shobj_description` still counts as non-null.
c229c67 to
8151bfe
Compare
| fluentSetters = true | ||
| generatedAnnotation = true | ||
| pojos = false | ||
| nonnullAnnotation = true |
There was a problem hiding this comment.
The annotations only land on record getters/setters and table methods. Field constants stay unannotated (TableField<PgAuthidRecord, String> ROLPASSWORD), and the operator only uses those plus fetchOneInto/fetchSingle, never the record getters.
So NullAway gains almost nothing today, while we take on the caveats documented in the readme. Maybe I'm wrong, so feel free to correct me on this.
Is it worth keeping this part, or should the PR stick to the NullAway options?
There was a problem hiding this comment.
The annotations only land on record getters/setters and table methods. Field constants stay unannotated (
TableField<PgAuthidRecord, String> ROLPASSWORD), and the operator only uses those plusfetchOneInto/fetchSingle, never the record getters.So NullAway gains almost nothing today, while we take on the caveats documented in the readme.
You are right about today: the field constants stay unannotated, and no operator code calls a record getter. I would still like to keep it, for three reasons.
1. Without the annotations, NullAway trusts every getter as non-null.
The generated package it.aboutbits.postgresql.core.infrastructure.persistence is inside -XepOpt:NullAway:AnnotatedPackages=it.aboutbits.postgresql. So NullAway treats the jOOQ code as annotated code, and an unannotated return type there means non-null. For a nullable column, that is wrong, and NullAway stays silent.
I checked this with a small probe class that dereferences PgAuthidRecord.getRolpassword():
return authid.getRolpassword().length();- On this branch:
error: [NullAway] dereferenced expression authid.getRolpassword() is @Nullable - On the base branch, without the annotations:
BUILD SUCCESSFUL
So the annotations do not only add information, they remove a false guarantee. The first time someone uses a record getter, NullAway checks it correctly. I first thought AcknowledgeRestrictiveAnnotations was the reason, but that option only applies to unannotated third-party code. The generated package is inside AnnotatedPackages, so NullAway reads it as annotated code.
2. It is a real-world example for the open jOOQ issue.
jOOQ does not support TYPE_USE positioning yet (jOOQ/jOOQ#10759, still open). The six array accessors in this module show the problem: @Nullable String[] getNspacl() reads as a non-null array of nullable strings. I would like to show this to Lukas Eder as a concrete case, possibly as the base for a reproducer.
3. It is a good case for feedback to NullAway, too.
The usual way to handle generated code, NullAway:TreatGeneratedAsUnannotated, would discard exactly these annotations. RestrictiveAnnotationHandler ignores annotations in generated code "no matter what", even with AcknowledgeRestrictiveAnnotations=true, which JSpecify mode turns on anyway. NullAway's own source names the gap in CodeAnnotationInfo: "In the future, we might want finer grain controls to distinguish code that is generated with nullability info and without." This module is exactly that case: generated code that carries nullability info.
The caveats are documented in the README and in generated/build.gradle.kts. The only wrong positions are the six array accessors, and no operator code calls them. If one of them is needed later, we can switch nullableAnnotationType to a declaration annotation or wait for the jOOQ fix.
… enhance-nullaway-configuration
Enhance the NullAway configuration
Two independent changes:
errorprone.args. All four measure zero findings.See the NullAway wiki, Adopting NullAway, Configuration and JSpecify Support for details.
Why now
This branch runs NullAway 0.13.4. The upgrade to 0.14.1 lands in a separate PR.
The NullAway 0.13.4 configuration surface has 37 options. Only two were set.
I read the full list out of
ErrorProneCLIFlagsConfigand measured every candidate that could apply to a JSpecify project, rather than picking from the wiki by eye. All four options below exist in 0.13.4.What changed
enable additional NullAway checks in errorprone.args# Nullnesssectionconfigure jOOQ code-generation to generate nullability annotationsgenerated/build.gradle.kts, the regenerated sources, and a README sectionThe four options
AcknowledgeRestrictiveAnnotations@Nullableannotation found in unannotated code, instead of ignoring itAnnotatedPackagesalready covers the generated package, so NullAway reads those annotations without this optionCheckOptionalEmptinessOptional.get()where the value was never checked as presentOptionalappears in 7 main source filesExhaustiveOverrideOverrideannotationMissingOverrideruns at ERROR, which guarantees the annotation is present. This is the setting the wiki page already linked frombuild.gradle.ktsdescribesHandleTestAssertionLibrariesassertThat(x).isNotNull()establishes non-nullnessThe jOOQ codegen change
Every generated accessor now carries the real nullness of its column, so NullAway models the persistence layer instead of assuming it.
One caveat is recorded in the README.
jOOQ does not officially support the
TYPE_USEpositioning that JSpecify requires (jOOQ#10759).The positioning is correct here only because the generated code uses no generics, collections, maps, arrays or forced types with inner classes.
If the
includeslist ever grows such an object, the annotation positions need a review.That review will not stay manual. NullAway's wiki documents a
JSpecifyUnrecognizedAnnotationLocationchecker that reports a JSpecify annotation in any location the specification does not read,and ships a suggested fix so Error Prone's patch mode can correct them in bulk.
It is documented as version 0.14.2, and 0.14.2 is not released - 0.14.1 is the latest release, and the class is absent from its sources. This branch runs 0.13.4, so the checker is two releases away.
The wiki tracks master. Worth enabling the moment it ships, because it checks exactly the property this caveat depends on.
Considered and not adopted
TreatGeneratedAsUnannotated- rejected, and it would now do harmIt was in the first draft of this PR and came out again. Two independent reasons:
:generated:compileJavadoes not fail onRequireExplicitNullMarking, since no generated class carries@NullMarked.It is not.
RequireExplicitNullMarkingis a standaloneBugCheckerthat never reads NullAway'sConfig, while the option lives inCodeAnnotationInfo.shouldTreatAsUnannotatedand only feeds theNullAwaycheck's model.Setting it or clearing it changes nothing here.
RestrictiveAnnotationHandler: "with the generated-as-unannotated option enabled, we want to ignore annotations in generated code no matter what" - including underAcknowledgeRestrictiveAnnotations=true.NullAway's own source notes the gap: "In the future, we might want finer grain controls to distinguish code that is generated with nullability info and without." This project is now exactly that case.
OnlyNullMarked- tried, measured, and rejectedThe JSpecify-native alternative to
AnnotatedPackages: only@NullMarkedcode counts as annotated.It reads better, because
@NullMarkedlives in the source where JSpecify tooling and IntelliJ read it, whileAnnotatedPackagesis a NullAway-only setting no other tool understands.It was switched on during this PR and switched back.
It is strictly weaker here, because it silently turns off checking of the generated module. jOOQ annotates the column accessors only.
120 generated methods have unannotated parameters -
as(String alias),rename(Name name),where(Condition condition)and their siblings - and this project calls such methods 32 times inoperator/src/main/java.PG_AUTHID.as((String) null)AnnotatedPackagesOnlyNullMarkedUnder
AnnotatedPackagesthe generated module is annotated code, so an unannotated parameter means non-null and NullAway enforces it.Under
OnlyNullMarkedthe module falls back to unannotated, where NullAway is optimistic and lets null through.AcknowledgeRestrictiveAnnotationsdoes not close the gap:it reads annotations that make an API stricter, and an absent annotation carries nothing to read.
The strictness can be restored under
OnlyNullMarked, but not cheaply.@NullMarkedon a package does not apply to subpackages - verified both ways:a
package-info.javain...persistencewhile the class sits in...persistence.tablesreports nothing, while the same file in...persistence.tablesreports the error.That means four
package-info.javafiles, one per generated package, which must also survive jOOQ's cleaning ofsrc/main/javaon every regeneration.That is more machinery than the tidiness is worth, so
AnnotatedPackagesstays.JSpecifyExperimental- a follow-up PRIt turns on three things at once: the jspecify/jdk standard-library models, wildcard generics, and inference-failure warnings. Measured on this codebase it reports 12 findings, and they are genuine signal rather than noise:
@Nullablevalue where@NonNullis required@Nullable, butFunction.applyreturns@NonNullUis constrained to be@Nullable@Nullableexpression@Nullableexpression from a@NonNullmethodOne is worth naming:
matcher.group(1)is passed where non-null is required. The JDK model knows that method can return null, so this is a latent NPE that nothing catches today.Upstream plans to make
JSpecifyExperimentalthe default in a future release, so the 12 findings must be fixed sooner or later. A follow-up PR fixes them and enables the flag.That follow-up need not be one step. Each of the three features has its own flag -
JSpecifyJDKModels,HandleWildcardGenericsandWarnOnGenericInferenceFailure- and the wiki notes that most new errors come from the JDK models.The 12 findings can therefore be taken a flag at a time rather than in one hit.
Rejected outright
AssertsEnabledassertstatementsCheckContracts,CustomContractAnnotations@ContractannotationsReview notes
RequireExplicitNullMarkingcorrectly uses-Xep:, not-XepOpt:. It is a check, not an option: NullAway 0.13.4 ships exactly two@BugPatterncheckers,NullAwayandRequireExplicitNullMarking. Writing it as-XepOpt:NullAway:RequireExplicitNullMarking:ERRORparses as an unknown option with no=, is silently ignored, and lets the check fall back to its defaultSUGGESTIONseverity, where it reports nothing. Verified by compiling a scratch class with no@NullMarked: the-XepOptform builds successfully with 0 hits, the-Xepform fails the build with 2. Please do not "fix" this line.@SuppressWarnings({"all", "unchecked", "rawtypes", "this-escape"}), and Error Prone honours that annotation for checks at ERROR too. Verified by deleting that one line fromRoutines.javaand recompiling:RequireExplicitNullMarking×1,PrivateConstructorForUtilityClass×2 andVarifier×6 fire immediately. The README records this, so nobody hunts for a flag that does not exist.ExhaustiveOverridehas a dependency. IfMissingOverrideis ever lowered from ERROR, this option must come out with it, or NullAway will silently stop checking overrides that lack the annotation. The comment inerrorprone.argsrecords this.HandleTestAssertionLibrariescannot break the build. It only teaches NullAway to trust an assertion, so it removes findings.AcknowledgeRestrictiveAnnotationsis the one that can bite later. It is clean today, but a dependency bump that adds@Nullableannotations to fabric8 or jOOQ will surface new errors. That is the point of the option, and the errors will be real.Test scope
:operator:compileJava,:operator:compileTestJavaand:generated:compileJavawith--rerun-tasks. All four report 0.--rerun-taskson the same three tasks - BUILD SUCCESSFUL, 0 warnings../gradlew build -x test- BUILD SUCCESSFUL.