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.IOException; 023import java.nio.charset.StandardCharsets; 024import java.nio.file.Files; 025import java.nio.file.Path; 026import java.nio.file.Paths; 027import java.util.ArrayList; 028import java.util.Collection; 029import java.util.List; 030import java.util.Optional; 031import java.util.regex.Matcher; 032import java.util.regex.Pattern; 033import java.util.regex.PatternSyntaxException; 034 035import com.puppycrawl.tools.checkstyle.AbstractAutomaticBean; 036import com.puppycrawl.tools.checkstyle.PropertyType; 037import com.puppycrawl.tools.checkstyle.XdocsPropertyType; 038import com.puppycrawl.tools.checkstyle.api.AuditEvent; 039import com.puppycrawl.tools.checkstyle.api.FileText; 040import com.puppycrawl.tools.checkstyle.api.Filter; 041import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 042 043/** 044 * <div> 045 * Filter {@code SuppressWithNearbyTextFilter} uses plain text to suppress 046 * nearby audit events. The filter can suppress all checks which have Checker as a parent module. 047 * </div> 048 * 049 * <p> 050 * Setting {@code .*} value to {@code nearbyTextPattern} property will see <b>any</b> 051 * text as a suppression and will likely suppress all audit events in the file. It is 052 * best to set this to a key phrase not commonly used in the file to help denote it 053 * out of the rest of the file as a suppression. See the default value as an example. 054 * </p> 055 * <ul> 056 * <li> 057 * Property {@code checkPattern} - Specify check name pattern to suppress. 058 * Property can also be a RegExp group index at {@code nearbyTextPattern} in 059 * format of {@code $x} and be picked from line that matches {@code nearbyTextPattern}. 060 * Type is {@code java.util.regex.Pattern}. 061 * Default value is {@code ".*"}. 062 * </li> 063 * <li> 064 * Property {@code idPattern} - Specify check ID pattern to suppress. 065 * Type is {@code java.util.regex.Pattern}. 066 * Default value is {@code null}. 067 * </li> 068 * <li> 069 * Property {@code lineRange} - Specify negative/zero/positive value that 070 * defines the number of lines preceding/at/following the suppressing nearby text. 071 * Property can also be a RegExp group index at {@code nearbyTextPattern} in 072 * format of {@code $x} and be picked from line that matches {@code nearbyTextPattern}. 073 * Type is {@code java.lang.String}. 074 * Default value is {@code "0"}. 075 * </li> 076 * <li> 077 * Property {@code messagePattern} - Specify check violation message pattern to suppress. 078 * Type is {@code java.util.regex.Pattern}. 079 * Default value is {@code null}. 080 * </li> 081 * <li> 082 * Property {@code nearbyTextPattern} - Specify nearby text 083 * pattern to trigger filter to begin suppression. 084 * Type is {@code java.util.regex.Pattern}. 085 * Default value is {@code "SUPPRESS CHECKSTYLE (\w+)"}. 086 * </li> 087 * </ul> 088 * 089 * <p> 090 * Parent is {@code com.puppycrawl.tools.checkstyle.Checker} 091 * </p> 092 * 093 * @since 10.10.0 094 */ 095public class SuppressWithNearbyTextFilter extends AbstractAutomaticBean implements Filter { 096 097 /** Default nearby text pattern to turn check reporting off. */ 098 private static final String DEFAULT_NEARBY_TEXT_PATTERN = "SUPPRESS CHECKSTYLE (\\w+)"; 099 100 /** Default regex for checks that should be suppressed. */ 101 private static final String DEFAULT_CHECK_PATTERN = ".*"; 102 103 /** Default number of lines that should be suppressed. */ 104 private static final String DEFAULT_LINE_RANGE = "0"; 105 106 /** Suppressions encountered in current file. */ 107 private final List<Suppression> suppressions = new ArrayList<>(); 108 109 /** Specify nearby text pattern to trigger filter to begin suppression. */ 110 @XdocsPropertyType(PropertyType.PATTERN) 111 private Pattern nearbyTextPattern = Pattern.compile(DEFAULT_NEARBY_TEXT_PATTERN); 112 113 /** 114 * Specify check name pattern to suppress. Property can also be a RegExp group index 115 * at {@code nearbyTextPattern} in format of {@code $x} and be picked from line that 116 * matches {@code nearbyTextPattern}. 117 */ 118 @XdocsPropertyType(PropertyType.PATTERN) 119 private String checkPattern = DEFAULT_CHECK_PATTERN; 120 121 /** Specify check violation message pattern to suppress. */ 122 @XdocsPropertyType(PropertyType.PATTERN) 123 private String messagePattern; 124 125 /** Specify check ID pattern to suppress. */ 126 @XdocsPropertyType(PropertyType.PATTERN) 127 private String idPattern; 128 129 /** 130 * Specify negative/zero/positive value that defines the number of lines 131 * preceding/at/following the suppressing nearby text. Property can also be a RegExp group 132 * index at {@code nearbyTextPattern} in format of {@code $x} and be picked 133 * from line that matches {@code nearbyTextPattern}. 134 */ 135 private String lineRange = DEFAULT_LINE_RANGE; 136 137 /** The absolute path to the currently processed file. */ 138 private String cachedFileAbsolutePath = ""; 139 140 /** 141 * Setter to specify nearby text pattern to trigger filter to begin suppression. 142 * 143 * @param pattern a {@code Pattern} value. 144 * @since 10.10.0 145 */ 146 public final void setNearbyTextPattern(Pattern pattern) { 147 nearbyTextPattern = pattern; 148 } 149 150 /** 151 * Setter to specify check name pattern to suppress. Property can also 152 * be a RegExp group index at {@code nearbyTextPattern} in 153 * format of {@code $x} and be picked from line that matches {@code nearbyTextPattern}. 154 * 155 * @param pattern a {@code String} value. 156 * @since 10.10.0 157 */ 158 public final void setCheckPattern(String pattern) { 159 checkPattern = pattern; 160 } 161 162 /** 163 * Setter to specify check violation message pattern to suppress. 164 * 165 * @param pattern a {@code String} value. 166 * @since 10.10.0 167 */ 168 public void setMessagePattern(String pattern) { 169 messagePattern = pattern; 170 } 171 172 /** 173 * Setter to specify check ID pattern to suppress. 174 * 175 * @param pattern a {@code String} value. 176 * @since 10.10.0 177 */ 178 public void setIdPattern(String pattern) { 179 idPattern = pattern; 180 } 181 182 /** 183 * Setter to specify negative/zero/positive value that defines the number 184 * of lines preceding/at/following the suppressing nearby text. Property can also 185 * be a RegExp group index at {@code nearbyTextPattern} in 186 * format of {@code $x} and be picked from line that matches {@code nearbyTextPattern}. 187 * 188 * @param format a {@code String} value. 189 * @since 10.10.0 190 */ 191 public final void setLineRange(String format) { 192 lineRange = format; 193 } 194 195 @Override 196 public boolean accept(AuditEvent event) { 197 boolean accepted = true; 198 199 if (event.getViolation() != null) { 200 final String eventFileTextAbsolutePath = event.getFileName(); 201 202 if (!cachedFileAbsolutePath.equals(eventFileTextAbsolutePath)) { 203 final FileText currentFileText = getFileText(eventFileTextAbsolutePath); 204 205 if (currentFileText != null) { 206 cachedFileAbsolutePath = currentFileText.getFile().getAbsolutePath(); 207 collectSuppressions(currentFileText); 208 } 209 } 210 211 final Optional<Suppression> nearestSuppression = 212 getNearestSuppression(suppressions, event); 213 accepted = nearestSuppression.isEmpty(); 214 } 215 return accepted; 216 } 217 218 @Override 219 protected void finishLocalSetup() { 220 // No code by default 221 } 222 223 /** 224 * Returns {@link FileText} instance created based on the given file name. 225 * 226 * @param fileName the name of the file. 227 * @return {@link FileText} instance. 228 * @throws IllegalStateException if the file could not be read. 229 */ 230 private static FileText getFileText(String fileName) { 231 final Path path = Paths.get(fileName); 232 FileText result = null; 233 234 // some violations can be on a directory, instead of a file 235 if (!Files.isDirectory(path)) { 236 try { 237 result = new FileText(path.toFile(), StandardCharsets.UTF_8.name()); 238 } 239 catch (IOException exc) { 240 throw new IllegalStateException("Cannot read source file: " + fileName, exc); 241 } 242 } 243 244 return result; 245 } 246 247 /** 248 * Collets all {@link Suppression} instances retrieved from the given {@link FileText}. 249 * 250 * @param fileText {@link FileText} instance. 251 */ 252 private void collectSuppressions(FileText fileText) { 253 suppressions.clear(); 254 255 for (int lineNo = 0; lineNo < fileText.size(); lineNo++) { 256 final Suppression suppression = getSuppression(fileText, lineNo); 257 if (suppression != null) { 258 suppressions.add(suppression); 259 } 260 } 261 } 262 263 /** 264 * Tries to extract the suppression from the given line. 265 * 266 * @param fileText {@link FileText} instance. 267 * @param lineNo line number. 268 * @return {@link Suppression} instance. 269 */ 270 private Suppression getSuppression(FileText fileText, int lineNo) { 271 final String line = fileText.get(lineNo); 272 final Matcher nearbyTextMatcher = nearbyTextPattern.matcher(line); 273 274 Suppression suppression = null; 275 if (nearbyTextMatcher.find()) { 276 final String text = nearbyTextMatcher.group(0); 277 suppression = new Suppression(text, lineNo + 1, this); 278 } 279 280 return suppression; 281 } 282 283 /** 284 * Finds the nearest {@link Suppression} instance which can suppress 285 * the given {@link AuditEvent}. The nearest suppression is the suppression which scope 286 * is before the line and column of the event. 287 * 288 * @param suppressions collection of {@link Suppression} instances. 289 * @param event {@link AuditEvent} instance. 290 * @return {@link Suppression} instance. 291 */ 292 private static Optional<Suppression> getNearestSuppression(Collection<Suppression> suppressions, 293 AuditEvent event) { 294 return suppressions 295 .stream() 296 .filter(suppression -> suppression.isMatch(event)) 297 .findFirst(); 298 } 299 300 /** The class which represents the suppression. */ 301 private static final class Suppression { 302 303 /** The first line where warnings may be suppressed. */ 304 private final int firstLine; 305 306 /** The last line where warnings may be suppressed. */ 307 private final int lastLine; 308 309 /** The regexp which is used to match the event source.*/ 310 private final Pattern eventSourceRegexp; 311 312 /** The regexp which is used to match the event message.*/ 313 private Pattern eventMessageRegexp; 314 315 /** The regexp which is used to match the event ID.*/ 316 private Pattern eventIdRegexp; 317 318 /** 319 * Constructs new {@code Suppression} instance. 320 * 321 * @param text suppression text. 322 * @param lineNo suppression line number. 323 * @param filter the {@code SuppressWithNearbyTextFilter} with the context. 324 * @throws IllegalArgumentException if there is an error in the filter regex syntax. 325 */ 326 private Suppression( 327 String text, 328 int lineNo, 329 SuppressWithNearbyTextFilter filter 330 ) { 331 final Pattern nearbyTextPattern = filter.nearbyTextPattern; 332 final String lineRange = filter.lineRange; 333 String format = ""; 334 try { 335 format = CommonUtil.fillTemplateWithStringsByRegexp( 336 filter.checkPattern, text, nearbyTextPattern); 337 eventSourceRegexp = Pattern.compile(format); 338 if (filter.messagePattern != null) { 339 format = CommonUtil.fillTemplateWithStringsByRegexp( 340 filter.messagePattern, text, nearbyTextPattern); 341 eventMessageRegexp = Pattern.compile(format); 342 } 343 if (filter.idPattern != null) { 344 format = CommonUtil.fillTemplateWithStringsByRegexp( 345 filter.idPattern, text, nearbyTextPattern); 346 eventIdRegexp = Pattern.compile(format); 347 } 348 format = CommonUtil.fillTemplateWithStringsByRegexp(lineRange, 349 text, nearbyTextPattern); 350 351 final int range = parseRange(format, lineRange, text); 352 353 firstLine = Math.min(lineNo, lineNo + range); 354 lastLine = Math.max(lineNo, lineNo + range); 355 } 356 catch (final PatternSyntaxException exc) { 357 throw new IllegalArgumentException( 358 "unable to parse expanded comment " + format, exc); 359 } 360 } 361 362 /** 363 * Gets range from suppress filter range format param. 364 * 365 * @param format range format to parse 366 * @param lineRange raw line range 367 * @param text text of the suppression 368 * @return parsed range 369 * @throws IllegalArgumentException when unable to parse int in format 370 */ 371 private static int parseRange(String format, String lineRange, String text) { 372 try { 373 return Integer.parseInt(format); 374 } 375 catch (final NumberFormatException exc) { 376 throw new IllegalArgumentException("unable to parse line range from '" + text 377 + "' using " + lineRange, exc); 378 } 379 } 380 381 /** 382 * Determines whether the source of an audit event 383 * matches the text of this suppression. 384 * 385 * @param event the {@code AuditEvent} to check. 386 * @return true if the source of event matches the text of this suppression. 387 */ 388 private boolean isMatch(AuditEvent event) { 389 return isInScopeOfSuppression(event) 390 && isCheckMatch(event) 391 && isIdMatch(event) 392 && isMessageMatch(event); 393 } 394 395 /** 396 * Checks whether the {@link AuditEvent} is in the scope of the suppression. 397 * 398 * @param event {@link AuditEvent} instance. 399 * @return true if the {@link AuditEvent} is in the scope of the suppression. 400 */ 401 private boolean isInScopeOfSuppression(AuditEvent event) { 402 final int eventLine = event.getLine(); 403 return eventLine >= firstLine && eventLine <= lastLine; 404 } 405 406 /** 407 * Checks whether {@link AuditEvent} source name matches the check pattern. 408 * 409 * @param event {@link AuditEvent} instance. 410 * @return true if the {@link AuditEvent} source name matches the check pattern. 411 */ 412 private boolean isCheckMatch(AuditEvent event) { 413 final Matcher checkMatcher = eventSourceRegexp.matcher(event.getSourceName()); 414 return checkMatcher.find(); 415 } 416 417 /** 418 * Checks whether the {@link AuditEvent} module ID matches the ID pattern. 419 * 420 * @param event {@link AuditEvent} instance. 421 * @return true if the {@link AuditEvent} module ID matches the ID pattern. 422 */ 423 private boolean isIdMatch(AuditEvent event) { 424 boolean match = true; 425 if (eventIdRegexp != null) { 426 if (event.getModuleId() == null) { 427 match = false; 428 } 429 else { 430 final Matcher idMatcher = eventIdRegexp.matcher(event.getModuleId()); 431 match = idMatcher.find(); 432 } 433 } 434 return match; 435 } 436 437 /** 438 * Checks whether the {@link AuditEvent} message matches the message pattern. 439 * 440 * @param event {@link AuditEvent} instance. 441 * @return true if the {@link AuditEvent} message matches the message pattern. 442 */ 443 private boolean isMessageMatch(AuditEvent event) { 444 boolean match = true; 445 if (eventMessageRegexp != null) { 446 final Matcher messageMatcher = eventMessageRegexp.matcher(event.getMessage()); 447 match = messageMatcher.find(); 448 } 449 return match; 450 } 451 } 452}