1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2019 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.regexp;
21  
22  import java.util.regex.Matcher;
23  import java.util.regex.Pattern;
24  
25  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
26  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
27  import com.puppycrawl.tools.checkstyle.api.DetailAST;
28  import com.puppycrawl.tools.checkstyle.api.FileContents;
29  import com.puppycrawl.tools.checkstyle.api.FileText;
30  import com.puppycrawl.tools.checkstyle.api.LineColumn;
31  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
32  
33  /**
34   * <p>
35   * A check that makes sure that a specified pattern exists (or not) in the file.
36   * </p>
37   * <p>
38   * An example of how to configure the check to make sure a copyright statement
39   * is included in the file (but without requirements on where in the file
40   * it should be):
41   * </p>
42   * <pre>
43   * &lt;module name="RegexpCheck"&gt;
44   *    &lt;property name="format" value="This code is copyrighted"/&gt;
45   * &lt;/module&gt;
46   * </pre>
47   * <p>
48   * And to make sure the same statement appears at the beginning of the file.
49   * </p>
50   * <pre>
51   * &lt;module name="RegexpCheck"&gt;
52   *    &lt;property name="format" value="\AThis code is copyrighted"/&gt;
53   * &lt;/module&gt;
54   * </pre>
55   */
56  @FileStatefulCheck
57  public class RegexpCheck extends AbstractCheck {
58  
59      /**
60       * A key is pointing to the warning message text in "messages.properties"
61       * file.
62       */
63      public static final String MSG_ILLEGAL_REGEXP = "illegal.regexp";
64  
65      /**
66       * A key is pointing to the warning message text in "messages.properties"
67       * file.
68       */
69      public static final String MSG_REQUIRED_REGEXP = "required.regexp";
70  
71      /**
72       * A key is pointing to the warning message text in "messages.properties"
73       * file.
74       */
75      public static final String MSG_DUPLICATE_REGEXP = "duplicate.regexp";
76  
77      /** Default duplicate limit. */
78      private static final int DEFAULT_DUPLICATE_LIMIT = -1;
79  
80      /** Default error report limit. */
81      private static final int DEFAULT_ERROR_LIMIT = 100;
82  
83      /** Error count exceeded message. */
84      private static final String ERROR_LIMIT_EXCEEDED_MESSAGE =
85          "The error limit has been exceeded, "
86          + "the check is aborting, there may be more unreported errors.";
87  
88      /** Custom message for report. */
89      private String message;
90  
91      /** Ignore matches within comments?. **/
92      private boolean ignoreComments;
93  
94      /** Pattern illegal?. */
95      private boolean illegalPattern;
96  
97      /** Error report limit. */
98      private int errorLimit = DEFAULT_ERROR_LIMIT;
99  
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 regexp to match against. */
113     private Pattern format = Pattern.compile("^$", Pattern.MULTILINE);
114 
115     /** The matcher. */
116     private Matcher matcher;
117 
118     /**
119      * Setter for message property.
120      * @param message custom message which should be used in report.
121      */
122     public void setMessage(String message) {
123         if (message == null) {
124             this.message = "";
125         }
126         else {
127             this.message = message;
128         }
129     }
130 
131     /**
132      * Sets if matches within comments should be ignored.
133      * @param ignoreComments True if comments should be ignored.
134      */
135     public void setIgnoreComments(boolean ignoreComments) {
136         this.ignoreComments = ignoreComments;
137     }
138 
139     /**
140      * Sets if pattern is illegal, otherwise pattern is required.
141      * @param illegalPattern True if pattern is not allowed.
142      */
143     public void setIllegalPattern(boolean illegalPattern) {
144         this.illegalPattern = illegalPattern;
145     }
146 
147     /**
148      * Sets the limit on the number of errors to report.
149      * @param errorLimit the number of errors to report.
150      */
151     public void setErrorLimit(int errorLimit) {
152         this.errorLimit = errorLimit;
153     }
154 
155     /**
156      * Sets the maximum number of instances of required pattern allowed.
157      * @param duplicateLimit negative values mean no duplicate checking,
158      *     any positive value is used as the limit.
159      */
160     public void setDuplicateLimit(int duplicateLimit) {
161         this.duplicateLimit = duplicateLimit;
162         checkForDuplicates = duplicateLimit > DEFAULT_DUPLICATE_LIMIT;
163     }
164 
165     /**
166      * Set the format to the specified regular expression.
167      * @param pattern the new pattern
168      * @throws org.apache.commons.beanutils.ConversionException unable to parse format
169      */
170     public final void setFormat(Pattern pattern) {
171         format = CommonUtil.createPattern(pattern.pattern(), Pattern.MULTILINE);
172     }
173 
174     @Override
175     public int[] getDefaultTokens() {
176         return getRequiredTokens();
177     }
178 
179     @Override
180     public int[] getAcceptableTokens() {
181         return getRequiredTokens();
182     }
183 
184     @Override
185     public int[] getRequiredTokens() {
186         return CommonUtil.EMPTY_INT_ARRAY;
187     }
188 
189     @Override
190     public void beginTree(DetailAST rootAST) {
191         matcher = format.matcher(getFileContents().getText().getFullText());
192         matchCount = 0;
193         errorCount = 0;
194         findMatch();
195     }
196 
197     /** Recursive method that finds the matches. */
198     private void findMatch() {
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      * Check if we can stop validation.
226      * @param ignore flag
227      * @return true is we can continue
228      */
229     private boolean canContinueValidation(boolean ignore) {
230         return errorCount <= errorLimit - 1
231                 && (ignore || illegalPattern || checkForDuplicates);
232     }
233 
234     /**
235      * Detect ignore situation.
236      * @param startLine position of line
237      * @param text file text
238      * @param start line column
239      * @return true is that need to be ignored
240      */
241     private boolean isIgnore(int startLine, FileText text, LineColumn start) {
242         final LineColumn end;
243         if (matcher.end() == 0) {
244             end = text.lineColumn(0);
245         }
246         else {
247             end = text.lineColumn(matcher.end() - 1);
248         }
249         boolean ignore = false;
250         if (ignoreComments) {
251             final FileContents theFileContents = getFileContents();
252             final int startColumn = start.getColumn();
253             final int endLine = end.getLine();
254             final int endColumn = end.getColumn();
255             ignore = theFileContents.hasIntersectionWithComment(startLine,
256                 startColumn, endLine, endColumn);
257         }
258         return ignore;
259     }
260 
261     /**
262      * Displays the right message.
263      * @param lineNumber the line number the message relates to.
264      */
265     private void logMessage(int lineNumber) {
266         String msg;
267 
268         if (message == null || message.isEmpty()) {
269             msg = format.pattern();
270         }
271         else {
272             msg = message;
273         }
274 
275         if (errorCount >= errorLimit) {
276             msg = ERROR_LIMIT_EXCEEDED_MESSAGE + msg;
277         }
278 
279         if (illegalPattern) {
280             log(lineNumber, MSG_ILLEGAL_REGEXP, msg);
281         }
282         else {
283             if (lineNumber > 0) {
284                 log(lineNumber, MSG_DUPLICATE_REGEXP, msg);
285             }
286             else {
287                 log(lineNumber, MSG_REQUIRED_REGEXP, msg);
288             }
289         }
290     }
291 
292 }