NullabilityGuard.java
/*******************************************************************************
* Copyright (c) 2026 Carsten Hammer.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Carsten Hammer
*******************************************************************************/
package org.sandbox.jdt.triggerpattern.nullability;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.ConditionalExpression;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.IAnnotationBinding;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.IfStatement;
import org.eclipse.jdt.core.dom.InfixExpression;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.NullLiteral;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.ThisExpression;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
/**
* Analyzes the nullability of expressions in Java AST using a four-stage model:
*
* <ol>
* <li><b>Stage 1</b>: Type-based whitelist (fast, cheap)</li>
* <li><b>Stage 2</b>: Initialization analysis (local data flow)</li>
* <li><b>Stage 3</b>: Contextual null-check analysis (SpotBugs-style)</li>
* <li><b>Stage 4</b>: Annotation-based checking ({@code @Nullable}/{@code @NonNull})</li>
* </ol>
*
* @since 1.2.6
*/
public class NullabilityGuard {
private static final String PROPERTIES_PATH = "org/sandbox/jdt/triggerpattern/nullability/non-null-types.properties"; //$NON-NLS-1$
/** Known non-null factory methods (class -> set of method names). */
private static final Map<String, Set<String>> NON_NULL_FACTORY_METHODS = Map.of(
"java.nio.file.Paths", Set.of("get"), //$NON-NLS-1$ //$NON-NLS-2$
"java.nio.file.Path", Set.of("of"), //$NON-NLS-1$ //$NON-NLS-2$
"java.util.List", Set.of("of", "copyOf"), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
"java.util.Set", Set.of("of", "copyOf"), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
"java.util.Map", Set.of("of", "copyOf", "ofEntries", "entry"), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
"java.util.Objects", Set.of("requireNonNull", "requireNonNullElse", "requireNonNullElseGet"), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
"java.util.Collections", Set.of("unmodifiableList", "unmodifiableSet", "unmodifiableMap", "emptyList", "emptySet", "emptyMap", "singletonList", "singleton", "singletonMap"), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$ //$NON-NLS-9$ //$NON-NLS-10$
"java.lang.String", Set.of("valueOf", "format", "join") //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
);
/** Nullable annotation simple names. */
private static final Set<String> NULLABLE_ANNOTATIONS = Set.of(
"Nullable", "CheckForNull"); //$NON-NLS-1$ //$NON-NLS-2$
/** Non-null annotation simple names. */
private static final Set<String> NON_NULL_ANNOTATIONS = Set.of(
"NonNull", "Nonnull", "NotNull"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
private final Map<String, String> nonNullTypes;
/**
* Creates a guard using the bundled whitelist.
*/
public NullabilityGuard() {
this.nonNullTypes = loadWhitelist();
}
/**
* Creates a guard using the given whitelist.
*
* @param nonNullTypes map of fully-qualified type names to reasons
*/
public NullabilityGuard(Map<String, String> nonNullTypes) {
this.nonNullTypes = new ConcurrentHashMap<>(nonNullTypes);
}
/**
* Analyzes the nullability of the given expression AST node.
*
* @param expression the expression to analyze (typically the receiver of a
* method call such as {@code x.toString()})
* @return a result containing the determined status, reason, and evidence
*/
public NullabilityResult analyze(Expression expression) {
if (expression == null) {
return new NullabilityResult(NullStatus.UNKNOWN, "null expression", List.of()); //$NON-NLS-1$
}
// Stage 1: Type-based whitelist
NullabilityResult stage1 = analyzeByType(expression);
if (stage1.status() != NullStatus.UNKNOWN) {
return stage1;
}
// Stage 2: Initialization analysis
NullabilityResult stage2 = analyzeByInitialization(expression);
if (stage2.status() != NullStatus.UNKNOWN) {
return stage2;
}
// Stage 3: Contextual null-check analysis
NullabilityResult stage3 = analyzeByNullChecks(expression);
if (stage3.status() != NullStatus.UNKNOWN) {
return stage3;
}
// Stage 4: Annotation-based
NullabilityResult stage4 = analyzeByAnnotations(expression);
if (stage4.status() != NullStatus.UNKNOWN) {
return stage4;
}
return new NullabilityResult(NullStatus.UNKNOWN,
"nullability could not be determined", List.of()); //$NON-NLS-1$
}
// ---- Stage 1: Type-based whitelist ----
NullabilityResult analyzeByType(Expression expression) {
// this expression is always non-null
if (expression instanceof ThisExpression) {
return new NullabilityResult(NullStatus.NON_NULL,
"'this' reference is always non-null", List.of()); //$NON-NLS-1$
}
ITypeBinding typeBinding = expression.resolveTypeBinding();
if (typeBinding != null) {
// Primitive types are never null
if (typeBinding.isPrimitive()) {
return new NullabilityResult(NullStatus.NON_NULL,
"primitive type is never null", List.of()); //$NON-NLS-1$
}
// Enum types are never null (when referenced as constants)
if (typeBinding.isEnum()) {
return new NullabilityResult(NullStatus.NON_NULL,
"enum type is never null", List.of()); //$NON-NLS-1$
}
// Check whitelist
String qualifiedName = typeBinding.getQualifiedName();
String reason = nonNullTypes.get(qualifiedName);
if (reason != null) {
return new NullabilityResult(NullStatus.NON_NULL,
"type '" + qualifiedName + "' is in non-null whitelist (" + reason + ")", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
List.of());
}
// Check if supertype is in whitelist
ITypeBinding superclass = typeBinding.getSuperclass();
while (superclass != null) {
String superName = superclass.getQualifiedName();
String superReason = nonNullTypes.get(superName);
if (superReason != null) {
return new NullabilityResult(NullStatus.NON_NULL,
"supertype '" + superName + "' is in non-null whitelist (" + superReason + ")", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
List.of());
}
superclass = superclass.getSuperclass();
}
}
return NullabilityResult.UNKNOWN;
}
// ---- Stage 2: Initialization analysis ----
NullabilityResult analyzeByInitialization(Expression expression) {
// new Constructor() is always non-null
if (expression instanceof ClassInstanceCreation) {
return new NullabilityResult(NullStatus.NON_NULL,
"'new' expression is always non-null", List.of()); //$NON-NLS-1$
}
// Check if expression is a method invocation on a known non-null factory
if (expression instanceof MethodInvocation mi) {
return analyzeMethodInvocation(mi);
}
// Check if it's a variable with a known initializer
if (expression instanceof SimpleName name) {
return analyzeVariableInitializer(name);
}
return NullabilityResult.UNKNOWN;
}
private NullabilityResult analyzeMethodInvocation(MethodInvocation mi) {
IMethodBinding methodBinding = mi.resolveMethodBinding();
if (methodBinding != null) {
ITypeBinding declaringClass = methodBinding.getDeclaringClass();
if (declaringClass != null) {
String className = declaringClass.getQualifiedName();
String methodName = methodBinding.getName();
Set<String> methods = NON_NULL_FACTORY_METHODS.get(className);
if (methods != null && methods.contains(methodName)) {
return new NullabilityResult(NullStatus.NON_NULL,
className + "." + methodName + "() is a known non-null factory method", //$NON-NLS-1$ //$NON-NLS-2$
List.of());
}
}
}
// AST-Getter on parsed nodes: non-null (structural children)
Expression receiver = mi.getExpression();
if (receiver != null) {
ITypeBinding receiverType = receiver.resolveTypeBinding();
if (receiverType != null) {
String receiverTypeName = receiverType.getQualifiedName();
String reason = nonNullTypes.get(receiverTypeName);
if (reason != null && "structural_child".equals(reason)) { //$NON-NLS-1$
String methodName = mi.getName().getIdentifier();
if (methodName.startsWith("get")) { //$NON-NLS-1$
return new NullabilityResult(NullStatus.NON_NULL,
"getter on AST node type '" + receiverTypeName + "' returns structural child", //$NON-NLS-1$ //$NON-NLS-2$
List.of());
}
}
}
}
return NullabilityResult.UNKNOWN;
}
private NullabilityResult analyzeVariableInitializer(SimpleName name) {
IVariableBinding varBinding = resolveVariableBinding(name);
if (varBinding == null) {
return NullabilityResult.UNKNOWN;
}
// Walk up to find the enclosing method/block
ASTNode scope = findEnclosingBlock(name);
if (scope == null) {
return NullabilityResult.UNKNOWN;
}
// Look for variable declaration with initializer
String varName = name.getIdentifier();
VariableInitializerFinder finder = new VariableInitializerFinder(varName);
scope.accept(finder);
if (finder.initializer != null) {
// new X() → non-null
if (finder.initializer instanceof ClassInstanceCreation) {
return new NullabilityResult(NullStatus.NON_NULL,
"variable '" + varName + "' initialized with 'new'", //$NON-NLS-1$ //$NON-NLS-2$
List.of());
}
// Known factory method → non-null
if (finder.initializer instanceof MethodInvocation mi) {
NullabilityResult initResult = analyzeMethodInvocation(mi);
if (initResult.status() == NullStatus.NON_NULL) {
return new NullabilityResult(NullStatus.NON_NULL,
"variable '" + varName + "' initialized with non-null factory: " + initResult.reason(), //$NON-NLS-1$ //$NON-NLS-2$
List.of());
}
}
}
// Check if initializer is a Map.get() call → nullable
if (finder.initializer != null && isCollectionGetCall(finder.initializer)) {
return new NullabilityResult(NullStatus.NULLABLE,
"variable may be result of Map.get() which can return null", //$NON-NLS-1$
List.of());
}
return NullabilityResult.UNKNOWN;
}
private boolean isCollectionGetCall(Expression initializer) {
if (!(initializer instanceof MethodInvocation mi)) {
return false;
}
String methodName = mi.getName().getIdentifier();
if (!"get".equals(methodName)) { //$NON-NLS-1$
return false;
}
Expression receiver = mi.getExpression();
if (receiver == null) {
return false;
}
ITypeBinding receiverType = receiver.resolveTypeBinding();
if (receiverType == null) {
return false;
}
String typeName = receiverType.getQualifiedName();
return typeName.startsWith("java.util.Map") //$NON-NLS-1$
|| typeName.startsWith("java.util.HashMap") //$NON-NLS-1$
|| typeName.startsWith("java.util.LinkedHashMap") //$NON-NLS-1$
|| typeName.startsWith("java.util.TreeMap"); //$NON-NLS-1$
}
// ---- Stage 3: Contextual null-check analysis (SpotBugs-style) ----
NullabilityResult analyzeByNullChecks(Expression expression) {
if (!(expression instanceof SimpleName name)) {
return NullabilityResult.UNKNOWN;
}
String varName = name.getIdentifier();
ASTNode enclosingMethod = findEnclosingMethod(expression);
if (enclosingMethod == null) {
return NullabilityResult.UNKNOWN;
}
int expressionOffset = expression.getStartPosition();
NullCheckFinder finder = new NullCheckFinder(varName);
enclosingMethod.accept(finder);
if (finder.nullCheckLocations.isEmpty()) {
return NullabilityResult.UNKNOWN;
}
List<String> evidence = new ArrayList<>();
boolean hasCheckBefore = false;
boolean hasCheckAfter = false;
boolean isInsideGuard = false;
for (NullCheckLocation loc : finder.nullCheckLocations) {
evidence.add("line " + loc.line + ": " + loc.description); //$NON-NLS-1$ //$NON-NLS-2$
if (loc.offset < expressionOffset) {
hasCheckBefore = true;
// Check if we are inside a guarded block (e.g., if (x != null) { ...here... })
if (isInsideNullGuard(expression, varName)) {
isInsideGuard = true;
}
}
if (loc.offset > expressionOffset) {
hasCheckAfter = true;
}
}
if (isInsideGuard) {
// Expression is inside a null guard → safe in this context
return new NullabilityResult(NullStatus.NON_NULL,
"variable '" + varName + "' is inside a null guard", //$NON-NLS-1$ //$NON-NLS-2$
evidence);
}
if (hasCheckAfter && !hasCheckBefore) {
// SpotBugs-style: null check AFTER the usage → high risk
return new NullabilityResult(NullStatus.NULLABLE,
"null-check for '" + varName + "' found after usage (SpotBugs-style: null is realistic)", //$NON-NLS-1$ //$NON-NLS-2$
evidence);
}
if (hasCheckBefore && !isInsideGuard) {
// There's a check before but we're not inside its guard
return new NullabilityResult(NullStatus.POTENTIALLY_NULLABLE,
"null-check for '" + varName + "' exists nearby but usage is not inside guard", //$NON-NLS-1$ //$NON-NLS-2$
evidence);
}
// Null check exists somewhere → the type can be null
return new NullabilityResult(NullStatus.POTENTIALLY_NULLABLE,
"null-check for '" + varName + "' found in same method", //$NON-NLS-1$ //$NON-NLS-2$
evidence);
}
/**
* Checks if the given expression is inside a block guarded by a null check
* for the given variable name.
*/
private boolean isInsideNullGuard(Expression expression, String varName) {
ASTNode current = expression.getParent();
while (current != null) {
if (current instanceof IfStatement ifStmt) {
Expression condition = ifStmt.getExpression();
if (isNonNullCheck(condition, varName)) {
// Check if expression is in the then-branch
Statement thenBranch = ifStmt.getThenStatement();
if (isAncestor(thenBranch, expression)) {
return true;
}
}
if (isNullCheck(condition, varName)) {
// if (x == null) return; → expression after this is guarded
Statement thenBranch = ifStmt.getThenStatement();
if (isEarlyReturn(thenBranch) && !isAncestor(thenBranch, expression)) {
return true;
}
}
}
current = current.getParent();
}
return false;
}
private boolean isNonNullCheck(Expression condition, String varName) {
if (condition instanceof InfixExpression infix) {
if (infix.getOperator() == InfixExpression.Operator.NOT_EQUALS) {
return isNullComparisonWith(infix, varName);
}
}
return false;
}
private boolean isNullCheck(Expression condition, String varName) {
if (condition instanceof InfixExpression infix) {
if (infix.getOperator() == InfixExpression.Operator.EQUALS) {
return isNullComparisonWith(infix, varName);
}
}
return false;
}
private boolean isNullComparisonWith(InfixExpression infix, String varName) {
Expression left = infix.getLeftOperand();
Expression right = infix.getRightOperand();
return (isNameMatch(left, varName) && right instanceof NullLiteral)
|| (isNameMatch(right, varName) && left instanceof NullLiteral);
}
private boolean isNameMatch(Expression expr, String varName) {
return expr instanceof SimpleName sn && sn.getIdentifier().equals(varName);
}
private boolean isEarlyReturn(Statement stmt) {
if (stmt instanceof ReturnStatement) {
return true;
}
if (stmt instanceof Block block) {
@SuppressWarnings("unchecked")
List<Statement> stmts = block.statements();
return !stmts.isEmpty() && stmts.get(0) instanceof ReturnStatement;
}
return false;
}
private boolean isAncestor(ASTNode ancestor, ASTNode descendant) {
ASTNode current = descendant;
while (current != null) {
if (current == ancestor) {
return true;
}
current = current.getParent();
}
return false;
}
// ---- Stage 4: Annotation-based checking ----
NullabilityResult analyzeByAnnotations(Expression expression) {
ITypeBinding typeBinding = expression.resolveTypeBinding();
if (typeBinding == null) {
return NullabilityResult.UNKNOWN;
}
// Check annotations on the type binding
for (IAnnotationBinding annotation : typeBinding.getTypeAnnotations()) {
String annotName = annotation.getAnnotationType().getName();
if (NULLABLE_ANNOTATIONS.contains(annotName)) {
return new NullabilityResult(NullStatus.NULLABLE,
"@" + annotName + " annotation found", //$NON-NLS-1$ //$NON-NLS-2$
List.of());
}
if (NON_NULL_ANNOTATIONS.contains(annotName)) {
return new NullabilityResult(NullStatus.NON_NULL,
"@" + annotName + " annotation found", //$NON-NLS-1$ //$NON-NLS-2$
List.of());
}
}
// Check if it's a variable → check variable annotations
if (expression instanceof SimpleName name) {
IVariableBinding varBinding = resolveVariableBinding(name);
if (varBinding != null) {
for (IAnnotationBinding annotation : varBinding.getAnnotations()) {
String annotName = annotation.getAnnotationType().getName();
if (NULLABLE_ANNOTATIONS.contains(annotName)) {
return new NullabilityResult(NullStatus.NULLABLE,
"@" + annotName + " annotation on variable", //$NON-NLS-1$ //$NON-NLS-2$
List.of());
}
if (NON_NULL_ANNOTATIONS.contains(annotName)) {
return new NullabilityResult(NullStatus.NON_NULL,
"@" + annotName + " annotation on variable", //$NON-NLS-1$ //$NON-NLS-2$
List.of());
}
}
}
}
// Check method return annotations for method invocations
if (expression instanceof MethodInvocation mi) {
IMethodBinding methodBinding = mi.resolveMethodBinding();
if (methodBinding != null) {
for (IAnnotationBinding annotation : methodBinding.getAnnotations()) {
String annotName = annotation.getAnnotationType().getName();
if (NULLABLE_ANNOTATIONS.contains(annotName)) {
return new NullabilityResult(NullStatus.NULLABLE,
"method return annotated @" + annotName, //$NON-NLS-1$
List.of());
}
if (NON_NULL_ANNOTATIONS.contains(annotName)) {
return new NullabilityResult(NullStatus.NON_NULL,
"method return annotated @" + annotName, //$NON-NLS-1$
List.of());
}
}
}
}
return NullabilityResult.UNKNOWN;
}
// ---- Helper methods and inner classes ----
private static IVariableBinding resolveVariableBinding(SimpleName name) {
if (name.resolveBinding() instanceof IVariableBinding vb) {
return vb;
}
return null;
}
private static ASTNode findEnclosingBlock(ASTNode node) {
ASTNode current = node.getParent();
while (current != null) {
if (current instanceof Block || current instanceof MethodDeclaration) {
return current;
}
current = current.getParent();
}
return null;
}
private static ASTNode findEnclosingMethod(ASTNode node) {
ASTNode current = node.getParent();
while (current != null) {
if (current instanceof MethodDeclaration) {
return current;
}
current = current.getParent();
}
return null;
}
private Map<String, String> loadWhitelist() {
Properties props = new Properties();
try (InputStream is = getClass().getClassLoader().getResourceAsStream(PROPERTIES_PATH)) {
if (is != null) {
props.load(is);
}
} catch (IOException e) {
// Fall through with empty properties
}
Map<String, String> result = new ConcurrentHashMap<>();
for (String key : props.stringPropertyNames()) {
result.put(key, props.getProperty(key));
}
return result;
}
/**
* Returns the loaded non-null type whitelist.
*
* @return unmodifiable view of the whitelist
*/
public Map<String, String> getNonNullTypes() {
return Map.copyOf(nonNullTypes);
}
/** Records a location where a null check was found. */
record NullCheckLocation(int offset, int line, String description) {}
/**
* Visitor that finds null checks for a specific variable within a method body.
*/
private static class NullCheckFinder extends ASTVisitor {
private final String varName;
final List<NullCheckLocation> nullCheckLocations = new ArrayList<>();
NullCheckFinder(String varName) {
this.varName = varName;
}
@Override
public boolean visit(InfixExpression node) {
InfixExpression.Operator op = node.getOperator();
if (op == InfixExpression.Operator.EQUALS || op == InfixExpression.Operator.NOT_EQUALS) {
Expression left = node.getLeftOperand();
Expression right = node.getRightOperand();
boolean leftIsVar = isNameMatch(left);
boolean rightIsVar = isNameMatch(right);
boolean leftIsNull = left instanceof NullLiteral;
boolean rightIsNull = right instanceof NullLiteral;
if ((leftIsVar && rightIsNull) || (rightIsVar && leftIsNull)) {
ASTNode root = node.getRoot();
int line = -1;
if (root instanceof org.eclipse.jdt.core.dom.CompilationUnit cu) {
line = cu.getLineNumber(node.getStartPosition());
}
String desc = varName + " " + op + " null"; //$NON-NLS-1$ //$NON-NLS-2$
nullCheckLocations.add(new NullCheckLocation(node.getStartPosition(), line, desc));
}
}
return true;
}
@Override
public boolean visit(MethodInvocation node) {
// Objects.requireNonNull(variable)
if ("requireNonNull".equals(node.getName().getIdentifier())) { //$NON-NLS-1$
@SuppressWarnings("unchecked")
List<Expression> args = node.arguments();
if (!args.isEmpty() && isNameMatch(args.get(0))) {
ASTNode root = node.getRoot();
int line = -1;
if (root instanceof org.eclipse.jdt.core.dom.CompilationUnit cu) {
line = cu.getLineNumber(node.getStartPosition());
}
nullCheckLocations.add(new NullCheckLocation(node.getStartPosition(), line,
"Objects.requireNonNull(" + varName + ")")); //$NON-NLS-1$ //$NON-NLS-2$
}
}
// Optional.ofNullable(variable)
if ("ofNullable".equals(node.getName().getIdentifier())) { //$NON-NLS-1$
@SuppressWarnings("unchecked")
List<Expression> args = node.arguments();
if (!args.isEmpty() && isNameMatch(args.get(0))) {
ASTNode root = node.getRoot();
int line = -1;
if (root instanceof org.eclipse.jdt.core.dom.CompilationUnit cu) {
line = cu.getLineNumber(node.getStartPosition());
}
nullCheckLocations.add(new NullCheckLocation(node.getStartPosition(), line,
"Optional.ofNullable(" + varName + ")")); //$NON-NLS-1$ //$NON-NLS-2$
}
}
return true;
}
@Override
public boolean visit(ConditionalExpression node) {
// variable != null ? ... : ...
Expression condition = node.getExpression();
if (condition instanceof InfixExpression infix) {
InfixExpression.Operator op = infix.getOperator();
if (op == InfixExpression.Operator.EQUALS || op == InfixExpression.Operator.NOT_EQUALS) {
Expression left = infix.getLeftOperand();
Expression right = infix.getRightOperand();
boolean leftIsVar = isNameMatch(left);
boolean rightIsVar = isNameMatch(right);
boolean leftIsNull = left instanceof NullLiteral;
boolean rightIsNull = right instanceof NullLiteral;
if ((leftIsVar && rightIsNull) || (rightIsVar && leftIsNull)) {
ASTNode root = node.getRoot();
int line = -1;
if (root instanceof org.eclipse.jdt.core.dom.CompilationUnit cu) {
line = cu.getLineNumber(node.getStartPosition());
}
nullCheckLocations.add(new NullCheckLocation(node.getStartPosition(), line,
varName + " " + op + " null (ternary)")); //$NON-NLS-1$ //$NON-NLS-2$
}
}
}
return true;
}
private boolean isNameMatch(Expression expr) {
return expr instanceof SimpleName sn && sn.getIdentifier().equals(varName);
}
}
/**
* Visitor that finds variable declarations with initializers.
*/
private static class VariableInitializerFinder extends ASTVisitor {
private final String varName;
Expression initializer;
VariableInitializerFinder(String varName) {
this.varName = varName;
}
@Override
public boolean visit(VariableDeclarationStatement node) {
@SuppressWarnings("unchecked")
List<VariableDeclarationFragment> fragments = node.fragments();
for (VariableDeclarationFragment frag : fragments) {
if (frag.getName().getIdentifier().equals(varName) && frag.getInitializer() != null) {
this.initializer = frag.getInitializer();
return false;
}
}
return true;
}
@Override
public boolean visit(SingleVariableDeclaration node) {
if (node.getName().getIdentifier().equals(varName) && node.getInitializer() != null) {
this.initializer = node.getInitializer();
return false;
}
return true;
}
}
}