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