1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2026 the original author or authors.
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 ///////////////////////////////////////////////////////////////////////////////////////////////
19
20 package com.puppycrawl.tools.checkstyle.checks.regexp;
21
22 import java.util.regex.Matcher;
23 import java.util.regex.Pattern;
24
25 import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
26 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
27 import com.puppycrawl.tools.checkstyle.api.DetailAST;
28 import com.puppycrawl.tools.checkstyle.api.FileContents;
29 import com.puppycrawl.tools.checkstyle.api.FileText;
30 import com.puppycrawl.tools.checkstyle.api.LineColumn;
31 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
32
33 /**
34 * <div>
35 * Checks that a specified pattern exists, exists less than
36 * a set number of times, or does not exist in the file.
37 * </div>
38 *
39 * <p>
40 * This check combines all the functionality provided by
41 * <a href="https://checkstyle.org/checks/header/regexpheader.html">RegexpHeader</a>
42 * except supplying the regular expression from a file.
43 * </p>
44 *
45 * <p>
46 * It differs from them in that it works in multiline mode. Its regular expression
47 * can span multiple lines and it checks this against the whole file at once.
48 * The others work in single-line mode. Their single or multiple regular expressions
49 * can only span one line. They check each of these against each line in the file in turn.
50 * </p>
51 *
52 * <p>
53 * <b>Note:</b> Because of the different mode of operation there may be some
54 * changes in the regular expressions used to achieve a particular end.
55 * </p>
56 *
57 * <p>
58 * In multiline mode...
59 * </p>
60 * <ul>
61 * <li>
62 * {@code ^} means the beginning of a line, as opposed to beginning of the input.
63 * </li>
64 * <li>
65 * For beginning of the input use {@code \A}.
66 * </li>
67 * <li>
68 * {@code $} means the end of a line, as opposed to the end of the input.
69 * </li>
70 * <li>
71 * For end of input use {@code \Z}.
72 * </li>
73 * <li>
74 * Each line in the file is terminated with a line feed character.
75 * </li>
76 * </ul>
77 *
78 * <p>
79 * <b>Note:</b> Not all regular expression engines are created equal.
80 * Some provide extra functions that others do not and some elements
81 * of the syntax may vary. This check makes use of the
82 * <a href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/regex/package-summary.html">
83 * java.util.regex package</a>; please check its documentation for details
84 * of how to construct a regular expression to achieve a particular goal.
85 * </p>
86 *
87 * <p>
88 * <b>Note:</b> When entering a regular expression as a parameter in
89 * the XML config file you must also take into account the XML rules. e.g.
90 * if you want to match a {@literal <} symbol you need to enter {@literal &}lt;.
91 * The regular expression should be entered on one line.
92 * </p>
93 *
94 * <p>
95 * <b>Note:</b> To search for parentheses () in a regular expression
96 * you must escape them like \(\). This is required by the regexp engine,
97 * otherwise it will think they are special instruction characters.
98 * </p>
99 *
100 * <p>
101 * <b>Note:</b> To search for things that mean something in XML, like
102 * {@literal <} you need to escape them like {@literal &}lt;. This is required so the
103 * XML parser does not act on them, but instead passes the correct
104 * character to the regexp engine.
105 * </p>
106 *
107 * @since 4.0
108 */
109 @FileStatefulCheck
110 public class RegexpCheck extends AbstractCheck {
111
112 /**
113 * A key is pointing to the warning message text in "messages.properties"
114 * file.
115 */
116 public static final String MSG_ILLEGAL_REGEXP = "illegal.regexp";
117
118 /**
119 * A key is pointing to the warning message text in "messages.properties"
120 * file.
121 */
122 public static final String MSG_REQUIRED_REGEXP = "required.regexp";
123
124 /**
125 * A key is pointing to the warning message text in "messages.properties"
126 * file.
127 */
128 public static final String MSG_DUPLICATE_REGEXP = "duplicate.regexp";
129
130 /** Default duplicate limit. */
131 private static final int DEFAULT_DUPLICATE_LIMIT = -1;
132
133 /** Default error report limit. */
134 private static final int DEFAULT_ERROR_LIMIT = 100;
135
136 /** Error count exceeded message. */
137 private static final String ERROR_LIMIT_EXCEEDED_MESSAGE =
138 "The error limit has been exceeded, "
139 + "the check is aborting, there may be more unreported errors.";
140
141 /**
142 * Specify message which is used to notify about violations,
143 * if empty then the default (hard-coded) message is used.
144 */
145 private String message;
146
147 /** Control whether to ignore matches found within comments. */
148 private boolean ignoreComments;
149
150 /** Control whether the pattern is required or illegal. */
151 private boolean illegalPattern;
152
153 /** Specify the maximum number of violations before the check will abort. */
154 private int errorLimit = DEFAULT_ERROR_LIMIT;
155
156 /**
157 * Control whether to check for duplicates of a required pattern,
158 * any negative value means no checking for duplicates,
159 * any positive value is used as the maximum number of allowed duplicates,
160 * if the limit is exceeded violations will be logged.
161 */
162 private int duplicateLimit;
163
164 /** Boolean to say if we should check for duplicates. */
165 private boolean checkForDuplicates;
166
167 /** Specify the pattern to match against. */
168 private Pattern format = Pattern.compile("^$", Pattern.MULTILINE);
169
170 /**
171 * Creates a new {@code RegexpCheck} instance.
172 */
173 public RegexpCheck() {
174 // no code by default
175 }
176
177 /**
178 * Setter to specify message which is used to notify about violations,
179 * if empty then the default (hard-coded) message is used.
180 *
181 * @param message custom message which should be used in report.
182 * @since 4.0
183 */
184 public void setMessage(String message) {
185 this.message = message;
186 }
187
188 /**
189 * Setter to control whether to ignore matches found within comments.
190 *
191 * @param ignoreComments True if comments should be ignored.
192 * @since 4.0
193 */
194 public void setIgnoreComments(boolean ignoreComments) {
195 this.ignoreComments = ignoreComments;
196 }
197
198 /**
199 * Setter to control whether the pattern is required or illegal.
200 *
201 * @param illegalPattern True if pattern is not allowed.
202 * @since 4.0
203 */
204 public void setIllegalPattern(boolean illegalPattern) {
205 this.illegalPattern = illegalPattern;
206 }
207
208 /**
209 * Setter to specify the maximum number of violations before the check will abort.
210 *
211 * @param errorLimit the number of errors to report.
212 * @since 4.0
213 */
214 public void setErrorLimit(int errorLimit) {
215 this.errorLimit = errorLimit;
216 }
217
218 /**
219 * Setter to control whether to check for duplicates of a required pattern,
220 * any negative value means no checking for duplicates,
221 * any positive value is used as the maximum number of allowed duplicates,
222 * if the limit is exceeded violations will be logged.
223 *
224 * @param duplicateLimit negative values mean no duplicate checking,
225 * any positive value is used as the limit.
226 * @since 4.0
227 */
228 public void setDuplicateLimit(int duplicateLimit) {
229 this.duplicateLimit = duplicateLimit;
230 checkForDuplicates = duplicateLimit > DEFAULT_DUPLICATE_LIMIT;
231 }
232
233 /**
234 * Setter to specify the pattern to match against.
235 *
236 * @param pattern the new pattern
237 * @since 4.0
238 */
239 public final void setFormat(Pattern pattern) {
240 format = CommonUtil.createPattern(pattern.pattern(), Pattern.MULTILINE);
241 }
242
243 @Override
244 public int[] getDefaultTokens() {
245 return getRequiredTokens();
246 }
247
248 @Override
249 public int[] getAcceptableTokens() {
250 return getRequiredTokens();
251 }
252
253 @Override
254 public int[] getRequiredTokens() {
255 return CommonUtil.EMPTY_INT_ARRAY;
256 }
257
258 @Override
259 public void beginTree(DetailAST rootAST) {
260 processRegexpMatches();
261 }
262
263 /**
264 * Processes the regexp matches and logs the number of errors in the file.
265 *
266 */
267 @SuppressWarnings("deprecation")
268 private void processRegexpMatches() {
269 final Matcher matcher = format.matcher(getFileContents().getText().getFullText());
270 int errorCount = 0;
271 int matchCount = 0;
272 final FileText text = getFileContents().getText();
273 while (errorCount < errorLimit && matcher.find()) {
274 final LineColumn start = text.lineColumn(matcher.start());
275 final int startLine = start.getLine();
276
277 final boolean ignore = isIgnore(startLine, text, start, matcher);
278 if (!ignore) {
279 matchCount++;
280 if (illegalPattern || checkForDuplicates
281 && matchCount - 1 > duplicateLimit) {
282 errorCount++;
283 logMessage(startLine, errorCount);
284 }
285 }
286 }
287 if (!illegalPattern && matchCount == 0) {
288 final String msg = getMessage(errorCount);
289 log(1, MSG_REQUIRED_REGEXP, msg);
290 }
291 }
292
293 /**
294 * Detect ignore situation.
295 *
296 * @param startLine position of line
297 * @param text file text
298 * @param start line column
299 * @param matcher The matcher
300 * @return true is that need to be ignored
301 */
302 @SuppressWarnings("deprecation")
303 private boolean isIgnore(int startLine, FileText text, LineColumn start, Matcher matcher) {
304 final LineColumn end;
305 if (matcher.end() == 0) {
306 end = text.lineColumn(0);
307 }
308 else {
309 end = text.lineColumn(matcher.end() - 1);
310 }
311 boolean ignore = false;
312 if (ignoreComments) {
313 final FileContents theFileContents = getFileContents();
314 final int startColumn = start.getColumn();
315 final int endLine = end.getLine();
316 final int endColumn = end.getColumn();
317 ignore = theFileContents.hasIntersectionWithComment(startLine,
318 startColumn, endLine, endColumn);
319 }
320 return ignore;
321 }
322
323 /**
324 * Displays the right message.
325 *
326 * @param lineNumber the line number the message relates to.
327 * @param errorCount number of errors in the file.
328 */
329 private void logMessage(int lineNumber, int errorCount) {
330 final String msg = getMessage(errorCount);
331
332 if (illegalPattern) {
333 log(lineNumber, MSG_ILLEGAL_REGEXP, msg);
334 }
335 else {
336 log(lineNumber, MSG_DUPLICATE_REGEXP, msg);
337 }
338 }
339
340 /**
341 * Provide right message.
342 *
343 * @param errorCount number of errors in the file.
344 * @return message for violation.
345 */
346 private String getMessage(int errorCount) {
347 String msg;
348
349 if (message == null || message.isEmpty()) {
350 msg = format.pattern();
351 }
352 else {
353 msg = message;
354 }
355
356 if (errorCount >= errorLimit) {
357 msg = ERROR_LIMIT_EXCEEDED_MESSAGE + msg;
358 }
359
360 return msg;
361 }
362
363 }