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