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 }