001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2015 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.Set;
024
025import com.google.common.collect.Sets;
026import com.puppycrawl.tools.checkstyle.api.Check;
027import com.puppycrawl.tools.checkstyle.api.DetailAST;
028import com.puppycrawl.tools.checkstyle.api.TokenTypes;
029
030/**
031 * Checks that any combination of String literals
032 * is on the left side of an equals() comparison.
033 * Also checks for String literals assigned to some field
034 * (such as <code>someString.equals(anotherString = "text")</code>).
035 *
036 * <p>Rationale: Calling the equals() method on String literals
037 * will avoid a potential NullPointerException.  Also, it is
038 * pretty common to see null check right before equals comparisons
039 * which is not necessary in the below example.
040 *
041 * <p>For example:
042 *
043 * <pre>
044 *  {@code
045 *    String nullString = null;
046 *    nullString.equals(&quot;My_Sweet_String&quot;);
047 *  }
048 * </pre>
049 * should be refactored to
050 *
051 * <pre>
052 *  {@code
053 *    String nullString = null;
054 *    &quot;My_Sweet_String&quot;.equals(nullString);
055 *  }
056 * </pre>
057 *
058 * @author Travis Schneeberger
059 * @author Vladislav Lisetskiy
060 */
061public class EqualsAvoidNullCheck extends Check {
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 getAcceptableTokens();
090    }
091
092    @Override
093    public int[] getAcceptableTokens() {
094        return new int[] {
095            TokenTypes.METHOD_CALL,
096            TokenTypes.CLASS_DEF,
097            TokenTypes.METHOD_DEF,
098            TokenTypes.LITERAL_IF,
099            TokenTypes.LITERAL_FOR,
100            TokenTypes.LITERAL_WHILE,
101            TokenTypes.LITERAL_DO,
102            TokenTypes.LITERAL_CATCH,
103            TokenTypes.LITERAL_TRY,
104            TokenTypes.VARIABLE_DEF,
105            TokenTypes.PARAMETER_DEF,
106            TokenTypes.CTOR_DEF,
107            TokenTypes.SLIST,
108            TokenTypes.ENUM_DEF,
109            TokenTypes.ENUM_CONSTANT_DEF,
110            TokenTypes.LITERAL_NEW,
111        };
112    }
113
114    @Override
115    public int[] getRequiredTokens() {
116        return getAcceptableTokens();
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.branchContains(TokenTypes.LCURLY)) {
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 and
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.branchContains(TokenTypes.LCURLY)) {
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            for (DetailAST methodCall: child.getMethodCalls()) {
262                checkMethodCall(methodCall);
263            }
264        }
265    }
266
267    /**
268     * Check whether the method call should be violated.
269     * @param methodCall method call to check.
270     */
271    private void checkMethodCall(DetailAST methodCall) {
272        DetailAST objCalledOn = methodCall.getFirstChild().getFirstChild();
273        if (objCalledOn.getType() == TokenTypes.DOT) {
274            objCalledOn = objCalledOn.getLastChild();
275        }
276        final DetailAST expr = methodCall.findFirstToken(TokenTypes.ELIST).getFirstChild();
277        if (isObjectValid(objCalledOn)
278                && containsOneArgument(methodCall)
279                && containsAllSafeTokens(expr)
280                && isCalledOnStringField(objCalledOn)) {
281            final String methodName = methodCall.getFirstChild().getLastChild().getText();
282            if (EQUALS.equals(methodName)) {
283                log(methodCall.getLineNo(), methodCall.getColumnNo(),
284                    MSG_EQUALS_AVOID_NULL);
285            }
286            else {
287                log(methodCall.getLineNo(), methodCall.getColumnNo(),
288                    MSG_EQUALS_IGNORE_CASE_AVOID_NULL);
289            }
290        }
291    }
292
293    /**
294     * Check whether the object equals method is called on is not a String literal
295     * and not too complex.
296     * @param objCalledOn the object equals method is called on ast.
297     * @return true if the object is valid.
298     */
299    private static boolean isObjectValid(DetailAST objCalledOn) {
300        boolean result = true;
301        final DetailAST previousSibling = objCalledOn.getPreviousSibling();
302        if (previousSibling != null
303                && previousSibling.getType() == TokenTypes.DOT) {
304            result = false;
305        }
306        if (isStringLiteral(objCalledOn)) {
307            result = false;
308        }
309        return result;
310    }
311
312    /**
313     * Checks for calling equals on String literal and
314     * anon object which cannot be null.
315     * @param objCalledOn object AST
316     * @return if it is string literal
317     */
318    private static boolean isStringLiteral(DetailAST objCalledOn) {
319        return objCalledOn.getType() == TokenTypes.STRING_LITERAL
320                || objCalledOn.getType() == TokenTypes.LITERAL_NEW;
321    }
322
323    /**
324     * Verify that method call has one argument.
325     *
326     * @param methodCall METHOD_CALL DetailAST
327     * @return true if method call has one argument.
328     */
329    private static boolean containsOneArgument(DetailAST methodCall) {
330        final DetailAST elist = methodCall.findFirstToken(TokenTypes.ELIST);
331        return elist.getChildCount() == 1;
332    }
333
334    /**
335     * Looks for all "safe" Token combinations in the argument
336     * expression branch.
337     * @param expr the argument expression
338     * @return - true if any child matches the set of tokens, false if not
339     */
340    private static boolean containsAllSafeTokens(final DetailAST expr) {
341        DetailAST arg = expr.getFirstChild();
342        if (arg.branchContains(TokenTypes.METHOD_CALL)) {
343            return false;
344        }
345        arg = skipVariableAssign(arg);
346
347        //Plus assignment can have ill affects
348        //do not want to recommend moving expression
349        //See example:
350        //String s = "SweetString";
351        //s.equals(s += "SweetString"); //false
352        //s = "SweetString";
353        //(s += "SweetString").equals(s); //true
354
355        return !arg.branchContains(TokenTypes.PLUS_ASSIGN)
356                && !arg.branchContains(TokenTypes.IDENT)
357                && !arg.branchContains(TokenTypes.LITERAL_NULL);
358    }
359
360    /**
361     * Skips over an inner assign portion of an argument expression.
362     * @param currentAST current token in the argument expression
363     * @return the next relevant token
364     */
365    private static DetailAST skipVariableAssign(final DetailAST currentAST) {
366        if (currentAST.getType() == TokenTypes.ASSIGN
367                && currentAST.getFirstChild().getType() == TokenTypes.IDENT) {
368            return currentAST.getFirstChild().getNextSibling();
369        }
370        return currentAST;
371    }
372
373    /**
374     * Determine, whether equals method is called on a field of String type.
375     * @param objCalledOn object ast.
376     * @return true if the object is of String type.
377     */
378    private boolean isCalledOnStringField(DetailAST objCalledOn) {
379        boolean result = false;
380        final DetailAST previousSiblingAst = objCalledOn.getPreviousSibling();
381        final String name = objCalledOn.getText();
382        if (previousSiblingAst != null) {
383            if (previousSiblingAst.getType() == TokenTypes.LITERAL_THIS) {
384                final DetailAST field = getObjectFrame(currentFrame).findField(name);
385                result = STRING.equals(getFieldType(field));
386            }
387            else {
388                final String className = previousSiblingAst.getText();
389                FieldFrame frame = getObjectFrame(currentFrame);
390                while (frame != null) {
391                    if (className.equals(frame.getFrameName())) {
392                        final DetailAST field = frame.findField(name);
393                        result = STRING.equals(getFieldType(field));
394                        break;
395                    }
396                    frame = getObjectFrame(frame.getParent());
397                }
398            }
399        }
400        else {
401            FieldFrame frame = currentFrame;
402            while (frame != null) {
403                final DetailAST field = frame.findField(name);
404                if (field != null
405                        && (frame.isClassOrEnumOrEnumConstDef()
406                                || checkLineNo(field, objCalledOn))) {
407                    result = STRING.equals(getFieldType(field));
408                    break;
409                }
410                frame = frame.getParent();
411            }
412        }
413        return result;
414    }
415
416    /**
417     * Get the nearest parent frame which is CLASS_DEF, ENUM_DEF or ENUM_CONST_DEF.
418     * @param frame to start the search from.
419     * @return the nearest parent frame which is CLASS_DEF, ENUM_DEF or ENUM_CONST_DEF.
420     */
421    private static FieldFrame getObjectFrame(FieldFrame frame) {
422        FieldFrame objectFrame = frame;
423        while (objectFrame != null && !objectFrame.isClassOrEnumOrEnumConstDef()) {
424            objectFrame = objectFrame.getParent();
425        }
426        return objectFrame;
427    }
428
429    /**
430     * Check whether the field is declared before the method call in case of
431     * methods and initialization blocks.
432     * @param field field to check.
433     * @param objCalledOn object equals method called on.
434     * @return true if the field is declared before the method call.
435     */
436    private static boolean checkLineNo(DetailAST field, DetailAST objCalledOn) {
437        boolean result = false;
438        if (field.getLineNo() < objCalledOn.getLineNo()
439                || field.getLineNo() == objCalledOn.getLineNo()
440                    && field.getColumnNo() < objCalledOn.getColumnNo()) {
441            result = true;
442        }
443        return result;
444    }
445
446    /**
447     * Get field type.
448     * @param field to get the type from.
449     * @return type of the field.
450     */
451    private static String getFieldType(DetailAST field) {
452        String fieldType = null;
453        final DetailAST identAst = field.findFirstToken(TokenTypes.TYPE)
454                .findFirstToken(TokenTypes.IDENT);
455        if (identAst != null) {
456            fieldType = identAst.getText();
457        }
458        return fieldType;
459    }
460
461    /**
462     * Holds the names of fields of a type.
463     */
464    private static class FieldFrame {
465        /** Name of the class, enum or enum constant declaration. */
466        private String frameName;
467
468        /** Parent frame. */
469        private final FieldFrame parent;
470
471        /** Set of frame's children. */
472        private final Set<FieldFrame> children = Sets.newHashSet();
473
474        /** Set of fields. */
475        private final Set<DetailAST> fields = Sets.newHashSet();
476
477        /** Set of equals calls. */
478        private final Set<DetailAST> methodCalls = Sets.newHashSet();
479
480        /** Whether the frame is CLASS_DEF, ENUM_DEF or ENUM_CONST_DEF. */
481        private boolean classOrEnumOrEnumConstDef;
482
483        /**
484         * Creates new frame.
485         * @param parent parent frame.
486         */
487        FieldFrame(FieldFrame parent) {
488            this.parent = parent;
489        }
490
491        /**
492         * Set the frame name.
493         * @param frameName value to set.
494         */
495        public void setFrameName(String frameName) {
496            this.frameName = frameName;
497        }
498
499        /**
500         * Getter for the frame name.
501         * @return frame name.
502         */
503        public String getFrameName() {
504            return frameName;
505        }
506
507        /**
508         * Getter for the parent frame.
509         * @return parent frame.
510         */
511        public FieldFrame getParent() {
512            return parent;
513        }
514
515        /**
516         * Getter for frame's children.
517         * @return children of this frame.
518         */
519        public Set<FieldFrame> getChildren() {
520            return Collections.unmodifiableSet(children);
521        }
522
523        /**
524         * Add child frame to this frame.
525         * @param child frame to add.
526         */
527        public void addChild(FieldFrame child) {
528            children.add(child);
529        }
530
531        /**
532         * Add field to this FieldFrame.
533         * @param field the ast of the field.
534         */
535        public void addField(DetailAST field) {
536            fields.add(field);
537        }
538
539        /**
540         * Sets isClassOrEnum.
541         * @param value value to set.
542         */
543        public void setClassOrEnumOrEnumConstDef(boolean value) {
544            classOrEnumOrEnumConstDef = value;
545        }
546
547        /**
548         * Getter for classOrEnumOrEnumConstDef.
549         * @return classOrEnumOrEnumConstDef.
550         */
551        public boolean isClassOrEnumOrEnumConstDef() {
552            return classOrEnumOrEnumConstDef;
553        }
554
555        /**
556         * Add method call to this frame.
557         * @param methodCall METHOD_CALL ast.
558         */
559        public void addMethodCall(DetailAST methodCall) {
560            methodCalls.add(methodCall);
561        }
562
563        /**
564         * Determines whether this FieldFrame contains the field.
565         * @param name name of the field to check.
566         * @return true if this FieldFrame contains instance field field.
567         */
568        public DetailAST findField(String name) {
569            for (DetailAST field: fields) {
570                if (getFieldName(field).equals(name)) {
571                    return field;
572                }
573            }
574            return null;
575        }
576
577        /**
578         * Getter for frame's method calls.
579         * @return method calls of this frame.
580         */
581        public Set<DetailAST> getMethodCalls() {
582            return Collections.unmodifiableSet(methodCalls);
583        }
584
585        /**
586         * Get the name of the field.
587         * @param field to get the name from.
588         * @return name of the field.
589         */
590        private static String getFieldName(DetailAST field) {
591            return field.findFirstToken(TokenTypes.IDENT).getText();
592        }
593    }
594}