1 ////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code for adherence to a set of rules.
3 // Copyright (C) 2001-2014 Oliver Burn
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 =
294 new WeakReference<FileContents>(null);
295
296 /**
297 * Constructs a SuppressionCoontFilter.
298 * Initializes comment on, comment off, and check formats
299 * to defaults.
300 */
301 public SuppressionCommentFilter()
302 {
303 setOnCommentFormat(DEFAULT_ON_FORMAT);
304 setOffCommentFormat(DEFAULT_OFF_FORMAT);
305 setCheckFormat(DEFAULT_CHECK_FORMAT);
306 }
307
308 /**
309 * Set the format for a comment that turns off reporting.
310 * @param format a <code>String</code> value.
311 * @throws ConversionException unable to parse format.
312 */
313 public void setOffCommentFormat(String format)
314 throws ConversionException
315 {
316 try {
317 offRegexp = Utils.getPattern(format);
318 }
319 catch (final PatternSyntaxException e) {
320 throw new ConversionException("unable to parse " + format, e);
321 }
322 }
323
324 /**
325 * Set the format for a comment that turns on reporting.
326 * @param format a <code>String</code> value
327 * @throws ConversionException unable to parse format
328 */
329 public void setOnCommentFormat(String format)
330 throws ConversionException
331 {
332 try {
333 onRegexp = Utils.getPattern(format);
334 }
335 catch (final PatternSyntaxException e) {
336 throw new ConversionException("unable to parse " + format, e);
337 }
338 }
339
340 /** @return the FileContents for this filter. */
341 public FileContents getFileContents()
342 {
343 return fileContentsReference.get();
344 }
345
346 /**
347 * Set the FileContents for this filter.
348 * @param fileContents the FileContents for this filter.
349 */
350 public void setFileContents(FileContents fileContents)
351 {
352 fileContentsReference = new WeakReference<FileContents>(fileContents);
353 }
354
355 /**
356 * Set the format for a check.
357 * @param format a <code>String</code> value
358 * @throws ConversionException unable to parse format
359 */
360 public void setCheckFormat(String format)
361 throws ConversionException
362 {
363 try {
364 checkRegexp = Utils.getPattern(format);
365 checkFormat = format;
366 }
367 catch (final PatternSyntaxException e) {
368 throw new ConversionException("unable to parse " + format, e);
369 }
370 }
371
372 /**
373 * Set the format for a message.
374 * @param format a <code>String</code> value
375 * @throws ConversionException unable to parse format
376 */
377 public void setMessageFormat(String format)
378 throws ConversionException
379 {
380 // check that format parses
381 try {
382 Utils.getPattern(format);
383 }
384 catch (final PatternSyntaxException e) {
385 throw new ConversionException("unable to parse " + format, e);
386 }
387 messageFormat = format;
388 }
389
390
391 /**
392 * Set whether to look in C++ comments.
393 * @param checkCPP <code>true</code> if C++ comments are checked.
394 */
395 public void setCheckCPP(boolean checkCPP)
396 {
397 this.checkCPP = checkCPP;
398 }
399
400 /**
401 * Set whether to look in C comments.
402 * @param checkC <code>true</code> if C comments are checked.
403 */
404 public void setCheckC(boolean checkC)
405 {
406 this.checkC = checkC;
407 }
408
409 /** {@inheritDoc} */
410 @Override
411 public boolean accept(AuditEvent event)
412 {
413 if (event.getLocalizedMessage() == null) {
414 return true; // A special event.
415 }
416
417 // Lazy update. If the first event for the current file, update file
418 // contents and tag suppressions
419 final FileContents currentContents = FileContentsHolder.getContents();
420 if (currentContents == null) {
421 // we have no contents, so we can not filter.
422 // TODO: perhaps we should notify user somehow?
423 return true;
424 }
425 if (getFileContents() != currentContents) {
426 setFileContents(currentContents);
427 tagSuppressions();
428 }
429 final Tag matchTag = findNearestMatch(event);
430 if ((matchTag != null) && !matchTag.isOn()) {
431 return false;
432 }
433 return true;
434 }
435
436 /**
437 * Finds the nearest comment text tag that matches an audit event.
438 * The nearest tag is before the line and column of the event.
439 * @param event the <code>AuditEvent</code> to match.
440 * @return The <code>Tag</code> nearest event.
441 */
442 private Tag findNearestMatch(AuditEvent event)
443 {
444 Tag result = null;
445 // TODO: try binary search if sequential search becomes a performance
446 // problem.
447 for (Tag tag : tags) {
448 if ((tag.getLine() > event.getLine())
449 || ((tag.getLine() == event.getLine())
450 && (tag.getColumn() > event.getColumn())))
451 {
452 break;
453 }
454 if (tag.isMatch(event)) {
455 result = tag;
456 }
457 };
458 return result;
459 }
460
461 /**
462 * Collects all the suppression tags for all comments into a list and
463 * sorts the list.
464 */
465 private void tagSuppressions()
466 {
467 tags.clear();
468 final FileContents contents = getFileContents();
469 if (checkCPP) {
470 tagSuppressions(contents.getCppComments().values());
471 }
472 if (checkC) {
473 final Collection<List<TextBlock>> cCoonts = contents
474 .getCComments().values();
475 for (List<TextBlock> eleont : cCoonts) {
476 tagSuppressions(eleont);
477 }
478 }
479 Collections.sort(tags);
480 }
481
482 /**
483 * Appends the suppressions in a collection of comments to the full
484 * set of suppression tags.
485 * @param comments the set of comments.
486 */
487 private void tagSuppressions(Collection<TextBlock> comments)
488 {
489 for (TextBlock comment : comments) {
490 final int startLineNo = comment.getStartLineNo();
491 final String[] text = comment.getText();
492 tagCommentLine(text[0], startLineNo, comment.getStartColNo());
493 for (int i = 1; i < text.length; i++) {
494 tagCommentLine(text[i], startLineNo + i, 0);
495 }
496 }
497 }
498
499 /**
500 * Tags a string if it matches the format for turning
501 * checkstyle reporting on or the format for turning reporting off.
502 * @param text the string to tag.
503 * @param line the line number of text.
504 * @param column the column number of text.
505 */
506 private void tagCommentLine(String text, int line, int column)
507 {
508 final Matcher offMatcher = offRegexp.matcher(text);
509 if (offMatcher.find()) {
510 addTag(offMatcher.group(0), line, column, false);
511 }
512 else {
513 final Matcher onMatcher = onRegexp.matcher(text);
514 if (onMatcher.find()) {
515 addTag(onMatcher.group(0), line, column, true);
516 }
517 }
518 }
519
520 /**
521 * Adds a <code>Tag</code> to the list of all tags.
522 * @param text the text of the tag.
523 * @param line the line number of the tag.
524 * @param column the column number of the tag.
525 * @param on <code>true</code> if the tag turns checkstyle reporting on.
526 */
527 private void addTag(String text, int line, int column, boolean on)
528 {
529 final Tag tag = new Tag(line, column, text, on);
530 tags.add(tag);
531 }
532 }