001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2017 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.filters;
021
022import java.lang.ref.WeakReference;
023import java.util.ArrayList;
024import java.util.Collection;
025import java.util.Collections;
026import java.util.List;
027import java.util.Objects;
028import java.util.regex.Matcher;
029import java.util.regex.Pattern;
030import java.util.regex.PatternSyntaxException;
031
032import org.apache.commons.beanutils.ConversionException;
033
034import com.puppycrawl.tools.checkstyle.api.AuditEvent;
035import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
036import com.puppycrawl.tools.checkstyle.api.FileContents;
037import com.puppycrawl.tools.checkstyle.api.Filter;
038import com.puppycrawl.tools.checkstyle.api.TextBlock;
039import com.puppycrawl.tools.checkstyle.checks.FileContentsHolder;
040import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
041
042/**
043 * <p>
044 * A filter that uses nearby comments to suppress audit events.
045 * </p>
046 *
047 * <p>This check is philosophically similar to {@link SuppressionCommentFilter}.
048 * Unlike {@link SuppressionCommentFilter}, this filter does not require
049 * pairs of comments.  This check may be used to suppress warnings in the
050 * current line:
051 * <pre>
052 *    offendingLine(for, whatever, reason); // SUPPRESS ParameterNumberCheck
053 * </pre>
054 * or it may be configured to span multiple lines, either forward:
055 * <pre>
056 *    // PERMIT MultipleVariableDeclarations NEXT 3 LINES
057 *    double x1 = 1.0, y1 = 0.0, z1 = 0.0;
058 *    double x2 = 0.0, y2 = 1.0, z2 = 0.0;
059 *    double x3 = 0.0, y3 = 0.0, z3 = 1.0;
060 * </pre>
061 * or reverse:
062 * <pre>
063 *   try {
064 *     thirdPartyLibrary.method();
065 *   } catch (RuntimeException ex) {
066 *     // ALLOW ILLEGAL CATCH BECAUSE third party API wraps everything
067 *     // in RuntimeExceptions.
068 *     ...
069 *   }
070 * </pre>
071 *
072 * <p>See {@link SuppressionCommentFilter} for usage notes.
073 *
074 * @author Mick Killianey
075 */
076public class SuppressWithNearbyCommentFilter
077    extends AutomaticBean
078    implements Filter {
079
080    /** Format to turns checkstyle reporting off. */
081    private static final String DEFAULT_COMMENT_FORMAT =
082        "SUPPRESS CHECKSTYLE (\\w+)";
083
084    /** Default regex for checks that should be suppressed. */
085    private static final String DEFAULT_CHECK_FORMAT = ".*";
086
087    /** Default regex for lines that should be suppressed. */
088    private static final String DEFAULT_INFLUENCE_FORMAT = "0";
089
090    /** Tagged comments. */
091    private final List<Tag> tags = new ArrayList<>();
092
093    /** Whether to look for trigger in C-style comments. */
094    private boolean checkC = true;
095
096    /** Whether to look for trigger in C++-style comments. */
097    // -@cs[AbbreviationAsWordInName] We can not change it as,
098    // check's property is a part of API (used in configurations).
099    private boolean checkCPP = true;
100
101    /** Parsed comment regexp that marks checkstyle suppression region. */
102    private Pattern commentFormat = Pattern.compile(DEFAULT_COMMENT_FORMAT);
103
104    /** The comment pattern that triggers suppression. */
105    private String checkFormat = DEFAULT_CHECK_FORMAT;
106
107    /** The message format to suppress. */
108    private String messageFormat;
109
110    /** The influence of the suppression comment. */
111    private String influenceFormat = DEFAULT_INFLUENCE_FORMAT;
112
113    /**
114     * References the current FileContents for this filter.
115     * Since this is a weak reference to the FileContents, the FileContents
116     * can be reclaimed as soon as the strong references in TreeWalker
117     * and FileContentsHolder are reassigned to the next FileContents,
118     * at which time filtering for the current FileContents is finished.
119     */
120    private WeakReference<FileContents> fileContentsReference = new WeakReference<>(null);
121
122    /**
123     * Set the format for a comment that turns off reporting.
124     * @param pattern a pattern.
125     */
126    public final void setCommentFormat(Pattern pattern) {
127        commentFormat = pattern;
128    }
129
130    /**
131     * @return the FileContents for this filter.
132     */
133    public FileContents getFileContents() {
134        return fileContentsReference.get();
135    }
136
137    /**
138     * Set the FileContents for this filter.
139     * @param fileContents the FileContents for this filter.
140     */
141    public void setFileContents(FileContents fileContents) {
142        fileContentsReference = new WeakReference<>(fileContents);
143    }
144
145    /**
146     * Set the format for a check.
147     * @param format a {@code String} value
148     */
149    public final void setCheckFormat(String format) {
150        checkFormat = format;
151    }
152
153    /**
154     * Set the format for a message.
155     * @param format a {@code String} value
156     */
157    public void setMessageFormat(String format) {
158        messageFormat = format;
159    }
160
161    /**
162     * Set the format for the influence of this check.
163     * @param format a {@code String} value
164     */
165    public final void setInfluenceFormat(String format) {
166        influenceFormat = format;
167    }
168
169    /**
170     * Set whether to look in C++ comments.
171     * @param checkCpp {@code true} if C++ comments are checked.
172     */
173    // -@cs[AbbreviationAsWordInName] We can not change it as,
174    // check's property is a part of API (used in configurations).
175    public void setCheckCPP(boolean checkCpp) {
176        checkCPP = checkCpp;
177    }
178
179    /**
180     * Set whether to look in C comments.
181     * @param checkC {@code true} if C comments are checked.
182     */
183    public void setCheckC(boolean checkC) {
184        this.checkC = checkC;
185    }
186
187    @Override
188    public boolean accept(AuditEvent 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 = FileContentsHolder.getCurrentFileContents();
195
196            if (getFileContents() != currentContents) {
197                setFileContents(currentContents);
198                tagSuppressions();
199            }
200            if (matchesTag(event)) {
201                accepted = false;
202            }
203        }
204        return accepted;
205    }
206
207    /**
208     * Whether current event matches any tag from {@link #tags}.
209     * @param event AuditEvent to test match on {@link #tags}.
210     * @return true if event matches any tag from {@link #tags}, false otherwise.
211     */
212    private boolean matchesTag(AuditEvent event) {
213        for (final Tag tag : tags) {
214            if (tag.isMatch(event)) {
215                return true;
216            }
217        }
218        return false;
219    }
220
221    /**
222     * Collects all the suppression tags for all comments into a list and
223     * sorts the list.
224     */
225    private void tagSuppressions() {
226        tags.clear();
227        final FileContents contents = getFileContents();
228        if (checkCPP) {
229            tagSuppressions(contents.getCppComments().values());
230        }
231        if (checkC) {
232            final Collection<List<TextBlock>> cComments =
233                contents.getCComments().values();
234            cComments.forEach(this::tagSuppressions);
235        }
236        Collections.sort(tags);
237    }
238
239    /**
240     * Appends the suppressions in a collection of comments to the full
241     * set of suppression tags.
242     * @param comments the set of comments.
243     */
244    private void tagSuppressions(Collection<TextBlock> comments) {
245        for (final TextBlock comment : comments) {
246            final int startLineNo = comment.getStartLineNo();
247            final String[] text = comment.getText();
248            tagCommentLine(text[0], startLineNo);
249            for (int i = 1; i < text.length; i++) {
250                tagCommentLine(text[i], startLineNo + i);
251            }
252        }
253    }
254
255    /**
256     * Tags a string if it matches the format for turning
257     * checkstyle reporting on or the format for turning reporting off.
258     * @param text the string to tag.
259     * @param line the line number of text.
260     */
261    private void tagCommentLine(String text, int line) {
262        final Matcher matcher = commentFormat.matcher(text);
263        if (matcher.find()) {
264            addTag(matcher.group(0), line);
265        }
266    }
267
268    /**
269     * Adds a comment suppression {@code Tag} to the list of all tags.
270     * @param text the text of the tag.
271     * @param line the line number of the tag.
272     */
273    private void addTag(String text, int line) {
274        final Tag tag = new Tag(text, line, this);
275        tags.add(tag);
276    }
277
278    /**
279     * A Tag holds a suppression comment and its location.
280     */
281    public static class Tag implements Comparable<Tag> {
282        /** The text of the tag. */
283        private final String text;
284
285        /** The first line where warnings may be suppressed. */
286        private final int firstLine;
287
288        /** The last line where warnings may be suppressed. */
289        private final int lastLine;
290
291        /** The parsed check regexp, expanded for the text of this tag. */
292        private final Pattern tagCheckRegexp;
293
294        /** The parsed message regexp, expanded for the text of this tag. */
295        private final Pattern tagMessageRegexp;
296
297        /**
298         * Constructs a tag.
299         * @param text the text of the suppression.
300         * @param line the line number.
301         * @param filter the {@code SuppressWithNearbyCommentFilter} with the context
302         * @throws ConversionException if unable to parse expanded text.
303         */
304        public Tag(String text, int line, SuppressWithNearbyCommentFilter filter) {
305            this.text = text;
306
307            //Expand regexp for check and message
308            //Does not intern Patterns with Utils.getPattern()
309            String format = "";
310            try {
311                format = CommonUtils.fillTemplateWithStringsByRegexp(
312                        filter.checkFormat, text, filter.commentFormat);
313                tagCheckRegexp = Pattern.compile(format);
314                if (filter.messageFormat == null) {
315                    tagMessageRegexp = null;
316                }
317                else {
318                    format = CommonUtils.fillTemplateWithStringsByRegexp(
319                            filter.messageFormat, text, filter.commentFormat);
320                    tagMessageRegexp = Pattern.compile(format);
321                }
322                format = CommonUtils.fillTemplateWithStringsByRegexp(
323                        filter.influenceFormat, text, filter.commentFormat);
324                final int influence;
325                try {
326                    if (CommonUtils.startsWithChar(format, '+')) {
327                        format = format.substring(1);
328                    }
329                    influence = Integer.parseInt(format);
330                }
331                catch (final NumberFormatException ex) {
332                    throw new ConversionException(
333                        "unable to parse influence from '" + text
334                            + "' using " + filter.influenceFormat, ex);
335                }
336                if (influence >= 0) {
337                    firstLine = line;
338                    lastLine = line + influence;
339                }
340                else {
341                    firstLine = line + influence;
342                    lastLine = line;
343                }
344            }
345            catch (final PatternSyntaxException ex) {
346                throw new ConversionException(
347                    "unable to parse expanded comment " + format,
348                    ex);
349            }
350        }
351
352        /**
353         * Compares the position of this tag in the file
354         * with the position of another tag.
355         * @param other the tag to compare with this one.
356         * @return a negative number if this tag is before the other tag,
357         *     0 if they are at the same position, and a positive number if this
358         *     tag is after the other tag.
359         */
360        @Override
361        public int compareTo(Tag other) {
362            if (firstLine == other.firstLine) {
363                return Integer.compare(lastLine, other.lastLine);
364            }
365
366            return Integer.compare(firstLine, other.firstLine);
367        }
368
369        @Override
370        public boolean equals(Object other) {
371            if (this == other) {
372                return true;
373            }
374            if (other == null || getClass() != other.getClass()) {
375                return false;
376            }
377            final Tag tag = (Tag) other;
378            return Objects.equals(firstLine, tag.firstLine)
379                    && Objects.equals(lastLine, tag.lastLine)
380                    && Objects.equals(text, tag.text)
381                    && Objects.equals(tagCheckRegexp, tag.tagCheckRegexp)
382                    && Objects.equals(tagMessageRegexp, tag.tagMessageRegexp);
383        }
384
385        @Override
386        public int hashCode() {
387            return Objects.hash(text, firstLine, lastLine, tagCheckRegexp, tagMessageRegexp);
388        }
389
390        /**
391         * Determines whether the source of an audit event
392         * matches the text of this tag.
393         * @param event the {@code AuditEvent} to check.
394         * @return true if the source of event matches the text of this tag.
395         */
396        public boolean isMatch(AuditEvent event) {
397            final int line = event.getLine();
398            boolean match = false;
399
400            if (line >= firstLine && line <= lastLine) {
401                final Matcher tagMatcher = tagCheckRegexp.matcher(event.getSourceName());
402
403                if (tagMatcher.find()) {
404                    match = true;
405                }
406                else if (tagMessageRegexp == null) {
407                    if (event.getModuleId() != null) {
408                        final Matcher idMatcher = tagCheckRegexp.matcher(event.getModuleId());
409                        match = idMatcher.find();
410                    }
411                }
412                else {
413                    final Matcher messageMatcher = tagMessageRegexp.matcher(event.getMessage());
414                    match = messageMatcher.find();
415                }
416            }
417            return match;
418        }
419
420        @Override
421        public final String toString() {
422            return "Tag[lines=[" + firstLine + " to " + lastLine
423                + "]; text='" + text + "']";
424        }
425    }
426}