001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2015 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.Collection;
024import java.util.Collections;
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 org.apache.commons.beanutils.ConversionException;
032
033import com.google.common.collect.Lists;
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 e) {
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    /** 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    private boolean checkCPP = true;
095
096    /** Parsed comment regexp that marks checkstyle suppression region. */
097    private Pattern commentRegexp;
098
099    /** The comment pattern that triggers suppression. */
100    private String checkFormat;
101
102    /** The message format to suppress. */
103    private String messageFormat;
104
105    /** The influence of the suppression comment. */
106    private String influenceFormat;
107
108    /** Tagged comments. */
109    private final List<Tag> tags = Lists.newArrayList();
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     * Constructs a SuppressionCommentFilter.
122     * Initializes comment on, comment off, and check formats
123     * to defaults.
124     */
125    public SuppressWithNearbyCommentFilter() {
126        setCommentFormat(DEFAULT_COMMENT_FORMAT);
127        checkFormat = DEFAULT_CHECK_FORMAT;
128        influenceFormat = DEFAULT_INFLUENCE_FORMAT;
129    }
130
131    /**
132     * Set the format for a comment that turns off reporting.
133     * @param format a {@code String} value.
134     * @throws ConversionException if unable to create Pattern object.
135     */
136    public final void setCommentFormat(String format) {
137        commentRegexp = CommonUtils.createPattern(format);
138    }
139
140    /**
141     * @return the FileContents for this filter.
142     */
143    public FileContents getFileContents() {
144        return fileContentsReference.get();
145    }
146
147    /**
148     * Set the FileContents for this filter.
149     * @param fileContents the FileContents for this filter.
150     */
151    public void setFileContents(FileContents fileContents) {
152        fileContentsReference = new WeakReference<>(fileContents);
153    }
154
155    /**
156     * Set the format for a check.
157     * @param format a {@code String} value
158     */
159    public final void setCheckFormat(String format) {
160        checkFormat = format;
161    }
162
163    /**
164     * Set the format for a message.
165     * @param format a {@code String} value
166     */
167    public void setMessageFormat(String format) {
168        messageFormat = format;
169    }
170
171    /**
172     * Set the format for the influence of this check.
173     * @param format a {@code String} value
174     */
175    public final void setInfluenceFormat(String format) {
176        influenceFormat = format;
177    }
178
179    /**
180     * Set whether to look in C++ comments.
181     * @param checkCpp {@code true} if C++ comments are checked.
182     */
183    public void setCheckCPP(boolean checkCpp) {
184        checkCPP = checkCpp;
185    }
186
187    /**
188     * Set whether to look in C comments.
189     * @param checkC {@code true} if C comments are checked.
190     */
191    public void setCheckC(boolean checkC) {
192        this.checkC = checkC;
193    }
194
195    @Override
196    public boolean accept(AuditEvent event) {
197        boolean accepted = true;
198
199        if (event.getLocalizedMessage() != null) {
200            // Lazy update. If the first event for the current file, update file
201            // contents and tag suppressions
202            final FileContents currentContents = FileContentsHolder.getContents();
203
204            if (currentContents != null) {
205                if (getFileContents() != currentContents) {
206                    setFileContents(currentContents);
207                    tagSuppressions();
208                }
209                if (matchesTag(event)) {
210                    accepted = false;
211                }
212            }
213        }
214        return accepted;
215    }
216
217    /**
218     * Whether current event matches any tag from {@link #tags}.
219     * @param event AuditEvent to test match on {@link #tags}.
220     * @return true if event matches any tag from {@link #tags}, false otherwise.
221     */
222    private boolean matchesTag(AuditEvent event) {
223        for (final Tag tag : tags) {
224            if (tag.isMatch(event)) {
225                return true;
226            }
227        }
228        return false;
229    }
230
231    /**
232     * Collects all the suppression tags for all comments into a list and
233     * sorts the list.
234     */
235    private void tagSuppressions() {
236        tags.clear();
237        final FileContents contents = getFileContents();
238        if (checkCPP) {
239            tagSuppressions(contents.getCppComments().values());
240        }
241        if (checkC) {
242            final Collection<List<TextBlock>> cComments =
243                contents.getCComments().values();
244            for (final List<TextBlock> element : cComments) {
245                tagSuppressions(element);
246            }
247        }
248        Collections.sort(tags);
249    }
250
251    /**
252     * Appends the suppressions in a collection of comments to the full
253     * set of suppression tags.
254     * @param comments the set of comments.
255     */
256    private void tagSuppressions(Collection<TextBlock> comments) {
257        for (final TextBlock comment : comments) {
258            final int startLineNo = comment.getStartLineNo();
259            final String[] text = comment.getText();
260            tagCommentLine(text[0], startLineNo);
261            for (int i = 1; i < text.length; i++) {
262                tagCommentLine(text[i], startLineNo + i);
263            }
264        }
265    }
266
267    /**
268     * Tags a string if it matches the format for turning
269     * checkstyle reporting on or the format for turning reporting off.
270     * @param text the string to tag.
271     * @param line the line number of text.
272     */
273    private void tagCommentLine(String text, int line) {
274        final Matcher matcher = commentRegexp.matcher(text);
275        if (matcher.find()) {
276            addTag(matcher.group(0), line);
277        }
278    }
279
280    /**
281     * Adds a comment suppression {@code Tag} to the list of all tags.
282     * @param text the text of the tag.
283     * @param line the line number of the tag.
284     */
285    private void addTag(String text, int line) {
286        final Tag tag = new Tag(text, line, this);
287        tags.add(tag);
288    }
289
290    /**
291     * A Tag holds a suppression comment and its location.
292     */
293    public static class Tag implements Comparable<Tag> {
294        /** The text of the tag. */
295        private final String text;
296
297        /** The first line where warnings may be suppressed. */
298        private final int firstLine;
299
300        /** The last line where warnings may be suppressed. */
301        private final int lastLine;
302
303        /** The parsed check regexp, expanded for the text of this tag. */
304        private final Pattern tagCheckRegexp;
305
306        /** The parsed message regexp, expanded for the text of this tag. */
307        private final Pattern tagMessageRegexp;
308
309        /**
310         * Constructs a tag.
311         * @param text the text of the suppression.
312         * @param line the line number.
313         * @param filter the {@code SuppressWithNearbyCommentFilter} with the context
314         * @throws ConversionException if unable to parse expanded text.
315         */
316        public Tag(String text, int line, SuppressWithNearbyCommentFilter filter) {
317            this.text = text;
318
319            //Expand regexp for check and message
320            //Does not intern Patterns with Utils.getPattern()
321            String format = "";
322            try {
323                format = CommonUtils.fillTemplateWithStringsByRegexp(
324                        filter.checkFormat, text, filter.commentRegexp);
325                tagCheckRegexp = Pattern.compile(format);
326                if (filter.messageFormat == null) {
327                    tagMessageRegexp = null;
328                }
329                else {
330                    format = CommonUtils.fillTemplateWithStringsByRegexp(
331                            filter.messageFormat, text, filter.commentRegexp);
332                    tagMessageRegexp = Pattern.compile(format);
333                }
334                format = CommonUtils.fillTemplateWithStringsByRegexp(
335                        filter.influenceFormat, text, filter.commentRegexp);
336                final int influence;
337                try {
338                    if (CommonUtils.startsWithChar(format, '+')) {
339                        format = format.substring(1);
340                    }
341                    influence = Integer.parseInt(format);
342                }
343                catch (final NumberFormatException e) {
344                    throw new ConversionException(
345                        "unable to parse influence from '" + text
346                            + "' using " + filter.influenceFormat, e);
347                }
348                if (influence >= 0) {
349                    firstLine = line;
350                    lastLine = line + influence;
351                }
352                else {
353                    firstLine = line + influence;
354                    lastLine = line;
355                }
356            }
357            catch (final PatternSyntaxException e) {
358                throw new ConversionException(
359                    "unable to parse expanded comment " + format,
360                    e);
361            }
362        }
363
364        /**
365         * Compares the position of this tag in the file
366         * with the position of another tag.
367         * @param other the tag to compare with this one.
368         * @return a negative number if this tag is before the other tag,
369         *     0 if they are at the same position, and a positive number if this
370         *     tag is after the other tag.
371         */
372        @Override
373        public int compareTo(Tag other) {
374            if (firstLine == other.firstLine) {
375                return Integer.compare(lastLine, other.lastLine);
376            }
377
378            return Integer.compare(firstLine, other.firstLine);
379        }
380
381        @Override
382        public boolean equals(Object other) {
383            if (this == other) {
384                return true;
385            }
386            if (other == null || getClass() != other.getClass()) {
387                return false;
388            }
389            final Tag tag = (Tag) other;
390            return Objects.equals(firstLine, tag.firstLine)
391                    && Objects.equals(lastLine, tag.lastLine)
392                    && Objects.equals(text, tag.text)
393                    && Objects.equals(tagCheckRegexp, tag.tagCheckRegexp)
394                    && Objects.equals(tagMessageRegexp, tag.tagMessageRegexp);
395        }
396
397        @Override
398        public int hashCode() {
399            return Objects.hash(text, firstLine, lastLine, tagCheckRegexp, tagMessageRegexp);
400        }
401
402        /**
403         * Determines whether the source of an audit event
404         * matches the text of this tag.
405         * @param event the {@code AuditEvent} to check.
406         * @return true if the source of event matches the text of this tag.
407         */
408        public boolean isMatch(AuditEvent event) {
409            final int line = event.getLine();
410            boolean match = false;
411
412            if (line >= firstLine && line <= lastLine) {
413                final Matcher tagMatcher = tagCheckRegexp.matcher(event.getSourceName());
414
415                if (tagMatcher.find()) {
416                    match = true;
417                }
418                else if (tagMessageRegexp != null) {
419                    final Matcher messageMatcher = tagMessageRegexp.matcher(event.getMessage());
420                    match = messageMatcher.find();
421                }
422            }
423            return match;
424        }
425
426        @Override
427        public final String toString() {
428            return "Tag[lines=[" + firstLine + " to " + lastLine
429                + "]; text='" + text + "']";
430        }
431    }
432}