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