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.io.File;
23  import java.io.IOException;
24  import java.nio.charset.StandardCharsets;
25  import java.util.ArrayList;
26  import java.util.List;
27  import java.util.Objects;
28  import java.util.Optional;
29  import java.util.regex.Matcher;
30  import java.util.regex.Pattern;
31  import java.util.regex.PatternSyntaxException;
32  
33  import com.puppycrawl.tools.checkstyle.api.AuditEvent;
34  import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
35  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
36  import com.puppycrawl.tools.checkstyle.api.FileText;
37  import com.puppycrawl.tools.checkstyle.api.Filter;
38  import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
39  
40  /**
41   * <p>
42   *     A filter that uses comments to suppress audit events.
43   *     The filter can be used only to suppress audit events received from
44   *     {@link com.puppycrawl.tools.checkstyle.api.FileSetCheck} checks.
45   *     SuppressWithPlainTextCommentFilter knows nothing about AST,
46   *     it treats only plain text comments and extracts the information required for suppression from
47   *     the plain text comments. Currently the filter supports only single line comments.
48   * </p>
49   * <p>
50   *     Rationale:
51   *     Sometimes there are legitimate reasons for violating a check. When
52   *     this is a matter of the code in question and not personal
53   *     preference, the best place to override the policy is in the code
54   *     itself.  Semi-structured comments can be associated with the check.
55   *     This is sometimes superior to a separate suppressions file, which
56   *     must be kept up-to-date as the source file is edited.
57   * </p>
58   * @author Andrei Selkin
59   */
60  public class SuppressWithPlainTextCommentFilter extends AutomaticBean implements Filter {
61  
62      /** Comment format which turns checkstyle reporting off. */
63      private static final String DEFAULT_OFF_FORMAT = "// CHECKSTYLE:OFF";
64  
65      /** Comment format which turns checkstyle reporting on. */
66      private static final String DEFAULT_ON_FORMAT = "// CHECKSTYLE:ON";
67  
68      /** Default check format to suppress. By default the filter suppress all checks. */
69      private static final String DEFAULT_CHECK_FORMAT = ".*";
70  
71      /** Regexp which turns checkstyle reporting off. */
72      private Pattern offCommentFormat = CommonUtils.createPattern(DEFAULT_OFF_FORMAT);
73  
74      /** Regexp which turns checkstyle reporting on. */
75      private Pattern onCommentFormat = CommonUtils.createPattern(DEFAULT_ON_FORMAT);
76  
77      /** The check format to suppress. */
78      private String checkFormat = DEFAULT_CHECK_FORMAT;
79  
80      /** The message format to suppress.*/
81      private String messageFormat;
82  
83      /**
84       * Sets an off comment format pattern.
85       * @param pattern off comment format pattern.
86       */
87      public final void setOffCommentFormat(Pattern pattern) {
88          offCommentFormat = pattern;
89      }
90  
91      /**
92       * Sets an on comment format pattern.
93       * @param pattern  on comment format pattern.
94       */
95      public final void setOnCommentFormat(Pattern pattern) {
96          onCommentFormat = pattern;
97      }
98  
99      /**
100      * Sets a pattern for check format.
101      * @param format pattern for check format.
102      */
103     public final void setCheckFormat(String format) {
104         checkFormat = format;
105     }
106 
107     /**
108      * Sets a pattern for message format.
109      * @param format pattern for message format.
110      */
111     public final void setMessageFormat(String format) {
112         messageFormat = format;
113     }
114 
115     @Override
116     public boolean accept(AuditEvent event) {
117         boolean accepted = true;
118         if (event.getLocalizedMessage() != null) {
119             final FileText fileText = getFileText(event.getFileName());
120             if (fileText != null) {
121                 final List<Suppression> suppressions = getSuppressions(fileText);
122                 accepted = getNearestSuppression(suppressions, event) == null;
123             }
124         }
125         return accepted;
126     }
127 
128     @Override
129     protected void finishLocalSetup() throws CheckstyleException {
130         // No code by default
131     }
132 
133     /**
134      * Returns {@link FileText} instance created based on the given file name.
135      * @param fileName the name of the file.
136      * @return {@link FileText} instance.
137      */
138     private static FileText getFileText(String fileName) {
139         final File file = new File(fileName);
140         FileText result = null;
141 
142         // some violations can be on a directory, instead of a file
143         if (!file.isDirectory()) {
144             try {
145                 result = new FileText(file, StandardCharsets.UTF_8.name());
146             }
147             catch (IOException ex) {
148                 throw new IllegalStateException("Cannot read source file: " + fileName, ex);
149             }
150         }
151 
152         return result;
153     }
154 
155     /**
156      * Returns the list of {@link Suppression} instances retrieved from the given {@link FileText}.
157      * @param fileText {@link FileText} instance.
158      * @return list of {@link Suppression} instances.
159      */
160     private List<Suppression> getSuppressions(FileText fileText) {
161         final List<Suppression> suppressions = new ArrayList<>();
162         for (int lineNo = 0; lineNo < fileText.size(); lineNo++) {
163             final Optional<Suppression> suppression = getSuppression(fileText, lineNo);
164             suppression.ifPresent(suppressions::add);
165         }
166         return suppressions;
167     }
168 
169     /**
170      * Tries to extract the suppression from the given line.
171      * @param fileText {@link FileText} instance.
172      * @param lineNo line number.
173      * @return {@link Optional} of {@link Suppression}.
174      */
175     private Optional<Suppression> getSuppression(FileText fileText, int lineNo) {
176         final String line = fileText.get(lineNo);
177         final Matcher onCommentMatcher = onCommentFormat.matcher(line);
178         final Matcher offCommentMatcher = offCommentFormat.matcher(line);
179 
180         Suppression suppression = null;
181         if (onCommentMatcher.find()) {
182             suppression = new Suppression(onCommentMatcher.group(0),
183                 lineNo + 1, onCommentMatcher.start(), SuppressionType.ON, this);
184         }
185         if (offCommentMatcher.find()) {
186             suppression = new Suppression(offCommentMatcher.group(0),
187                 lineNo + 1, offCommentMatcher.start(), SuppressionType.OFF, this);
188         }
189 
190         return Optional.ofNullable(suppression);
191     }
192 
193     /**
194      * Finds the nearest {@link Suppression} instance which can suppress
195      * the given {@link AuditEvent}. The nearest suppression is the suppression which scope
196      * is before the line and column of the event.
197      * @param suppressions {@link Suppression} instance.
198      * @param event {@link AuditEvent} instance.
199      * @return {@link Suppression} instance.
200      */
201     private static Suppression getNearestSuppression(List<Suppression> suppressions,
202                                                      AuditEvent event) {
203         return suppressions
204             .stream()
205             .filter(suppression -> suppression.isMatch(event))
206             .reduce((first, second) -> second)
207             .filter(suppression -> suppression.suppressionType != SuppressionType.ON)
208             .orElse(null);
209     }
210 
211     /** Enum which represents the type of the suppression. */
212     private enum SuppressionType {
213 
214         /** On suppression type. */
215         ON,
216         /** Off suppression type. */
217         OFF
218 
219     }
220 
221     /** The class which represents the suppression. */
222     public static class Suppression {
223 
224         /** The regexp which is used to match the event source.*/
225         private final Pattern eventSourceRegexp;
226         /** The regexp which is used to match the event message.*/
227         private final Pattern eventMessageRegexp;
228 
229         /** Suppression text.*/
230         private final String text;
231         /** Suppression line.*/
232         private final int lineNo;
233         /** Suppression column number.*/
234         private final int columnNo;
235         /** Suppression type. */
236         private final SuppressionType suppressionType;
237 
238         /**
239          * Creates new suppression instance.
240          * @param text suppression text.
241          * @param lineNo suppression line number.
242          * @param columnNo suppression column number.
243          * @param suppressionType suppression type.
244          * @param filter the {@link SuppressWithPlainTextCommentFilter} with the context.
245          */
246         protected Suppression(
247             String text,
248             int lineNo,
249             int columnNo,
250             SuppressionType suppressionType,
251             SuppressWithPlainTextCommentFilter filter
252         ) {
253             this.text = text;
254             this.lineNo = lineNo;
255             this.columnNo = columnNo;
256             this.suppressionType = suppressionType;
257 
258             //Expand regexp for check and message
259             //Does not intern Patterns with Utils.getPattern()
260             String format = "";
261             try {
262                 if (this.suppressionType == SuppressionType.ON) {
263                     format = CommonUtils.fillTemplateWithStringsByRegexp(
264                             filter.checkFormat, text, filter.onCommentFormat);
265                     eventSourceRegexp = Pattern.compile(format);
266                     if (filter.messageFormat == null) {
267                         eventMessageRegexp = null;
268                     }
269                     else {
270                         format = CommonUtils.fillTemplateWithStringsByRegexp(
271                                 filter.messageFormat, text, filter.onCommentFormat);
272                         eventMessageRegexp = Pattern.compile(format);
273                     }
274                 }
275                 else {
276                     format = CommonUtils.fillTemplateWithStringsByRegexp(
277                             filter.checkFormat, text, filter.offCommentFormat);
278                     eventSourceRegexp = Pattern.compile(format);
279                     if (filter.messageFormat == null) {
280                         eventMessageRegexp = null;
281                     }
282                     else {
283                         format = CommonUtils.fillTemplateWithStringsByRegexp(
284                                 filter.messageFormat, text, filter.offCommentFormat);
285                         eventMessageRegexp = Pattern.compile(format);
286                     }
287                 }
288             }
289             catch (final PatternSyntaxException ex) {
290                 throw new IllegalArgumentException(
291                     "unable to parse expanded comment " + format, ex);
292             }
293         }
294 
295         @Override
296         public boolean equals(Object other) {
297             if (this == other) {
298                 return true;
299             }
300             if (other == null || getClass() != other.getClass()) {
301                 return false;
302             }
303             final Suppression suppression = (Suppression) other;
304             return Objects.equals(lineNo, suppression.lineNo)
305                     && Objects.equals(columnNo, suppression.columnNo)
306                     && Objects.equals(suppressionType, suppression.suppressionType)
307                     && Objects.equals(text, suppression.text)
308                     && Objects.equals(eventSourceRegexp, suppression.eventSourceRegexp)
309                     && Objects.equals(eventMessageRegexp, suppression.eventMessageRegexp);
310         }
311 
312         @Override
313         public int hashCode() {
314             return Objects.hash(
315                 text, lineNo, columnNo, suppressionType, eventSourceRegexp, eventMessageRegexp);
316         }
317 
318         /**
319          * Checks whether the suppression matches the given {@link AuditEvent}.
320          * @param event {@link AuditEvent} instance.
321          * @return true if the suppression matches {@link AuditEvent}.
322          */
323         private boolean isMatch(AuditEvent event) {
324             boolean match = false;
325             if (isInScopeOfSuppression(event)) {
326                 final Matcher sourceNameMatcher = eventSourceRegexp.matcher(event.getSourceName());
327                 if (sourceNameMatcher.find()) {
328                     match = eventMessageRegexp == null
329                         || eventMessageRegexp.matcher(event.getMessage()).find();
330                 }
331                 else {
332                     match = event.getModuleId() != null
333                         && eventSourceRegexp.matcher(event.getModuleId()).find();
334                 }
335             }
336             return match;
337         }
338 
339         /**
340          * Checks whether {@link AuditEvent} is in the scope of the suppression.
341          * @param event {@link AuditEvent} instance.
342          * @return true if {@link AuditEvent} is in the scope of the suppression.
343          */
344         private boolean isInScopeOfSuppression(AuditEvent event) {
345             return lineNo <= event.getLine();
346         }
347     }
348 
349 }