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