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