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