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