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