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