//////////////////////////////////////////////////////////////////////////////// // checkstyle: Checks Java source code for adherence to a set of rules. // Copyright (C) 2001-2017 the original author or authors. // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 2.1 of the License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA //////////////////////////////////////////////////////////////////////////////// package com.puppycrawl.tools.checkstyle.filters; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import com.puppycrawl.tools.checkstyle.api.AuditEvent; import com.puppycrawl.tools.checkstyle.api.AutomaticBean; import com.puppycrawl.tools.checkstyle.api.FileContents; import com.puppycrawl.tools.checkstyle.api.Filter; import com.puppycrawl.tools.checkstyle.api.TextBlock; import com.puppycrawl.tools.checkstyle.checks.FileContentsHolder; import com.puppycrawl.tools.checkstyle.utils.CommonUtils; /** * <p> * A filter that uses nearby comments to suppress audit events. * </p> * * <p>This check is philosophically similar to {@link SuppressionCommentFilter}. * Unlike {@link SuppressionCommentFilter}, this filter does not require * pairs of comments. This check may be used to suppress warnings in the * current line: * <pre> * offendingLine(for, whatever, reason); // SUPPRESS ParameterNumberCheck * </pre> * or it may be configured to span multiple lines, either forward: * <pre> * // PERMIT MultipleVariableDeclarations NEXT 3 LINES * double x1 = 1.0, y1 = 0.0, z1 = 0.0; * double x2 = 0.0, y2 = 1.0, z2 = 0.0; * double x3 = 0.0, y3 = 0.0, z3 = 1.0; * </pre> * or reverse: * <pre> * try { * thirdPartyLibrary.method(); * } catch (RuntimeException ex) { * // ALLOW ILLEGAL CATCH BECAUSE third party API wraps everything * // in RuntimeExceptions. * ... * } * </pre> * * <p>See {@link SuppressionCommentFilter} for usage notes. * * @author Mick Killianey */ public class SuppressWithNearbyCommentFilter extends AutomaticBean implements Filter { /** Format to turns checkstyle reporting off. */ private static final String DEFAULT_COMMENT_FORMAT = "SUPPRESS CHECKSTYLE (\\w+)"; /** Default regex for checks that should be suppressed. */ private static final String DEFAULT_CHECK_FORMAT = ".*"; /** Default regex for lines that should be suppressed. */ private static final String DEFAULT_INFLUENCE_FORMAT = "0"; /** Tagged comments. */ private final List<Tag> tags = new ArrayList<>(); /** Whether to look for trigger in C-style comments. */ private boolean checkC = true; /** Whether to look for trigger in C++-style comments. */ // -@cs[AbbreviationAsWordInName] We can not change it as, // check's property is a part of API (used in configurations). private boolean checkCPP = true; /** Parsed comment regexp that marks checkstyle suppression region. */ private Pattern commentFormat = Pattern.compile(DEFAULT_COMMENT_FORMAT); /** The comment pattern that triggers suppression. */ private String checkFormat = DEFAULT_CHECK_FORMAT; /** The message format to suppress. */ private String messageFormat; /** The influence of the suppression comment. */ private String influenceFormat = DEFAULT_INFLUENCE_FORMAT; /** * References the current FileContents for this filter. * Since this is a weak reference to the FileContents, the FileContents * can be reclaimed as soon as the strong references in TreeWalker * and FileContentsHolder are reassigned to the next FileContents, * at which time filtering for the current FileContents is finished. */ private WeakReference<FileContents> fileContentsReference = new WeakReference<>(null); /** * Set the format for a comment that turns off reporting. * @param pattern a pattern. */ public final void setCommentFormat(Pattern pattern) { commentFormat = pattern; } /** * @return the FileContents for this filter. */ public FileContents getFileContents() { return fileContentsReference.get(); } /** * Set the FileContents for this filter. * @param fileContents the FileContents for this filter. */ public void setFileContents(FileContents fileContents) { fileContentsReference = new WeakReference<>(fileContents); } /** * Set the format for a check. * @param format a {@code String} value */ public final void setCheckFormat(String format) { checkFormat = format; } /** * Set the format for a message. * @param format a {@code String} value */ public void setMessageFormat(String format) { messageFormat = format; } /** * Set the format for the influence of this check. * @param format a {@code String} value */ public final void setInfluenceFormat(String format) { influenceFormat = format; } /** * Set whether to look in C++ comments. * @param checkCpp {@code true} if C++ comments are checked. */ // -@cs[AbbreviationAsWordInName] We can not change it as, // check's property is a part of API (used in configurations). public void setCheckCPP(boolean checkCpp) { checkCPP = checkCpp; } /** * Set whether to look in C comments. * @param checkC {@code true} if C comments are checked. */ public void setCheckC(boolean checkC) { this.checkC = checkC; } @Override public boolean accept(AuditEvent event) { boolean accepted = true; if (event.getLocalizedMessage() != null) { // Lazy update. If the first event for the current file, update file // contents and tag suppressions final FileContents currentContents = FileContentsHolder.getCurrentFileContents(); if (getFileContents() != currentContents) { setFileContents(currentContents); tagSuppressions(); } if (matchesTag(event)) { accepted = false; } } return accepted; } /** * Whether current event matches any tag from {@link #tags}. * @param event AuditEvent to test match on {@link #tags}. * @return true if event matches any tag from {@link #tags}, false otherwise. */ private boolean matchesTag(AuditEvent event) { boolean result = false; for (final Tag tag : tags) { if (tag.isMatch(event)) { result = true; break; } } return result; } /** * Collects all the suppression tags for all comments into a list and * sorts the list. */ private void tagSuppressions() { tags.clear(); final FileContents contents = getFileContents(); if (checkCPP) { tagSuppressions(contents.getSingleLineComments().values()); } if (checkC) { final Collection<List<TextBlock>> cComments = contents.getBlockComments().values(); cComments.forEach(this::tagSuppressions); } Collections.sort(tags); } /** * Appends the suppressions in a collection of comments to the full * set of suppression tags. * @param comments the set of comments. */ private void tagSuppressions(Collection<TextBlock> comments) { for (final TextBlock comment : comments) { final int startLineNo = comment.getStartLineNo(); final String[] text = comment.getText(); tagCommentLine(text[0], startLineNo); for (int i = 1; i < text.length; i++) { tagCommentLine(text[i], startLineNo + i); } } } /** * Tags a string if it matches the format for turning * checkstyle reporting on or the format for turning reporting off. * @param text the string to tag. * @param line the line number of text. */ private void tagCommentLine(String text, int line) { final Matcher matcher = commentFormat.matcher(text); if (matcher.find()) { addTag(matcher.group(0), line); } } /** * Adds a comment suppression {@code Tag} to the list of all tags. * @param text the text of the tag. * @param line the line number of the tag. */ private void addTag(String text, int line) { final Tag tag = new Tag(text, line, this); tags.add(tag); } /** * A Tag holds a suppression comment and its location. */ public static class Tag implements Comparable<Tag> { /** The text of the tag. */ private final String text; /** The first line where warnings may be suppressed. */ private final int firstLine; /** The last line where warnings may be suppressed. */ private final int lastLine; /** The parsed check regexp, expanded for the text of this tag. */ private final Pattern tagCheckRegexp; /** The parsed message regexp, expanded for the text of this tag. */ private final Pattern tagMessageRegexp; /** * Constructs a tag. * @param text the text of the suppression. * @param line the line number. * @param filter the {@code SuppressWithNearbyCommentFilter} with the context * @throws IllegalArgumentException if unable to parse expanded text. */ public Tag(String text, int line, SuppressWithNearbyCommentFilter filter) { this.text = text; //Expand regexp for check and message //Does not intern Patterns with Utils.getPattern() String format = ""; try { format = CommonUtils.fillTemplateWithStringsByRegexp( filter.checkFormat, text, filter.commentFormat); tagCheckRegexp = Pattern.compile(format); if (filter.messageFormat == null) { tagMessageRegexp = null; } else { format = CommonUtils.fillTemplateWithStringsByRegexp( filter.messageFormat, text, filter.commentFormat); tagMessageRegexp = Pattern.compile(format); } format = CommonUtils.fillTemplateWithStringsByRegexp( filter.influenceFormat, text, filter.commentFormat); final int influence; try { if (CommonUtils.startsWithChar(format, '+')) { format = format.substring(1); } influence = Integer.parseInt(format); } catch (final NumberFormatException ex) { throw new IllegalArgumentException("unable to parse influence from '" + text + "' using " + filter.influenceFormat, ex); } if (influence >= 0) { firstLine = line; lastLine = line + influence; } else { firstLine = line + influence; lastLine = line; } } catch (final PatternSyntaxException ex) { throw new IllegalArgumentException( "unable to parse expanded comment " + format, ex); } } /** * Compares the position of this tag in the file * with the position of another tag. * @param other the tag to compare with this one. * @return a negative number if this tag is before the other tag, * 0 if they are at the same position, and a positive number if this * tag is after the other tag. */ @Override public int compareTo(Tag other) { final int result; if (firstLine == other.firstLine) { result = Integer.compare(lastLine, other.lastLine); } else { result = Integer.compare(firstLine, other.firstLine); } return result; } @Override public boolean equals(Object other) { if (this == other) { return true; } if (other == null || getClass() != other.getClass()) { return false; } final Tag tag = (Tag) other; return Objects.equals(firstLine, tag.firstLine) && Objects.equals(lastLine, tag.lastLine) && Objects.equals(text, tag.text) && Objects.equals(tagCheckRegexp, tag.tagCheckRegexp) && Objects.equals(tagMessageRegexp, tag.tagMessageRegexp); } @Override public int hashCode() { return Objects.hash(text, firstLine, lastLine, tagCheckRegexp, tagMessageRegexp); } /** * Determines whether the source of an audit event * matches the text of this tag. * @param event the {@code AuditEvent} to check. * @return true if the source of event matches the text of this tag. */ public boolean isMatch(AuditEvent event) { final int line = event.getLine(); boolean match = false; if (line >= firstLine && line <= lastLine) { final Matcher tagMatcher = tagCheckRegexp.matcher(event.getSourceName()); if (tagMatcher.find()) { match = true; } else if (tagMessageRegexp == null) { if (event.getModuleId() != null) { final Matcher idMatcher = tagCheckRegexp.matcher(event.getModuleId()); match = idMatcher.find(); } } else { final Matcher messageMatcher = tagMessageRegexp.matcher(event.getMessage()); match = messageMatcher.find(); } } return match; } @Override public final String toString() { return "Tag[lines=[" + firstLine + " to " + lastLine + "]; text='" + text + "']"; } } }