001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2018 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.CommonUtil;
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 */
053public class SuppressionCommentFilter
054    extends AutomaticBean
055    implements TreeWalkerFilter {
056
057    /**
058     * Enum to be used for switching checkstyle reporting for tags.
059     */
060    public enum TagType {
061
062        /**
063         * Switch reporting on.
064         */
065        ON,
066        /**
067         * Switch reporting off.
068         */
069        OFF
070
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    protected void finishLocalSetup() {
183        // No code by default
184    }
185
186    @Override
187    public boolean accept(TreeWalkerAuditEvent event) {
188        boolean accepted = true;
189
190        if (event.getLocalizedMessage() != null) {
191            // Lazy update. If the first event for the current file, update file
192            // contents and tag suppressions
193            final FileContents currentContents = event.getFileContents();
194
195            if (getFileContents() != currentContents) {
196                setFileContents(currentContents);
197                tagSuppressions();
198            }
199            final Tag matchTag = findNearestMatch(event);
200            accepted = matchTag == null || matchTag.getTagType() == TagType.ON;
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 TreeWalkerAuditEvent} to match.
209     * @return The {@code Tag} nearest event.
210     */
211    private Tag findNearestMatch(TreeWalkerAuditEvent 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.getSingleLineComments().values());
235        }
236        if (checkC) {
237            final Collection<List<TextBlock>> cComments = contents
238                    .getBlockComments().values();
239            cComments.forEach(this::tagSuppressions);
240        }
241        Collections.sort(tags);
242    }
243
244    /**
245     * Appends the suppressions in a collection of comments to the full
246     * set of suppression tags.
247     * @param comments the set of comments.
248     */
249    private void tagSuppressions(Collection<TextBlock> comments) {
250        for (TextBlock comment : comments) {
251            final int startLineNo = comment.getStartLineNo();
252            final String[] text = comment.getText();
253            tagCommentLine(text[0], startLineNo, comment.getStartColNo());
254            for (int i = 1; i < text.length; i++) {
255                tagCommentLine(text[i], startLineNo + i, 0);
256            }
257        }
258    }
259
260    /**
261     * Tags a string if it matches the format for turning
262     * checkstyle reporting on or the format for turning reporting off.
263     * @param text the string to tag.
264     * @param line the line number of text.
265     * @param column the column number of text.
266     */
267    private void tagCommentLine(String text, int line, int column) {
268        final Matcher offMatcher = offCommentFormat.matcher(text);
269        if (offMatcher.find()) {
270            addTag(offMatcher.group(0), line, column, TagType.OFF);
271        }
272        else {
273            final Matcher onMatcher = onCommentFormat.matcher(text);
274            if (onMatcher.find()) {
275                addTag(onMatcher.group(0), line, column, TagType.ON);
276            }
277        }
278    }
279
280    /**
281     * Adds a {@code Tag} to the list of all tags.
282     * @param text the text of the tag.
283     * @param line the line number of the tag.
284     * @param column the column number of the tag.
285     * @param reportingOn {@code true} if the tag turns checkstyle reporting on.
286     */
287    private void addTag(String text, int line, int column, TagType reportingOn) {
288        final Tag tag = new Tag(line, column, text, reportingOn, this);
289        tags.add(tag);
290    }
291
292    /**
293     * A Tag holds a suppression comment and its location, and determines
294     * whether the suppression turns checkstyle reporting on or off.
295     */
296    public static class Tag
297        implements Comparable<Tag> {
298
299        /** The text of the tag. */
300        private final String text;
301
302        /** The line number of the tag. */
303        private final int line;
304
305        /** The column number of the tag. */
306        private final int column;
307
308        /** Determines whether the suppression turns checkstyle reporting on. */
309        private final TagType tagType;
310
311        /** The parsed check regexp, expanded for the text of this tag. */
312        private final Pattern tagCheckRegexp;
313
314        /** The parsed message regexp, expanded for the text of this tag. */
315        private final Pattern tagMessageRegexp;
316
317        /**
318         * Constructs a tag.
319         * @param line the line number.
320         * @param column the column number.
321         * @param text the text of the suppression.
322         * @param tagType {@code ON} if the tag turns checkstyle reporting.
323         * @param filter the {@code SuppressionCommentFilter} with the context
324         * @throws IllegalArgumentException if unable to parse expanded text.
325         */
326        public Tag(int line, int column, String text, TagType tagType,
327                   SuppressionCommentFilter filter) {
328            this.line = line;
329            this.column = column;
330            this.text = text;
331            this.tagType = tagType;
332
333            //Expand regexp for check and message
334            //Does not intern Patterns with Utils.getPattern()
335            String format = "";
336            try {
337                if (this.tagType == TagType.ON) {
338                    format = CommonUtil.fillTemplateWithStringsByRegexp(
339                            filter.checkFormat, text, filter.onCommentFormat);
340                    tagCheckRegexp = Pattern.compile(format);
341                    if (filter.messageFormat == null) {
342                        tagMessageRegexp = null;
343                    }
344                    else {
345                        format = CommonUtil.fillTemplateWithStringsByRegexp(
346                                filter.messageFormat, text, filter.onCommentFormat);
347                        tagMessageRegexp = Pattern.compile(format);
348                    }
349                }
350                else {
351                    format = CommonUtil.fillTemplateWithStringsByRegexp(
352                            filter.checkFormat, text, filter.offCommentFormat);
353                    tagCheckRegexp = Pattern.compile(format);
354                    if (filter.messageFormat == null) {
355                        tagMessageRegexp = null;
356                    }
357                    else {
358                        format = CommonUtil.fillTemplateWithStringsByRegexp(
359                                filter.messageFormat, text, filter.offCommentFormat);
360                        tagMessageRegexp = Pattern.compile(format);
361                    }
362                }
363            }
364            catch (final PatternSyntaxException ex) {
365                throw new IllegalArgumentException(
366                    "unable to parse expanded comment " + format, ex);
367            }
368        }
369
370        /**
371         * Returns line number of the tag in the source file.
372         * @return the line number of the tag in the source file.
373         */
374        public int getLine() {
375            return line;
376        }
377
378        /**
379         * Determines the column number of the tag in the source file.
380         * Will be 0 for all lines of multiline comment, except the
381         * first line.
382         * @return the column number of the tag in the source file.
383         */
384        public int getColumn() {
385            return column;
386        }
387
388        /**
389         * Determines whether the suppression turns checkstyle reporting on or
390         * off.
391         * @return {@code ON} if the suppression turns reporting on.
392         */
393        public TagType getTagType() {
394            return tagType;
395        }
396
397        /**
398         * Compares the position of this tag in the file
399         * with the position of another tag.
400         * @param object the tag to compare with this one.
401         * @return a negative number if this tag is before the other tag,
402         *     0 if they are at the same position, and a positive number if this
403         *     tag is after the other tag.
404         */
405        @Override
406        public int compareTo(Tag object) {
407            final int result;
408            if (line == object.line) {
409                result = Integer.compare(column, object.column);
410            }
411            else {
412                result = Integer.compare(line, object.line);
413            }
414            return result;
415        }
416
417        @Override
418        public boolean equals(Object other) {
419            if (this == other) {
420                return true;
421            }
422            if (other == null || getClass() != other.getClass()) {
423                return false;
424            }
425            final Tag tag = (Tag) other;
426            return Objects.equals(line, tag.line)
427                    && Objects.equals(column, tag.column)
428                    && Objects.equals(tagType, tag.tagType)
429                    && Objects.equals(text, tag.text)
430                    && Objects.equals(tagCheckRegexp, tag.tagCheckRegexp)
431                    && Objects.equals(tagMessageRegexp, tag.tagMessageRegexp);
432        }
433
434        @Override
435        public int hashCode() {
436            return Objects.hash(text, line, column, tagType, tagCheckRegexp, tagMessageRegexp);
437        }
438
439        /**
440         * Determines whether the source of an audit event
441         * matches the text of this tag.
442         * @param event the {@code TreeWalkerAuditEvent} to check.
443         * @return true if the source of event matches the text of this tag.
444         */
445        public boolean isMatch(TreeWalkerAuditEvent event) {
446            boolean match = false;
447            final Matcher tagMatcher = tagCheckRegexp.matcher(event.getSourceName());
448            if (tagMatcher.find()) {
449                if (tagMessageRegexp == null) {
450                    match = true;
451                }
452                else {
453                    final Matcher messageMatcher = tagMessageRegexp.matcher(event.getMessage());
454                    match = messageMatcher.find();
455                }
456            }
457            else if (event.getModuleId() != null) {
458                final Matcher idMatcher = tagCheckRegexp.matcher(event.getModuleId());
459                match = idMatcher.find();
460            }
461            return match;
462        }
463
464        @Override
465        public String toString() {
466            return "Tag[text='" + text + '\''
467                    + ", line=" + line
468                    + ", column=" + column
469                    + ", type=" + tagType
470                    + ", tagCheckRegexp=" + tagCheckRegexp
471                    + ", tagMessageRegexp=" + tagMessageRegexp + ']';
472        }
473
474    }
475
476}