View Javadoc
1   /*
2    * SPDX-FileCopyrightText: Copyright (c) 2011-2026 Yegor Bugayenko
3    * SPDX-License-Identifier: MIT
4    */
5   package com.qulice.checkstyle;
6   
7   import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
8   import com.puppycrawl.tools.checkstyle.api.DetailAST;
9   import com.puppycrawl.tools.checkstyle.api.TokenTypes;
10  import java.util.LinkedList;
11  import java.util.List;
12  
13  /**
14   * Checks for not using concatenation of string literals in any form.
15   *
16   * <p>The following constructs are prohibited:
17   *
18   * <pre>
19   * String a = "done in " + time + " seconds";
20   * System.out.println("File not found: " + file);
21   * x += "done";
22   * </pre>
23   *
24   * <p>You should avoid string concatenation at all cost. Why? There are two
25   * reasons: readability of the code and translateability. First of all it's
26   * difficult to understand how the text will look after concatenation,
27   * especially if the text is long and there are more than a few {@code +}
28   * operators. Second, you won't be able to translate your text to other
29   * languages later, if you don't have solid string literals.
30   *
31   * <p>There are two alternatives to concatenation: {@link StringBuilder}
32   * and {@link String#format(String,Object[])}.
33   *
34   * @since 0.3
35   */
36  public final class StringLiteralsConcatenationCheck extends AbstractCheck {
37  
38      @Override
39      public int[] getDefaultTokens() {
40          return new int[] {TokenTypes.OBJBLOCK};
41      }
42  
43      @Override
44      public int[] getAcceptableTokens() {
45          return this.getDefaultTokens();
46      }
47  
48      @Override
49      public int[] getRequiredTokens() {
50          return this.getDefaultTokens();
51      }
52  
53      @Override
54      public void visitToken(final DetailAST ast) {
55          final List<DetailAST> pluses = this.findChildAstsOfType(
56              ast,
57              TokenTypes.PLUS,
58              TokenTypes.PLUS_ASSIGN
59          );
60          for (final DetailAST plus : pluses) {
61              if (this.hasStringLiteralOperand(plus)) {
62                  this.log(plus, "Concatenation of string literals prohibited");
63              }
64          }
65      }
66  
67      /**
68       * Recursively traverse the <code>tree</code> and return all ASTs subtrees
69       * matching any type from <code>types</code>.
70       * @param tree AST to traverse
71       * @param types Token types to match against
72       * @return All ASTs subtrees with token types matching any from
73       *  <tt>types</tt>
74       * @see TokenTypes
75       */
76      private List<DetailAST> findChildAstsOfType(final DetailAST tree,
77          final int... types) {
78          final List<DetailAST> children = new LinkedList<>();
79          DetailAST child = tree.getFirstChild();
80          while (child != null) {
81              if (StringLiteralsConcatenationCheck.isOfType(child, types)) {
82                  children.add(child);
83              } else {
84                  children.addAll(this.findChildAstsOfType(child, types));
85              }
86              child = child.getNextSibling();
87          }
88          return children;
89      }
90  
91      /**
92       * Checks whether an operand of the given PLUS/PLUS_ASSIGN node is a
93       * string literal. Traverses only through nested PLUS/PLUS_ASSIGN nodes
94       * so that STRING_LITERALs nested inside method-call arguments or other
95       * sub-expressions of an operand do not count.
96       * @param node PLUS or PLUS_ASSIGN node to inspect
97       * @return True if any operand of the concatenation chain is a string
98       *  literal, false otherwise
99       */
100     private boolean hasStringLiteralOperand(final DetailAST node) {
101         boolean found = false;
102         DetailAST child = node.getFirstChild();
103         while (child != null) {
104             final int type = child.getType();
105             if (type == TokenTypes.STRING_LITERAL) {
106                 found = true;
107                 break;
108             }
109             if ((type == TokenTypes.PLUS || type == TokenTypes.PLUS_ASSIGN)
110                 && this.hasStringLiteralOperand(child)) {
111                 found = true;
112                 break;
113             }
114             child = child.getNextSibling();
115         }
116         return found;
117     }
118 
119     /**
120      * Checks if this <code>ast</code> is of any type from <code>types</code>.
121      * @param ast AST to check
122      * @param types Token types to match against
123      * @return True if of type, false otherwise
124      * @see TokenTypes
125      */
126     private static boolean isOfType(final DetailAST ast, final int... types) {
127         boolean yes = false;
128         for (final int type : types) {
129             if (ast.getType() == type) {
130                 yes = true;
131                 break;
132             }
133         }
134         return yes;
135     }
136 }