001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2017 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 regexp to match against. */
112    private Pattern format = Pattern.compile("$^", Pattern.MULTILINE);
113
114    /** The matcher. */
115    private Matcher matcher;
116
117    /**
118     * Setter for message property.
119     * @param message custom message which should be used in report.
120     */
121    public void setMessage(String message) {
122        if (message == null) {
123            this.message = "";
124        }
125        else {
126            this.message = message;
127        }
128    }
129
130    /**
131     * Sets if matches within comments should be ignored.
132     * @param ignoreComments True if comments should be ignored.
133     */
134    public void setIgnoreComments(boolean ignoreComments) {
135        this.ignoreComments = ignoreComments;
136    }
137
138    /**
139     * Sets if pattern is illegal, otherwise pattern is required.
140     * @param illegalPattern True if pattern is not allowed.
141     */
142    public void setIllegalPattern(boolean illegalPattern) {
143        this.illegalPattern = illegalPattern;
144    }
145
146    /**
147     * Sets the limit on the number of errors to report.
148     * @param errorLimit the number of errors to report.
149     */
150    public void setErrorLimit(int errorLimit) {
151        this.errorLimit = errorLimit;
152    }
153
154    /**
155     * Sets the maximum number of instances of required pattern allowed.
156     * @param duplicateLimit negative values mean no duplicate checking,
157     *     any positive value is used as the limit.
158     */
159    public void setDuplicateLimit(int duplicateLimit) {
160        this.duplicateLimit = duplicateLimit;
161        checkForDuplicates = duplicateLimit > DEFAULT_DUPLICATE_LIMIT;
162    }
163
164    /**
165     * Set the format to the specified regular expression.
166     * @param pattern the new pattern
167     * @throws org.apache.commons.beanutils.ConversionException unable to parse format
168     */
169    public final void setFormat(Pattern pattern) {
170        format = CommonUtils.createPattern(pattern.pattern(), Pattern.MULTILINE);
171    }
172
173    @Override
174    public int[] getDefaultTokens() {
175        return getAcceptableTokens();
176    }
177
178    @Override
179    public int[] getAcceptableTokens() {
180        return CommonUtils.EMPTY_INT_ARRAY;
181    }
182
183    @Override
184    public int[] getRequiredTokens() {
185        return getAcceptableTokens();
186    }
187
188    @Override
189    public void beginTree(DetailAST rootAST) {
190        matcher = format.matcher(getFileContents().getText().getFullText());
191        matchCount = 0;
192        errorCount = 0;
193        findMatch();
194    }
195
196    /** Recursive method that finds the matches. */
197    private void findMatch() {
198
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    /**
226     * Check if we can stop validation.
227     * @param ignore flag
228     * @return true is we can continue
229     */
230    private boolean canContinueValidation(boolean ignore) {
231        return errorCount < errorLimit
232                && (ignore || illegalPattern || checkForDuplicates);
233    }
234
235    /**
236     * Detect ignore situation.
237     * @param startLine position of line
238     * @param text file text
239     * @param start line column
240     * @return true is that need to be ignored
241     */
242    private boolean isIgnore(int startLine, FileText text, LineColumn start) {
243        final LineColumn end;
244        if (matcher.end() == 0) {
245            end = text.lineColumn(0);
246        }
247        else {
248            end = text.lineColumn(matcher.end() - 1);
249        }
250        boolean ignore = false;
251        if (ignoreComments) {
252            final FileContents theFileContents = getFileContents();
253            final int startColumn = start.getColumn();
254            final int endLine = end.getLine();
255            final int endColumn = end.getColumn();
256            ignore = theFileContents.hasIntersectionWithComment(startLine,
257                startColumn, endLine, endColumn);
258        }
259        return ignore;
260    }
261
262    /**
263     * Displays the right message.
264     * @param lineNumber the line number the message relates to.
265     */
266    private void logMessage(int lineNumber) {
267        String msg;
268
269        if (message.isEmpty()) {
270            msg = format.pattern();
271        }
272        else {
273            msg = message;
274        }
275
276        if (errorCount >= errorLimit) {
277            msg = ERROR_LIMIT_EXCEEDED_MESSAGE + msg;
278        }
279
280        if (illegalPattern) {
281            log(lineNumber, MSG_ILLEGAL_REGEXP, msg);
282        }
283        else {
284            if (lineNumber > 0) {
285                log(lineNumber, MSG_DUPLICATE_REGEXP, msg);
286            }
287            else {
288                log(lineNumber, MSG_REQUIRED_REGEXP, msg);
289            }
290        }
291    }
292}