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