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