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