View Javadoc
1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2018 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.FileContents;
36  import com.puppycrawl.tools.checkstyle.api.TextBlock;
37  import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
38  
39  /**
40   * <p>
41   * A filter that uses comments to suppress audit events.
42   * </p>
43   * <p>
44   * Rationale:
45   * Sometimes there are legitimate reasons for violating a check.  When
46   * this is a matter of the code in question and not personal
47   * preference, the best place to override the policy is in the code
48   * itself.  Semi-structured comments can be associated with the check.
49   * This is sometimes superior to a separate suppressions file, which
50   * must be kept up-to-date as the source file is edited.
51   * </p>
52   * @author Mike McMahon
53   * @author Rick Giles
54   */
55  public class SuppressionCommentFilter
56      extends AutomaticBean
57      implements TreeWalkerFilter {
58  
59      /**
60       * Enum to be used for switching checkstyle reporting for tags.
61       */
62      public enum TagType {
63  
64          /**
65           * Switch reporting on.
66           */
67          ON,
68          /**
69           * Switch reporting off.
70           */
71          OFF
72  
73      }
74  
75      /** Turns checkstyle reporting off. */
76      private static final String DEFAULT_OFF_FORMAT = "CHECKSTYLE:OFF";
77  
78      /** Turns checkstyle reporting on. */
79      private static final String DEFAULT_ON_FORMAT = "CHECKSTYLE:ON";
80  
81      /** Control all checks. */
82      private static final String DEFAULT_CHECK_FORMAT = ".*";
83  
84      /** Tagged comments. */
85      private final List<Tag> tags = new ArrayList<>();
86  
87      /** Whether to look in comments of the C type. */
88      private boolean checkC = true;
89  
90      /** Whether to look in comments of the C++ type. */
91      // -@cs[AbbreviationAsWordInName] we can not change it as,
92      // Check property is a part of API (used in configurations)
93      private boolean checkCPP = true;
94  
95      /** Parsed comment regexp that turns checkstyle reporting off. */
96      private Pattern offCommentFormat = Pattern.compile(DEFAULT_OFF_FORMAT);
97  
98      /** Parsed comment regexp that turns checkstyle reporting on. */
99      private Pattern onCommentFormat = Pattern.compile(DEFAULT_ON_FORMAT);
100 
101     /** The check format to suppress. */
102     private String checkFormat = DEFAULT_CHECK_FORMAT;
103 
104     /** The message format to suppress. */
105     private String messageFormat;
106 
107     /**
108      * References the current FileContents for this filter.
109      * Since this is a weak reference to the FileContents, the FileContents
110      * can be reclaimed as soon as the strong references in TreeWalker
111      * are reassigned to the next FileContents, at which time filtering for
112      * the current FileContents is finished.
113      */
114     private WeakReference<FileContents> fileContentsReference = new WeakReference<>(null);
115 
116     /**
117      * Set the format for a comment that turns off reporting.
118      * @param pattern a pattern.
119      */
120     public final void setOffCommentFormat(Pattern pattern) {
121         offCommentFormat = pattern;
122     }
123 
124     /**
125      * Set the format for a comment that turns on reporting.
126      * @param pattern a pattern.
127      */
128     public final void setOnCommentFormat(Pattern pattern) {
129         onCommentFormat = pattern;
130     }
131 
132     /**
133      * Returns FileContents for this filter.
134      * @return the FileContents for this filter.
135      */
136     private FileContents getFileContents() {
137         return fileContentsReference.get();
138     }
139 
140     /**
141      * Set the FileContents for this filter.
142      * @param fileContents the FileContents for this filter.
143      * @noinspection WeakerAccess
144      */
145     public void setFileContents(FileContents fileContents) {
146         fileContentsReference = new WeakReference<>(fileContents);
147     }
148 
149     /**
150      * Set the format for a check.
151      * @param format a {@code String} value
152      */
153     public final void setCheckFormat(String format) {
154         checkFormat = format;
155     }
156 
157     /**
158      * Set the format for a message.
159      * @param format a {@code String} value
160      */
161     public void setMessageFormat(String format) {
162         messageFormat = format;
163     }
164 
165     /**
166      * Set whether to look in C++ comments.
167      * @param checkCpp {@code true} if C++ comments are checked.
168      */
169     // -@cs[AbbreviationAsWordInName] We can not change it as,
170     // check's property is a part of API (used in configurations).
171     public void setCheckCPP(boolean checkCpp) {
172         checkCPP = checkCpp;
173     }
174 
175     /**
176      * Set whether to look in C comments.
177      * @param checkC {@code true} if C comments are checked.
178      */
179     public void setCheckC(boolean checkC) {
180         this.checkC = checkC;
181     }
182 
183     @Override
184     protected void finishLocalSetup() {
185         // No code by default
186     }
187 
188     @Override
189     public boolean accept(TreeWalkerAuditEvent event) {
190         boolean accepted = true;
191 
192         if (event.getLocalizedMessage() != null) {
193             // Lazy update. If the first event for the current file, update file
194             // contents and tag suppressions
195             final FileContents currentContents = event.getFileContents();
196 
197             if (getFileContents() != currentContents) {
198                 setFileContents(currentContents);
199                 tagSuppressions();
200             }
201             final Tag matchTag = findNearestMatch(event);
202             accepted = matchTag == null || matchTag.getTagType() == TagType.ON;
203         }
204         return accepted;
205     }
206 
207     /**
208      * Finds the nearest comment text tag that matches an audit event.
209      * The nearest tag is before the line and column of the event.
210      * @param event the {@code TreeWalkerAuditEvent} to match.
211      * @return The {@code Tag} nearest event.
212      */
213     private Tag findNearestMatch(TreeWalkerAuditEvent event) {
214         Tag result = null;
215         for (Tag tag : tags) {
216             if (tag.getLine() > event.getLine()
217                 || tag.getLine() == event.getLine()
218                     && tag.getColumn() > event.getColumn()) {
219                 break;
220             }
221             if (tag.isMatch(event)) {
222                 result = tag;
223             }
224         }
225         return result;
226     }
227 
228     /**
229      * Collects all the suppression tags for all comments into a list and
230      * sorts the list.
231      */
232     private void tagSuppressions() {
233         tags.clear();
234         final FileContents contents = getFileContents();
235         if (checkCPP) {
236             tagSuppressions(contents.getSingleLineComments().values());
237         }
238         if (checkC) {
239             final Collection<List<TextBlock>> cComments = contents
240                     .getBlockComments().values();
241             cComments.forEach(this::tagSuppressions);
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 = offCommentFormat.matcher(text);
271         if (offMatcher.find()) {
272             addTag(offMatcher.group(0), line, column, TagType.OFF);
273         }
274         else {
275             final Matcher onMatcher = onCommentFormat.matcher(text);
276             if (onMatcher.find()) {
277                 addTag(onMatcher.group(0), line, column, TagType.ON);
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, TagType 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 
302         /** The text of the tag. */
303         private final String text;
304 
305         /** The line number of the tag. */
306         private final int line;
307 
308         /** The column number of the tag. */
309         private final int column;
310 
311         /** Determines whether the suppression turns checkstyle reporting on. */
312         private final TagType tagType;
313 
314         /** The parsed check regexp, expanded for the text of this tag. */
315         private final Pattern tagCheckRegexp;
316 
317         /** The parsed message regexp, expanded for the text of this tag. */
318         private final Pattern tagMessageRegexp;
319 
320         /**
321          * Constructs a tag.
322          * @param line the line number.
323          * @param column the column number.
324          * @param text the text of the suppression.
325          * @param tagType {@code ON} if the tag turns checkstyle reporting.
326          * @param filter the {@code SuppressionCommentFilter} with the context
327          * @throws IllegalArgumentException if unable to parse expanded text.
328          */
329         public Tag(int line, int column, String text, TagType tagType,
330                    SuppressionCommentFilter filter) {
331             this.line = line;
332             this.column = column;
333             this.text = text;
334             this.tagType = tagType;
335 
336             //Expand regexp for check and message
337             //Does not intern Patterns with Utils.getPattern()
338             String format = "";
339             try {
340                 if (this.tagType == TagType.ON) {
341                     format = CommonUtils.fillTemplateWithStringsByRegexp(
342                             filter.checkFormat, text, filter.onCommentFormat);
343                     tagCheckRegexp = Pattern.compile(format);
344                     if (filter.messageFormat == null) {
345                         tagMessageRegexp = null;
346                     }
347                     else {
348                         format = CommonUtils.fillTemplateWithStringsByRegexp(
349                                 filter.messageFormat, text, filter.onCommentFormat);
350                         tagMessageRegexp = Pattern.compile(format);
351                     }
352                 }
353                 else {
354                     format = CommonUtils.fillTemplateWithStringsByRegexp(
355                             filter.checkFormat, text, filter.offCommentFormat);
356                     tagCheckRegexp = Pattern.compile(format);
357                     if (filter.messageFormat == null) {
358                         tagMessageRegexp = null;
359                     }
360                     else {
361                         format = CommonUtils.fillTemplateWithStringsByRegexp(
362                                 filter.messageFormat, text, filter.offCommentFormat);
363                         tagMessageRegexp = Pattern.compile(format);
364                     }
365                 }
366             }
367             catch (final PatternSyntaxException ex) {
368                 throw new IllegalArgumentException(
369                     "unable to parse expanded comment " + format, ex);
370             }
371         }
372 
373         /**
374          * Returns line number of the tag in the source file.
375          * @return the line number of the tag in the source file.
376          */
377         public int getLine() {
378             return line;
379         }
380 
381         /**
382          * Determines the column number of the tag in the source file.
383          * Will be 0 for all lines of multiline comment, except the
384          * first line.
385          * @return the column number of the tag in the source file.
386          */
387         public int getColumn() {
388             return column;
389         }
390 
391         /**
392          * Determines whether the suppression turns checkstyle reporting on or
393          * off.
394          * @return {@code ON} if the suppression turns reporting on.
395          */
396         public TagType getTagType() {
397             return tagType;
398         }
399 
400         /**
401          * Compares the position of this tag in the file
402          * with the position of another tag.
403          * @param object the tag to compare with this one.
404          * @return a negative number if this tag is before the other tag,
405          *     0 if they are at the same position, and a positive number if this
406          *     tag is after the other tag.
407          */
408         @Override
409         public int compareTo(Tag object) {
410             final int result;
411             if (line == object.line) {
412                 result = Integer.compare(column, object.column);
413             }
414             else {
415                 result = Integer.compare(line, object.line);
416             }
417             return result;
418         }
419 
420         @Override
421         public boolean equals(Object other) {
422             if (this == other) {
423                 return true;
424             }
425             if (other == null || getClass() != other.getClass()) {
426                 return false;
427             }
428             final Tag tag = (Tag) other;
429             return Objects.equals(line, tag.line)
430                     && Objects.equals(column, tag.column)
431                     && Objects.equals(tagType, tag.tagType)
432                     && Objects.equals(text, tag.text)
433                     && Objects.equals(tagCheckRegexp, tag.tagCheckRegexp)
434                     && Objects.equals(tagMessageRegexp, tag.tagMessageRegexp);
435         }
436 
437         @Override
438         public int hashCode() {
439             return Objects.hash(text, line, column, tagType, tagCheckRegexp, tagMessageRegexp);
440         }
441 
442         /**
443          * Determines whether the source of an audit event
444          * matches the text of this tag.
445          * @param event the {@code TreeWalkerAuditEvent} to check.
446          * @return true if the source of event matches the text of this tag.
447          */
448         public boolean isMatch(TreeWalkerAuditEvent event) {
449             boolean match = false;
450             final Matcher tagMatcher = tagCheckRegexp.matcher(event.getSourceName());
451             if (tagMatcher.find()) {
452                 if (tagMessageRegexp == null) {
453                     match = true;
454                 }
455                 else {
456                     final Matcher messageMatcher = tagMessageRegexp.matcher(event.getMessage());
457                     match = messageMatcher.find();
458                 }
459             }
460             else if (event.getModuleId() != null) {
461                 final Matcher idMatcher = tagCheckRegexp.matcher(event.getModuleId());
462                 match = idMatcher.find();
463             }
464             return match;
465         }
466 
467         @Override
468         public String toString() {
469             return "Tag[text='" + text + '\''
470                     + ", line=" + line
471                     + ", column=" + column
472                     + ", type=" + tagType
473                     + ", tagCheckRegexp=" + tagCheckRegexp
474                     + ", tagMessageRegexp=" + tagMessageRegexp + ']';
475         }
476 
477     }
478 
479 }