[
https://issues.apache.org/jira/browse/GROOVY-12251?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Paul King updated GROOVY-12251:
-------------------------------
Description:
The {{NullChecker}} type-checking extension catches _assigning_ null to a
{{@NonNull}} field, but a {{@NonNull}} field that is simply *never initialized*
passes silently — and is then trusted as non-null at every dereference, so the
one field guaranteed to hold null is the one the checker never warns about.
Definite-initialization checking of non-null fields is a core check in
comparable tools (NullAway, the Checker Framework's nullness checker).
This issue adds, in flow-sensitive mode ({{NullChecker(strict: true)}}), a
check that each explicitly-annotated {{@NonNull}} instance field is definitely
initialized in one of the accepted ways:
* at its declaration ({{@NonNull String name = 'unknown'}})
* in an instance initializer block
* by *every* declared constructor — a constructor delegating via {{this(...)}}
relies on its delegate
A class with no declared constructor reports {{@NonNull field 'name' is not
initialized}}; a class where some but not all constructors assign the field
reports {{@NonNull field 'name' is not initialized by all constructors}}.
Example:
{code:groovy}
import groovy.transform.TypeChecked
@TypeChecked(extensions = 'groovy.typecheckers.NullChecker(strict: true)')
class Library {
@NonNull String catalog // never assigned
Library(String catalog) { } // oops — forgot this.catalog =
catalog
}
{code}
{noformat}
[Static type checking] - @NonNull field 'catalog' is not initialized by all
constructors
{noformat}
Deliberate scoping, to keep the check noise-free for idiomatic Groovy:
* *Strict mode only.* The lenient checker's contract is incremental adoption;
enabling this there would immediately flag existing {{@NonNull}} property
classes constructed via named arguments ({{new Foo(name: 'x')}} initializes the
property _after_ default construction). Strict mode is documented as the "full
coverage" tier, which is where a definite-initialization guarantee belongs.
Promoting it to lenient mode later remains open for discussion.
* *Explicitly-annotated fields only.* Fields that are merely non-null by
default (under {{@NullMarked}} / {{@NonNullByDefault}} without their own
annotation) are not checked, for the same named-argument-bean reason.
* *Exclusions:* {{@MonotonicNonNull}} and {{@Lazy}} fields (deliberately
initialized late), primitives, and interfaces. Static fields are deferred for
now.
* *Not path-sensitive:* an assignment anywhere in a constructor counts;
per-path definite-assignment analysis could tighten this later.
> NullChecker: check @NonNull fields are definitely initialized (strict mode)
> ---------------------------------------------------------------------------
>
> Key: GROOVY-12251
> URL: https://issues.apache.org/jira/browse/GROOVY-12251
> Project: Groovy
> Issue Type: Improvement
> Components: groovy-typecheckers
> Reporter: Paul King
> Assignee: Paul King
> Priority: Major
>
> The {{NullChecker}} type-checking extension catches _assigning_ null to a
> {{@NonNull}} field, but a {{@NonNull}} field that is simply *never
> initialized* passes silently — and is then trusted as non-null at every
> dereference, so the one field guaranteed to hold null is the one the checker
> never warns about. Definite-initialization checking of non-null fields is a
> core check in comparable tools (NullAway, the Checker Framework's nullness
> checker).
> This issue adds, in flow-sensitive mode ({{NullChecker(strict: true)}}), a
> check that each explicitly-annotated {{@NonNull}} instance field is
> definitely initialized in one of the accepted ways:
> * at its declaration ({{@NonNull String name = 'unknown'}})
> * in an instance initializer block
> * by *every* declared constructor — a constructor delegating via
> {{this(...)}} relies on its delegate
> A class with no declared constructor reports {{@NonNull field 'name' is not
> initialized}}; a class where some but not all constructors assign the field
> reports {{@NonNull field 'name' is not initialized by all constructors}}.
> Example:
> {code:groovy}
> import groovy.transform.TypeChecked
> @TypeChecked(extensions = 'groovy.typecheckers.NullChecker(strict: true)')
> class Library {
> @NonNull String catalog // never assigned
> Library(String catalog) { } // oops — forgot this.catalog =
> catalog
> }
> {code}
> {noformat}
> [Static type checking] - @NonNull field 'catalog' is not initialized by all
> constructors
> {noformat}
> Deliberate scoping, to keep the check noise-free for idiomatic Groovy:
> * *Strict mode only.* The lenient checker's contract is incremental adoption;
> enabling this there would immediately flag existing {{@NonNull}} property
> classes constructed via named arguments ({{new Foo(name: 'x')}} initializes
> the property _after_ default construction). Strict mode is documented as the
> "full coverage" tier, which is where a definite-initialization guarantee
> belongs. Promoting it to lenient mode later remains open for discussion.
> * *Explicitly-annotated fields only.* Fields that are merely non-null by
> default (under {{@NullMarked}} / {{@NonNullByDefault}} without their own
> annotation) are not checked, for the same named-argument-bean reason.
> * *Exclusions:* {{@MonotonicNonNull}} and {{@Lazy}} fields (deliberately
> initialized late), primitives, and interfaces. Static fields are deferred for
> now.
> * *Not path-sensitive:* an assignment anywhere in a constructor counts;
> per-path definite-assignment analysis could tighten this later.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)