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.io.IOException;
023import java.nio.charset.StandardCharsets;
024import java.nio.file.Files;
025import java.nio.file.Path;
026import java.nio.file.Paths;
027import java.util.ArrayList;
028import java.util.Collection;
029import java.util.List;
030import java.util.Optional;
031import java.util.regex.Matcher;
032import java.util.regex.Pattern;
033import java.util.regex.PatternSyntaxException;
034
035import com.puppycrawl.tools.checkstyle.AbstractAutomaticBean;
036import com.puppycrawl.tools.checkstyle.PropertyType;
037import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
038import com.puppycrawl.tools.checkstyle.api.AuditEvent;
039import com.puppycrawl.tools.checkstyle.api.FileText;
040import com.puppycrawl.tools.checkstyle.api.Filter;
041import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
042
043/**
044 * <div>
045 * Filter {@code SuppressWithNearbyTextFilter} uses plain text to suppress
046 * nearby audit events. The filter can suppress all checks which have Checker as a parent module.
047 * </div>
048 *
049 * <p>
050 * Setting {@code .*} value to {@code nearbyTextPattern} property will see <b>any</b>
051 * text as a suppression and will likely suppress all audit events in the file. It is
052 * best to set this to a key phrase not commonly used in the file to help denote it
053 * out of the rest of the file as a suppression. See the default value as an example.
054 * </p>
055 * <ul>
056 * <li>
057 * Property {@code checkPattern} - Specify check name pattern to suppress.
058 * Property can also be a RegExp group index at {@code nearbyTextPattern} in
059 * format of {@code $x} and be picked from line that matches {@code nearbyTextPattern}.
060 * Type is {@code java.util.regex.Pattern}.
061 * Default value is {@code ".*"}.
062 * </li>
063 * <li>
064 * Property {@code idPattern} - Specify check ID pattern to suppress.
065 * Type is {@code java.util.regex.Pattern}.
066 * Default value is {@code null}.
067 * </li>
068 * <li>
069 * Property {@code lineRange} - Specify negative/zero/positive value that
070 * defines the number of lines preceding/at/following the suppressing nearby text.
071 * Property can also be a RegExp group index at {@code nearbyTextPattern} in
072 * format of {@code $x} and be picked from line that matches {@code nearbyTextPattern}.
073 * Type is {@code java.lang.String}.
074 * Default value is {@code "0"}.
075 * </li>
076 * <li>
077 * Property {@code messagePattern} - Specify check violation message pattern to suppress.
078 * Type is {@code java.util.regex.Pattern}.
079 * Default value is {@code null}.
080 * </li>
081 * <li>
082 * Property {@code nearbyTextPattern} - Specify nearby text
083 * pattern to trigger filter to begin suppression.
084 * Type is {@code java.util.regex.Pattern}.
085 * Default value is {@code "SUPPRESS CHECKSTYLE (\w+)"}.
086 * </li>
087 * </ul>
088 *
089 * <p>
090 * Parent is {@code com.puppycrawl.tools.checkstyle.Checker}
091 * </p>
092 *
093 * @since 10.10.0
094 */
095public class SuppressWithNearbyTextFilter extends AbstractAutomaticBean implements Filter {
096
097    /** Default nearby text pattern to turn check reporting off. */
098    private static final String DEFAULT_NEARBY_TEXT_PATTERN = "SUPPRESS CHECKSTYLE (\\w+)";
099
100    /** Default regex for checks that should be suppressed. */
101    private static final String DEFAULT_CHECK_PATTERN = ".*";
102
103    /** Default number of lines that should be suppressed. */
104    private static final String DEFAULT_LINE_RANGE = "0";
105
106    /** Suppressions encountered in current file. */
107    private final List<Suppression> suppressions = new ArrayList<>();
108
109    /** Specify nearby text pattern to trigger filter to begin suppression. */
110    @XdocsPropertyType(PropertyType.PATTERN)
111    private Pattern nearbyTextPattern = Pattern.compile(DEFAULT_NEARBY_TEXT_PATTERN);
112
113    /**
114     * Specify check name pattern to suppress. Property can also be a RegExp group index
115     * at {@code nearbyTextPattern} in format of {@code $x} and be picked from line that
116     * matches {@code nearbyTextPattern}.
117     */
118    @XdocsPropertyType(PropertyType.PATTERN)
119    private String checkPattern = DEFAULT_CHECK_PATTERN;
120
121    /** Specify check violation message pattern to suppress. */
122    @XdocsPropertyType(PropertyType.PATTERN)
123    private String messagePattern;
124
125    /** Specify check ID pattern to suppress. */
126    @XdocsPropertyType(PropertyType.PATTERN)
127    private String idPattern;
128
129    /**
130     * Specify negative/zero/positive value that defines the number of lines
131     * preceding/at/following the suppressing nearby text. Property can also be a RegExp group
132     * index at {@code nearbyTextPattern} in format of {@code $x} and be picked
133     * from line that matches {@code nearbyTextPattern}.
134     */
135    private String lineRange = DEFAULT_LINE_RANGE;
136
137    /** The absolute path to the currently processed file. */
138    private String cachedFileAbsolutePath = "";
139
140    /**
141     * Setter to specify nearby text pattern to trigger filter to begin suppression.
142     *
143     * @param pattern a {@code Pattern} value.
144     * @since 10.10.0
145     */
146    public final void setNearbyTextPattern(Pattern pattern) {
147        nearbyTextPattern = pattern;
148    }
149
150    /**
151     * Setter to specify check name pattern to suppress. Property can also
152     * be a RegExp group index at {@code nearbyTextPattern} in
153     * format of {@code $x} and be picked from line that matches {@code nearbyTextPattern}.
154     *
155     * @param pattern a {@code String} value.
156     * @since 10.10.0
157     */
158    public final void setCheckPattern(String pattern) {
159        checkPattern = pattern;
160    }
161
162    /**
163     * Setter to specify check violation message pattern to suppress.
164     *
165     * @param pattern a {@code String} value.
166     * @since 10.10.0
167     */
168    public void setMessagePattern(String pattern) {
169        messagePattern = pattern;
170    }
171
172    /**
173     * Setter to specify check ID pattern to suppress.
174     *
175     * @param pattern a {@code String} value.
176     * @since 10.10.0
177     */
178    public void setIdPattern(String pattern) {
179        idPattern = pattern;
180    }
181
182    /**
183     * Setter to specify negative/zero/positive value that defines the number
184     * of lines preceding/at/following the suppressing nearby text. Property can also
185     * be a RegExp group index at {@code nearbyTextPattern} in
186     * format of {@code $x} and be picked from line that matches {@code nearbyTextPattern}.
187     *
188     * @param format a {@code String} value.
189     * @since 10.10.0
190     */
191    public final void setLineRange(String format) {
192        lineRange = format;
193    }
194
195    @Override
196    public boolean accept(AuditEvent event) {
197        boolean accepted = true;
198
199        if (event.getViolation() != null) {
200            final String eventFileTextAbsolutePath = event.getFileName();
201
202            if (!cachedFileAbsolutePath.equals(eventFileTextAbsolutePath)) {
203                final FileText currentFileText = getFileText(eventFileTextAbsolutePath);
204
205                if (currentFileText != null) {
206                    cachedFileAbsolutePath = currentFileText.getFile().getAbsolutePath();
207                    collectSuppressions(currentFileText);
208                }
209            }
210
211            final Optional<Suppression> nearestSuppression =
212                    getNearestSuppression(suppressions, event);
213            accepted = nearestSuppression.isEmpty();
214        }
215        return accepted;
216    }
217
218    @Override
219    protected void finishLocalSetup() {
220        // No code by default
221    }
222
223    /**
224     * Returns {@link FileText} instance created based on the given file name.
225     *
226     * @param fileName the name of the file.
227     * @return {@link FileText} instance.
228     * @throws IllegalStateException if the file could not be read.
229     */
230    private static FileText getFileText(String fileName) {
231        final Path path = Paths.get(fileName);
232        FileText result = null;
233
234        // some violations can be on a directory, instead of a file
235        if (!Files.isDirectory(path)) {
236            try {
237                result = new FileText(path.toFile(), StandardCharsets.UTF_8.name());
238            }
239            catch (IOException exc) {
240                throw new IllegalStateException("Cannot read source file: " + fileName, exc);
241            }
242        }
243
244        return result;
245    }
246
247    /**
248     * Collets all {@link Suppression} instances retrieved from the given {@link FileText}.
249     *
250     * @param fileText {@link FileText} instance.
251     */
252    private void collectSuppressions(FileText fileText) {
253        suppressions.clear();
254
255        for (int lineNo = 0; lineNo < fileText.size(); lineNo++) {
256            final Suppression suppression = getSuppression(fileText, lineNo);
257            if (suppression != null) {
258                suppressions.add(suppression);
259            }
260        }
261    }
262
263    /**
264     * Tries to extract the suppression from the given line.
265     *
266     * @param fileText {@link FileText} instance.
267     * @param lineNo line number.
268     * @return {@link Suppression} instance.
269     */
270    private Suppression getSuppression(FileText fileText, int lineNo) {
271        final String line = fileText.get(lineNo);
272        final Matcher nearbyTextMatcher = nearbyTextPattern.matcher(line);
273
274        Suppression suppression = null;
275        if (nearbyTextMatcher.find()) {
276            final String text = nearbyTextMatcher.group(0);
277            suppression = new Suppression(text, lineNo + 1, this);
278        }
279
280        return suppression;
281    }
282
283    /**
284     * Finds the nearest {@link Suppression} instance which can suppress
285     * the given {@link AuditEvent}. The nearest suppression is the suppression which scope
286     * is before the line and column of the event.
287     *
288     * @param suppressions collection of {@link Suppression} instances.
289     * @param event {@link AuditEvent} instance.
290     * @return {@link Suppression} instance.
291     */
292    private static Optional<Suppression> getNearestSuppression(Collection<Suppression> suppressions,
293                                                               AuditEvent event) {
294        return suppressions
295                .stream()
296                .filter(suppression -> suppression.isMatch(event))
297                .findFirst();
298    }
299
300    /** The class which represents the suppression. */
301    private static final class Suppression {
302
303        /** The first line where warnings may be suppressed. */
304        private final int firstLine;
305
306        /** The last line where warnings may be suppressed. */
307        private final int lastLine;
308
309        /** The regexp which is used to match the event source.*/
310        private final Pattern eventSourceRegexp;
311
312        /** The regexp which is used to match the event message.*/
313        private Pattern eventMessageRegexp;
314
315        /** The regexp which is used to match the event ID.*/
316        private Pattern eventIdRegexp;
317
318        /**
319         * Constructs new {@code Suppression} instance.
320         *
321         * @param text suppression text.
322         * @param lineNo suppression line number.
323         * @param filter the {@code SuppressWithNearbyTextFilter} with the context.
324         * @throws IllegalArgumentException if there is an error in the filter regex syntax.
325         */
326        private Suppression(
327                String text,
328                int lineNo,
329                SuppressWithNearbyTextFilter filter
330        ) {
331            final Pattern nearbyTextPattern = filter.nearbyTextPattern;
332            final String lineRange = filter.lineRange;
333            String format = "";
334            try {
335                format = CommonUtil.fillTemplateWithStringsByRegexp(
336                        filter.checkPattern, text, nearbyTextPattern);
337                eventSourceRegexp = Pattern.compile(format);
338                if (filter.messagePattern != null) {
339                    format = CommonUtil.fillTemplateWithStringsByRegexp(
340                            filter.messagePattern, text, nearbyTextPattern);
341                    eventMessageRegexp = Pattern.compile(format);
342                }
343                if (filter.idPattern != null) {
344                    format = CommonUtil.fillTemplateWithStringsByRegexp(
345                            filter.idPattern, text, nearbyTextPattern);
346                    eventIdRegexp = Pattern.compile(format);
347                }
348                format = CommonUtil.fillTemplateWithStringsByRegexp(lineRange,
349                                                                    text, nearbyTextPattern);
350
351                final int range = parseRange(format, lineRange, text);
352
353                firstLine = Math.min(lineNo, lineNo + range);
354                lastLine = Math.max(lineNo, lineNo + range);
355            }
356            catch (final PatternSyntaxException exc) {
357                throw new IllegalArgumentException(
358                    "unable to parse expanded comment " + format, exc);
359            }
360        }
361
362        /**
363         * Gets range from suppress filter range format param.
364         *
365         * @param format range format to parse
366         * @param lineRange raw line range
367         * @param text text of the suppression
368         * @return parsed range
369         * @throws IllegalArgumentException when unable to parse int in format
370         */
371        private static int parseRange(String format, String lineRange, String text) {
372            try {
373                return Integer.parseInt(format);
374            }
375            catch (final NumberFormatException exc) {
376                throw new IllegalArgumentException("unable to parse line range from '" + text
377                        + "' using " + lineRange, exc);
378            }
379        }
380
381        /**
382         * Determines whether the source of an audit event
383         * matches the text of this suppression.
384         *
385         * @param event the {@code AuditEvent} to check.
386         * @return true if the source of event matches the text of this suppression.
387         */
388        private boolean isMatch(AuditEvent event) {
389            return isInScopeOfSuppression(event)
390                    && isCheckMatch(event)
391                    && isIdMatch(event)
392                    && isMessageMatch(event);
393        }
394
395        /**
396         * Checks whether the {@link AuditEvent} is in the scope of the suppression.
397         *
398         * @param event {@link AuditEvent} instance.
399         * @return true if the {@link AuditEvent} is in the scope of the suppression.
400         */
401        private boolean isInScopeOfSuppression(AuditEvent event) {
402            final int eventLine = event.getLine();
403            return eventLine >= firstLine && eventLine <= lastLine;
404        }
405
406        /**
407         * Checks whether {@link AuditEvent} source name matches the check pattern.
408         *
409         * @param event {@link AuditEvent} instance.
410         * @return true if the {@link AuditEvent} source name matches the check pattern.
411         */
412        private boolean isCheckMatch(AuditEvent event) {
413            final Matcher checkMatcher = eventSourceRegexp.matcher(event.getSourceName());
414            return checkMatcher.find();
415        }
416
417        /**
418         * Checks whether the {@link AuditEvent} module ID matches the ID pattern.
419         *
420         * @param event {@link AuditEvent} instance.
421         * @return true if the {@link AuditEvent} module ID matches the ID pattern.
422         */
423        private boolean isIdMatch(AuditEvent event) {
424            boolean match = true;
425            if (eventIdRegexp != null) {
426                if (event.getModuleId() == null) {
427                    match = false;
428                }
429                else {
430                    final Matcher idMatcher = eventIdRegexp.matcher(event.getModuleId());
431                    match = idMatcher.find();
432                }
433            }
434            return match;
435        }
436
437        /**
438         * Checks whether the {@link AuditEvent} message matches the message pattern.
439         *
440         * @param event {@link AuditEvent} instance.
441         * @return true if the {@link AuditEvent} message matches the message pattern.
442         */
443        private boolean isMessageMatch(AuditEvent event) {
444            boolean match = true;
445            if (eventMessageRegexp != null) {
446                final Matcher messageMatcher = eventMessageRegexp.matcher(event.getMessage());
447                match = messageMatcher.find();
448            }
449            return match;
450        }
451    }
452}