001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2015 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 org.apache.commons.lang3.ArrayUtils;
026
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.checks.AbstractFormatCheck;
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="RequiredRegexp"&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="RequiredRegexp"&gt;
052 *    &lt;property name="format" value="\AThis code is copyrighted"/&gt;
053 * &lt;/module&gt;
054 * </pre>
055 * @author Stan Quinn
056 */
057public class RegexpCheck extends AbstractFormatCheck {
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 matcher. */
113    private Matcher matcher;
114
115    /**
116     * Instantiates an new RegexpCheck.
117     */
118    public RegexpCheck() {
119        // the empty language
120        super("$^", Pattern.MULTILINE);
121    }
122
123    /**
124     * Setter for message property.
125     * @param message custom message which should be used in report.
126     */
127    public void setMessage(String message) {
128        if (message == null) {
129            this.message = "";
130        }
131        else {
132            this.message = message;
133        }
134    }
135
136    /**
137     * Sets if matches within comments should be ignored.
138     * @param ignoreComments True if comments should be ignored.
139     */
140    public void setIgnoreComments(boolean ignoreComments) {
141        this.ignoreComments = ignoreComments;
142    }
143
144    /**
145     * Sets if pattern is illegal, otherwise pattern is required.
146     * @param illegalPattern True if pattern is not allowed.
147     */
148    public void setIllegalPattern(boolean illegalPattern) {
149        this.illegalPattern = illegalPattern;
150    }
151
152    /**
153     * Sets the limit on the number of errors to report.
154     * @param errorLimit the number of errors to report.
155     */
156    public void setErrorLimit(int errorLimit) {
157        this.errorLimit = errorLimit;
158    }
159
160    /**
161     * Sets the maximum number of instances of required pattern allowed.
162     * @param duplicateLimit negative values mean no duplicate checking,
163     *     any positive value is used as the limit.
164     */
165    public void setDuplicateLimit(int duplicateLimit) {
166        this.duplicateLimit = duplicateLimit;
167        checkForDuplicates = duplicateLimit > DEFAULT_DUPLICATE_LIMIT;
168    }
169
170    @Override
171    public int[] getDefaultTokens() {
172        return getAcceptableTokens();
173    }
174
175    @Override
176    public int[] getAcceptableTokens() {
177        return ArrayUtils.EMPTY_INT_ARRAY;
178    }
179
180    @Override
181    public int[] getRequiredTokens() {
182        return getAcceptableTokens();
183    }
184
185    @Override
186    public void beginTree(DetailAST rootAST) {
187        final Pattern pattern = getRegexp();
188        matcher = pattern.matcher(getFileContents().getText().getFullText());
189        matchCount = 0;
190        errorCount = 0;
191        findMatch();
192    }
193
194    /** Recursive method that finds the matches. */
195    private void findMatch() {
196
197        final boolean foundMatch = matcher.find();
198        if (foundMatch) {
199            final FileText text = getFileContents().getText();
200            final LineColumn start = text.lineColumn(matcher.start());
201            final int startLine = start.getLine();
202
203            final boolean ignore = isIgnore(startLine, text, start);
204
205            if (!ignore) {
206                matchCount++;
207                if (illegalPattern || checkForDuplicates
208                        && matchCount - 1 > duplicateLimit) {
209                    errorCount++;
210                    logMessage(startLine);
211                }
212            }
213            if (canContinueValidation(ignore)) {
214                findMatch();
215            }
216        }
217        else if (!illegalPattern && matchCount == 0) {
218            logMessage(0);
219        }
220
221    }
222
223    /**
224     * Check if we can stop validation.
225     * @param ignore flag
226     * @return true is we can continue
227     */
228    private boolean canContinueValidation(boolean ignore) {
229        return errorCount < errorLimit
230                && (ignore || illegalPattern || checkForDuplicates);
231    }
232
233    /**
234     * Detect ignore situation.
235     * @param startLine position of line
236     * @param text file text
237     * @param start line column
238     * @return true is that need to be ignored
239     */
240    private boolean isIgnore(int startLine, FileText text, LineColumn start) {
241        final LineColumn end;
242        if (matcher.end() == 0) {
243            end = text.lineColumn(0);
244        }
245        else {
246            end = text.lineColumn(matcher.end() - 1);
247        }
248        final int startColumn = start.getColumn();
249        final int endLine = end.getLine();
250        final int endColumn = end.getColumn();
251        boolean ignore = false;
252        if (ignoreComments) {
253            final FileContents theFileContents = getFileContents();
254            ignore = theFileContents.hasIntersectionWithComment(startLine,
255                startColumn, endLine, endColumn);
256        }
257        return ignore;
258    }
259
260    /**
261     * Displays the right message.
262     * @param lineNumber the line number the message relates to.
263     */
264    private void logMessage(int lineNumber) {
265        String msg;
266
267        if (message.isEmpty()) {
268            msg = getFormat();
269        }
270        else {
271            msg = message;
272        }
273
274        if (errorCount >= errorLimit) {
275            msg = ERROR_LIMIT_EXCEEDED_MESSAGE + msg;
276        }
277
278        if (illegalPattern) {
279            log(lineNumber, MSG_ILLEGAL_REGEXP, msg);
280        }
281        else {
282            if (lineNumber > 0) {
283                log(lineNumber, MSG_DUPLICATE_REGEXP, msg);
284            }
285            else {
286                log(lineNumber, MSG_REQUIRED_REGEXP, msg);
287            }
288        }
289    }
290}