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