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