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.List;
033import java.util.regex.Matcher;
034import java.util.regex.Pattern;
035import java.util.regex.PatternSyntaxException;
036import org.apache.commons.beanutils.ConversionException;
037
038/**
039 * <p>
040 * A filter that uses comments to suppress audit events.
041 * </p>
042 * <p>
043 * Rationale:
044 * Sometimes there are legitimate reasons for violating a check.  When
045 * this is a matter of the code in question and not personal
046 * preference, the best place to override the policy is in the code
047 * itself.  Semi-structured comments can be associated with the check.
048 * This is sometimes superior to a separate suppressions file, which
049 * must be kept up-to-date as the source file is edited.
050 * </p>
051 * <p>
052 * Usage:
053 * This check only works in conjunction with the FileContentsHolder module
054 * since that module makes the suppression comments in the .java
055 * files available <i>sub rosa</i>.
056 * </p>
057 * @see FileContentsHolder
058 * @author Mike McMahon
059 * @author Rick Giles
060 */
061public class SuppressionCommentFilter
062    extends AutomaticBean
063    implements Filter
064{
065    /**
066     * A Tag holds a suppression comment and its location, and determines
067     * whether the supression turns checkstyle reporting on or off.
068     * @author Rick Giles
069     */
070    public class Tag
071        implements Comparable<Tag>
072    {
073        /** The text of the tag. */
074        private final String mText;
075
076        /** The line number of the tag. */
077        private final int mLine;
078
079        /** The column number of the tag. */
080        private final int mColumn;
081
082        /** Determines whether the suppression turns checkstyle reporting on. */
083        private final boolean mOn;
084
085        /** The parsed check regexp, expanded for the text of this tag. */
086        private Pattern mTagCheckRegexp;
087
088        /** The parsed message regexp, expanded for the text of this tag. */
089        private Pattern mTagMessageRegexp;
090
091        /**
092         * Constructs a tag.
093         * @param aLine the line number.
094         * @param aColumn the column number.
095         * @param aText the text of the suppression.
096         * @param aOn <code>true</code> if the tag turns checkstyle reporting.
097         * @throws ConversionException if unable to parse expanded aText.
098         * on.
099         */
100        public Tag(int aLine, int aColumn, String aText, boolean aOn)
101            throws ConversionException
102        {
103            mLine = aLine;
104            mColumn = aColumn;
105            mText = aText;
106            mOn = aOn;
107
108            mTagCheckRegexp = mCheckRegexp;
109            //Expand regexp for check and message
110            //Does not intern Patterns with Utils.getPattern()
111            String format = "";
112            try {
113                if (aOn) {
114                    format =
115                        expandFromComment(aText, mCheckFormat, mOnRegexp);
116                    mTagCheckRegexp = Pattern.compile(format);
117                    if (mMessageFormat != null) {
118                        format =
119                            expandFromComment(aText, mMessageFormat, mOnRegexp);
120                        mTagMessageRegexp = Pattern.compile(format);
121                    }
122                }
123                else {
124                    format =
125                        expandFromComment(aText, mCheckFormat, mOffRegexp);
126                    mTagCheckRegexp = Pattern.compile(format);
127                    if (mMessageFormat != null) {
128                        format =
129                            expandFromComment(
130                                aText,
131                                mMessageFormat,
132                                mOffRegexp);
133                        mTagMessageRegexp = Pattern.compile(format);
134                    }
135                }
136            }
137            catch (final PatternSyntaxException e) {
138                throw new ConversionException(
139                    "unable to parse expanded comment " + format,
140                    e);
141            }
142        }
143
144        /** @return the text of the tag. */
145        public String getText()
146        {
147            return mText;
148        }
149
150        /** @return the line number of the tag in the source file. */
151        public int getLine()
152        {
153            return mLine;
154        }
155
156        /**
157         * Determines the column number of the tag in the source file.
158         * Will be 0 for all lines of multiline comment, except the
159         * first line.
160         * @return the column number of the tag in the source file.
161         */
162        public int getColumn()
163        {
164            return mColumn;
165        }
166
167        /**
168         * Determines whether the suppression turns checkstyle reporting on or
169         * off.
170         * @return <code>true</code>if the suppression turns reporting on.
171         */
172        public boolean isOn()
173        {
174            return mOn;
175        }
176
177        /**
178         * Compares the position of this tag in the file
179         * with the position of another tag.
180         * @param aObject the tag to compare with this one.
181         * @return a negative number if this tag is before the other tag,
182         * 0 if they are at the same position, and a positive number if this
183         * tag is after the other tag.
184         * @see java.lang.Comparable#compareTo(java.lang.Object)
185         */
186        public int compareTo(Tag aObject)
187        {
188            if (mLine == aObject.mLine) {
189                return mColumn - aObject.mColumn;
190            }
191
192            return (mLine - aObject.mLine);
193        }
194
195        /**
196         * Determines whether the source of an audit event
197         * matches the text of this tag.
198         * @param aEvent the <code>AuditEvent</code> to check.
199         * @return true if the source of aEvent matches the text of this tag.
200         */
201        public boolean isMatch(AuditEvent aEvent)
202        {
203            final Matcher tagMatcher =
204                mTagCheckRegexp.matcher(aEvent.getSourceName());
205            if (tagMatcher.find()) {
206                if (mTagMessageRegexp != null) {
207                    final Matcher messageMatcher =
208                            mTagMessageRegexp.matcher(aEvent.getMessage());
209                    return messageMatcher.find();
210                }
211                return true;
212            }
213            return false;
214        }
215
216        /**
217         * Expand based on a matching comment.
218         * @param aComment the comment.
219         * @param aString the string to expand.
220         * @param aRegexp the parsed expander.
221         * @return the expanded string
222         */
223        private String expandFromComment(
224            String aComment,
225            String aString,
226            Pattern aRegexp)
227        {
228            final Matcher matcher = aRegexp.matcher(aComment);
229            // Match primarily for effect.
230            if (!matcher.find()) {
231                ///CLOVER:OFF
232                return aString;
233                ///CLOVER:ON
234            }
235            String result = aString;
236            for (int i = 0; i <= matcher.groupCount(); i++) {
237                // $n expands comment match like in Pattern.subst().
238                result = result.replaceAll("\\$" + i, matcher.group(i));
239            }
240            return result;
241        }
242
243        @Override
244        public final String toString()
245        {
246            return "Tag[line=" + getLine() + "; col=" + getColumn()
247                + "; on=" + isOn() + "; text='" + getText() + "']";
248        }
249    }
250
251    /** Turns checkstyle reporting off. */
252    private static final String DEFAULT_OFF_FORMAT = "CHECKSTYLE\\:OFF";
253
254    /** Turns checkstyle reporting on. */
255    private static final String DEFAULT_ON_FORMAT = "CHECKSTYLE\\:ON";
256
257    /** Control all checks */
258    private static final String DEFAULT_CHECK_FORMAT = ".*";
259
260    /** Whether to look in comments of the C type. */
261    private boolean mCheckC = true;
262
263    /** Whether to look in comments of the C++ type. */
264    private boolean mCheckCPP = true;
265
266    /** Parsed comment regexp that turns checkstyle reporting off. */
267    private Pattern mOffRegexp;
268
269    /** Parsed comment regexp that turns checkstyle reporting on. */
270    private Pattern mOnRegexp;
271
272    /** The check format to suppress. */
273    private String mCheckFormat;
274
275    /** The parsed check regexp. */
276    private Pattern mCheckRegexp;
277
278    /** The message format to suppress. */
279    private String mMessageFormat;
280
281    //TODO: Investigate performance improvement with array
282    /** Tagged comments */
283    private final List<Tag> mTags = Lists.newArrayList();
284
285    /**
286     * References the current FileContents for this filter.
287     * Since this is a weak reference to the FileContents, the FileContents
288     * can be reclaimed as soon as the strong references in TreeWalker
289     * and FileContentsHolder are reassigned to the next FileContents,
290     * at which time filtering for the current FileContents is finished.
291     */
292    private WeakReference<FileContents> mFileContentsReference =
293        new WeakReference<FileContents>(null);
294
295    /**
296     * Constructs a SuppressionCommentFilter.
297     * Initializes comment on, comment off, and check formats
298     * to defaults.
299     */
300    public SuppressionCommentFilter()
301    {
302        setOnCommentFormat(DEFAULT_ON_FORMAT);
303        setOffCommentFormat(DEFAULT_OFF_FORMAT);
304        setCheckFormat(DEFAULT_CHECK_FORMAT);
305    }
306
307    /**
308     * Set the format for a comment that turns off reporting.
309     * @param aFormat a <code>String</code> value.
310     * @throws ConversionException unable to parse aFormat.
311     */
312    public void setOffCommentFormat(String aFormat)
313        throws ConversionException
314    {
315        try {
316            mOffRegexp = Utils.getPattern(aFormat);
317        }
318        catch (final PatternSyntaxException e) {
319            throw new ConversionException("unable to parse " + aFormat, e);
320        }
321    }
322
323    /**
324     * Set the format for a comment that turns on reporting.
325     * @param aFormat a <code>String</code> value
326     * @throws ConversionException unable to parse aFormat
327     */
328    public void setOnCommentFormat(String aFormat)
329        throws ConversionException
330    {
331        try {
332            mOnRegexp = Utils.getPattern(aFormat);
333        }
334        catch (final PatternSyntaxException e) {
335            throw new ConversionException("unable to parse " + aFormat, e);
336        }
337    }
338
339    /** @return the FileContents for this filter. */
340    public FileContents getFileContents()
341    {
342        return mFileContentsReference.get();
343    }
344
345    /**
346     * Set the FileContents for this filter.
347     * @param aFileContents the FileContents for this filter.
348     */
349    public void setFileContents(FileContents aFileContents)
350    {
351        mFileContentsReference = new WeakReference<FileContents>(aFileContents);
352    }
353
354    /**
355     * Set the format for a check.
356     * @param aFormat a <code>String</code> value
357     * @throws ConversionException unable to parse aFormat
358     */
359    public void setCheckFormat(String aFormat)
360        throws ConversionException
361    {
362        try {
363            mCheckRegexp = Utils.getPattern(aFormat);
364            mCheckFormat = aFormat;
365        }
366        catch (final PatternSyntaxException e) {
367            throw new ConversionException("unable to parse " + aFormat, e);
368        }
369    }
370
371    /**
372     * Set the format for a message.
373     * @param aFormat a <code>String</code> value
374     * @throws ConversionException unable to parse aFormat
375     */
376    public void setMessageFormat(String aFormat)
377        throws ConversionException
378    {
379        // check that aFormat parses
380        try {
381            Utils.getPattern(aFormat);
382        }
383        catch (final PatternSyntaxException e) {
384            throw new ConversionException("unable to parse " + aFormat, e);
385        }
386        mMessageFormat = aFormat;
387    }
388
389
390    /**
391     * Set whether to look in C++ comments.
392     * @param aCheckCPP <code>true</code> if C++ comments are checked.
393     */
394    public void setCheckCPP(boolean aCheckCPP)
395    {
396        mCheckCPP = aCheckCPP;
397    }
398
399    /**
400     * Set whether to look in C comments.
401     * @param aCheckC <code>true</code> if C comments are checked.
402     */
403    public void setCheckC(boolean aCheckC)
404    {
405        mCheckC = aCheckC;
406    }
407
408    /** {@inheritDoc} */
409    public boolean accept(AuditEvent aEvent)
410    {
411        if (aEvent.getLocalizedMessage() == null) {
412            return true;        // A special event.
413        }
414
415        // Lazy update. If the first event for the current file, update file
416        // contents and tag suppressions
417        final FileContents currentContents = FileContentsHolder.getContents();
418        if (currentContents == null) {
419            // we have no contents, so we can not filter.
420            // TODO: perhaps we should notify user somehow?
421            return true;
422        }
423        if (getFileContents() != currentContents) {
424            setFileContents(currentContents);
425            tagSuppressions();
426        }
427        final Tag matchTag = findNearestMatch(aEvent);
428        if ((matchTag != null) && !matchTag.isOn()) {
429            return false;
430        }
431        return true;
432    }
433
434    /**
435     * Finds the nearest comment text tag that matches an audit event.
436     * The nearest tag is before the line and column of the event.
437     * @param aEvent the <code>AuditEvent</code> to match.
438     * @return The <code>Tag</code> nearest aEvent.
439     */
440    private Tag findNearestMatch(AuditEvent aEvent)
441    {
442        Tag result = null;
443        // TODO: try binary search if sequential search becomes a performance
444        // problem.
445        for (Tag tag : mTags) {
446            if ((tag.getLine() > aEvent.getLine())
447                || ((tag.getLine() == aEvent.getLine())
448                    && (tag.getColumn() > aEvent.getColumn())))
449            {
450                break;
451            }
452            if (tag.isMatch(aEvent)) {
453                result = tag;
454            }
455        };
456        return result;
457    }
458
459    /**
460     * Collects all the suppression tags for all comments into a list and
461     * sorts the list.
462     */
463    private void tagSuppressions()
464    {
465        mTags.clear();
466        final FileContents contents = getFileContents();
467        if (mCheckCPP) {
468            tagSuppressions(contents.getCppComments().values());
469        }
470        if (mCheckC) {
471            final Collection<List<TextBlock>> cComments = contents
472                    .getCComments().values();
473            for (List<TextBlock> element : cComments) {
474                tagSuppressions(element);
475            }
476        }
477        Collections.sort(mTags);
478    }
479
480    /**
481     * Appends the suppressions in a collection of comments to the full
482     * set of suppression tags.
483     * @param aComments the set of comments.
484     */
485    private void tagSuppressions(Collection<TextBlock> aComments)
486    {
487        for (TextBlock comment : aComments) {
488            final int startLineNo = comment.getStartLineNo();
489            final String[] text = comment.getText();
490            tagCommentLine(text[0], startLineNo, comment.getStartColNo());
491            for (int i = 1; i < text.length; i++) {
492                tagCommentLine(text[i], startLineNo + i, 0);
493            }
494        }
495    }
496
497    /**
498     * Tags a string if it matches the format for turning
499     * checkstyle reporting on or the format for turning reporting off.
500     * @param aText the string to tag.
501     * @param aLine the line number of aText.
502     * @param aColumn the column number of aText.
503     */
504    private void tagCommentLine(String aText, int aLine, int aColumn)
505    {
506        final Matcher offMatcher = mOffRegexp.matcher(aText);
507        if (offMatcher.find()) {
508            addTag(offMatcher.group(0), aLine, aColumn, false);
509        }
510        else {
511            final Matcher onMatcher = mOnRegexp.matcher(aText);
512            if (onMatcher.find()) {
513                addTag(onMatcher.group(0), aLine, aColumn, true);
514            }
515        }
516    }
517
518    /**
519     * Adds a <code>Tag</code> to the list of all tags.
520     * @param aText the text of the tag.
521     * @param aLine the line number of the tag.
522     * @param aColumn the column number of the tag.
523     * @param aOn <code>true</code> if the tag turns checkstyle reporting on.
524     */
525    private void addTag(String aText, int aLine, int aColumn, boolean aOn)
526    {
527        final Tag tag = new Tag(aLine, aColumn, aText, aOn);
528        mTags.add(tag);
529    }
530}