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