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.io.File; 023import java.io.IOException; 024import java.nio.charset.StandardCharsets; 025import java.util.ArrayList; 026import java.util.Collection; 027import java.util.Objects; 028import java.util.Optional; 029import java.util.regex.Matcher; 030import java.util.regex.Pattern; 031import java.util.regex.PatternSyntaxException; 032 033import com.puppycrawl.tools.checkstyle.AbstractAutomaticBean; 034import com.puppycrawl.tools.checkstyle.PropertyType; 035import com.puppycrawl.tools.checkstyle.XdocsPropertyType; 036import com.puppycrawl.tools.checkstyle.api.AuditEvent; 037import com.puppycrawl.tools.checkstyle.api.FileText; 038import com.puppycrawl.tools.checkstyle.api.Filter; 039import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 040 041/** 042 * <div> 043 * Filter {@code SuppressWithPlainTextCommentFilter} uses plain text to suppress 044 * audit events. The filter can be used only to suppress audit events received 045 * from the checks which implement FileSetCheck interface. In other words, the 046 * checks which have Checker as a parent module. The filter knows nothing about 047 * AST, it treats only plain text comments and extracts the information required 048 * for suppression from the plain text comments. Currently, the filter supports 049 * only single-line comments. 050 * </div> 051 * 052 * <p> 053 * Please, be aware of the fact that, it is not recommended to use the filter 054 * for Java code anymore, however you still are able to use it to suppress audit 055 * events received from the checks which implement FileSetCheck interface. 056 * </p> 057 * 058 * <p> 059 * Rationale: Sometimes there are legitimate reasons for violating a check. 060 * When this is a matter of the code in question and not personal preference, 061 * the best place to override the policy is in the code itself. Semi-structured 062 * comments can be associated with the check. This is sometimes superior to 063 * a separate suppressions file, which must be kept up-to-date as the source 064 * file is edited. 065 * </p> 066 * 067 * <p> 068 * Note that the suppression comment should be put before the violation. 069 * You can use more than one suppression comment each on separate line. 070 * </p> 071 * 072 * <p> 073 * Properties {@code offCommentFormat} and {@code onCommentFormat} must have equal 074 * <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/regex/Matcher.html#groupCount()"> 075 * paren counts</a>. 076 * </p> 077 * 078 * <p> 079 * SuppressionWithPlainTextCommentFilter can suppress Checks that have Treewalker or 080 * Checker as parent module. 081 * </p> 082 * <ul> 083 * <li> 084 * Property {@code checkFormat} - Specify check pattern to suppress. 085 * Type is {@code java.util.regex.Pattern}. 086 * Default value is {@code ".*"}. 087 * </li> 088 * <li> 089 * Property {@code idFormat} - Specify check ID pattern to suppress. 090 * Type is {@code java.util.regex.Pattern}. 091 * Default value is {@code null}. 092 * </li> 093 * <li> 094 * Property {@code messageFormat} - Specify message pattern to suppress. 095 * Type is {@code java.util.regex.Pattern}. 096 * Default value is {@code null}. 097 * </li> 098 * <li> 099 * Property {@code offCommentFormat} - Specify comment pattern to trigger filter 100 * to begin suppression. 101 * Type is {@code java.util.regex.Pattern}. 102 * Default value is {@code "// CHECKSTYLE:OFF"}. 103 * </li> 104 * <li> 105 * Property {@code onCommentFormat} - Specify comment pattern to trigger filter 106 * to end suppression. 107 * Type is {@code java.util.regex.Pattern}. 108 * Default value is {@code "// CHECKSTYLE:ON"}. 109 * </li> 110 * </ul> 111 * 112 * <p> 113 * Parent is {@code com.puppycrawl.tools.checkstyle.Checker} 114 * </p> 115 * 116 * @since 8.6 117 */ 118public class SuppressWithPlainTextCommentFilter extends AbstractAutomaticBean implements Filter { 119 120 /** Comment format which turns checkstyle reporting off. */ 121 private static final String DEFAULT_OFF_FORMAT = "// CHECKSTYLE:OFF"; 122 123 /** Comment format which turns checkstyle reporting on. */ 124 private static final String DEFAULT_ON_FORMAT = "// CHECKSTYLE:ON"; 125 126 /** Default check format to suppress. By default, the filter suppress all checks. */ 127 private static final String DEFAULT_CHECK_FORMAT = ".*"; 128 129 /** List of suppressions from the file. By default, Its null. */ 130 private final Collection<Suppression> currentFileSuppressionCache = new ArrayList<>(); 131 132 /** File name that was suppressed. By default, Its empty. */ 133 private String currentFileName = ""; 134 135 /** Specify comment pattern to trigger filter to begin suppression. */ 136 private Pattern offCommentFormat = CommonUtil.createPattern(DEFAULT_OFF_FORMAT); 137 138 /** Specify comment pattern to trigger filter to end suppression. */ 139 private Pattern onCommentFormat = CommonUtil.createPattern(DEFAULT_ON_FORMAT); 140 141 /** Specify check pattern to suppress. */ 142 @XdocsPropertyType(PropertyType.PATTERN) 143 private String checkFormat = DEFAULT_CHECK_FORMAT; 144 145 /** Specify message pattern to suppress. */ 146 @XdocsPropertyType(PropertyType.PATTERN) 147 private String messageFormat; 148 149 /** Specify check ID pattern to suppress. */ 150 @XdocsPropertyType(PropertyType.PATTERN) 151 private String idFormat; 152 153 /** 154 * Setter to specify comment pattern to trigger filter to begin suppression. 155 * 156 * @param pattern off comment format pattern. 157 * @since 8.6 158 */ 159 public final void setOffCommentFormat(Pattern pattern) { 160 offCommentFormat = pattern; 161 } 162 163 /** 164 * Setter to specify comment pattern to trigger filter to end suppression. 165 * 166 * @param pattern on comment format pattern. 167 * @since 8.6 168 */ 169 public final void setOnCommentFormat(Pattern pattern) { 170 onCommentFormat = pattern; 171 } 172 173 /** 174 * Setter to specify check pattern to suppress. 175 * 176 * @param format pattern for check format. 177 * @since 8.6 178 */ 179 public final void setCheckFormat(String format) { 180 checkFormat = format; 181 } 182 183 /** 184 * Setter to specify message pattern to suppress. 185 * 186 * @param format pattern for message format. 187 * @since 8.6 188 */ 189 public final void setMessageFormat(String format) { 190 messageFormat = format; 191 } 192 193 /** 194 * Setter to specify check ID pattern to suppress. 195 * 196 * @param format pattern for check ID format 197 * @since 8.24 198 */ 199 public final void setIdFormat(String format) { 200 idFormat = format; 201 } 202 203 @Override 204 public boolean accept(AuditEvent event) { 205 boolean accepted = true; 206 if (event.getViolation() != null) { 207 final String eventFileName = event.getFileName(); 208 209 if (!currentFileName.equals(eventFileName)) { 210 currentFileName = eventFileName; 211 final FileText fileText = getFileText(eventFileName); 212 currentFileSuppressionCache.clear(); 213 if (fileText != null) { 214 cacheSuppressions(fileText); 215 } 216 } 217 218 accepted = getNearestSuppression(currentFileSuppressionCache, event) == null; 219 } 220 return accepted; 221 } 222 223 @Override 224 protected void finishLocalSetup() { 225 // No code by default 226 } 227 228 /** 229 * Caches {@link FileText} instance created based on the given file name. 230 * 231 * @param fileName the name of the file. 232 * @return {@link FileText} instance. 233 * @throws IllegalStateException if the file could not be read. 234 */ 235 private static FileText getFileText(String fileName) { 236 final File file = new File(fileName); 237 FileText result = null; 238 239 // some violations can be on a directory, instead of a file 240 if (!file.isDirectory()) { 241 try { 242 result = new FileText(file, StandardCharsets.UTF_8.name()); 243 } 244 catch (IOException exc) { 245 throw new IllegalStateException("Cannot read source file: " + fileName, exc); 246 } 247 } 248 249 return result; 250 } 251 252 /** 253 * Collects the list of {@link Suppression} instances retrieved from the given {@link FileText}. 254 * 255 * @param fileText {@link FileText} instance. 256 */ 257 private void cacheSuppressions(FileText fileText) { 258 for (int lineNo = 0; lineNo < fileText.size(); lineNo++) { 259 final Optional<Suppression> suppression = getSuppression(fileText, lineNo); 260 suppression.ifPresent(currentFileSuppressionCache::add); 261 } 262 } 263 264 /** 265 * Tries to extract the suppression from the given line. 266 * 267 * @param fileText {@link FileText} instance. 268 * @param lineNo line number. 269 * @return {@link Optional} of {@link Suppression}. 270 */ 271 private Optional<Suppression> getSuppression(FileText fileText, int lineNo) { 272 final String line = fileText.get(lineNo); 273 final Matcher onCommentMatcher = onCommentFormat.matcher(line); 274 final Matcher offCommentMatcher = offCommentFormat.matcher(line); 275 276 Suppression suppression = null; 277 if (onCommentMatcher.find()) { 278 suppression = new Suppression(onCommentMatcher.group(0), 279 lineNo + 1, SuppressionType.ON, this); 280 } 281 if (offCommentMatcher.find()) { 282 suppression = new Suppression(offCommentMatcher.group(0), 283 lineNo + 1, SuppressionType.OFF, this); 284 } 285 286 return Optional.ofNullable(suppression); 287 } 288 289 /** 290 * Finds the nearest {@link Suppression} instance which can suppress 291 * the given {@link AuditEvent}. The nearest suppression is the suppression which scope 292 * is before the line and column of the event. 293 * 294 * @param suppressions collection of {@link Suppression} instances. 295 * @param event {@link AuditEvent} instance. 296 * @return {@link Suppression} instance. 297 */ 298 private static Suppression getNearestSuppression(Collection<Suppression> suppressions, 299 AuditEvent event) { 300 return suppressions 301 .stream() 302 .filter(suppression -> suppression.isMatch(event)) 303 .reduce((first, second) -> second) 304 .filter(suppression -> suppression.suppressionType != SuppressionType.ON) 305 .orElse(null); 306 } 307 308 /** Enum which represents the type of the suppression. */ 309 private enum SuppressionType { 310 311 /** On suppression type. */ 312 ON, 313 /** Off suppression type. */ 314 OFF, 315 316 } 317 318 /** The class which represents the suppression. */ 319 private static final class Suppression { 320 321 /** The regexp which is used to match the event source.*/ 322 private final Pattern eventSourceRegexp; 323 /** The regexp which is used to match the event message.*/ 324 private final Pattern eventMessageRegexp; 325 /** The regexp which is used to match the event ID.*/ 326 private final Pattern eventIdRegexp; 327 328 /** Suppression line.*/ 329 private final int lineNo; 330 331 /** Suppression type. */ 332 private final SuppressionType suppressionType; 333 334 /** 335 * Creates new suppression instance. 336 * 337 * @param text suppression text. 338 * @param lineNo suppression line number. 339 * @param suppressionType suppression type. 340 * @param filter the {@link SuppressWithPlainTextCommentFilter} with the context. 341 * @throws IllegalArgumentException if there is an error in the filter regex syntax. 342 */ 343 private Suppression( 344 String text, 345 int lineNo, 346 SuppressionType suppressionType, 347 SuppressWithPlainTextCommentFilter filter 348 ) { 349 this.lineNo = lineNo; 350 this.suppressionType = suppressionType; 351 352 final Pattern commentFormat; 353 if (this.suppressionType == SuppressionType.ON) { 354 commentFormat = filter.onCommentFormat; 355 } 356 else { 357 commentFormat = filter.offCommentFormat; 358 } 359 360 // Expand regexp for check and message 361 // Does not intern Patterns with Utils.getPattern() 362 String format = ""; 363 try { 364 format = CommonUtil.fillTemplateWithStringsByRegexp( 365 filter.checkFormat, text, commentFormat); 366 eventSourceRegexp = Pattern.compile(format); 367 if (filter.messageFormat == null) { 368 eventMessageRegexp = null; 369 } 370 else { 371 format = CommonUtil.fillTemplateWithStringsByRegexp( 372 filter.messageFormat, text, commentFormat); 373 eventMessageRegexp = Pattern.compile(format); 374 } 375 if (filter.idFormat == null) { 376 eventIdRegexp = null; 377 } 378 else { 379 format = CommonUtil.fillTemplateWithStringsByRegexp( 380 filter.idFormat, text, commentFormat); 381 eventIdRegexp = Pattern.compile(format); 382 } 383 } 384 catch (final PatternSyntaxException exc) { 385 throw new IllegalArgumentException( 386 "unable to parse expanded comment " + format, exc); 387 } 388 } 389 390 /** 391 * Indicates whether some other object is "equal to" this one. 392 * 393 * @noinspection EqualsCalledOnEnumConstant 394 * @noinspectionreason EqualsCalledOnEnumConstant - enumeration is needed to keep 395 * code consistent 396 */ 397 @Override 398 public boolean equals(Object other) { 399 if (this == other) { 400 return true; 401 } 402 if (other == null || getClass() != other.getClass()) { 403 return false; 404 } 405 final Suppression suppression = (Suppression) other; 406 return Objects.equals(lineNo, suppression.lineNo) 407 && Objects.equals(suppressionType, suppression.suppressionType) 408 && Objects.equals(eventSourceRegexp, suppression.eventSourceRegexp) 409 && Objects.equals(eventMessageRegexp, suppression.eventMessageRegexp) 410 && Objects.equals(eventIdRegexp, suppression.eventIdRegexp); 411 } 412 413 @Override 414 public int hashCode() { 415 return Objects.hash( 416 lineNo, suppressionType, eventSourceRegexp, eventMessageRegexp, 417 eventIdRegexp); 418 } 419 420 /** 421 * Checks whether the suppression matches the given {@link AuditEvent}. 422 * 423 * @param event {@link AuditEvent} instance. 424 * @return true if the suppression matches {@link AuditEvent}. 425 */ 426 private boolean isMatch(AuditEvent event) { 427 return isInScopeOfSuppression(event) 428 && isCheckMatch(event) 429 && isIdMatch(event) 430 && isMessageMatch(event); 431 } 432 433 /** 434 * Checks whether {@link AuditEvent} is in the scope of the suppression. 435 * 436 * @param event {@link AuditEvent} instance. 437 * @return true if {@link AuditEvent} is in the scope of the suppression. 438 */ 439 private boolean isInScopeOfSuppression(AuditEvent event) { 440 return lineNo <= event.getLine(); 441 } 442 443 /** 444 * Checks whether {@link AuditEvent} source name matches the check format. 445 * 446 * @param event {@link AuditEvent} instance. 447 * @return true if the {@link AuditEvent} source name matches the check format. 448 */ 449 private boolean isCheckMatch(AuditEvent event) { 450 final Matcher checkMatcher = eventSourceRegexp.matcher(event.getSourceName()); 451 return checkMatcher.find(); 452 } 453 454 /** 455 * Checks whether the {@link AuditEvent} module ID matches the ID format. 456 * 457 * @param event {@link AuditEvent} instance. 458 * @return true if the {@link AuditEvent} module ID matches the ID format. 459 */ 460 private boolean isIdMatch(AuditEvent event) { 461 boolean match = true; 462 if (eventIdRegexp != null) { 463 if (event.getModuleId() == null) { 464 match = false; 465 } 466 else { 467 final Matcher idMatcher = eventIdRegexp.matcher(event.getModuleId()); 468 match = idMatcher.find(); 469 } 470 } 471 return match; 472 } 473 474 /** 475 * Checks whether the {@link AuditEvent} message matches the message format. 476 * 477 * @param event {@link AuditEvent} instance. 478 * @return true if the {@link AuditEvent} message matches the message format. 479 */ 480 private boolean isMessageMatch(AuditEvent event) { 481 boolean match = true; 482 if (eventMessageRegexp != null) { 483 final Matcher messageMatcher = eventMessageRegexp.matcher(event.getMessage()); 484 match = messageMatcher.find(); 485 } 486 return match; 487 } 488 } 489 490}