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