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