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