001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2019 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.lang.ref.WeakReference; 023import java.util.ArrayList; 024import java.util.Collection; 025import java.util.List; 026import java.util.Objects; 027import java.util.regex.Matcher; 028import java.util.regex.Pattern; 029import java.util.regex.PatternSyntaxException; 030 031import com.puppycrawl.tools.checkstyle.TreeWalkerAuditEvent; 032import com.puppycrawl.tools.checkstyle.TreeWalkerFilter; 033import com.puppycrawl.tools.checkstyle.api.AutomaticBean; 034import com.puppycrawl.tools.checkstyle.api.FileContents; 035import com.puppycrawl.tools.checkstyle.api.TextBlock; 036import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 037 038/** 039 * <p> 040 * A filter that uses nearby comments to suppress audit events. 041 * </p> 042 * 043 * <p>This check is philosophically similar to {@link SuppressionCommentFilter}. 044 * Unlike {@link SuppressionCommentFilter}, this filter does not require 045 * pairs of comments. This check may be used to suppress warnings in the 046 * current line: 047 * <pre> 048 * offendingLine(for, whatever, reason); // SUPPRESS ParameterNumberCheck 049 * </pre> 050 * or it may be configured to span multiple lines, either forward: 051 * <pre> 052 * // PERMIT MultipleVariableDeclarations NEXT 3 LINES 053 * double x1 = 1.0, y1 = 0.0, z1 = 0.0; 054 * double x2 = 0.0, y2 = 1.0, z2 = 0.0; 055 * double x3 = 0.0, y3 = 0.0, z3 = 1.0; 056 * </pre> 057 * or reverse: 058 * <pre> 059 * try { 060 * thirdPartyLibrary.method(); 061 * } catch (RuntimeException ex) { 062 * // ALLOW ILLEGAL CATCH BECAUSE third party API wraps everything 063 * // in RuntimeExceptions. 064 * ... 065 * } 066 * </pre> 067 * 068 * <p>See {@link SuppressionCommentFilter} for usage notes. 069 * 070 */ 071public class SuppressWithNearbyCommentFilter 072 extends AutomaticBean 073 implements TreeWalkerFilter { 074 075 /** Format to turns checkstyle reporting off. */ 076 private static final String DEFAULT_COMMENT_FORMAT = 077 "SUPPRESS CHECKSTYLE (\\w+)"; 078 079 /** Default regex for checks that should be suppressed. */ 080 private static final String DEFAULT_CHECK_FORMAT = ".*"; 081 082 /** Default regex for lines that should be suppressed. */ 083 private static final String DEFAULT_INFLUENCE_FORMAT = "0"; 084 085 /** Tagged comments. */ 086 private final List<Tag> tags = new ArrayList<>(); 087 088 /** Whether to look for trigger in C-style comments. */ 089 private boolean checkC = true; 090 091 /** Whether to look for trigger in C++-style comments. */ 092 // -@cs[AbbreviationAsWordInName] We can not change it as, 093 // check's property is a part of API (used in configurations). 094 private boolean checkCPP = true; 095 096 /** Parsed comment regexp that marks checkstyle suppression region. */ 097 private Pattern commentFormat = Pattern.compile(DEFAULT_COMMENT_FORMAT); 098 099 /** The comment pattern that triggers suppression. */ 100 private String checkFormat = DEFAULT_CHECK_FORMAT; 101 102 /** The message format to suppress. */ 103 private String messageFormat; 104 105 /** The influence of the suppression comment. */ 106 private String influenceFormat = DEFAULT_INFLUENCE_FORMAT; 107 108 /** 109 * References the current FileContents for this filter. 110 * Since this is a weak reference to the FileContents, the FileContents 111 * can be reclaimed as soon as the strong references in TreeWalker 112 * are reassigned to the next FileContents, at which time filtering for 113 * the current FileContents is finished. 114 */ 115 private WeakReference<FileContents> fileContentsReference = new WeakReference<>(null); 116 117 /** 118 * Set the format for a comment that turns off reporting. 119 * @param pattern a pattern. 120 */ 121 public final void setCommentFormat(Pattern pattern) { 122 commentFormat = pattern; 123 } 124 125 /** 126 * Returns FileContents for this filter. 127 * @return the FileContents for this filter. 128 */ 129 private FileContents getFileContents() { 130 return fileContentsReference.get(); 131 } 132 133 /** 134 * Set the FileContents for this filter. 135 * @param fileContents the FileContents for this filter. 136 * @noinspection WeakerAccess 137 */ 138 public void setFileContents(FileContents fileContents) { 139 fileContentsReference = new WeakReference<>(fileContents); 140 } 141 142 /** 143 * Set the format for a check. 144 * @param format a {@code String} value 145 */ 146 public final void setCheckFormat(String format) { 147 checkFormat = format; 148 } 149 150 /** 151 * Set the format for a message. 152 * @param format a {@code String} value 153 */ 154 public void setMessageFormat(String format) { 155 messageFormat = format; 156 } 157 158 /** 159 * Set the format for the influence of this check. 160 * @param format a {@code String} value 161 */ 162 public final void setInfluenceFormat(String format) { 163 influenceFormat = format; 164 } 165 166 /** 167 * Set whether to look in C++ comments. 168 * @param checkCpp {@code true} if C++ comments are checked. 169 */ 170 // -@cs[AbbreviationAsWordInName] We can not change it as, 171 // check's property is a part of API (used in configurations). 172 public void setCheckCPP(boolean checkCpp) { 173 checkCPP = checkCpp; 174 } 175 176 /** 177 * Set whether to look in C comments. 178 * @param checkC {@code true} if C comments are checked. 179 */ 180 public void setCheckC(boolean checkC) { 181 this.checkC = checkC; 182 } 183 184 @Override 185 protected void finishLocalSetup() { 186 // No code by default 187 } 188 189 @Override 190 public boolean accept(TreeWalkerAuditEvent event) { 191 boolean accepted = true; 192 193 if (event.getLocalizedMessage() != null) { 194 // Lazy update. If the first event for the current file, update file 195 // contents and tag suppressions 196 final FileContents currentContents = event.getFileContents(); 197 198 if (getFileContents() != currentContents) { 199 setFileContents(currentContents); 200 tagSuppressions(); 201 } 202 if (matchesTag(event)) { 203 accepted = false; 204 } 205 } 206 return accepted; 207 } 208 209 /** 210 * Whether current event matches any tag from {@link #tags}. 211 * @param event TreeWalkerAuditEvent to test match on {@link #tags}. 212 * @return true if event matches any tag from {@link #tags}, false otherwise. 213 */ 214 private boolean matchesTag(TreeWalkerAuditEvent event) { 215 boolean result = false; 216 for (final Tag tag : tags) { 217 if (tag.isMatch(event)) { 218 result = true; 219 break; 220 } 221 } 222 return result; 223 } 224 225 /** 226 * Collects all the suppression tags for all comments into a list and 227 * sorts the list. 228 */ 229 private void tagSuppressions() { 230 tags.clear(); 231 final FileContents contents = getFileContents(); 232 if (checkCPP) { 233 tagSuppressions(contents.getSingleLineComments().values()); 234 } 235 if (checkC) { 236 final Collection<List<TextBlock>> cComments = 237 contents.getBlockComments().values(); 238 cComments.forEach(this::tagSuppressions); 239 } 240 } 241 242 /** 243 * Appends the suppressions in a collection of comments to the full 244 * set of suppression tags. 245 * @param comments the set of comments. 246 */ 247 private void tagSuppressions(Collection<TextBlock> comments) { 248 for (final TextBlock comment : comments) { 249 final int startLineNo = comment.getStartLineNo(); 250 final String[] text = comment.getText(); 251 tagCommentLine(text[0], startLineNo); 252 for (int i = 1; i < text.length; i++) { 253 tagCommentLine(text[i], startLineNo + i); 254 } 255 } 256 } 257 258 /** 259 * Tags a string if it matches the format for turning 260 * checkstyle reporting on or the format for turning reporting off. 261 * @param text the string to tag. 262 * @param line the line number of text. 263 */ 264 private void tagCommentLine(String text, int line) { 265 final Matcher matcher = commentFormat.matcher(text); 266 if (matcher.find()) { 267 addTag(matcher.group(0), line); 268 } 269 } 270 271 /** 272 * Adds a comment suppression {@code Tag} to the list of all tags. 273 * @param text the text of the tag. 274 * @param line the line number of the tag. 275 */ 276 private void addTag(String text, int line) { 277 final Tag tag = new Tag(text, line, this); 278 tags.add(tag); 279 } 280 281 /** 282 * A Tag holds a suppression comment and its location. 283 */ 284 public static class Tag { 285 286 /** The text of the tag. */ 287 private final String text; 288 289 /** The first line where warnings may be suppressed. */ 290 private final int firstLine; 291 292 /** The last line where warnings may be suppressed. */ 293 private final int lastLine; 294 295 /** The parsed check regexp, expanded for the text of this tag. */ 296 private final Pattern tagCheckRegexp; 297 298 /** The parsed message regexp, expanded for the text of this tag. */ 299 private final Pattern tagMessageRegexp; 300 301 /** 302 * Constructs a tag. 303 * @param text the text of the suppression. 304 * @param line the line number. 305 * @param filter the {@code SuppressWithNearbyCommentFilter} with the context 306 * @throws IllegalArgumentException if unable to parse expanded text. 307 */ 308 public Tag(String text, int line, SuppressWithNearbyCommentFilter filter) { 309 this.text = text; 310 311 //Expand regexp for check and message 312 //Does not intern Patterns with Utils.getPattern() 313 String format = ""; 314 try { 315 format = CommonUtil.fillTemplateWithStringsByRegexp( 316 filter.checkFormat, text, filter.commentFormat); 317 tagCheckRegexp = Pattern.compile(format); 318 if (filter.messageFormat == null) { 319 tagMessageRegexp = null; 320 } 321 else { 322 format = CommonUtil.fillTemplateWithStringsByRegexp( 323 filter.messageFormat, text, filter.commentFormat); 324 tagMessageRegexp = Pattern.compile(format); 325 } 326 format = CommonUtil.fillTemplateWithStringsByRegexp( 327 filter.influenceFormat, text, filter.commentFormat); 328 329 if (CommonUtil.startsWithChar(format, '+')) { 330 format = format.substring(1); 331 } 332 final int influence = parseInfluence(format, filter.influenceFormat, text); 333 334 if (influence >= 1) { 335 firstLine = line; 336 lastLine = line + influence; 337 } 338 else { 339 firstLine = line + influence; 340 lastLine = line; 341 } 342 } 343 catch (final PatternSyntaxException ex) { 344 throw new IllegalArgumentException( 345 "unable to parse expanded comment " + format, ex); 346 } 347 } 348 349 /** 350 * Gets influence from suppress filter influence format param. 351 * 352 * @param format influence format to parse 353 * @param influenceFormat raw influence format 354 * @param text text of the suppression 355 * @return parsed influence 356 */ 357 private static int parseInfluence(String format, String influenceFormat, String text) { 358 try { 359 return Integer.parseInt(format); 360 } 361 catch (final NumberFormatException ex) { 362 throw new IllegalArgumentException("unable to parse influence from '" + text 363 + "' using " + influenceFormat, ex); 364 } 365 } 366 367 @Override 368 public boolean equals(Object other) { 369 if (this == other) { 370 return true; 371 } 372 if (other == null || getClass() != other.getClass()) { 373 return false; 374 } 375 final Tag tag = (Tag) other; 376 return Objects.equals(firstLine, tag.firstLine) 377 && Objects.equals(lastLine, tag.lastLine) 378 && Objects.equals(text, tag.text) 379 && Objects.equals(tagCheckRegexp, tag.tagCheckRegexp) 380 && Objects.equals(tagMessageRegexp, tag.tagMessageRegexp); 381 } 382 383 @Override 384 public int hashCode() { 385 return Objects.hash(text, firstLine, lastLine, tagCheckRegexp, tagMessageRegexp); 386 } 387 388 /** 389 * Determines whether the source of an audit event 390 * matches the text of this tag. 391 * @param event the {@code TreeWalkerAuditEvent} to check. 392 * @return true if the source of event matches the text of this tag. 393 */ 394 public boolean isMatch(TreeWalkerAuditEvent event) { 395 final int line = event.getLine(); 396 boolean match = false; 397 398 if (line >= firstLine && line <= lastLine) { 399 final Matcher tagMatcher = tagCheckRegexp.matcher(event.getSourceName()); 400 401 if (tagMatcher.find()) { 402 match = true; 403 } 404 else if (tagMessageRegexp == null) { 405 if (event.getModuleId() != null) { 406 final Matcher idMatcher = tagCheckRegexp.matcher(event.getModuleId()); 407 match = idMatcher.find(); 408 } 409 } 410 else { 411 final Matcher messageMatcher = tagMessageRegexp.matcher(event.getMessage()); 412 match = messageMatcher.find(); 413 } 414 } 415 return match; 416 } 417 418 @Override 419 public String toString() { 420 return "Tag[text='" + text + '\'' 421 + ", firstLine=" + firstLine 422 + ", lastLine=" + lastLine 423 + ", tagCheckRegexp=" + tagCheckRegexp 424 + ", tagMessageRegexp=" + tagMessageRegexp 425 + ']'; 426 } 427 428 } 429 430}