1 ////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code for adherence to a set of rules.
3 // Copyright (C) 2001-2015 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 package com.puppycrawl.tools.checkstyle.filters;
20
21 import com.google.common.collect.Lists;
22 import com.puppycrawl.tools.checkstyle.api.AuditEvent;
23 import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
24 import com.puppycrawl.tools.checkstyle.api.FileContents;
25 import com.puppycrawl.tools.checkstyle.api.Filter;
26 import com.puppycrawl.tools.checkstyle.api.TextBlock;
27 import com.puppycrawl.tools.checkstyle.api.Utils;
28 import com.puppycrawl.tools.checkstyle.checks.FileContentsHolder;
29 import java.lang.ref.WeakReference;
30 import java.util.Collection;
31 import java.util.Collections;
32 import java.util.List;
33 import java.util.regex.Matcher;
34 import java.util.regex.Pattern;
35 import java.util.regex.PatternSyntaxException;
36 import org.apache.commons.beanutils.ConversionException;
37
38 /**
39 * <p>
40 * A filter that uses comments to suppress audit events.
41 * </p>
42 * <p>
43 * Rationale:
44 * Sometimes there are legitimate reasons for violating a check. When
45 * this is a matter of the code in question and not personal
46 * preference, the best place to override the policy is in the code
47 * itself. Semi-structured comments can be associated with the check.
48 * This is sometimes superior to a separate suppressions file, which
49 * must be kept up-to-date as the source file is edited.
50 * </p>
51 * <p>
52 * Usage:
53 * This check only works in conjunction with the FileContentsHolder module
54 * since that module makes the suppression comments in the .java
55 * files available <i>sub rosa</i>.
56 * </p>
57 * @see FileContentsHolder
58 * @author Mike McMahon
59 * @author Rick Giles
60 */
61 public class SuppressionCommentFilter
62 extends AutomaticBean
63 implements Filter
64 {
65 /**
66 * A Tag holds a suppression comment and its location, and determines
67 * whether the supression turns checkstyle reporting on or off.
68 * @author Rick Giles
69 */
70 public class Tag
71 implements Comparable<Tag>
72 {
73 /** The text of the tag. */
74 private final String text;
75
76 /** The line number of the tag. */
77 private final int line;
78
79 /** The column number of the tag. */
80 private final int column;
81
82 /** Determines whether the suppression turns checkstyle reporting on. */
83 private final boolean on;
84
85 /** The parsed check regexp, expanded for the text of this tag. */
86 private Pattern tagCheckRegexp;
87
88 /** The parsed message regexp, expanded for the text of this tag. */
89 private Pattern tagMessageRegexp;
90
91 /**
92 * Constructs a tag.
93 * @param line the line number.
94 * @param column the column number.
95 * @param text the text of the suppression.
96 * @param on <code>true</code> if the tag turns checkstyle reporting.
97 * @throws ConversionException if unable to parse expanded text.
98 * on.
99 */
100 public Tag(int line, int column, String text, boolean on)
101 throws ConversionException
102 {
103 this.line = line;
104 this.column = column;
105 this.text = text;
106 this.on = on;
107
108 tagCheckRegexp = checkRegexp;
109 //Expand regexp for check and message
110 //Does not intern Patterns with Utils.getPattern()
111 String format = "";
112 try {
113 if (on) {
114 format =
115 expandFromCoont(text, checkFormat, onRegexp);
116 tagCheckRegexp = Pattern.compile(format);
117 if (messageFormat != null) {
118 format =
119 expandFromCoont(text, messageFormat, onRegexp);
120 tagMessageRegexp = Pattern.compile(format);
121 }
122 }
123 else {
124 format =
125 expandFromCoont(text, checkFormat, offRegexp);
126 tagCheckRegexp = Pattern.compile(format);
127 if (messageFormat != null) {
128 format =
129 expandFromCoont(
130 text,
131 messageFormat,
132 offRegexp);
133 tagMessageRegexp = Pattern.compile(format);
134 }
135 }
136 }
137 catch (final PatternSyntaxException e) {
138 throw new ConversionException(
139 "unable to parse expanded comment " + format,
140 e);
141 }
142 }
143
144 /** @return the text of the tag. */
145 public String getText()
146 {
147 return text;
148 }
149
150 /** @return the line number of the tag in the source file. */
151 public int getLine()
152 {
153 return line;
154 }
155
156 /**
157 * Determines the column number of the tag in the source file.
158 * Will be 0 for all lines of multiline comment, except the
159 * first line.
160 * @return the column number of the tag in the source file.
161 */
162 public int getColumn()
163 {
164 return column;
165 }
166
167 /**
168 * Determines whether the suppression turns checkstyle reporting on or
169 * off.
170 * @return <code>true</code>if the suppression turns reporting on.
171 */
172 public boolean isOn()
173 {
174 return on;
175 }
176
177 /**
178 * Compares the position of this tag in the file
179 * with the position of another tag.
180 * @param object the tag to compare with this one.
181 * @return a negative number if this tag is before the other tag,
182 * 0 if they are at the same position, and a positive number if this
183 * tag is after the other tag.
184 * @see java.lang.Comparable#compareTo(java.lang.Object)
185 */
186 @Override
187 public int compareTo(Tag object)
188 {
189 if (line == object.line) {
190 return column - object.column;
191 }
192
193 return (line - object.line);
194 }
195
196 /**
197 * Determines whether the source of an audit event
198 * matches the text of this tag.
199 * @param event the <code>AuditEvent</code> to check.
200 * @return true if the source of event matches the text of this tag.
201 */
202 public boolean isMatch(AuditEvent event)
203 {
204 final Matcher tagMatcher =
205 tagCheckRegexp.matcher(event.getSourceName());
206 if (tagMatcher.find()) {
207 if (tagMessageRegexp != null) {
208 final Matcher messageMatcher =
209 tagMessageRegexp.matcher(event.getMessage());
210 return messageMatcher.find();
211 }
212 return true;
213 }
214 return false;
215 }
216
217 /**
218 * Expand based on a matching comment.
219 * @param comment the comment.
220 * @param string the string to expand.
221 * @param regexp the parsed expander.
222 * @return the expanded string
223 */
224 private String expandFromCoont(
225 String comment,
226 String string,
227 Pattern regexp)
228 {
229 final Matcher matcher = regexp.matcher(comment);
230 // Match primarily for effect.
231 if (!matcher.find()) {
232 ///CLOVER:OFF
233 return string;
234 ///CLOVER:ON
235 }
236 String result = string;
237 for (int i = 0; i <= matcher.groupCount(); i++) {
238 // $n expands comment match like in Pattern.subst().
239 result = result.replaceAll("\\$" + i, matcher.group(i));
240 }
241 return result;
242 }
243
244 @Override
245 public final String toString()
246 {
247 return "Tag[line=" + getLine() + "; col=" + getColumn()
248 + "; on=" + isOn() + "; text='" + getText() + "']";
249 }
250 }
251
252 /** Turns checkstyle reporting off. */
253 private static final String DEFAULT_OFF_FORMAT = "CHECKSTYLE\\:OFF";
254
255 /** Turns checkstyle reporting on. */
256 private static final String DEFAULT_ON_FORMAT = "CHECKSTYLE\\:ON";
257
258 /** Control all checks */
259 private static final String DEFAULT_CHECK_FORMAT = ".*";
260
261 /** Whether to look in comments of the C type. */
262 private boolean checkC = true;
263
264 /** Whether to look in comments of the C++ type. */
265 private boolean checkCPP = true;
266
267 /** Parsed comment regexp that turns checkstyle reporting off. */
268 private Pattern offRegexp;
269
270 /** Parsed comment regexp that turns checkstyle reporting on. */
271 private Pattern onRegexp;
272
273 /** The check format to suppress. */
274 private String checkFormat;
275
276 /** The parsed check regexp. */
277 private Pattern checkRegexp;
278
279 /** The message format to suppress. */
280 private String messageFormat;
281
282 //TODO: Investigate performance improvement with array
283 /** Tagged comments */
284 private final List<Tag> tags = Lists.newArrayList();
285
286 /**
287 * References the current FileContents for this filter.
288 * Since this is a weak reference to the FileContents, the FileContents
289 * can be reclaimed as soon as the strong references in TreeWalker
290 * and FileContentsHolder are reassigned to the next FileContents,
291 * at which time filtering for the current FileContents is finished.
292 */
293 private WeakReference<FileContents> fileContentsReference = new WeakReference<>(null);
294
295 /**
296 * Constructs a SuppressionCoontFilter.
297 * Initializes comment on, comment off, and check formats
298 * to defaults.
299 */
300 public SuppressionCommentFilter()
301 {
302 setOnCommentFormat(DEFAULT_ON_FORMAT);
303 setOffCommentFormat(DEFAULT_OFF_FORMAT);
304 setCheckFormat(DEFAULT_CHECK_FORMAT);
305 }
306
307 /**
308 * Set the format for a comment that turns off reporting.
309 * @param format a <code>String</code> value.
310 * @throws ConversionException unable to parse format.
311 */
312 public void setOffCommentFormat(String format)
313 throws ConversionException
314 {
315 try {
316 offRegexp = Utils.getPattern(format);
317 }
318 catch (final PatternSyntaxException e) {
319 throw new ConversionException("unable to parse " + format, e);
320 }
321 }
322
323 /**
324 * Set the format for a comment that turns on reporting.
325 * @param format a <code>String</code> value
326 * @throws ConversionException unable to parse format
327 */
328 public void setOnCommentFormat(String format)
329 throws ConversionException
330 {
331 try {
332 onRegexp = Utils.getPattern(format);
333 }
334 catch (final PatternSyntaxException e) {
335 throw new ConversionException("unable to parse " + format, e);
336 }
337 }
338
339 /** @return the FileContents for this filter. */
340 public FileContents getFileContents()
341 {
342 return fileContentsReference.get();
343 }
344
345 /**
346 * Set the FileContents for this filter.
347 * @param fileContents the FileContents for this filter.
348 */
349 public void setFileContents(FileContents fileContents)
350 {
351 fileContentsReference = new WeakReference<>(fileContents);
352 }
353
354 /**
355 * Set the format for a check.
356 * @param format a <code>String</code> value
357 * @throws ConversionException unable to parse format
358 */
359 public void setCheckFormat(String format)
360 throws ConversionException
361 {
362 try {
363 checkRegexp = Utils.getPattern(format);
364 checkFormat = format;
365 }
366 catch (final PatternSyntaxException e) {
367 throw new ConversionException("unable to parse " + format, e);
368 }
369 }
370
371 /**
372 * Set the format for a message.
373 * @param format a <code>String</code> value
374 * @throws ConversionException unable to parse format
375 */
376 public void setMessageFormat(String format)
377 throws ConversionException
378 {
379 // check that format parses
380 try {
381 Utils.getPattern(format);
382 }
383 catch (final PatternSyntaxException e) {
384 throw new ConversionException("unable to parse " + format, e);
385 }
386 messageFormat = format;
387 }
388
389
390 /**
391 * Set whether to look in C++ comments.
392 * @param checkCPP <code>true</code> if C++ comments are checked.
393 */
394 public void setCheckCPP(boolean checkCPP)
395 {
396 this.checkCPP = checkCPP;
397 }
398
399 /**
400 * Set whether to look in C comments.
401 * @param checkC <code>true</code> if C comments are checked.
402 */
403 public void setCheckC(boolean checkC)
404 {
405 this.checkC = checkC;
406 }
407
408 /** {@inheritDoc} */
409 @Override
410 public boolean accept(AuditEvent event)
411 {
412 if (event.getLocalizedMessage() == null) {
413 return true; // A special event.
414 }
415
416 // Lazy update. If the first event for the current file, update file
417 // contents and tag suppressions
418 final FileContents currentContents = FileContentsHolder.getContents();
419 if (currentContents == null) {
420 // we have no contents, so we can not filter.
421 // TODO: perhaps we should notify user somehow?
422 return true;
423 }
424 if (getFileContents() != currentContents) {
425 setFileContents(currentContents);
426 tagSuppressions();
427 }
428 final Tag matchTag = findNearestMatch(event);
429 if ((matchTag != null) && !matchTag.isOn()) {
430 return false;
431 }
432 return true;
433 }
434
435 /**
436 * Finds the nearest comment text tag that matches an audit event.
437 * The nearest tag is before the line and column of the event.
438 * @param event the <code>AuditEvent</code> to match.
439 * @return The <code>Tag</code> nearest event.
440 */
441 private Tag findNearestMatch(AuditEvent event)
442 {
443 Tag result = null;
444 // TODO: try binary search if sequential search becomes a performance
445 // problem.
446 for (Tag tag : tags) {
447 if ((tag.getLine() > event.getLine())
448 || ((tag.getLine() == event.getLine())
449 && (tag.getColumn() > event.getColumn())))
450 {
451 break;
452 }
453 if (tag.isMatch(event)) {
454 result = tag;
455 }
456 };
457 return result;
458 }
459
460 /**
461 * Collects all the suppression tags for all comments into a list and
462 * sorts the list.
463 */
464 private void tagSuppressions()
465 {
466 tags.clear();
467 final FileContents contents = getFileContents();
468 if (checkCPP) {
469 tagSuppressions(contents.getCppComments().values());
470 }
471 if (checkC) {
472 final Collection<List<TextBlock>> cCoonts = contents
473 .getCComments().values();
474 for (List<TextBlock> eleont : cCoonts) {
475 tagSuppressions(eleont);
476 }
477 }
478 Collections.sort(tags);
479 }
480
481 /**
482 * Appends the suppressions in a collection of comments to the full
483 * set of suppression tags.
484 * @param comments the set of comments.
485 */
486 private void tagSuppressions(Collection<TextBlock> comments)
487 {
488 for (TextBlock comment : comments) {
489 final int startLineNo = comment.getStartLineNo();
490 final String[] text = comment.getText();
491 tagCommentLine(text[0], startLineNo, comment.getStartColNo());
492 for (int i = 1; i < text.length; i++) {
493 tagCommentLine(text[i], startLineNo + i, 0);
494 }
495 }
496 }
497
498 /**
499 * Tags a string if it matches the format for turning
500 * checkstyle reporting on or the format for turning reporting off.
501 * @param text the string to tag.
502 * @param line the line number of text.
503 * @param column the column number of text.
504 */
505 private void tagCommentLine(String text, int line, int column)
506 {
507 final Matcher offMatcher = offRegexp.matcher(text);
508 if (offMatcher.find()) {
509 addTag(offMatcher.group(0), line, column, false);
510 }
511 else {
512 final Matcher onMatcher = onRegexp.matcher(text);
513 if (onMatcher.find()) {
514 addTag(onMatcher.group(0), line, column, true);
515 }
516 }
517 }
518
519 /**
520 * Adds a <code>Tag</code> to the list of all tags.
521 * @param text the text of the tag.
522 * @param line the line number of the tag.
523 * @param column the column number of the tag.
524 * @param on <code>true</code> if the tag turns checkstyle reporting on.
525 */
526 private void addTag(String text, int line, int column, boolean on)
527 {
528 final Tag tag = new Tag(line, column, text, on);
529 tags.add(tag);
530 }
531 }