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