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 * Setter to specify comment pattern to trigger filter to begin suppression. 120 * 121 * @param pattern off comment format pattern. 122 * @since 8.6 123 */ 124 public final void setOffCommentFormat(Pattern pattern) { 125 offCommentFormat = pattern; 126 } 127 128 /** 129 * Setter to specify comment pattern to trigger filter to end suppression. 130 * 131 * @param pattern on comment format pattern. 132 * @since 8.6 133 */ 134 public final void setOnCommentFormat(Pattern pattern) { 135 onCommentFormat = pattern; 136 } 137 138 /** 139 * Setter to specify check pattern to suppress. 140 * The pattern is matched against the fully qualified class name of the Check. 141 * 142 * @param format pattern for check format. 143 * @since 8.6 144 */ 145 public final void setCheckFormat(String format) { 146 checkFormat = format; 147 } 148 149 /** 150 * Setter to specify message pattern to suppress. 151 * 152 * @param format pattern for message format. 153 * @since 8.6 154 */ 155 public final void setMessageFormat(String format) { 156 messageFormat = format; 157 } 158 159 /** 160 * Setter to specify check ID pattern to suppress. 161 * 162 * @param format pattern for check ID format 163 * @since 8.24 164 */ 165 public final void setIdFormat(String format) { 166 idFormat = format; 167 } 168 169 @Override 170 public boolean accept(AuditEvent event) { 171 boolean accepted = true; 172 if (event.getViolation() != null) { 173 final String eventFileName = event.getFileName(); 174 175 if (!currentFileName.equals(eventFileName)) { 176 currentFileName = eventFileName; 177 final FileText fileText = getFileText(eventFileName); 178 currentFileSuppressionCache.clear(); 179 if (fileText != null) { 180 cacheSuppressions(fileText); 181 } 182 } 183 184 accepted = getNearestSuppression(currentFileSuppressionCache, event) == null; 185 } 186 return accepted; 187 } 188 189 @Override 190 protected void finishLocalSetup() { 191 // No code by default 192 } 193 194 /** 195 * Caches {@link FileText} instance created based on the given file name. 196 * 197 * @param fileName the name of the file. 198 * @return {@link FileText} instance. 199 * @throws IllegalStateException if the file could not be read. 200 */ 201 private static FileText getFileText(String fileName) { 202 final Path path = Path.of(fileName); 203 FileText result = null; 204 205 // some violations can be on a directory, instead of a file 206 if (!Files.isDirectory(path)) { 207 try { 208 result = new FileText(path.toFile(), StandardCharsets.UTF_8.name()); 209 } 210 catch (IOException exc) { 211 throw new IllegalStateException("Cannot read source file: " + fileName, exc); 212 } 213 } 214 215 return result; 216 } 217 218 /** 219 * Collects the list of {@link Suppression} instances retrieved from the given {@link FileText}. 220 * 221 * @param fileText {@link FileText} instance. 222 */ 223 private void cacheSuppressions(FileText fileText) { 224 for (int lineNo = 0; lineNo < fileText.size(); lineNo++) { 225 final Optional<Suppression> suppression = getSuppression(fileText, lineNo); 226 suppression.ifPresent(currentFileSuppressionCache::add); 227 } 228 } 229 230 /** 231 * Tries to extract the suppression from the given line. 232 * 233 * @param fileText {@link FileText} instance. 234 * @param lineNo line number. 235 * @return {@link Optional} of {@link Suppression}. 236 */ 237 private Optional<Suppression> getSuppression(FileText fileText, int lineNo) { 238 final String line = fileText.get(lineNo); 239 final Matcher onCommentMatcher = onCommentFormat.matcher(line); 240 final Matcher offCommentMatcher = offCommentFormat.matcher(line); 241 242 Suppression suppression = null; 243 if (onCommentMatcher.find()) { 244 suppression = new Suppression(onCommentMatcher.group(0), 245 lineNo + 1, SuppressionType.ON, this); 246 } 247 if (offCommentMatcher.find()) { 248 suppression = new Suppression(offCommentMatcher.group(0), 249 lineNo + 1, SuppressionType.OFF, this); 250 } 251 252 return Optional.ofNullable(suppression); 253 } 254 255 /** 256 * Finds the nearest {@link Suppression} instance which can suppress 257 * the given {@link AuditEvent}. The nearest suppression is the suppression which scope 258 * is before the line and column of the event. 259 * 260 * @param suppressions collection of {@link Suppression} instances. 261 * @param event {@link AuditEvent} instance. 262 * @return {@link Suppression} instance. 263 */ 264 private static Suppression getNearestSuppression(Collection<Suppression> suppressions, 265 AuditEvent event) { 266 return suppressions 267 .stream() 268 .filter(suppression -> suppression.isMatch(event)) 269 .reduce((first, second) -> second) 270 .filter(suppression -> suppression.suppressionType != SuppressionType.ON) 271 .orElse(null); 272 } 273 274 /** Enum which represents the type of the suppression. */ 275 private enum SuppressionType { 276 277 /** On suppression type. */ 278 ON, 279 /** Off suppression type. */ 280 OFF, 281 282 } 283 284 /** The class which represents the suppression. */ 285 private static final class Suppression { 286 287 /** The regexp which is used to match the event source.*/ 288 private final Pattern eventSourceRegexp; 289 /** The regexp which is used to match the event message.*/ 290 private final Pattern eventMessageRegexp; 291 /** The regexp which is used to match the event ID.*/ 292 private final Pattern eventIdRegexp; 293 294 /** Suppression line.*/ 295 private final int lineNo; 296 297 /** Suppression type. */ 298 private final SuppressionType suppressionType; 299 300 /** 301 * Creates new suppression instance. 302 * 303 * @param text suppression text. 304 * @param lineNo suppression line number. 305 * @param suppressionType suppression type. 306 * @param filter the {@link SuppressWithPlainTextCommentFilter} with the context. 307 * @throws IllegalArgumentException if there is an error in the filter regex syntax. 308 */ 309 private Suppression( 310 String text, 311 int lineNo, 312 SuppressionType suppressionType, 313 SuppressWithPlainTextCommentFilter filter 314 ) { 315 this.lineNo = lineNo; 316 this.suppressionType = suppressionType; 317 318 final Pattern commentFormat; 319 if (this.suppressionType == SuppressionType.ON) { 320 commentFormat = filter.onCommentFormat; 321 } 322 else { 323 commentFormat = filter.offCommentFormat; 324 } 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, commentFormat); 332 eventSourceRegexp = Pattern.compile(format); 333 if (filter.messageFormat == null) { 334 eventMessageRegexp = null; 335 } 336 else { 337 format = CommonUtil.fillTemplateWithStringsByRegexp( 338 filter.messageFormat, text, commentFormat); 339 eventMessageRegexp = Pattern.compile(format); 340 } 341 if (filter.idFormat == null) { 342 eventIdRegexp = null; 343 } 344 else { 345 format = CommonUtil.fillTemplateWithStringsByRegexp( 346 filter.idFormat, text, commentFormat); 347 eventIdRegexp = Pattern.compile(format); 348 } 349 } 350 catch (final PatternSyntaxException exc) { 351 throw new IllegalArgumentException( 352 "unable to parse expanded comment " + format, exc); 353 } 354 } 355 356 /** 357 * Indicates whether some other object is "equal to" this one. 358 * 359 * @noinspection EqualsCalledOnEnumConstant 360 * @noinspectionreason EqualsCalledOnEnumConstant - enumeration is needed to keep 361 * code consistent 362 */ 363 @Override 364 public boolean equals(Object other) { 365 if (this == other) { 366 return true; 367 } 368 if (other == null || getClass() != other.getClass()) { 369 return false; 370 } 371 final Suppression suppression = (Suppression) other; 372 return lineNo == suppression.lineNo 373 && Objects.equals(suppressionType, suppression.suppressionType) 374 && Objects.equals(eventSourceRegexp, suppression.eventSourceRegexp) 375 && Objects.equals(eventMessageRegexp, suppression.eventMessageRegexp) 376 && Objects.equals(eventIdRegexp, suppression.eventIdRegexp); 377 } 378 379 @Override 380 public int hashCode() { 381 return Objects.hash( 382 lineNo, suppressionType, eventSourceRegexp, eventMessageRegexp, 383 eventIdRegexp); 384 } 385 386 /** 387 * Checks whether the suppression matches the given {@link AuditEvent}. 388 * 389 * @param event {@link AuditEvent} instance. 390 * @return true if the suppression matches {@link AuditEvent}. 391 */ 392 private boolean isMatch(AuditEvent event) { 393 return isInScopeOfSuppression(event) 394 && isCheckMatch(event) 395 && isIdMatch(event) 396 && isMessageMatch(event); 397 } 398 399 /** 400 * Checks whether {@link AuditEvent} is in the scope of the suppression. 401 * 402 * @param event {@link AuditEvent} instance. 403 * @return true if {@link AuditEvent} is in the scope of the suppression. 404 */ 405 private boolean isInScopeOfSuppression(AuditEvent event) { 406 return lineNo <= event.getLine(); 407 } 408 409 /** 410 * Checks whether {@link AuditEvent} source name matches the check format. 411 * 412 * @param event {@link AuditEvent} instance. 413 * @return true if the {@link AuditEvent} source name matches the check format. 414 */ 415 private boolean isCheckMatch(AuditEvent event) { 416 final Matcher checkMatcher = eventSourceRegexp.matcher(event.getSourceName()); 417 return checkMatcher.find(); 418 } 419 420 /** 421 * Checks whether the {@link AuditEvent} module ID matches the ID format. 422 * 423 * @param event {@link AuditEvent} instance. 424 * @return true if the {@link AuditEvent} module ID matches the ID format. 425 */ 426 private boolean isIdMatch(AuditEvent event) { 427 boolean match = true; 428 if (eventIdRegexp != null) { 429 if (event.getModuleId() == null) { 430 match = false; 431 } 432 else { 433 final Matcher idMatcher = eventIdRegexp.matcher(event.getModuleId()); 434 match = idMatcher.find(); 435 } 436 } 437 return match; 438 } 439 440 /** 441 * Checks whether the {@link AuditEvent} message matches the message format. 442 * 443 * @param event {@link AuditEvent} instance. 444 * @return true if the {@link AuditEvent} message matches the message format. 445 */ 446 private boolean isMessageMatch(AuditEvent event) { 447 boolean match = true; 448 if (eventMessageRegexp != null) { 449 final Matcher messageMatcher = eventMessageRegexp.matcher(event.getMessage()); 450 match = messageMatcher.find(); 451 } 452 return match; 453 } 454 } 455 456}