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 sorts the list.
275     */
276    private void tagSuppressions() {
277        tags.clear();
278        final FileContents contents = fileContentsHolder.get();
279        if (checkCPP) {
280            tagSuppressions(contents.getSingleLineComments().values());
281        }
282        if (checkC) {
283            final Collection<List<TextBlock>> cComments = contents
284                    .getBlockComments().values();
285            cComments.forEach(this::tagSuppressions);
286        }
287        tags.sort(Comparator.naturalOrder());
288    }
289
290    /**
291     * Appends the suppressions in a collection of comments to the full set of suppression tags.
292     *
293     * @param comments the set of comments.
294     */
295    private void tagSuppressions(Collection<TextBlock> comments) {
296        for (TextBlock comment : comments) {
297            final int startLineNo = comment.getStartLineNo();
298            final String[] text = comment.getText();
299            tagCommentLine(text[0], startLineNo, comment.getStartColNo());
300            for (int index = 1; index < text.length; index++) {
301                tagCommentLine(text[index], startLineNo + index, 0);
302            }
303        }
304    }
305
306    /**
307     * Tags a string if it matches the format for turning
308     * checkstyle reporting on or the format for turning reporting off.
309     *
310     * @param text the string to tag.
311     * @param line the line number of text.
312     * @param column the column number of text.
313     */
314    private void tagCommentLine(String text, int line, int column) {
315        final Matcher offMatcher = offCommentFormat.matcher(text);
316        if (offMatcher.find()) {
317            addTag(offMatcher.group(0), line, column, TagType.OFF);
318        }
319        else {
320            final Matcher onMatcher = onCommentFormat.matcher(text);
321            if (onMatcher.find()) {
322                addTag(onMatcher.group(0), line, column, TagType.ON);
323            }
324        }
325    }
326
327    /**
328     * Adds a {@code Tag} to the list of all tags.
329     *
330     * @param text the text of the tag.
331     * @param line the line number of the tag.
332     * @param column the column number of the tag.
333     * @param reportingOn {@code true} if the tag turns checkstyle reporting on.
334     */
335    private void addTag(String text, int line, int column, TagType reportingOn) {
336        final Tag tag = new Tag(line, column, text, reportingOn, this);
337        tags.add(tag);
338    }
339
340    /**
341     * A Tag holds a suppression comment and its location, and determines
342     * whether the suppression turns checkstyle reporting on or off.
343     */
344    private static final class Tag implements Comparable<Tag> {
345
346        /** The text of the tag. */
347        private final String text;
348
349        /** The line number of the tag. */
350        private final int line;
351
352        /** The column number of the tag. */
353        private final int column;
354
355        /** Determines whether the suppression turns checkstyle reporting on. */
356        private final TagType tagType;
357
358        /** The parsed check regexp, expanded for the text of this tag. */
359        private final Pattern tagCheckRegexp;
360
361        /** The parsed message regexp, expanded for the text of this tag. */
362        private final Pattern tagMessageRegexp;
363
364        /** The parsed check ID regexp, expanded for the text of this tag. */
365        private final Pattern tagIdRegexp;
366
367        /**
368         * Constructs a tag.
369         *
370         * @param line the line number.
371         * @param column the column number.
372         * @param text the text of the suppression.
373         * @param tagType {@code ON} if the tag turns checkstyle reporting.
374         * @param filter the {@code SuppressionCommentFilter} with the context
375         * @throws IllegalArgumentException if unable to parse expanded text.
376         */
377        private Tag(int line, int column, String text, TagType tagType,
378                   SuppressionCommentFilter filter) {
379            this.line = line;
380            this.column = column;
381            this.text = text;
382            this.tagType = tagType;
383
384            final Pattern commentFormat;
385            if (this.tagType == TagType.ON) {
386                commentFormat = filter.onCommentFormat;
387            }
388            else {
389                commentFormat = filter.offCommentFormat;
390            }
391
392            // Expand regexp for check and message
393            // Does not intern Patterns with Utils.getPattern()
394            String format = "";
395            try {
396                format = CommonUtil.fillTemplateWithStringsByRegexp(
397                        filter.checkFormat, text, commentFormat);
398                tagCheckRegexp = Pattern.compile(format);
399
400                if (filter.messageFormat == null) {
401                    tagMessageRegexp = null;
402                }
403                else {
404                    format = CommonUtil.fillTemplateWithStringsByRegexp(
405                            filter.messageFormat, text, commentFormat);
406                    tagMessageRegexp = Pattern.compile(format);
407                }
408
409                if (filter.idFormat == null) {
410                    tagIdRegexp = null;
411                }
412                else {
413                    format = CommonUtil.fillTemplateWithStringsByRegexp(
414                            filter.idFormat, text, commentFormat);
415                    tagIdRegexp = Pattern.compile(format);
416                }
417            }
418            catch (final PatternSyntaxException exc) {
419                throw new IllegalArgumentException(
420                    "unable to parse expanded comment " + format, exc);
421            }
422        }
423
424        /**
425         * Returns line number of the tag in the source file.
426         *
427         * @return the line number of the tag in the source file.
428         */
429        /* package */ int getLine() {
430            return line;
431        }
432
433        /**
434         * Determines the column number of the tag in the source file.
435         * Will be 0 for all lines of multiline comment, except the first line.
436         *
437         * @return the column number of the tag in the source file.
438         */
439        /* package */ int getColumn() {
440            return column;
441        }
442
443        /**
444         * Determines whether the suppression turns checkstyle reporting on or off.
445         *
446         * @return {@code ON} if the suppression turns reporting on.
447         */
448        /* package */ TagType getTagType() {
449            return tagType;
450        }
451
452        /**
453         * Compares the position of this tag in the file with the position of another tag.
454         *
455         * @param object the tag to compare with this one.
456         * @return a negative number if this tag is before the other tag,
457         *     0 if they are at the same position, and a positive number if this
458         *     tag is after the other tag.
459         */
460        @Override
461        public int compareTo(Tag object) {
462            final int result;
463            if (line == object.line) {
464                result = Integer.compare(column, object.column);
465            }
466            else {
467                result = Integer.compare(line, object.line);
468            }
469            return result;
470        }
471
472        /**
473         * Indicates whether some other object is "equal to" this one.
474         * Suppression on enumeration is needed so code stays consistent.
475         *
476         * @noinspection EqualsCalledOnEnumConstant
477         * @noinspectionreason EqualsCalledOnEnumConstant - enumeration is needed to keep
478         *      code consistent
479         */
480        @Override
481        public boolean equals(Object other) {
482            if (this == other) {
483                return true;
484            }
485            if (other == null || getClass() != other.getClass()) {
486                return false;
487            }
488            final Tag tag = (Tag) other;
489            return line == tag.line
490                    && column == tag.column
491                    && Objects.equals(tagType, tag.tagType)
492                    && Objects.equals(text, tag.text)
493                    && Objects.equals(tagCheckRegexp, tag.tagCheckRegexp)
494                    && Objects.equals(tagMessageRegexp, tag.tagMessageRegexp)
495                    && Objects.equals(tagIdRegexp, tag.tagIdRegexp);
496        }
497
498        @Override
499        public int hashCode() {
500            return Objects.hash(text, line, column, tagType, tagCheckRegexp, tagMessageRegexp,
501                    tagIdRegexp);
502        }
503
504        /**
505         * Determines whether the source of an audit event 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 {@code TreeWalkerAuditEvent} instance.
518         * @return true if the {@code 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 {@code TreeWalkerAuditEvent} instance.
529         * @return true if the {@code 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 {@code TreeWalkerAuditEvent} instance.
549         * @return true if the {@code 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}