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.Collections;
023import java.util.HashMap;
024import java.util.HashSet;
025import java.util.Map;
026import java.util.Set;
027
028import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
029import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
030import com.puppycrawl.tools.checkstyle.api.DetailAST;
031import com.puppycrawl.tools.checkstyle.api.TokenTypes;
032import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
033import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
034
035/**
036 * <div>
037 * Checks that any combination of String literals
038 * is on the left side of an {@code equals()} comparison.
039 * Also checks for String literals assigned to some field
040 * (such as {@code someString.equals(anotherString = "text")}).
041 * </div>
042 *
043 * <p>Rationale: Calling the {@code equals()} method on String literals
044 * will avoid a potential {@code NullPointerException}. Also, it is
045 * pretty common to see null checks right before equals comparisons
046 * but following this rule such checks are not required.
047 * </p>
048 *
049 * @since 5.0
050 */
051@FileStatefulCheck
052public class EqualsAvoidNullCheck extends AbstractCheck {
053
054    /**
055     * A key is pointing to the warning message text in "messages.properties"
056     * file.
057     */
058    public static final String MSG_EQUALS_AVOID_NULL = "equals.avoid.null";
059
060    /**
061     * A key is pointing to the warning message text in "messages.properties"
062     * file.
063     */
064    public static final String MSG_EQUALS_IGNORE_CASE_AVOID_NULL = "equalsIgnoreCase.avoid.null";
065
066    /** Method name for comparison. */
067    private static final String EQUALS = "equals";
068
069    /** Type name for comparison. */
070    private static final String STRING = "String";
071
072    /** Curly for comparison. */
073    private static final String LEFT_CURLY = "{";
074
075    /** Control whether to ignore {@code String.equalsIgnoreCase(String)} invocations. */
076    private boolean ignoreEqualsIgnoreCase;
077
078    /** Stack of sets of field names, one for each class of a set of nested classes. */
079    private FieldFrame currentFrame;
080
081    /**
082     * Creates a new {@code EqualsAvoidNullCheck} instance.
083     */
084    public EqualsAvoidNullCheck() {
085        // no code by default
086    }
087
088    @Override
089    public int[] getDefaultTokens() {
090        return getRequiredTokens();
091    }
092
093    @Override
094    public int[] getAcceptableTokens() {
095        return getRequiredTokens();
096    }
097
098    @Override
099    public int[] getRequiredTokens() {
100        return new int[] {
101            TokenTypes.METHOD_CALL,
102            TokenTypes.CLASS_DEF,
103            TokenTypes.METHOD_DEF,
104            TokenTypes.LITERAL_FOR,
105            TokenTypes.LITERAL_CATCH,
106            TokenTypes.LITERAL_TRY,
107            TokenTypes.LITERAL_SWITCH,
108            TokenTypes.VARIABLE_DEF,
109            TokenTypes.PARAMETER_DEF,
110            TokenTypes.CTOR_DEF,
111            TokenTypes.SLIST,
112            TokenTypes.OBJBLOCK,
113            TokenTypes.ENUM_DEF,
114            TokenTypes.ENUM_CONSTANT_DEF,
115            TokenTypes.LITERAL_NEW,
116            TokenTypes.LAMBDA,
117            TokenTypes.PATTERN_VARIABLE_DEF,
118            TokenTypes.RECORD_DEF,
119            TokenTypes.COMPACT_CTOR_DEF,
120            TokenTypes.RECORD_COMPONENT_DEF,
121        };
122    }
123
124    /**
125     * Setter to control whether to ignore {@code String.equalsIgnoreCase(String)} invocations.
126     *
127     * @param newValue whether to ignore checking
128     *     {@code String.equalsIgnoreCase(String)}.
129     * @since 5.4
130     */
131    public void setIgnoreEqualsIgnoreCase(boolean newValue) {
132        ignoreEqualsIgnoreCase = newValue;
133    }
134
135    @Override
136    public void beginTree(DetailAST rootAST) {
137        currentFrame = new FieldFrame(null);
138    }
139
140    @Override
141    public void visitToken(final DetailAST ast) {
142        switch (ast.getType()) {
143            case TokenTypes.VARIABLE_DEF,
144                 TokenTypes.PARAMETER_DEF,
145                 TokenTypes.PATTERN_VARIABLE_DEF,
146                 TokenTypes.RECORD_COMPONENT_DEF -> currentFrame.addField(ast);
147
148            case TokenTypes.METHOD_CALL -> processMethodCall(ast);
149
150            case TokenTypes.SLIST -> processSlist(ast);
151
152            case TokenTypes.LITERAL_NEW -> processLiteralNew(ast);
153
154            case TokenTypes.OBJBLOCK -> {
155                final int parentType = ast.getParent().getType();
156                if (!astTypeIsClassOrEnumOrRecordDef(parentType)) {
157                    processFrame(ast);
158                }
159            }
160
161            default -> processFrame(ast);
162        }
163    }
164
165    @Override
166    public void leaveToken(DetailAST ast) {
167        switch (ast.getType()) {
168            case TokenTypes.SLIST -> leaveSlist(ast);
169
170            case TokenTypes.LITERAL_NEW -> leaveLiteralNew(ast);
171
172            case TokenTypes.OBJBLOCK -> {
173                final int parentType = ast.getParent().getType();
174                if (!astTypeIsClassOrEnumOrRecordDef(parentType)) {
175                    currentFrame = currentFrame.getParent();
176                }
177            }
178
179            case TokenTypes.VARIABLE_DEF,
180                 TokenTypes.PARAMETER_DEF,
181                 TokenTypes.RECORD_COMPONENT_DEF,
182                 TokenTypes.METHOD_CALL,
183                 TokenTypes.PATTERN_VARIABLE_DEF -> {
184                // intentionally do nothing
185            }
186
187            default -> currentFrame = currentFrame.getParent();
188        }
189    }
190
191    @Override
192    public void finishTree(DetailAST ast) {
193        traverseFieldFrameTree(currentFrame);
194    }
195
196    /**
197     * Determine whether SLIST begins a block, determined by braces, and add it as
198     * a frame in this case.
199     *
200     * @param ast SLIST ast.
201     */
202    private void processSlist(DetailAST ast) {
203        if (LEFT_CURLY.equals(ast.getText())) {
204            final FieldFrame frame = new FieldFrame(currentFrame);
205            currentFrame.addChild(frame);
206            currentFrame = frame;
207        }
208    }
209
210    /**
211     * Determine whether SLIST begins a block, determined by braces.
212     *
213     * @param ast SLIST ast.
214     */
215    private void leaveSlist(DetailAST ast) {
216        if (LEFT_CURLY.equals(ast.getText())) {
217            currentFrame = currentFrame.getParent();
218        }
219    }
220
221    /**
222     * Process CLASS_DEF, METHOD_DEF, LITERAL_IF, LITERAL_FOR, LITERAL_WHILE, LITERAL_DO,
223     * LITERAL_CATCH, LITERAL_TRY, CTOR_DEF, ENUM_DEF, ENUM_CONSTANT_DEF.
224     *
225     * @param ast processed ast.
226     */
227    private void processFrame(DetailAST ast) {
228        final FieldFrame frame = new FieldFrame(currentFrame);
229        final int astType = ast.getType();
230        if (astTypeIsClassOrEnumOrRecordDef(astType)) {
231            frame.setClassOrEnumOrRecordDef(true);
232            frame.setFrameName(TokenUtil.getIdent(ast).getText());
233        }
234        currentFrame.addChild(frame);
235        currentFrame = frame;
236    }
237
238    /**
239     * Add the method call to the current frame if it should be processed.
240     *
241     * @param methodCall METHOD_CALL ast.
242     */
243    private void processMethodCall(DetailAST methodCall) {
244        final DetailAST dot = methodCall.getFirstChild();
245        if (dot.getType() == TokenTypes.DOT) {
246            final String methodName = dot.getLastChild().getText();
247            if (EQUALS.equals(methodName)
248                    || !ignoreEqualsIgnoreCase && "equalsIgnoreCase".equals(methodName)) {
249                currentFrame.addMethodCall(methodCall);
250            }
251        }
252    }
253
254    /**
255     * Determine whether LITERAL_NEW is an anonymous class definition and add it as
256     * a frame in this case.
257     *
258     * @param ast LITERAL_NEW ast.
259     */
260    private void processLiteralNew(DetailAST ast) {
261        if (ast.findFirstToken(TokenTypes.OBJBLOCK) != null) {
262            final FieldFrame frame = new FieldFrame(currentFrame);
263            currentFrame.addChild(frame);
264            currentFrame = frame;
265        }
266    }
267
268    /**
269     * Determine whether LITERAL_NEW is an anonymous class definition and leave
270     * the frame it is in.
271     *
272     * @param ast LITERAL_NEW ast.
273     */
274    private void leaveLiteralNew(DetailAST ast) {
275        if (ast.findFirstToken(TokenTypes.OBJBLOCK) != null) {
276            currentFrame = currentFrame.getParent();
277        }
278    }
279
280    /**
281     * Traverse the tree of the field frames to check all equals method calls.
282     *
283     * @param frame to check method calls in.
284     */
285    private void traverseFieldFrameTree(FieldFrame frame) {
286        for (FieldFrame child: frame.getChildren()) {
287            traverseFieldFrameTree(child);
288
289            currentFrame = child;
290            child.getMethodCalls().forEach(this::checkMethodCall);
291        }
292    }
293
294    /**
295     * Check whether the method call should be violated.
296     *
297     * @param methodCall method call to check.
298     */
299    private void checkMethodCall(DetailAST methodCall) {
300        DetailAST objCalledOn = methodCall.getFirstChild().getFirstChild();
301        if (objCalledOn.getType() == TokenTypes.DOT) {
302            objCalledOn = objCalledOn.getLastChild();
303        }
304        final DetailAST expr = methodCall.findFirstToken(TokenTypes.ELIST).getFirstChild();
305        if (containsOneArgument(methodCall)
306                && containsAllSafeTokens(expr)
307                && isCalledOnStringFieldOrVariable(objCalledOn)) {
308            final String methodName = methodCall.getFirstChild().getLastChild().getText();
309            if (EQUALS.equals(methodName)) {
310                log(methodCall, MSG_EQUALS_AVOID_NULL);
311            }
312            else {
313                log(methodCall, MSG_EQUALS_IGNORE_CASE_AVOID_NULL);
314            }
315        }
316    }
317
318    /**
319     * Verify that method call has one argument.
320     *
321     * @param methodCall METHOD_CALL DetailAST
322     * @return true if method call has one argument.
323     */
324    private static boolean containsOneArgument(DetailAST methodCall) {
325        final DetailAST elist = methodCall.findFirstToken(TokenTypes.ELIST);
326        return elist.getChildCount() == 1;
327    }
328
329    /**
330     * Looks for all "safe" Token combinations in the argument
331     * expression branch.
332     *
333     * @param expr the argument expression
334     * @return - true if any child matches the set of tokens, false if not
335     */
336    private static boolean containsAllSafeTokens(final DetailAST expr) {
337        DetailAST arg = expr.getFirstChild();
338        arg = skipVariableAssign(arg);
339
340        boolean argIsNotNull = false;
341        if (arg.getType() == TokenTypes.PLUS) {
342            DetailAST child = arg.getFirstChild();
343            while (child != null
344                    && !argIsNotNull) {
345                argIsNotNull = child.getType() == TokenTypes.STRING_LITERAL
346                        || child.getType() == TokenTypes.TEXT_BLOCK_LITERAL_BEGIN
347                        || child.getType() == TokenTypes.IDENT;
348                child = child.getNextSibling();
349            }
350        }
351        else {
352            argIsNotNull = arg.getType() == TokenTypes.STRING_LITERAL
353                    || arg.getType() == TokenTypes.TEXT_BLOCK_LITERAL_BEGIN;
354        }
355
356        return argIsNotNull;
357    }
358
359    /**
360     * Skips over an inner assign portion of an argument expression.
361     *
362     * @param currentAST current token in the argument expression
363     * @return the next relevant token
364     */
365    private static DetailAST skipVariableAssign(final DetailAST currentAST) {
366        DetailAST result = currentAST;
367        while (result.getType() == TokenTypes.LPAREN) {
368            result = result.getNextSibling();
369        }
370        if (result.getType() == TokenTypes.ASSIGN) {
371            result = result.getFirstChild().getNextSibling();
372        }
373        return result;
374    }
375
376    /**
377     * Determine, whether equals method is called on a field of String type.
378     *
379     * @param objCalledOn object ast.
380     * @return true if the object is of String type.
381     */
382    private boolean isCalledOnStringFieldOrVariable(DetailAST objCalledOn) {
383        final boolean result;
384        final DetailAST previousSiblingAst = objCalledOn.getPreviousSibling();
385        if (previousSiblingAst == null) {
386            result = isStringFieldOrVariable(objCalledOn);
387        }
388        else {
389            if (previousSiblingAst.getType() == TokenTypes.LITERAL_THIS) {
390                result = isStringFieldOrVariableFromThisInstance(objCalledOn);
391            }
392            else {
393                final String className = previousSiblingAst.getText();
394                result = isStringFieldOrVariableFromClass(objCalledOn, className);
395            }
396        }
397        return result;
398    }
399
400    /**
401     * Whether the field or the variable is of String type.
402     *
403     * @param objCalledOn the field or the variable to check.
404     * @return true if the field or the variable is of String type.
405     */
406    private boolean isStringFieldOrVariable(DetailAST objCalledOn) {
407        boolean result = false;
408        final String name = objCalledOn.getText();
409        FieldFrame frame = currentFrame;
410        while (frame != null) {
411            final DetailAST field = frame.findField(name);
412            if (field != null
413                    && (frame.isClassOrEnumOrRecordDef()
414                            || CheckUtil.isBeforeInSource(field, objCalledOn))) {
415                result = STRING.equals(getFieldType(field));
416                break;
417            }
418            frame = frame.getParent();
419        }
420        return result;
421    }
422
423    /**
424     * Whether the field or the variable from THIS instance is of String type.
425     *
426     * @param objCalledOn the field or the variable from THIS instance to check.
427     * @return true if the field or the variable from THIS instance is of String type.
428     */
429    private boolean isStringFieldOrVariableFromThisInstance(DetailAST objCalledOn) {
430        final String name = objCalledOn.getText();
431        final DetailAST field = getObjectFrame(currentFrame).findField(name);
432        return field != null && STRING.equals(getFieldType(field));
433    }
434
435    /**
436     * Whether the field or the variable from the specified class is of String type.
437     *
438     * @param objCalledOn the field or the variable from the specified class to check.
439     * @param className the name of the class to check in.
440     * @return true if the field or the variable from the specified class is of String type.
441     */
442    private boolean isStringFieldOrVariableFromClass(DetailAST objCalledOn,
443            final String className) {
444        boolean result = false;
445        final String name = objCalledOn.getText();
446        FieldFrame frame = currentFrame;
447        while (frame != null) {
448            if (className.equals(frame.getFrameName())) {
449                final DetailAST field = frame.findField(name);
450                result = STRING.equals(getFieldType(field));
451                break;
452            }
453            frame = frame.getParent();
454        }
455        return result;
456    }
457
458    /**
459     * Get the nearest parent frame which is CLASS_DEF, ENUM_DEF or ENUM_CONST_DEF.
460     *
461     * @param frame to start the search from.
462     * @return the nearest parent frame which is CLASS_DEF, ENUM_DEF or ENUM_CONST_DEF.
463     */
464    private static FieldFrame getObjectFrame(FieldFrame frame) {
465        FieldFrame objectFrame = frame;
466        while (!objectFrame.isClassOrEnumOrRecordDef()) {
467            objectFrame = objectFrame.getParent();
468        }
469        return objectFrame;
470    }
471
472    /**
473     * Get field type.
474     *
475     * @param field to get the type from.
476     * @return type of the field.
477     */
478    private static String getFieldType(DetailAST field) {
479        String fieldType = null;
480        final DetailAST identAst = field.findFirstToken(TokenTypes.TYPE)
481                .findFirstToken(TokenTypes.IDENT);
482        if (identAst != null) {
483            fieldType = identAst.getText();
484        }
485        return fieldType;
486    }
487
488    /**
489     * Verify that a token is either CLASS_DEF, RECORD_DEF, or ENUM_DEF.
490     *
491     * @param tokenType the type of token
492     * @return true if token is of specified type.
493     */
494    private static boolean astTypeIsClassOrEnumOrRecordDef(int tokenType) {
495        return tokenType == TokenTypes.CLASS_DEF
496                || tokenType == TokenTypes.RECORD_DEF
497                || tokenType == TokenTypes.ENUM_DEF;
498    }
499
500    /**
501     * Holds the names of fields of a type.
502     */
503    private static final class FieldFrame {
504
505        /** Parent frame. */
506        private final FieldFrame parent;
507
508        /** Set of frame's children. */
509        private final Set<FieldFrame> children = new HashSet<>();
510
511        /** Map of field name to field DetailAst. */
512        private final Map<String, DetailAST> fieldNameToAst = new HashMap<>();
513
514        /** Set of equals calls. */
515        private final Set<DetailAST> methodCalls = new HashSet<>();
516
517        /** Name of the class, enum or enum constant declaration. */
518        private String frameName;
519
520        /** Whether the frame is CLASS_DEF, ENUM_DEF, ENUM_CONST_DEF, or RECORD_DEF. */
521        private boolean classOrEnumOrRecordDef;
522
523        /**
524         * Creates new frame.
525         *
526         * @param parent parent frame.
527         */
528        private FieldFrame(FieldFrame parent) {
529            this.parent = parent;
530        }
531
532        /**
533         * Set the frame name.
534         *
535         * @param frameName value to set.
536         */
537        /* package */ void setFrameName(String frameName) {
538            this.frameName = frameName;
539        }
540
541        /**
542         * Getter for the frame name.
543         *
544         * @return frame name.
545         */
546        /* package */ String getFrameName() {
547            return frameName;
548        }
549
550        /**
551         * Getter for the parent frame.
552         *
553         * @return parent frame.
554         */
555        /* package */ FieldFrame getParent() {
556            return parent;
557        }
558
559        /**
560         * Getter for frame's children.
561         *
562         * @return children of this frame.
563         */
564        /* package */ Set<FieldFrame> getChildren() {
565            return Collections.unmodifiableSet(children);
566        }
567
568        /**
569         * Add child frame to this frame.
570         *
571         * @param child frame to add.
572         */
573        /* package */ void addChild(FieldFrame child) {
574            children.add(child);
575        }
576
577        /**
578         * Add field to this FieldFrame.
579         *
580         * @param field the ast of the field.
581         */
582        /* package */ void addField(DetailAST field) {
583            if (field.findFirstToken(TokenTypes.IDENT) != null) {
584                fieldNameToAst.put(getFieldName(field), field);
585            }
586        }
587
588        /**
589         * Sets isClassOrEnumOrRecordDef.
590         *
591         * @param value value to set.
592         */
593        /* package */ void setClassOrEnumOrRecordDef(boolean value) {
594            classOrEnumOrRecordDef = value;
595        }
596
597        /**
598         * Getter for classOrEnumOrRecordDef.
599         *
600         * @return classOrEnumOrRecordDef.
601         */
602        /* package */ boolean isClassOrEnumOrRecordDef() {
603            return classOrEnumOrRecordDef;
604        }
605
606        /**
607         * Add method call to this frame.
608         *
609         * @param methodCall METHOD_CALL ast.
610         */
611        /* package */ void addMethodCall(DetailAST methodCall) {
612            methodCalls.add(methodCall);
613        }
614
615        /**
616         * Determines whether this FieldFrame contains the field.
617         *
618         * @param name name of the field to check.
619         * @return DetailAST if this FieldFrame contains instance field.
620         */
621        /* package */ DetailAST findField(String name) {
622            return fieldNameToAst.get(name);
623        }
624
625        /**
626         * Getter for frame's method calls.
627         *
628         * @return method calls of this frame.
629         */
630        /* package */ Set<DetailAST> getMethodCalls() {
631            return Collections.unmodifiableSet(methodCalls);
632        }
633
634        /**
635         * Get the name of the field.
636         *
637         * @param field to get the name from.
638         * @return name of the field.
639         */
640        private static String getFieldName(DetailAST field) {
641            return TokenUtil.getIdent(field).getText();
642        }
643
644    }
645
646}