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