001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2016 the original author or authors. 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//////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.filters; 021 022import java.lang.ref.WeakReference; 023import java.util.Collection; 024import java.util.Collections; 025import java.util.List; 026import java.util.Objects; 027import java.util.regex.Matcher; 028import java.util.regex.Pattern; 029import java.util.regex.PatternSyntaxException; 030 031import org.apache.commons.beanutils.ConversionException; 032 033import com.google.common.collect.Lists; 034import com.puppycrawl.tools.checkstyle.api.AuditEvent; 035import com.puppycrawl.tools.checkstyle.api.AutomaticBean; 036import com.puppycrawl.tools.checkstyle.api.FileContents; 037import com.puppycrawl.tools.checkstyle.api.Filter; 038import com.puppycrawl.tools.checkstyle.api.TextBlock; 039import com.puppycrawl.tools.checkstyle.checks.FileContentsHolder; 040import com.puppycrawl.tools.checkstyle.utils.CommonUtils; 041 042/** 043 * <p> 044 * A filter that uses nearby comments to suppress audit events. 045 * </p> 046 * 047 * <p>This check is philosophically similar to {@link SuppressionCommentFilter}. 048 * Unlike {@link SuppressionCommentFilter}, this filter does not require 049 * pairs of comments. This check may be used to suppress warnings in the 050 * current line: 051 * <pre> 052 * offendingLine(for, whatever, reason); // SUPPRESS ParameterNumberCheck 053 * </pre> 054 * or it may be configured to span multiple lines, either forward: 055 * <pre> 056 * // PERMIT MultipleVariableDeclarations NEXT 3 LINES 057 * double x1 = 1.0, y1 = 0.0, z1 = 0.0; 058 * double x2 = 0.0, y2 = 1.0, z2 = 0.0; 059 * double x3 = 0.0, y3 = 0.0, z3 = 1.0; 060 * </pre> 061 * or reverse: 062 * <pre> 063 * try { 064 * thirdPartyLibrary.method(); 065 * } catch (RuntimeException ex) { 066 * // ALLOW ILLEGAL CATCH BECAUSE third party API wraps everything 067 * // in RuntimeExceptions. 068 * ... 069 * } 070 * </pre> 071 * 072 * <p>See {@link SuppressionCommentFilter} for usage notes. 073 * 074 * @author Mick Killianey 075 */ 076public class SuppressWithNearbyCommentFilter 077 extends AutomaticBean 078 implements Filter { 079 080 /** Format to turns checkstyle reporting off. */ 081 private static final String DEFAULT_COMMENT_FORMAT = 082 "SUPPRESS CHECKSTYLE (\\w+)"; 083 084 /** Default regex for checks that should be suppressed. */ 085 private static final String DEFAULT_CHECK_FORMAT = ".*"; 086 087 /** Default regex for lines that should be suppressed. */ 088 private static final String DEFAULT_INFLUENCE_FORMAT = "0"; 089 090 /** Tagged comments. */ 091 private final List<Tag> tags = Lists.newArrayList(); 092 093 /** Whether to look for trigger in C-style comments. */ 094 private boolean checkC = true; 095 096 /** Whether to look for trigger in C++-style comments. */ 097 private boolean checkCPP = true; 098 099 /** Parsed comment regexp that marks checkstyle suppression region. */ 100 private Pattern commentRegexp; 101 102 /** The comment pattern that triggers suppression. */ 103 private String checkFormat; 104 105 /** The message format to suppress. */ 106 private String messageFormat; 107 108 /** The influence of the suppression comment. */ 109 private String influenceFormat; 110 111 /** 112 * References the current FileContents for this filter. 113 * Since this is a weak reference to the FileContents, the FileContents 114 * can be reclaimed as soon as the strong references in TreeWalker 115 * and FileContentsHolder are reassigned to the next FileContents, 116 * at which time filtering for the current FileContents is finished. 117 */ 118 private WeakReference<FileContents> fileContentsReference = new WeakReference<>(null); 119 120 /** 121 * Constructs a SuppressionCommentFilter. 122 * Initializes comment on, comment off, and check formats 123 * to defaults. 124 */ 125 public SuppressWithNearbyCommentFilter() { 126 setCommentFormat(DEFAULT_COMMENT_FORMAT); 127 checkFormat = DEFAULT_CHECK_FORMAT; 128 influenceFormat = DEFAULT_INFLUENCE_FORMAT; 129 } 130 131 /** 132 * Set the format for a comment that turns off reporting. 133 * @param format a {@code String} value. 134 * @throws ConversionException if unable to create Pattern object. 135 */ 136 public final void setCommentFormat(String format) { 137 commentRegexp = CommonUtils.createPattern(format); 138 } 139 140 /** 141 * @return the FileContents for this filter. 142 */ 143 public FileContents getFileContents() { 144 return fileContentsReference.get(); 145 } 146 147 /** 148 * Set the FileContents for this filter. 149 * @param fileContents the FileContents for this filter. 150 */ 151 public void setFileContents(FileContents fileContents) { 152 fileContentsReference = new WeakReference<>(fileContents); 153 } 154 155 /** 156 * Set the format for a check. 157 * @param format a {@code String} value 158 */ 159 public final void setCheckFormat(String format) { 160 checkFormat = format; 161 } 162 163 /** 164 * Set the format for a message. 165 * @param format a {@code String} value 166 */ 167 public void setMessageFormat(String format) { 168 messageFormat = format; 169 } 170 171 /** 172 * Set the format for the influence of this check. 173 * @param format a {@code String} value 174 */ 175 public final void setInfluenceFormat(String format) { 176 influenceFormat = format; 177 } 178 179 /** 180 * Set whether to look in C++ comments. 181 * @param checkCpp {@code true} if C++ comments are checked. 182 */ 183 public void setCheckCPP(boolean checkCpp) { 184 checkCPP = checkCpp; 185 } 186 187 /** 188 * Set whether to look in C comments. 189 * @param checkC {@code true} if C comments are checked. 190 */ 191 public void setCheckC(boolean checkC) { 192 this.checkC = checkC; 193 } 194 195 @Override 196 public boolean accept(AuditEvent event) { 197 boolean accepted = true; 198 199 if (event.getLocalizedMessage() != null) { 200 // Lazy update. If the first event for the current file, update file 201 // contents and tag suppressions 202 final FileContents currentContents = FileContentsHolder.getContents(); 203 204 if (currentContents != null) { 205 if (getFileContents() != currentContents) { 206 setFileContents(currentContents); 207 tagSuppressions(); 208 } 209 if (matchesTag(event)) { 210 accepted = false; 211 } 212 } 213 } 214 return accepted; 215 } 216 217 /** 218 * Whether current event matches any tag from {@link #tags}. 219 * @param event AuditEvent to test match on {@link #tags}. 220 * @return true if event matches any tag from {@link #tags}, false otherwise. 221 */ 222 private boolean matchesTag(AuditEvent event) { 223 for (final Tag tag : tags) { 224 if (tag.isMatch(event)) { 225 return true; 226 } 227 } 228 return false; 229 } 230 231 /** 232 * Collects all the suppression tags for all comments into a list and 233 * sorts the list. 234 */ 235 private void tagSuppressions() { 236 tags.clear(); 237 final FileContents contents = getFileContents(); 238 if (checkCPP) { 239 tagSuppressions(contents.getCppComments().values()); 240 } 241 if (checkC) { 242 final Collection<List<TextBlock>> cComments = 243 contents.getCComments().values(); 244 for (final List<TextBlock> element : cComments) { 245 tagSuppressions(element); 246 } 247 } 248 Collections.sort(tags); 249 } 250 251 /** 252 * Appends the suppressions in a collection of comments to the full 253 * set of suppression tags. 254 * @param comments the set of comments. 255 */ 256 private void tagSuppressions(Collection<TextBlock> comments) { 257 for (final TextBlock comment : comments) { 258 final int startLineNo = comment.getStartLineNo(); 259 final String[] text = comment.getText(); 260 tagCommentLine(text[0], startLineNo); 261 for (int i = 1; i < text.length; i++) { 262 tagCommentLine(text[i], startLineNo + i); 263 } 264 } 265 } 266 267 /** 268 * Tags a string if it matches the format for turning 269 * checkstyle reporting on or the format for turning reporting off. 270 * @param text the string to tag. 271 * @param line the line number of text. 272 */ 273 private void tagCommentLine(String text, int line) { 274 final Matcher matcher = commentRegexp.matcher(text); 275 if (matcher.find()) { 276 addTag(matcher.group(0), line); 277 } 278 } 279 280 /** 281 * Adds a comment suppression {@code Tag} to the list of all tags. 282 * @param text the text of the tag. 283 * @param line the line number of the tag. 284 */ 285 private void addTag(String text, int line) { 286 final Tag tag = new Tag(text, line, this); 287 tags.add(tag); 288 } 289 290 /** 291 * A Tag holds a suppression comment and its location. 292 */ 293 public static class Tag implements Comparable<Tag> { 294 /** The text of the tag. */ 295 private final String text; 296 297 /** The first line where warnings may be suppressed. */ 298 private final int firstLine; 299 300 /** The last line where warnings may be suppressed. */ 301 private final int lastLine; 302 303 /** The parsed check regexp, expanded for the text of this tag. */ 304 private final Pattern tagCheckRegexp; 305 306 /** The parsed message regexp, expanded for the text of this tag. */ 307 private final Pattern tagMessageRegexp; 308 309 /** 310 * Constructs a tag. 311 * @param text the text of the suppression. 312 * @param line the line number. 313 * @param filter the {@code SuppressWithNearbyCommentFilter} with the context 314 * @throws ConversionException if unable to parse expanded text. 315 */ 316 public Tag(String text, int line, SuppressWithNearbyCommentFilter filter) { 317 this.text = text; 318 319 //Expand regexp for check and message 320 //Does not intern Patterns with Utils.getPattern() 321 String format = ""; 322 try { 323 format = CommonUtils.fillTemplateWithStringsByRegexp( 324 filter.checkFormat, text, filter.commentRegexp); 325 tagCheckRegexp = Pattern.compile(format); 326 if (filter.messageFormat == null) { 327 tagMessageRegexp = null; 328 } 329 else { 330 format = CommonUtils.fillTemplateWithStringsByRegexp( 331 filter.messageFormat, text, filter.commentRegexp); 332 tagMessageRegexp = Pattern.compile(format); 333 } 334 format = CommonUtils.fillTemplateWithStringsByRegexp( 335 filter.influenceFormat, text, filter.commentRegexp); 336 final int influence; 337 try { 338 if (CommonUtils.startsWithChar(format, '+')) { 339 format = format.substring(1); 340 } 341 influence = Integer.parseInt(format); 342 } 343 catch (final NumberFormatException ex) { 344 throw new ConversionException( 345 "unable to parse influence from '" + text 346 + "' using " + filter.influenceFormat, ex); 347 } 348 if (influence >= 0) { 349 firstLine = line; 350 lastLine = line + influence; 351 } 352 else { 353 firstLine = line + influence; 354 lastLine = line; 355 } 356 } 357 catch (final PatternSyntaxException ex) { 358 throw new ConversionException( 359 "unable to parse expanded comment " + format, 360 ex); 361 } 362 } 363 364 /** 365 * Compares the position of this tag in the file 366 * with the position of another tag. 367 * @param other the tag to compare with this one. 368 * @return a negative number if this tag is before the other tag, 369 * 0 if they are at the same position, and a positive number if this 370 * tag is after the other tag. 371 */ 372 @Override 373 public int compareTo(Tag other) { 374 if (firstLine == other.firstLine) { 375 return Integer.compare(lastLine, other.lastLine); 376 } 377 378 return Integer.compare(firstLine, other.firstLine); 379 } 380 381 @Override 382 public boolean equals(Object other) { 383 if (this == other) { 384 return true; 385 } 386 if (other == null || getClass() != other.getClass()) { 387 return false; 388 } 389 final Tag tag = (Tag) other; 390 return Objects.equals(firstLine, tag.firstLine) 391 && Objects.equals(lastLine, tag.lastLine) 392 && Objects.equals(text, tag.text) 393 && Objects.equals(tagCheckRegexp, tag.tagCheckRegexp) 394 && Objects.equals(tagMessageRegexp, tag.tagMessageRegexp); 395 } 396 397 @Override 398 public int hashCode() { 399 return Objects.hash(text, firstLine, lastLine, tagCheckRegexp, tagMessageRegexp); 400 } 401 402 /** 403 * Determines whether the source of an audit event 404 * matches the text of this tag. 405 * @param event the {@code AuditEvent} to check. 406 * @return true if the source of event matches the text of this tag. 407 */ 408 public boolean isMatch(AuditEvent event) { 409 final int line = event.getLine(); 410 boolean match = false; 411 412 if (line >= firstLine && line <= lastLine) { 413 final Matcher tagMatcher = tagCheckRegexp.matcher(event.getSourceName()); 414 415 if (tagMatcher.find()) { 416 match = true; 417 } 418 else if (tagMessageRegexp == null) { 419 if (event.getModuleId() != null) { 420 final Matcher idMatcher = tagCheckRegexp.matcher(event.getModuleId()); 421 match = idMatcher.find(); 422 } 423 } 424 else { 425 final Matcher messageMatcher = tagMessageRegexp.matcher(event.getMessage()); 426 match = messageMatcher.find(); 427 } 428 } 429 return match; 430 } 431 432 @Override 433 public final String toString() { 434 return "Tag[lines=[" + firstLine + " to " + lastLine 435 + "]; text='" + text + "']"; 436 } 437 } 438}