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     * @return the FileContents for this filter.
126     */
127    public FileContents getFileContents() {
128        return fileContentsReference.get();
129    }
130
131    /**
132     * Set the FileContents for this filter.
133     * @param fileContents the FileContents for this filter.
134     */
135    public void setFileContents(FileContents fileContents) {
136        fileContentsReference = new WeakReference<>(fileContents);
137    }
138
139    /**
140     * Set the format for a check.
141     * @param format a {@code String} value
142     */
143    public final void setCheckFormat(String format) {
144        checkFormat = format;
145    }
146
147    /**
148     * Set the format for a message.
149     * @param format a {@code String} value
150     */
151    public void setMessageFormat(String format) {
152        messageFormat = format;
153    }
154
155    /**
156     * Set whether to look in C++ comments.
157     * @param checkCpp {@code true} if C++ comments are checked.
158     */
159    // -@cs[AbbreviationAsWordInName] We can not change it as,
160    // check's property is a part of API (used in configurations).
161    public void setCheckCPP(boolean checkCpp) {
162        checkCPP = checkCpp;
163    }
164
165    /**
166     * Set whether to look in C comments.
167     * @param checkC {@code true} if C comments are checked.
168     */
169    public void setCheckC(boolean checkC) {
170        this.checkC = checkC;
171    }
172
173    @Override
174    public boolean accept(AuditEvent event) {
175        boolean accepted = true;
176
177        if (event.getLocalizedMessage() != null) {
178            // Lazy update. If the first event for the current file, update file
179            // contents and tag suppressions
180            final FileContents currentContents = FileContentsHolder.getCurrentFileContents();
181
182            if (getFileContents() != currentContents) {
183                setFileContents(currentContents);
184                tagSuppressions();
185            }
186            final Tag matchTag = findNearestMatch(event);
187            accepted = matchTag == null || matchTag.isReportingOn();
188        }
189        return accepted;
190    }
191
192    /**
193     * Finds the nearest comment text tag that matches an audit event.
194     * The nearest tag is before the line and column of the event.
195     * @param event the {@code AuditEvent} to match.
196     * @return The {@code Tag} nearest event.
197     */
198    private Tag findNearestMatch(AuditEvent event) {
199        Tag result = null;
200        for (Tag tag : tags) {
201            if (tag.getLine() > event.getLine()
202                || tag.getLine() == event.getLine()
203                    && tag.getColumn() > event.getColumn()) {
204                break;
205            }
206            if (tag.isMatch(event)) {
207                result = tag;
208            }
209        }
210        return result;
211    }
212
213    /**
214     * Collects all the suppression tags for all comments into a list and
215     * sorts the list.
216     */
217    private void tagSuppressions() {
218        tags.clear();
219        final FileContents contents = getFileContents();
220        if (checkCPP) {
221            tagSuppressions(contents.getSingleLineComments().values());
222        }
223        if (checkC) {
224            final Collection<List<TextBlock>> cComments = contents
225                    .getBlockComments().values();
226            cComments.forEach(this::tagSuppressions);
227        }
228        Collections.sort(tags);
229    }
230
231    /**
232     * Appends the suppressions in a collection of comments to the full
233     * set of suppression tags.
234     * @param comments the set of comments.
235     */
236    private void tagSuppressions(Collection<TextBlock> comments) {
237        for (TextBlock comment : comments) {
238            final int startLineNo = comment.getStartLineNo();
239            final String[] text = comment.getText();
240            tagCommentLine(text[0], startLineNo, comment.getStartColNo());
241            for (int i = 1; i < text.length; i++) {
242                tagCommentLine(text[i], startLineNo + i, 0);
243            }
244        }
245    }
246
247    /**
248     * Tags a string if it matches the format for turning
249     * checkstyle reporting on or the format for turning reporting off.
250     * @param text the string to tag.
251     * @param line the line number of text.
252     * @param column the column number of text.
253     */
254    private void tagCommentLine(String text, int line, int column) {
255        final Matcher offMatcher = offCommentFormat.matcher(text);
256        if (offMatcher.find()) {
257            addTag(offMatcher.group(0), line, column, false);
258        }
259        else {
260            final Matcher onMatcher = onCommentFormat.matcher(text);
261            if (onMatcher.find()) {
262                addTag(onMatcher.group(0), line, column, true);
263            }
264        }
265    }
266
267    /**
268     * Adds a {@code Tag} to the list of all tags.
269     * @param text the text of the tag.
270     * @param line the line number of the tag.
271     * @param column the column number of the tag.
272     * @param reportingOn {@code true} if the tag turns checkstyle reporting on.
273     */
274    private void addTag(String text, int line, int column, boolean reportingOn) {
275        final Tag tag = new Tag(line, column, text, reportingOn, this);
276        tags.add(tag);
277    }
278
279    /**
280     * A Tag holds a suppression comment and its location, and determines
281     * whether the suppression turns checkstyle reporting on or off.
282     * @author Rick Giles
283     */
284    public static class Tag
285        implements Comparable<Tag> {
286        /** The text of the tag. */
287        private final String text;
288
289        /** The line number of the tag. */
290        private final int line;
291
292        /** The column number of the tag. */
293        private final int column;
294
295        /** Determines whether the suppression turns checkstyle reporting on. */
296        private final boolean reportingOn;
297
298        /** The parsed check regexp, expanded for the text of this tag. */
299        private final Pattern tagCheckRegexp;
300
301        /** The parsed message regexp, expanded for the text of this tag. */
302        private final Pattern tagMessageRegexp;
303
304        /**
305         * Constructs a tag.
306         * @param line the line number.
307         * @param column the column number.
308         * @param text the text of the suppression.
309         * @param reportingOn {@code true} if the tag turns checkstyle reporting.
310         * @param filter the {@code SuppressionCommentFilter} with the context
311         * @throws IllegalArgumentException if unable to parse expanded text.
312         */
313        public Tag(int line, int column, String text, boolean reportingOn,
314                   SuppressionCommentFilter filter) {
315            this.line = line;
316            this.column = column;
317            this.text = text;
318            this.reportingOn = reportingOn;
319
320            //Expand regexp for check and message
321            //Does not intern Patterns with Utils.getPattern()
322            String format = "";
323            try {
324                if (reportingOn) {
325                    format = CommonUtils.fillTemplateWithStringsByRegexp(
326                            filter.checkFormat, text, filter.onCommentFormat);
327                    tagCheckRegexp = Pattern.compile(format);
328                    if (filter.messageFormat == null) {
329                        tagMessageRegexp = null;
330                    }
331                    else {
332                        format = CommonUtils.fillTemplateWithStringsByRegexp(
333                                filter.messageFormat, text, filter.onCommentFormat);
334                        tagMessageRegexp = Pattern.compile(format);
335                    }
336                }
337                else {
338                    format = CommonUtils.fillTemplateWithStringsByRegexp(
339                            filter.checkFormat, text, filter.offCommentFormat);
340                    tagCheckRegexp = Pattern.compile(format);
341                    if (filter.messageFormat == null) {
342                        tagMessageRegexp = null;
343                    }
344                    else {
345                        format = CommonUtils.fillTemplateWithStringsByRegexp(
346                                filter.messageFormat, text, filter.offCommentFormat);
347                        tagMessageRegexp = Pattern.compile(format);
348                    }
349                }
350            }
351            catch (final PatternSyntaxException ex) {
352                throw new IllegalArgumentException(
353                    "unable to parse expanded comment " + format, ex);
354            }
355        }
356
357        /**
358         * @return the line number of the tag in the source file.
359         */
360        public int getLine() {
361            return line;
362        }
363
364        /**
365         * Determines the column number of the tag in the source file.
366         * Will be 0 for all lines of multiline comment, except the
367         * first line.
368         * @return the column number of the tag in the source file.
369         */
370        public int getColumn() {
371            return column;
372        }
373
374        /**
375         * Determines whether the suppression turns checkstyle reporting on or
376         * off.
377         * @return {@code true}if the suppression turns reporting on.
378         */
379        public boolean isReportingOn() {
380            return reportingOn;
381        }
382
383        /**
384         * Compares the position of this tag in the file
385         * with the position of another tag.
386         * @param object the tag to compare with this one.
387         * @return a negative number if this tag is before the other tag,
388         *     0 if they are at the same position, and a positive number if this
389         *     tag is after the other tag.
390         */
391        @Override
392        public int compareTo(Tag object) {
393            final int result;
394            if (line == object.line) {
395                result = Integer.compare(column, object.column);
396            }
397            else {
398                result = Integer.compare(line, object.line);
399            }
400            return result;
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}