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