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