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