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