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.List;
025import java.util.Objects;
026import java.util.regex.Matcher;
027import java.util.regex.Pattern;
028import java.util.regex.PatternSyntaxException;
029
030import com.puppycrawl.tools.checkstyle.AbstractAutomaticBean;
031import com.puppycrawl.tools.checkstyle.PropertyType;
032import com.puppycrawl.tools.checkstyle.TreeWalkerAuditEvent;
033import com.puppycrawl.tools.checkstyle.TreeWalkerFilter;
034import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
035import com.puppycrawl.tools.checkstyle.api.FileContents;
036import com.puppycrawl.tools.checkstyle.api.TextBlock;
037import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
038import com.puppycrawl.tools.checkstyle.utils.WeakReferenceHolder;
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 <module name="TreeWalker"/>}) 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 Treewalker as parent module.
064 * </p>
065 *
066 * @since 5.0
067 */
068public class SuppressWithNearbyCommentFilter
069    extends AbstractAutomaticBean
070    implements TreeWalkerFilter {
071
072    /** Format to turn checkstyle reporting off. */
073    private static final String DEFAULT_COMMENT_FORMAT =
074        "SUPPRESS CHECKSTYLE (\\w+)";
075
076    /** Default regex for checks that should be suppressed. */
077    private static final String DEFAULT_CHECK_FORMAT = ".*";
078
079    /** Default regex for lines that should be suppressed. */
080    private static final String DEFAULT_INFLUENCE_FORMAT = "0";
081
082    /** Tagged comments. */
083    private final List<Tag> tags = new ArrayList<>();
084
085    /**
086     * References the current FileContents for this filter.
087     * Since this is a weak reference to the FileContents, the FileContents
088     * can be reclaimed as soon as the strong references in TreeWalker
089     * are reassigned to the next FileContents, at which time filtering for
090     * the current FileContents is finished.
091     */
092    private final WeakReferenceHolder<FileContents> fileContentsHolder =
093            new WeakReferenceHolder<>();
094
095    /** Control whether to check C style comments (&#47;* ... *&#47;). */
096    private boolean checkC = true;
097
098    /** Control whether to check C++ style comments ({@code //}). */
099    // -@cs[AbbreviationAsWordInName] We can not change it as,
100    // check's property is a part of API (used in configurations).
101    private boolean checkCPP = true;
102
103    /** Specify comment pattern to trigger filter to begin suppression. */
104    private Pattern commentFormat = Pattern.compile(DEFAULT_COMMENT_FORMAT);
105
106    /** Specify check pattern to suppress. */
107    @XdocsPropertyType(PropertyType.PATTERN)
108    private String checkFormat = DEFAULT_CHECK_FORMAT;
109
110    /** Define message pattern to suppress. */
111    @XdocsPropertyType(PropertyType.PATTERN)
112    private String messageFormat;
113
114    /** Specify check ID pattern to suppress. */
115    @XdocsPropertyType(PropertyType.PATTERN)
116    private String idFormat;
117
118    /**
119     * Specify negative/zero/positive value that defines the number of lines
120     * preceding/at/following the suppression comment.
121     */
122    private String influenceFormat = DEFAULT_INFLUENCE_FORMAT;
123
124    /**
125     * Creates a new {@code SuppressWithNearbyCommentFilter} instance.
126     */
127    public SuppressWithNearbyCommentFilter() {
128        // no code by default
129    }
130
131    /**
132     * Setter to specify comment pattern to trigger filter to begin suppression.
133     *
134     * @param pattern a pattern.
135     * @since 5.0
136     */
137    public final void setCommentFormat(Pattern pattern) {
138        commentFormat = pattern;
139    }
140
141    /**
142     * Setter to specify check pattern to suppress.
143     * The pattern is matched against the fully qualified class name of the Check.
144     *
145     * @param format a {@code String} value
146     * @since 5.0
147     */
148    public final void setCheckFormat(String format) {
149        checkFormat = format;
150    }
151
152    /**
153     * Setter to define message pattern to suppress.
154     *
155     * @param format a {@code String} value
156     * @since 5.0
157     */
158    public void setMessageFormat(String format) {
159        messageFormat = format;
160    }
161
162    /**
163     * Setter to specify check ID pattern to suppress.
164     *
165     * @param format a {@code String} value
166     * @since 8.24
167     */
168    public void setIdFormat(String format) {
169        idFormat = format;
170    }
171
172    /**
173     * Setter to specify negative/zero/positive value that defines the number
174     * of lines preceding/at/following the suppression comment.
175     *
176     * @param format a {@code String} value
177     * @since 5.0
178     */
179    public final void setInfluenceFormat(String format) {
180        influenceFormat = format;
181    }
182
183    /**
184     * Setter to control whether to check C++ style comments ({@code //}).
185     *
186     * @param checkCppComments {@code true} if C++ comments are checked.
187     * @since 5.0
188     */
189    // -@cs[AbbreviationAsWordInName] We can not change it as,
190    // check's property is a part of API (used in configurations).
191    public void setCheckCPP(boolean checkCppComments) {
192        checkCPP = checkCppComments;
193    }
194
195    /**
196     * Setter to control whether to check C style comments (&#47;* ... *&#47;).
197     *
198     * @param checkC {@code true} if C comments are checked.
199     * @since 5.0
200     */
201    public void setCheckC(boolean checkC) {
202        this.checkC = checkC;
203    }
204
205    @Override
206    protected void finishLocalSetup() {
207        // No code by default
208    }
209
210    @Override
211    public boolean accept(TreeWalkerAuditEvent event) {
212        boolean accepted = true;
213
214        if (event.violation() != null) {
215            fileContentsHolder.lazyUpdate(event.fileContents(), this::tagSuppressions);
216            if (matchesTag(event)) {
217                accepted = false;
218            }
219        }
220        return accepted;
221    }
222
223    /**
224     * Whether current event matches any tag from {@link #tags}.
225     *
226     * @param event TreeWalkerAuditEvent to test match on {@code #tags}.
227     * @return true if event matches any tag from {@code #tags}, false otherwise.
228     */
229    private boolean matchesTag(TreeWalkerAuditEvent event) {
230        boolean result = false;
231        for (final Tag tag : tags) {
232            if (tag.isMatch(event)) {
233                result = true;
234                break;
235            }
236        }
237        return result;
238    }
239
240    /**
241     * Collects all the suppression tags for all comments into a list and sorts the list.
242     */
243    private void tagSuppressions() {
244        tags.clear();
245        final FileContents contents = fileContentsHolder.get();
246        if (checkCPP) {
247            tagSuppressions(contents.getSingleLineComments().values());
248        }
249        if (checkC) {
250            final Collection<List<TextBlock>> cComments =
251                contents.getBlockComments().values();
252            cComments.forEach(this::tagSuppressions);
253        }
254    }
255
256    /**
257     * Appends the suppressions in a collection of comments to the full set of suppression tags.
258     *
259     * @param comments the set of comments.
260     */
261    private void tagSuppressions(Collection<TextBlock> comments) {
262        for (final TextBlock comment : comments) {
263            final int startLineNo = comment.getStartLineNo();
264            final String[] text = comment.getText();
265            tagCommentLine(text[0], startLineNo);
266            for (int index = 1; index < text.length; index++) {
267                tagCommentLine(text[index], startLineNo + index);
268            }
269        }
270    }
271
272    /**
273     * Tags a string if it matches the format for turning
274     * checkstyle reporting on or the format for turning reporting off.
275     *
276     * @param text the string to tag.
277     * @param line the line number of text.
278     */
279    private void tagCommentLine(String text, int line) {
280        final Matcher matcher = commentFormat.matcher(text);
281        if (matcher.find()) {
282            addTag(matcher.group(0), line);
283        }
284    }
285
286    /**
287     * Adds a comment suppression {@code Tag} to the list of all tags.
288     *
289     * @param text the text of the tag.
290     * @param line the line number of the tag.
291     */
292    private void addTag(String text, int line) {
293        final Tag tag = new Tag(text, line, this);
294        tags.add(tag);
295    }
296
297    /**
298     * A Tag holds a suppression comment and its location.
299     */
300    private static final class Tag {
301
302        /** The text of the tag. */
303        private final String text;
304
305        /** The first line where warnings may be suppressed. */
306        private final int firstLine;
307
308        /** The last line where warnings may be suppressed. */
309        private final int lastLine;
310
311        /** The parsed check regexp, expanded for the text of this tag. */
312        private final Pattern tagCheckRegexp;
313
314        /** The parsed message regexp, expanded for the text of this tag. */
315        private final Pattern tagMessageRegexp;
316
317        /** The parsed check ID regexp, expanded for the text of this tag. */
318        private final Pattern tagIdRegexp;
319
320        /**
321         * Constructs a tag.
322         *
323         * @param text the text of the suppression.
324         * @param line the line number.
325         * @param filter the {@code SuppressWithNearbyCommentFilter} with the context
326         * @throws IllegalArgumentException if unable to parse expanded text.
327         */
328        private Tag(String text, int line, SuppressWithNearbyCommentFilter filter) {
329            this.text = text;
330
331            // Expand regexp for check and message
332            // Does not intern Patterns with Utils.getPattern()
333            String format = "";
334            try {
335                format = CommonUtil.fillTemplateWithStringsByRegexp(
336                        filter.checkFormat, text, filter.commentFormat);
337                tagCheckRegexp = Pattern.compile(format);
338                if (filter.messageFormat == null) {
339                    tagMessageRegexp = null;
340                }
341                else {
342                    format = CommonUtil.fillTemplateWithStringsByRegexp(
343                            filter.messageFormat, text, filter.commentFormat);
344                    tagMessageRegexp = Pattern.compile(format);
345                }
346                if (filter.idFormat == null) {
347                    tagIdRegexp = null;
348                }
349                else {
350                    format = CommonUtil.fillTemplateWithStringsByRegexp(
351                            filter.idFormat, text, filter.commentFormat);
352                    tagIdRegexp = Pattern.compile(format);
353                }
354                format = CommonUtil.fillTemplateWithStringsByRegexp(
355                        filter.influenceFormat, text, filter.commentFormat);
356
357                final int influence = parseInfluence(format, filter.influenceFormat, text);
358
359                if (influence >= 1) {
360                    firstLine = line;
361                    lastLine = line + influence;
362                }
363                else {
364                    firstLine = line + influence;
365                    lastLine = line;
366                }
367            }
368            catch (final PatternSyntaxException exc) {
369                throw new IllegalArgumentException(
370                    "unable to parse expanded comment " + format, exc);
371            }
372        }
373
374        /**
375         * Gets influence from suppress filter influence format param.
376         *
377         * @param format          influence format to parse
378         * @param influenceFormat raw influence format
379         * @param text            text of the suppression
380         * @return parsed influence
381         * @throws IllegalArgumentException when unable to parse int in format
382         */
383        private static int parseInfluence(String format, String influenceFormat, String text) {
384            try {
385                return Integer.parseInt(format);
386            }
387            catch (final NumberFormatException exc) {
388                throw new IllegalArgumentException("unable to parse influence from '" + text
389                        + "' using " + influenceFormat, exc);
390            }
391        }
392
393        @Override
394        public boolean equals(Object other) {
395            if (this == other) {
396                return true;
397            }
398            if (other == null || getClass() != other.getClass()) {
399                return false;
400            }
401            final Tag tag = (Tag) other;
402            return firstLine == tag.firstLine
403                    && lastLine == tag.lastLine
404                    && Objects.equals(text, tag.text)
405                    && Objects.equals(tagCheckRegexp, tag.tagCheckRegexp)
406                    && Objects.equals(tagMessageRegexp, tag.tagMessageRegexp)
407                    && Objects.equals(tagIdRegexp, tag.tagIdRegexp);
408        }
409
410        @Override
411        public int hashCode() {
412            return Objects.hash(text, firstLine, lastLine, tagCheckRegexp, tagMessageRegexp,
413                    tagIdRegexp);
414        }
415
416        /**
417         * Determines whether the source of an audit event matches the text of this tag.
418         *
419         * @param event the {@code TreeWalkerAuditEvent} to check.
420         * @return true if the source of event matches the text of this tag.
421         */
422        /* package */ boolean isMatch(TreeWalkerAuditEvent event) {
423            return isInScopeOfSuppression(event)
424                    && isCheckMatch(event)
425                    && isIdMatch(event)
426                    && isMessageMatch(event);
427        }
428
429        /**
430         * Checks whether the {@link TreeWalkerAuditEvent} is in the scope of the suppression.
431         *
432         * @param event {@code TreeWalkerAuditEvent} instance.
433         * @return true if the {@code TreeWalkerAuditEvent} is in the scope of the suppression.
434         */
435        private boolean isInScopeOfSuppression(TreeWalkerAuditEvent event) {
436            final int line = event.getLine();
437            return line >= firstLine && line <= lastLine;
438        }
439
440        /**
441         * Checks whether {@link TreeWalkerAuditEvent} source name matches the check format.
442         *
443         * @param event {@code TreeWalkerAuditEvent} instance.
444         * @return true if the {@code TreeWalkerAuditEvent} source name matches the check format.
445         */
446        private boolean isCheckMatch(TreeWalkerAuditEvent event) {
447            final Matcher checkMatcher = tagCheckRegexp.matcher(event.getSourceName());
448            return checkMatcher.find();
449        }
450
451        /**
452         * Checks whether the {@link TreeWalkerAuditEvent} module ID matches the ID format.
453         *
454         * @param event {@code TreeWalkerAuditEvent} instance.
455         * @return true if the {@code TreeWalkerAuditEvent} module ID matches the ID format.
456         */
457        private boolean isIdMatch(TreeWalkerAuditEvent event) {
458            boolean match = true;
459            if (tagIdRegexp != null) {
460                if (event.getModuleId() == null) {
461                    match = false;
462                }
463                else {
464                    final Matcher idMatcher = tagIdRegexp.matcher(event.getModuleId());
465                    match = idMatcher.find();
466                }
467            }
468            return match;
469        }
470
471        /**
472         * Checks whether the {@link TreeWalkerAuditEvent} message matches the message format.
473         *
474         * @param event {@code TreeWalkerAuditEvent} instance.
475         * @return true if the {@code TreeWalkerAuditEvent} message matches the message format.
476         */
477        private boolean isMessageMatch(TreeWalkerAuditEvent event) {
478            boolean match = true;
479            if (tagMessageRegexp != null) {
480                final Matcher messageMatcher = tagMessageRegexp.matcher(event.getMessage());
481                match = messageMatcher.find();
482            }
483            return match;
484        }
485
486        @Override
487        public String toString() {
488            return "Tag[text='" + text + '\''
489                    + ", firstLine=" + firstLine
490                    + ", lastLine=" + lastLine
491                    + ", tagCheckRegexp=" + tagCheckRegexp
492                    + ", tagMessageRegexp=" + tagMessageRegexp
493                    + ", tagIdRegexp=" + tagIdRegexp
494                    + ']';
495        }
496
497    }
498
499}