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