View Javadoc
1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2017 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.filters;
21  
22  import java.lang.ref.WeakReference;
23  import java.util.ArrayList;
24  import java.util.Collection;
25  import java.util.Collections;
26  import java.util.List;
27  import java.util.Objects;
28  import java.util.regex.Matcher;
29  import java.util.regex.Pattern;
30  import java.util.regex.PatternSyntaxException;
31  
32  import com.puppycrawl.tools.checkstyle.TreeWalkerAuditEvent;
33  import com.puppycrawl.tools.checkstyle.TreeWalkerFilter;
34  import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
35  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
36  import com.puppycrawl.tools.checkstyle.api.FileContents;
37  import com.puppycrawl.tools.checkstyle.api.TextBlock;
38  import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
39  
40  /**
41   * <p>
42   * A filter that uses comments to suppress audit events.
43   * </p>
44   * <p>
45   * Rationale:
46   * Sometimes there are legitimate reasons for violating a check.  When
47   * this is a matter of the code in question and not personal
48   * preference, the best place to override the policy is in the code
49   * itself.  Semi-structured comments can be associated with the check.
50   * This is sometimes superior to a separate suppressions file, which
51   * must be kept up-to-date as the source file is edited.
52   * </p>
53   * @author Mike McMahon
54   * @author Rick Giles
55   */
56  public class SuppressionCommentFilter
57      extends AutomaticBean
58      implements TreeWalkerFilter {
59  
60      /**
61       * Enum to be used for switching checkstyle reporting for tags.
62       */
63      public enum TagType {
64          /**
65           * Switch reporting on.
66           */
67          ON,
68          /**
69           * Switch reporting off.
70           */
71          OFF
72      }
73  
74      /** Turns checkstyle reporting off. */
75      private static final String DEFAULT_OFF_FORMAT = "CHECKSTYLE:OFF";
76  
77      /** Turns checkstyle reporting on. */
78      private static final String DEFAULT_ON_FORMAT = "CHECKSTYLE:ON";
79  
80      /** Control all checks. */
81      private static final String DEFAULT_CHECK_FORMAT = ".*";
82  
83      /** Tagged comments. */
84      private final List<Tag> tags = new ArrayList<>();
85  
86      /** Whether to look in comments of the C type. */
87      private boolean checkC = true;
88  
89      /** Whether to look in comments of the C++ type. */
90      // -@cs[AbbreviationAsWordInName] we can not change it as,
91      // Check property is a part of API (used in configurations)
92      private boolean checkCPP = true;
93  
94      /** Parsed comment regexp that turns checkstyle reporting off. */
95      private Pattern offCommentFormat = Pattern.compile(DEFAULT_OFF_FORMAT);
96  
97      /** Parsed comment regexp that turns checkstyle reporting on. */
98      private Pattern onCommentFormat = Pattern.compile(DEFAULT_ON_FORMAT);
99  
100     /** The check format to suppress. */
101     private String checkFormat = DEFAULT_CHECK_FORMAT;
102 
103     /** The message format to suppress. */
104     private String messageFormat;
105 
106     /**
107      * References the current FileContents for this filter.
108      * Since this is a weak reference to the FileContents, the FileContents
109      * can be reclaimed as soon as the strong references in TreeWalker
110      * are reassigned to the next FileContents, at which time filtering for
111      * the current FileContents is finished.
112      */
113     private WeakReference<FileContents> fileContentsReference = new WeakReference<>(null);
114 
115     /**
116      * Set the format for a comment that turns off reporting.
117      * @param pattern a pattern.
118      */
119     public final void setOffCommentFormat(Pattern pattern) {
120         offCommentFormat = pattern;
121     }
122 
123     /**
124      * Set the format for a comment that turns on reporting.
125      * @param pattern a pattern.
126      */
127     public final void setOnCommentFormat(Pattern pattern) {
128         onCommentFormat = pattern;
129     }
130 
131     /**
132      * Returns FileContents for this filter.
133      * @return the FileContents for this filter.
134      */
135     private FileContents getFileContents() {
136         return fileContentsReference.get();
137     }
138 
139     /**
140      * Set the FileContents for this filter.
141      * @param fileContents the FileContents for this filter.
142      * @noinspection WeakerAccess
143      */
144     public void setFileContents(FileContents fileContents) {
145         fileContentsReference = new WeakReference<>(fileContents);
146     }
147 
148     /**
149      * Set the format for a check.
150      * @param format a {@code String} value
151      */
152     public final void setCheckFormat(String format) {
153         checkFormat = format;
154     }
155 
156     /**
157      * Set the format for a message.
158      * @param format a {@code String} value
159      */
160     public void setMessageFormat(String format) {
161         messageFormat = format;
162     }
163 
164     /**
165      * Set whether to look in C++ comments.
166      * @param checkCpp {@code true} if C++ comments are checked.
167      */
168     // -@cs[AbbreviationAsWordInName] We can not change it as,
169     // check's property is a part of API (used in configurations).
170     public void setCheckCPP(boolean checkCpp) {
171         checkCPP = checkCpp;
172     }
173 
174     /**
175      * Set whether to look in C comments.
176      * @param checkC {@code true} if C comments are checked.
177      */
178     public void setCheckC(boolean checkC) {
179         this.checkC = checkC;
180     }
181 
182     @Override
183     protected void finishLocalSetup() throws CheckstyleException {
184         // No code by default
185     }
186 
187     @Override
188     public boolean accept(TreeWalkerAuditEvent event) {
189         boolean accepted = true;
190 
191         if (event.getLocalizedMessage() != null) {
192             // Lazy update. If the first event for the current file, update file
193             // contents and tag suppressions
194             final FileContents currentContents = event.getFileContents();
195 
196             if (getFileContents() != currentContents) {
197                 setFileContents(currentContents);
198                 tagSuppressions();
199             }
200             final Tag matchTag = findNearestMatch(event);
201             accepted = matchTag == null || matchTag.getTagType() == TagType.ON;
202         }
203         return accepted;
204     }
205 
206     /**
207      * Finds the nearest comment text tag that matches an audit event.
208      * The nearest tag is before the line and column of the event.
209      * @param event the {@code TreeWalkerAuditEvent} to match.
210      * @return The {@code Tag} nearest event.
211      */
212     private Tag findNearestMatch(TreeWalkerAuditEvent event) {
213         Tag result = null;
214         for (Tag tag : tags) {
215             if (tag.getLine() > event.getLine()
216                 || tag.getLine() == event.getLine()
217                     && tag.getColumn() > event.getColumn()) {
218                 break;
219             }
220             if (tag.isMatch(event)) {
221                 result = tag;
222             }
223         }
224         return result;
225     }
226 
227     /**
228      * Collects all the suppression tags for all comments into a list and
229      * sorts the list.
230      */
231     private void tagSuppressions() {
232         tags.clear();
233         final FileContents contents = getFileContents();
234         if (checkCPP) {
235             tagSuppressions(contents.getSingleLineComments().values());
236         }
237         if (checkC) {
238             final Collection<List<TextBlock>> cComments = contents
239                     .getBlockComments().values();
240             cComments.forEach(this::tagSuppressions);
241         }
242         Collections.sort(tags);
243     }
244 
245     /**
246      * Appends the suppressions in a collection of comments to the full
247      * set of suppression tags.
248      * @param comments the set of comments.
249      */
250     private void tagSuppressions(Collection<TextBlock> comments) {
251         for (TextBlock comment : comments) {
252             final int startLineNo = comment.getStartLineNo();
253             final String[] text = comment.getText();
254             tagCommentLine(text[0], startLineNo, comment.getStartColNo());
255             for (int i = 1; i < text.length; i++) {
256                 tagCommentLine(text[i], startLineNo + i, 0);
257             }
258         }
259     }
260 
261     /**
262      * Tags a string if it matches the format for turning
263      * checkstyle reporting on or the format for turning reporting off.
264      * @param text the string to tag.
265      * @param line the line number of text.
266      * @param column the column number of text.
267      */
268     private void tagCommentLine(String text, int line, int column) {
269         final Matcher offMatcher = offCommentFormat.matcher(text);
270         if (offMatcher.find()) {
271             addTag(offMatcher.group(0), line, column, TagType.OFF);
272         }
273         else {
274             final Matcher onMatcher = onCommentFormat.matcher(text);
275             if (onMatcher.find()) {
276                 addTag(onMatcher.group(0), line, column, TagType.ON);
277             }
278         }
279     }
280 
281     /**
282      * Adds a {@code Tag} to the list of all tags.
283      * @param text the text of the tag.
284      * @param line the line number of the tag.
285      * @param column the column number of the tag.
286      * @param reportingOn {@code true} if the tag turns checkstyle reporting on.
287      */
288     private void addTag(String text, int line, int column, TagType reportingOn) {
289         final Tag tag = new Tag(line, column, text, reportingOn, this);
290         tags.add(tag);
291     }
292 
293     /**
294      * A Tag holds a suppression comment and its location, and determines
295      * whether the suppression turns checkstyle reporting on or off.
296      * @author Rick Giles
297      */
298     public static class Tag
299         implements Comparable<Tag> {
300         /** The text of the tag. */
301         private final String text;
302 
303         /** The line number of the tag. */
304         private final int line;
305 
306         /** The column number of the tag. */
307         private final int column;
308 
309         /** Determines whether the suppression turns checkstyle reporting on. */
310         private final TagType tagType;
311 
312         /** The parsed check regexp, expanded for the text of this tag. */
313         private final Pattern tagCheckRegexp;
314 
315         /** The parsed message regexp, expanded for the text of this tag. */
316         private final Pattern tagMessageRegexp;
317 
318         /**
319          * Constructs a tag.
320          * @param line the line number.
321          * @param column the column number.
322          * @param text the text of the suppression.
323          * @param tagType {@code ON} if the tag turns checkstyle reporting.
324          * @param filter the {@code SuppressionCommentFilter} with the context
325          * @throws IllegalArgumentException if unable to parse expanded text.
326          */
327         public Tag(int line, int column, String text, TagType tagType,
328                    SuppressionCommentFilter filter) {
329             this.line = line;
330             this.column = column;
331             this.text = text;
332             this.tagType = tagType;
333 
334             //Expand regexp for check and message
335             //Does not intern Patterns with Utils.getPattern()
336             String format = "";
337             try {
338                 if (this.tagType == TagType.ON) {
339                     format = CommonUtils.fillTemplateWithStringsByRegexp(
340                             filter.checkFormat, text, filter.onCommentFormat);
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.onCommentFormat);
348                         tagMessageRegexp = Pattern.compile(format);
349                     }
350                 }
351                 else {
352                     format = CommonUtils.fillTemplateWithStringsByRegexp(
353                             filter.checkFormat, text, filter.offCommentFormat);
354                     tagCheckRegexp = Pattern.compile(format);
355                     if (filter.messageFormat == null) {
356                         tagMessageRegexp = null;
357                     }
358                     else {
359                         format = CommonUtils.fillTemplateWithStringsByRegexp(
360                                 filter.messageFormat, text, filter.offCommentFormat);
361                         tagMessageRegexp = Pattern.compile(format);
362                     }
363                 }
364             }
365             catch (final PatternSyntaxException ex) {
366                 throw new IllegalArgumentException(
367                     "unable to parse expanded comment " + format, ex);
368             }
369         }
370 
371         /**
372          * Returns line number of the tag in the source file.
373          * @return the line number of the tag in the source file.
374          */
375         public int getLine() {
376             return line;
377         }
378 
379         /**
380          * Determines the column number of the tag in the source file.
381          * Will be 0 for all lines of multiline comment, except the
382          * first line.
383          * @return the column number of the tag in the source file.
384          */
385         public int getColumn() {
386             return column;
387         }
388 
389         /**
390          * Determines whether the suppression turns checkstyle reporting on or
391          * off.
392          * @return {@code ON} if the suppression turns reporting on.
393          */
394         public TagType getTagType() {
395             return tagType;
396         }
397 
398         /**
399          * Compares the position of this tag in the file
400          * with the position of another tag.
401          * @param object the tag to compare with this one.
402          * @return a negative number if this tag is before the other tag,
403          *     0 if they are at the same position, and a positive number if this
404          *     tag is after the other tag.
405          */
406         @Override
407         public int compareTo(Tag object) {
408             final int result;
409             if (line == object.line) {
410                 result = Integer.compare(column, object.column);
411             }
412             else {
413                 result = Integer.compare(line, object.line);
414             }
415             return result;
416         }
417 
418         @Override
419         public boolean equals(Object other) {
420             if (this == other) {
421                 return true;
422             }
423             if (other == null || getClass() != other.getClass()) {
424                 return false;
425             }
426             final Tag tag = (Tag) other;
427             return Objects.equals(line, tag.line)
428                     && Objects.equals(column, tag.column)
429                     && Objects.equals(tagType, tag.tagType)
430                     && Objects.equals(text, tag.text)
431                     && Objects.equals(tagCheckRegexp, tag.tagCheckRegexp)
432                     && Objects.equals(tagMessageRegexp, tag.tagMessageRegexp);
433         }
434 
435         @Override
436         public int hashCode() {
437             return Objects.hash(text, line, column, tagType, tagCheckRegexp, tagMessageRegexp);
438         }
439 
440         /**
441          * Determines whether the source of an audit event
442          * matches the text of this tag.
443          * @param event the {@code TreeWalkerAuditEvent} to check.
444          * @return true if the source of event matches the text of this tag.
445          */
446         public boolean isMatch(TreeWalkerAuditEvent event) {
447             boolean match = false;
448             final Matcher tagMatcher = tagCheckRegexp.matcher(event.getSourceName());
449             if (tagMatcher.find()) {
450                 if (tagMessageRegexp == null) {
451                     match = true;
452                 }
453                 else {
454                     final Matcher messageMatcher = tagMessageRegexp.matcher(event.getMessage());
455                     match = messageMatcher.find();
456                 }
457             }
458             else if (event.getModuleId() != null) {
459                 final Matcher idMatcher = tagCheckRegexp.matcher(event.getModuleId());
460                 match = idMatcher.find();
461             }
462             return match;
463         }
464 
465         @Override
466         public String toString() {
467             return "Tag[text='" + text + '\''
468                     + ", line=" + line
469                     + ", column=" + column
470                     + ", type=" + tagType
471                     + ", tagCheckRegexp=" + tagCheckRegexp
472                     + ", tagMessageRegexp=" + tagMessageRegexp + ']';
473         }
474     }
475 }