001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2018 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.List;
026import java.util.Objects;
027import java.util.regex.Matcher;
028import java.util.regex.Pattern;
029import java.util.regex.PatternSyntaxException;
030
031import com.puppycrawl.tools.checkstyle.TreeWalkerAuditEvent;
032import com.puppycrawl.tools.checkstyle.TreeWalkerFilter;
033import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
034import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
035import com.puppycrawl.tools.checkstyle.api.FileContents;
036import com.puppycrawl.tools.checkstyle.api.TextBlock;
037import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
038
039/**
040 * <p>
041 * A filter that uses nearby comments to suppress audit events.
042 * </p>
043 *
044 * <p>This check is philosophically similar to {@link SuppressionCommentFilter}.
045 * Unlike {@link SuppressionCommentFilter}, this filter does not require
046 * pairs of comments.  This check may be used to suppress warnings in the
047 * current line:
048 * <pre>
049 *    offendingLine(for, whatever, reason); // SUPPRESS ParameterNumberCheck
050 * </pre>
051 * or it may be configured to span multiple lines, either forward:
052 * <pre>
053 *    // PERMIT MultipleVariableDeclarations NEXT 3 LINES
054 *    double x1 = 1.0, y1 = 0.0, z1 = 0.0;
055 *    double x2 = 0.0, y2 = 1.0, z2 = 0.0;
056 *    double x3 = 0.0, y3 = 0.0, z3 = 1.0;
057 * </pre>
058 * or reverse:
059 * <pre>
060 *   try {
061 *     thirdPartyLibrary.method();
062 *   } catch (RuntimeException ex) {
063 *     // ALLOW ILLEGAL CATCH BECAUSE third party API wraps everything
064 *     // in RuntimeExceptions.
065 *     ...
066 *   }
067 * </pre>
068 *
069 * <p>See {@link SuppressionCommentFilter} for usage notes.
070 *
071 * @author Mick Killianey
072 */
073public class SuppressWithNearbyCommentFilter
074    extends AutomaticBean
075    implements TreeWalkerFilter {
076
077    /** Format to turns checkstyle reporting off. */
078    private static final String DEFAULT_COMMENT_FORMAT =
079        "SUPPRESS CHECKSTYLE (\\w+)";
080
081    /** Default regex for checks that should be suppressed. */
082    private static final String DEFAULT_CHECK_FORMAT = ".*";
083
084    /** Default regex for lines that should be suppressed. */
085    private static final String DEFAULT_INFLUENCE_FORMAT = "0";
086
087    /** Tagged comments. */
088    private final List<Tag> tags = new ArrayList<>();
089
090    /** Whether to look for trigger in C-style comments. */
091    private boolean checkC = true;
092
093    /** Whether to look for trigger in C++-style comments. */
094    // -@cs[AbbreviationAsWordInName] We can not change it as,
095    // check's property is a part of API (used in configurations).
096    private boolean checkCPP = true;
097
098    /** Parsed comment regexp that marks checkstyle suppression region. */
099    private Pattern commentFormat = Pattern.compile(DEFAULT_COMMENT_FORMAT);
100
101    /** The comment pattern that triggers suppression. */
102    private String checkFormat = DEFAULT_CHECK_FORMAT;
103
104    /** The message format to suppress. */
105    private String messageFormat;
106
107    /** The influence of the suppression comment. */
108    private String influenceFormat = DEFAULT_INFLUENCE_FORMAT;
109
110    /**
111     * References the current FileContents for this filter.
112     * Since this is a weak reference to the FileContents, the FileContents
113     * can be reclaimed as soon as the strong references in TreeWalker
114     * are reassigned to the next FileContents, at which time filtering for
115     * the current FileContents is finished.
116     */
117    private WeakReference<FileContents> fileContentsReference = new WeakReference<>(null);
118
119    /**
120     * Set the format for a comment that turns off reporting.
121     * @param pattern a pattern.
122     */
123    public final void setCommentFormat(Pattern pattern) {
124        commentFormat = pattern;
125    }
126
127    /**
128     * Returns FileContents for this filter.
129     * @return the FileContents for this filter.
130     */
131    private 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     * @noinspection WeakerAccess
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    protected void finishLocalSetup() throws CheckstyleException {
188        // No code by default
189    }
190
191    @Override
192    public boolean accept(TreeWalkerAuditEvent event) {
193        boolean accepted = true;
194
195        if (event.getLocalizedMessage() != null) {
196            // Lazy update. If the first event for the current file, update file
197            // contents and tag suppressions
198            final FileContents currentContents = event.getFileContents();
199
200            if (getFileContents() != currentContents) {
201                setFileContents(currentContents);
202                tagSuppressions();
203            }
204            if (matchesTag(event)) {
205                accepted = false;
206            }
207        }
208        return accepted;
209    }
210
211    /**
212     * Whether current event matches any tag from {@link #tags}.
213     * @param event TreeWalkerAuditEvent to test match on {@link #tags}.
214     * @return true if event matches any tag from {@link #tags}, false otherwise.
215     */
216    private boolean matchesTag(TreeWalkerAuditEvent event) {
217        boolean result = false;
218        for (final Tag tag : tags) {
219            if (tag.isMatch(event)) {
220                result = true;
221                break;
222            }
223        }
224        return result;
225    }
226
227    /**
228     * Collects all the suppression tags for all comments into a list and
229     * sorts the list.
230     */
231    private void tagSuppressions() {
232        tags.clear();
233        final FileContents contents = getFileContents();
234        if (checkCPP) {
235            tagSuppressions(contents.getSingleLineComments().values());
236        }
237        if (checkC) {
238            final Collection<List<TextBlock>> cComments =
239                contents.getBlockComments().values();
240            cComments.forEach(this::tagSuppressions);
241        }
242    }
243
244    /**
245     * Appends the suppressions in a collection of comments to the full
246     * set of suppression tags.
247     * @param comments the set of comments.
248     */
249    private void tagSuppressions(Collection<TextBlock> comments) {
250        for (final TextBlock comment : comments) {
251            final int startLineNo = comment.getStartLineNo();
252            final String[] text = comment.getText();
253            tagCommentLine(text[0], startLineNo);
254            for (int i = 1; i < text.length; i++) {
255                tagCommentLine(text[i], startLineNo + i);
256            }
257        }
258    }
259
260    /**
261     * Tags a string if it matches the format for turning
262     * checkstyle reporting on or the format for turning reporting off.
263     * @param text the string to tag.
264     * @param line the line number of text.
265     */
266    private void tagCommentLine(String text, int line) {
267        final Matcher matcher = commentFormat.matcher(text);
268        if (matcher.find()) {
269            addTag(matcher.group(0), line);
270        }
271    }
272
273    /**
274     * Adds a comment suppression {@code Tag} to the list of all tags.
275     * @param text the text of the tag.
276     * @param line the line number of the tag.
277     */
278    private void addTag(String text, int line) {
279        final Tag tag = new Tag(text, line, this);
280        tags.add(tag);
281    }
282
283    /**
284     * A Tag holds a suppression comment and its location.
285     */
286    public static class Tag {
287
288        /** The text of the tag. */
289        private final String text;
290
291        /** The first line where warnings may be suppressed. */
292        private final int firstLine;
293
294        /** The last line where warnings may be suppressed. */
295        private final int lastLine;
296
297        /** The parsed check regexp, expanded for the text of this tag. */
298        private final Pattern tagCheckRegexp;
299
300        /** The parsed message regexp, expanded for the text of this tag. */
301        private final Pattern tagMessageRegexp;
302
303        /**
304         * Constructs a tag.
305         * @param text the text of the suppression.
306         * @param line the line number.
307         * @param filter the {@code SuppressWithNearbyCommentFilter} with the context
308         * @throws IllegalArgumentException if unable to parse expanded text.
309         */
310        public Tag(String text, int line, SuppressWithNearbyCommentFilter filter) {
311            this.text = text;
312
313            //Expand regexp for check and message
314            //Does not intern Patterns with Utils.getPattern()
315            String format = "";
316            try {
317                format = CommonUtils.fillTemplateWithStringsByRegexp(
318                        filter.checkFormat, text, filter.commentFormat);
319                tagCheckRegexp = Pattern.compile(format);
320                if (filter.messageFormat == null) {
321                    tagMessageRegexp = null;
322                }
323                else {
324                    format = CommonUtils.fillTemplateWithStringsByRegexp(
325                            filter.messageFormat, text, filter.commentFormat);
326                    tagMessageRegexp = Pattern.compile(format);
327                }
328                format = CommonUtils.fillTemplateWithStringsByRegexp(
329                        filter.influenceFormat, text, filter.commentFormat);
330
331                if (CommonUtils.startsWithChar(format, '+')) {
332                    format = format.substring(1);
333                }
334                final int influence = parseInfluence(format, filter.influenceFormat, text);
335
336                if (influence >= 1) {
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         * Gets influence from suppress filter influence format param.
353         *
354         * @param format          influence format to parse
355         * @param influenceFormat raw influence format
356         * @param text            text of the suppression
357         * @return parsed influence
358         */
359        private static int parseInfluence(String format, String influenceFormat, String text) {
360            try {
361                return Integer.parseInt(format);
362            }
363            catch (final NumberFormatException ex) {
364                throw new IllegalArgumentException("unable to parse influence from '" + text
365                        + "' using " + influenceFormat, ex);
366            }
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 TreeWalkerAuditEvent} to check.
394         * @return true if the source of event matches the text of this tag.
395         */
396        public boolean isMatch(TreeWalkerAuditEvent 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 String toString() {
422            return "Tag[text='" + text + '\''
423                    + ", firstLine=" + firstLine
424                    + ", lastLine=" + lastLine
425                    + ", tagCheckRegexp=" + tagCheckRegexp
426                    + ", tagMessageRegexp=" + tagMessageRegexp
427                    + ']';
428        }
429
430    }
431
432}