001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.coding;
021
022import java.util.Optional;
023
024import com.puppycrawl.tools.checkstyle.StatelessCheck;
025import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
026import com.puppycrawl.tools.checkstyle.api.DetailAST;
027import com.puppycrawl.tools.checkstyle.api.TokenTypes;
028
029/**
030 * <div>
031 * Checks for redundant null checks with the instanceof operator.
032 * </div>
033 *
034 * <p>
035 * The instanceof operator inherently returns false when the left operand is null,
036 * making explicit null checks redundant in boolean expressions with instanceof.
037 * </p>
038 *
039 * @since 10.25.0
040 */
041@StatelessCheck
042public class UnnecessaryNullCheckWithInstanceOfCheck extends AbstractCheck {
043
044    /**
045     * The error message key for reporting unnecessary null checks.
046     */
047    public static final String MSG_UNNECESSARY_NULLCHECK = "unnecessary.nullcheck.with.instanceof";
048
049    /**
050     * Creates a new {@code UnnecessaryNullCheckWithInstanceOfCheck} instance.
051     */
052    public UnnecessaryNullCheckWithInstanceOfCheck() {
053        // no code by default
054    }
055
056    @Override
057    public int[] getDefaultTokens() {
058        return getRequiredTokens();
059    }
060
061    @Override
062    public int[] getAcceptableTokens() {
063        return getRequiredTokens();
064    }
065
066    @Override
067    public int[] getRequiredTokens() {
068        return new int[] {TokenTypes.LITERAL_INSTANCEOF};
069    }
070
071    @Override
072    public void visitToken(DetailAST instanceofNode) {
073        findUnnecessaryNullCheck(instanceofNode)
074                .ifPresent(violationNode -> log(violationNode, MSG_UNNECESSARY_NULLCHECK));
075    }
076
077    /**
078     * Checks for an unnecessary null check within a logical AND expression.
079     *
080     * @param instanceOfNode the AST node representing the instanceof expression
081     * @return the identifier if the check is redundant, otherwise {@code null}
082     */
083    private static Optional<DetailAST> findUnnecessaryNullCheck(DetailAST instanceOfNode) {
084        final DetailAST topLevelExpr = findTopLevelLogicalExpression(instanceOfNode);
085
086        Optional<DetailAST> result = Optional.empty();
087        if (topLevelExpr.getType() == TokenTypes.LAND) {
088            result = findRedundantNullCheck(topLevelExpr, instanceOfNode)
089                    .map(DetailAST::getFirstChild);
090        }
091        return result;
092    }
093
094    /**
095     * Traverses up through LAND and LOR operators to find the top-level logical expression.
096     *
097     * @param node the starting node
098     * @return the top-level logical expression node
099     */
100    private static DetailAST findTopLevelLogicalExpression(DetailAST node) {
101        DetailAST currentParent = node;
102        while (currentParent.getParent().getType() == TokenTypes.LAND
103                || currentParent.getParent().getType() == TokenTypes.LOR) {
104            currentParent = currentParent.getParent();
105        }
106        return currentParent;
107    }
108
109    /**
110     * Finds a redundant null check in a logical AND expression combined with an instanceof check.
111     *
112     * @param logicalAndNode the root node of the logical AND expression
113     * @param instanceOfNode the instanceof expression node
114     * @return the AST node representing the redundant null check, or null if not found
115     */
116    private static Optional<DetailAST> findRedundantNullCheck(DetailAST logicalAndNode,
117        DetailAST instanceOfNode) {
118
119        Optional<DetailAST> nullCheckNode = Optional.empty();
120        final DetailAST instanceOfIdent = instanceOfNode.findFirstToken(TokenTypes.IDENT);
121
122        if (instanceOfIdent != null
123            && !containsVariableDereference(logicalAndNode, instanceOfIdent.getText())) {
124
125            nullCheckNode = searchForNullCheck(logicalAndNode, instanceOfNode, instanceOfIdent);
126        }
127        return nullCheckNode;
128    }
129
130    /**
131     * Searches for a redundant null check in the children of a logical AND node.
132     *
133     * @param logicalAndNode the LAND node to search
134     * @param instanceOfNode the instanceof expression node
135     * @param instanceOfIdent the identifier from the instanceof expression
136     * @return the null check node if found, null otherwise
137     */
138    private static Optional<DetailAST> searchForNullCheck(DetailAST logicalAndNode,
139        DetailAST instanceOfNode, DetailAST instanceOfIdent) {
140
141        Optional<DetailAST> nullCheckNode = Optional.empty();
142
143        final Optional<DetailAST> instanceOfSubtree =
144                findDirectChildContaining(logicalAndNode, instanceOfNode);
145
146        final DetailAST instanceOfSubtreeNode =
147                instanceOfSubtree.orElse(null);
148
149        final boolean instanceOfInLor =
150                instanceOfSubtreeNode != null
151                        && instanceOfSubtreeNode.getType() == TokenTypes.LOR;
152
153        DetailAST currentChild = logicalAndNode.getFirstChild();
154        while (currentChild != null) {
155            if (instanceOfInLor && currentChild.equals(instanceOfSubtreeNode)) {
156                break;
157            }
158
159            if (nullCheckNode.isEmpty()) {
160                nullCheckNode = checkChildForNullCheck(
161                        currentChild, instanceOfNode, instanceOfIdent);
162            }
163
164            currentChild = currentChild.getNextSibling();
165        }
166
167        return nullCheckNode;
168    }
169
170    /**
171     * Checks a child node for a redundant null check.
172     *
173     * @param currentChild the child node to check
174     * @param instanceOfNode the instanceof expression node
175     * @param instanceOfIdent the identifier from the instanceof expression
176     * @return the null check node if found, otherwise empty
177     */
178    private static Optional<DetailAST> checkChildForNullCheck(DetailAST currentChild,
179        DetailAST instanceOfNode, DetailAST instanceOfIdent) {
180
181        Optional<DetailAST> result = Optional.empty();
182
183        if (isNotEqual(currentChild)
184                && isNullCheckRedundant(instanceOfIdent, currentChild)) {
185            result = Optional.of(currentChild);
186        }
187        else if (currentChild.getType() == TokenTypes.LAND) {
188            result = findRedundantNullCheck(currentChild, instanceOfNode);
189        }
190
191        return result;
192    }
193
194    /**
195     * Finds the direct child of parent that contains the target node.
196     *
197     * @param parent the parent node
198     * @param target the target node to find
199     * @return the direct child containing target, or null if not found
200     */
201    private static Optional<DetailAST> findDirectChildContaining(DetailAST parent,
202        DetailAST target) {
203
204        DetailAST result = null;
205        DetailAST child = parent.getFirstChild();
206        while (child != null) {
207            if (isAncestorOf(child, target)) {
208                result = child;
209                break;
210            }
211            child = child.getNextSibling();
212        }
213        return Optional.ofNullable(result);
214    }
215
216    /**
217     * Checks if the given node is an ancestor of (or the same as) the target node.
218     *
219     * @param node the potential ancestor node
220     * @param target the target node to check
221     * @return true if node is an ancestor of target, false otherwise
222     */
223    private static boolean isAncestorOf(DetailAST node, DetailAST target) {
224        boolean found = false;
225        DetailAST current = target;
226        while (current != null) {
227            if (current.equals(node)) {
228                found = true;
229                break;
230            }
231            current = current.getParent();
232        }
233        return found;
234    }
235
236    /**
237     * Checks if the given AST node contains a method call or field access
238     * on the specified variable.
239     *
240     * @param node the AST node to check
241     * @param variableName the name of the variable
242     * @return true if the variable is dereferenced, false otherwise
243     */
244    private static boolean containsVariableDereference(DetailAST node, String variableName) {
245
246        boolean found = false;
247
248        if (node.getType() == TokenTypes.DOT
249            || node.getType() == TokenTypes.METHOD_CALL
250            || node.getType() == TokenTypes.LAND
251            || node.getType() == TokenTypes.LOR) {
252
253            DetailAST firstChild = node.getFirstChild();
254
255            while (firstChild != null) {
256                if (variableName.equals(firstChild.getText())
257                        && firstChild.getNextSibling().getType() != TokenTypes.ELIST
258                            || containsVariableDereference(firstChild, variableName)) {
259                    found = true;
260                    break;
261                }
262                firstChild = firstChild.getNextSibling();
263            }
264        }
265        return found;
266    }
267
268    /**
269     * Checks if the given AST node represents a {@code !=} (not equal) operator.
270     *
271     * @param node the AST node to check
272     * @return {@code true} if the node is a not equal operator, otherwise {@code false}
273     */
274    private static boolean isNotEqual(DetailAST node) {
275        return node.getType() == TokenTypes.NOT_EQUAL;
276    }
277
278    /**
279     * Checks if the given AST node is a null literal.
280     *
281     * @param node AST node to check
282     * @return true if the node is a null literal, false otherwise
283     */
284    private static boolean isNullLiteral(DetailAST node) {
285        return node.getType() == TokenTypes.LITERAL_NULL;
286    }
287
288    /**
289     * Determines if the null check is redundant with the instanceof check.
290     *
291     * @param instanceOfIdent the identifier from the instanceof check
292     * @param nullCheckNode the node representing the null check
293     * @return true if the null check is unnecessary, false otherwise
294     */
295    private static boolean isNullCheckRedundant(DetailAST instanceOfIdent,
296        final DetailAST nullCheckNode) {
297
298        final DetailAST nullCheckIdent = nullCheckNode.findFirstToken(TokenTypes.IDENT);
299        return nullCheckIdent != null
300                && (isNullLiteral(nullCheckNode.getFirstChild().getNextSibling())
301                    || isNullLiteral(nullCheckNode.getFirstChild()))
302                && instanceOfIdent.getText().equals(nullCheckIdent.getText());
303    }
304
305}