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