001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2018 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 * <p>
035 * A check that makes sure that a specified pattern exists (or not) in the file.
036 * </p>
037 * <p>
038 * An example of how to configure the check to make sure a copyright statement
039 * is included in the file (but without requirements on where in the file
040 * it should be):
041 * </p>
042 * <pre>
043 * &lt;module name="RegexpCheck"&gt;
044 *    &lt;property name="format" value="This code is copyrighted"/&gt;
045 * &lt;/module&gt;
046 * </pre>
047 * <p>
048 * And to make sure the same statement appears at the beginning of the file.
049 * </p>
050 * <pre>
051 * &lt;module name="RegexpCheck"&gt;
052 *    &lt;property name="format" value="\AThis code is copyrighted"/&gt;
053 * &lt;/module&gt;
054 * </pre>
055 */
056@FileStatefulCheck
057public class RegexpCheck extends AbstractCheck {
058
059    /**
060     * A key is pointing to the warning message text in "messages.properties"
061     * file.
062     */
063    public static final String MSG_ILLEGAL_REGEXP = "illegal.regexp";
064
065    /**
066     * A key is pointing to the warning message text in "messages.properties"
067     * file.
068     */
069    public static final String MSG_REQUIRED_REGEXP = "required.regexp";
070
071    /**
072     * A key is pointing to the warning message text in "messages.properties"
073     * file.
074     */
075    public static final String MSG_DUPLICATE_REGEXP = "duplicate.regexp";
076
077    /** Default duplicate limit. */
078    private static final int DEFAULT_DUPLICATE_LIMIT = -1;
079
080    /** Default error report limit. */
081    private static final int DEFAULT_ERROR_LIMIT = 100;
082
083    /** Error count exceeded message. */
084    private static final String ERROR_LIMIT_EXCEEDED_MESSAGE =
085        "The error limit has been exceeded, "
086        + "the check is aborting, there may be more unreported errors.";
087
088    /** Custom message for report. */
089    private String message;
090
091    /** Ignore matches within comments?. **/
092    private boolean ignoreComments;
093
094    /** Pattern illegal?. */
095    private boolean illegalPattern;
096
097    /** Error report limit. */
098    private int errorLimit = DEFAULT_ERROR_LIMIT;
099
100    /** Disallow more than x duplicates?. */
101    private int duplicateLimit;
102
103    /** Boolean to say if we should check for duplicates. */
104    private boolean checkForDuplicates;
105
106    /** Tracks number of matches made. */
107    private int matchCount;
108
109    /** Tracks number of errors. */
110    private int errorCount;
111
112    /** The regexp to match against. */
113    private Pattern format = Pattern.compile("^$", Pattern.MULTILINE);
114
115    /** The matcher. */
116    private Matcher matcher;
117
118    /**
119     * Setter for message property.
120     * @param message custom message which should be used in report.
121     */
122    public void setMessage(String message) {
123        if (message == null) {
124            this.message = "";
125        }
126        else {
127            this.message = message;
128        }
129    }
130
131    /**
132     * Sets if matches within comments should be ignored.
133     * @param ignoreComments True if comments should be ignored.
134     */
135    public void setIgnoreComments(boolean ignoreComments) {
136        this.ignoreComments = ignoreComments;
137    }
138
139    /**
140     * Sets if pattern is illegal, otherwise pattern is required.
141     * @param illegalPattern True if pattern is not allowed.
142     */
143    public void setIllegalPattern(boolean illegalPattern) {
144        this.illegalPattern = illegalPattern;
145    }
146
147    /**
148     * Sets the limit on the number of errors to report.
149     * @param errorLimit the number of errors to report.
150     */
151    public void setErrorLimit(int errorLimit) {
152        this.errorLimit = errorLimit;
153    }
154
155    /**
156     * Sets the maximum number of instances of required pattern allowed.
157     * @param duplicateLimit negative values mean no duplicate checking,
158     *     any positive value is used as the limit.
159     */
160    public void setDuplicateLimit(int duplicateLimit) {
161        this.duplicateLimit = duplicateLimit;
162        checkForDuplicates = duplicateLimit > DEFAULT_DUPLICATE_LIMIT;
163    }
164
165    /**
166     * Set the format to the specified regular expression.
167     * @param pattern the new pattern
168     * @throws org.apache.commons.beanutils.ConversionException unable to parse format
169     */
170    public final void setFormat(Pattern pattern) {
171        format = CommonUtil.createPattern(pattern.pattern(), Pattern.MULTILINE);
172    }
173
174    @Override
175    public int[] getDefaultTokens() {
176        return getRequiredTokens();
177    }
178
179    @Override
180    public int[] getAcceptableTokens() {
181        return getRequiredTokens();
182    }
183
184    @Override
185    public int[] getRequiredTokens() {
186        return CommonUtil.EMPTY_INT_ARRAY;
187    }
188
189    @Override
190    public void beginTree(DetailAST rootAST) {
191        matcher = format.matcher(getFileContents().getText().getFullText());
192        matchCount = 0;
193        errorCount = 0;
194        findMatch();
195    }
196
197    /** Recursive method that finds the matches. */
198    private void findMatch() {
199        final boolean foundMatch = matcher.find();
200        if (foundMatch) {
201            final FileText text = getFileContents().getText();
202            final LineColumn start = text.lineColumn(matcher.start());
203            final int startLine = start.getLine();
204
205            final boolean ignore = isIgnore(startLine, text, start);
206
207            if (!ignore) {
208                matchCount++;
209                if (illegalPattern || checkForDuplicates
210                        && matchCount - 1 > duplicateLimit) {
211                    errorCount++;
212                    logMessage(startLine);
213                }
214            }
215            if (canContinueValidation(ignore)) {
216                findMatch();
217            }
218        }
219        else if (!illegalPattern && matchCount == 0) {
220            logMessage(0);
221        }
222    }
223
224    /**
225     * Check if we can stop validation.
226     * @param ignore flag
227     * @return true is we can continue
228     */
229    private boolean canContinueValidation(boolean ignore) {
230        return errorCount <= errorLimit - 1
231                && (ignore || illegalPattern || checkForDuplicates);
232    }
233
234    /**
235     * Detect ignore situation.
236     * @param startLine position of line
237     * @param text file text
238     * @param start line column
239     * @return true is that need to be ignored
240     */
241    private boolean isIgnore(int startLine, FileText text, LineColumn start) {
242        final LineColumn end;
243        if (matcher.end() == 0) {
244            end = text.lineColumn(0);
245        }
246        else {
247            end = text.lineColumn(matcher.end() - 1);
248        }
249        boolean ignore = false;
250        if (ignoreComments) {
251            final FileContents theFileContents = getFileContents();
252            final int startColumn = start.getColumn();
253            final int endLine = end.getLine();
254            final int endColumn = end.getColumn();
255            ignore = theFileContents.hasIntersectionWithComment(startLine,
256                startColumn, endLine, endColumn);
257        }
258        return ignore;
259    }
260
261    /**
262     * Displays the right message.
263     * @param lineNumber the line number the message relates to.
264     */
265    private void logMessage(int lineNumber) {
266        String msg;
267
268        if (message == null || message.isEmpty()) {
269            msg = format.pattern();
270        }
271        else {
272            msg = message;
273        }
274
275        if (errorCount >= errorLimit) {
276            msg = ERROR_LIMIT_EXCEEDED_MESSAGE + msg;
277        }
278
279        if (illegalPattern) {
280            log(lineNumber, MSG_ILLEGAL_REGEXP, msg);
281        }
282        else {
283            if (lineNumber > 0) {
284                log(lineNumber, MSG_DUPLICATE_REGEXP, msg);
285            }
286            else {
287                log(lineNumber, MSG_REQUIRED_REGEXP, msg);
288            }
289        }
290    }
291
292}