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