001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2016 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.Collection; 024import java.util.Collections; 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 org.apache.commons.beanutils.ConversionException; 032 033import com.google.common.collect.Lists; 034import com.puppycrawl.tools.checkstyle.api.AuditEvent; 035import com.puppycrawl.tools.checkstyle.api.AutomaticBean; 036import com.puppycrawl.tools.checkstyle.api.FileContents; 037import com.puppycrawl.tools.checkstyle.api.Filter; 038import com.puppycrawl.tools.checkstyle.api.TextBlock; 039import com.puppycrawl.tools.checkstyle.checks.FileContentsHolder; 040import com.puppycrawl.tools.checkstyle.utils.CommonUtils; 041 042/** 043 * <p> 044 * A filter that uses comments to suppress audit events. 045 * </p> 046 * <p> 047 * Rationale: 048 * Sometimes there are legitimate reasons for violating a check. When 049 * this is a matter of the code in question and not personal 050 * preference, the best place to override the policy is in the code 051 * itself. Semi-structured comments can be associated with the check. 052 * This is sometimes superior to a separate suppressions file, which 053 * must be kept up-to-date as the source file is edited. 054 * </p> 055 * <p> 056 * Usage: 057 * This check only works in conjunction with the FileContentsHolder module 058 * since that module makes the suppression comments in the .java 059 * files available <i>sub rosa</i>. 060 * </p> 061 * @author Mike McMahon 062 * @author Rick Giles 063 * @see FileContentsHolder 064 */ 065public class SuppressionCommentFilter 066 extends AutomaticBean 067 implements Filter { 068 069 /** Turns checkstyle reporting off. */ 070 private static final String DEFAULT_OFF_FORMAT = "CHECKSTYLE\\:OFF"; 071 072 /** Turns checkstyle reporting on. */ 073 private static final String DEFAULT_ON_FORMAT = "CHECKSTYLE\\:ON"; 074 075 /** Control all checks. */ 076 private static final String DEFAULT_CHECK_FORMAT = ".*"; 077 078 /** Tagged comments. */ 079 private final List<Tag> tags = Lists.newArrayList(); 080 081 /** Whether to look in comments of the C type. */ 082 private boolean checkC = true; 083 084 /** Whether to look in comments of the C++ type. */ 085 private boolean checkCPP = true; 086 087 /** Parsed comment regexp that turns checkstyle reporting off. */ 088 private Pattern offRegexp; 089 090 /** Parsed comment regexp that turns checkstyle reporting on. */ 091 private Pattern onRegexp; 092 093 /** The check format to suppress. */ 094 private String checkFormat; 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 * Constructs a SuppressionCommentFilter. 110 * Initializes comment on, comment off, and check formats 111 * to defaults. 112 */ 113 public SuppressionCommentFilter() { 114 setOnCommentFormat(DEFAULT_ON_FORMAT); 115 setOffCommentFormat(DEFAULT_OFF_FORMAT); 116 checkFormat = DEFAULT_CHECK_FORMAT; 117 } 118 119 /** 120 * Set the format for a comment that turns off reporting. 121 * @param format a {@code String} value. 122 * @throws ConversionException if unable to create Pattern object. 123 */ 124 public final void setOffCommentFormat(String format) { 125 offRegexp = CommonUtils.createPattern(format); 126 } 127 128 /** 129 * Set the format for a comment that turns on reporting. 130 * @param format a {@code String} value 131 * @throws ConversionException if unable to create Pattern object. 132 */ 133 public final void setOnCommentFormat(String format) { 134 onRegexp = CommonUtils.createPattern(format); 135 } 136 137 /** 138 * @return the FileContents for this filter. 139 */ 140 public FileContents getFileContents() { 141 return fileContentsReference.get(); 142 } 143 144 /** 145 * Set the FileContents for this filter. 146 * @param fileContents the FileContents for this filter. 147 */ 148 public void setFileContents(FileContents fileContents) { 149 fileContentsReference = new WeakReference<>(fileContents); 150 } 151 152 /** 153 * Set the format for a check. 154 * @param format a {@code String} value 155 */ 156 public final void setCheckFormat(String format) { 157 checkFormat = format; 158 } 159 160 /** 161 * Set the format for a message. 162 * @param format a {@code String} value 163 */ 164 public void setMessageFormat(String format) { 165 messageFormat = format; 166 } 167 168 /** 169 * Set whether to look in C++ comments. 170 * @param checkCpp {@code true} if C++ comments are checked. 171 */ 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 public boolean accept(AuditEvent event) { 186 boolean accepted = true; 187 188 if (event.getLocalizedMessage() != null) { 189 // Lazy update. If the first event for the current file, update file 190 // contents and tag suppressions 191 final FileContents currentContents = FileContentsHolder.getContents(); 192 193 if (currentContents != null) { 194 if (getFileContents() != currentContents) { 195 setFileContents(currentContents); 196 tagSuppressions(); 197 } 198 final Tag matchTag = findNearestMatch(event); 199 accepted = matchTag == null || matchTag.isReportingOn(); 200 } 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 AuditEvent} to match. 209 * @return The {@code Tag} nearest event. 210 */ 211 private Tag findNearestMatch(AuditEvent 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.getCppComments().values()); 235 } 236 if (checkC) { 237 final Collection<List<TextBlock>> cComments = contents 238 .getCComments().values(); 239 for (List<TextBlock> element : cComments) { 240 tagSuppressions(element); 241 } 242 } 243 Collections.sort(tags); 244 } 245 246 /** 247 * Appends the suppressions in a collection of comments to the full 248 * set of suppression tags. 249 * @param comments the set of comments. 250 */ 251 private void tagSuppressions(Collection<TextBlock> comments) { 252 for (TextBlock comment : comments) { 253 final int startLineNo = comment.getStartLineNo(); 254 final String[] text = comment.getText(); 255 tagCommentLine(text[0], startLineNo, comment.getStartColNo()); 256 for (int i = 1; i < text.length; i++) { 257 tagCommentLine(text[i], startLineNo + i, 0); 258 } 259 } 260 } 261 262 /** 263 * Tags a string if it matches the format for turning 264 * checkstyle reporting on or the format for turning reporting off. 265 * @param text the string to tag. 266 * @param line the line number of text. 267 * @param column the column number of text. 268 */ 269 private void tagCommentLine(String text, int line, int column) { 270 final Matcher offMatcher = offRegexp.matcher(text); 271 if (offMatcher.find()) { 272 addTag(offMatcher.group(0), line, column, false); 273 } 274 else { 275 final Matcher onMatcher = onRegexp.matcher(text); 276 if (onMatcher.find()) { 277 addTag(onMatcher.group(0), line, column, true); 278 } 279 } 280 } 281 282 /** 283 * Adds a {@code Tag} to the list of all tags. 284 * @param text the text of the tag. 285 * @param line the line number of the tag. 286 * @param column the column number of the tag. 287 * @param reportingOn {@code true} if the tag turns checkstyle reporting on. 288 */ 289 private void addTag(String text, int line, int column, boolean reportingOn) { 290 final Tag tag = new Tag(line, column, text, reportingOn, this); 291 tags.add(tag); 292 } 293 294 /** 295 * A Tag holds a suppression comment and its location, and determines 296 * whether the suppression turns checkstyle reporting on or off. 297 * @author Rick Giles 298 */ 299 public static class Tag 300 implements Comparable<Tag> { 301 /** The text of the tag. */ 302 private final String text; 303 304 /** The line number of the tag. */ 305 private final int line; 306 307 /** The column number of the tag. */ 308 private final int column; 309 310 /** Determines whether the suppression turns checkstyle reporting on. */ 311 private final boolean reportingOn; 312 313 /** The parsed check regexp, expanded for the text of this tag. */ 314 private final Pattern tagCheckRegexp; 315 316 /** The parsed message regexp, expanded for the text of this tag. */ 317 private final Pattern tagMessageRegexp; 318 319 /** 320 * Constructs a tag. 321 * @param line the line number. 322 * @param column the column number. 323 * @param text the text of the suppression. 324 * @param reportingOn {@code true} if the tag turns checkstyle reporting. 325 * @param filter the {@code SuppressionCommentFilter} with the context 326 * @throws ConversionException if unable to parse expanded text. 327 */ 328 public Tag(int line, int column, String text, boolean reportingOn, 329 SuppressionCommentFilter filter) { 330 this.line = line; 331 this.column = column; 332 this.text = text; 333 this.reportingOn = reportingOn; 334 335 //Expand regexp for check and message 336 //Does not intern Patterns with Utils.getPattern() 337 String format = ""; 338 try { 339 if (reportingOn) { 340 format = CommonUtils.fillTemplateWithStringsByRegexp( 341 filter.checkFormat, text, filter.onRegexp); 342 tagCheckRegexp = Pattern.compile(format); 343 if (filter.messageFormat == null) { 344 tagMessageRegexp = null; 345 } 346 else { 347 format = CommonUtils.fillTemplateWithStringsByRegexp( 348 filter.messageFormat, text, filter.onRegexp); 349 tagMessageRegexp = Pattern.compile(format); 350 } 351 } 352 else { 353 format = CommonUtils.fillTemplateWithStringsByRegexp( 354 filter.checkFormat, text, filter.offRegexp); 355 tagCheckRegexp = Pattern.compile(format); 356 if (filter.messageFormat == null) { 357 tagMessageRegexp = null; 358 } 359 else { 360 format = CommonUtils.fillTemplateWithStringsByRegexp( 361 filter.messageFormat, text, filter.offRegexp); 362 tagMessageRegexp = Pattern.compile(format); 363 } 364 } 365 } 366 catch (final PatternSyntaxException ex) { 367 throw new ConversionException( 368 "unable to parse expanded comment " + format, 369 ex); 370 } 371 } 372 373 /** 374 * @return the line number of the tag in the source file. 375 */ 376 public int getLine() { 377 return line; 378 } 379 380 /** 381 * Determines the column number of the tag in the source file. 382 * Will be 0 for all lines of multiline comment, except the 383 * first line. 384 * @return the column number of the tag in the source file. 385 */ 386 public int getColumn() { 387 return column; 388 } 389 390 /** 391 * Determines whether the suppression turns checkstyle reporting on or 392 * off. 393 * @return {@code true}if the suppression turns reporting on. 394 */ 395 public boolean isReportingOn() { 396 return reportingOn; 397 } 398 399 /** 400 * Compares the position of this tag in the file 401 * with the position of another tag. 402 * @param object the tag to compare with this one. 403 * @return a negative number if this tag is before the other tag, 404 * 0 if they are at the same position, and a positive number if this 405 * tag is after the other tag. 406 */ 407 @Override 408 public int compareTo(Tag object) { 409 if (line == object.line) { 410 return Integer.compare(column, object.column); 411 } 412 413 return Integer.compare(line, object.line); 414 } 415 416 @Override 417 public boolean equals(Object other) { 418 if (this == other) { 419 return true; 420 } 421 if (other == null || getClass() != other.getClass()) { 422 return false; 423 } 424 final Tag tag = (Tag) other; 425 return Objects.equals(line, tag.line) 426 && Objects.equals(column, tag.column) 427 && Objects.equals(reportingOn, tag.reportingOn) 428 && Objects.equals(text, tag.text) 429 && Objects.equals(tagCheckRegexp, tag.tagCheckRegexp) 430 && Objects.equals(tagMessageRegexp, tag.tagMessageRegexp); 431 } 432 433 @Override 434 public int hashCode() { 435 return Objects.hash(text, line, column, reportingOn, tagCheckRegexp, tagMessageRegexp); 436 } 437 438 /** 439 * Determines whether the source of an audit event 440 * matches the text of this tag. 441 * @param event the {@code AuditEvent} to check. 442 * @return true if the source of event matches the text of this tag. 443 */ 444 public boolean isMatch(AuditEvent event) { 445 boolean match = false; 446 final Matcher tagMatcher = tagCheckRegexp.matcher(event.getSourceName()); 447 if (tagMatcher.find()) { 448 if (tagMessageRegexp == null) { 449 match = true; 450 } 451 else { 452 final Matcher messageMatcher = tagMessageRegexp.matcher(event.getMessage()); 453 match = messageMatcher.find(); 454 } 455 } 456 else if (event.getModuleId() != null) { 457 final Matcher idMatcher = tagCheckRegexp.matcher(event.getModuleId()); 458 match = idMatcher.find(); 459 } 460 return match; 461 } 462 463 @Override 464 public final String toString() { 465 return "Tag[line=" + line + "; col=" + column 466 + "; on=" + reportingOn + "; text='" + text + "']"; 467 } 468 } 469}