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