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