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