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.Collections;
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     * Setter to specify comment pattern to trigger filter to begin suppression.
152     *
153     * @param pattern a pattern.
154     * @since 3.5
155     */
156    public final void setOffCommentFormat(Pattern pattern) {
157        offCommentFormat = pattern;
158    }
159
160    /**
161     * Setter to specify comment pattern to trigger filter to end suppression.
162     *
163     * @param pattern a pattern.
164     * @since 3.5
165     */
166    public final void setOnCommentFormat(Pattern pattern) {
167        onCommentFormat = pattern;
168    }
169
170    /**
171     * Setter to specify check pattern to suppress.
172     * The pattern is matched against the fully qualified class name of the Check.
173     *
174     * @param format a {@code String} value
175     * @since 3.5
176     */
177    public final void setCheckFormat(String format) {
178        checkFormat = format;
179    }
180
181    /**
182     * Setter to specify message pattern to suppress.
183     *
184     * @param format a {@code String} value
185     * @since 3.5
186     */
187    public void setMessageFormat(String format) {
188        messageFormat = format;
189    }
190
191    /**
192     * Setter to specify check ID pattern to suppress.
193     *
194     * @param format a {@code String} value
195     * @since 8.24
196     */
197    public void setIdFormat(String format) {
198        idFormat = format;
199    }
200
201    /**
202     * Setter to control whether to check C++ style comments ({@code //}).
203     *
204     * @param checkCppComments {@code true} if C++ comments are checked.
205     * @since 3.5
206     */
207    // -@cs[AbbreviationAsWordInName] We can not change it as,
208    // check's property is a part of API (used in configurations).
209    public void setCheckCPP(boolean checkCppComments) {
210        checkCPP = checkCppComments;
211    }
212
213    /**
214     * Setter to control whether to check C style comments (&#47;* ... *&#47;).
215     *
216     * @param checkC {@code true} if C comments are checked.
217     * @since 3.5
218     */
219    public void setCheckC(boolean checkC) {
220        this.checkC = checkC;
221    }
222
223    @Override
224    protected void finishLocalSetup() {
225        // No code by default
226    }
227
228    @Override
229    public boolean accept(TreeWalkerAuditEvent event) {
230        boolean accepted = true;
231
232        if (event.violation() != null) {
233            // Lazy update. If the first event for the current file, update file
234            // contents and tag suppressions
235            final FileContents currentContents = event.fileContents();
236            fileContentsHolder.lazyUpdate(currentContents, this::tagSuppressions);
237            final Tag matchTag = findNearestMatch(event);
238            accepted = matchTag == null || matchTag.getTagType() == TagType.ON;
239        }
240        return accepted;
241    }
242
243    /**
244     * Finds the nearest comment text tag that matches an audit event.
245     * The nearest tag is before the line and column of the event.
246     *
247     * @param event the {@code TreeWalkerAuditEvent} to match.
248     * @return The {@code Tag} nearest event.
249     */
250    private Tag findNearestMatch(TreeWalkerAuditEvent event) {
251        Tag result = null;
252        for (Tag tag : tags) {
253            final int eventLine = event.getLine();
254            if (tag.getLine() > eventLine
255                || tag.getLine() == eventLine
256                    && tag.getColumn() > event.getColumn()) {
257                break;
258            }
259            if (tag.isMatch(event)) {
260                result = tag;
261            }
262        }
263        return result;
264    }
265
266    /**
267     * Collects all the suppression tags for all comments into a list and
268     * sorts the list.
269     */
270    private void tagSuppressions() {
271        tags.clear();
272        final FileContents contents = fileContentsHolder.get();
273        if (checkCPP) {
274            tagSuppressions(contents.getSingleLineComments().values());
275        }
276        if (checkC) {
277            final Collection<List<TextBlock>> cComments = contents
278                    .getBlockComments().values();
279            cComments.forEach(this::tagSuppressions);
280        }
281        Collections.sort(tags);
282    }
283
284    /**
285     * Appends the suppressions in a collection of comments to the full
286     * set of suppression tags.
287     *
288     * @param comments the set of comments.
289     */
290    private void tagSuppressions(Collection<TextBlock> comments) {
291        for (TextBlock comment : comments) {
292            final int startLineNo = comment.getStartLineNo();
293            final String[] text = comment.getText();
294            tagCommentLine(text[0], startLineNo, comment.getStartColNo());
295            for (int i = 1; i < text.length; i++) {
296                tagCommentLine(text[i], startLineNo + i, 0);
297            }
298        }
299    }
300
301    /**
302     * Tags a string if it matches the format for turning
303     * checkstyle reporting on or the format for turning reporting off.
304     *
305     * @param text the string to tag.
306     * @param line the line number of text.
307     * @param column the column number of text.
308     */
309    private void tagCommentLine(String text, int line, int column) {
310        final Matcher offMatcher = offCommentFormat.matcher(text);
311        if (offMatcher.find()) {
312            addTag(offMatcher.group(0), line, column, TagType.OFF);
313        }
314        else {
315            final Matcher onMatcher = onCommentFormat.matcher(text);
316            if (onMatcher.find()) {
317                addTag(onMatcher.group(0), line, column, TagType.ON);
318            }
319        }
320    }
321
322    /**
323     * Adds a {@code Tag} to the list of all tags.
324     *
325     * @param text the text of the tag.
326     * @param line the line number of the tag.
327     * @param column the column number of the tag.
328     * @param reportingOn {@code true} if the tag turns checkstyle reporting on.
329     */
330    private void addTag(String text, int line, int column, TagType reportingOn) {
331        final Tag tag = new Tag(line, column, text, reportingOn, this);
332        tags.add(tag);
333    }
334
335    /**
336     * A Tag holds a suppression comment and its location, and determines
337     * whether the suppression turns checkstyle reporting on or off.
338     */
339    private static final class Tag
340        implements Comparable<Tag> {
341
342        /** The text of the tag. */
343        private final String text;
344
345        /** The line number of the tag. */
346        private final int line;
347
348        /** The column number of the tag. */
349        private final int column;
350
351        /** Determines whether the suppression turns checkstyle reporting on. */
352        private final TagType tagType;
353
354        /** The parsed check regexp, expanded for the text of this tag. */
355        private final Pattern tagCheckRegexp;
356
357        /** The parsed message regexp, expanded for the text of this tag. */
358        private final Pattern tagMessageRegexp;
359
360        /** The parsed check ID regexp, expanded for the text of this tag. */
361        private final Pattern tagIdRegexp;
362
363        /**
364         * Constructs a tag.
365         *
366         * @param line the line number.
367         * @param column the column number.
368         * @param text the text of the suppression.
369         * @param tagType {@code ON} if the tag turns checkstyle reporting.
370         * @param filter the {@code SuppressionCommentFilter} with the context
371         * @throws IllegalArgumentException if unable to parse expanded text.
372         */
373        private Tag(int line, int column, String text, TagType tagType,
374                   SuppressionCommentFilter filter) {
375            this.line = line;
376            this.column = column;
377            this.text = text;
378            this.tagType = tagType;
379
380            final Pattern commentFormat;
381            if (this.tagType == TagType.ON) {
382                commentFormat = filter.onCommentFormat;
383            }
384            else {
385                commentFormat = filter.offCommentFormat;
386            }
387
388            // Expand regexp for check and message
389            // Does not intern Patterns with Utils.getPattern()
390            String format = "";
391            try {
392                format = CommonUtil.fillTemplateWithStringsByRegexp(
393                        filter.checkFormat, text, commentFormat);
394                tagCheckRegexp = Pattern.compile(format);
395
396                if (filter.messageFormat == null) {
397                    tagMessageRegexp = null;
398                }
399                else {
400                    format = CommonUtil.fillTemplateWithStringsByRegexp(
401                            filter.messageFormat, text, commentFormat);
402                    tagMessageRegexp = Pattern.compile(format);
403                }
404
405                if (filter.idFormat == null) {
406                    tagIdRegexp = null;
407                }
408                else {
409                    format = CommonUtil.fillTemplateWithStringsByRegexp(
410                            filter.idFormat, text, commentFormat);
411                    tagIdRegexp = Pattern.compile(format);
412                }
413            }
414            catch (final PatternSyntaxException exc) {
415                throw new IllegalArgumentException(
416                    "unable to parse expanded comment " + format, exc);
417            }
418        }
419
420        /**
421         * Returns line number of the tag in the source file.
422         *
423         * @return the line number of the tag in the source file.
424         */
425        /* package */ int getLine() {
426            return line;
427        }
428
429        /**
430         * Determines the column number of the tag in the source file.
431         * Will be 0 for all lines of multiline comment, except the
432         * first line.
433         *
434         * @return the column number of the tag in the source file.
435         */
436        /* package */ int getColumn() {
437            return column;
438        }
439
440        /**
441         * Determines whether the suppression turns checkstyle reporting on or
442         * off.
443         *
444         * @return {@code ON} if the suppression turns reporting on.
445         */
446        /* package */ TagType getTagType() {
447            return tagType;
448        }
449
450        /**
451         * Compares the position of this tag in the file
452         * with the position of another tag.
453         *
454         * @param object the tag to compare with this one.
455         * @return a negative number if this tag is before the other tag,
456         *     0 if they are at the same position, and a positive number if this
457         *     tag is after the other tag.
458         */
459        @Override
460        public int compareTo(Tag object) {
461            final int result;
462            if (line == object.line) {
463                result = Integer.compare(column, object.column);
464            }
465            else {
466                result = Integer.compare(line, object.line);
467            }
468            return result;
469        }
470
471        /**
472         * Indicates whether some other object is "equal to" this one.
473         * Suppression on enumeration is needed so code stays consistent.
474         *
475         * @noinspection EqualsCalledOnEnumConstant
476         * @noinspectionreason EqualsCalledOnEnumConstant - enumeration is needed to keep
477         *      code consistent
478         */
479        @Override
480        public boolean equals(Object other) {
481            if (this == other) {
482                return true;
483            }
484            if (other == null || getClass() != other.getClass()) {
485                return false;
486            }
487            final Tag tag = (Tag) other;
488            return line == tag.line
489                    && column == tag.column
490                    && Objects.equals(tagType, tag.tagType)
491                    && Objects.equals(text, tag.text)
492                    && Objects.equals(tagCheckRegexp, tag.tagCheckRegexp)
493                    && Objects.equals(tagMessageRegexp, tag.tagMessageRegexp)
494                    && Objects.equals(tagIdRegexp, tag.tagIdRegexp);
495        }
496
497        @Override
498        public int hashCode() {
499            return Objects.hash(text, line, column, tagType, tagCheckRegexp, tagMessageRegexp,
500                    tagIdRegexp);
501        }
502
503        /**
504         * Determines whether the source of an audit event
505         * matches the text of this tag.
506         *
507         * @param event the {@code TreeWalkerAuditEvent} to check.
508         * @return true if the source of event matches the text of this tag.
509         */
510        /* package */ boolean isMatch(TreeWalkerAuditEvent event) {
511            return isCheckMatch(event) && isIdMatch(event) && isMessageMatch(event);
512        }
513
514        /**
515         * Checks whether {@link TreeWalkerAuditEvent} source name matches the check format.
516         *
517         * @param event {@link TreeWalkerAuditEvent} instance.
518         * @return true if the {@link TreeWalkerAuditEvent} source name matches the check format.
519         */
520        private boolean isCheckMatch(TreeWalkerAuditEvent event) {
521            final Matcher checkMatcher = tagCheckRegexp.matcher(event.getSourceName());
522            return checkMatcher.find();
523        }
524
525        /**
526         * Checks whether the {@link TreeWalkerAuditEvent} module ID matches the ID format.
527         *
528         * @param event {@link TreeWalkerAuditEvent} instance.
529         * @return true if the {@link TreeWalkerAuditEvent} module ID matches the ID format.
530         */
531        private boolean isIdMatch(TreeWalkerAuditEvent event) {
532            boolean match = true;
533            if (tagIdRegexp != null) {
534                if (event.getModuleId() == null) {
535                    match = false;
536                }
537                else {
538                    final Matcher idMatcher = tagIdRegexp.matcher(event.getModuleId());
539                    match = idMatcher.find();
540                }
541            }
542            return match;
543        }
544
545        /**
546         * Checks whether the {@link TreeWalkerAuditEvent} message matches the message format.
547         *
548         * @param event {@link TreeWalkerAuditEvent} instance.
549         * @return true if the {@link TreeWalkerAuditEvent} message matches the message format.
550         */
551        private boolean isMessageMatch(TreeWalkerAuditEvent event) {
552            boolean match = true;
553            if (tagMessageRegexp != null) {
554                final Matcher messageMatcher = tagMessageRegexp.matcher(event.getMessage());
555                match = messageMatcher.find();
556            }
557            return match;
558        }
559
560        @Override
561        public String toString() {
562            return "Tag[text='" + text + '\''
563                    + ", line=" + line
564                    + ", column=" + column
565                    + ", type=" + tagType
566                    + ", tagCheckRegexp=" + tagCheckRegexp
567                    + ", tagMessageRegexp=" + tagMessageRegexp
568                    + ", tagIdRegexp=" + tagIdRegexp + ']';
569        }
570
571    }
572
573}