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