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