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