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