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