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