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