001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2024 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.coding;
021
022import java.util.Collections;
023import java.util.HashMap;
024import java.util.HashSet;
025import java.util.Map;
026import java.util.Set;
027
028import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
029import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
030import com.puppycrawl.tools.checkstyle.api.DetailAST;
031import com.puppycrawl.tools.checkstyle.api.TokenTypes;
032import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
033
034/**
035 * <div>
036 * Checks that any combination of String literals
037 * is on the left side of an {@code equals()} comparison.
038 * Also checks for String literals assigned to some field
039 * (such as {@code someString.equals(anotherString = "text")}).
040 * </div>
041 *
042 * <p>Rationale: Calling the {@code equals()} method on String literals
043 * will avoid a potential {@code NullPointerException}. Also, it is
044 * pretty common to see null checks right before equals comparisons
045 * but following this rule such checks are not required.
046 * </p>
047 * <ul>
048 * <li>
049 * Property {@code ignoreEqualsIgnoreCase} - Control whether to ignore
050 * {@code String.equalsIgnoreCase(String)} invocations.
051 * Type is {@code boolean}.
052 * Default value is {@code false}.
053 * </li>
054 * </ul>
055 *
056 * <p>
057 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
058 * </p>
059 *
060 * <p>
061 * Violation Message Keys:
062 * </p>
063 * <ul>
064 * <li>
065 * {@code equals.avoid.null}
066 * </li>
067 * <li>
068 * {@code equalsIgnoreCase.avoid.null}
069 * </li>
070 * </ul>
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            TokenTypes.PATTERN_VARIABLE_DEF,
134            TokenTypes.RECORD_DEF,
135            TokenTypes.COMPACT_CTOR_DEF,
136            TokenTypes.RECORD_COMPONENT_DEF,
137        };
138    }
139
140    /**
141     * Setter to control whether to ignore {@code String.equalsIgnoreCase(String)} invocations.
142     *
143     * @param newValue whether to ignore checking
144     *     {@code String.equalsIgnoreCase(String)}.
145     * @since 5.4
146     */
147    public void setIgnoreEqualsIgnoreCase(boolean newValue) {
148        ignoreEqualsIgnoreCase = newValue;
149    }
150
151    @Override
152    public void beginTree(DetailAST rootAST) {
153        currentFrame = new FieldFrame(null);
154    }
155
156    @Override
157    public void visitToken(final DetailAST ast) {
158        switch (ast.getType()) {
159            case TokenTypes.VARIABLE_DEF:
160            case TokenTypes.PARAMETER_DEF:
161            case TokenTypes.PATTERN_VARIABLE_DEF:
162            case TokenTypes.RECORD_COMPONENT_DEF:
163                currentFrame.addField(ast);
164                break;
165            case TokenTypes.METHOD_CALL:
166                processMethodCall(ast);
167                break;
168            case TokenTypes.SLIST:
169                processSlist(ast);
170                break;
171            case TokenTypes.LITERAL_NEW:
172                processLiteralNew(ast);
173                break;
174            case TokenTypes.OBJBLOCK:
175                final int parentType = ast.getParent().getType();
176                if (!astTypeIsClassOrEnumOrRecordDef(parentType)) {
177                    processFrame(ast);
178                }
179                break;
180            default:
181                processFrame(ast);
182        }
183    }
184
185    @Override
186    public void leaveToken(DetailAST ast) {
187        switch (ast.getType()) {
188            case TokenTypes.SLIST:
189                leaveSlist(ast);
190                break;
191            case TokenTypes.LITERAL_NEW:
192                leaveLiteralNew(ast);
193                break;
194            case TokenTypes.OBJBLOCK:
195                final int parentType = ast.getParent().getType();
196                if (!astTypeIsClassOrEnumOrRecordDef(parentType)) {
197                    currentFrame = currentFrame.getParent();
198                }
199                break;
200            case TokenTypes.VARIABLE_DEF:
201            case TokenTypes.PARAMETER_DEF:
202            case TokenTypes.RECORD_COMPONENT_DEF:
203            case TokenTypes.METHOD_CALL:
204            case TokenTypes.PATTERN_VARIABLE_DEF:
205                break;
206            default:
207                currentFrame = currentFrame.getParent();
208                break;
209        }
210    }
211
212    @Override
213    public void finishTree(DetailAST ast) {
214        traverseFieldFrameTree(currentFrame);
215    }
216
217    /**
218     * Determine whether SLIST begins a block, determined by braces, and add it as
219     * a frame in this case.
220     *
221     * @param ast SLIST ast.
222     */
223    private void processSlist(DetailAST ast) {
224        if (LEFT_CURLY.equals(ast.getText())) {
225            final FieldFrame frame = new FieldFrame(currentFrame);
226            currentFrame.addChild(frame);
227            currentFrame = frame;
228        }
229    }
230
231    /**
232     * Determine whether SLIST begins a block, determined by braces.
233     *
234     * @param ast SLIST ast.
235     */
236    private void leaveSlist(DetailAST ast) {
237        if (LEFT_CURLY.equals(ast.getText())) {
238            currentFrame = currentFrame.getParent();
239        }
240    }
241
242    /**
243     * Process CLASS_DEF, METHOD_DEF, LITERAL_IF, LITERAL_FOR, LITERAL_WHILE, LITERAL_DO,
244     * LITERAL_CATCH, LITERAL_TRY, CTOR_DEF, ENUM_DEF, ENUM_CONSTANT_DEF.
245     *
246     * @param ast processed ast.
247     */
248    private void processFrame(DetailAST ast) {
249        final FieldFrame frame = new FieldFrame(currentFrame);
250        final int astType = ast.getType();
251        if (astTypeIsClassOrEnumOrRecordDef(astType)) {
252            frame.setClassOrEnumOrRecordDef(true);
253            frame.setFrameName(ast.findFirstToken(TokenTypes.IDENT).getText());
254        }
255        currentFrame.addChild(frame);
256        currentFrame = frame;
257    }
258
259    /**
260     * Add the method call to the current frame if it should be processed.
261     *
262     * @param methodCall METHOD_CALL ast.
263     */
264    private void processMethodCall(DetailAST methodCall) {
265        final DetailAST dot = methodCall.getFirstChild();
266        if (dot.getType() == TokenTypes.DOT) {
267            final String methodName = dot.getLastChild().getText();
268            if (EQUALS.equals(methodName)
269                    || !ignoreEqualsIgnoreCase && "equalsIgnoreCase".equals(methodName)) {
270                currentFrame.addMethodCall(methodCall);
271            }
272        }
273    }
274
275    /**
276     * Determine whether LITERAL_NEW is an anonymous class definition and add it as
277     * a frame in this case.
278     *
279     * @param ast LITERAL_NEW ast.
280     */
281    private void processLiteralNew(DetailAST ast) {
282        if (ast.findFirstToken(TokenTypes.OBJBLOCK) != null) {
283            final FieldFrame frame = new FieldFrame(currentFrame);
284            currentFrame.addChild(frame);
285            currentFrame = frame;
286        }
287    }
288
289    /**
290     * Determine whether LITERAL_NEW is an anonymous class definition and leave
291     * the frame it is in.
292     *
293     * @param ast LITERAL_NEW ast.
294     */
295    private void leaveLiteralNew(DetailAST ast) {
296        if (ast.findFirstToken(TokenTypes.OBJBLOCK) != null) {
297            currentFrame = currentFrame.getParent();
298        }
299    }
300
301    /**
302     * Traverse the tree of the field frames to check all equals method calls.
303     *
304     * @param frame to check method calls in.
305     */
306    private void traverseFieldFrameTree(FieldFrame frame) {
307        for (FieldFrame child: frame.getChildren()) {
308            traverseFieldFrameTree(child);
309
310            currentFrame = child;
311            child.getMethodCalls().forEach(this::checkMethodCall);
312        }
313    }
314
315    /**
316     * Check whether the method call should be violated.
317     *
318     * @param methodCall method call to check.
319     */
320    private void checkMethodCall(DetailAST methodCall) {
321        DetailAST objCalledOn = methodCall.getFirstChild().getFirstChild();
322        if (objCalledOn.getType() == TokenTypes.DOT) {
323            objCalledOn = objCalledOn.getLastChild();
324        }
325        final DetailAST expr = methodCall.findFirstToken(TokenTypes.ELIST).getFirstChild();
326        if (containsOneArgument(methodCall)
327                && containsAllSafeTokens(expr)
328                && isCalledOnStringFieldOrVariable(objCalledOn)) {
329            final String methodName = methodCall.getFirstChild().getLastChild().getText();
330            if (EQUALS.equals(methodName)) {
331                log(methodCall, MSG_EQUALS_AVOID_NULL);
332            }
333            else {
334                log(methodCall, MSG_EQUALS_IGNORE_CASE_AVOID_NULL);
335            }
336        }
337    }
338
339    /**
340     * Verify that method call has one argument.
341     *
342     * @param methodCall METHOD_CALL DetailAST
343     * @return true if method call has one argument.
344     */
345    private static boolean containsOneArgument(DetailAST methodCall) {
346        final DetailAST elist = methodCall.findFirstToken(TokenTypes.ELIST);
347        return elist.getChildCount() == 1;
348    }
349
350    /**
351     * Looks for all "safe" Token combinations in the argument
352     * expression branch.
353     *
354     * @param expr the argument expression
355     * @return - true if any child matches the set of tokens, false if not
356     */
357    private static boolean containsAllSafeTokens(final DetailAST expr) {
358        DetailAST arg = expr.getFirstChild();
359        arg = skipVariableAssign(arg);
360
361        boolean argIsNotNull = false;
362        if (arg.getType() == TokenTypes.PLUS) {
363            DetailAST child = arg.getFirstChild();
364            while (child != null
365                    && !argIsNotNull) {
366                argIsNotNull = child.getType() == TokenTypes.STRING_LITERAL
367                        || child.getType() == TokenTypes.TEXT_BLOCK_LITERAL_BEGIN
368                        || child.getType() == TokenTypes.IDENT;
369                child = child.getNextSibling();
370            }
371        }
372        else {
373            argIsNotNull = arg.getType() == TokenTypes.STRING_LITERAL
374                    || arg.getType() == TokenTypes.TEXT_BLOCK_LITERAL_BEGIN;
375        }
376
377        return argIsNotNull;
378    }
379
380    /**
381     * Skips over an inner assign portion of an argument expression.
382     *
383     * @param currentAST current token in the argument expression
384     * @return the next relevant token
385     */
386    private static DetailAST skipVariableAssign(final DetailAST currentAST) {
387        DetailAST result = currentAST;
388        while (result.getType() == TokenTypes.LPAREN) {
389            result = result.getNextSibling();
390        }
391        if (result.getType() == TokenTypes.ASSIGN) {
392            result = result.getFirstChild().getNextSibling();
393        }
394        return result;
395    }
396
397    /**
398     * Determine, whether equals method is called on a field of String type.
399     *
400     * @param objCalledOn object ast.
401     * @return true if the object is of String type.
402     */
403    private boolean isCalledOnStringFieldOrVariable(DetailAST objCalledOn) {
404        final boolean result;
405        final DetailAST previousSiblingAst = objCalledOn.getPreviousSibling();
406        if (previousSiblingAst == null) {
407            result = isStringFieldOrVariable(objCalledOn);
408        }
409        else {
410            if (previousSiblingAst.getType() == TokenTypes.LITERAL_THIS) {
411                result = isStringFieldOrVariableFromThisInstance(objCalledOn);
412            }
413            else {
414                final String className = previousSiblingAst.getText();
415                result = isStringFieldOrVariableFromClass(objCalledOn, className);
416            }
417        }
418        return result;
419    }
420
421    /**
422     * Whether the field or the variable is of String type.
423     *
424     * @param objCalledOn the field or the variable to check.
425     * @return true if the field or the variable is of String type.
426     */
427    private boolean isStringFieldOrVariable(DetailAST objCalledOn) {
428        boolean result = false;
429        final String name = objCalledOn.getText();
430        FieldFrame frame = currentFrame;
431        while (frame != null) {
432            final DetailAST field = frame.findField(name);
433            if (field != null
434                    && (frame.isClassOrEnumOrRecordDef()
435                            || CheckUtil.isBeforeInSource(field, objCalledOn))) {
436                result = STRING.equals(getFieldType(field));
437                break;
438            }
439            frame = frame.getParent();
440        }
441        return result;
442    }
443
444    /**
445     * Whether the field or the variable from THIS instance is of String type.
446     *
447     * @param objCalledOn the field or the variable from THIS instance to check.
448     * @return true if the field or the variable from THIS instance is of String type.
449     */
450    private boolean isStringFieldOrVariableFromThisInstance(DetailAST objCalledOn) {
451        final String name = objCalledOn.getText();
452        final DetailAST field = getObjectFrame(currentFrame).findField(name);
453        return field != null && STRING.equals(getFieldType(field));
454    }
455
456    /**
457     * Whether the field or the variable from the specified class is of String type.
458     *
459     * @param objCalledOn the field or the variable from the specified class to check.
460     * @param className the name of the class to check in.
461     * @return true if the field or the variable from the specified class is of String type.
462     */
463    private boolean isStringFieldOrVariableFromClass(DetailAST objCalledOn,
464            final String className) {
465        boolean result = false;
466        final String name = objCalledOn.getText();
467        FieldFrame frame = currentFrame;
468        while (frame != null) {
469            if (className.equals(frame.getFrameName())) {
470                final DetailAST field = frame.findField(name);
471                result = STRING.equals(getFieldType(field));
472                break;
473            }
474            frame = frame.getParent();
475        }
476        return result;
477    }
478
479    /**
480     * Get the nearest parent frame which is CLASS_DEF, ENUM_DEF or ENUM_CONST_DEF.
481     *
482     * @param frame to start the search from.
483     * @return the nearest parent frame which is CLASS_DEF, ENUM_DEF or ENUM_CONST_DEF.
484     */
485    private static FieldFrame getObjectFrame(FieldFrame frame) {
486        FieldFrame objectFrame = frame;
487        while (!objectFrame.isClassOrEnumOrRecordDef()) {
488            objectFrame = objectFrame.getParent();
489        }
490        return objectFrame;
491    }
492
493    /**
494     * Get field type.
495     *
496     * @param field to get the type from.
497     * @return type of the field.
498     */
499    private static String getFieldType(DetailAST field) {
500        String fieldType = null;
501        final DetailAST identAst = field.findFirstToken(TokenTypes.TYPE)
502                .findFirstToken(TokenTypes.IDENT);
503        if (identAst != null) {
504            fieldType = identAst.getText();
505        }
506        return fieldType;
507    }
508
509    /**
510     * Verify that a token is either CLASS_DEF, RECORD_DEF, or ENUM_DEF.
511     *
512     * @param tokenType the type of token
513     * @return true if token is of specified type.
514     */
515    private static boolean astTypeIsClassOrEnumOrRecordDef(int tokenType) {
516        return tokenType == TokenTypes.CLASS_DEF
517                || tokenType == TokenTypes.RECORD_DEF
518                || tokenType == TokenTypes.ENUM_DEF;
519    }
520
521    /**
522     * Holds the names of fields of a type.
523     */
524    private static final class FieldFrame {
525
526        /** Parent frame. */
527        private final FieldFrame parent;
528
529        /** Set of frame's children. */
530        private final Set<FieldFrame> children = new HashSet<>();
531
532        /** Map of field name to field DetailAst. */
533        private final Map<String, DetailAST> fieldNameToAst = new HashMap<>();
534
535        /** Set of equals calls. */
536        private final Set<DetailAST> methodCalls = new HashSet<>();
537
538        /** Name of the class, enum or enum constant declaration. */
539        private String frameName;
540
541        /** Whether the frame is CLASS_DEF, ENUM_DEF, ENUM_CONST_DEF, or RECORD_DEF. */
542        private boolean classOrEnumOrRecordDef;
543
544        /**
545         * Creates new frame.
546         *
547         * @param parent parent frame.
548         */
549        private FieldFrame(FieldFrame parent) {
550            this.parent = parent;
551        }
552
553        /**
554         * Set the frame name.
555         *
556         * @param frameName value to set.
557         */
558        public void setFrameName(String frameName) {
559            this.frameName = frameName;
560        }
561
562        /**
563         * Getter for the frame name.
564         *
565         * @return frame name.
566         */
567        public String getFrameName() {
568            return frameName;
569        }
570
571        /**
572         * Getter for the parent frame.
573         *
574         * @return parent frame.
575         */
576        public FieldFrame getParent() {
577            return parent;
578        }
579
580        /**
581         * Getter for frame's children.
582         *
583         * @return children of this frame.
584         */
585        public Set<FieldFrame> getChildren() {
586            return Collections.unmodifiableSet(children);
587        }
588
589        /**
590         * Add child frame to this frame.
591         *
592         * @param child frame to add.
593         */
594        public void addChild(FieldFrame child) {
595            children.add(child);
596        }
597
598        /**
599         * Add field to this FieldFrame.
600         *
601         * @param field the ast of the field.
602         */
603        public void addField(DetailAST field) {
604            if (field.findFirstToken(TokenTypes.IDENT) != null) {
605                fieldNameToAst.put(getFieldName(field), field);
606            }
607        }
608
609        /**
610         * Sets isClassOrEnumOrRecordDef.
611         *
612         * @param value value to set.
613         */
614        public void setClassOrEnumOrRecordDef(boolean value) {
615            classOrEnumOrRecordDef = value;
616        }
617
618        /**
619         * Getter for classOrEnumOrRecordDef.
620         *
621         * @return classOrEnumOrRecordDef.
622         */
623        public boolean isClassOrEnumOrRecordDef() {
624            return classOrEnumOrRecordDef;
625        }
626
627        /**
628         * Add method call to this frame.
629         *
630         * @param methodCall METHOD_CALL ast.
631         */
632        public void addMethodCall(DetailAST methodCall) {
633            methodCalls.add(methodCall);
634        }
635
636        /**
637         * Determines whether this FieldFrame contains the field.
638         *
639         * @param name name of the field to check.
640         * @return DetailAST if this FieldFrame contains instance field.
641         */
642        public DetailAST findField(String name) {
643            return fieldNameToAst.get(name);
644        }
645
646        /**
647         * Getter for frame's method calls.
648         *
649         * @return method calls of this frame.
650         */
651        public Set<DetailAST> getMethodCalls() {
652            return Collections.unmodifiableSet(methodCalls);
653        }
654
655        /**
656         * Get the name of the field.
657         *
658         * @param field to get the name from.
659         * @return name of the field.
660         */
661        private static String getFieldName(DetailAST field) {
662            return field.findFirstToken(TokenTypes.IDENT).getText();
663        }
664
665    }
666
667}