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