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