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     * @return the FileContents for this filter.
130     */
131    public FileContents getFileContents() {
132        return fileContentsReference.get();
133    }
134
135    /**
136     * Set the FileContents for this filter.
137     * @param fileContents the FileContents for this filter.
138     */
139    public void setFileContents(FileContents fileContents) {
140        fileContentsReference = new WeakReference<>(fileContents);
141    }
142
143    /**
144     * Set the format for a check.
145     * @param format a {@code String} value
146     */
147    public final void setCheckFormat(String format) {
148        checkFormat = format;
149    }
150
151    /**
152     * Set the format for a message.
153     * @param format a {@code String} value
154     */
155    public void setMessageFormat(String format) {
156        messageFormat = format;
157    }
158
159    /**
160     * Set the format for the influence of this check.
161     * @param format a {@code String} value
162     */
163    public final void setInfluenceFormat(String format) {
164        influenceFormat = format;
165    }
166
167    /**
168     * Set whether to look in C++ comments.
169     * @param checkCpp {@code true} if C++ comments are checked.
170     */
171    // -@cs[AbbreviationAsWordInName] We can not change it as,
172    // check's property is a part of API (used in configurations).
173    public void setCheckCPP(boolean checkCpp) {
174        checkCPP = checkCpp;
175    }
176
177    /**
178     * Set whether to look in C comments.
179     * @param checkC {@code true} if C comments are checked.
180     */
181    public void setCheckC(boolean checkC) {
182        this.checkC = checkC;
183    }
184
185    @Override
186    public boolean accept(AuditEvent event) {
187        boolean accepted = true;
188
189        if (event.getLocalizedMessage() != null) {
190            // Lazy update. If the first event for the current file, update file
191            // contents and tag suppressions
192            final FileContents currentContents = FileContentsHolder.getCurrentFileContents();
193
194            if (getFileContents() != currentContents) {
195                setFileContents(currentContents);
196                tagSuppressions();
197            }
198            if (matchesTag(event)) {
199                accepted = false;
200            }
201        }
202        return accepted;
203    }
204
205    /**
206     * Whether current event matches any tag from {@link #tags}.
207     * @param event AuditEvent to test match on {@link #tags}.
208     * @return true if event matches any tag from {@link #tags}, false otherwise.
209     */
210    private boolean matchesTag(AuditEvent event) {
211        boolean result = false;
212        for (final Tag tag : tags) {
213            if (tag.isMatch(event)) {
214                result = true;
215                break;
216            }
217        }
218        return result;
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.getSingleLineComments().values());
230        }
231        if (checkC) {
232            final Collection<List<TextBlock>> cComments =
233                contents.getBlockComments().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 IllegalArgumentException 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 IllegalArgumentException("unable to parse influence from '" + text
333                            + "' using " + filter.influenceFormat, ex);
334                }
335                if (influence >= 0) {
336                    firstLine = line;
337                    lastLine = line + influence;
338                }
339                else {
340                    firstLine = line + influence;
341                    lastLine = line;
342                }
343            }
344            catch (final PatternSyntaxException ex) {
345                throw new IllegalArgumentException(
346                    "unable to parse expanded comment " + format, ex);
347            }
348        }
349
350        /**
351         * Compares the position of this tag in the file
352         * with the position of another tag.
353         * @param other the tag to compare with this one.
354         * @return a negative number if this tag is before the other tag,
355         *     0 if they are at the same position, and a positive number if this
356         *     tag is after the other tag.
357         */
358        @Override
359        public int compareTo(Tag other) {
360            final int result;
361            if (firstLine == other.firstLine) {
362                result = Integer.compare(lastLine, other.lastLine);
363            }
364            else {
365                result = Integer.compare(firstLine, other.firstLine);
366            }
367            return result;
368        }
369
370        @Override
371        public boolean equals(Object other) {
372            if (this == other) {
373                return true;
374            }
375            if (other == null || getClass() != other.getClass()) {
376                return false;
377            }
378            final Tag tag = (Tag) other;
379            return Objects.equals(firstLine, tag.firstLine)
380                    && Objects.equals(lastLine, tag.lastLine)
381                    && Objects.equals(text, tag.text)
382                    && Objects.equals(tagCheckRegexp, tag.tagCheckRegexp)
383                    && Objects.equals(tagMessageRegexp, tag.tagMessageRegexp);
384        }
385
386        @Override
387        public int hashCode() {
388            return Objects.hash(text, firstLine, lastLine, tagCheckRegexp, tagMessageRegexp);
389        }
390
391        /**
392         * Determines whether the source of an audit event
393         * matches the text of this tag.
394         * @param event the {@code AuditEvent} to check.
395         * @return true if the source of event matches the text of this tag.
396         */
397        public boolean isMatch(AuditEvent event) {
398            final int line = event.getLine();
399            boolean match = false;
400
401            if (line >= firstLine && line <= lastLine) {
402                final Matcher tagMatcher = tagCheckRegexp.matcher(event.getSourceName());
403
404                if (tagMatcher.find()) {
405                    match = true;
406                }
407                else if (tagMessageRegexp == null) {
408                    if (event.getModuleId() != null) {
409                        final Matcher idMatcher = tagCheckRegexp.matcher(event.getModuleId());
410                        match = idMatcher.find();
411                    }
412                }
413                else {
414                    final Matcher messageMatcher = tagMessageRegexp.matcher(event.getMessage());
415                    match = messageMatcher.find();
416                }
417            }
418            return match;
419        }
420
421        @Override
422        public final String toString() {
423            return "Tag[lines=[" + firstLine + " to " + lastLine
424                + "]; text='" + text + "']";
425        }
426    }
427}