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    @Override
050    public int[] getDefaultTokens() {
051        return getRequiredTokens();
052    }
053
054    @Override
055    public int[] getAcceptableTokens() {
056        return getRequiredTokens();
057    }
058
059    @Override
060    public int[] getRequiredTokens() {
061        return new int[] {TokenTypes.LITERAL_INSTANCEOF};
062    }
063
064    @Override
065    public void visitToken(DetailAST instanceofNode) {
066        findUnnecessaryNullCheck(instanceofNode)
067                .ifPresent(violationNode -> log(violationNode, MSG_UNNECESSARY_NULLCHECK));
068    }
069
070    /**
071     * Checks for an unnecessary null check within a logical AND expression.
072     *
073     * @param instanceOfNode the AST node representing the instanceof expression
074     * @return the identifier if the check is redundant, otherwise {@code null}
075     */
076    private static Optional<DetailAST> findUnnecessaryNullCheck(DetailAST instanceOfNode) {
077        final DetailAST topLevelExpr = findTopLevelLogicalExpression(instanceOfNode);
078
079        Optional<DetailAST> result = Optional.empty();
080        if (topLevelExpr.getType() == TokenTypes.LAND) {
081            result = findRedundantNullCheck(topLevelExpr, instanceOfNode)
082                    .map(DetailAST::getFirstChild);
083        }
084        return result;
085    }
086
087    /**
088     * Traverses up through LAND and LOR operators to find the top-level logical expression.
089     *
090     * @param node the starting node
091     * @return the top-level logical expression node
092     */
093    private static DetailAST findTopLevelLogicalExpression(DetailAST node) {
094        DetailAST currentParent = node;
095        while (currentParent.getParent().getType() == TokenTypes.LAND
096                || currentParent.getParent().getType() == TokenTypes.LOR) {
097            currentParent = currentParent.getParent();
098        }
099        return currentParent;
100    }
101
102    /**
103     * Finds a redundant null check in a logical AND expression combined with an instanceof check.
104     *
105     * @param logicalAndNode the root node of the logical AND expression
106     * @param instanceOfNode the instanceof expression node
107     * @return the AST node representing the redundant null check, or null if not found
108     */
109    private static Optional<DetailAST> findRedundantNullCheck(DetailAST logicalAndNode,
110        DetailAST instanceOfNode) {
111
112        Optional<DetailAST> nullCheckNode = Optional.empty();
113        final DetailAST instanceOfIdent = instanceOfNode.findFirstToken(TokenTypes.IDENT);
114
115        if (instanceOfIdent != null
116            && !containsVariableDereference(logicalAndNode, instanceOfIdent.getText())) {
117
118            nullCheckNode = searchForNullCheck(logicalAndNode, instanceOfNode, instanceOfIdent);
119        }
120        return nullCheckNode;
121    }
122
123    /**
124     * Searches for a redundant null check in the children of a logical AND node.
125     *
126     * @param logicalAndNode the LAND node to search
127     * @param instanceOfNode the instanceof expression node
128     * @param instanceOfIdent the identifier from the instanceof expression
129     * @return the null check node if found, null otherwise
130     */
131    private static Optional<DetailAST> searchForNullCheck(DetailAST logicalAndNode,
132        DetailAST instanceOfNode, DetailAST instanceOfIdent) {
133
134        Optional<DetailAST> nullCheckNode = Optional.empty();
135
136        final Optional<DetailAST> instanceOfSubtree =
137                findDirectChildContaining(logicalAndNode, instanceOfNode);
138
139        final DetailAST instanceOfSubtreeNode =
140                instanceOfSubtree.orElse(null);
141
142        final boolean instanceOfInLor =
143                instanceOfSubtreeNode != null
144                        && instanceOfSubtreeNode.getType() == TokenTypes.LOR;
145
146        DetailAST currentChild = logicalAndNode.getFirstChild();
147        while (currentChild != null) {
148            if (instanceOfInLor && currentChild.equals(instanceOfSubtreeNode)) {
149                break;
150            }
151
152            if (nullCheckNode.isEmpty()) {
153                nullCheckNode = checkChildForNullCheck(
154                        currentChild, instanceOfNode, instanceOfIdent);
155            }
156
157            currentChild = currentChild.getNextSibling();
158        }
159
160        return nullCheckNode;
161    }
162
163    /**
164     * Checks a child node for a redundant null check.
165     *
166     * @param currentChild the child node to check
167     * @param instanceOfNode the instanceof expression node
168     * @param instanceOfIdent the identifier from the instanceof expression
169     * @return the null check node if found, otherwise empty
170     */
171    private static Optional<DetailAST> checkChildForNullCheck(DetailAST currentChild,
172        DetailAST instanceOfNode, DetailAST instanceOfIdent) {
173
174        Optional<DetailAST> result = Optional.empty();
175
176        if (isNotEqual(currentChild)
177                && isNullCheckRedundant(instanceOfIdent, currentChild)) {
178            result = Optional.of(currentChild);
179        }
180        else if (currentChild.getType() == TokenTypes.LAND) {
181            result = findRedundantNullCheck(currentChild, instanceOfNode);
182        }
183
184        return result;
185    }
186
187    /**
188     * Finds the direct child of parent that contains the target node.
189     *
190     * @param parent the parent node
191     * @param target the target node to find
192     * @return the direct child containing target, or null if not found
193     */
194    private static Optional<DetailAST> findDirectChildContaining(DetailAST parent,
195        DetailAST target) {
196
197        DetailAST result = null;
198        DetailAST child = parent.getFirstChild();
199        while (child != null) {
200            if (isAncestorOf(child, target)) {
201                result = child;
202                break;
203            }
204            child = child.getNextSibling();
205        }
206        return Optional.ofNullable(result);
207    }
208
209    /**
210     * Checks if the given node is an ancestor of (or the same as) the target node.
211     *
212     * @param node the potential ancestor node
213     * @param target the target node to check
214     * @return true if node is an ancestor of target, false otherwise
215     */
216    private static boolean isAncestorOf(DetailAST node, DetailAST target) {
217        boolean found = false;
218        DetailAST current = target;
219        while (current != null) {
220            if (current.equals(node)) {
221                found = true;
222                break;
223            }
224            current = current.getParent();
225        }
226        return found;
227    }
228
229    /**
230     * Checks if the given AST node contains a method call or field access
231     * on the specified variable.
232     *
233     * @param node the AST node to check
234     * @param variableName the name of the variable
235     * @return true if the variable is dereferenced, false otherwise
236     */
237    private static boolean containsVariableDereference(DetailAST node, String variableName) {
238
239        boolean found = false;
240
241        if (node.getType() == TokenTypes.DOT
242            || node.getType() == TokenTypes.METHOD_CALL
243            || node.getType() == TokenTypes.LAND
244            || node.getType() == TokenTypes.LOR) {
245
246            DetailAST firstChild = node.getFirstChild();
247
248            while (firstChild != null) {
249                if (variableName.equals(firstChild.getText())
250                        && firstChild.getNextSibling().getType() != TokenTypes.ELIST
251                            || containsVariableDereference(firstChild, variableName)) {
252                    found = true;
253                    break;
254                }
255                firstChild = firstChild.getNextSibling();
256            }
257        }
258        return found;
259    }
260
261    /**
262     * Checks if the given AST node represents a {@code !=} (not equal) operator.
263     *
264     * @param node the AST node to check
265     * @return {@code true} if the node is a not equal operator, otherwise {@code false}
266     */
267    private static boolean isNotEqual(DetailAST node) {
268        return node.getType() == TokenTypes.NOT_EQUAL;
269    }
270
271    /**
272     * Checks if the given AST node is a null literal.
273     *
274     * @param node AST node to check
275     * @return true if the node is a null literal, false otherwise
276     */
277    private static boolean isNullLiteral(DetailAST node) {
278        return node.getType() == TokenTypes.LITERAL_NULL;
279    }
280
281    /**
282     * Determines if the null check is redundant with the instanceof check.
283     *
284     * @param instanceOfIdent the identifier from the instanceof check
285     * @param nullCheckNode the node representing the null check
286     * @return true if the null check is unnecessary, false otherwise
287     */
288    private static boolean isNullCheckRedundant(DetailAST instanceOfIdent,
289        final DetailAST nullCheckNode) {
290
291        final DetailAST nullCheckIdent = nullCheckNode.findFirstToken(TokenTypes.IDENT);
292        return nullCheckIdent != null
293                && (isNullLiteral(nullCheckNode.getFirstChild().getNextSibling())
294                    || isNullLiteral(nullCheckNode.getFirstChild()))
295                && instanceOfIdent.getText().equals(nullCheckIdent.getText());
296    }
297}