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