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