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