1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2019 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.CommonUtil;
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   */
53  public class SuppressionCommentFilter
54      extends AutomaticBean
55      implements TreeWalkerFilter {
56  
57      /**
58       * Enum to be used for switching checkstyle reporting for tags.
59       */
60      public enum TagType {
61  
62          /**
63           * Switch reporting on.
64           */
65          ON,
66          /**
67           * Switch reporting off.
68           */
69          OFF,
70  
71      }
72  
73      /** Turns checkstyle reporting off. */
74      private static final String DEFAULT_OFF_FORMAT = "CHECKSTYLE:OFF";
75  
76      /** Turns checkstyle reporting on. */
77      private static final String DEFAULT_ON_FORMAT = "CHECKSTYLE:ON";
78  
79      /** Control all checks. */
80      private static final String DEFAULT_CHECK_FORMAT = ".*";
81  
82      /** Tagged comments. */
83      private final List<Tag> tags = new ArrayList<>();
84  
85      /** Whether to look in comments of the C type. */
86      private boolean checkC = true;
87  
88      /** Whether to look in comments of the C++ type. */
89      // -@cs[AbbreviationAsWordInName] we can not change it as,
90      // Check property is a part of API (used in configurations)
91      private boolean checkCPP = true;
92  
93      /** Parsed comment regexp that turns checkstyle reporting off. */
94      private Pattern offCommentFormat = Pattern.compile(DEFAULT_OFF_FORMAT);
95  
96      /** Parsed comment regexp that turns checkstyle reporting on. */
97      private Pattern onCommentFormat = Pattern.compile(DEFAULT_ON_FORMAT);
98  
99      /** The check format to suppress. */
100     private String checkFormat = DEFAULT_CHECK_FORMAT;
101 
102     /** The message format to suppress. */
103     private String messageFormat;
104 
105     /**
106      * References the current FileContents for this filter.
107      * Since this is a weak reference to the FileContents, the FileContents
108      * can be reclaimed as soon as the strong references in TreeWalker
109      * are reassigned to the next FileContents, at which time filtering for
110      * the current FileContents is finished.
111      */
112     private WeakReference<FileContents> fileContentsReference = new WeakReference<>(null);
113 
114     /**
115      * Set the format for a comment that turns off reporting.
116      * @param pattern a pattern.
117      */
118     public final void setOffCommentFormat(Pattern pattern) {
119         offCommentFormat = pattern;
120     }
121 
122     /**
123      * Set the format for a comment that turns on reporting.
124      * @param pattern a pattern.
125      */
126     public final void setOnCommentFormat(Pattern pattern) {
127         onCommentFormat = pattern;
128     }
129 
130     /**
131      * Returns FileContents for this filter.
132      * @return the FileContents for this filter.
133      */
134     private FileContents getFileContents() {
135         return fileContentsReference.get();
136     }
137 
138     /**
139      * Set the FileContents for this filter.
140      * @param fileContents the FileContents for this filter.
141      * @noinspection WeakerAccess
142      */
143     public void setFileContents(FileContents fileContents) {
144         fileContentsReference = new WeakReference<>(fileContents);
145     }
146 
147     /**
148      * Set the format for a check.
149      * @param format a {@code String} value
150      */
151     public final void setCheckFormat(String format) {
152         checkFormat = format;
153     }
154 
155     /**
156      * Set the format for a message.
157      * @param format a {@code String} value
158      */
159     public void setMessageFormat(String format) {
160         messageFormat = format;
161     }
162 
163     /**
164      * Set whether to look in C++ comments.
165      * @param checkCpp {@code true} if C++ comments are checked.
166      */
167     // -@cs[AbbreviationAsWordInName] We can not change it as,
168     // check's property is a part of API (used in configurations).
169     public void setCheckCPP(boolean checkCpp) {
170         checkCPP = checkCpp;
171     }
172 
173     /**
174      * Set whether to look in C comments.
175      * @param checkC {@code true} if C comments are checked.
176      */
177     public void setCheckC(boolean checkC) {
178         this.checkC = checkC;
179     }
180 
181     @Override
182     protected void finishLocalSetup() {
183         // No code by default
184     }
185 
186     @Override
187     public boolean accept(TreeWalkerAuditEvent event) {
188         boolean accepted = true;
189 
190         if (event.getLocalizedMessage() != null) {
191             // Lazy update. If the first event for the current file, update file
192             // contents and tag suppressions
193             final FileContents currentContents = event.getFileContents();
194 
195             if (getFileContents() != currentContents) {
196                 setFileContents(currentContents);
197                 tagSuppressions();
198             }
199             final Tag matchTag = findNearestMatch(event);
200             accepted = matchTag == null || matchTag.getTagType() == TagType.ON;
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 TreeWalkerAuditEvent} to match.
209      * @return The {@code Tag} nearest event.
210      */
211     private Tag findNearestMatch(TreeWalkerAuditEvent 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.getSingleLineComments().values());
235         }
236         if (checkC) {
237             final Collection<List<TextBlock>> cComments = contents
238                     .getBlockComments().values();
239             cComments.forEach(this::tagSuppressions);
240         }
241         Collections.sort(tags);
242     }
243 
244     /**
245      * Appends the suppressions in a collection of comments to the full
246      * set of suppression tags.
247      * @param comments the set of comments.
248      */
249     private void tagSuppressions(Collection<TextBlock> comments) {
250         for (TextBlock comment : comments) {
251             final int startLineNo = comment.getStartLineNo();
252             final String[] text = comment.getText();
253             tagCommentLine(text[0], startLineNo, comment.getStartColNo());
254             for (int i = 1; i < text.length; i++) {
255                 tagCommentLine(text[i], startLineNo + i, 0);
256             }
257         }
258     }
259 
260     /**
261      * Tags a string if it matches the format for turning
262      * checkstyle reporting on or the format for turning reporting off.
263      * @param text the string to tag.
264      * @param line the line number of text.
265      * @param column the column number of text.
266      */
267     private void tagCommentLine(String text, int line, int column) {
268         final Matcher offMatcher = offCommentFormat.matcher(text);
269         if (offMatcher.find()) {
270             addTag(offMatcher.group(0), line, column, TagType.OFF);
271         }
272         else {
273             final Matcher onMatcher = onCommentFormat.matcher(text);
274             if (onMatcher.find()) {
275                 addTag(onMatcher.group(0), line, column, TagType.ON);
276             }
277         }
278     }
279 
280     /**
281      * Adds a {@code Tag} to the list of all tags.
282      * @param text the text of the tag.
283      * @param line the line number of the tag.
284      * @param column the column number of the tag.
285      * @param reportingOn {@code true} if the tag turns checkstyle reporting on.
286      */
287     private void addTag(String text, int line, int column, TagType reportingOn) {
288         final Tag tag = new Tag(line, column, text, reportingOn, this);
289         tags.add(tag);
290     }
291 
292     /**
293      * A Tag holds a suppression comment and its location, and determines
294      * whether the suppression turns checkstyle reporting on or off.
295      */
296     public static class Tag
297         implements Comparable<Tag> {
298 
299         /** The text of the tag. */
300         private final String text;
301 
302         /** The line number of the tag. */
303         private final int line;
304 
305         /** The column number of the tag. */
306         private final int column;
307 
308         /** Determines whether the suppression turns checkstyle reporting on. */
309         private final TagType tagType;
310 
311         /** The parsed check regexp, expanded for the text of this tag. */
312         private final Pattern tagCheckRegexp;
313 
314         /** The parsed message regexp, expanded for the text of this tag. */
315         private final Pattern tagMessageRegexp;
316 
317         /**
318          * Constructs a tag.
319          * @param line the line number.
320          * @param column the column number.
321          * @param text the text of the suppression.
322          * @param tagType {@code ON} if the tag turns checkstyle reporting.
323          * @param filter the {@code SuppressionCommentFilter} with the context
324          * @throws IllegalArgumentException if unable to parse expanded text.
325          */
326         public Tag(int line, int column, String text, TagType tagType,
327                    SuppressionCommentFilter filter) {
328             this.line = line;
329             this.column = column;
330             this.text = text;
331             this.tagType = tagType;
332 
333             //Expand regexp for check and message
334             //Does not intern Patterns with Utils.getPattern()
335             String format = "";
336             try {
337                 if (this.tagType == TagType.ON) {
338                     format = CommonUtil.fillTemplateWithStringsByRegexp(
339                             filter.checkFormat, text, filter.onCommentFormat);
340                     tagCheckRegexp = Pattern.compile(format);
341                     if (filter.messageFormat == null) {
342                         tagMessageRegexp = null;
343                     }
344                     else {
345                         format = CommonUtil.fillTemplateWithStringsByRegexp(
346                                 filter.messageFormat, text, filter.onCommentFormat);
347                         tagMessageRegexp = Pattern.compile(format);
348                     }
349                 }
350                 else {
351                     format = CommonUtil.fillTemplateWithStringsByRegexp(
352                             filter.checkFormat, text, filter.offCommentFormat);
353                     tagCheckRegexp = Pattern.compile(format);
354                     if (filter.messageFormat == null) {
355                         tagMessageRegexp = null;
356                     }
357                     else {
358                         format = CommonUtil.fillTemplateWithStringsByRegexp(
359                                 filter.messageFormat, text, filter.offCommentFormat);
360                         tagMessageRegexp = Pattern.compile(format);
361                     }
362                 }
363             }
364             catch (final PatternSyntaxException ex) {
365                 throw new IllegalArgumentException(
366                     "unable to parse expanded comment " + format, ex);
367             }
368         }
369 
370         /**
371          * Returns line number of the tag in the source file.
372          * @return the line number of the tag in the source file.
373          */
374         public int getLine() {
375             return line;
376         }
377 
378         /**
379          * Determines the column number of the tag in the source file.
380          * Will be 0 for all lines of multiline comment, except the
381          * first line.
382          * @return the column number of the tag in the source file.
383          */
384         public int getColumn() {
385             return column;
386         }
387 
388         /**
389          * Determines whether the suppression turns checkstyle reporting on or
390          * off.
391          * @return {@code ON} if the suppression turns reporting on.
392          */
393         public TagType getTagType() {
394             return tagType;
395         }
396 
397         /**
398          * Compares the position of this tag in the file
399          * with the position of another tag.
400          * @param object the tag to compare with this one.
401          * @return a negative number if this tag is before the other tag,
402          *     0 if they are at the same position, and a positive number if this
403          *     tag is after the other tag.
404          */
405         @Override
406         public int compareTo(Tag object) {
407             final int result;
408             if (line == object.line) {
409                 result = Integer.compare(column, object.column);
410             }
411             else {
412                 result = Integer.compare(line, object.line);
413             }
414             return result;
415         }
416 
417         /**
418          * Indicates whether some other object is "equal to" this one.
419          * Suppression on enumeration is needed so code stays consistent.
420          * @noinspection EqualsCalledOnEnumConstant
421          */
422         @Override
423         public boolean equals(Object other) {
424             if (this == other) {
425                 return true;
426             }
427             if (other == null || getClass() != other.getClass()) {
428                 return false;
429             }
430             final Tag tag = (Tag) other;
431             return Objects.equals(line, tag.line)
432                     && Objects.equals(column, tag.column)
433                     && Objects.equals(tagType, tag.tagType)
434                     && Objects.equals(text, tag.text)
435                     && Objects.equals(tagCheckRegexp, tag.tagCheckRegexp)
436                     && Objects.equals(tagMessageRegexp, tag.tagMessageRegexp);
437         }
438 
439         @Override
440         public int hashCode() {
441             return Objects.hash(text, line, column, tagType, tagCheckRegexp, tagMessageRegexp);
442         }
443 
444         /**
445          * Determines whether the source of an audit event
446          * matches the text of this tag.
447          * @param event the {@code TreeWalkerAuditEvent} to check.
448          * @return true if the source of event matches the text of this tag.
449          */
450         public boolean isMatch(TreeWalkerAuditEvent event) {
451             boolean match = false;
452             final Matcher tagMatcher = tagCheckRegexp.matcher(event.getSourceName());
453             if (tagMatcher.find()) {
454                 if (tagMessageRegexp == null) {
455                     match = true;
456                 }
457                 else {
458                     final Matcher messageMatcher = tagMessageRegexp.matcher(event.getMessage());
459                     match = messageMatcher.find();
460                 }
461             }
462             else if (event.getModuleId() != null) {
463                 final Matcher idMatcher = tagCheckRegexp.matcher(event.getModuleId());
464                 match = idMatcher.find();
465             }
466             return match;
467         }
468 
469         @Override
470         public String toString() {
471             return "Tag[text='" + text + '\''
472                     + ", line=" + line
473                     + ", column=" + column
474                     + ", type=" + tagType
475                     + ", tagCheckRegexp=" + tagCheckRegexp
476                     + ", tagMessageRegexp=" + tagMessageRegexp + ']';
477         }
478 
479     }
480 
481 }