001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2015 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 /** Whether to look in comments of the C type. */ 079 private boolean checkC = true; 080 081 /** Whether to look in comments of the C++ type. */ 082 private boolean checkCPP = true; 083 084 /** Parsed comment regexp that turns checkstyle reporting off. */ 085 private Pattern offRegexp; 086 087 /** Parsed comment regexp that turns checkstyle reporting on. */ 088 private Pattern onRegexp; 089 090 /** The check format to suppress. */ 091 private String checkFormat; 092 093 /** The message format to suppress. */ 094 private String messageFormat; 095 096 /** Tagged comments. */ 097 private final List<Tag> tags = Lists.newArrayList(); 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 this.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.isOn(); 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 on {@code true} if the tag turns checkstyle reporting on. 288 */ 289 private void addTag(String text, int line, int column, boolean on) { 290 final Tag tag = new Tag(line, column, text, on, 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 on; 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 on {@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 on, SuppressionCommentFilter filter) { 329 this.line = line; 330 this.column = column; 331 this.text = text; 332 this.on = on; 333 334 //Expand regexp for check and message 335 //Does not intern Patterns with Utils.getPattern() 336 String format = ""; 337 try { 338 if (on) { 339 format = 340 expandFromComment(text, filter.checkFormat, filter.onRegexp); 341 tagCheckRegexp = Pattern.compile(format); 342 if (filter.messageFormat != null) { 343 format = 344 expandFromComment(text, filter.messageFormat, filter.onRegexp); 345 tagMessageRegexp = Pattern.compile(format); 346 } 347 else { 348 tagMessageRegexp = null; 349 } 350 } 351 else { 352 format = 353 expandFromComment(text, filter.checkFormat, filter.offRegexp); 354 tagCheckRegexp = Pattern.compile(format); 355 if (filter.messageFormat != null) { 356 format = 357 expandFromComment( 358 text, 359 filter.messageFormat, 360 filter.offRegexp); 361 tagMessageRegexp = Pattern.compile(format); 362 } 363 else { 364 tagMessageRegexp = null; 365 } 366 } 367 } 368 catch (final PatternSyntaxException e) { 369 throw new ConversionException( 370 "unable to parse expanded comment " + format, 371 e); 372 } 373 } 374 375 /** 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 true}if the suppression turns reporting on. 396 */ 397 public boolean isOn() { 398 return on; 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 if (line == object.line) { 412 return Integer.compare(column, object.column); 413 } 414 415 return Integer.compare(line, object.line); 416 } 417 418 @Override 419 public boolean equals(Object o) { 420 if (this == o) { 421 return true; 422 } 423 if (o == null || getClass() != o.getClass()) { 424 return false; 425 } 426 final Tag tag = (Tag) o; 427 return Objects.equals(line, tag.line) 428 && Objects.equals(column, tag.column) 429 && Objects.equals(on, tag.on) 430 && Objects.equals(text, tag.text); 431 } 432 433 @Override 434 public int hashCode() { 435 return Objects.hash(text, line, column, on); 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 final Matcher messageMatcher = tagMessageRegexp.matcher(event.getMessage()); 450 match = messageMatcher.find(); 451 } 452 else { 453 match = true; 454 } 455 } 456 return match; 457 } 458 459 /** 460 * Expand based on a matching comment. 461 * @param comment the comment. 462 * @param stringToExpand the string to expand. 463 * @param regexp the parsed expander. 464 * @return the expanded string 465 */ 466 private static String expandFromComment( 467 String comment, 468 String stringToExpand, 469 Pattern regexp) { 470 final Matcher matcher = regexp.matcher(comment); 471 // Match primarily for effect. 472 if (!matcher.find()) { 473 return stringToExpand; 474 } 475 String result = stringToExpand; 476 for (int i = 0; i <= matcher.groupCount(); i++) { 477 // $n expands comment match like in Pattern.subst(). 478 result = result.replaceAll("\\$" + i, matcher.group(i)); 479 } 480 return result; 481 } 482 483 @Override 484 public final String toString() { 485 return "Tag[line=" + line + "; col=" + column 486 + "; on=" + on + "; text='" + text + "']"; 487 } 488 } 489}