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