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