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.Optional;
11  
12  /**
13   * Checks that String.split is only invoked with regex arguments that the JDK
14   * handles via its fastpath.
15   *
16   * <p>For anything beyond the fastpath, String.split builds a fresh Pattern
17   * on every call, which is wasteful in tight loops. Extract the regex into a
18   * private static final Pattern field and use Pattern.split(CharSequence)
19   * instead.</p>
20   *
21   * <p>The JDK fastpath accepts only a one-char string whose sole character is
22   * not a regex meta character, or a two-char string whose first character is
23   * a backslash and whose second character is not an ASCII letter or digit.</p>
24   *
25   * <p>Examples that are flagged:</p>
26   *
27   * <pre>
28   * "abxxdexxzy".split("xx");
29   * "abxxdexxzy".split("xx", 1);
30   * "abxxdexxzy".split(".");
31   * </pre>
32   *
33   * <p>Examples that are accepted:</p>
34   *
35   * <pre>
36   * "abxdexzy".split("x");
37   * "abxdexzy".split("x", 2);
38   * "abxdexzy".split("\n");
39   * "ab.ex.zy".split("\\.");
40   * </pre>
41   *
42   * <p>The check only reports calls whose first argument is a string literal:
43   * when the regex is a variable the optimization cannot be determined from
44   * the AST alone.</p>
45   *
46   * @since 0.24
47   */
48  public final class SimpleStringSplitCheck extends AbstractCheck {
49  
50      /**
51       * Default constructor.
52       */
53      public SimpleStringSplitCheck() {
54          // nothing to initialize
55      }
56  
57      @Override
58      public int[] getDefaultTokens() {
59          return new int[] {TokenTypes.METHOD_CALL};
60      }
61  
62      @Override
63      public int[] getAcceptableTokens() {
64          return this.getDefaultTokens();
65      }
66  
67      @Override
68      public int[] getRequiredTokens() {
69          return this.getDefaultTokens();
70      }
71  
72      @Override
73      public void visitToken(final DetailAST ast) {
74          final Optional<DetailAST> literal =
75              SimpleStringSplitCheck.regexLiteral(ast);
76          final Optional<String> regex = literal.flatMap(
77              node -> SimpleStringSplitCheck.decode(node.getText())
78          );
79          if (regex.isPresent()
80              && !SimpleStringSplitCheck.optimized(regex.get())) {
81              this.log(
82                  literal.get(),
83                  "String.split regex is not JDK-optimized, use a precompiled java.util.regex.Pattern"
84              );
85          }
86      }
87  
88      private static Optional<DetailAST> regexLiteral(final DetailAST call) {
89          Optional<DetailAST> result = Optional.empty();
90          if (SimpleStringSplitCheck.isSplitCall(call)) {
91              result = SimpleStringSplitCheck.firstLiteralArg(call);
92          }
93          return result;
94      }
95  
96      private static boolean isSplitCall(final DetailAST call) {
97          final DetailAST dot = call.getFirstChild();
98          final boolean result;
99          if (dot == null || dot.getType() != TokenTypes.DOT) {
100             result = false;
101         } else {
102             final DetailAST name = dot.getLastChild();
103             result = name != null && name.getType() == TokenTypes.IDENT
104                 && "split".equals(name.getText());
105         }
106         return result;
107     }
108 
109     private static Optional<DetailAST> firstLiteralArg(final DetailAST call) {
110         final DetailAST elist = call.findFirstToken(TokenTypes.ELIST);
111         Optional<DetailAST> result = Optional.empty();
112         if (elist != null && SimpleStringSplitCheck.isOneOrTwoArgs(elist)) {
113             final DetailAST expr = elist.findFirstToken(TokenTypes.EXPR);
114             if (expr != null && SimpleStringSplitCheck.isLoneStringLiteral(expr)) {
115                 result = Optional.of(expr.getFirstChild());
116             }
117         }
118         return result;
119     }
120 
121     private static boolean isOneOrTwoArgs(final DetailAST elist) {
122         final int args = elist.getChildCount(TokenTypes.EXPR);
123         return args == 1 || args == 2;
124     }
125 
126     private static boolean isLoneStringLiteral(final DetailAST expr) {
127         return expr.getChildCount() == 1
128             && expr.getFirstChild().getType() == TokenTypes.STRING_LITERAL;
129     }
130 
131     private static boolean optimized(final String regex) {
132         final boolean result;
133         final int len = regex.length();
134         if (len == 1) {
135             result = ".$|()[{^?*+\\".indexOf(regex.charAt(0)) < 0;
136         } else if (len == 2 && regex.charAt(0) == '\\') {
137             result = !SimpleStringSplitCheck.isAsciiAlphanumeric(regex.charAt(1));
138         } else {
139             result = false;
140         }
141         return result;
142     }
143 
144     private static boolean isAsciiAlphanumeric(final char chr) {
145         return SimpleStringSplitCheck.isAsciiDigit(chr)
146             || SimpleStringSplitCheck.isAsciiLetter(chr);
147     }
148 
149     private static boolean isAsciiDigit(final char chr) {
150         return chr >= '0' && chr <= '9';
151     }
152 
153     private static boolean isAsciiLetter(final char chr) {
154         return chr >= 'a' && chr <= 'z'
155             || chr >= 'A' && chr <= 'Z';
156     }
157 
158     private static Optional<String> decode(final String text) {
159         final String body = text.substring(1, text.length() - 1);
160         final StringBuilder out = new StringBuilder(body.length());
161         int idx = 0;
162         boolean failed = false;
163         while (idx < body.length() && !failed) {
164             final int advance = SimpleStringSplitCheck.step(body, idx, out);
165             if (advance < 0) {
166                 failed = true;
167             } else {
168                 idx += advance;
169             }
170         }
171         final Optional<String> result;
172         if (failed) {
173             result = Optional.empty();
174         } else {
175             result = Optional.of(out.toString());
176         }
177         return result;
178     }
179 
180     private static int step(
181         final String body, final int idx, final StringBuilder out
182     ) {
183         final char chr = body.charAt(idx);
184         final int advance;
185         if (chr == '\\') {
186             advance = SimpleStringSplitCheck.handleEscape(body, idx, out);
187         } else {
188             out.append(chr);
189             advance = 1;
190         }
191         return advance;
192     }
193 
194     private static int handleEscape(
195         final String body, final int idx, final StringBuilder out
196     ) {
197         final int advance;
198         if (idx + 1 >= body.length()) {
199             advance = -1;
200         } else {
201             final int decoded = SimpleStringSplitCheck.escape(
202                 body.charAt(idx + 1)
203             );
204             if (decoded < 0) {
205                 advance = -1;
206             } else {
207                 out.append((char) decoded);
208                 advance = 2;
209             }
210         }
211         return advance;
212     }
213 
214     private static int escape(final char chr) {
215         return switch (chr) {
216             case 'n' -> '\n';
217             case 't' -> '\t';
218             case 'r' -> '\r';
219             case 'b' -> '\b';
220             case 'f' -> '\f';
221             case 's' -> ' ';
222             case '\'' -> '\'';
223             case '"' -> '"';
224             case '\\' -> '\\';
225             default -> -1;
226         };
227     }
228 }