001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2024 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.AbstractAutomaticBean;
033import com.puppycrawl.tools.checkstyle.PropertyType;
034import com.puppycrawl.tools.checkstyle.TreeWalkerAuditEvent;
035import com.puppycrawl.tools.checkstyle.TreeWalkerFilter;
036import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
037import com.puppycrawl.tools.checkstyle.api.FileContents;
038import com.puppycrawl.tools.checkstyle.api.TextBlock;
039import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
040
041/**
042 * <div>
043 * Filter {@code SuppressionCommentFilter} uses pairs of comments to suppress audit events.
044 * </div>
045 *
046 * <p>
047 * Rationale:
048 * Sometimes there are legitimate reasons for violating a check. When
049 * this is a matter of the code in question and not personal
050 * preference, the best place to override the policy is in the code
051 * itself. Semi-structured comments can be associated with the check.
052 * This is sometimes superior to a separate suppressions file, which
053 * must be kept up-to-date as the source file is edited.
054 * </p>
055 *
056 * <p>
057 * Note that the suppression comment should be put before the violation.
058 * You can use more than one suppression comment each on separate line.
059 * </p>
060 *
061 * <p>
062 * Attention: This filter may only be specified within the TreeWalker module
063 * ({@code &lt;module name="TreeWalker"/&gt;}) and only applies to checks which are also
064 * defined within this module. To filter non-TreeWalker checks like {@code RegexpSingleline}, a
065 * <a href="https://checkstyle.org/filters/suppresswithplaintextcommentfilter.html#SuppressWithPlainTextCommentFilter">
066 * SuppressWithPlainTextCommentFilter</a> or similar filter must be used.
067 * </p>
068 *
069 * <p>
070 * {@code offCommentFormat} and {@code onCommentFormat} must have equal
071 * <a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/regex/Matcher.html#groupCount()">
072 * paren counts</a>.
073 * </p>
074 *
075 * <p>
076 * SuppressionCommentFilter can suppress Checks that have Treewalker as parent module.
077 * </p>
078 * <ul>
079 * <li>
080 * Property {@code checkC} - Control whether to check C style comments ({@code &#47;* ... *&#47;}).
081 * Type is {@code boolean}.
082 * Default value is {@code true}.
083 * </li>
084 * <li>
085 * Property {@code checkCPP} - Control whether to check C++ style comments ({@code //}).
086 * Type is {@code boolean}.
087 * Default value is {@code true}.
088 * </li>
089 * <li>
090 * Property {@code checkFormat} - Specify check pattern to suppress.
091 * Type is {@code java.util.regex.Pattern}.
092 * Default value is {@code ".*"}.
093 * </li>
094 * <li>
095 * Property {@code idFormat} - Specify check ID pattern to suppress.
096 * Type is {@code java.util.regex.Pattern}.
097 * Default value is {@code null}.
098 * </li>
099 * <li>
100 * Property {@code messageFormat} - Specify message pattern to suppress.
101 * Type is {@code java.util.regex.Pattern}.
102 * Default value is {@code null}.
103 * </li>
104 * <li>
105 * Property {@code offCommentFormat} - Specify comment pattern to
106 * trigger filter to begin suppression.
107 * Type is {@code java.util.regex.Pattern}.
108 * Default value is {@code "CHECKSTYLE:OFF"}.
109 * </li>
110 * <li>
111 * Property {@code onCommentFormat} - Specify comment pattern to trigger filter to end suppression.
112 * Type is {@code java.util.regex.Pattern}.
113 * Default value is {@code "CHECKSTYLE:ON"}.
114 * </li>
115 * </ul>
116 *
117 * <p>
118 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
119 * </p>
120 *
121 * @since 3.5
122 */
123public class SuppressionCommentFilter
124    extends AbstractAutomaticBean
125    implements TreeWalkerFilter {
126
127    /**
128     * Enum to be used for switching checkstyle reporting for tags.
129     */
130    public enum TagType {
131
132        /**
133         * Switch reporting on.
134         */
135        ON,
136        /**
137         * Switch reporting off.
138         */
139        OFF,
140
141    }
142
143    /** Turns checkstyle reporting off. */
144    private static final String DEFAULT_OFF_FORMAT = "CHECKSTYLE:OFF";
145
146    /** Turns checkstyle reporting on. */
147    private static final String DEFAULT_ON_FORMAT = "CHECKSTYLE:ON";
148
149    /** Control all checks. */
150    private static final String DEFAULT_CHECK_FORMAT = ".*";
151
152    /** Tagged comments. */
153    private final List<Tag> tags = new ArrayList<>();
154
155    /** Control whether to check C style comments ({@code &#47;* ... *&#47;}). */
156    private boolean checkC = true;
157
158    /** Control whether to check C++ style comments ({@code //}). */
159    // -@cs[AbbreviationAsWordInName] we can not change it as,
160    // Check property is a part of API (used in configurations)
161    private boolean checkCPP = true;
162
163    /** Specify comment pattern to trigger filter to begin suppression. */
164    private Pattern offCommentFormat = Pattern.compile(DEFAULT_OFF_FORMAT);
165
166    /** Specify comment pattern to trigger filter to end suppression. */
167    private Pattern onCommentFormat = Pattern.compile(DEFAULT_ON_FORMAT);
168
169    /** Specify check pattern to suppress. */
170    @XdocsPropertyType(PropertyType.PATTERN)
171    private String checkFormat = DEFAULT_CHECK_FORMAT;
172
173    /** Specify message pattern to suppress. */
174    @XdocsPropertyType(PropertyType.PATTERN)
175    private String messageFormat;
176
177    /** Specify check ID pattern to suppress. */
178    @XdocsPropertyType(PropertyType.PATTERN)
179    private String idFormat;
180
181    /**
182     * References the current FileContents for this filter.
183     * Since this is a weak reference to the FileContents, the FileContents
184     * can be reclaimed as soon as the strong references in TreeWalker
185     * are reassigned to the next FileContents, at which time filtering for
186     * the current FileContents is finished.
187     */
188    private WeakReference<FileContents> fileContentsReference = new WeakReference<>(null);
189
190    /**
191     * Setter to specify comment pattern to trigger filter to begin suppression.
192     *
193     * @param pattern a pattern.
194     * @since 3.5
195     */
196    public final void setOffCommentFormat(Pattern pattern) {
197        offCommentFormat = pattern;
198    }
199
200    /**
201     * Setter to specify comment pattern to trigger filter to end suppression.
202     *
203     * @param pattern a pattern.
204     * @since 3.5
205     */
206    public final void setOnCommentFormat(Pattern pattern) {
207        onCommentFormat = pattern;
208    }
209
210    /**
211     * Returns FileContents for this filter.
212     *
213     * @return the FileContents for this filter.
214     */
215    private FileContents getFileContents() {
216        return fileContentsReference.get();
217    }
218
219    /**
220     * Set the FileContents for this filter.
221     *
222     * @param fileContents the FileContents for this filter.
223     */
224    private void setFileContents(FileContents fileContents) {
225        fileContentsReference = new WeakReference<>(fileContents);
226    }
227
228    /**
229     * Setter to specify check pattern to suppress.
230     *
231     * @param format a {@code String} value
232     * @since 3.5
233     */
234    public final void setCheckFormat(String format) {
235        checkFormat = format;
236    }
237
238    /**
239     * Setter to specify message pattern to suppress.
240     *
241     * @param format a {@code String} value
242     * @since 3.5
243     */
244    public void setMessageFormat(String format) {
245        messageFormat = format;
246    }
247
248    /**
249     * Setter to specify check ID pattern to suppress.
250     *
251     * @param format a {@code String} value
252     * @since 8.24
253     */
254    public void setIdFormat(String format) {
255        idFormat = format;
256    }
257
258    /**
259     * Setter to control whether to check C++ style comments ({@code //}).
260     *
261     * @param checkCpp {@code true} if C++ comments are checked.
262     * @since 3.5
263     */
264    // -@cs[AbbreviationAsWordInName] We can not change it as,
265    // check's property is a part of API (used in configurations).
266    public void setCheckCPP(boolean checkCpp) {
267        checkCPP = checkCpp;
268    }
269
270    /**
271     * Setter to control whether to check C style comments ({@code &#47;* ... *&#47;}).
272     *
273     * @param checkC {@code true} if C comments are checked.
274     * @since 3.5
275     */
276    public void setCheckC(boolean checkC) {
277        this.checkC = checkC;
278    }
279
280    @Override
281    protected void finishLocalSetup() {
282        // No code by default
283    }
284
285    @Override
286    public boolean accept(TreeWalkerAuditEvent event) {
287        boolean accepted = true;
288
289        if (event.getViolation() != null) {
290            // Lazy update. If the first event for the current file, update file
291            // contents and tag suppressions
292            final FileContents currentContents = event.getFileContents();
293
294            if (getFileContents() != currentContents) {
295                setFileContents(currentContents);
296                tagSuppressions();
297            }
298            final Tag matchTag = findNearestMatch(event);
299            accepted = matchTag == null || matchTag.getTagType() == TagType.ON;
300        }
301        return accepted;
302    }
303
304    /**
305     * Finds the nearest comment text tag that matches an audit event.
306     * The nearest tag is before the line and column of the event.
307     *
308     * @param event the {@code TreeWalkerAuditEvent} to match.
309     * @return The {@code Tag} nearest event.
310     */
311    private Tag findNearestMatch(TreeWalkerAuditEvent event) {
312        Tag result = null;
313        for (Tag tag : tags) {
314            final int eventLine = event.getLine();
315            if (tag.getLine() > eventLine
316                || tag.getLine() == eventLine
317                    && tag.getColumn() > event.getColumn()) {
318                break;
319            }
320            if (tag.isMatch(event)) {
321                result = tag;
322            }
323        }
324        return result;
325    }
326
327    /**
328     * Collects all the suppression tags for all comments into a list and
329     * sorts the list.
330     */
331    private void tagSuppressions() {
332        tags.clear();
333        final FileContents contents = getFileContents();
334        if (checkCPP) {
335            tagSuppressions(contents.getSingleLineComments().values());
336        }
337        if (checkC) {
338            final Collection<List<TextBlock>> cComments = contents
339                    .getBlockComments().values();
340            cComments.forEach(this::tagSuppressions);
341        }
342        Collections.sort(tags);
343    }
344
345    /**
346     * Appends the suppressions in a collection of comments to the full
347     * set of suppression tags.
348     *
349     * @param comments the set of comments.
350     */
351    private void tagSuppressions(Collection<TextBlock> comments) {
352        for (TextBlock comment : comments) {
353            final int startLineNo = comment.getStartLineNo();
354            final String[] text = comment.getText();
355            tagCommentLine(text[0], startLineNo, comment.getStartColNo());
356            for (int i = 1; i < text.length; i++) {
357                tagCommentLine(text[i], startLineNo + i, 0);
358            }
359        }
360    }
361
362    /**
363     * Tags a string if it matches the format for turning
364     * checkstyle reporting on or the format for turning reporting off.
365     *
366     * @param text the string to tag.
367     * @param line the line number of text.
368     * @param column the column number of text.
369     */
370    private void tagCommentLine(String text, int line, int column) {
371        final Matcher offMatcher = offCommentFormat.matcher(text);
372        if (offMatcher.find()) {
373            addTag(offMatcher.group(0), line, column, TagType.OFF);
374        }
375        else {
376            final Matcher onMatcher = onCommentFormat.matcher(text);
377            if (onMatcher.find()) {
378                addTag(onMatcher.group(0), line, column, TagType.ON);
379            }
380        }
381    }
382
383    /**
384     * Adds a {@code Tag} to the list of all tags.
385     *
386     * @param text the text of the tag.
387     * @param line the line number of the tag.
388     * @param column the column number of the tag.
389     * @param reportingOn {@code true} if the tag turns checkstyle reporting on.
390     */
391    private void addTag(String text, int line, int column, TagType reportingOn) {
392        final Tag tag = new Tag(line, column, text, reportingOn, this);
393        tags.add(tag);
394    }
395
396    /**
397     * A Tag holds a suppression comment and its location, and determines
398     * whether the suppression turns checkstyle reporting on or off.
399     */
400    private static final class Tag
401        implements Comparable<Tag> {
402
403        /** The text of the tag. */
404        private final String text;
405
406        /** The line number of the tag. */
407        private final int line;
408
409        /** The column number of the tag. */
410        private final int column;
411
412        /** Determines whether the suppression turns checkstyle reporting on. */
413        private final TagType tagType;
414
415        /** The parsed check regexp, expanded for the text of this tag. */
416        private final Pattern tagCheckRegexp;
417
418        /** The parsed message regexp, expanded for the text of this tag. */
419        private final Pattern tagMessageRegexp;
420
421        /** The parsed check ID regexp, expanded for the text of this tag. */
422        private final Pattern tagIdRegexp;
423
424        /**
425         * Constructs a tag.
426         *
427         * @param line the line number.
428         * @param column the column number.
429         * @param text the text of the suppression.
430         * @param tagType {@code ON} if the tag turns checkstyle reporting.
431         * @param filter the {@code SuppressionCommentFilter} with the context
432         * @throws IllegalArgumentException if unable to parse expanded text.
433         */
434        private Tag(int line, int column, String text, TagType tagType,
435                   SuppressionCommentFilter filter) {
436            this.line = line;
437            this.column = column;
438            this.text = text;
439            this.tagType = tagType;
440
441            final Pattern commentFormat;
442            if (this.tagType == TagType.ON) {
443                commentFormat = filter.onCommentFormat;
444            }
445            else {
446                commentFormat = filter.offCommentFormat;
447            }
448
449            // Expand regexp for check and message
450            // Does not intern Patterns with Utils.getPattern()
451            String format = "";
452            try {
453                format = CommonUtil.fillTemplateWithStringsByRegexp(
454                        filter.checkFormat, text, commentFormat);
455                tagCheckRegexp = Pattern.compile(format);
456
457                if (filter.messageFormat == null) {
458                    tagMessageRegexp = null;
459                }
460                else {
461                    format = CommonUtil.fillTemplateWithStringsByRegexp(
462                            filter.messageFormat, text, commentFormat);
463                    tagMessageRegexp = Pattern.compile(format);
464                }
465
466                if (filter.idFormat == null) {
467                    tagIdRegexp = null;
468                }
469                else {
470                    format = CommonUtil.fillTemplateWithStringsByRegexp(
471                            filter.idFormat, text, commentFormat);
472                    tagIdRegexp = Pattern.compile(format);
473                }
474            }
475            catch (final PatternSyntaxException ex) {
476                throw new IllegalArgumentException(
477                    "unable to parse expanded comment " + format, ex);
478            }
479        }
480
481        /**
482         * Returns line number of the tag in the source file.
483         *
484         * @return the line number of the tag in the source file.
485         */
486        public int getLine() {
487            return line;
488        }
489
490        /**
491         * Determines the column number of the tag in the source file.
492         * Will be 0 for all lines of multiline comment, except the
493         * first line.
494         *
495         * @return the column number of the tag in the source file.
496         */
497        public int getColumn() {
498            return column;
499        }
500
501        /**
502         * Determines whether the suppression turns checkstyle reporting on or
503         * off.
504         *
505         * @return {@code ON} if the suppression turns reporting on.
506         */
507        public TagType getTagType() {
508            return tagType;
509        }
510
511        /**
512         * Compares the position of this tag in the file
513         * with the position of another tag.
514         *
515         * @param object the tag to compare with this one.
516         * @return a negative number if this tag is before the other tag,
517         *     0 if they are at the same position, and a positive number if this
518         *     tag is after the other tag.
519         */
520        @Override
521        public int compareTo(Tag object) {
522            final int result;
523            if (line == object.line) {
524                result = Integer.compare(column, object.column);
525            }
526            else {
527                result = Integer.compare(line, object.line);
528            }
529            return result;
530        }
531
532        /**
533         * Indicates whether some other object is "equal to" this one.
534         * Suppression on enumeration is needed so code stays consistent.
535         *
536         * @noinspection EqualsCalledOnEnumConstant
537         * @noinspectionreason EqualsCalledOnEnumConstant - enumeration is needed to keep
538         *      code consistent
539         */
540        @Override
541        public boolean equals(Object other) {
542            if (this == other) {
543                return true;
544            }
545            if (other == null || getClass() != other.getClass()) {
546                return false;
547            }
548            final Tag tag = (Tag) other;
549            return Objects.equals(line, tag.line)
550                    && Objects.equals(column, tag.column)
551                    && Objects.equals(tagType, tag.tagType)
552                    && Objects.equals(text, tag.text)
553                    && Objects.equals(tagCheckRegexp, tag.tagCheckRegexp)
554                    && Objects.equals(tagMessageRegexp, tag.tagMessageRegexp)
555                    && Objects.equals(tagIdRegexp, tag.tagIdRegexp);
556        }
557
558        @Override
559        public int hashCode() {
560            return Objects.hash(text, line, column, tagType, tagCheckRegexp, tagMessageRegexp,
561                    tagIdRegexp);
562        }
563
564        /**
565         * Determines whether the source of an audit event
566         * matches the text of this tag.
567         *
568         * @param event the {@code TreeWalkerAuditEvent} to check.
569         * @return true if the source of event matches the text of this tag.
570         */
571        public boolean isMatch(TreeWalkerAuditEvent event) {
572            return isCheckMatch(event) && isIdMatch(event) && isMessageMatch(event);
573        }
574
575        /**
576         * Checks whether {@link TreeWalkerAuditEvent} source name matches the check format.
577         *
578         * @param event {@link TreeWalkerAuditEvent} instance.
579         * @return true if the {@link TreeWalkerAuditEvent} source name matches the check format.
580         */
581        private boolean isCheckMatch(TreeWalkerAuditEvent event) {
582            final Matcher checkMatcher = tagCheckRegexp.matcher(event.getSourceName());
583            return checkMatcher.find();
584        }
585
586        /**
587         * Checks whether the {@link TreeWalkerAuditEvent} module ID matches the ID format.
588         *
589         * @param event {@link TreeWalkerAuditEvent} instance.
590         * @return true if the {@link TreeWalkerAuditEvent} module ID matches the ID format.
591         */
592        private boolean isIdMatch(TreeWalkerAuditEvent event) {
593            boolean match = true;
594            if (tagIdRegexp != null) {
595                if (event.getModuleId() == null) {
596                    match = false;
597                }
598                else {
599                    final Matcher idMatcher = tagIdRegexp.matcher(event.getModuleId());
600                    match = idMatcher.find();
601                }
602            }
603            return match;
604        }
605
606        /**
607         * Checks whether the {@link TreeWalkerAuditEvent} message matches the message format.
608         *
609         * @param event {@link TreeWalkerAuditEvent} instance.
610         * @return true if the {@link TreeWalkerAuditEvent} message matches the message format.
611         */
612        private boolean isMessageMatch(TreeWalkerAuditEvent event) {
613            boolean match = true;
614            if (tagMessageRegexp != null) {
615                final Matcher messageMatcher = tagMessageRegexp.matcher(event.getMessage());
616                match = messageMatcher.find();
617            }
618            return match;
619        }
620
621        @Override
622        public String toString() {
623            return "Tag[text='" + text + '\''
624                    + ", line=" + line
625                    + ", column=" + column
626                    + ", type=" + tagType
627                    + ", tagCheckRegexp=" + tagCheckRegexp
628                    + ", tagMessageRegexp=" + tagMessageRegexp
629                    + ", tagIdRegexp=" + tagIdRegexp + ']';
630        }
631
632    }
633
634}