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.Iterator; 033import java.util.List; 034import java.util.regex.Matcher; 035import java.util.regex.Pattern; 036import java.util.regex.PatternSyntaxException; 037import org.apache.commons.beanutils.ConversionException; 038 039/** 040 * <p> 041 * A filter that uses nearby comments to suppress audit events. 042 * </p> 043 * <p> 044 * This check is philosophically similar to {@link SuppressionCommentFilter}. 045 * Unlike {@link SuppressionCommentFilter}, this filter does not require 046 * pairs of comments. This check may be used to suppress warnings in the 047 * current line: 048 * <pre> 049 * offendingLine(for, whatever, reason); // SUPPRESS ParameterNumberCheck 050 * </pre> 051 * or it may be configured to span multiple lines, either forward: 052 * <pre> 053 * // PERMIT MultipleVariableDeclarations NEXT 3 LINES 054 * double x1 = 1.0, y1 = 0.0, z1 = 0.0; 055 * double x2 = 0.0, y2 = 1.0, z2 = 0.0; 056 * double x3 = 0.0, y3 = 0.0, z3 = 1.0; 057 * </pre> 058 * or reverse: 059 * <pre> 060 * try { 061 * thirdPartyLibrary.method(); 062 * } catch (RuntimeException e) { 063 * // ALLOW ILLEGAL CATCH BECAUSE third party API wraps everything 064 * // in RuntimeExceptions. 065 * ... 066 * } 067 * </pre> 068 * 069 * <p> 070 * See {@link SuppressionCommentFilter} for usage notes. 071 * 072 * 073 * @author Mick Killianey 074 */ 075public class SuppressWithNearbyCommentFilter 076 extends AutomaticBean 077 implements Filter 078{ 079 /** 080 * A Tag holds a suppression comment and its location. 081 */ 082 public class Tag implements Comparable<Tag> 083 { 084 /** The text of the tag. */ 085 private final String text; 086 087 /** The first line where warnings may be suppressed. */ 088 private int firstLine; 089 090 /** The last line where warnings may be suppressed. */ 091 private int lastLine; 092 093 /** The parsed check regexp, expanded for the text of this tag. */ 094 private Pattern tagCheckRegexp; 095 096 /** The parsed message regexp, expanded for the text of this tag. */ 097 private Pattern tagMessageRegexp; 098 099 /** 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 = 303 new WeakReference<FileContents>(null); 304 305 /** 306 * Constructs a SuppressionCommentFilter. 307 * Initializes comment on, comment off, and check formats 308 * to defaults. 309 */ 310 public SuppressWithNearbyCommentFilter() 311 { 312 if (DEFAULT_COMMENT_FORMAT != null) { 313 setCommentFormat(DEFAULT_COMMENT_FORMAT); 314 } 315 if (DEFAULT_CHECK_FORMAT != null) { 316 setCheckFormat(DEFAULT_CHECK_FORMAT); 317 } 318 if (DEFAULT_MESSAGE_FORMAT != null) { 319 setMessageFormat(DEFAULT_MESSAGE_FORMAT); 320 } 321 if (DEFAULT_INFLUENCE_FORMAT != null) { 322 setInfluenceFormat(DEFAULT_INFLUENCE_FORMAT); 323 } 324 } 325 326 /** 327 * Set the format for a comment that turns off reporting. 328 * @param format a <code>String</code> value. 329 * @throws ConversionException unable to parse format. 330 */ 331 public void setCommentFormat(String format) 332 throws ConversionException 333 { 334 try { 335 commentRegexp = Utils.getPattern(format); 336 } 337 catch (final PatternSyntaxException e) { 338 throw new ConversionException("unable to parse " + format, e); 339 } 340 } 341 342 /** @return the FileContents for this filter. */ 343 public FileContents getFileContents() 344 { 345 return fileContentsReference.get(); 346 } 347 348 /** 349 * Set the FileContents for this filter. 350 * @param fileContents the FileContents for this filter. 351 */ 352 public void setFileContents(FileContents fileContents) 353 { 354 fileContentsReference = new WeakReference<FileContents>(fileContents); 355 } 356 357 /** 358 * Set the format for a check. 359 * @param format a <code>String</code> value 360 * @throws ConversionException unable to parse format 361 */ 362 public void setCheckFormat(String format) 363 throws ConversionException 364 { 365 try { 366 checkRegexp = Utils.getPattern(format); 367 checkFormat = format; 368 } 369 catch (final PatternSyntaxException e) { 370 throw new ConversionException("unable to parse " + format, e); 371 } 372 } 373 374 /** 375 * Set the format for a message. 376 * @param format a <code>String</code> value 377 * @throws ConversionException unable to parse format 378 */ 379 public void setMessageFormat(String format) 380 throws ConversionException 381 { 382 // check that format parses 383 try { 384 Utils.getPattern(format); 385 } 386 catch (final PatternSyntaxException e) { 387 throw new ConversionException("unable to parse " + format, e); 388 } 389 messageFormat = format; 390 } 391 392 /** 393 * Set the format for the influence of this check. 394 * @param format a <code>String</code> value 395 * @throws ConversionException unable to parse format 396 */ 397 public void setInfluenceFormat(String format) 398 throws ConversionException 399 { 400 // check that format parses 401 try { 402 Utils.getPattern(format); 403 } 404 catch (final PatternSyntaxException e) { 405 throw new ConversionException("unable to parse " + format, e); 406 } 407 influenceFormat = format; 408 } 409 410 411 /** 412 * Set whether to look in C++ comments. 413 * @param checkCPP <code>true</code> if C++ comments are checked. 414 */ 415 public void setCheckCPP(boolean checkCPP) 416 { 417 this.checkCPP = checkCPP; 418 } 419 420 /** 421 * Set whether to look in C comments. 422 * @param checkC <code>true</code> if C comments are checked. 423 */ 424 public void setCheckC(boolean checkC) 425 { 426 this.checkC = checkC; 427 } 428 429 /** {@inheritDoc} */ 430 @Override 431 public boolean accept(AuditEvent event) 432 { 433 if (event.getLocalizedMessage() == null) { 434 return true; // A special event. 435 } 436 437 // Lazy update. If the first event for the current file, update file 438 // contents and tag suppressions 439 final FileContents currentContents = FileContentsHolder.getContents(); 440 if (currentContents == null) { 441 // we have no contents, so we can not filter. 442 // TODO: perhaps we should notify user somehow? 443 return true; 444 } 445 if (getFileContents() != currentContents) { 446 setFileContents(currentContents); 447 tagSuppressions(); 448 } 449 for (final Iterator<Tag> iter = tags.iterator(); iter.hasNext();) { 450 final Tag tag = iter.next(); 451 if (tag.isMatch(event)) { 452 return false; 453 } 454 } 455 return true; 456 } 457 458 /** 459 * Collects all the suppression tags for all comments into a list and 460 * sorts the list. 461 */ 462 private void tagSuppressions() 463 { 464 tags.clear(); 465 final FileContents contents = getFileContents(); 466 if (checkCPP) { 467 tagSuppressions(contents.getCppComments().values()); 468 } 469 if (checkC) { 470 final Collection<List<TextBlock>> cComments = 471 contents.getCComments().values(); 472 for (final List<TextBlock> element : cComments) { 473 tagSuppressions(element); 474 } 475 } 476 Collections.sort(tags); 477 } 478 479 /** 480 * Appends the suppressions in a collection of comments to the full 481 * set of suppression tags. 482 * @param comments the set of comments. 483 */ 484 private void tagSuppressions(Collection<TextBlock> comments) 485 { 486 for (final TextBlock comment : comments) { 487 final int startLineNo = comment.getStartLineNo(); 488 final String[] text = comment.getText(); 489 tagCommentLine(text[0], startLineNo); 490 for (int i = 1; i < text.length; i++) { 491 tagCommentLine(text[i], startLineNo + i); 492 } 493 } 494 } 495 496 /** 497 * Tags a string if it matches the format for turning 498 * checkstyle reporting on or the format for turning reporting off. 499 * @param text the string to tag. 500 * @param line the line number of text. 501 */ 502 private void tagCommentLine(String text, int line) 503 { 504 final Matcher matcher = commentRegexp.matcher(text); 505 if (matcher.find()) { 506 addTag(matcher.group(0), line); 507 } 508 } 509 510 /** 511 * Adds a comment suppression <code>Tag</code> to the list of all tags. 512 * @param text the text of the tag. 513 * @param line the line number of the tag. 514 */ 515 private void addTag(String text, int line) 516 { 517 final Tag tag = new Tag(text, line); 518 tags.add(tag); 519 } 520}