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 &amp;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 &amp;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
110public 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}