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