View Javadoc
1   /*
2    * SPDX-FileCopyrightText: Copyright (c) 2011-2026 Yegor Bugayenko
3    * SPDX-License-Identifier: MIT
4    */
5   package com.qulice.pmd.rules;
6   
7   import net.sourceforge.pmd.lang.java.ast.ASTExpression;
8   import net.sourceforge.pmd.lang.java.ast.ASTInfixExpression;
9   import net.sourceforge.pmd.lang.java.ast.ASTMethodCall;
10  import net.sourceforge.pmd.lang.java.ast.ASTNumericLiteral;
11  import net.sourceforge.pmd.lang.java.ast.BinaryOp;
12  import net.sourceforge.pmd.lang.java.rule.AbstractJavaRulechainRule;
13  import net.sourceforge.pmd.lang.java.types.JTypeMirror;
14  
15  /**
16   * Rule to prohibit use of String.length() when checking for empty string.
17   * String.isEmpty() should be used instead.
18   *
19   * <p>Only comparisons that have an exact {@code isEmpty()} equivalent are
20   * flagged. With {@code length()} on the left, those are {@code == 0},
21   * {@code != 0}, {@code > 0}, {@code <= 0}, {@code < 1} and {@code >= 1};
22   * the same six are recognised when the operands are swapped. Comparisons
23   * against {@code 1} such as {@code length() == 1} or {@code length() > 1}
24   * describe single-character strings and have no {@code isEmpty()}
25   * counterpart, so they are left alone.</p>
26   *
27   * @since 0.18
28   */
29  public final class UseStringIsEmptyRule extends AbstractJavaRulechainRule {
30  
31      /**
32       * Default constructor.
33       */
34      public UseStringIsEmptyRule() {
35          super(ASTInfixExpression.class);
36      }
37  
38      @Override
39      public Object visit(final ASTInfixExpression expr, final Object data) {
40          final ASTExpression left = expr.getLeftOperand();
41          final ASTExpression right = expr.getRightOperand();
42          if (isEmptyEquivalent(expr.getOperator(), left, right)
43              || isEmptyEquivalent(mirror(expr.getOperator()), right, left)
44          ) {
45              asCtx(data).addViolation(expr);
46          }
47          return data;
48      }
49  
50      private static boolean isEmptyEquivalent(
51          final BinaryOp operator,
52          final ASTExpression length,
53          final ASTExpression literal
54      ) {
55          return isStringLength(length) && isWhitelisted(operator, literal);
56      }
57  
58      private static boolean isWhitelisted(
59          final BinaryOp operator,
60          final ASTExpression literal
61      ) {
62          boolean result = false;
63          if (literal instanceof ASTNumericLiteral lit) {
64              final String image = lit.getImage();
65              result = switch (operator) {
66                  case EQ, NE, GT, LE -> "0".equals(image);
67                  case LT, GE -> "1".equals(image);
68                  default -> false;
69              };
70          }
71          return result;
72      }
73  
74      private static BinaryOp mirror(final BinaryOp operator) {
75          return switch (operator) {
76              case GT -> BinaryOp.LT;
77              case LT -> BinaryOp.GT;
78              case GE -> BinaryOp.LE;
79              case LE -> BinaryOp.GE;
80              default -> operator;
81          };
82      }
83  
84      private static boolean isStringLength(final ASTExpression expr) {
85          boolean result = false;
86          if (expr instanceof ASTMethodCall call && call.getQualifier() != null) {
87              result = "length".equals(call.getMethodName())
88                  && call.getArguments().isEmpty()
89                  && isStringExpression(call.getQualifier());
90          }
91          return result;
92      }
93  
94      private static boolean isStringExpression(final ASTExpression expr) {
95          final JTypeMirror type = expr.getTypeMirror();
96          return type.isClassOrInterface()
97              && String.class.getName().equals(type.toString());
98      }
99  }