001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.regexp;
021
022import java.util.regex.Matcher;
023import java.util.regex.Pattern;
024
025import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
026import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
027import com.puppycrawl.tools.checkstyle.api.DetailAST;
028import com.puppycrawl.tools.checkstyle.api.FileContents;
029import com.puppycrawl.tools.checkstyle.api.FileText;
030import com.puppycrawl.tools.checkstyle.api.LineColumn;
031import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
032
033/**
034 * <div>
035 * Checks that a specified pattern exists, exists less than
036 * a set number of times, or does not exist in the file.
037 * </div>
038 *
039 * <p>
040 * This check combines all the functionality provided by
041 * <a href="https://checkstyle.org/checks/header/regexpheader.html">RegexpHeader</a>
042 * except supplying the regular expression from a file.
043 * </p>
044 *
045 * <p>
046 * It differs from them in that it works in multiline mode. Its regular expression
047 * can span multiple lines and it checks this against the whole file at once.
048 * The others work in single-line mode. Their single or multiple regular expressions
049 * can only span one line. They check each of these against each line in the file in turn.
050 * </p>
051 *
052 * <p>
053 * <b>Note:</b> Because of the different mode of operation there may be some
054 * changes in the regular expressions used to achieve a particular end.
055 * </p>
056 *
057 * <p>
058 * In multiline mode...
059 * </p>
060 * <ul>
061 * <li>
062 * {@code ^} means the beginning of a line, as opposed to beginning of the input.
063 * </li>
064 * <li>
065 * For beginning of the input use {@code \A}.
066 * </li>
067 * <li>
068 * {@code $} means the end of a line, as opposed to the end of the input.
069 * </li>
070 * <li>
071 * For end of input use {@code \Z}.
072 * </li>
073 * <li>
074 * Each line in the file is terminated with a line feed character.
075 * </li>
076 * </ul>
077 *
078 * <p>
079 * <b>Note:</b> Not all regular expression engines are created equal.
080 * Some provide extra functions that others do not and some elements
081 * of the syntax may vary. This check makes use of the
082 * <a href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/regex/package-summary.html">
083 * java.util.regex package</a>; please check its documentation for details
084 * of how to construct a regular expression to achieve a particular goal.
085 * </p>
086 *
087 * <p>
088 * <b>Note:</b> When entering a regular expression as a parameter in
089 * the XML config file you must also take into account the XML rules. e.g.
090 * if you want to match a {@literal <} symbol you need to enter {@literal &}lt;.
091 * The regular expression should be entered on one line.
092 * </p>
093 *
094 * <p>
095 * <b>Note:</b> To search for parentheses () in a regular expression
096 * you must escape them like \(\). This is required by the regexp engine,
097 * otherwise it will think they are special instruction characters.
098 * </p>
099 *
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 character to the regexp engine.
104 * </p>
105 *
106 * @since 4.0
107 */
108@FileStatefulCheck
109public class RegexpCheck extends AbstractCheck {
110
111    /**
112     * A key is pointing to the warning message text in "messages.properties" file.
113     */
114    public static final String MSG_ILLEGAL_REGEXP = "illegal.regexp";
115
116    /**
117     * A key is pointing to the warning message text in "messages.properties" file.
118     */
119    public static final String MSG_REQUIRED_REGEXP = "required.regexp";
120
121    /**
122     * A key is pointing to the warning message text in "messages.properties" file.
123     */
124    public static final String MSG_DUPLICATE_REGEXP = "duplicate.regexp";
125
126    /** Default duplicate limit. */
127    private static final int DEFAULT_DUPLICATE_LIMIT = -1;
128
129    /** Default error report limit. */
130    private static final int DEFAULT_ERROR_LIMIT = 100;
131
132    /** Error count exceeded message. */
133    private static final String ERROR_LIMIT_EXCEEDED_MESSAGE =
134        "The error limit has been exceeded, "
135        + "the check is aborting, there may be more unreported errors.";
136
137    /**
138     * Specify message which is used to notify about violations,
139     * if empty then the default (hard-coded) message is used.
140     */
141    private String message;
142
143    /** Control whether to ignore matches found within comments. */
144    private boolean ignoreComments;
145
146    /** Control whether the pattern is required or illegal. */
147    private boolean illegalPattern;
148
149    /** Specify the maximum number of violations before the check will abort. */
150    private int errorLimit = DEFAULT_ERROR_LIMIT;
151
152    /**
153     * Control whether to check for duplicates of a required pattern,
154     * any negative value means no checking for duplicates,
155     * any positive value is used as the maximum number of allowed duplicates,
156     * if the limit is exceeded violations will be logged.
157     */
158    private int duplicateLimit;
159
160    /** Boolean to say if we should check for duplicates. */
161    private boolean checkForDuplicates;
162
163    /** Specify the pattern to match against. */
164    private Pattern format = Pattern.compile("^$", Pattern.MULTILINE);
165
166    /**
167     * Creates a new {@code RegexpCheck} instance.
168     */
169    public RegexpCheck() {
170        // no code by default
171    }
172
173    /**
174     * Setter to specify message which is used to notify about violations,
175     * if empty then the default (hard-coded) message is used.
176     *
177     * @param message custom message which should be used in report.
178     * @since 4.0
179     */
180    public void setMessage(String message) {
181        this.message = message;
182    }
183
184    /**
185     * Setter to control whether to ignore matches found within comments.
186     *
187     * @param ignoreComments True if comments should be ignored.
188     * @since 4.0
189     */
190    public void setIgnoreComments(boolean ignoreComments) {
191        this.ignoreComments = ignoreComments;
192    }
193
194    /**
195     * Setter to control whether the pattern is required or illegal.
196     *
197     * @param illegalPattern True if pattern is not allowed.
198     * @since 4.0
199     */
200    public void setIllegalPattern(boolean illegalPattern) {
201        this.illegalPattern = illegalPattern;
202    }
203
204    /**
205     * Setter to specify the maximum number of violations before the check will abort.
206     *
207     * @param errorLimit the number of errors to report.
208     * @since 4.0
209     */
210    public void setErrorLimit(int errorLimit) {
211        this.errorLimit = errorLimit;
212    }
213
214    /**
215     * Setter to control whether to check for duplicates of a required pattern,
216     * any negative value means no checking for duplicates,
217     * any positive value is used as the maximum number of allowed duplicates,
218     * if the limit is exceeded violations will be logged.
219     *
220     * @param duplicateLimit negative values mean no duplicate checking,
221     *     any positive value is used as the limit.
222     * @since 4.0
223     */
224    public void setDuplicateLimit(int duplicateLimit) {
225        this.duplicateLimit = duplicateLimit;
226        checkForDuplicates = duplicateLimit > DEFAULT_DUPLICATE_LIMIT;
227    }
228
229    /**
230     * Setter to specify the pattern to match against.
231     *
232     * @param pattern the new pattern
233     * @since 4.0
234     */
235    public final void setFormat(Pattern pattern) {
236        format = CommonUtil.createPattern(pattern.pattern(), Pattern.MULTILINE);
237    }
238
239    @Override
240    public int[] getDefaultTokens() {
241        return getRequiredTokens();
242    }
243
244    @Override
245    public int[] getAcceptableTokens() {
246        return getRequiredTokens();
247    }
248
249    @Override
250    public int[] getRequiredTokens() {
251        return CommonUtil.EMPTY_INT_ARRAY;
252    }
253
254    @Override
255    public void beginTree(DetailAST rootAST) {
256        processRegexpMatches();
257    }
258
259    /**
260     * Processes the regexp matches and logs the number of errors in the file.
261     *
262     */
263    @SuppressWarnings("deprecation")
264    private void processRegexpMatches() {
265        final Matcher matcher = format.matcher(getFileContents().getText().getFullText());
266        int errorCount = 0;
267        int matchCount = 0;
268        final FileText text = getFileContents().getText();
269        while (errorCount < errorLimit && matcher.find()) {
270            final LineColumn start = text.lineColumn(matcher.start());
271            final int startLine = start.getLine();
272
273            final boolean ignore = isIgnore(startLine, text, start, matcher);
274            if (!ignore) {
275                matchCount++;
276                if (illegalPattern || checkForDuplicates
277                        && matchCount - 1 > duplicateLimit) {
278                    errorCount++;
279                    logMessage(startLine, errorCount);
280                }
281            }
282        }
283        if (!illegalPattern && matchCount == 0) {
284            final String msg = getMessage(errorCount);
285            log(1, MSG_REQUIRED_REGEXP, msg);
286        }
287    }
288
289    /**
290     * Detect ignore situation.
291     *
292     * @param startLine position of line
293     * @param text file text
294     * @param start line column
295     * @param matcher The matcher
296     * @return true is that need to be ignored
297     */
298    @SuppressWarnings("deprecation")
299    private boolean isIgnore(int startLine, FileText text, LineColumn start, Matcher matcher) {
300        final LineColumn end;
301        if (matcher.end() == 0) {
302            end = text.lineColumn(0);
303        }
304        else {
305            end = text.lineColumn(matcher.end() - 1);
306        }
307        boolean ignore = false;
308        if (ignoreComments) {
309            final FileContents theFileContents = getFileContents();
310            final int startColumn = start.getColumn();
311            final int endLine = end.getLine();
312            final int endColumn = end.getColumn();
313            ignore = theFileContents.hasIntersectionWithComment(startLine,
314                startColumn, endLine, endColumn);
315        }
316        return ignore;
317    }
318
319    /**
320     * Displays the right message.
321     *
322     * @param lineNumber the line number the message relates to.
323     * @param errorCount number of errors in the file.
324     */
325    private void logMessage(int lineNumber, int errorCount) {
326        final String msg = getMessage(errorCount);
327
328        if (illegalPattern) {
329            log(lineNumber, MSG_ILLEGAL_REGEXP, msg);
330        }
331        else {
332            log(lineNumber, MSG_DUPLICATE_REGEXP, msg);
333        }
334    }
335
336    /**
337     * Provide right message.
338     *
339     * @param errorCount number of errors in the file.
340     * @return message for violation.
341     */
342    private String getMessage(int errorCount) {
343        String msg;
344
345        if (message == null || message.isEmpty()) {
346            msg = format.pattern();
347        }
348        else {
349            msg = message;
350        }
351
352        if (errorCount >= errorLimit) {
353            msg = ERROR_LIMIT_EXCEEDED_MESSAGE + msg;
354        }
355
356        return msg;
357    }
358
359}