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.Collections; 026import java.util.List; 027import java.util.Objects; 028import java.util.regex.Matcher; 029import java.util.regex.Pattern; 030import java.util.regex.PatternSyntaxException; 031 032import com.puppycrawl.tools.checkstyle.TreeWalkerAuditEvent; 033import com.puppycrawl.tools.checkstyle.TreeWalkerFilter; 034import com.puppycrawl.tools.checkstyle.api.AutomaticBean; 035import com.puppycrawl.tools.checkstyle.api.FileContents; 036import com.puppycrawl.tools.checkstyle.api.TextBlock; 037import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 038 039/** 040 * <p> 041 * A filter that uses comments to suppress audit events. 042 * </p> 043 * <p> 044 * Rationale: 045 * Sometimes there are legitimate reasons for violating a check. When 046 * this is a matter of the code in question and not personal 047 * preference, the best place to override the policy is in the code 048 * itself. Semi-structured comments can be associated with the check. 049 * This is sometimes superior to a separate suppressions file, which 050 * must be kept up-to-date as the source file is edited. 051 * </p> 052 */ 053public class SuppressionCommentFilter 054 extends AutomaticBean 055 implements TreeWalkerFilter { 056 057 /** 058 * Enum to be used for switching checkstyle reporting for tags. 059 */ 060 public enum TagType { 061 062 /** 063 * Switch reporting on. 064 */ 065 ON, 066 /** 067 * Switch reporting off. 068 */ 069 OFF 070 071 } 072 073 /** Turns checkstyle reporting off. */ 074 private static final String DEFAULT_OFF_FORMAT = "CHECKSTYLE:OFF"; 075 076 /** Turns checkstyle reporting on. */ 077 private static final String DEFAULT_ON_FORMAT = "CHECKSTYLE:ON"; 078 079 /** Control all checks. */ 080 private static final String DEFAULT_CHECK_FORMAT = ".*"; 081 082 /** Tagged comments. */ 083 private final List<Tag> tags = new ArrayList<>(); 084 085 /** Whether to look in comments of the C type. */ 086 private boolean checkC = true; 087 088 /** Whether to look in comments of the C++ type. */ 089 // -@cs[AbbreviationAsWordInName] we can not change it as, 090 // Check property is a part of API (used in configurations) 091 private boolean checkCPP = true; 092 093 /** Parsed comment regexp that turns checkstyle reporting off. */ 094 private Pattern offCommentFormat = Pattern.compile(DEFAULT_OFF_FORMAT); 095 096 /** Parsed comment regexp that turns checkstyle reporting on. */ 097 private Pattern onCommentFormat = Pattern.compile(DEFAULT_ON_FORMAT); 098 099 /** The check format to suppress. */ 100 private String checkFormat = DEFAULT_CHECK_FORMAT; 101 102 /** The message format to suppress. */ 103 private String messageFormat; 104 105 /** 106 * References the current FileContents for this filter. 107 * Since this is a weak reference to the FileContents, the FileContents 108 * can be reclaimed as soon as the strong references in TreeWalker 109 * are reassigned to the next FileContents, at which time filtering for 110 * the current FileContents is finished. 111 */ 112 private WeakReference<FileContents> fileContentsReference = new WeakReference<>(null); 113 114 /** 115 * Set the format for a comment that turns off reporting. 116 * @param pattern a pattern. 117 */ 118 public final void setOffCommentFormat(Pattern pattern) { 119 offCommentFormat = pattern; 120 } 121 122 /** 123 * Set the format for a comment that turns on reporting. 124 * @param pattern a pattern. 125 */ 126 public final void setOnCommentFormat(Pattern pattern) { 127 onCommentFormat = pattern; 128 } 129 130 /** 131 * Returns FileContents for this filter. 132 * @return the FileContents for this filter. 133 */ 134 private FileContents getFileContents() { 135 return fileContentsReference.get(); 136 } 137 138 /** 139 * Set the FileContents for this filter. 140 * @param fileContents the FileContents for this filter. 141 * @noinspection WeakerAccess 142 */ 143 public void setFileContents(FileContents fileContents) { 144 fileContentsReference = new WeakReference<>(fileContents); 145 } 146 147 /** 148 * Set the format for a check. 149 * @param format a {@code String} value 150 */ 151 public final void setCheckFormat(String format) { 152 checkFormat = format; 153 } 154 155 /** 156 * Set the format for a message. 157 * @param format a {@code String} value 158 */ 159 public void setMessageFormat(String format) { 160 messageFormat = format; 161 } 162 163 /** 164 * Set whether to look in C++ comments. 165 * @param checkCpp {@code true} if C++ comments are checked. 166 */ 167 // -@cs[AbbreviationAsWordInName] We can not change it as, 168 // check's property is a part of API (used in configurations). 169 public void setCheckCPP(boolean checkCpp) { 170 checkCPP = checkCpp; 171 } 172 173 /** 174 * Set whether to look in C comments. 175 * @param checkC {@code true} if C comments are checked. 176 */ 177 public void setCheckC(boolean checkC) { 178 this.checkC = checkC; 179 } 180 181 @Override 182 protected void finishLocalSetup() { 183 // No code by default 184 } 185 186 @Override 187 public boolean accept(TreeWalkerAuditEvent event) { 188 boolean accepted = true; 189 190 if (event.getLocalizedMessage() != null) { 191 // Lazy update. If the first event for the current file, update file 192 // contents and tag suppressions 193 final FileContents currentContents = event.getFileContents(); 194 195 if (getFileContents() != currentContents) { 196 setFileContents(currentContents); 197 tagSuppressions(); 198 } 199 final Tag matchTag = findNearestMatch(event); 200 accepted = matchTag == null || matchTag.getTagType() == TagType.ON; 201 } 202 return accepted; 203 } 204 205 /** 206 * Finds the nearest comment text tag that matches an audit event. 207 * The nearest tag is before the line and column of the event. 208 * @param event the {@code TreeWalkerAuditEvent} to match. 209 * @return The {@code Tag} nearest event. 210 */ 211 private Tag findNearestMatch(TreeWalkerAuditEvent event) { 212 Tag result = null; 213 for (Tag tag : tags) { 214 if (tag.getLine() > event.getLine() 215 || tag.getLine() == event.getLine() 216 && tag.getColumn() > event.getColumn()) { 217 break; 218 } 219 if (tag.isMatch(event)) { 220 result = tag; 221 } 222 } 223 return result; 224 } 225 226 /** 227 * Collects all the suppression tags for all comments into a list and 228 * sorts the list. 229 */ 230 private void tagSuppressions() { 231 tags.clear(); 232 final FileContents contents = getFileContents(); 233 if (checkCPP) { 234 tagSuppressions(contents.getSingleLineComments().values()); 235 } 236 if (checkC) { 237 final Collection<List<TextBlock>> cComments = contents 238 .getBlockComments().values(); 239 cComments.forEach(this::tagSuppressions); 240 } 241 Collections.sort(tags); 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 (TextBlock comment : comments) { 251 final int startLineNo = comment.getStartLineNo(); 252 final String[] text = comment.getText(); 253 tagCommentLine(text[0], startLineNo, comment.getStartColNo()); 254 for (int i = 1; i < text.length; i++) { 255 tagCommentLine(text[i], startLineNo + i, 0); 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 * @param column the column number of text. 266 */ 267 private void tagCommentLine(String text, int line, int column) { 268 final Matcher offMatcher = offCommentFormat.matcher(text); 269 if (offMatcher.find()) { 270 addTag(offMatcher.group(0), line, column, TagType.OFF); 271 } 272 else { 273 final Matcher onMatcher = onCommentFormat.matcher(text); 274 if (onMatcher.find()) { 275 addTag(onMatcher.group(0), line, column, TagType.ON); 276 } 277 } 278 } 279 280 /** 281 * Adds a {@code Tag} to the list of all tags. 282 * @param text the text of the tag. 283 * @param line the line number of the tag. 284 * @param column the column number of the tag. 285 * @param reportingOn {@code true} if the tag turns checkstyle reporting on. 286 */ 287 private void addTag(String text, int line, int column, TagType reportingOn) { 288 final Tag tag = new Tag(line, column, text, reportingOn, this); 289 tags.add(tag); 290 } 291 292 /** 293 * A Tag holds a suppression comment and its location, and determines 294 * whether the suppression turns checkstyle reporting on or off. 295 */ 296 public static class Tag 297 implements Comparable<Tag> { 298 299 /** The text of the tag. */ 300 private final String text; 301 302 /** The line number of the tag. */ 303 private final int line; 304 305 /** The column number of the tag. */ 306 private final int column; 307 308 /** Determines whether the suppression turns checkstyle reporting on. */ 309 private final TagType tagType; 310 311 /** The parsed check regexp, expanded for the text of this tag. */ 312 private final Pattern tagCheckRegexp; 313 314 /** The parsed message regexp, expanded for the text of this tag. */ 315 private final Pattern tagMessageRegexp; 316 317 /** 318 * Constructs a tag. 319 * @param line the line number. 320 * @param column the column number. 321 * @param text the text of the suppression. 322 * @param tagType {@code ON} if the tag turns checkstyle reporting. 323 * @param filter the {@code SuppressionCommentFilter} with the context 324 * @throws IllegalArgumentException if unable to parse expanded text. 325 */ 326 public Tag(int line, int column, String text, TagType tagType, 327 SuppressionCommentFilter filter) { 328 this.line = line; 329 this.column = column; 330 this.text = text; 331 this.tagType = tagType; 332 333 //Expand regexp for check and message 334 //Does not intern Patterns with Utils.getPattern() 335 String format = ""; 336 try { 337 if (this.tagType == TagType.ON) { 338 format = CommonUtil.fillTemplateWithStringsByRegexp( 339 filter.checkFormat, text, filter.onCommentFormat); 340 tagCheckRegexp = Pattern.compile(format); 341 if (filter.messageFormat == null) { 342 tagMessageRegexp = null; 343 } 344 else { 345 format = CommonUtil.fillTemplateWithStringsByRegexp( 346 filter.messageFormat, text, filter.onCommentFormat); 347 tagMessageRegexp = Pattern.compile(format); 348 } 349 } 350 else { 351 format = CommonUtil.fillTemplateWithStringsByRegexp( 352 filter.checkFormat, text, filter.offCommentFormat); 353 tagCheckRegexp = Pattern.compile(format); 354 if (filter.messageFormat == null) { 355 tagMessageRegexp = null; 356 } 357 else { 358 format = CommonUtil.fillTemplateWithStringsByRegexp( 359 filter.messageFormat, text, filter.offCommentFormat); 360 tagMessageRegexp = Pattern.compile(format); 361 } 362 } 363 } 364 catch (final PatternSyntaxException ex) { 365 throw new IllegalArgumentException( 366 "unable to parse expanded comment " + format, ex); 367 } 368 } 369 370 /** 371 * Returns line number of the tag in the source file. 372 * @return the line number of the tag in the source file. 373 */ 374 public int getLine() { 375 return line; 376 } 377 378 /** 379 * Determines the column number of the tag in the source file. 380 * Will be 0 for all lines of multiline comment, except the 381 * first line. 382 * @return the column number of the tag in the source file. 383 */ 384 public int getColumn() { 385 return column; 386 } 387 388 /** 389 * Determines whether the suppression turns checkstyle reporting on or 390 * off. 391 * @return {@code ON} if the suppression turns reporting on. 392 */ 393 public TagType getTagType() { 394 return tagType; 395 } 396 397 /** 398 * Compares the position of this tag in the file 399 * with the position of another tag. 400 * @param object the tag to compare with this one. 401 * @return a negative number if this tag is before the other tag, 402 * 0 if they are at the same position, and a positive number if this 403 * tag is after the other tag. 404 */ 405 @Override 406 public int compareTo(Tag object) { 407 final int result; 408 if (line == object.line) { 409 result = Integer.compare(column, object.column); 410 } 411 else { 412 result = Integer.compare(line, object.line); 413 } 414 return result; 415 } 416 417 @Override 418 public boolean equals(Object other) { 419 if (this == other) { 420 return true; 421 } 422 if (other == null || getClass() != other.getClass()) { 423 return false; 424 } 425 final Tag tag = (Tag) other; 426 return Objects.equals(line, tag.line) 427 && Objects.equals(column, tag.column) 428 && Objects.equals(tagType, tag.tagType) 429 && Objects.equals(text, tag.text) 430 && Objects.equals(tagCheckRegexp, tag.tagCheckRegexp) 431 && Objects.equals(tagMessageRegexp, tag.tagMessageRegexp); 432 } 433 434 @Override 435 public int hashCode() { 436 return Objects.hash(text, line, column, tagType, tagCheckRegexp, tagMessageRegexp); 437 } 438 439 /** 440 * Determines whether the source of an audit event 441 * matches the text of this tag. 442 * @param event the {@code TreeWalkerAuditEvent} to check. 443 * @return true if the source of event matches the text of this tag. 444 */ 445 public boolean isMatch(TreeWalkerAuditEvent event) { 446 boolean match = false; 447 final Matcher tagMatcher = tagCheckRegexp.matcher(event.getSourceName()); 448 if (tagMatcher.find()) { 449 if (tagMessageRegexp == null) { 450 match = true; 451 } 452 else { 453 final Matcher messageMatcher = tagMessageRegexp.matcher(event.getMessage()); 454 match = messageMatcher.find(); 455 } 456 } 457 else if (event.getModuleId() != null) { 458 final Matcher idMatcher = tagCheckRegexp.matcher(event.getModuleId()); 459 match = idMatcher.find(); 460 } 461 return match; 462 } 463 464 @Override 465 public String toString() { 466 return "Tag[text='" + text + '\'' 467 + ", line=" + line 468 + ", column=" + column 469 + ", type=" + tagType 470 + ", tagCheckRegexp=" + tagCheckRegexp 471 + ", tagMessageRegexp=" + tagMessageRegexp + ']'; 472 } 473 474 } 475 476}