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