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