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