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