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