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