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.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;
039
040/**
041 * <div>
042 * Filter {@code SuppressWithNearbyCommentFilter} uses nearby comments to suppress audit events.
043 * </div>
044 *
045 * <p>
046 * Rationale: Same as {@code SuppressionCommentFilter}.
047 * Whereas the SuppressionCommentFilter uses matched pairs of filters to turn
048 * on/off comment matching, {@code SuppressWithNearbyCommentFilter} uses single comments.
049 * This requires fewer lines to mark a region, and may be aesthetically preferable in some contexts.
050 * </p>
051 *
052 * <p>
053 * Attention: This filter may only be specified within the TreeWalker module
054 * ({@code &lt;module name="TreeWalker"/&gt;}) and only applies to checks which are also
055 * defined within this module. To filter non-TreeWalker checks like {@code RegexpSingleline},
056 * a
057 * <a href="https://checkstyle.org/filters/suppresswithplaintextcommentfilter.html">
058 * SuppressWithPlainTextCommentFilter</a> or similar filter must be used.
059 * </p>
060 *
061 * <p>
062 * Notes:
063 * SuppressWithNearbyCommentFilter can suppress Checks that have
064 * Treewalker as parent module.
065 * </p>
066 * <ul>
067 * <li>
068 * Property {@code checkC} - Control whether to check C style comments ({@code &#47;* ... *&#47;}).
069 * Type is {@code boolean}.
070 * Default value is {@code true}.
071 * </li>
072 * <li>
073 * Property {@code checkCPP} - Control whether to check C++ style comments ({@code //}).
074 * Type is {@code boolean}.
075 * Default value is {@code true}.
076 * </li>
077 * <li>
078 * Property {@code checkFormat} - Specify check pattern to suppress.
079 * Type is {@code java.util.regex.Pattern}.
080 * Default value is {@code ".*"}.
081 * </li>
082 * <li>
083 * Property {@code commentFormat} - Specify comment pattern to trigger filter to begin suppression.
084 * Type is {@code java.util.regex.Pattern}.
085 * Default value is {@code "SUPPRESS CHECKSTYLE (\w+)"}.
086 * </li>
087 * <li>
088 * Property {@code idFormat} - Specify check ID pattern to suppress.
089 * Type is {@code java.util.regex.Pattern}.
090 * Default value is {@code null}.
091 * </li>
092 * <li>
093 * Property {@code influenceFormat} - Specify negative/zero/positive value that
094 * defines the number of lines preceding/at/following the suppression comment.
095 * Type is {@code java.lang.String}.
096 * Default value is {@code "0"}.
097 * </li>
098 * <li>
099 * Property {@code messageFormat} - Define message pattern to suppress.
100 * Type is {@code java.util.regex.Pattern}.
101 * Default value is {@code null}.
102 * </li>
103 * </ul>
104 *
105 * <p>
106 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
107 * </p>
108 *
109 * @since 5.0
110 */
111public class SuppressWithNearbyCommentFilter
112    extends AbstractAutomaticBean
113    implements TreeWalkerFilter {
114
115    /** Format to turn checkstyle reporting off. */
116    private static final String DEFAULT_COMMENT_FORMAT =
117        "SUPPRESS CHECKSTYLE (\\w+)";
118
119    /** Default regex for checks that should be suppressed. */
120    private static final String DEFAULT_CHECK_FORMAT = ".*";
121
122    /** Default regex for lines that should be suppressed. */
123    private static final String DEFAULT_INFLUENCE_FORMAT = "0";
124
125    /** Tagged comments. */
126    private final List<Tag> tags = new ArrayList<>();
127
128    /** Control whether to check C style comments ({@code &#47;* ... *&#47;}). */
129    private boolean checkC = true;
130
131    /** Control whether to check C++ style comments ({@code //}). */
132    // -@cs[AbbreviationAsWordInName] We can not change it as,
133    // check's property is a part of API (used in configurations).
134    private boolean checkCPP = true;
135
136    /** Specify comment pattern to trigger filter to begin suppression. */
137    private Pattern commentFormat = Pattern.compile(DEFAULT_COMMENT_FORMAT);
138
139    /** Specify check pattern to suppress. */
140    @XdocsPropertyType(PropertyType.PATTERN)
141    private String checkFormat = DEFAULT_CHECK_FORMAT;
142
143    /** Define message pattern to suppress. */
144    @XdocsPropertyType(PropertyType.PATTERN)
145    private String messageFormat;
146
147    /** Specify check ID pattern to suppress. */
148    @XdocsPropertyType(PropertyType.PATTERN)
149    private String idFormat;
150
151    /**
152     * Specify negative/zero/positive value that defines the number of lines
153     * preceding/at/following the suppression comment.
154     */
155    private String influenceFormat = DEFAULT_INFLUENCE_FORMAT;
156
157    /**
158     * References the current FileContents for this filter.
159     * Since this is a weak reference to the FileContents, the FileContents
160     * can be reclaimed as soon as the strong references in TreeWalker
161     * are reassigned to the next FileContents, at which time filtering for
162     * the current FileContents is finished.
163     */
164    private WeakReference<FileContents> fileContentsReference = new WeakReference<>(null);
165
166    /**
167     * Setter to specify comment pattern to trigger filter to begin suppression.
168     *
169     * @param pattern a pattern.
170     * @since 5.0
171     */
172    public final void setCommentFormat(Pattern pattern) {
173        commentFormat = pattern;
174    }
175
176    /**
177     * Returns FileContents for this filter.
178     *
179     * @return the FileContents for this filter.
180     */
181    private FileContents getFileContents() {
182        return fileContentsReference.get();
183    }
184
185    /**
186     * Set the FileContents for this filter.
187     *
188     * @param fileContents the FileContents for this filter.
189     */
190    private void setFileContents(FileContents fileContents) {
191        fileContentsReference = new WeakReference<>(fileContents);
192    }
193
194    /**
195     * Setter to specify check pattern to suppress.
196     *
197     * @param format a {@code String} value
198     * @since 5.0
199     */
200    public final void setCheckFormat(String format) {
201        checkFormat = format;
202    }
203
204    /**
205     * Setter to define message pattern to suppress.
206     *
207     * @param format a {@code String} value
208     * @since 5.0
209     */
210    public void setMessageFormat(String format) {
211        messageFormat = format;
212    }
213
214    /**
215     * Setter to specify check ID pattern to suppress.
216     *
217     * @param format a {@code String} value
218     * @since 8.24
219     */
220    public void setIdFormat(String format) {
221        idFormat = format;
222    }
223
224    /**
225     * Setter to specify negative/zero/positive value that defines the number
226     * of lines preceding/at/following the suppression comment.
227     *
228     * @param format a {@code String} value
229     * @since 5.0
230     */
231    public final void setInfluenceFormat(String format) {
232        influenceFormat = format;
233    }
234
235    /**
236     * Setter to control whether to check C++ style comments ({@code //}).
237     *
238     * @param checkCpp {@code true} if C++ comments are checked.
239     * @since 5.0
240     */
241    // -@cs[AbbreviationAsWordInName] We can not change it as,
242    // check's property is a part of API (used in configurations).
243    public void setCheckCPP(boolean checkCpp) {
244        checkCPP = checkCpp;
245    }
246
247    /**
248     * Setter to control whether to check C style comments ({@code &#47;* ... *&#47;}).
249     *
250     * @param checkC {@code true} if C comments are checked.
251     * @since 5.0
252     */
253    public void setCheckC(boolean checkC) {
254        this.checkC = checkC;
255    }
256
257    @Override
258    protected void finishLocalSetup() {
259        // No code by default
260    }
261
262    @Override
263    public boolean accept(TreeWalkerAuditEvent event) {
264        boolean accepted = true;
265
266        if (event.getViolation() != null) {
267            // Lazy update. If the first event for the current file, update file
268            // contents and tag suppressions
269            final FileContents currentContents = event.getFileContents();
270
271            if (getFileContents() != currentContents) {
272                setFileContents(currentContents);
273                tagSuppressions();
274            }
275            if (matchesTag(event)) {
276                accepted = false;
277            }
278        }
279        return accepted;
280    }
281
282    /**
283     * Whether current event matches any tag from {@link #tags}.
284     *
285     * @param event TreeWalkerAuditEvent to test match on {@link #tags}.
286     * @return true if event matches any tag from {@link #tags}, false otherwise.
287     */
288    private boolean matchesTag(TreeWalkerAuditEvent event) {
289        boolean result = false;
290        for (final Tag tag : tags) {
291            if (tag.isMatch(event)) {
292                result = true;
293                break;
294            }
295        }
296        return result;
297    }
298
299    /**
300     * Collects all the suppression tags for all comments into a list and
301     * sorts the list.
302     */
303    private void tagSuppressions() {
304        tags.clear();
305        final FileContents contents = getFileContents();
306        if (checkCPP) {
307            tagSuppressions(contents.getSingleLineComments().values());
308        }
309        if (checkC) {
310            final Collection<List<TextBlock>> cComments =
311                contents.getBlockComments().values();
312            cComments.forEach(this::tagSuppressions);
313        }
314    }
315
316    /**
317     * Appends the suppressions in a collection of comments to the full
318     * set of suppression tags.
319     *
320     * @param comments the set of comments.
321     */
322    private void tagSuppressions(Collection<TextBlock> comments) {
323        for (final TextBlock comment : comments) {
324            final int startLineNo = comment.getStartLineNo();
325            final String[] text = comment.getText();
326            tagCommentLine(text[0], startLineNo);
327            for (int i = 1; i < text.length; i++) {
328                tagCommentLine(text[i], startLineNo + i);
329            }
330        }
331    }
332
333    /**
334     * Tags a string if it matches the format for turning
335     * checkstyle reporting on or the format for turning reporting off.
336     *
337     * @param text the string to tag.
338     * @param line the line number of text.
339     */
340    private void tagCommentLine(String text, int line) {
341        final Matcher matcher = commentFormat.matcher(text);
342        if (matcher.find()) {
343            addTag(matcher.group(0), line);
344        }
345    }
346
347    /**
348     * Adds a comment suppression {@code Tag} to the list of all tags.
349     *
350     * @param text the text of the tag.
351     * @param line the line number of the tag.
352     */
353    private void addTag(String text, int line) {
354        final Tag tag = new Tag(text, line, this);
355        tags.add(tag);
356    }
357
358    /**
359     * A Tag holds a suppression comment and its location.
360     */
361    private static final class Tag {
362
363        /** The text of the tag. */
364        private final String text;
365
366        /** The first line where warnings may be suppressed. */
367        private final int firstLine;
368
369        /** The last line where warnings may be suppressed. */
370        private final int lastLine;
371
372        /** The parsed check regexp, expanded for the text of this tag. */
373        private final Pattern tagCheckRegexp;
374
375        /** The parsed message regexp, expanded for the text of this tag. */
376        private final Pattern tagMessageRegexp;
377
378        /** The parsed check ID regexp, expanded for the text of this tag. */
379        private final Pattern tagIdRegexp;
380
381        /**
382         * Constructs a tag.
383         *
384         * @param text the text of the suppression.
385         * @param line the line number.
386         * @param filter the {@code SuppressWithNearbyCommentFilter} with the context
387         * @throws IllegalArgumentException if unable to parse expanded text.
388         */
389        private Tag(String text, int line, SuppressWithNearbyCommentFilter filter) {
390            this.text = text;
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, filter.commentFormat);
398                tagCheckRegexp = Pattern.compile(format);
399                if (filter.messageFormat == null) {
400                    tagMessageRegexp = null;
401                }
402                else {
403                    format = CommonUtil.fillTemplateWithStringsByRegexp(
404                            filter.messageFormat, text, filter.commentFormat);
405                    tagMessageRegexp = Pattern.compile(format);
406                }
407                if (filter.idFormat == null) {
408                    tagIdRegexp = null;
409                }
410                else {
411                    format = CommonUtil.fillTemplateWithStringsByRegexp(
412                            filter.idFormat, text, filter.commentFormat);
413                    tagIdRegexp = Pattern.compile(format);
414                }
415                format = CommonUtil.fillTemplateWithStringsByRegexp(
416                        filter.influenceFormat, text, filter.commentFormat);
417
418                final int influence = parseInfluence(format, filter.influenceFormat, text);
419
420                if (influence >= 1) {
421                    firstLine = line;
422                    lastLine = line + influence;
423                }
424                else {
425                    firstLine = line + influence;
426                    lastLine = line;
427                }
428            }
429            catch (final PatternSyntaxException exc) {
430                throw new IllegalArgumentException(
431                    "unable to parse expanded comment " + format, exc);
432            }
433        }
434
435        /**
436         * Gets influence from suppress filter influence format param.
437         *
438         * @param format          influence format to parse
439         * @param influenceFormat raw influence format
440         * @param text            text of the suppression
441         * @return parsed influence
442         * @throws IllegalArgumentException when unable to parse int in format
443         */
444        private static int parseInfluence(String format, String influenceFormat, String text) {
445            try {
446                return Integer.parseInt(format);
447            }
448            catch (final NumberFormatException exc) {
449                throw new IllegalArgumentException("unable to parse influence from '" + text
450                        + "' using " + influenceFormat, exc);
451            }
452        }
453
454        @Override
455        public boolean equals(Object other) {
456            if (this == other) {
457                return true;
458            }
459            if (other == null || getClass() != other.getClass()) {
460                return false;
461            }
462            final Tag tag = (Tag) other;
463            return Objects.equals(firstLine, tag.firstLine)
464                    && Objects.equals(lastLine, tag.lastLine)
465                    && Objects.equals(text, tag.text)
466                    && Objects.equals(tagCheckRegexp, tag.tagCheckRegexp)
467                    && Objects.equals(tagMessageRegexp, tag.tagMessageRegexp)
468                    && Objects.equals(tagIdRegexp, tag.tagIdRegexp);
469        }
470
471        @Override
472        public int hashCode() {
473            return Objects.hash(text, firstLine, lastLine, tagCheckRegexp, tagMessageRegexp,
474                    tagIdRegexp);
475        }
476
477        /**
478         * Determines whether the source of an audit event
479         * matches the text of this tag.
480         *
481         * @param event the {@code TreeWalkerAuditEvent} to check.
482         * @return true if the source of event matches the text of this tag.
483         */
484        public boolean isMatch(TreeWalkerAuditEvent event) {
485            return isInScopeOfSuppression(event)
486                    && isCheckMatch(event)
487                    && isIdMatch(event)
488                    && isMessageMatch(event);
489        }
490
491        /**
492         * Checks whether the {@link TreeWalkerAuditEvent} is in the scope of the suppression.
493         *
494         * @param event {@link TreeWalkerAuditEvent} instance.
495         * @return true if the {@link TreeWalkerAuditEvent} is in the scope of the suppression.
496         */
497        private boolean isInScopeOfSuppression(TreeWalkerAuditEvent event) {
498            final int line = event.getLine();
499            return line >= firstLine && line <= lastLine;
500        }
501
502        /**
503         * Checks whether {@link TreeWalkerAuditEvent} source name matches the check format.
504         *
505         * @param event {@link TreeWalkerAuditEvent} instance.
506         * @return true if the {@link TreeWalkerAuditEvent} source name matches the check format.
507         */
508        private boolean isCheckMatch(TreeWalkerAuditEvent event) {
509            final Matcher checkMatcher = tagCheckRegexp.matcher(event.getSourceName());
510            return checkMatcher.find();
511        }
512
513        /**
514         * Checks whether the {@link TreeWalkerAuditEvent} module ID matches the ID format.
515         *
516         * @param event {@link TreeWalkerAuditEvent} instance.
517         * @return true if the {@link TreeWalkerAuditEvent} module ID matches the ID format.
518         */
519        private boolean isIdMatch(TreeWalkerAuditEvent event) {
520            boolean match = true;
521            if (tagIdRegexp != null) {
522                if (event.getModuleId() == null) {
523                    match = false;
524                }
525                else {
526                    final Matcher idMatcher = tagIdRegexp.matcher(event.getModuleId());
527                    match = idMatcher.find();
528                }
529            }
530            return match;
531        }
532
533        /**
534         * Checks whether the {@link TreeWalkerAuditEvent} message matches the message format.
535         *
536         * @param event {@link TreeWalkerAuditEvent} instance.
537         * @return true if the {@link TreeWalkerAuditEvent} message matches the message format.
538         */
539        private boolean isMessageMatch(TreeWalkerAuditEvent event) {
540            boolean match = true;
541            if (tagMessageRegexp != null) {
542                final Matcher messageMatcher = tagMessageRegexp.matcher(event.getMessage());
543                match = messageMatcher.find();
544            }
545            return match;
546        }
547
548        @Override
549        public String toString() {
550            return "Tag[text='" + text + '\''
551                    + ", firstLine=" + firstLine
552                    + ", lastLine=" + lastLine
553                    + ", tagCheckRegexp=" + tagCheckRegexp
554                    + ", tagMessageRegexp=" + tagMessageRegexp
555                    + ", tagIdRegexp=" + tagIdRegexp
556                    + ']';
557        }
558
559    }
560
561}