001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2024 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.List; 026import java.util.Objects; 027import java.util.regex.Matcher; 028import java.util.regex.Pattern; 029import java.util.regex.PatternSyntaxException; 030 031import com.puppycrawl.tools.checkstyle.AbstractAutomaticBean; 032import com.puppycrawl.tools.checkstyle.PropertyType; 033import com.puppycrawl.tools.checkstyle.TreeWalkerAuditEvent; 034import com.puppycrawl.tools.checkstyle.TreeWalkerFilter; 035import com.puppycrawl.tools.checkstyle.XdocsPropertyType; 036import com.puppycrawl.tools.checkstyle.api.FileContents; 037import com.puppycrawl.tools.checkstyle.api.TextBlock; 038import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 039 040/** 041 * <div> 042 * Filter {@code SuppressWithNearbyCommentFilter} uses nearby comments to suppress audit events. 043 * </div> 044 * 045 * <p> 046 * Rationale: Same as {@code SuppressionCommentFilter}. 047 * Whereas the SuppressionCommentFilter uses matched pairs of filters to turn 048 * on/off comment matching, {@code SuppressWithNearbyCommentFilter} uses single comments. 049 * This requires fewer lines to mark a region, and may be aesthetically preferable in some contexts. 050 * </p> 051 * 052 * <p> 053 * Attention: This filter may only be specified within the TreeWalker module 054 * ({@code <module name="TreeWalker"/>}) and only applies to checks which are also 055 * defined within this module. To filter non-TreeWalker checks like {@code RegexpSingleline}, 056 * a 057 * <a href="https://checkstyle.org/filters/suppresswithplaintextcommentfilter.html#SuppressWithPlainTextCommentFilter"> 058 * SuppressWithPlainTextCommentFilter</a> or similar filter must be used. 059 * </p> 060 * 061 * <p> 062 * SuppressWithNearbyCommentFilter can suppress Checks that have 063 * Treewalker as parent module. 064 * </p> 065 * <ul> 066 * <li> 067 * Property {@code checkC} - Control whether to check C style comments ({@code /* ... */}). 068 * Type is {@code boolean}. 069 * Default value is {@code true}. 070 * </li> 071 * <li> 072 * Property {@code checkCPP} - Control whether to check C++ style comments ({@code //}). 073 * Type is {@code boolean}. 074 * Default value is {@code true}. 075 * </li> 076 * <li> 077 * Property {@code checkFormat} - Specify check pattern to suppress. 078 * Type is {@code java.util.regex.Pattern}. 079 * Default value is {@code ".*"}. 080 * </li> 081 * <li> 082 * Property {@code commentFormat} - Specify comment pattern to trigger filter to begin suppression. 083 * Type is {@code java.util.regex.Pattern}. 084 * Default value is {@code "SUPPRESS CHECKSTYLE (\w+)"}. 085 * </li> 086 * <li> 087 * Property {@code idFormat} - Specify check ID pattern to suppress. 088 * Type is {@code java.util.regex.Pattern}. 089 * Default value is {@code null}. 090 * </li> 091 * <li> 092 * Property {@code influenceFormat} - Specify negative/zero/positive value that 093 * defines the number of lines preceding/at/following the suppression comment. 094 * Type is {@code java.lang.String}. 095 * Default value is {@code "0"}. 096 * </li> 097 * <li> 098 * Property {@code messageFormat} - Define message pattern to suppress. 099 * Type is {@code java.util.regex.Pattern}. 100 * Default value is {@code null}. 101 * </li> 102 * </ul> 103 * 104 * <p> 105 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 106 * </p> 107 * 108 * @since 5.0 109 */ 110public class SuppressWithNearbyCommentFilter 111 extends AbstractAutomaticBean 112 implements TreeWalkerFilter { 113 114 /** Format to turn checkstyle reporting off. */ 115 private static final String DEFAULT_COMMENT_FORMAT = 116 "SUPPRESS CHECKSTYLE (\\w+)"; 117 118 /** Default regex for checks that should be suppressed. */ 119 private static final String DEFAULT_CHECK_FORMAT = ".*"; 120 121 /** Default regex for lines that should be suppressed. */ 122 private static final String DEFAULT_INFLUENCE_FORMAT = "0"; 123 124 /** Tagged comments. */ 125 private final List<Tag> tags = new ArrayList<>(); 126 127 /** Control whether to check C style comments ({@code /* ... */}). */ 128 private boolean checkC = true; 129 130 /** Control whether to check C++ style comments ({@code //}). */ 131 // -@cs[AbbreviationAsWordInName] We can not change it as, 132 // check's property is a part of API (used in configurations). 133 private boolean checkCPP = true; 134 135 /** Specify comment pattern to trigger filter to begin suppression. */ 136 private Pattern commentFormat = Pattern.compile(DEFAULT_COMMENT_FORMAT); 137 138 /** Specify check pattern to suppress. */ 139 @XdocsPropertyType(PropertyType.PATTERN) 140 private String checkFormat = DEFAULT_CHECK_FORMAT; 141 142 /** Define message pattern to suppress. */ 143 @XdocsPropertyType(PropertyType.PATTERN) 144 private String messageFormat; 145 146 /** Specify check ID pattern to suppress. */ 147 @XdocsPropertyType(PropertyType.PATTERN) 148 private String idFormat; 149 150 /** 151 * Specify negative/zero/positive value that defines the number of lines 152 * preceding/at/following the suppression comment. 153 */ 154 private String influenceFormat = DEFAULT_INFLUENCE_FORMAT; 155 156 /** 157 * References the current FileContents for this filter. 158 * Since this is a weak reference to the FileContents, the FileContents 159 * can be reclaimed as soon as the strong references in TreeWalker 160 * are reassigned to the next FileContents, at which time filtering for 161 * the current FileContents is finished. 162 */ 163 private WeakReference<FileContents> fileContentsReference = new WeakReference<>(null); 164 165 /** 166 * Setter to specify comment pattern to trigger filter to begin suppression. 167 * 168 * @param pattern a pattern. 169 * @since 5.0 170 */ 171 public final void setCommentFormat(Pattern pattern) { 172 commentFormat = pattern; 173 } 174 175 /** 176 * Returns FileContents for this filter. 177 * 178 * @return the FileContents for this filter. 179 */ 180 private FileContents getFileContents() { 181 return fileContentsReference.get(); 182 } 183 184 /** 185 * Set the FileContents for this filter. 186 * 187 * @param fileContents the FileContents for this filter. 188 */ 189 private void setFileContents(FileContents fileContents) { 190 fileContentsReference = new WeakReference<>(fileContents); 191 } 192 193 /** 194 * Setter to specify check pattern to suppress. 195 * 196 * @param format a {@code String} value 197 * @since 5.0 198 */ 199 public final void setCheckFormat(String format) { 200 checkFormat = format; 201 } 202 203 /** 204 * Setter to define message pattern to suppress. 205 * 206 * @param format a {@code String} value 207 * @since 5.0 208 */ 209 public void setMessageFormat(String format) { 210 messageFormat = format; 211 } 212 213 /** 214 * Setter to specify check ID pattern to suppress. 215 * 216 * @param format a {@code String} value 217 * @since 8.24 218 */ 219 public void setIdFormat(String format) { 220 idFormat = format; 221 } 222 223 /** 224 * Setter to specify negative/zero/positive value that defines the number 225 * of lines preceding/at/following the suppression comment. 226 * 227 * @param format a {@code String} value 228 * @since 5.0 229 */ 230 public final void setInfluenceFormat(String format) { 231 influenceFormat = format; 232 } 233 234 /** 235 * Setter to control whether to check C++ style comments ({@code //}). 236 * 237 * @param checkCpp {@code true} if C++ comments are checked. 238 * @since 5.0 239 */ 240 // -@cs[AbbreviationAsWordInName] We can not change it as, 241 // check's property is a part of API (used in configurations). 242 public void setCheckCPP(boolean checkCpp) { 243 checkCPP = checkCpp; 244 } 245 246 /** 247 * Setter to control whether to check C style comments ({@code /* ... */}). 248 * 249 * @param checkC {@code true} if C comments are checked. 250 * @since 5.0 251 */ 252 public void setCheckC(boolean checkC) { 253 this.checkC = checkC; 254 } 255 256 @Override 257 protected void finishLocalSetup() { 258 // No code by default 259 } 260 261 @Override 262 public boolean accept(TreeWalkerAuditEvent event) { 263 boolean accepted = true; 264 265 if (event.getViolation() != null) { 266 // Lazy update. If the first event for the current file, update file 267 // contents and tag suppressions 268 final FileContents currentContents = event.getFileContents(); 269 270 if (getFileContents() != currentContents) { 271 setFileContents(currentContents); 272 tagSuppressions(); 273 } 274 if (matchesTag(event)) { 275 accepted = false; 276 } 277 } 278 return accepted; 279 } 280 281 /** 282 * Whether current event matches any tag from {@link #tags}. 283 * 284 * @param event TreeWalkerAuditEvent to test match on {@link #tags}. 285 * @return true if event matches any tag from {@link #tags}, false otherwise. 286 */ 287 private boolean matchesTag(TreeWalkerAuditEvent event) { 288 boolean result = false; 289 for (final Tag tag : tags) { 290 if (tag.isMatch(event)) { 291 result = true; 292 break; 293 } 294 } 295 return result; 296 } 297 298 /** 299 * Collects all the suppression tags for all comments into a list and 300 * sorts the list. 301 */ 302 private void tagSuppressions() { 303 tags.clear(); 304 final FileContents contents = getFileContents(); 305 if (checkCPP) { 306 tagSuppressions(contents.getSingleLineComments().values()); 307 } 308 if (checkC) { 309 final Collection<List<TextBlock>> cComments = 310 contents.getBlockComments().values(); 311 cComments.forEach(this::tagSuppressions); 312 } 313 } 314 315 /** 316 * Appends the suppressions in a collection of comments to the full 317 * set of suppression tags. 318 * 319 * @param comments the set of comments. 320 */ 321 private void tagSuppressions(Collection<TextBlock> comments) { 322 for (final TextBlock comment : comments) { 323 final int startLineNo = comment.getStartLineNo(); 324 final String[] text = comment.getText(); 325 tagCommentLine(text[0], startLineNo); 326 for (int i = 1; i < text.length; i++) { 327 tagCommentLine(text[i], startLineNo + i); 328 } 329 } 330 } 331 332 /** 333 * Tags a string if it matches the format for turning 334 * checkstyle reporting on or the format for turning reporting off. 335 * 336 * @param text the string to tag. 337 * @param line the line number of text. 338 */ 339 private void tagCommentLine(String text, int line) { 340 final Matcher matcher = commentFormat.matcher(text); 341 if (matcher.find()) { 342 addTag(matcher.group(0), line); 343 } 344 } 345 346 /** 347 * Adds a comment suppression {@code Tag} to the list of all tags. 348 * 349 * @param text the text of the tag. 350 * @param line the line number of the tag. 351 */ 352 private void addTag(String text, int line) { 353 final Tag tag = new Tag(text, line, this); 354 tags.add(tag); 355 } 356 357 /** 358 * A Tag holds a suppression comment and its location. 359 */ 360 private static final class Tag { 361 362 /** The text of the tag. */ 363 private final String text; 364 365 /** The first line where warnings may be suppressed. */ 366 private final int firstLine; 367 368 /** The last line where warnings may be suppressed. */ 369 private final int lastLine; 370 371 /** The parsed check regexp, expanded for the text of this tag. */ 372 private final Pattern tagCheckRegexp; 373 374 /** The parsed message regexp, expanded for the text of this tag. */ 375 private final Pattern tagMessageRegexp; 376 377 /** The parsed check ID regexp, expanded for the text of this tag. */ 378 private final Pattern tagIdRegexp; 379 380 /** 381 * Constructs a tag. 382 * 383 * @param text the text of the suppression. 384 * @param line the line number. 385 * @param filter the {@code SuppressWithNearbyCommentFilter} with the context 386 * @throws IllegalArgumentException if unable to parse expanded text. 387 */ 388 private Tag(String text, int line, SuppressWithNearbyCommentFilter filter) { 389 this.text = text; 390 391 // Expand regexp for check and message 392 // Does not intern Patterns with Utils.getPattern() 393 String format = ""; 394 try { 395 format = CommonUtil.fillTemplateWithStringsByRegexp( 396 filter.checkFormat, text, filter.commentFormat); 397 tagCheckRegexp = Pattern.compile(format); 398 if (filter.messageFormat == null) { 399 tagMessageRegexp = null; 400 } 401 else { 402 format = CommonUtil.fillTemplateWithStringsByRegexp( 403 filter.messageFormat, text, filter.commentFormat); 404 tagMessageRegexp = Pattern.compile(format); 405 } 406 if (filter.idFormat == null) { 407 tagIdRegexp = null; 408 } 409 else { 410 format = CommonUtil.fillTemplateWithStringsByRegexp( 411 filter.idFormat, text, filter.commentFormat); 412 tagIdRegexp = Pattern.compile(format); 413 } 414 format = CommonUtil.fillTemplateWithStringsByRegexp( 415 filter.influenceFormat, text, filter.commentFormat); 416 417 final int influence = parseInfluence(format, filter.influenceFormat, text); 418 419 if (influence >= 1) { 420 firstLine = line; 421 lastLine = line + influence; 422 } 423 else { 424 firstLine = line + influence; 425 lastLine = line; 426 } 427 } 428 catch (final PatternSyntaxException ex) { 429 throw new IllegalArgumentException( 430 "unable to parse expanded comment " + format, ex); 431 } 432 } 433 434 /** 435 * Gets influence from suppress filter influence format param. 436 * 437 * @param format influence format to parse 438 * @param influenceFormat raw influence format 439 * @param text text of the suppression 440 * @return parsed influence 441 * @throws IllegalArgumentException when unable to parse int in format 442 */ 443 private static int parseInfluence(String format, String influenceFormat, String text) { 444 try { 445 return Integer.parseInt(format); 446 } 447 catch (final NumberFormatException ex) { 448 throw new IllegalArgumentException("unable to parse influence from '" + text 449 + "' using " + influenceFormat, ex); 450 } 451 } 452 453 @Override 454 public boolean equals(Object other) { 455 if (this == other) { 456 return true; 457 } 458 if (other == null || getClass() != other.getClass()) { 459 return false; 460 } 461 final Tag tag = (Tag) other; 462 return Objects.equals(firstLine, tag.firstLine) 463 && Objects.equals(lastLine, tag.lastLine) 464 && Objects.equals(text, tag.text) 465 && Objects.equals(tagCheckRegexp, tag.tagCheckRegexp) 466 && Objects.equals(tagMessageRegexp, tag.tagMessageRegexp) 467 && Objects.equals(tagIdRegexp, tag.tagIdRegexp); 468 } 469 470 @Override 471 public int hashCode() { 472 return Objects.hash(text, firstLine, lastLine, tagCheckRegexp, tagMessageRegexp, 473 tagIdRegexp); 474 } 475 476 /** 477 * Determines whether the source of an audit event 478 * matches the text of this tag. 479 * 480 * @param event the {@code TreeWalkerAuditEvent} to check. 481 * @return true if the source of event matches the text of this tag. 482 */ 483 public boolean isMatch(TreeWalkerAuditEvent event) { 484 return isInScopeOfSuppression(event) 485 && isCheckMatch(event) 486 && isIdMatch(event) 487 && isMessageMatch(event); 488 } 489 490 /** 491 * Checks whether the {@link TreeWalkerAuditEvent} is in the scope of the suppression. 492 * 493 * @param event {@link TreeWalkerAuditEvent} instance. 494 * @return true if the {@link TreeWalkerAuditEvent} is in the scope of the suppression. 495 */ 496 private boolean isInScopeOfSuppression(TreeWalkerAuditEvent event) { 497 final int line = event.getLine(); 498 return line >= firstLine && line <= lastLine; 499 } 500 501 /** 502 * Checks whether {@link TreeWalkerAuditEvent} source name matches the check format. 503 * 504 * @param event {@link TreeWalkerAuditEvent} instance. 505 * @return true if the {@link TreeWalkerAuditEvent} source name matches the check format. 506 */ 507 private boolean isCheckMatch(TreeWalkerAuditEvent event) { 508 final Matcher checkMatcher = tagCheckRegexp.matcher(event.getSourceName()); 509 return checkMatcher.find(); 510 } 511 512 /** 513 * Checks whether the {@link TreeWalkerAuditEvent} module ID matches the ID format. 514 * 515 * @param event {@link TreeWalkerAuditEvent} instance. 516 * @return true if the {@link TreeWalkerAuditEvent} module ID matches the ID format. 517 */ 518 private boolean isIdMatch(TreeWalkerAuditEvent event) { 519 boolean match = true; 520 if (tagIdRegexp != null) { 521 if (event.getModuleId() == null) { 522 match = false; 523 } 524 else { 525 final Matcher idMatcher = tagIdRegexp.matcher(event.getModuleId()); 526 match = idMatcher.find(); 527 } 528 } 529 return match; 530 } 531 532 /** 533 * Checks whether the {@link TreeWalkerAuditEvent} message matches the message format. 534 * 535 * @param event {@link TreeWalkerAuditEvent} instance. 536 * @return true if the {@link TreeWalkerAuditEvent} message matches the message format. 537 */ 538 private boolean isMessageMatch(TreeWalkerAuditEvent event) { 539 boolean match = true; 540 if (tagMessageRegexp != null) { 541 final Matcher messageMatcher = tagMessageRegexp.matcher(event.getMessage()); 542 match = messageMatcher.find(); 543 } 544 return match; 545 } 546 547 @Override 548 public String toString() { 549 return "Tag[text='" + text + '\'' 550 + ", firstLine=" + firstLine 551 + ", lastLine=" + lastLine 552 + ", tagCheckRegexp=" + tagCheckRegexp 553 + ", tagMessageRegexp=" + tagMessageRegexp 554 + ", tagIdRegexp=" + tagIdRegexp 555 + ']'; 556 } 557 558 } 559 560}