001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2017 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.ArrayList;
024import java.util.Collection;
025import java.util.Collections;
026import java.util.List;
027import java.util.Objects;
028import java.util.regex.Matcher;
029import java.util.regex.Pattern;
030import java.util.regex.PatternSyntaxException;
031
032import org.apache.commons.beanutils.ConversionException;
033
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    /** Tagged comments. */
079    private final List<Tag> tags = new ArrayList<>();
080
081    /** Whether to look in comments of the C type. */
082    private boolean checkC = true;
083
084    /** Whether to look in comments of the C++ type. */
085    // -@cs[AbbreviationAsWordInName] we can not change it as,
086    // Check property is a part of API (used in configurations)
087    private boolean checkCPP = true;
088
089    /** Parsed comment regexp that turns checkstyle reporting off. */
090    private Pattern offCommentFormat = Pattern.compile(DEFAULT_OFF_FORMAT);
091
092    /** Parsed comment regexp that turns checkstyle reporting on. */
093    private Pattern onCommentFormat = Pattern.compile(DEFAULT_ON_FORMAT);
094
095    /** The check format to suppress. */
096    private String checkFormat = DEFAULT_CHECK_FORMAT;
097
098    /** The message format to suppress. */
099    private String messageFormat;
100
101    /**
102     * References the current FileContents for this filter.
103     * Since this is a weak reference to the FileContents, the FileContents
104     * can be reclaimed as soon as the strong references in TreeWalker
105     * and FileContentsHolder are reassigned to the next FileContents,
106     * at which time filtering for the current FileContents is finished.
107     */
108    private WeakReference<FileContents> fileContentsReference = new WeakReference<>(null);
109
110    /**
111     * Set the format for a comment that turns off reporting.
112     * @param pattern a pattern.
113     */
114    public final void setOffCommentFormat(Pattern pattern) {
115        offCommentFormat = pattern;
116    }
117
118    /**
119     * Set the format for a comment that turns on reporting.
120     * @param pattern a pattern.
121     */
122    public final void setOnCommentFormat(Pattern pattern) {
123        onCommentFormat = pattern;
124    }
125
126    /**
127     * @return the FileContents for this filter.
128     */
129    public FileContents getFileContents() {
130        return fileContentsReference.get();
131    }
132
133    /**
134     * Set the FileContents for this filter.
135     * @param fileContents the FileContents for this filter.
136     */
137    public void setFileContents(FileContents fileContents) {
138        fileContentsReference = new WeakReference<>(fileContents);
139    }
140
141    /**
142     * Set the format for a check.
143     * @param format a {@code String} value
144     */
145    public final void setCheckFormat(String format) {
146        checkFormat = format;
147    }
148
149    /**
150     * Set the format for a message.
151     * @param format a {@code String} value
152     */
153    public void setMessageFormat(String format) {
154        messageFormat = format;
155    }
156
157    /**
158     * Set whether to look in C++ comments.
159     * @param checkCpp {@code true} if C++ comments are checked.
160     */
161    // -@cs[AbbreviationAsWordInName] We can not change it as,
162    // check's property is a part of API (used in configurations).
163    public void setCheckCPP(boolean checkCpp) {
164        checkCPP = checkCpp;
165    }
166
167    /**
168     * Set whether to look in C comments.
169     * @param checkC {@code true} if C comments are checked.
170     */
171    public void setCheckC(boolean checkC) {
172        this.checkC = checkC;
173    }
174
175    @Override
176    public boolean accept(AuditEvent event) {
177        boolean accepted = true;
178
179        if (event.getLocalizedMessage() != null) {
180            // Lazy update. If the first event for the current file, update file
181            // contents and tag suppressions
182            final FileContents currentContents = FileContentsHolder.getCurrentFileContents();
183
184            if (getFileContents() != currentContents) {
185                setFileContents(currentContents);
186                tagSuppressions();
187            }
188            final Tag matchTag = findNearestMatch(event);
189            accepted = matchTag == null || matchTag.isReportingOn();
190        }
191        return accepted;
192    }
193
194    /**
195     * Finds the nearest comment text tag that matches an audit event.
196     * The nearest tag is before the line and column of the event.
197     * @param event the {@code AuditEvent} to match.
198     * @return The {@code Tag} nearest event.
199     */
200    private Tag findNearestMatch(AuditEvent event) {
201        Tag result = null;
202        for (Tag tag : tags) {
203            if (tag.getLine() > event.getLine()
204                || tag.getLine() == event.getLine()
205                    && tag.getColumn() > event.getColumn()) {
206                break;
207            }
208            if (tag.isMatch(event)) {
209                result = tag;
210            }
211        }
212        return result;
213    }
214
215    /**
216     * Collects all the suppression tags for all comments into a list and
217     * sorts the list.
218     */
219    private void tagSuppressions() {
220        tags.clear();
221        final FileContents contents = getFileContents();
222        if (checkCPP) {
223            tagSuppressions(contents.getCppComments().values());
224        }
225        if (checkC) {
226            final Collection<List<TextBlock>> cComments = contents
227                    .getCComments().values();
228            cComments.forEach(this::tagSuppressions);
229        }
230        Collections.sort(tags);
231    }
232
233    /**
234     * Appends the suppressions in a collection of comments to the full
235     * set of suppression tags.
236     * @param comments the set of comments.
237     */
238    private void tagSuppressions(Collection<TextBlock> comments) {
239        for (TextBlock comment : comments) {
240            final int startLineNo = comment.getStartLineNo();
241            final String[] text = comment.getText();
242            tagCommentLine(text[0], startLineNo, comment.getStartColNo());
243            for (int i = 1; i < text.length; i++) {
244                tagCommentLine(text[i], startLineNo + i, 0);
245            }
246        }
247    }
248
249    /**
250     * Tags a string if it matches the format for turning
251     * checkstyle reporting on or the format for turning reporting off.
252     * @param text the string to tag.
253     * @param line the line number of text.
254     * @param column the column number of text.
255     */
256    private void tagCommentLine(String text, int line, int column) {
257        final Matcher offMatcher = offCommentFormat.matcher(text);
258        if (offMatcher.find()) {
259            addTag(offMatcher.group(0), line, column, false);
260        }
261        else {
262            final Matcher onMatcher = onCommentFormat.matcher(text);
263            if (onMatcher.find()) {
264                addTag(onMatcher.group(0), line, column, true);
265            }
266        }
267    }
268
269    /**
270     * Adds a {@code Tag} to the list of all tags.
271     * @param text the text of the tag.
272     * @param line the line number of the tag.
273     * @param column the column number of the tag.
274     * @param reportingOn {@code true} if the tag turns checkstyle reporting on.
275     */
276    private void addTag(String text, int line, int column, boolean reportingOn) {
277        final Tag tag = new Tag(line, column, text, reportingOn, this);
278        tags.add(tag);
279    }
280
281    /**
282     * A Tag holds a suppression comment and its location, and determines
283     * whether the suppression turns checkstyle reporting on or off.
284     * @author Rick Giles
285     */
286    public static class Tag
287        implements Comparable<Tag> {
288        /** The text of the tag. */
289        private final String text;
290
291        /** The line number of the tag. */
292        private final int line;
293
294        /** The column number of the tag. */
295        private final int column;
296
297        /** Determines whether the suppression turns checkstyle reporting on. */
298        private final boolean reportingOn;
299
300        /** The parsed check regexp, expanded for the text of this tag. */
301        private final Pattern tagCheckRegexp;
302
303        /** The parsed message regexp, expanded for the text of this tag. */
304        private final Pattern tagMessageRegexp;
305
306        /**
307         * Constructs a tag.
308         * @param line the line number.
309         * @param column the column number.
310         * @param text the text of the suppression.
311         * @param reportingOn {@code true} if the tag turns checkstyle reporting.
312         * @param filter the {@code SuppressionCommentFilter} with the context
313         * @throws ConversionException if unable to parse expanded text.
314         */
315        public Tag(int line, int column, String text, boolean reportingOn,
316                   SuppressionCommentFilter filter) {
317            this.line = line;
318            this.column = column;
319            this.text = text;
320            this.reportingOn = reportingOn;
321
322            //Expand regexp for check and message
323            //Does not intern Patterns with Utils.getPattern()
324            String format = "";
325            try {
326                if (reportingOn) {
327                    format = CommonUtils.fillTemplateWithStringsByRegexp(
328                            filter.checkFormat, text, filter.onCommentFormat);
329                    tagCheckRegexp = Pattern.compile(format);
330                    if (filter.messageFormat == null) {
331                        tagMessageRegexp = null;
332                    }
333                    else {
334                        format = CommonUtils.fillTemplateWithStringsByRegexp(
335                                filter.messageFormat, text, filter.onCommentFormat);
336                        tagMessageRegexp = Pattern.compile(format);
337                    }
338                }
339                else {
340                    format = CommonUtils.fillTemplateWithStringsByRegexp(
341                            filter.checkFormat, text, filter.offCommentFormat);
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.offCommentFormat);
349                        tagMessageRegexp = Pattern.compile(format);
350                    }
351                }
352            }
353            catch (final PatternSyntaxException ex) {
354                throw new ConversionException(
355                    "unable to parse expanded comment " + format,
356                    ex);
357            }
358        }
359
360        /**
361         * @return the line number of the tag in the source file.
362         */
363        public int getLine() {
364            return line;
365        }
366
367        /**
368         * Determines the column number of the tag in the source file.
369         * Will be 0 for all lines of multiline comment, except the
370         * first line.
371         * @return the column number of the tag in the source file.
372         */
373        public int getColumn() {
374            return column;
375        }
376
377        /**
378         * Determines whether the suppression turns checkstyle reporting on or
379         * off.
380         * @return {@code true}if the suppression turns reporting on.
381         */
382        public boolean isReportingOn() {
383            return reportingOn;
384        }
385
386        /**
387         * Compares the position of this tag in the file
388         * with the position of another tag.
389         * @param object the tag to compare with this one.
390         * @return a negative number if this tag is before the other tag,
391         *     0 if they are at the same position, and a positive number if this
392         *     tag is after the other tag.
393         */
394        @Override
395        public int compareTo(Tag object) {
396            if (line == object.line) {
397                return Integer.compare(column, object.column);
398            }
399
400            return Integer.compare(line, object.line);
401        }
402
403        @Override
404        public boolean equals(Object other) {
405            if (this == other) {
406                return true;
407            }
408            if (other == null || getClass() != other.getClass()) {
409                return false;
410            }
411            final Tag tag = (Tag) other;
412            return Objects.equals(line, tag.line)
413                    && Objects.equals(column, tag.column)
414                    && Objects.equals(reportingOn, tag.reportingOn)
415                    && Objects.equals(text, tag.text)
416                    && Objects.equals(tagCheckRegexp, tag.tagCheckRegexp)
417                    && Objects.equals(tagMessageRegexp, tag.tagMessageRegexp);
418        }
419
420        @Override
421        public int hashCode() {
422            return Objects.hash(text, line, column, reportingOn, tagCheckRegexp, tagMessageRegexp);
423        }
424
425        /**
426         * Determines whether the source of an audit event
427         * matches the text of this tag.
428         * @param event the {@code AuditEvent} to check.
429         * @return true if the source of event matches the text of this tag.
430         */
431        public boolean isMatch(AuditEvent event) {
432            boolean match = false;
433            final Matcher tagMatcher = tagCheckRegexp.matcher(event.getSourceName());
434            if (tagMatcher.find()) {
435                if (tagMessageRegexp == null) {
436                    match = true;
437                }
438                else {
439                    final Matcher messageMatcher = tagMessageRegexp.matcher(event.getMessage());
440                    match = messageMatcher.find();
441                }
442            }
443            else if (event.getModuleId() != null) {
444                final Matcher idMatcher = tagCheckRegexp.matcher(event.getModuleId());
445                match = idMatcher.find();
446            }
447            return match;
448        }
449
450        @Override
451        public final String toString() {
452            return "Tag[line=" + line + "; col=" + column
453                + "; on=" + reportingOn + "; text='" + text + "']";
454        }
455    }
456}