[ 
https://issues.apache.org/jira/browse/GROOVY-12252?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Paul King updated GROOVY-12252:
-------------------------------
    Description: 
GROOVY-12206 and GROOVY-12207 made type-use nullability annotations available 
from compiled dependencies, and the parser attaches them within source type 
arguments (GROOVY-11178) — but the {{NullChecker}} type-checking extension 
never looked _inside_ type arguments. Calling {{get(0)}} on a {{List<@Nullable 
String>}} — JSpecify's headline capability, generics nullness — passed silently.

This issue teaches the checker to honour {{@Nullable}} type arguments wherever 
the static type checker's generics inference carries them through a *class or 
method level type variable*:

* {{get(0)}} on a {{List<@Nullable String>}}, including subtype receivers such 
as {{ArrayList<@Nullable String>}}
* the idiomatic subscript {{xs\[0]}} and GDK calls like {{head()}} / {{first()}}
* {{get(key)}} and {{m\[key]}} on a {{Map<String, @Nullable Integer>}}

The nullable result participates in the checker's existing analyses: unguarded 
dereference is an error, safe navigation and the recognised guard patterns are 
accepted, passing the result to a {{@NonNull}} parameter or returning it from a 
{{@NonNull}} method is an error, and in flow-sensitive mode 
({{NullChecker(strict: true)}}) the nullness is tracked through assignments to 
unannotated variables. It works whether the annotated type appears in source or 
is read from a compiled dependency, and — consistent with the checker's design 
— with {{@Nullable}} annotations from any library, matched by simple name.

Example:

{code:groovy}
import groovy.transform.TypeChecked
import org.jspecify.annotations.Nullable

@TypeChecked(extensions = 'groovy.typecheckers.NullChecker')
class Catalog {
    static int firstTitleLength(List<@Nullable String> titles) {
        titles.get(0).length()               // also flagged: titles[0].length()
    }
}
{code}

{noformat}
[Static type checking] - Potential null dereference: 'get()' may return null
{noformat}

Implementation note: the static type checker's generics inference preserves 
type-use annotations when instantiating class and method level type variables, 
so the check reads the {{@Nullable}} annotation directly off an expression's 
inferred type — no separate substitution machinery, and method-level type 
variables (the DGM/subscript cases) come along for free.

Not yet covered, left for a follow-up umbrella issue: nullable type parameters 
and bounds ({{<T extends @Nullable Object>}}), wildcards at use sites, 
{{@Nullable}} checking of arguments against instantiated parameter types (e.g. 
{{add(null)}} on a {{List<@NonNull String>}}), and array component positions.


> NullChecker: honour @Nullable type arguments reaching results via generics 
> (get(0), xs[0], head())
> --------------------------------------------------------------------------------------------------
>
>                 Key: GROOVY-12252
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12252
>             Project: Groovy
>          Issue Type: Improvement
>          Components: groovy-typecheckers
>            Reporter: Paul King
>            Assignee: Paul King
>            Priority: Major
>
> GROOVY-12206 and GROOVY-12207 made type-use nullability annotations available 
> from compiled dependencies, and the parser attaches them within source type 
> arguments (GROOVY-11178) — but the {{NullChecker}} type-checking extension 
> never looked _inside_ type arguments. Calling {{get(0)}} on a 
> {{List<@Nullable String>}} — JSpecify's headline capability, generics 
> nullness — passed silently.
> This issue teaches the checker to honour {{@Nullable}} type arguments 
> wherever the static type checker's generics inference carries them through a 
> *class or method level type variable*:
> * {{get(0)}} on a {{List<@Nullable String>}}, including subtype receivers 
> such as {{ArrayList<@Nullable String>}}
> * the idiomatic subscript {{xs\[0]}} and GDK calls like {{head()}} / 
> {{first()}}
> * {{get(key)}} and {{m\[key]}} on a {{Map<String, @Nullable Integer>}}
> The nullable result participates in the checker's existing analyses: 
> unguarded dereference is an error, safe navigation and the recognised guard 
> patterns are accepted, passing the result to a {{@NonNull}} parameter or 
> returning it from a {{@NonNull}} method is an error, and in flow-sensitive 
> mode ({{NullChecker(strict: true)}}) the nullness is tracked through 
> assignments to unannotated variables. It works whether the annotated type 
> appears in source or is read from a compiled dependency, and — consistent 
> with the checker's design — with {{@Nullable}} annotations from any library, 
> matched by simple name.
> Example:
> {code:groovy}
> import groovy.transform.TypeChecked
> import org.jspecify.annotations.Nullable
> @TypeChecked(extensions = 'groovy.typecheckers.NullChecker')
> class Catalog {
>     static int firstTitleLength(List<@Nullable String> titles) {
>         titles.get(0).length()               // also flagged: 
> titles[0].length()
>     }
> }
> {code}
> {noformat}
> [Static type checking] - Potential null dereference: 'get()' may return null
> {noformat}
> Implementation note: the static type checker's generics inference preserves 
> type-use annotations when instantiating class and method level type 
> variables, so the check reads the {{@Nullable}} annotation directly off an 
> expression's inferred type — no separate substitution machinery, and 
> method-level type variables (the DGM/subscript cases) come along for free.
> Not yet covered, left for a follow-up umbrella issue: nullable type 
> parameters and bounds ({{<T extends @Nullable Object>}}), wildcards at use 
> sites, {{@Nullable}} checking of arguments against instantiated parameter 
> types (e.g. {{add(null)}} on a {{List<@NonNull String>}}), and array 
> component positions.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to