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