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