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.Iterator;
33 import java.util.List;
34 import java.util.regex.Matcher;
35 import java.util.regex.Pattern;
36 import java.util.regex.PatternSyntaxException;
37 import org.apache.commons.beanutils.ConversionException;
38
39 /**
40 * <p>
41 * A filter that uses nearby comments to suppress audit events.
42 * </p>
43 * <p>
44 * This check is philosophically similar to {@link SuppressionCommentFilter}.
45 * Unlike {@link SuppressionCommentFilter}, this filter does not require
46 * pairs of comments. This check may be used to suppress warnings in the
47 * current line:
48 * <pre>
49 * offendingLine(for, whatever, reason); // SUPPRESS ParameterNumberCheck
50 * </pre>
51 * or it may be configured to span multiple lines, either forward:
52 * <pre>
53 * // PERMIT MultipleVariableDeclarations NEXT 3 LINES
54 * double x1 = 1.0, y1 = 0.0, z1 = 0.0;
55 * double x2 = 0.0, y2 = 1.0, z2 = 0.0;
56 * double x3 = 0.0, y3 = 0.0, z3 = 1.0;
57 * </pre>
58 * or reverse:
59 * <pre>
60 * try {
61 * thirdPartyLibrary.method();
62 * } catch (RuntimeException e) {
63 * // ALLOW ILLEGAL CATCH BECAUSE third party API wraps everything
64 * // in RuntimeExceptions.
65 * ...
66 * }
67 * </pre>
68 *
69 * <p>
70 * See {@link SuppressionCommentFilter} for usage notes.
71 *
72 *
73 * @author Mick Killianey
74 */
75 public class SuppressWithNearbyCommentFilter
76 extends AutomaticBean
77 implements Filter
78 {
79 /**
80 * A Tag holds a suppression comment and its location.
81 */
82 public class Tag implements Comparable<Tag>
83 {
84 /** The text of the tag. */
85 private final String text;
86
87 /** The first line where warnings may be suppressed. */
88 private int firstLine;
89
90 /** The last line where warnings may be suppressed. */
91 private int lastLine;
92
93 /** The parsed check regexp, expanded for the text of this tag. */
94 private Pattern tagCheckRegexp;
95
96 /** The parsed message regexp, expanded for the text of this tag. */
97 private Pattern tagMessageRegexp;
98
99 /**
100 * Constructs a tag.
101 * @param text the text of the suppression.
102 * @param line the line number.
103 * @throws ConversionException if unable to parse expanded text.
104 * on.
105 */
106 public Tag(String text, int line)
107 throws ConversionException
108 {
109 this.text = text;
110
111 tagCheckRegexp = checkRegexp;
112 //Expand regexp for check and message
113 //Does not intern Patterns with Utils.getPattern()
114 String format = "";
115 try {
116 format = expandFrocomment(text, checkFormat, commentRegexp);
117 tagCheckRegexp = Pattern.compile(format);
118 if (messageFormat != null) {
119 format = expandFrocomment(
120 text, messageFormat, commentRegexp);
121 tagMessageRegexp = Pattern.compile(format);
122 }
123 int influence = 0;
124 if (influenceFormat != null) {
125 format = expandFrocomment(
126 text, influenceFormat, commentRegexp);
127 try {
128 if (format.startsWith("+")) {
129 format = format.substring(1);
130 }
131 influence = Integer.parseInt(format);
132 }
133 catch (final NumberFormatException e) {
134 throw new ConversionException(
135 "unable to parse influence from '" + text
136 + "' using " + influenceFormat, e);
137 }
138 }
139 if (influence >= 0) {
140 firstLine = line;
141 lastLine = line + influence;
142 }
143 else {
144 firstLine = line + influence;
145 lastLine = line;
146 }
147 }
148 catch (final PatternSyntaxException e) {
149 throw new ConversionException(
150 "unable to parse expanded comment " + format,
151 e);
152 }
153 }
154
155 /** @return the text of the tag. */
156 public String getText()
157 {
158 return text;
159 }
160
161 /** @return the line number of the first suppressed line. */
162 public int getFirstLine()
163 {
164 return firstLine;
165 }
166
167 /** @return the line number of the last suppressed line. */
168 public int getLastLine()
169 {
170 return lastLine;
171 }
172
173 /**
174 * Compares the position of this tag in the file
175 * with the position of another tag.
176 * @param other the tag to compare with this one.
177 * @return a negative number if this tag is before the other tag,
178 * 0 if they are at the same position, and a positive number if this
179 * tag is after the other tag.
180 * @see java.lang.Comparable#compareTo(java.lang.Object)
181 */
182 @Override
183 public int compareTo(Tag other)
184 {
185 if (firstLine == other.firstLine) {
186 return lastLine - other.lastLine;
187 }
188
189 return (firstLine - other.firstLine);
190 }
191
192 /**
193 * Determines whether the source of an audit event
194 * matches the text of this tag.
195 * @param event the <code>AuditEvent</code> to check.
196 * @return true if the source of event matches the text of this tag.
197 */
198 public boolean isMatch(AuditEvent event)
199 {
200 final int line = event.getLine();
201 if (line < firstLine) {
202 return false;
203 }
204 if (line > lastLine) {
205 return false;
206 }
207 final Matcher tagMatcher =
208 tagCheckRegexp.matcher(event.getSourceName());
209 if (tagMatcher.find()) {
210 return true;
211 }
212 if (tagMessageRegexp != null) {
213 final Matcher messageMatcher =
214 tagMessageRegexp.matcher(event.getMessage());
215 return messageMatcher.find();
216 }
217 return false;
218 }
219
220 /**
221 * Expand based on a matching comment.
222 * @param comment the comment.
223 * @param string the string to expand.
224 * @param regexp the parsed expander.
225 * @return the expanded string
226 */
227 private String expandFrocomment(
228 String comment,
229 String string,
230 Pattern regexp)
231 {
232 final Matcher matcher = regexp.matcher(comment);
233 // Match primarily for effect.
234 if (!matcher.find()) {
235 ///CLOVER:OFF
236 return string;
237 ///CLOVER:ON
238 }
239 String result = string;
240 for (int i = 0; i <= matcher.groupCount(); i++) {
241 // $n expands comment match like in Pattern.subst().
242 result = result.replaceAll("\\$" + i, matcher.group(i));
243 }
244 return result;
245 }
246
247 /** {@inheritDoc} */
248 @Override
249 public final String toString()
250 {
251 return "Tag[lines=[" + getFirstLine() + " to " + getLastLine()
252 + "]; text='" + getText() + "']";
253 }
254 }
255
256 /** Format to turns checkstyle reporting off. */
257 private static final String DEFAULT_COMMENT_FORMAT =
258 "SUPPRESS CHECKSTYLE (\\w+)";
259
260 /** Default regex for checks that should be suppressed. */
261 private static final String DEFAULT_CHECK_FORMAT = ".*";
262
263 /** Default regex for messages that should be suppressed. */
264 private static final String DEFAULT_MESSAGE_FORMAT = null;
265
266 /** Default regex for lines that should be suppressed. */
267 private static final String DEFAULT_INFLUENCE_FORMAT = "0";
268
269 /** Whether to look for trigger in C-style comments. */
270 private boolean checkC = true;
271
272 /** Whether to look for trigger in C++-style comments. */
273 private boolean checkCPP = true;
274
275 /** Parsed comment regexp that marks checkstyle suppression region. */
276 private Pattern commentRegexp;
277
278 /** The comment pattern that triggers suppression. */
279 private String checkFormat;
280
281 /** The parsed check regexp. */
282 private Pattern checkRegexp;
283
284 /** The message format to suppress. */
285 private String messageFormat;
286
287 /** The influence of the suppression comment. */
288 private String influenceFormat;
289
290
291 //TODO: Investigate performance improvement with array
292 /** Tagged comments */
293 private final List<Tag> tags = Lists.newArrayList();
294
295 /**
296 * References the current FileContents for this filter.
297 * Since this is a weak reference to the FileContents, the FileContents
298 * can be reclaimed as soon as the strong references in TreeWalker
299 * and FileContentsHolder are reassigned to the next FileContents,
300 * at which time filtering for the current FileContents is finished.
301 */
302 private WeakReference<FileContents> fileContentsReference =
303 new WeakReference<FileContents>(null);
304
305 /**
306 * Constructs a SuppressionCommentFilter.
307 * Initializes comment on, comment off, and check formats
308 * to defaults.
309 */
310 public SuppressWithNearbyCommentFilter()
311 {
312 if (DEFAULT_COMMENT_FORMAT != null) {
313 setCommentFormat(DEFAULT_COMMENT_FORMAT);
314 }
315 if (DEFAULT_CHECK_FORMAT != null) {
316 setCheckFormat(DEFAULT_CHECK_FORMAT);
317 }
318 if (DEFAULT_MESSAGE_FORMAT != null) {
319 setMessageFormat(DEFAULT_MESSAGE_FORMAT);
320 }
321 if (DEFAULT_INFLUENCE_FORMAT != null) {
322 setInfluenceFormat(DEFAULT_INFLUENCE_FORMAT);
323 }
324 }
325
326 /**
327 * Set the format for a comment that turns off reporting.
328 * @param format a <code>String</code> value.
329 * @throws ConversionException unable to parse format.
330 */
331 public void setCommentFormat(String format)
332 throws ConversionException
333 {
334 try {
335 commentRegexp = Utils.getPattern(format);
336 }
337 catch (final PatternSyntaxException e) {
338 throw new ConversionException("unable to parse " + format, e);
339 }
340 }
341
342 /** @return the FileContents for this filter. */
343 public FileContents getFileContents()
344 {
345 return fileContentsReference.get();
346 }
347
348 /**
349 * Set the FileContents for this filter.
350 * @param fileContents the FileContents for this filter.
351 */
352 public void setFileContents(FileContents fileContents)
353 {
354 fileContentsReference = new WeakReference<FileContents>(fileContents);
355 }
356
357 /**
358 * Set the format for a check.
359 * @param format a <code>String</code> value
360 * @throws ConversionException unable to parse format
361 */
362 public void setCheckFormat(String format)
363 throws ConversionException
364 {
365 try {
366 checkRegexp = Utils.getPattern(format);
367 checkFormat = format;
368 }
369 catch (final PatternSyntaxException e) {
370 throw new ConversionException("unable to parse " + format, e);
371 }
372 }
373
374 /**
375 * Set the format for a message.
376 * @param format a <code>String</code> value
377 * @throws ConversionException unable to parse format
378 */
379 public void setMessageFormat(String format)
380 throws ConversionException
381 {
382 // check that format parses
383 try {
384 Utils.getPattern(format);
385 }
386 catch (final PatternSyntaxException e) {
387 throw new ConversionException("unable to parse " + format, e);
388 }
389 messageFormat = format;
390 }
391
392 /**
393 * Set the format for the influence of this check.
394 * @param format a <code>String</code> value
395 * @throws ConversionException unable to parse format
396 */
397 public void setInfluenceFormat(String format)
398 throws ConversionException
399 {
400 // check that format parses
401 try {
402 Utils.getPattern(format);
403 }
404 catch (final PatternSyntaxException e) {
405 throw new ConversionException("unable to parse " + format, e);
406 }
407 influenceFormat = format;
408 }
409
410
411 /**
412 * Set whether to look in C++ comments.
413 * @param checkCPP <code>true</code> if C++ comments are checked.
414 */
415 public void setCheckCPP(boolean checkCPP)
416 {
417 this.checkCPP = checkCPP;
418 }
419
420 /**
421 * Set whether to look in C comments.
422 * @param checkC <code>true</code> if C comments are checked.
423 */
424 public void setCheckC(boolean checkC)
425 {
426 this.checkC = checkC;
427 }
428
429 /** {@inheritDoc} */
430 @Override
431 public boolean accept(AuditEvent event)
432 {
433 if (event.getLocalizedMessage() == null) {
434 return true; // A special event.
435 }
436
437 // Lazy update. If the first event for the current file, update file
438 // contents and tag suppressions
439 final FileContents currentContents = FileContentsHolder.getContents();
440 if (currentContents == null) {
441 // we have no contents, so we can not filter.
442 // TODO: perhaps we should notify user somehow?
443 return true;
444 }
445 if (getFileContents() != currentContents) {
446 setFileContents(currentContents);
447 tagSuppressions();
448 }
449 for (final Iterator<Tag> iter = tags.iterator(); iter.hasNext();) {
450 final Tag tag = iter.next();
451 if (tag.isMatch(event)) {
452 return false;
453 }
454 }
455 return true;
456 }
457
458 /**
459 * Collects all the suppression tags for all comments into a list and
460 * sorts the list.
461 */
462 private void tagSuppressions()
463 {
464 tags.clear();
465 final FileContents contents = getFileContents();
466 if (checkCPP) {
467 tagSuppressions(contents.getCppComments().values());
468 }
469 if (checkC) {
470 final Collection<List<TextBlock>> cComments =
471 contents.getCComments().values();
472 for (final List<TextBlock> element : cComments) {
473 tagSuppressions(element);
474 }
475 }
476 Collections.sort(tags);
477 }
478
479 /**
480 * Appends the suppressions in a collection of comments to the full
481 * set of suppression tags.
482 * @param comments the set of comments.
483 */
484 private void tagSuppressions(Collection<TextBlock> comments)
485 {
486 for (final TextBlock comment : comments) {
487 final int startLineNo = comment.getStartLineNo();
488 final String[] text = comment.getText();
489 tagCommentLine(text[0], startLineNo);
490 for (int i = 1; i < text.length; i++) {
491 tagCommentLine(text[i], startLineNo + i);
492 }
493 }
494 }
495
496 /**
497 * Tags a string if it matches the format for turning
498 * checkstyle reporting on or the format for turning reporting off.
499 * @param text the string to tag.
500 * @param line the line number of text.
501 */
502 private void tagCommentLine(String text, int line)
503 {
504 final Matcher matcher = commentRegexp.matcher(text);
505 if (matcher.find()) {
506 addTag(matcher.group(0), line);
507 }
508 }
509
510 /**
511 * Adds a comment suppression <code>Tag</code> to the list of all tags.
512 * @param text the text of the tag.
513 * @param line the line number of the tag.
514 */
515 private void addTag(String text, int line)
516 {
517 final Tag tag = new Tag(text, line);
518 tags.add(tag);
519 }
520 }