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