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