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 mText;
086
087        /** The first line where warnings may be suppressed. */
088        private int mFirstLine;
089
090        /** The last line where warnings may be suppressed. */
091        private int mLastLine;
092
093        /** The parsed check regexp, expanded for the text of this tag. */
094        private Pattern mTagCheckRegexp;
095
096        /** The parsed message regexp, expanded for the text of this tag. */
097        private Pattern mTagMessageRegexp;
098
099        /**
100         * Constructs a tag.
101         * @param aText the text of the suppression.
102         * @param aLine the line number.
103         * @throws ConversionException if unable to parse expanded aText.
104         * on.
105         */
106        public Tag(String aText, int aLine)
107            throws ConversionException
108        {
109            mText = aText;
110
111            mTagCheckRegexp = mCheckRegexp;
112            //Expand regexp for check and message
113            //Does not intern Patterns with Utils.getPattern()
114            String format = "";
115            try {
116                format = expandFromComment(aText, mCheckFormat, mCommentRegexp);
117                mTagCheckRegexp = Pattern.compile(format);
118                if (mMessageFormat != null) {
119                    format = expandFromComment(
120                         aText, mMessageFormat, mCommentRegexp);
121                    mTagMessageRegexp = Pattern.compile(format);
122                }
123                int influence = 0;
124                if (mInfluenceFormat != null) {
125                    format = expandFromComment(
126                        aText, mInfluenceFormat, mCommentRegexp);
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 '" + aText
136                                + "' using " + mInfluenceFormat, e);
137                    }
138                }
139                if (influence >= 0) {
140                    mFirstLine = aLine;
141                    mLastLine = aLine + influence;
142                }
143                else {
144                    mFirstLine = aLine + influence;
145                    mLastLine = aLine;
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 mText;
159        }
160
161        /** @return the line number of the first suppressed line. */
162        public int getFirstLine()
163        {
164            return mFirstLine;
165        }
166
167        /** @return the line number of the last suppressed line. */
168        public int getLastLine()
169        {
170            return mLastLine;
171        }
172
173        /**
174         * Compares the position of this tag in the file
175         * with the position of another tag.
176         * @param aOther 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        public int compareTo(Tag aOther)
183        {
184            if (mFirstLine == aOther.mFirstLine) {
185                return mLastLine - aOther.mLastLine;
186            }
187
188            return (mFirstLine - aOther.mFirstLine);
189        }
190
191        /**
192         * Determines whether the source of an audit event
193         * matches the text of this tag.
194         * @param aEvent the <code>AuditEvent</code> to check.
195         * @return true if the source of aEvent matches the text of this tag.
196         */
197        public boolean isMatch(AuditEvent aEvent)
198        {
199            final int line = aEvent.getLine();
200            if (line < mFirstLine) {
201                return false;
202            }
203            if (line > mLastLine) {
204                return false;
205            }
206            final Matcher tagMatcher =
207                mTagCheckRegexp.matcher(aEvent.getSourceName());
208            if (tagMatcher.find()) {
209                return true;
210            }
211            if (mTagMessageRegexp != null) {
212                final Matcher messageMatcher =
213                    mTagMessageRegexp.matcher(aEvent.getMessage());
214                return messageMatcher.find();
215            }
216            return false;
217        }
218
219        /**
220         * Expand based on a matching comment.
221         * @param aComment the comment.
222         * @param aString the string to expand.
223         * @param aRegexp the parsed expander.
224         * @return the expanded string
225         */
226        private String expandFromComment(
227            String aComment,
228            String aString,
229            Pattern aRegexp)
230        {
231            final Matcher matcher = aRegexp.matcher(aComment);
232            // Match primarily for effect.
233            if (!matcher.find()) {
234                ///CLOVER:OFF
235                return aString;
236                ///CLOVER:ON
237            }
238            String result = aString;
239            for (int i = 0; i <= matcher.groupCount(); i++) {
240                // $n expands comment match like in Pattern.subst().
241                result = result.replaceAll("\\$" + i, matcher.group(i));
242            }
243            return result;
244        }
245
246        /** {@inheritDoc} */
247        @Override
248        public final String toString()
249        {
250            return "Tag[lines=[" + getFirstLine() + " to " + getLastLine()
251                + "]; text='" + getText() + "']";
252        }
253    }
254
255    /** Format to turns checkstyle reporting off. */
256    private static final String DEFAULT_COMMENT_FORMAT =
257        "SUPPRESS CHECKSTYLE (\\w+)";
258
259    /** Default regex for checks that should be suppressed. */
260    private static final String DEFAULT_CHECK_FORMAT = ".*";
261
262    /** Default regex for messages that should be suppressed. */
263    private static final String DEFAULT_MESSAGE_FORMAT = null;
264
265    /** Default regex for lines that should be suppressed. */
266    private static final String DEFAULT_INFLUENCE_FORMAT = "0";
267
268    /** Whether to look for trigger in C-style comments. */
269    private boolean mCheckC = true;
270
271    /** Whether to look for trigger in C++-style comments. */
272    private boolean mCheckCPP = true;
273
274    /** Parsed comment regexp that marks checkstyle suppression region. */
275    private Pattern mCommentRegexp;
276
277    /** The comment pattern that triggers suppression. */
278    private String mCheckFormat;
279
280    /** The parsed check regexp. */
281    private Pattern mCheckRegexp;
282
283    /** The message format to suppress. */
284    private String mMessageFormat;
285
286    /** The influence of the suppression comment. */
287    private String mInfluenceFormat;
288
289
290    //TODO: Investigate performance improvement with array
291    /** Tagged comments */
292    private final List<Tag> mTags = Lists.newArrayList();
293
294    /**
295     * References the current FileContents for this filter.
296     * Since this is a weak reference to the FileContents, the FileContents
297     * can be reclaimed as soon as the strong references in TreeWalker
298     * and FileContentsHolder are reassigned to the next FileContents,
299     * at which time filtering for the current FileContents is finished.
300     */
301    private WeakReference<FileContents> mFileContentsReference =
302        new WeakReference<FileContents>(null);
303
304    /**
305     * Constructs a SuppressionCommentFilter.
306     * Initializes comment on, comment off, and check formats
307     * to defaults.
308     */
309    public SuppressWithNearbyCommentFilter()
310    {
311        if (DEFAULT_COMMENT_FORMAT != null) {
312            setCommentFormat(DEFAULT_COMMENT_FORMAT);
313        }
314        if (DEFAULT_CHECK_FORMAT != null) {
315            setCheckFormat(DEFAULT_CHECK_FORMAT);
316        }
317        if (DEFAULT_MESSAGE_FORMAT != null) {
318            setMessageFormat(DEFAULT_MESSAGE_FORMAT);
319        }
320        if (DEFAULT_INFLUENCE_FORMAT != null) {
321            setInfluenceFormat(DEFAULT_INFLUENCE_FORMAT);
322        }
323    }
324
325    /**
326     * Set the format for a comment that turns off reporting.
327     * @param aFormat a <code>String</code> value.
328     * @throws ConversionException unable to parse aFormat.
329     */
330    public void setCommentFormat(String aFormat)
331        throws ConversionException
332    {
333        try {
334            mCommentRegexp = Utils.getPattern(aFormat);
335        }
336        catch (final PatternSyntaxException e) {
337            throw new ConversionException("unable to parse " + aFormat, e);
338        }
339    }
340
341    /** @return the FileContents for this filter. */
342    public FileContents getFileContents()
343    {
344        return mFileContentsReference.get();
345    }
346
347    /**
348     * Set the FileContents for this filter.
349     * @param aFileContents the FileContents for this filter.
350     */
351    public void setFileContents(FileContents aFileContents)
352    {
353        mFileContentsReference = new WeakReference<FileContents>(aFileContents);
354    }
355
356    /**
357     * Set the format for a check.
358     * @param aFormat a <code>String</code> value
359     * @throws ConversionException unable to parse aFormat
360     */
361    public void setCheckFormat(String aFormat)
362        throws ConversionException
363    {
364        try {
365            mCheckRegexp = Utils.getPattern(aFormat);
366            mCheckFormat = aFormat;
367        }
368        catch (final PatternSyntaxException e) {
369            throw new ConversionException("unable to parse " + aFormat, e);
370        }
371    }
372
373    /**
374     * Set the format for a message.
375     * @param aFormat a <code>String</code> value
376     * @throws ConversionException unable to parse aFormat
377     */
378    public void setMessageFormat(String aFormat)
379        throws ConversionException
380    {
381        // check that aFormat parses
382        try {
383            Utils.getPattern(aFormat);
384        }
385        catch (final PatternSyntaxException e) {
386            throw new ConversionException("unable to parse " + aFormat, e);
387        }
388        mMessageFormat = aFormat;
389    }
390
391    /**
392     * Set the format for the influence of this check.
393     * @param aFormat a <code>String</code> value
394     * @throws ConversionException unable to parse aFormat
395     */
396    public void setInfluenceFormat(String aFormat)
397        throws ConversionException
398    {
399        // check that aFormat parses
400        try {
401            Utils.getPattern(aFormat);
402        }
403        catch (final PatternSyntaxException e) {
404            throw new ConversionException("unable to parse " + aFormat, e);
405        }
406        mInfluenceFormat = aFormat;
407    }
408
409
410    /**
411     * Set whether to look in C++ comments.
412     * @param aCheckCPP <code>true</code> if C++ comments are checked.
413     */
414    public void setCheckCPP(boolean aCheckCPP)
415    {
416        mCheckCPP = aCheckCPP;
417    }
418
419    /**
420     * Set whether to look in C comments.
421     * @param aCheckC <code>true</code> if C comments are checked.
422     */
423    public void setCheckC(boolean aCheckC)
424    {
425        mCheckC = aCheckC;
426    }
427
428    /** {@inheritDoc} */
429    public boolean accept(AuditEvent aEvent)
430    {
431        if (aEvent.getLocalizedMessage() == null) {
432            return true;        // A special event.
433        }
434
435        // Lazy update. If the first event for the current file, update file
436        // contents and tag suppressions
437        final FileContents currentContents = FileContentsHolder.getContents();
438        if (currentContents == null) {
439            // we have no contents, so we can not filter.
440            // TODO: perhaps we should notify user somehow?
441            return true;
442        }
443        if (getFileContents() != currentContents) {
444            setFileContents(currentContents);
445            tagSuppressions();
446        }
447        for (final Iterator<Tag> iter = mTags.iterator(); iter.hasNext();) {
448            final Tag tag = iter.next();
449            if (tag.isMatch(aEvent)) {
450                return false;
451            }
452        }
453        return true;
454    }
455
456    /**
457     * Collects all the suppression tags for all comments into a list and
458     * sorts the list.
459     */
460    private void tagSuppressions()
461    {
462        mTags.clear();
463        final FileContents contents = getFileContents();
464        if (mCheckCPP) {
465            tagSuppressions(contents.getCppComments().values());
466        }
467        if (mCheckC) {
468            final Collection<List<TextBlock>> cComments =
469                contents.getCComments().values();
470            for (final List<TextBlock> element : cComments) {
471                tagSuppressions(element);
472            }
473        }
474        Collections.sort(mTags);
475    }
476
477    /**
478     * Appends the suppressions in a collection of comments to the full
479     * set of suppression tags.
480     * @param aComments the set of comments.
481     */
482    private void tagSuppressions(Collection<TextBlock> aComments)
483    {
484        for (final TextBlock comment : aComments) {
485            final int startLineNo = comment.getStartLineNo();
486            final String[] text = comment.getText();
487            tagCommentLine(text[0], startLineNo);
488            for (int i = 1; i < text.length; i++) {
489                tagCommentLine(text[i], startLineNo + i);
490            }
491        }
492    }
493
494    /**
495     * Tags a string if it matches the format for turning
496     * checkstyle reporting on or the format for turning reporting off.
497     * @param aText the string to tag.
498     * @param aLine the line number of aText.
499     */
500    private void tagCommentLine(String aText, int aLine)
501    {
502        final Matcher matcher = mCommentRegexp.matcher(aText);
503        if (matcher.find()) {
504            addTag(matcher.group(0), aLine);
505        }
506    }
507
508    /**
509     * Adds a comment suppression <code>Tag</code> to the list of all tags.
510     * @param aText the text of the tag.
511     * @param aLine the line number of the tag.
512     */
513    private void addTag(String aText, int aLine)
514    {
515        final Tag tag = new Tag(aText, aLine);
516        mTags.add(tag);
517    }
518}