View Javadoc
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 }