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