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