001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 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.util.ArrayList;
027import java.util.Collection;
028import java.util.Objects;
029import java.util.Optional;
030import java.util.regex.Matcher;
031import java.util.regex.Pattern;
032import java.util.regex.PatternSyntaxException;
033
034import com.puppycrawl.tools.checkstyle.AbstractAutomaticBean;
035import com.puppycrawl.tools.checkstyle.PropertyType;
036import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
037import com.puppycrawl.tools.checkstyle.api.AuditEvent;
038import com.puppycrawl.tools.checkstyle.api.FileText;
039import com.puppycrawl.tools.checkstyle.api.Filter;
040import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
041
042/**
043 * <div>
044 * Filter {@code SuppressWithPlainTextCommentFilter} uses plain text to suppress
045 * audit events. The filter knows nothing about AST, it treats only plain text
046 * comments and extracts the information required for suppression from the plain
047 * text comments. Currently, the filter supports only single-line comments.
048 * </div>
049 *
050 * <p>
051 * Please, be aware of the fact that, it is not recommended to use the filter
052 * for Java code anymore.
053 * </p>
054 *
055 * <p>
056 * Rationale: Sometimes there are legitimate reasons for violating a check.
057 * When this is a matter of the code in question and not personal preference,
058 * the best place to override the policy is in the code itself. Semi-structured
059 * comments can be associated with the check. This is sometimes superior to
060 * a separate suppressions file, which must be kept up-to-date as the source file is edited.
061 * </p>
062 *
063 * <p>
064 * Note that the suppression comment should be put before the violation.
065 * You can use more than one suppression comment each on separate line.
066 * </p>
067 *
068 * <p>
069 * Notes:
070 * Properties {@code offCommentFormat} and {@code onCommentFormat} must have equal
071 * <a href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/regex/Matcher.html#groupCount()">
072 * paren counts</a>.
073 * </p>
074 *
075 * <p>
076 * SuppressWithPlainTextCommentFilter can suppress Checks that have Treewalker or
077 * Checker as parent module.
078 * </p>
079 *
080 * @since 8.6
081 */
082public class SuppressWithPlainTextCommentFilter extends AbstractAutomaticBean implements Filter {
083
084    /** Comment format which turns checkstyle reporting off. */
085    private static final String DEFAULT_OFF_FORMAT = "// CHECKSTYLE:OFF";
086
087    /** Comment format which turns checkstyle reporting on. */
088    private static final String DEFAULT_ON_FORMAT = "// CHECKSTYLE:ON";
089
090    /** Default check format to suppress. By default, the filter suppress all checks. */
091    private static final String DEFAULT_CHECK_FORMAT = ".*";
092
093    /** List of suppressions from the file. By default, Its null. */
094    private final Collection<Suppression> currentFileSuppressionCache = new ArrayList<>();
095
096    /** File name that was suppressed. By default, Its empty. */
097    private String currentFileName = "";
098
099    /** Specify comment pattern to trigger filter to begin suppression. */
100    private Pattern offCommentFormat = CommonUtil.createPattern(DEFAULT_OFF_FORMAT);
101
102    /** Specify comment pattern to trigger filter to end suppression. */
103    private Pattern onCommentFormat = CommonUtil.createPattern(DEFAULT_ON_FORMAT);
104
105    /** Specify check pattern to suppress. */
106    @XdocsPropertyType(PropertyType.PATTERN)
107    private String checkFormat = DEFAULT_CHECK_FORMAT;
108
109    /** Specify message pattern to suppress. */
110    @XdocsPropertyType(PropertyType.PATTERN)
111    private String messageFormat;
112
113    /** Specify check ID pattern to suppress. */
114    @XdocsPropertyType(PropertyType.PATTERN)
115    private String idFormat;
116
117    /**
118     * Creates a new {@code SuppressWithPlainTextCommentFilter} instance.
119     */
120    public SuppressWithPlainTextCommentFilter() {
121        // no code by default
122    }
123
124    /**
125     * Setter to specify comment pattern to trigger filter to begin suppression.
126     *
127     * @param pattern off comment format pattern.
128     * @since 8.6
129     */
130    public final void setOffCommentFormat(Pattern pattern) {
131        offCommentFormat = pattern;
132    }
133
134    /**
135     * Setter to specify comment pattern to trigger filter to end suppression.
136     *
137     * @param pattern  on comment format pattern.
138     * @since 8.6
139     */
140    public final void setOnCommentFormat(Pattern pattern) {
141        onCommentFormat = pattern;
142    }
143
144    /**
145     * Setter to specify check pattern to suppress.
146     * The pattern is matched against the fully qualified class name of the Check.
147     *
148     * @param format pattern for check format.
149     * @since 8.6
150     */
151    public final void setCheckFormat(String format) {
152        checkFormat = format;
153    }
154
155    /**
156     * Setter to specify message pattern to suppress.
157     *
158     * @param format pattern for message format.
159     * @since 8.6
160     */
161    public final void setMessageFormat(String format) {
162        messageFormat = format;
163    }
164
165    /**
166     * Setter to specify check ID pattern to suppress.
167     *
168     * @param format pattern for check ID format
169     * @since 8.24
170     */
171    public final void setIdFormat(String format) {
172        idFormat = format;
173    }
174
175    @Override
176    public boolean accept(AuditEvent event) {
177        boolean accepted = true;
178        if (event.getViolation() != null) {
179            final String eventFileName = event.getFileName();
180
181            if (!currentFileName.equals(eventFileName)) {
182                currentFileName = eventFileName;
183                final FileText fileText = getFileText(eventFileName);
184                currentFileSuppressionCache.clear();
185                if (fileText != null) {
186                    cacheSuppressions(fileText);
187                }
188            }
189
190            accepted = getNearestSuppression(currentFileSuppressionCache, event) == null;
191        }
192        return accepted;
193    }
194
195    @Override
196    protected void finishLocalSetup() {
197        // No code by default
198    }
199
200    /**
201     * Caches {@link FileText} instance created based on the given file name.
202     *
203     * @param fileName the name of the file.
204     * @return {@code FileText} instance.
205     * @throws IllegalStateException if the file could not be read.
206     */
207    private static FileText getFileText(String fileName) {
208        final Path path = Path.of(fileName);
209        FileText result = null;
210
211        // some violations can be on a directory, instead of a file
212        if (!Files.isDirectory(path)) {
213            try {
214                result = new FileText(path.toFile(), StandardCharsets.UTF_8.name());
215            }
216            catch (IOException exc) {
217                throw new IllegalStateException("Cannot read source file: " + fileName, exc);
218            }
219        }
220
221        return result;
222    }
223
224    /**
225     * Collects the list of {@link Suppression} instances retrieved from the given {@link FileText}.
226     *
227     * @param fileText {@code FileText} instance.
228     */
229    private void cacheSuppressions(FileText fileText) {
230        for (int lineNo = 0; lineNo < fileText.size(); lineNo++) {
231            final Optional<Suppression> suppression = getSuppression(fileText, lineNo);
232            suppression.ifPresent(currentFileSuppressionCache::add);
233        }
234    }
235
236    /**
237     * Tries to extract the suppression from the given line.
238     *
239     * @param fileText {@link FileText} instance.
240     * @param lineNo line number.
241     * @return {@link Optional} of {@link Suppression}.
242     */
243    private Optional<Suppression> getSuppression(FileText fileText, int lineNo) {
244        final String line = fileText.get(lineNo);
245        final Matcher onCommentMatcher = onCommentFormat.matcher(line);
246        final Matcher offCommentMatcher = offCommentFormat.matcher(line);
247
248        Suppression suppression = null;
249        if (onCommentMatcher.find()) {
250            suppression = new Suppression(onCommentMatcher.group(0),
251                lineNo + 1, SuppressionType.ON, this);
252        }
253        if (offCommentMatcher.find()) {
254            suppression = new Suppression(offCommentMatcher.group(0),
255                lineNo + 1, SuppressionType.OFF, this);
256        }
257
258        return Optional.ofNullable(suppression);
259    }
260
261    /**
262     * Finds the nearest {@link Suppression} instance which can suppress
263     * the given {@link AuditEvent}. The nearest suppression is the suppression which scope
264     * is before the line and column of the event.
265     *
266     * @param suppressions collection of {@code Suppression} instances.
267     * @param event {@code AuditEvent} instance.
268     * @return {@code Suppression} instance.
269     */
270    private static Suppression getNearestSuppression(Collection<Suppression> suppressions,
271                                                     AuditEvent event) {
272        return suppressions
273            .stream()
274            .filter(suppression -> suppression.isMatch(event))
275            .reduce((first, second) -> second)
276            .filter(suppression -> suppression.suppressionType != SuppressionType.ON)
277            .orElse(null);
278    }
279
280    /** Enum which represents the type of the suppression. */
281    private enum SuppressionType {
282
283        /** On suppression type. */
284        ON,
285        /** Off suppression type. */
286        OFF,
287
288    }
289
290    /** The class which represents the suppression. */
291    private static final class Suppression {
292
293        /** The regexp which is used to match the event source.*/
294        private final Pattern eventSourceRegexp;
295        /** The regexp which is used to match the event message.*/
296        private final Pattern eventMessageRegexp;
297        /** The regexp which is used to match the event ID.*/
298        private final Pattern eventIdRegexp;
299
300        /** Suppression line.*/
301        private final int lineNo;
302
303        /** Suppression type. */
304        private final SuppressionType suppressionType;
305
306        /**
307         * Creates new suppression instance.
308         *
309         * @param text suppression text.
310         * @param lineNo suppression line number.
311         * @param suppressionType suppression type.
312         * @param filter the {@link SuppressWithPlainTextCommentFilter} with the context.
313         * @throws IllegalArgumentException if there is an error in the filter regex syntax.
314         */
315        private Suppression(
316            String text,
317            int lineNo,
318            SuppressionType suppressionType,
319            SuppressWithPlainTextCommentFilter filter
320        ) {
321            this.lineNo = lineNo;
322            this.suppressionType = suppressionType;
323
324            final Pattern commentFormat;
325            if (this.suppressionType == SuppressionType.ON) {
326                commentFormat = filter.onCommentFormat;
327            }
328            else {
329                commentFormat = filter.offCommentFormat;
330            }
331
332            // Expand regexp for check and message
333            // Does not intern Patterns with Utils.getPattern()
334            String format = "";
335            try {
336                format = CommonUtil.fillTemplateWithStringsByRegexp(
337                        filter.checkFormat, text, commentFormat);
338                eventSourceRegexp = Pattern.compile(format);
339                if (filter.messageFormat == null) {
340                    eventMessageRegexp = null;
341                }
342                else {
343                    format = CommonUtil.fillTemplateWithStringsByRegexp(
344                            filter.messageFormat, text, commentFormat);
345                    eventMessageRegexp = Pattern.compile(format);
346                }
347                if (filter.idFormat == null) {
348                    eventIdRegexp = null;
349                }
350                else {
351                    format = CommonUtil.fillTemplateWithStringsByRegexp(
352                            filter.idFormat, text, commentFormat);
353                    eventIdRegexp = Pattern.compile(format);
354                }
355            }
356            catch (final PatternSyntaxException exc) {
357                throw new IllegalArgumentException(
358                    "unable to parse expanded comment " + format, exc);
359            }
360        }
361
362        /**
363         * Indicates whether some other object is "equal to" this one.
364         *
365         * @noinspection EqualsCalledOnEnumConstant
366         * @noinspectionreason EqualsCalledOnEnumConstant - enumeration is needed to keep
367         *      code consistent
368         */
369        @Override
370        public boolean equals(Object other) {
371            if (this == other) {
372                return true;
373            }
374            if (other == null || getClass() != other.getClass()) {
375                return false;
376            }
377            final Suppression suppression = (Suppression) other;
378            return lineNo == suppression.lineNo
379                    && Objects.equals(suppressionType, suppression.suppressionType)
380                    && Objects.equals(eventSourceRegexp, suppression.eventSourceRegexp)
381                    && Objects.equals(eventMessageRegexp, suppression.eventMessageRegexp)
382                    && Objects.equals(eventIdRegexp, suppression.eventIdRegexp);
383        }
384
385        @Override
386        public int hashCode() {
387            return Objects.hash(
388                lineNo, suppressionType, eventSourceRegexp, eventMessageRegexp,
389                eventIdRegexp);
390        }
391
392        /**
393         * Checks whether the suppression matches the given {@link AuditEvent}.
394         *
395         * @param event {@code AuditEvent} instance.
396         * @return true if the suppression matches {@code AuditEvent}.
397         */
398        private boolean isMatch(AuditEvent event) {
399            return isInScopeOfSuppression(event)
400                    && isCheckMatch(event)
401                    && isIdMatch(event)
402                    && isMessageMatch(event);
403        }
404
405        /**
406         * Checks whether {@link AuditEvent} is in the scope of the suppression.
407         *
408         * @param event {@code AuditEvent} instance.
409         * @return true if {@code AuditEvent} is in the scope of the suppression.
410         */
411        private boolean isInScopeOfSuppression(AuditEvent event) {
412            return lineNo <= event.getLine();
413        }
414
415        /**
416         * Checks whether {@link AuditEvent} source name matches the check format.
417         *
418         * @param event {@code AuditEvent} instance.
419         * @return true if the {@code AuditEvent} source name matches the check format.
420         */
421        private boolean isCheckMatch(AuditEvent event) {
422            final Matcher checkMatcher = eventSourceRegexp.matcher(event.getSourceName());
423            return checkMatcher.find();
424        }
425
426        /**
427         * Checks whether the {@link AuditEvent} module ID matches the ID format.
428         *
429         * @param event {@code AuditEvent} instance.
430         * @return true if the {@code AuditEvent} module ID matches the ID format.
431         */
432        private boolean isIdMatch(AuditEvent event) {
433            boolean match = true;
434            if (eventIdRegexp != null) {
435                if (event.getModuleId() == null) {
436                    match = false;
437                }
438                else {
439                    final Matcher idMatcher = eventIdRegexp.matcher(event.getModuleId());
440                    match = idMatcher.find();
441                }
442            }
443            return match;
444        }
445
446        /**
447         * Checks whether the {@link AuditEvent} message matches the message format.
448         *
449         * @param event {@code AuditEvent} instance.
450         * @return true if the {@code AuditEvent} message matches the message format.
451         */
452        private boolean isMessageMatch(AuditEvent event) {
453            boolean match = true;
454            if (eventMessageRegexp != null) {
455                final Matcher messageMatcher = eventMessageRegexp.matcher(event.getMessage());
456                match = messageMatcher.find();
457            }
458            return match;
459        }
460
461    }
462
463}