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 mText; 086 087 /** The first line where warnings may be suppressed. */ 088 private int mFirstLine; 089 090 /** The last line where warnings may be suppressed. */ 091 private int mLastLine; 092 093 /** The parsed check regexp, expanded for the text of this tag. */ 094 private Pattern mTagCheckRegexp; 095 096 /** The parsed message regexp, expanded for the text of this tag. */ 097 private Pattern mTagMessageRegexp; 098 099 /** 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 { 109 mText = aText; 110 111 mTagCheckRegexp = mCheckRegexp; 112 //Expand regexp for check and message 113 //Does not intern Patterns with Utils.getPattern() 114 String format = ""; 115 try { 116 format = expandFromComment(aText, mCheckFormat, mCommentRegexp); 117 mTagCheckRegexp = Pattern.compile(format); 118 if (mMessageFormat != null) { 119 format = expandFromComment( 120 aText, mMessageFormat, mCommentRegexp); 121 mTagMessageRegexp = Pattern.compile(format); 122 } 123 int influence = 0; 124 if (mInfluenceFormat != null) { 125 format = expandFromComment( 126 aText, mInfluenceFormat, mCommentRegexp); 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 '" + aText 136 + "' using " + mInfluenceFormat, e); 137 } 138 } 139 if (influence >= 0) { 140 mFirstLine = aLine; 141 mLastLine = aLine + influence; 142 } 143 else { 144 mFirstLine = aLine + influence; 145 mLastLine = aLine; 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 mText; 159 } 160 161 /** @return the line number of the first suppressed line. */ 162 public int getFirstLine() 163 { 164 return mFirstLine; 165 } 166 167 /** @return the line number of the last suppressed line. */ 168 public int getLastLine() 169 { 170 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 if (mFirstLine == aOther.mFirstLine) { 185 return mLastLine - aOther.mLastLine; 186 } 187 188 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 final int line = aEvent.getLine(); 200 if (line < mFirstLine) { 201 return false; 202 } 203 if (line > mLastLine) { 204 return false; 205 } 206 final Matcher tagMatcher = 207 mTagCheckRegexp.matcher(aEvent.getSourceName()); 208 if (tagMatcher.find()) { 209 return true; 210 } 211 if (mTagMessageRegexp != null) { 212 final Matcher messageMatcher = 213 mTagMessageRegexp.matcher(aEvent.getMessage()); 214 return messageMatcher.find(); 215 } 216 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 final Matcher matcher = aRegexp.matcher(aComment); 232 // Match primarily for effect. 233 if (!matcher.find()) { 234 ///CLOVER:OFF 235 return aString; 236 ///CLOVER:ON 237 } 238 String result = aString; 239 for (int i = 0; i <= matcher.groupCount(); i++) { 240 // $n expands comment match like in Pattern.subst(). 241 result = result.replaceAll("\\$" + i, matcher.group(i)); 242 } 243 return result; 244 } 245 246 /** {@inheritDoc} */ 247 @Override 248 public final String toString() 249 { 250 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 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 private boolean mCheckC = true; 270 271 /** Whether to look for trigger in C++-style comments. */ 272 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 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 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 { 311 if (DEFAULT_COMMENT_FORMAT != null) { 312 setCommentFormat(DEFAULT_COMMENT_FORMAT); 313 } 314 if (DEFAULT_CHECK_FORMAT != null) { 315 setCheckFormat(DEFAULT_CHECK_FORMAT); 316 } 317 if (DEFAULT_MESSAGE_FORMAT != null) { 318 setMessageFormat(DEFAULT_MESSAGE_FORMAT); 319 } 320 if (DEFAULT_INFLUENCE_FORMAT != null) { 321 setInfluenceFormat(DEFAULT_INFLUENCE_FORMAT); 322 } 323 } 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 mCommentRegexp = Utils.getPattern(aFormat); 335 } 336 catch (final PatternSyntaxException e) { 337 throw new ConversionException("unable to parse " + aFormat, e); 338 } 339 } 340 341 /** @return the FileContents for this filter. */ 342 public FileContents getFileContents() 343 { 344 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 mFileContentsReference = new WeakReference<FileContents>(aFileContents); 354 } 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 mCheckRegexp = Utils.getPattern(aFormat); 366 mCheckFormat = aFormat; 367 } 368 catch (final PatternSyntaxException e) { 369 throw new ConversionException("unable to parse " + aFormat, e); 370 } 371 } 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 Utils.getPattern(aFormat); 384 } 385 catch (final PatternSyntaxException e) { 386 throw new ConversionException("unable to parse " + aFormat, e); 387 } 388 mMessageFormat = aFormat; 389 } 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 Utils.getPattern(aFormat); 402 } 403 catch (final PatternSyntaxException e) { 404 throw new ConversionException("unable to parse " + aFormat, e); 405 } 406 mInfluenceFormat = aFormat; 407 } 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 mCheckCPP = aCheckCPP; 417 } 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 mCheckC = aCheckC; 426 } 427 428 /** {@inheritDoc} */ 429 public boolean accept(AuditEvent aEvent) 430 { 431 if (aEvent.getLocalizedMessage() == null) { 432 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 final FileContents currentContents = FileContentsHolder.getContents(); 438 if (currentContents == null) { 439 // we have no contents, so we can not filter. 440 // TODO: perhaps we should notify user somehow? 441 return true; 442 } 443 if (getFileContents() != currentContents) { 444 setFileContents(currentContents); 445 tagSuppressions(); 446 } 447 for (final Iterator<Tag> iter = mTags.iterator(); iter.hasNext();) { 448 final Tag tag = iter.next(); 449 if (tag.isMatch(aEvent)) { 450 return false; 451 } 452 } 453 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 mTags.clear(); 463 final FileContents contents = getFileContents(); 464 if (mCheckCPP) { 465 tagSuppressions(contents.getCppComments().values()); 466 } 467 if (mCheckC) { 468 final Collection<List<TextBlock>> cComments = 469 contents.getCComments().values(); 470 for (final List<TextBlock> element : cComments) { 471 tagSuppressions(element); 472 } 473 } 474 Collections.sort(mTags); 475 } 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 for (final TextBlock comment : aComments) { 485 final int startLineNo = comment.getStartLineNo(); 486 final String[] text = comment.getText(); 487 tagCommentLine(text[0], startLineNo); 488 for (int i = 1; i < text.length; i++) { 489 tagCommentLine(text[i], startLineNo + i); 490 } 491 } 492 } 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 final Matcher matcher = mCommentRegexp.matcher(aText); 503 if (matcher.find()) { 504 addTag(matcher.group(0), aLine); 505 } 506 } 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 final Tag tag = new Tag(aText, aLine); 516 mTags.add(tag); 517 } 518}