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)) {
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 field access or method call on the
238     * variable of the instanceof operand, evaluated before that operand.
239     *
240     * @param node the AST node to check
241     * @param instanceOfIdent the identifier from the instanceof expression
242     * @return true if the variable is dereferenced before the instanceof, false otherwise
243     */
244    private static boolean containsVariableDereference(DetailAST node,
245        DetailAST instanceOfIdent) {
246
247        boolean found = false;
248        DetailAST child = node.getFirstChild();
249
250        while (child != null && !found) {
251            found = node.getType() == TokenTypes.DOT
252                    && instanceOfIdent.getText().equals(child.getText())
253                    && child.getNextSibling() != null
254                    && isBefore(child, instanceOfIdent)
255                || containsVariableDereference(child, instanceOfIdent);
256            child = child.getNextSibling();
257        }
258        return found;
259    }
260
261    /**
262     * Checks if the first node appears earlier in the source than the second node.
263     *
264     * @param first the node expected to appear first
265     * @param second the node expected to appear second
266     * @return true if first is located before second, false otherwise
267     */
268    private static boolean isBefore(DetailAST first, DetailAST second) {
269        int result = Integer.compare(first.getLineNo(), second.getLineNo());
270        if (result == 0) {
271            result = Integer.compare(first.getColumnNo(), second.getColumnNo());
272        }
273        return result == -1;
274    }
275
276    /**
277     * Checks if the given AST node represents a {@code !=} (not equal) operator.
278     *
279     * @param node the AST node to check
280     * @return {@code true} if the node is a not equal operator, otherwise {@code false}
281     */
282    private static boolean isNotEqual(DetailAST node) {
283        return node.getType() == TokenTypes.NOT_EQUAL;
284    }
285
286    /**
287     * Checks if the given AST node is a null literal.
288     *
289     * @param node AST node to check
290     * @return true if the node is a null literal, false otherwise
291     */
292    private static boolean isNullLiteral(DetailAST node) {
293        return node.getType() == TokenTypes.LITERAL_NULL;
294    }
295
296    /**
297     * Determines if the null check is redundant with the instanceof check.
298     *
299     * @param instanceOfIdent the identifier from the instanceof check
300     * @param nullCheckNode the node representing the null check
301     * @return true if the null check is unnecessary, false otherwise
302     */
303    private static boolean isNullCheckRedundant(DetailAST instanceOfIdent,
304        final DetailAST nullCheckNode) {
305
306        final DetailAST nullCheckIdent = nullCheckNode.findFirstToken(TokenTypes.IDENT);
307        return nullCheckIdent != null
308                && (isNullLiteral(nullCheckNode.getFirstChild().getNextSibling())
309                    || isNullLiteral(nullCheckNode.getFirstChild()))
310                && instanceOfIdent.getText().equals(nullCheckIdent.getText());
311    }
312
313}