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