001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2014  Oliver Burn
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////////////////////////////////////////////////////////////////////////////////
019package com.puppycrawl.tools.checkstyle.filters;
020
021import com.google.common.collect.Lists;
022import com.puppycrawl.tools.checkstyle.api.AuditEvent;
023import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
024import com.puppycrawl.tools.checkstyle.api.FileContents;
025import com.puppycrawl.tools.checkstyle.api.Filter;
026import com.puppycrawl.tools.checkstyle.api.TextBlock;
027import com.puppycrawl.tools.checkstyle.api.Utils;
028import com.puppycrawl.tools.checkstyle.checks.FileContentsHolder;
029import java.lang.ref.WeakReference;
030import java.util.Collection;
031import java.util.Collections;
032import java.util.Iterator;
033import java.util.List;
034import java.util.regex.Matcher;
035import java.util.regex.Pattern;
036import java.util.regex.PatternSyntaxException;
037import org.apache.commons.beanutils.ConversionException;
038
039/**
040 * <p>
041 * A filter that uses nearby comments to suppress audit events.
042 * </p>
043 * <p>
044 * 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 e) {
063 *     // ALLOW ILLEGAL CATCH BECAUSE third party API wraps everything
064 *     // in RuntimeExceptions.
065 *     ...
066 *   }
067 * </pre>
068 *
069 * <p>
070 * See {@link SuppressionCommentFilter} for usage notes.
071 *
072 *
073 * @author Mick Killianey
074 */
075public class SuppressWithNearbyCommentFilter
076    extends AutomaticBean
077    implements Filter
078{
079    /**
080     * A Tag holds a suppression comment and its location.
081     */
082    public class Tag implements Comparable<Tag>
083    {
084        /** The text of the tag. */
085        private final String text;
086
087        /** The first line where warnings may be suppressed. */
088        private int firstLine;
089
090        /** The last line where warnings may be suppressed. */
091        private int lastLine;
092
093        /** The parsed check regexp, expanded for the text of this tag. */
094        private Pattern tagCheckRegexp;
095
096        /** The parsed message regexp, expanded for the text of this tag. */
097        private Pattern tagMessageRegexp;
098
099        /**
100         * Constructs a tag.
101         * @param text the text of the suppression.
102         * @param line the line number.
103         * @throws ConversionException if unable to parse expanded text.
104         * on.
105         */
106        public Tag(String text, int line)
107            throws ConversionException
108        {
109            this.text = text;
110
111            tagCheckRegexp = checkRegexp;
112            //Expand regexp for check and message
113            //Does not intern Patterns with Utils.getPattern()
114            String format = "";
115            try {
116                format = expandFrocomment(text, checkFormat, commentRegexp);
117                tagCheckRegexp = Pattern.compile(format);
118                if (messageFormat != null) {
119                    format = expandFrocomment(
120                         text, messageFormat, commentRegexp);
121                    tagMessageRegexp = Pattern.compile(format);
122                }
123                int influence = 0;
124                if (influenceFormat != null) {
125                    format = expandFrocomment(
126                        text, influenceFormat, commentRegexp);
127                    try {
128                        if (format.startsWith("+")) {
129                            format = format.substring(1);
130                        }
131                        influence = Integer.parseInt(format);
132                    }
133                    catch (final NumberFormatException e) {
134                        throw new ConversionException(
135                            "unable to parse influence from '" + text
136                                + "' using " + influenceFormat, e);
137                    }
138                }
139                if (influence >= 0) {
140                    firstLine = line;
141                    lastLine = line + influence;
142                }
143                else {
144                    firstLine = line + influence;
145                    lastLine = line;
146                }
147            }
148            catch (final PatternSyntaxException e) {
149                throw new ConversionException(
150                    "unable to parse expanded comment " + format,
151                    e);
152            }
153        }
154
155        /** @return the text of the tag. */
156        public String getText()
157        {
158            return text;
159        }
160
161        /** @return the line number of the first suppressed line. */
162        public int getFirstLine()
163        {
164            return firstLine;
165        }
166
167        /** @return the line number of the last suppressed line. */
168        public int getLastLine()
169        {
170            return lastLine;
171        }
172
173        /**
174         * Compares the position of this tag in the file
175         * with the position of another tag.
176         * @param other the tag to compare with this one.
177         * @return a negative number if this tag is before the other tag,
178         * 0 if they are at the same position, and a positive number if this
179         * tag is after the other tag.
180         * @see java.lang.Comparable#compareTo(java.lang.Object)
181         */
182        @Override
183        public int compareTo(Tag other)
184        {
185            if (firstLine == other.firstLine) {
186                return lastLine - other.lastLine;
187            }
188
189            return (firstLine - other.firstLine);
190        }
191
192        /**
193         * Determines whether the source of an audit event
194         * matches the text of this tag.
195         * @param event the <code>AuditEvent</code> to check.
196         * @return true if the source of event matches the text of this tag.
197         */
198        public boolean isMatch(AuditEvent event)
199        {
200            final int line = event.getLine();
201            if (line < firstLine) {
202                return false;
203            }
204            if (line > lastLine) {
205                return false;
206            }
207            final Matcher tagMatcher =
208                tagCheckRegexp.matcher(event.getSourceName());
209            if (tagMatcher.find()) {
210                return true;
211            }
212            if (tagMessageRegexp != null) {
213                final Matcher messageMatcher =
214                    tagMessageRegexp.matcher(event.getMessage());
215                return messageMatcher.find();
216            }
217            return false;
218        }
219
220        /**
221         * Expand based on a matching comment.
222         * @param comment the comment.
223         * @param string the string to expand.
224         * @param regexp the parsed expander.
225         * @return the expanded string
226         */
227        private String expandFrocomment(
228            String comment,
229            String string,
230            Pattern regexp)
231        {
232            final Matcher matcher = regexp.matcher(comment);
233            // Match primarily for effect.
234            if (!matcher.find()) {
235                ///CLOVER:OFF
236                return string;
237                ///CLOVER:ON
238            }
239            String result = string;
240            for (int i = 0; i <= matcher.groupCount(); i++) {
241                // $n expands comment match like in Pattern.subst().
242                result = result.replaceAll("\\$" + i, matcher.group(i));
243            }
244            return result;
245        }
246
247        /** {@inheritDoc} */
248        @Override
249        public final String toString()
250        {
251            return "Tag[lines=[" + getFirstLine() + " to " + getLastLine()
252                + "]; text='" + getText() + "']";
253        }
254    }
255
256    /** Format to turns checkstyle reporting off. */
257    private static final String DEFAULT_COMMENT_FORMAT =
258        "SUPPRESS CHECKSTYLE (\\w+)";
259
260    /** Default regex for checks that should be suppressed. */
261    private static final String DEFAULT_CHECK_FORMAT = ".*";
262
263    /** Default regex for messages that should be suppressed. */
264    private static final String DEFAULT_MESSAGE_FORMAT = null;
265
266    /** Default regex for lines that should be suppressed. */
267    private static final String DEFAULT_INFLUENCE_FORMAT = "0";
268
269    /** Whether to look for trigger in C-style comments. */
270    private boolean checkC = true;
271
272    /** Whether to look for trigger in C++-style comments. */
273    private boolean checkCPP = true;
274
275    /** Parsed comment regexp that marks checkstyle suppression region. */
276    private Pattern commentRegexp;
277
278    /** The comment pattern that triggers suppression. */
279    private String checkFormat;
280
281    /** The parsed check regexp. */
282    private Pattern checkRegexp;
283
284    /** The message format to suppress. */
285    private String messageFormat;
286
287    /** The influence of the suppression comment. */
288    private String influenceFormat;
289
290
291    //TODO: Investigate performance improvement with array
292    /** Tagged comments */
293    private final List<Tag> tags = Lists.newArrayList();
294
295    /**
296     * References the current FileContents for this filter.
297     * Since this is a weak reference to the FileContents, the FileContents
298     * can be reclaimed as soon as the strong references in TreeWalker
299     * and FileContentsHolder are reassigned to the next FileContents,
300     * at which time filtering for the current FileContents is finished.
301     */
302    private WeakReference<FileContents> fileContentsReference =
303        new WeakReference<FileContents>(null);
304
305    /**
306     * Constructs a SuppressionCommentFilter.
307     * Initializes comment on, comment off, and check formats
308     * to defaults.
309     */
310    public SuppressWithNearbyCommentFilter()
311    {
312        if (DEFAULT_COMMENT_FORMAT != null) {
313            setCommentFormat(DEFAULT_COMMENT_FORMAT);
314        }
315        if (DEFAULT_CHECK_FORMAT != null) {
316            setCheckFormat(DEFAULT_CHECK_FORMAT);
317        }
318        if (DEFAULT_MESSAGE_FORMAT != null) {
319            setMessageFormat(DEFAULT_MESSAGE_FORMAT);
320        }
321        if (DEFAULT_INFLUENCE_FORMAT != null) {
322            setInfluenceFormat(DEFAULT_INFLUENCE_FORMAT);
323        }
324    }
325
326    /**
327     * Set the format for a comment that turns off reporting.
328     * @param format a <code>String</code> value.
329     * @throws ConversionException unable to parse format.
330     */
331    public void setCommentFormat(String format)
332        throws ConversionException
333    {
334        try {
335            commentRegexp = Utils.getPattern(format);
336        }
337        catch (final PatternSyntaxException e) {
338            throw new ConversionException("unable to parse " + format, e);
339        }
340    }
341
342    /** @return the FileContents for this filter. */
343    public FileContents getFileContents()
344    {
345        return fileContentsReference.get();
346    }
347
348    /**
349     * Set the FileContents for this filter.
350     * @param fileContents the FileContents for this filter.
351     */
352    public void setFileContents(FileContents fileContents)
353    {
354        fileContentsReference = new WeakReference<FileContents>(fileContents);
355    }
356
357    /**
358     * Set the format for a check.
359     * @param format a <code>String</code> value
360     * @throws ConversionException unable to parse format
361     */
362    public void setCheckFormat(String format)
363        throws ConversionException
364    {
365        try {
366            checkRegexp = Utils.getPattern(format);
367            checkFormat = format;
368        }
369        catch (final PatternSyntaxException e) {
370            throw new ConversionException("unable to parse " + format, e);
371        }
372    }
373
374    /**
375     * Set the format for a message.
376     * @param format a <code>String</code> value
377     * @throws ConversionException unable to parse format
378     */
379    public void setMessageFormat(String format)
380        throws ConversionException
381    {
382        // check that format parses
383        try {
384            Utils.getPattern(format);
385        }
386        catch (final PatternSyntaxException e) {
387            throw new ConversionException("unable to parse " + format, e);
388        }
389        messageFormat = format;
390    }
391
392    /**
393     * Set the format for the influence of this check.
394     * @param format a <code>String</code> value
395     * @throws ConversionException unable to parse format
396     */
397    public void setInfluenceFormat(String format)
398        throws ConversionException
399    {
400        // check that format parses
401        try {
402            Utils.getPattern(format);
403        }
404        catch (final PatternSyntaxException e) {
405            throw new ConversionException("unable to parse " + format, e);
406        }
407        influenceFormat = format;
408    }
409
410
411    /**
412     * Set whether to look in C++ comments.
413     * @param checkCPP <code>true</code> if C++ comments are checked.
414     */
415    public void setCheckCPP(boolean checkCPP)
416    {
417        this.checkCPP = checkCPP;
418    }
419
420    /**
421     * Set whether to look in C comments.
422     * @param checkC <code>true</code> if C comments are checked.
423     */
424    public void setCheckC(boolean checkC)
425    {
426        this.checkC = checkC;
427    }
428
429    /** {@inheritDoc} */
430    @Override
431    public boolean accept(AuditEvent event)
432    {
433        if (event.getLocalizedMessage() == null) {
434            return true;        // A special event.
435        }
436
437        // Lazy update. If the first event for the current file, update file
438        // contents and tag suppressions
439        final FileContents currentContents = FileContentsHolder.getContents();
440        if (currentContents == null) {
441            // we have no contents, so we can not filter.
442            // TODO: perhaps we should notify user somehow?
443            return true;
444        }
445        if (getFileContents() != currentContents) {
446            setFileContents(currentContents);
447            tagSuppressions();
448        }
449        for (final Iterator<Tag> iter = tags.iterator(); iter.hasNext();) {
450            final Tag tag = iter.next();
451            if (tag.isMatch(event)) {
452                return false;
453            }
454        }
455        return true;
456    }
457
458    /**
459     * Collects all the suppression tags for all comments into a list and
460     * sorts the list.
461     */
462    private void tagSuppressions()
463    {
464        tags.clear();
465        final FileContents contents = getFileContents();
466        if (checkCPP) {
467            tagSuppressions(contents.getCppComments().values());
468        }
469        if (checkC) {
470            final Collection<List<TextBlock>> cComments =
471                contents.getCComments().values();
472            for (final List<TextBlock> element : cComments) {
473                tagSuppressions(element);
474            }
475        }
476        Collections.sort(tags);
477    }
478
479    /**
480     * Appends the suppressions in a collection of comments to the full
481     * set of suppression tags.
482     * @param comments the set of comments.
483     */
484    private void tagSuppressions(Collection<TextBlock> comments)
485    {
486        for (final TextBlock comment : comments) {
487            final int startLineNo = comment.getStartLineNo();
488            final String[] text = comment.getText();
489            tagCommentLine(text[0], startLineNo);
490            for (int i = 1; i < text.length; i++) {
491                tagCommentLine(text[i], startLineNo + i);
492            }
493        }
494    }
495
496    /**
497     * Tags a string if it matches the format for turning
498     * checkstyle reporting on or the format for turning reporting off.
499     * @param text the string to tag.
500     * @param line the line number of text.
501     */
502    private void tagCommentLine(String text, int line)
503    {
504        final Matcher matcher = commentRegexp.matcher(text);
505        if (matcher.find()) {
506            addTag(matcher.group(0), line);
507        }
508    }
509
510    /**
511     * Adds a comment suppression <code>Tag</code> to the list of all tags.
512     * @param text the text of the tag.
513     * @param line the line number of the tag.
514     */
515    private void addTag(String text, int line)
516    {
517        final Tag tag = new Tag(text, line);
518        tags.add(tag);
519    }
520}