001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2014 Oliver Burn 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018//////////////////////////////////////////////////////////////////////////////// 019package com.puppycrawl.tools.checkstyle.filters; 020 021import com.google.common.collect.Lists; 022import com.puppycrawl.tools.checkstyle.api.AuditEvent; 023import com.puppycrawl.tools.checkstyle.api.AutomaticBean; 024import com.puppycrawl.tools.checkstyle.api.FileContents; 025import com.puppycrawl.tools.checkstyle.api.Filter; 026import com.puppycrawl.tools.checkstyle.api.TextBlock; 027import com.puppycrawl.tools.checkstyle.api.Utils; 028import com.puppycrawl.tools.checkstyle.checks.FileContentsHolder; 029import java.lang.ref.WeakReference; 030import java.util.Collection; 031import java.util.Collections; 032import java.util.List; 033import java.util.regex.Matcher; 034import java.util.regex.Pattern; 035import java.util.regex.PatternSyntaxException; 036import org.apache.commons.beanutils.ConversionException; 037 038/** 039 * <p> 040 * A filter that uses comments to suppress audit events. 041 * </p> 042 * <p> 043 * Rationale: 044 * Sometimes there are legitimate reasons for violating a check. When 045 * this is a matter of the code in question and not personal 046 * preference, the best place to override the policy is in the code 047 * itself. Semi-structured comments can be associated with the check. 048 * This is sometimes superior to a separate suppressions file, which 049 * must be kept up-to-date as the source file is edited. 050 * </p> 051 * <p> 052 * Usage: 053 * This check only works in conjunction with the FileContentsHolder module 054 * since that module makes the suppression comments in the .java 055 * files available <i>sub rosa</i>. 056 * </p> 057 * @see FileContentsHolder 058 * @author Mike McMahon 059 * @author Rick Giles 060 */ 061public class SuppressionCommentFilter 062 extends AutomaticBean 063 implements Filter 064{ 065 /** 066 * A Tag holds a suppression comment and its location, and determines 067 * whether the supression turns checkstyle reporting on or off. 068 * @author Rick Giles 069 */ 070 public class Tag 071 implements Comparable<Tag> 072 { 073 /** The text of the tag. */ 074 private final String mText; 075 076 /** The line number of the tag. */ 077 private final int mLine; 078 079 /** The column number of the tag. */ 080 private final int mColumn; 081 082 /** Determines whether the suppression turns checkstyle reporting on. */ 083 private final boolean mOn; 084 085 /** The parsed check regexp, expanded for the text of this tag. */ 086 private Pattern mTagCheckRegexp; 087 088 /** The parsed message regexp, expanded for the text of this tag. */ 089 private Pattern mTagMessageRegexp; 090 091 /** 092 * Constructs a tag. 093 * @param aLine the line number. 094 * @param aColumn the column number. 095 * @param aText the text of the suppression. 096 * @param aOn <code>true</code> if the tag turns checkstyle reporting. 097 * @throws ConversionException if unable to parse expanded aText. 098 * on. 099 */ 100 public Tag(int aLine, int aColumn, String aText, boolean aOn) 101 throws ConversionException 102 { 103 mLine = aLine; 104 mColumn = aColumn; 105 mText = aText; 106 mOn = aOn; 107 108 mTagCheckRegexp = mCheckRegexp; 109 //Expand regexp for check and message 110 //Does not intern Patterns with Utils.getPattern() 111 String format = ""; 112 try { 113 if (aOn) { 114 format = 115 expandFromComment(aText, mCheckFormat, mOnRegexp); 116 mTagCheckRegexp = Pattern.compile(format); 117 if (mMessageFormat != null) { 118 format = 119 expandFromComment(aText, mMessageFormat, mOnRegexp); 120 mTagMessageRegexp = Pattern.compile(format); 121 } 122 } 123 else { 124 format = 125 expandFromComment(aText, mCheckFormat, mOffRegexp); 126 mTagCheckRegexp = Pattern.compile(format); 127 if (mMessageFormat != null) { 128 format = 129 expandFromComment( 130 aText, 131 mMessageFormat, 132 mOffRegexp); 133 mTagMessageRegexp = 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 mText; 148 } 149 150 /** @return the line number of the tag in the source file. */ 151 public int getLine() 152 { 153 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 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 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 if (mLine == aObject.mLine) { 189 return mColumn - aObject.mColumn; 190 } 191 192 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 final Matcher tagMatcher = 204 mTagCheckRegexp.matcher(aEvent.getSourceName()); 205 if (tagMatcher.find()) { 206 if (mTagMessageRegexp != null) { 207 final Matcher messageMatcher = 208 mTagMessageRegexp.matcher(aEvent.getMessage()); 209 return messageMatcher.find(); 210 } 211 return true; 212 } 213 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 final Matcher matcher = aRegexp.matcher(aComment); 229 // Match primarily for effect. 230 if (!matcher.find()) { 231 ///CLOVER:OFF 232 return aString; 233 ///CLOVER:ON 234 } 235 String result = aString; 236 for (int i = 0; i <= matcher.groupCount(); i++) { 237 // $n expands comment match like in Pattern.subst(). 238 result = result.replaceAll("\\$" + i, matcher.group(i)); 239 } 240 return result; 241 } 242 243 @Override 244 public final String toString() 245 { 246 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 private boolean mCheckC = true; 262 263 /** Whether to look in comments of the C++ type. */ 264 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 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 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 { 302 setOnCommentFormat(DEFAULT_ON_FORMAT); 303 setOffCommentFormat(DEFAULT_OFF_FORMAT); 304 setCheckFormat(DEFAULT_CHECK_FORMAT); 305 } 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 mOffRegexp = Utils.getPattern(aFormat); 317 } 318 catch (final PatternSyntaxException e) { 319 throw new ConversionException("unable to parse " + aFormat, e); 320 } 321 } 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 mOnRegexp = Utils.getPattern(aFormat); 333 } 334 catch (final PatternSyntaxException e) { 335 throw new ConversionException("unable to parse " + aFormat, e); 336 } 337 } 338 339 /** @return the FileContents for this filter. */ 340 public FileContents getFileContents() 341 { 342 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 mFileContentsReference = new WeakReference<FileContents>(aFileContents); 352 } 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 mCheckRegexp = Utils.getPattern(aFormat); 364 mCheckFormat = aFormat; 365 } 366 catch (final PatternSyntaxException e) { 367 throw new ConversionException("unable to parse " + aFormat, e); 368 } 369 } 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 Utils.getPattern(aFormat); 382 } 383 catch (final PatternSyntaxException e) { 384 throw new ConversionException("unable to parse " + aFormat, e); 385 } 386 mMessageFormat = aFormat; 387 } 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 mCheckCPP = aCheckCPP; 397 } 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 mCheckC = aCheckC; 406 } 407 408 /** {@inheritDoc} */ 409 public boolean accept(AuditEvent aEvent) 410 { 411 if (aEvent.getLocalizedMessage() == null) { 412 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 final FileContents currentContents = FileContentsHolder.getContents(); 418 if (currentContents == null) { 419 // we have no contents, so we can not filter. 420 // TODO: perhaps we should notify user somehow? 421 return true; 422 } 423 if (getFileContents() != currentContents) { 424 setFileContents(currentContents); 425 tagSuppressions(); 426 } 427 final Tag matchTag = findNearestMatch(aEvent); 428 if ((matchTag != null) && !matchTag.isOn()) { 429 return false; 430 } 431 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 Tag result = null; 443 // TODO: try binary search if sequential search becomes a performance 444 // problem. 445 for (Tag tag : mTags) { 446 if ((tag.getLine() > aEvent.getLine()) 447 || ((tag.getLine() == aEvent.getLine()) 448 && (tag.getColumn() > aEvent.getColumn()))) 449 { 450 break; 451 } 452 if (tag.isMatch(aEvent)) { 453 result = tag; 454 } 455 }; 456 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 mTags.clear(); 466 final FileContents contents = getFileContents(); 467 if (mCheckCPP) { 468 tagSuppressions(contents.getCppComments().values()); 469 } 470 if (mCheckC) { 471 final Collection<List<TextBlock>> cComments = contents 472 .getCComments().values(); 473 for (List<TextBlock> element : cComments) { 474 tagSuppressions(element); 475 } 476 } 477 Collections.sort(mTags); 478 } 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 for (TextBlock comment : aComments) { 488 final int startLineNo = comment.getStartLineNo(); 489 final String[] text = comment.getText(); 490 tagCommentLine(text[0], startLineNo, comment.getStartColNo()); 491 for (int i = 1; i < text.length; i++) { 492 tagCommentLine(text[i], startLineNo + i, 0); 493 } 494 } 495 } 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 final Matcher offMatcher = mOffRegexp.matcher(aText); 507 if (offMatcher.find()) { 508 addTag(offMatcher.group(0), aLine, aColumn, false); 509 } 510 else { 511 final Matcher onMatcher = mOnRegexp.matcher(aText); 512 if (onMatcher.find()) { 513 addTag(onMatcher.group(0), aLine, aColumn, true); 514 } 515 } 516 } 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 final Tag tag = new Tag(aLine, aColumn, aText, aOn); 528 mTags.add(tag); 529 } 530}