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