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