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 comments to suppress audit events.
045 * </p>
046 * <p>
047 * Rationale:
048 * Sometimes there are legitimate reasons for violating a check.  When
049 * this is a matter of the code in question and not personal
050 * preference, the best place to override the policy is in the code
051 * itself.  Semi-structured comments can be associated with the check.
052 * This is sometimes superior to a separate suppressions file, which
053 * must be kept up-to-date as the source file is edited.
054 * </p>
055 * <p>
056 * Usage:
057 * This check only works in conjunction with the FileContentsHolder module
058 * since that module makes the suppression comments in the .java
059 * files available <i>sub rosa</i>.
060 * </p>
061 * @author Mike McMahon
062 * @author Rick Giles
063 * @see FileContentsHolder
064 */
065public class SuppressionCommentFilter
066    extends AutomaticBean
067    implements Filter {
068
069    /** Turns checkstyle reporting off. */
070    private static final String DEFAULT_OFF_FORMAT = "CHECKSTYLE\\:OFF";
071
072    /** Turns checkstyle reporting on. */
073    private static final String DEFAULT_ON_FORMAT = "CHECKSTYLE\\:ON";
074
075    /** Control all checks. */
076    private static final String DEFAULT_CHECK_FORMAT = ".*";
077
078    /** Whether to look in comments of the C type. */
079    private boolean checkC = true;
080
081    /** Whether to look in comments of the C++ type. */
082    private boolean checkCPP = true;
083
084    /** Parsed comment regexp that turns checkstyle reporting off. */
085    private Pattern offRegexp;
086
087    /** Parsed comment regexp that turns checkstyle reporting on. */
088    private Pattern onRegexp;
089
090    /** The check format to suppress. */
091    private String checkFormat;
092
093    /** The message format to suppress. */
094    private String messageFormat;
095
096    /** Tagged comments. */
097    private final List<Tag> tags = Lists.newArrayList();
098
099    /**
100     * References the current FileContents for this filter.
101     * Since this is a weak reference to the FileContents, the FileContents
102     * can be reclaimed as soon as the strong references in TreeWalker
103     * and FileContentsHolder are reassigned to the next FileContents,
104     * at which time filtering for the current FileContents is finished.
105     */
106    private WeakReference<FileContents> fileContentsReference = new WeakReference<>(null);
107
108    /**
109     * Constructs a SuppressionCommentFilter.
110     * Initializes comment on, comment off, and check formats
111     * to defaults.
112     */
113    public SuppressionCommentFilter() {
114        setOnCommentFormat(DEFAULT_ON_FORMAT);
115        setOffCommentFormat(DEFAULT_OFF_FORMAT);
116        checkFormat = DEFAULT_CHECK_FORMAT;
117    }
118
119    /**
120     * Set the format for a comment that turns off reporting.
121     * @param format a {@code String} value.
122     * @throws ConversionException if unable to create Pattern object.
123     */
124    public final void setOffCommentFormat(String format) {
125        offRegexp = CommonUtils.createPattern(format);
126    }
127
128    /**
129     * Set the format for a comment that turns on reporting.
130     * @param format a {@code String} value
131     * @throws ConversionException if unable to create Pattern object.
132     */
133    public final void setOnCommentFormat(String format) {
134        onRegexp = CommonUtils.createPattern(format);
135    }
136
137    /**
138     * @return the FileContents for this filter.
139     */
140    public FileContents getFileContents() {
141        return fileContentsReference.get();
142    }
143
144    /**
145     * Set the FileContents for this filter.
146     * @param fileContents the FileContents for this filter.
147     */
148    public void setFileContents(FileContents fileContents) {
149        fileContentsReference = new WeakReference<>(fileContents);
150    }
151
152    /**
153     * Set the format for a check.
154     * @param format a {@code String} value
155     */
156    public final void setCheckFormat(String format) {
157        checkFormat = format;
158    }
159
160    /**
161     * Set the format for a message.
162     * @param format a {@code String} value
163     */
164    public void setMessageFormat(String format) {
165        messageFormat = format;
166    }
167
168    /**
169     * Set whether to look in C++ comments.
170     * @param checkCpp {@code true} if C++ comments are checked.
171     */
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    public boolean accept(AuditEvent event) {
186        boolean accepted = true;
187
188        if (event.getLocalizedMessage() != null) {
189            // Lazy update. If the first event for the current file, update file
190            // contents and tag suppressions
191            final FileContents currentContents = FileContentsHolder.getContents();
192
193            if (currentContents != null) {
194                if (getFileContents() != currentContents) {
195                    setFileContents(currentContents);
196                    tagSuppressions();
197                }
198                final Tag matchTag = findNearestMatch(event);
199                accepted = matchTag == null || matchTag.isReportingOn();
200            }
201        }
202        return accepted;
203    }
204
205    /**
206     * Finds the nearest comment text tag that matches an audit event.
207     * The nearest tag is before the line and column of the event.
208     * @param event the {@code AuditEvent} to match.
209     * @return The {@code Tag} nearest event.
210     */
211    private Tag findNearestMatch(AuditEvent event) {
212        Tag result = null;
213        for (Tag tag : tags) {
214            if (tag.getLine() > event.getLine()
215                || tag.getLine() == event.getLine()
216                    && tag.getColumn() > event.getColumn()) {
217                break;
218            }
219            if (tag.isMatch(event)) {
220                result = tag;
221            }
222        }
223        return result;
224    }
225
226    /**
227     * Collects all the suppression tags for all comments into a list and
228     * sorts the list.
229     */
230    private void tagSuppressions() {
231        tags.clear();
232        final FileContents contents = getFileContents();
233        if (checkCPP) {
234            tagSuppressions(contents.getCppComments().values());
235        }
236        if (checkC) {
237            final Collection<List<TextBlock>> cComments = contents
238                    .getCComments().values();
239            for (List<TextBlock> element : cComments) {
240                tagSuppressions(element);
241            }
242        }
243        Collections.sort(tags);
244    }
245
246    /**
247     * Appends the suppressions in a collection of comments to the full
248     * set of suppression tags.
249     * @param comments the set of comments.
250     */
251    private void tagSuppressions(Collection<TextBlock> comments) {
252        for (TextBlock comment : comments) {
253            final int startLineNo = comment.getStartLineNo();
254            final String[] text = comment.getText();
255            tagCommentLine(text[0], startLineNo, comment.getStartColNo());
256            for (int i = 1; i < text.length; i++) {
257                tagCommentLine(text[i], startLineNo + i, 0);
258            }
259        }
260    }
261
262    /**
263     * Tags a string if it matches the format for turning
264     * checkstyle reporting on or the format for turning reporting off.
265     * @param text the string to tag.
266     * @param line the line number of text.
267     * @param column the column number of text.
268     */
269    private void tagCommentLine(String text, int line, int column) {
270        final Matcher offMatcher = offRegexp.matcher(text);
271        if (offMatcher.find()) {
272            addTag(offMatcher.group(0), line, column, false);
273        }
274        else {
275            final Matcher onMatcher = onRegexp.matcher(text);
276            if (onMatcher.find()) {
277                addTag(onMatcher.group(0), line, column, true);
278            }
279        }
280    }
281
282    /**
283     * Adds a {@code Tag} to the list of all tags.
284     * @param text the text of the tag.
285     * @param line the line number of the tag.
286     * @param column the column number of the tag.
287     * @param reportingOn {@code true} if the tag turns checkstyle reporting on.
288     */
289    private void addTag(String text, int line, int column, boolean reportingOn) {
290        final Tag tag = new Tag(line, column, text, reportingOn, this);
291        tags.add(tag);
292    }
293
294    /**
295     * A Tag holds a suppression comment and its location, and determines
296     * whether the suppression turns checkstyle reporting on or off.
297     * @author Rick Giles
298     */
299    public static class Tag
300        implements Comparable<Tag> {
301        /** The text of the tag. */
302        private final String text;
303
304        /** The line number of the tag. */
305        private final int line;
306
307        /** The column number of the tag. */
308        private final int column;
309
310        /** Determines whether the suppression turns checkstyle reporting on. */
311        private final boolean reportingOn;
312
313        /** The parsed check regexp, expanded for the text of this tag. */
314        private final Pattern tagCheckRegexp;
315
316        /** The parsed message regexp, expanded for the text of this tag. */
317        private final Pattern tagMessageRegexp;
318
319        /**
320         * Constructs a tag.
321         * @param line the line number.
322         * @param column the column number.
323         * @param text the text of the suppression.
324         * @param reportingOn {@code true} if the tag turns checkstyle reporting.
325         * @param filter the {@code SuppressionCommentFilter} with the context
326         * @throws ConversionException if unable to parse expanded text.
327         */
328        public Tag(int line, int column, String text, boolean reportingOn,
329                   SuppressionCommentFilter filter) {
330            this.line = line;
331            this.column = column;
332            this.text = text;
333            this.reportingOn = reportingOn;
334
335            //Expand regexp for check and message
336            //Does not intern Patterns with Utils.getPattern()
337            String format = "";
338            try {
339                if (reportingOn) {
340                    format = CommonUtils.fillTemplateWithStringsByRegexp(
341                            filter.checkFormat, text, filter.onRegexp);
342                    tagCheckRegexp = Pattern.compile(format);
343                    if (filter.messageFormat == null) {
344                        tagMessageRegexp = null;
345                    }
346                    else {
347                        format = CommonUtils.fillTemplateWithStringsByRegexp(
348                                filter.messageFormat, text, filter.onRegexp);
349                        tagMessageRegexp = Pattern.compile(format);
350                    }
351                }
352                else {
353                    format = CommonUtils.fillTemplateWithStringsByRegexp(
354                            filter.checkFormat, text, filter.offRegexp);
355                    tagCheckRegexp = Pattern.compile(format);
356                    if (filter.messageFormat == null) {
357                        tagMessageRegexp = null;
358                    }
359                    else {
360                        format = CommonUtils.fillTemplateWithStringsByRegexp(
361                                filter.messageFormat, text, filter.offRegexp);
362                        tagMessageRegexp = Pattern.compile(format);
363                    }
364                }
365            }
366            catch (final PatternSyntaxException e) {
367                throw new ConversionException(
368                    "unable to parse expanded comment " + format,
369                    e);
370            }
371        }
372
373        /**
374         * @return the line number of the tag in the source file.
375         */
376        public int getLine() {
377            return line;
378        }
379
380        /**
381         * Determines the column number of the tag in the source file.
382         * Will be 0 for all lines of multiline comment, except the
383         * first line.
384         * @return the column number of the tag in the source file.
385         */
386        public int getColumn() {
387            return column;
388        }
389
390        /**
391         * Determines whether the suppression turns checkstyle reporting on or
392         * off.
393         * @return {@code true}if the suppression turns reporting on.
394         */
395        public boolean isReportingOn() {
396            return reportingOn;
397        }
398
399        /**
400         * Compares the position of this tag in the file
401         * with the position of another tag.
402         * @param object the tag to compare with this one.
403         * @return a negative number if this tag is before the other tag,
404         *     0 if they are at the same position, and a positive number if this
405         *     tag is after the other tag.
406         */
407        @Override
408        public int compareTo(Tag object) {
409            if (line == object.line) {
410                return Integer.compare(column, object.column);
411            }
412
413            return Integer.compare(line, object.line);
414        }
415
416        @Override
417        public boolean equals(Object other) {
418            if (this == other) {
419                return true;
420            }
421            if (other == null || getClass() != other.getClass()) {
422                return false;
423            }
424            final Tag tag = (Tag) other;
425            return Objects.equals(line, tag.line)
426                    && Objects.equals(column, tag.column)
427                    && Objects.equals(reportingOn, tag.reportingOn)
428                    && Objects.equals(text, tag.text);
429        }
430
431        @Override
432        public int hashCode() {
433            return Objects.hash(text, line, column, reportingOn);
434        }
435
436        /**
437         * Determines whether the source of an audit event
438         * matches the text of this tag.
439         * @param event the {@code AuditEvent} to check.
440         * @return true if the source of event matches the text of this tag.
441         */
442        public boolean isMatch(AuditEvent event) {
443            boolean match = false;
444            final Matcher tagMatcher = tagCheckRegexp.matcher(event.getSourceName());
445            if (tagMatcher.find()) {
446                if (tagMessageRegexp == null) {
447                    match = true;
448                }
449                else {
450                    final Matcher messageMatcher = tagMessageRegexp.matcher(event.getMessage());
451                    match = messageMatcher.find();
452                }
453            }
454            return match;
455        }
456
457        @Override
458        public final String toString() {
459            return "Tag[line=" + line + "; col=" + column
460                + "; on=" + reportingOn + "; text='" + text + "']";
461        }
462    }
463}