This is an automated email from the ASF dual-hosted git repository.
paulk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/master by this push:
new 132bc66de2 GROOVY-11908: Support parameterized type checking extensions
132bc66de2 is described below
commit 132bc66de2559eff4fc2d458d4aae9770883b58f
Author: Paul King <[email protected]>
AuthorDate: Tue Apr 7 06:47:04 2026 +1000
GROOVY-11908: Support parameterized type checking extensions
---
.../stc/GroovyTypeCheckingExtensionSupport.java | 72 +++-
.../transform/stc/TypeCheckingExtension.java | 25 ++
src/spec/doc/_type-checking-extensions.adoc | 42 +++
.../groovy/groovy/typecheckers/NullChecker.groovy | 331 +++++++++++++++++-
.../groovy/typecheckers/StrictNullChecker.groovy | 61 ----
.../groovy/typecheckers/NullCheckingVisitor.groovy | 372 ---------------------
.../src/spec/doc/typecheckers.adoc | 16 +-
.../src/spec/test/NullCheckerTest.groovy | 4 +-
.../groovy/typecheckers/NullCheckerTest.groovy | 4 +-
9 files changed, 467 insertions(+), 460 deletions(-)
diff --git
a/src/main/java/org/codehaus/groovy/transform/stc/GroovyTypeCheckingExtensionSupport.java
b/src/main/java/org/codehaus/groovy/transform/stc/GroovyTypeCheckingExtensionSupport.java
index e837def17d..1aa2586137 100644
---
a/src/main/java/org/codehaus/groovy/transform/stc/GroovyTypeCheckingExtensionSupport.java
+++
b/src/main/java/org/codehaus/groovy/transform/stc/GroovyTypeCheckingExtensionSupport.java
@@ -49,6 +49,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
+import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@@ -85,6 +86,12 @@ public class GroovyTypeCheckingExtensionSupport extends
AbstractTypeCheckingExte
private final String scriptPath;
+ /** The class/script path with any parameter suffix removed. */
+ private final String resolvedPath;
+
+ /** Parameters parsed from the extension spec, e.g. {@code (strict:
true)}. */
+ private final Map<String, Object> extensionParameters;
+
private final CompilationUnit compilationUnit;
private TypeCheckingExtension delegateExtension;
@@ -94,9 +101,11 @@ public class GroovyTypeCheckingExtensionSupport extends
AbstractTypeCheckingExte
/**
* Builds a type checking extension relying on a Groovy script (type
checking DSL).
+ * The {@code scriptPath} may include parameters in Groovy named-argument
style, e.g.
+ * {@code "groovy.typecheckers.NullChecker(strict: true)"}.
*
* @param typeCheckingVisitor the type checking visitor
- * @param scriptPath the path to the type checking script (in classpath)
+ * @param scriptPath the path to the type checking script (in classpath),
optionally with parameters
* @param compilationUnit
*/
public GroovyTypeCheckingExtensionSupport(
@@ -105,6 +114,54 @@ public class GroovyTypeCheckingExtensionSupport extends
AbstractTypeCheckingExte
super(typeCheckingVisitor);
this.scriptPath = scriptPath;
this.compilationUnit = compilationUnit;
+ int parenIdx = scriptPath.indexOf('(');
+ if (parenIdx >= 0 && scriptPath.endsWith(")")) {
+ this.resolvedPath = scriptPath.substring(0, parenIdx).trim();
+ this.extensionParameters =
parseParameters(scriptPath.substring(parenIdx + 1, scriptPath.length() - 1));
+ } else {
+ this.resolvedPath = scriptPath;
+ this.extensionParameters = Collections.emptyMap();
+ }
+ }
+
+ /**
+ * Parses a parameter string of the form {@code "key1: value1, key2:
value2"} into a map.
+ * Supports boolean, integer, long, double, and string (quoted) values.
+ * String values cannot contain commas or colons since these are used as
delimiters.
+ */
+ private static Map<String, Object> parseParameters(final String paramStr) {
+ if (paramStr == null || paramStr.isBlank()) return
Collections.emptyMap();
+ Map<String, Object> result = new LinkedHashMap<>();
+ for (String pair : paramStr.split(",")) {
+ pair = pair.trim();
+ if (pair.isEmpty()) continue;
+ int colonIdx = pair.indexOf(':');
+ if (colonIdx < 0) continue;
+ String key = pair.substring(0, colonIdx).trim();
+ String value = pair.substring(colonIdx + 1).trim();
+ result.put(key, parseValue(value));
+ }
+ return Collections.unmodifiableMap(result);
+ }
+
+ private static Object parseValue(final String value) {
+ if ("true".equals(value)) return Boolean.TRUE;
+ if ("false".equals(value)) return Boolean.FALSE;
+ if ("null".equals(value)) return null;
+ if (value.length() >= 2
+ && ((value.startsWith("'") && value.endsWith("'"))
+ || (value.startsWith("\"") && value.endsWith("\"")))) {
+ return value.substring(1, value.length() - 1);
+ }
+ try { return Integer.valueOf(value); } catch (NumberFormatException
ignored) { }
+ try { return Long.valueOf(value); } catch (NumberFormatException
ignored) { }
+ try { return Double.valueOf(value); } catch (NumberFormatException
ignored) { }
+ return value;
+ }
+
+ @Override
+ public Map<String, Object> getOptions() {
+ return extensionParameters;
}
@Override
@@ -144,7 +201,7 @@ public class GroovyTypeCheckingExtensionSupport extends
AbstractTypeCheckingExte
// since Groovy 2.2, it is possible to use FQCN for type checking
extension scripts
TypeCheckingDSL script = null;
try {
- Class<?> clazz = transformLoader.loadClass(scriptPath, false,
true);
+ Class<?> clazz = transformLoader.loadClass(resolvedPath, false,
true);
if (TypeCheckingDSL.class.isAssignableFrom(clazz)) {
script = (TypeCheckingDSL)
clazz.getDeclaredConstructor().newInstance();
} else if (TypeCheckingExtension.class.isAssignableFrom(clazz)) {
@@ -152,6 +209,7 @@ public class GroovyTypeCheckingExtensionSupport extends
AbstractTypeCheckingExte
try {
Constructor<?> declaredConstructor =
clazz.getDeclaredConstructor(StaticTypeCheckingVisitor.class);
delegateExtension = (TypeCheckingExtension)
declaredConstructor.newInstance(typeCheckingVisitor);
+ delegateExtension.setOptions(extensionParameters);
typeCheckingVisitor.addTypeCheckingExtension(delegateExtension);
delegateExtension.setup();
return;
@@ -159,7 +217,7 @@ public class GroovyTypeCheckingExtensionSupport extends
AbstractTypeCheckingExte
addLoadingError(config);
} catch (NoSuchMethodException e) {
context.getErrorCollector().addFatalError(
- new SimpleMessage("Static type checking extension
'" + scriptPath + "' could not be loaded because it doesn't have a constructor
accepting StaticTypeCheckingVisitor.",
+ new SimpleMessage("Static type checking extension
'" + resolvedPath + "' could not be loaded because it doesn't have a
constructor accepting StaticTypeCheckingVisitor.",
config.getDebug(),
typeCheckingVisitor.getSourceUnit())
);
}
@@ -172,20 +230,20 @@ public class GroovyTypeCheckingExtensionSupport extends
AbstractTypeCheckingExte
if (script == null) {
ClassLoader cl =
typeCheckingVisitor.getSourceUnit().getClassLoader();
// cast to prevent incorrect @since 1.7 warning
- InputStream is =
((ClassLoader)transformLoader).getResourceAsStream(scriptPath);
+ InputStream is =
((ClassLoader)transformLoader).getResourceAsStream(resolvedPath);
if (is == null) {
// fallback to the source unit classloader
- is = cl.getResourceAsStream(scriptPath);
+ is = cl.getResourceAsStream(resolvedPath);
}
if (is == null) {
// fallback to the compiler classloader
cl = GroovyTypeCheckingExtensionSupport.class.getClassLoader();
- is = cl.getResourceAsStream(scriptPath);
+ is = cl.getResourceAsStream(resolvedPath);
}
if (is == null) {
// if the input stream is still null, we've not found the
extension
context.getErrorCollector().addFatalError(
- new SimpleMessage("Static type checking extension '" +
scriptPath + "' was not found on the classpath.",
+ new SimpleMessage("Static type checking extension '" +
resolvedPath + "' was not found on the classpath.",
config.getDebug(),
typeCheckingVisitor.getSourceUnit()));
}
try {
diff --git
a/src/main/java/org/codehaus/groovy/transform/stc/TypeCheckingExtension.java
b/src/main/java/org/codehaus/groovy/transform/stc/TypeCheckingExtension.java
index c662a08158..b408ae7eca 100644
--- a/src/main/java/org/codehaus/groovy/transform/stc/TypeCheckingExtension.java
+++ b/src/main/java/org/codehaus/groovy/transform/stc/TypeCheckingExtension.java
@@ -39,6 +39,7 @@ import org.codehaus.groovy.ast.stmt.ReturnStatement;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
/**
* This interface defines a high-level API for handling type checking errors.
As a dynamic language and a platform
@@ -55,10 +56,34 @@ public class TypeCheckingExtension {
protected final StaticTypeCheckingVisitor typeCheckingVisitor;
+ private Map<String, Object> options = Collections.emptyMap();
+
public TypeCheckingExtension(final StaticTypeCheckingVisitor
typeCheckingVisitor) {
this.typeCheckingVisitor = typeCheckingVisitor;
}
+ /**
+ * Returns the options passed to this extension via the parameterized
extension syntax,
+ * e.g. {@code @TypeChecked(extensions='my.Extension(key: value)')}.
+ *
+ * @return the options map, never null
+ * @since 6.0.0
+ */
+ public Map<String, Object> getOptions() {
+ return options;
+ }
+
+ /**
+ * Sets the options for this extension. Called by the framework when
loading
+ * a parameterized extension.
+ *
+ * @param options the options map
+ * @since 6.0.0
+ */
+ public void setOptions(final Map<String, Object> options) {
+ this.options = options != null ? options : Collections.emptyMap();
+ }
+
/**
* Subclasses should implement this method whenever they need to perform
* special checks before the type checker starts working.
diff --git a/src/spec/doc/_type-checking-extensions.adoc
b/src/spec/doc/_type-checking-extensions.adoc
index 334cecaeac..b496d68e20 100644
--- a/src/spec/doc/_type-checking-extensions.adoc
+++ b/src/spec/doc/_type-checking-extensions.adoc
@@ -112,6 +112,48 @@ checker supports multiple mechanisms to implement type
checking
extensions (including plain old java code), the recommended way is to
use those type checking extension scripts.
+==== Parameterized extensions
+
+Extensions can accept named parameters using Groovy's named-argument style
within the extension string:
+
+[source,groovy]
+------------------------------------------------------
+@TypeChecked(extensions = 'groovy.typecheckers.NullChecker(strict: true)')
+void foo() { ... }
+------------------------------------------------------
+
+Parameters are passed as `key: value` pairs inside parentheses appended to the
extension name.
+Supported value types include booleans (`true`/`false`), integers, strings
(single or double quoted), and `null`.
+Multiple parameters are separated by commas.
+Note that string values cannot contain commas or colons since these are used
as delimiters:
+
+[source,groovy]
+------------------------------------------------------
+@TypeChecked(extensions = 'com.example.MyChecker(strict: true, threshold: 5,
mode: "lenient")')
+void foo() { ... }
+------------------------------------------------------
+
+Within an extension, the parameters are available via the `options` property,
which returns an unmodifiable `Map<String, Object>`:
+
+[source,groovy]
+------------------------------------------------------
+// in a TypeCheckingDSL extension:
+Object run() {
+ boolean strict = options?.strict ?: false
+ // ...
+}
+------------------------------------------------------
+
+For precompiled Java extensions (subclasses of `TypeCheckingExtension` or
`AbstractTypeCheckingExtension`),
+options are accessible via the `getOptions()` method.
+
+The parameterized syntax also works when configuring extensions
programmatically:
+
+[source,groovy]
+------------------------------------------------------
+new ASTTransformationCustomizer(TypeChecked, extensions: ['my.Extension(debug:
true)'])
+------------------------------------------------------
+
=== A DSL for type checking
The idea behind type checking extensions is to use a DSL to extend the
type checker capabilities. This DSL allows you to hook into the
diff --git
a/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/NullChecker.groovy
b/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/NullChecker.groovy
index 18f77dcac8..2bb01234a9 100644
---
a/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/NullChecker.groovy
+++
b/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/NullChecker.groovy
@@ -19,16 +19,47 @@
package groovy.typecheckers
import org.apache.groovy.lang.annotation.Incubating
-import org.apache.groovy.typecheckers.NullCheckingVisitor
+import org.apache.groovy.typecheckers.CheckingVisitor
+import org.codehaus.groovy.ast.AnnotatedNode
+import org.codehaus.groovy.ast.FieldNode
+import org.codehaus.groovy.ast.MethodNode
+import org.codehaus.groovy.ast.Variable
+import org.codehaus.groovy.ast.expr.BinaryExpression
+import org.codehaus.groovy.ast.expr.CastExpression
+import org.codehaus.groovy.ast.expr.ConstantExpression
+import org.codehaus.groovy.ast.expr.DeclarationExpression
+import org.codehaus.groovy.ast.expr.EmptyExpression
+import org.codehaus.groovy.ast.expr.Expression
+import org.codehaus.groovy.ast.expr.MethodCallExpression
+import org.codehaus.groovy.ast.expr.PropertyExpression
+import org.codehaus.groovy.ast.expr.StaticMethodCallExpression
+import org.codehaus.groovy.ast.expr.TernaryExpression
+import org.codehaus.groovy.ast.expr.TupleExpression
+import org.codehaus.groovy.ast.expr.VariableExpression
+import org.codehaus.groovy.ast.stmt.BlockStatement
+import org.codehaus.groovy.ast.stmt.EmptyStatement
+import org.codehaus.groovy.ast.stmt.IfStatement
+import org.codehaus.groovy.ast.stmt.ReturnStatement
+import org.codehaus.groovy.ast.stmt.Statement
+import org.codehaus.groovy.ast.stmt.ThrowStatement
+import org.codehaus.groovy.syntax.Types
import org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport
+import org.codehaus.groovy.transform.stc.StaticTypesMarker
+
+import static org.codehaus.groovy.ast.ClassHelper.VOID_TYPE
+import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveType
+import static org.codehaus.groovy.syntax.Types.isAssignment
/**
* A compile-time type checker that detects potential null dereferences and
null-safety violations
* in code annotated with {@code @Nullable}, {@code @NonNull}, and {@code
@MonotonicNonNull} annotations.
* <p>
- * This checker performs annotation-based null checking only. For additional
flow-sensitive analysis
- * that tracks nullability through assignments and control flow (even in
unannotated code),
- * use {@link StrictNullChecker} instead.
+ * By default, this checker performs annotation-based null checking only. For
additional flow-sensitive
+ * analysis that tracks nullability through assignments and control flow (even
in unannotated code),
+ * enable the {@code strict} option:
+ * <pre>
+ * {@code @TypeChecked(extensions = 'groovy.typecheckers.NullChecker(strict:
true)')}
+ * </pre>
* <p>
* Supported annotations are recognized by simple name from any package:
* <ul>
@@ -44,6 +75,7 @@ import
org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport
* <li>Dereferencing a {@code @Nullable} variable without a null check or
safe navigation ({@code ?.})</li>
* <li>Dereferencing the result of a {@code @Nullable}-returning method
without a null check</li>
* <li>Re-assigning {@code null} to a {@code @MonotonicNonNull} field
after initialization</li>
+ * <li>Dereferencing a variable known to be null through flow analysis
({@code strict} mode only)</li>
* </ul>
* <p>
* The checker recognizes null guards ({@code if (x != null)}), early exit
patterns
@@ -62,15 +94,298 @@ import
org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport
*
* Over time, the idea would be to support more cases as per:
* https://checkerframework.org/manual/#nullness-checker
- *
- * @see StrictNullChecker
- * @see NullCheckingVisitor
*/
@Incubating
class NullChecker extends GroovyTypeCheckingExtensionSupport.TypeCheckingDSL {
+ private static final Set<String> NULLABLE_ANNOS = Set.of('Nullable',
'CheckForNull', 'MonotonicNonNull')
+ private static final Set<String> NONNULL_ANNOS = Set.of('NonNull',
'NotNull', 'Nonnull')
+ private static final Set<String> MONOTONIC_ANNOS =
Set.of('MonotonicNonNull', 'Lazy')
+ private static final Set<String> NULLCHECK_ANNOS = Set.of('NullCheck',
'ParametersAreNonnullByDefault', 'ParametersAreNonNullByDefault')
+ private static final Set<String> NONNULL_BY_DEFAULT_ANNOS =
Set.of('NonNullByDefault', 'NonnullByDefault', 'NullMarked')
+ private static final Set<String> NULL_UNMARKED_ANNOS =
Set.of('NullUnmarked')
+
@Override
Object run() {
- NullCheckingVisitor.install(this, false)
+ boolean strict = options?.strict ?: false
+ afterVisitMethod { MethodNode method ->
+ method.code?.visit(makeVisitor(strict, method))
+ }
+ }
+
+ private CheckingVisitor makeVisitor(boolean flowSensitive, MethodNode
method) {
+ boolean classNonNullByDefault = method.declaringClass != null &&
hasNonNullByDefaultAnno(method.declaringClass)
+ boolean methodNonNull = method.returnType != VOID_TYPE &&
(hasNonNullAnno(method) || (classNonNullByDefault && !hasNullableAnno(method)))
+ def initialNullable = method.parameters.findAll { hasNullableAnno(it)
} as Set<Variable>
+
+ new CheckingVisitor() {
+ private final Set<Variable> nullableVars = new
HashSet<>(initialNullable)
+ private final Set<Variable> monotonicInitialized = new HashSet<>()
+ private final Set<Variable> guardedVars = new HashSet<>()
+
+ @Override
+ void visitDeclarationExpression(DeclarationExpression decl) {
+ super.visitDeclarationExpression(decl)
+ def ve = decl.variableExpression
+ if (ve == null) return
+ if (decl.rightExpression instanceof ConstantExpression) {
+ localConstVars.put(ve, decl.rightExpression)
+ }
+ if (hasNonNullAnno(ve) && isNullExpr(decl.rightExpression)) {
+ addStaticTypeError("Cannot assign null to @NonNull
variable '${ve.name}'", decl)
+ }
+ // Uninitialized non-primitive declaration (e.g. "String x")
is implicitly null
+ boolean implicitlyNull = decl.rightExpression instanceof
EmptyExpression && !isPrimitiveType(ve.type)
+ if (hasNullableAnno(ve) || isNullExpr(decl.rightExpression) ||
(flowSensitive && (implicitlyNull || canBeNull(decl.rightExpression) ||
isKnownNullable(decl.rightExpression)))) {
+ nullableVars.add(ve)
+ }
+ if (flowSensitive) {
+ trackNullableReturn(ve, decl.rightExpression)
+ }
+ }
+
+ @Override
+ void visitBinaryExpression(BinaryExpression expression) {
+ super.visitBinaryExpression(expression)
+ if (isAssignment(expression.operation.type) &&
expression.leftExpression instanceof VariableExpression) {
+ def target = findTargetVariable(expression.leftExpression)
+ boolean fieldNonNull = target instanceof AnnotatedNode &&
hasNonNullAnno(target)
+ if (!fieldNonNull && target instanceof FieldNode &&
!hasNullableAnno(target)) {
+ fieldNonNull = target.declaringClass != null &&
hasNonNullByDefaultAnno(target.declaringClass)
+ }
+ if (fieldNonNull &&
isNullExpr(expression.rightExpression)) {
+ addStaticTypeError("Cannot assign null to @NonNull
variable '${expression.leftExpression.name}'", expression)
+ }
+ // @MonotonicNonNull: once initialized with non-null,
cannot assign null again
+ if (target instanceof AnnotatedNode &&
hasMonotonicAnno(target)) {
+ if (!isNullExpr(expression.rightExpression)) {
+ monotonicInitialized.add(target)
+ } else if (monotonicInitialized.contains(target)) {
+ addStaticTypeError("Cannot assign null to
@MonotonicNonNull variable '${expression.leftExpression.name}' after non-null
assignment", expression)
+ }
+ }
+ if (isNullExpr(expression.rightExpression)) {
+ nullableVars.add(target)
+ guardedVars.remove(target)
+ } else if (flowSensitive &&
(canBeNull(expression.rightExpression) ||
isKnownNullable(expression.rightExpression))) {
+ nullableVars.add(target)
+ guardedVars.remove(target)
+ } else {
+ nullableVars.remove(target)
+ if (flowSensitive) {
+ trackNullableReturn(target,
expression.rightExpression)
+ }
+ }
+ }
+ }
+
+ @Override
+ void visitMethodCallExpression(MethodCallExpression call) {
+ super.visitMethodCallExpression(call)
+ if (!call.safe && !call.implicitThis) {
+ checkDereference(call.objectExpression, call)
+ }
+ checkMethodArguments(call)
+ }
+
+ @Override
+ void visitStaticMethodCallExpression(StaticMethodCallExpression
call) {
+ super.visitStaticMethodCallExpression(call)
+ checkMethodArguments(call)
+ }
+
+ @Override
+ void visitPropertyExpression(PropertyExpression expression) {
+ super.visitPropertyExpression(expression)
+ if (!expression.safe) {
+ checkDereference(expression.objectExpression, expression)
+ }
+ }
+
+ @Override
+ void visitReturnStatement(ReturnStatement statement) {
+ super.visitReturnStatement(statement)
+ if (methodNonNull) {
+ if (isNullExpr(statement.expression)) {
+ addStaticTypeError("Cannot return null from @NonNull
method '${method.name}'", statement)
+ } else if (isKnownNullable(statement.expression)) {
+ addStaticTypeError("Cannot return @Nullable value from
@NonNull method '${method.name}'", statement)
+ }
+ }
+ }
+
+ @Override
+ void visitIfElse(IfStatement ifElse) {
+ ifElse.booleanExpression.visit(this)
+ def guard = findNullGuard(ifElse.booleanExpression.expression)
+ if (guard != null) {
+ handleNullGuard(ifElse, guard.v1, guard.v2)
+ } else {
+ ifElse.ifBlock.visit(this)
+ ifElse.elseBlock.visit(this)
+ }
+ }
+
+
//------------------------------------------------------------------
+
+ private void handleNullGuard(IfStatement ifElse, Variable
guardVar, boolean isNotNull) {
+ if (isNotNull) {
+ // if (x != null) { ... } else { ... }
+ def saved = new HashSet<>(guardedVars)
+ guardedVars.add(guardVar)
+ ifElse.ifBlock.visit(this)
+ guardedVars.clear()
+ guardedVars.addAll(saved)
+ ifElse.elseBlock.visit(this)
+ } else {
+ // if (x == null) { ... } else { ... }
+ ifElse.ifBlock.visit(this)
+ if (!(ifElse.elseBlock instanceof EmptyStatement)) {
+ def saved = new HashSet<>(guardedVars)
+ guardedVars.add(guardVar)
+ ifElse.elseBlock.visit(this)
+ guardedVars.clear()
+ guardedVars.addAll(saved)
+ }
+ // Early exit: if (x == null) return/throw → x is non-null
after
+ if (isEarlyExit(ifElse.ifBlock)) {
+ nullableVars.remove(guardVar)
+ guardedVars.add(guardVar)
+ }
+ }
+ }
+
+ private void checkDereference(Expression receiver, Expression
context) {
+ if (isNullExpr(receiver)) {
+ addStaticTypeError('Cannot dereference null', context)
+ return
+ }
+ if (receiver instanceof VariableExpression) {
+ if (receiver.isThisExpression() ||
receiver.isSuperExpression()) return
+ def target = findTargetVariable(receiver)
+ boolean isMonotonicAndInitialized = target instanceof
AnnotatedNode && hasMonotonicAnno(target) &&
monotonicInitialized.contains(target)
+ if (target instanceof AnnotatedNode &&
hasNullableAnno(target) && !guardedVars.contains(target) &&
!isMonotonicAndInitialized) {
+ addStaticTypeError("Potential null dereference:
'${receiver.name}' is @Nullable", context)
+ } else if (flowSensitive && nullableVars.contains(target)
&& !guardedVars.contains(target)) {
+ addStaticTypeError("Potential null dereference:
'${receiver.name}' may be null", context)
+ }
+ } else if (receiver instanceof MethodCallExpression ||
receiver instanceof StaticMethodCallExpression) {
+ def targetMethod =
receiver.getNodeMetaData(StaticTypesMarker.DIRECT_METHOD_CALL_TARGET)
+ if (targetMethod instanceof MethodNode &&
hasNullableAnno(targetMethod)) {
+ addStaticTypeError("Potential null dereference:
'${targetMethod.name}()' may return null", context)
+ }
+ }
+ }
+
+ private void checkMethodArguments(call) {
+ def target =
call.getNodeMetaData(StaticTypesMarker.DIRECT_METHOD_CALL_TARGET)
+ if (!(target instanceof MethodNode)) return
+ def args = call.arguments
+ if (!(args instanceof TupleExpression)) return
+ def params = target.parameters
+ // @NullCheck/@ParametersAreNonnullByDefault/@NonNullByDefault
on method or class makes non-primitive params effectively @NonNull
+ def declaringClass = target.declaringClass
+ boolean nullChecked = hasNullCheckAnno(target) ||
hasNonNullByDefaultAnno(target) ||
+ (declaringClass != null &&
(hasNullCheckAnno(declaringClass) || hasNonNullByDefaultAnno(declaringClass)))
+ int limit = Math.min(args.expressions.size(), params.length)
+ for (int i = 0; i < limit; i++) {
+ def arg = args.getExpression(i)
+ boolean paramIsNonNull = hasNonNullAnno(params[i]) ||
(nullChecked && !isPrimitiveType(params[i].type) && !hasNullableAnno(params[i]))
+ if (paramIsNonNull) {
+ if (isNullExpr(arg)) {
+ addStaticTypeError("Cannot pass null to @NonNull
parameter '${params[i].name}' of '${target.name}'", call)
+ } else if (isKnownNullable(arg)) {
+ addStaticTypeError("Cannot pass @Nullable value to
@NonNull parameter '${params[i].name}' of '${target.name}'", call)
+ }
+ }
+ }
+ }
+
+ private boolean isKnownNullable(Expression expr) {
+ if (expr instanceof VariableExpression) {
+ def target = findTargetVariable(expr)
+ if (target instanceof AnnotatedNode &&
hasNullableAnno(target)) return true
+ if (nullableVars.contains(target)) return true
+ }
+ false
+ }
+
+ private void trackNullableReturn(Variable variable, Expression
rhs) {
+ if (rhs instanceof MethodCallExpression || rhs instanceof
StaticMethodCallExpression) {
+ def target =
rhs.getNodeMetaData(StaticTypesMarker.DIRECT_METHOD_CALL_TARGET)
+ if (target instanceof MethodNode &&
hasNullableAnno(target)) {
+ nullableVars.add(variable)
+ }
+ }
+ }
+
+ private Tuple2<Variable, Boolean> findNullGuard(Expression
condition) {
+ if (condition instanceof BinaryExpression) {
+ def op = condition.operation.type
+ boolean isNotEqual = (op == Types.COMPARE_NOT_EQUAL || op
== Types.COMPARE_NOT_IDENTICAL)
+ boolean isEqual = (op == Types.COMPARE_EQUAL || op ==
Types.COMPARE_IDENTICAL)
+ if (isNotEqual || isEqual) {
+ Variable var = null
+ if (isNullExpr(condition.rightExpression) &&
condition.leftExpression instanceof VariableExpression) {
+ var = findTargetVariable(condition.leftExpression)
+ } else if (isNullExpr(condition.leftExpression) &&
condition.rightExpression instanceof VariableExpression) {
+ var = findTargetVariable(condition.rightExpression)
+ }
+ if (var != null) return new Tuple2<>(var, isNotEqual)
+ }
+ }
+ null
+ }
+ }
+ }
+
+
//--------------------------------------------------------------------------
+
+ private static boolean hasNullableAnno(AnnotatedNode node) {
+ node.annotations?.any { it.classNode?.nameWithoutPackage in
NULLABLE_ANNOS } ?: false
+ }
+
+ private static boolean hasNonNullAnno(AnnotatedNode node) {
+ node.annotations?.any { it.classNode?.nameWithoutPackage in
NONNULL_ANNOS } ?: false
+ }
+
+ private static boolean hasMonotonicAnno(AnnotatedNode node) {
+ node.annotations?.any { it.classNode?.nameWithoutPackage in
MONOTONIC_ANNOS } ?: false
+ }
+
+ private static boolean hasNullCheckAnno(AnnotatedNode node) {
+ node.annotations?.any { it.classNode?.nameWithoutPackage in
NULLCHECK_ANNOS } ?: false
+ }
+
+ private static boolean hasNonNullByDefaultAnno(AnnotatedNode node) {
+ if (hasNullUnmarkedAnno(node)) return false
+ node.annotations?.any { it.classNode?.nameWithoutPackage in
NONNULL_BY_DEFAULT_ANNOS } ?: false
+ }
+
+ private static boolean hasNullUnmarkedAnno(AnnotatedNode node) {
+ node.annotations?.any { it.classNode?.nameWithoutPackage in
NULL_UNMARKED_ANNOS } ?: false
+ }
+
+ private static boolean isNullExpr(Expression expr) {
+ if (expr instanceof ConstantExpression) return ((ConstantExpression)
expr).isNullExpression()
+ if (expr instanceof CastExpression) return
isNullExpr(((CastExpression) expr).expression)
+ false
+ }
+
+ private static boolean canBeNull(Expression expr) {
+ if (isNullExpr(expr)) return true
+ if (expr instanceof TernaryExpression) {
+ return canBeNull(expr.trueExpression) ||
canBeNull(expr.falseExpression)
+ }
+ false
+ }
+
+ private static boolean isEarlyExit(Statement stmt) {
+ if (stmt instanceof ReturnStatement || stmt instanceof ThrowStatement)
return true
+ if (stmt instanceof BlockStatement) {
+ def stmts = stmt.statements
+ return stmts && isEarlyExit(stmts.last())
+ }
+ false
}
}
diff --git
a/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/StrictNullChecker.groovy
b/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/StrictNullChecker.groovy
deleted file mode 100644
index 722bca5d12..0000000000
---
a/subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/StrictNullChecker.groovy
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package groovy.typecheckers
-
-import org.apache.groovy.lang.annotation.Incubating
-import org.apache.groovy.typecheckers.NullCheckingVisitor
-import org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport
-
-/**
- * A compile-time type checker that performs all the annotation-based null
checks of {@link NullChecker}
- * plus flow-sensitive null tracking for unannotated code.
- * <p>
- * In addition to the annotation-based checks, this checker tracks nullability
through variable
- * assignments and control flow. This means it can detect potential null
dereferences even when
- * code does not use {@code @Nullable}/{@code @NonNull} annotations:
- *
- * <pre>
- * {@code @TypeChecked(extensions = 'groovy.typecheckers.StrictNullChecker')}
- * void process() {
- * def x = null
- * // x.toString() // error: x may be null
- * x = 'hello'
- * x.toString() // ok: x reassigned non-null
- * }
- * </pre>
- * <p>
- * The flow-sensitive tracking also recognizes return values from {@code
@Nullable} methods
- * assigned to variables, null guards, and early exit patterns.
- * <p>
- * Use this checker for code that benefits from stricter null analysis. For
code bases
- * with a mix of strictness requirements, apply {@code NullChecker} to relaxed
code and
- * {@code StrictNullChecker} to strict code via per-class or per-method
- * {@code @TypeChecked} annotations.
- *
- * @see NullChecker
- * @see NullCheckingVisitor
- */
-@Incubating
-class StrictNullChecker extends
GroovyTypeCheckingExtensionSupport.TypeCheckingDSL {
-
- @Override
- Object run() {
- NullCheckingVisitor.install(this, true)
- }
-}
diff --git
a/subprojects/groovy-typecheckers/src/main/groovy/org/apache/groovy/typecheckers/NullCheckingVisitor.groovy
b/subprojects/groovy-typecheckers/src/main/groovy/org/apache/groovy/typecheckers/NullCheckingVisitor.groovy
deleted file mode 100644
index 625887855a..0000000000
---
a/subprojects/groovy-typecheckers/src/main/groovy/org/apache/groovy/typecheckers/NullCheckingVisitor.groovy
+++ /dev/null
@@ -1,372 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.groovy.typecheckers
-
-import groovy.lang.Tuple2
-import org.codehaus.groovy.ast.AnnotatedNode
-import org.codehaus.groovy.ast.FieldNode
-import org.codehaus.groovy.ast.MethodNode
-import org.codehaus.groovy.ast.Variable
-import org.codehaus.groovy.ast.expr.BinaryExpression
-import org.codehaus.groovy.ast.expr.ConstantExpression
-import org.codehaus.groovy.ast.expr.DeclarationExpression
-import org.codehaus.groovy.ast.expr.CastExpression
-import org.codehaus.groovy.ast.expr.EmptyExpression
-import org.codehaus.groovy.ast.expr.Expression
-import org.codehaus.groovy.ast.expr.MethodCallExpression
-import org.codehaus.groovy.ast.expr.PropertyExpression
-import org.codehaus.groovy.ast.expr.StaticMethodCallExpression
-import org.codehaus.groovy.ast.expr.TernaryExpression
-import org.codehaus.groovy.ast.expr.TupleExpression
-import org.codehaus.groovy.ast.expr.VariableExpression
-import org.codehaus.groovy.ast.stmt.BlockStatement
-import org.codehaus.groovy.ast.stmt.EmptyStatement
-import org.codehaus.groovy.ast.stmt.IfStatement
-import org.codehaus.groovy.ast.stmt.ReturnStatement
-import org.codehaus.groovy.ast.stmt.Statement
-import org.codehaus.groovy.ast.stmt.ThrowStatement
-import org.codehaus.groovy.syntax.Types
-import org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport
-import org.codehaus.groovy.transform.stc.StaticTypesMarker
-
-import static org.codehaus.groovy.ast.ClassHelper.VOID_TYPE
-import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveType
-import static org.codehaus.groovy.syntax.Types.isAssignment
-
-/**
- * Shared visitor implementation for null-safety type checking.
- * Used by both {@code NullChecker} (annotation-only) and {@code
StrictNullChecker} (annotation + flow-sensitive).
- *
- * @see groovy.typecheckers.NullChecker
- * @see groovy.typecheckers.StrictNullChecker
- */
-class NullCheckingVisitor extends CheckingVisitor {
-
- private static final Set<String> NULLABLE_ANNOS = Set.of('Nullable',
'CheckForNull', 'MonotonicNonNull')
- private static final Set<String> NONNULL_ANNOS = Set.of('NonNull',
'NotNull', 'Nonnull')
- private static final Set<String> MONOTONIC_ANNOS =
Set.of('MonotonicNonNull', 'Lazy')
- private static final Set<String> NULLCHECK_ANNOS = Set.of('NullCheck',
'ParametersAreNonnullByDefault', 'ParametersAreNonNullByDefault')
- private static final Set<String> NONNULL_BY_DEFAULT_ANNOS =
Set.of('NonNullByDefault', 'NonnullByDefault', 'NullMarked')
- private static final Set<String> NULL_UNMARKED_ANNOS =
Set.of('NullUnmarked')
-
- private final boolean flowSensitive
- private final Closure errorReporter
- private final MethodNode method
- private final boolean methodNonNull
- private final Set<Variable> nullableVars = new HashSet<>()
- private final Set<Variable> monotonicInitialized = new HashSet<>()
- private final Set<Variable> guardedVars = new HashSet<>()
-
- NullCheckingVisitor(boolean flowSensitive, MethodNode method, Closure
errorReporter) {
- this.flowSensitive = flowSensitive
- this.errorReporter = errorReporter
- this.method = method
- boolean classNonNullByDefault = method.declaringClass != null &&
hasNonNullByDefaultAnno(method.declaringClass)
- this.methodNonNull = method.returnType != VOID_TYPE &&
(hasNonNullAnno(method) || (classNonNullByDefault && !hasNullableAnno(method)))
- for (param in method.parameters) {
- if (hasNullableAnno(param)) {
- nullableVars.add(param)
- }
- }
- }
-
- /**
- * Installs the null-checking visitor into a type checking DSL.
- * Called from the DSL's {@code run()} method to set up the {@code
afterVisitMethod} callback.
- *
- * @param dsl the type checking DSL instance
- * @param flowSensitive whether to enable flow-sensitive null tracking
- */
- static void install(GroovyTypeCheckingExtensionSupport.TypeCheckingDSL
dsl, boolean flowSensitive) {
- dsl.afterVisitMethod { MethodNode method ->
- def reporter = { String msg, node -> dsl.addStaticTypeError(msg,
node) }
- method.code?.visit(new NullCheckingVisitor(flowSensitive, method,
reporter))
- }
- }
-
-
//--------------------------------------------------------------------------
-
- @Override
- void visitDeclarationExpression(DeclarationExpression decl) {
- super.visitDeclarationExpression(decl)
- def ve = decl.variableExpression
- if (ve == null) return
- if (decl.rightExpression instanceof ConstantExpression) {
- localConstVars.put(ve, decl.rightExpression)
- }
- if (hasNonNullAnno(ve) && isNullExpr(decl.rightExpression)) {
- reportError("Cannot assign null to @NonNull variable
'${ve.name}'", decl)
- }
- // Uninitialized non-primitive declaration (e.g. "String x") is
implicitly null
- boolean implicitlyNull = decl.rightExpression instanceof
EmptyExpression && !isPrimitiveType(ve.type)
- if (hasNullableAnno(ve) || isNullExpr(decl.rightExpression) ||
(flowSensitive && (implicitlyNull || canBeNull(decl.rightExpression) ||
isKnownNullable(decl.rightExpression)))) {
- nullableVars.add(ve)
- }
- if (flowSensitive) {
- trackNullableReturn(ve, decl.rightExpression)
- }
- }
-
- @Override
- void visitBinaryExpression(BinaryExpression expression) {
- super.visitBinaryExpression(expression)
- if (isAssignment(expression.operation.type) &&
expression.leftExpression instanceof VariableExpression) {
- def target = findTargetVariable(expression.leftExpression)
- boolean fieldNonNull = target instanceof AnnotatedNode &&
hasNonNullAnno(target)
- if (!fieldNonNull && target instanceof FieldNode &&
!hasNullableAnno(target)) {
- fieldNonNull = target.declaringClass != null &&
hasNonNullByDefaultAnno(target.declaringClass)
- }
- if (fieldNonNull && isNullExpr(expression.rightExpression)) {
- reportError("Cannot assign null to @NonNull variable
'${expression.leftExpression.name}'", expression)
- }
- // @MonotonicNonNull: once initialized with non-null, cannot
assign null again
- if (target instanceof AnnotatedNode && hasMonotonicAnno(target)) {
- if (!isNullExpr(expression.rightExpression)) {
- monotonicInitialized.add(target)
- } else if (monotonicInitialized.contains(target)) {
- reportError("Cannot assign null to @MonotonicNonNull
variable '${expression.leftExpression.name}' after non-null assignment",
expression)
- }
- }
- if (isNullExpr(expression.rightExpression)) {
- nullableVars.add(target)
- guardedVars.remove(target)
- } else if (flowSensitive && (canBeNull(expression.rightExpression)
|| isKnownNullable(expression.rightExpression))) {
- nullableVars.add(target)
- guardedVars.remove(target)
- } else {
- nullableVars.remove(target)
- if (flowSensitive) {
- trackNullableReturn(target, expression.rightExpression)
- }
- }
- }
- }
-
- @Override
- void visitMethodCallExpression(MethodCallExpression call) {
- super.visitMethodCallExpression(call)
- if (!call.safe && !call.implicitThis) {
- checkDereference(call.objectExpression, call)
- }
- checkMethodArguments(call)
- }
-
- @Override
- void visitStaticMethodCallExpression(StaticMethodCallExpression call) {
- super.visitStaticMethodCallExpression(call)
- checkMethodArguments(call)
- }
-
- @Override
- void visitPropertyExpression(PropertyExpression expression) {
- super.visitPropertyExpression(expression)
- if (!expression.safe) {
- checkDereference(expression.objectExpression, expression)
- }
- }
-
- @Override
- void visitReturnStatement(ReturnStatement statement) {
- super.visitReturnStatement(statement)
- if (methodNonNull) {
- if (isNullExpr(statement.expression)) {
- reportError("Cannot return null from @NonNull method
'${method.name}'", statement)
- } else if (isKnownNullable(statement.expression)) {
- reportError("Cannot return @Nullable value from @NonNull
method '${method.name}'", statement)
- }
- }
- }
-
- @Override
- void visitIfElse(IfStatement ifElse) {
- ifElse.booleanExpression.visit(this)
- def guard = findNullGuard(ifElse.booleanExpression.expression)
- if (guard != null) {
- handleNullGuard(ifElse, guard.v1, guard.v2)
- } else {
- ifElse.ifBlock.visit(this)
- ifElse.elseBlock.visit(this)
- }
- }
-
-
//--------------------------------------------------------------------------
-
- private void handleNullGuard(IfStatement ifElse, Variable guardVar,
boolean isNotNull) {
- if (isNotNull) {
- // if (x != null) { ... } else { ... }
- def saved = new HashSet<>(guardedVars)
- guardedVars.add(guardVar)
- ifElse.ifBlock.visit(this)
- guardedVars.clear()
- guardedVars.addAll(saved)
- ifElse.elseBlock.visit(this)
- } else {
- // if (x == null) { ... } else { ... }
- ifElse.ifBlock.visit(this)
- if (!(ifElse.elseBlock instanceof EmptyStatement)) {
- def saved = new HashSet<>(guardedVars)
- guardedVars.add(guardVar)
- ifElse.elseBlock.visit(this)
- guardedVars.clear()
- guardedVars.addAll(saved)
- }
- // Early exit: if (x == null) return/throw → x is non-null after
- if (isEarlyExit(ifElse.ifBlock)) {
- nullableVars.remove(guardVar)
- guardedVars.add(guardVar)
- }
- }
- }
-
- private void checkDereference(Expression receiver, Expression context) {
- if (isNullExpr(receiver)) {
- reportError('Cannot dereference null', context)
- return
- }
- if (receiver instanceof VariableExpression) {
- if (receiver.isThisExpression() || receiver.isSuperExpression())
return
- def target = findTargetVariable(receiver)
- boolean isMonotonicAndInitialized = target instanceof
AnnotatedNode && hasMonotonicAnno(target) &&
monotonicInitialized.contains(target)
- if (target instanceof AnnotatedNode && hasNullableAnno(target) &&
!guardedVars.contains(target) && !isMonotonicAndInitialized) {
- reportError("Potential null dereference: '${receiver.name}' is
@Nullable", context)
- } else if (flowSensitive && nullableVars.contains(target) &&
!guardedVars.contains(target)) {
- reportError("Potential null dereference: '${receiver.name}'
may be null", context)
- }
- } else if (receiver instanceof MethodCallExpression || receiver
instanceof StaticMethodCallExpression) {
- def targetMethod =
receiver.getNodeMetaData(StaticTypesMarker.DIRECT_METHOD_CALL_TARGET)
- if (targetMethod instanceof MethodNode &&
hasNullableAnno(targetMethod)) {
- reportError("Potential null dereference:
'${targetMethod.name}()' may return null", context)
- }
- }
- }
-
- private void checkMethodArguments(call) {
- def target =
call.getNodeMetaData(StaticTypesMarker.DIRECT_METHOD_CALL_TARGET)
- if (!(target instanceof MethodNode)) return
- def args = call.arguments
- if (!(args instanceof TupleExpression)) return
- def params = target.parameters
- // @NullCheck/@ParametersAreNonnullByDefault/@NonNullByDefault on
method or class makes non-primitive params effectively @NonNull
- def declaringClass = target.declaringClass
- boolean nullChecked = hasNullCheckAnno(target) ||
hasNonNullByDefaultAnno(target) ||
- (declaringClass != null && (hasNullCheckAnno(declaringClass) ||
hasNonNullByDefaultAnno(declaringClass)))
- int limit = Math.min(args.expressions.size(), params.length)
- for (int i = 0; i < limit; i++) {
- def arg = args.getExpression(i)
- boolean paramIsNonNull = hasNonNullAnno(params[i]) || (nullChecked
&& !isPrimitiveType(params[i].type) && !hasNullableAnno(params[i]))
- if (paramIsNonNull) {
- if (isNullExpr(arg)) {
- reportError("Cannot pass null to @NonNull parameter
'${params[i].name}' of '${target.name}'", call)
- } else if (isKnownNullable(arg)) {
- reportError("Cannot pass @Nullable value to @NonNull
parameter '${params[i].name}' of '${target.name}'", call)
- }
- }
- }
- }
-
- private boolean isKnownNullable(Expression expr) {
- if (expr instanceof VariableExpression) {
- def target = findTargetVariable(expr)
- if (target instanceof AnnotatedNode && hasNullableAnno(target))
return true
- if (nullableVars.contains(target)) return true
- }
- false
- }
-
- private void trackNullableReturn(Variable variable, Expression rhs) {
- if (rhs instanceof MethodCallExpression || rhs instanceof
StaticMethodCallExpression) {
- def target =
rhs.getNodeMetaData(StaticTypesMarker.DIRECT_METHOD_CALL_TARGET)
- if (target instanceof MethodNode && hasNullableAnno(target)) {
- nullableVars.add(variable)
- }
- }
- }
-
- private Tuple2<Variable, Boolean> findNullGuard(Expression condition) {
- if (condition instanceof BinaryExpression) {
- def op = condition.operation.type
- boolean isNotEqual = (op == Types.COMPARE_NOT_EQUAL || op ==
Types.COMPARE_NOT_IDENTICAL)
- boolean isEqual = (op == Types.COMPARE_EQUAL || op ==
Types.COMPARE_IDENTICAL)
- if (isNotEqual || isEqual) {
- Variable var = null
- if (isNullExpr(condition.rightExpression) &&
condition.leftExpression instanceof VariableExpression) {
- var = findTargetVariable(condition.leftExpression)
- } else if (isNullExpr(condition.leftExpression) &&
condition.rightExpression instanceof VariableExpression) {
- var = findTargetVariable(condition.rightExpression)
- }
- if (var != null) return new Tuple2<>(var, isNotEqual)
- }
- }
- null
- }
-
- private void reportError(String msg, node) {
- errorReporter.call(msg, node)
- }
-
-
//--------------------------------------------------------------------------
-
- static boolean hasNullableAnno(AnnotatedNode node) {
- node.annotations?.any { it.classNode?.nameWithoutPackage in
NULLABLE_ANNOS } ?: false
- }
-
- static boolean hasNonNullAnno(AnnotatedNode node) {
- node.annotations?.any { it.classNode?.nameWithoutPackage in
NONNULL_ANNOS } ?: false
- }
-
- static boolean hasMonotonicAnno(AnnotatedNode node) {
- node.annotations?.any { it.classNode?.nameWithoutPackage in
MONOTONIC_ANNOS } ?: false
- }
-
- static boolean hasNullCheckAnno(AnnotatedNode node) {
- node.annotations?.any { it.classNode?.nameWithoutPackage in
NULLCHECK_ANNOS } ?: false
- }
-
- static boolean hasNonNullByDefaultAnno(AnnotatedNode node) {
- if (hasNullUnmarkedAnno(node)) return false
- node.annotations?.any { it.classNode?.nameWithoutPackage in
NONNULL_BY_DEFAULT_ANNOS } ?: false
- }
-
- static boolean hasNullUnmarkedAnno(AnnotatedNode node) {
- node.annotations?.any { it.classNode?.nameWithoutPackage in
NULL_UNMARKED_ANNOS } ?: false
- }
-
- static boolean isNullExpr(Expression expr) {
- if (expr instanceof ConstantExpression) return ((ConstantExpression)
expr).isNullExpression()
- if (expr instanceof CastExpression) return
isNullExpr(((CastExpression) expr).expression)
- false
- }
-
- static boolean canBeNull(Expression expr) {
- if (isNullExpr(expr)) return true
- if (expr instanceof TernaryExpression) {
- return canBeNull(expr.trueExpression) ||
canBeNull(expr.falseExpression)
- }
- false
- }
-
- static boolean isEarlyExit(Statement stmt) {
- if (stmt instanceof ReturnStatement || stmt instanceof ThrowStatement)
return true
- if (stmt instanceof BlockStatement) {
- def stmts = stmt.statements
- return stmts && isEarlyExit(stmts.last())
- }
- false
- }
-}
diff --git a/subprojects/groovy-typecheckers/src/spec/doc/typecheckers.adoc
b/subprojects/groovy-typecheckers/src/spec/doc/typecheckers.adoc
index f33d36ddd4..30791d7b2e 100644
--- a/subprojects/groovy-typecheckers/src/spec/doc/typecheckers.adoc
+++ b/subprojects/groovy-typecheckers/src/spec/doc/typecheckers.adoc
@@ -375,16 +375,16 @@ Null pointer dereferences are one of the most common
sources of runtime errors.
The `NullChecker` performs compile-time null-safety analysis, detecting
potential
null dereferences and null-safety violations before code is executed.
-Two checkers are provided:
+Two modes are provided:
* `NullChecker` — *Annotation-based*: Checks code annotated with `@Nullable`
and
`@NonNull` (or equivalent) annotations.
-* `StrictNullChecker` — *Annotation-based + flow-sensitive*: Includes all
checks from
+* `NullChecker(strict: true)` — *Annotation-based + flow-sensitive*: Includes
all checks from
`NullChecker` plus flow-sensitive tracking through assignments and control
flow,
detecting potential null dereferences even in unannotated code.
For code bases with a mix of strictness requirements, apply `NullChecker` to
relaxed
-code and `StrictNullChecker` to strict code via per-class or per-method
`@TypeChecked` annotations.
+code and `NullChecker(strict: true)` to strict code via per-class or
per-method `@TypeChecked` annotations.
=== Typical usage
@@ -471,7 +471,7 @@
include::../test/NullCheckerTest.groovy[tags=null_guard,indent=0]
include::../test/NullCheckerTest.groovy[tags=early_return,indent=0]
----
-*Ternary and elvis expressions*: in flow-sensitive mode (`StrictNullChecker`),
the checker
+*Ternary and elvis expressions*: in flow-sensitive mode (`NullChecker(strict:
true)`), the checker
examines both branches of ternary (`condition ? a : b`) and elvis (`x ?:
fallback`) expressions.
If either branch can be null, the result is treated as nullable. The elvis
assignment
`x ?= nonNullValue` clears any nullable state on `x`.
@@ -626,13 +626,13 @@ We would see the following error at compile-time:
include::../test/NullCheckerTest.groovy[tags=nonnull_by_default_message,indent=0]
----
-=== Flow-sensitive mode with StrictNullChecker
+=== Flow-sensitive mode with strict option
-`NullChecker` only flags issues involving annotated code. `StrictNullChecker`
adds
+`NullChecker` only flags issues involving annotated code. `NullChecker(strict:
true)` adds
flow-sensitive tracking, detecting potential null dereferences in unannotated
code
by tracking nullability through variable assignments and control flow:
-With `StrictNullChecker`, the following code would be flagged:
+With `NullChecker(strict: true)`, the following code would be flagged:
[source,groovy]
----
@@ -661,7 +661,7 @@ The complete list of errors detected include:
* dereferencing a `@Nullable` variable without a null check or safe navigation
* dereferencing the return value of a `@Nullable` method without a null check
* re-assigning `null` to a `@MonotonicNonNull` or `@Lazy` field after
initialization
-* dereferencing a variable known to be null through flow analysis
(`StrictNullChecker` only)
+* dereferencing a variable known to be null through flow analysis (`strict`
mode only)
NOTE: For complementary null-related checks such as detecting broken
null-check logic,
unnecessary null guards before `instanceof`, or `Boolean` methods returning
`null`,
diff --git
a/subprojects/groovy-typecheckers/src/spec/test/NullCheckerTest.groovy
b/subprojects/groovy-typecheckers/src/spec/test/NullCheckerTest.groovy
index 59c5c00d80..5d3dae7629 100644
--- a/subprojects/groovy-typecheckers/src/spec/test/NullCheckerTest.groovy
+++ b/subprojects/groovy-typecheckers/src/spec/test/NullCheckerTest.groovy
@@ -384,7 +384,7 @@ class NullCheckerTest {
def err = shouldFail('''
import groovy.transform.TypeChecked
- @TypeChecked(extensions='groovy.typecheckers.StrictNullChecker')
+ @TypeChecked(extensions='groovy.typecheckers.NullChecker(strict:
true)')
static main(args) {
// tag::flow_sensitive[]
def x = null
@@ -406,7 +406,7 @@ class NullCheckerTest {
import groovy.transform.TypeChecked
// tag::flow_reassign[]
- @TypeChecked(extensions='groovy.typecheckers.StrictNullChecker')
+ @TypeChecked(extensions='groovy.typecheckers.NullChecker(strict:
true)')
static main(args) {
def x = null
x = 'hello'
diff --git
a/subprojects/groovy-typecheckers/src/test/groovy/groovy/typecheckers/NullCheckerTest.groovy
b/subprojects/groovy-typecheckers/src/test/groovy/groovy/typecheckers/NullCheckerTest.groovy
index 9a2ed0c5c6..ecace95c67 100644
---
a/subprojects/groovy-typecheckers/src/test/groovy/groovy/typecheckers/NullCheckerTest.groovy
+++
b/subprojects/groovy-typecheckers/src/test/groovy/groovy/typecheckers/NullCheckerTest.groovy
@@ -53,7 +53,7 @@ final class NullCheckerTest {
})
strictShell = new GroovyShell(new CompilerConfiguration().tap {
def customizer = new
ASTTransformationCustomizer(groovy.transform.TypeChecked)
- customizer.annotationParameters = [extensions:
'groovy.typecheckers.StrictNullChecker']
+ customizer.annotationParameters = [extensions:
'groovy.typecheckers.NullChecker(strict: true)']
addCompilationCustomizers(customizer)
})
}
@@ -804,7 +804,7 @@ final class NullCheckerTest {
assert err.message.contains("Cannot assign null to @MonotonicNonNull
variable 'name' after non-null assignment")
}
- // === StrictNullChecker: flow-sensitive checks ===
+ // === NullChecker(strict: true): flow-sensitive checks ===
@Test
void testStrictFlowNullReassignInsideGuard() {