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