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