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