001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2019 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.List;
027import java.util.Objects;
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.api.AuditEvent;
034import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
035import com.puppycrawl.tools.checkstyle.api.FileText;
036import com.puppycrawl.tools.checkstyle.api.Filter;
037import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
038
039/**
040 * <p>
041 *     A filter that uses comments to suppress audit events.
042 *     The filter can be used only to suppress audit events received from
043 *     {@link com.puppycrawl.tools.checkstyle.api.FileSetCheck} checks.
044 *     SuppressWithPlainTextCommentFilter knows nothing about AST,
045 *     it treats only plain text comments and extracts the information required for suppression from
046 *     the plain text comments. Currently the filter supports only single line comments.
047 * </p>
048 * <p>
049 *     Rationale:
050 *     Sometimes there are legitimate reasons for violating a check. When
051 *     this is a matter of the code in question and not personal
052 *     preference, the best place to override the policy is in the code
053 *     itself.  Semi-structured comments can be associated with the check.
054 *     This is sometimes superior to a separate suppressions file, which
055 *     must be kept up-to-date as the source file is edited.
056 * </p>
057 */
058public class SuppressWithPlainTextCommentFilter extends AutomaticBean implements Filter {
059
060    /** Comment format which turns checkstyle reporting off. */
061    private static final String DEFAULT_OFF_FORMAT = "// CHECKSTYLE:OFF";
062
063    /** Comment format which turns checkstyle reporting on. */
064    private static final String DEFAULT_ON_FORMAT = "// CHECKSTYLE:ON";
065
066    /** Default check format to suppress. By default the filter suppress all checks. */
067    private static final String DEFAULT_CHECK_FORMAT = ".*";
068
069    /** Regexp which turns checkstyle reporting off. */
070    private Pattern offCommentFormat = CommonUtil.createPattern(DEFAULT_OFF_FORMAT);
071
072    /** Regexp which turns checkstyle reporting on. */
073    private Pattern onCommentFormat = CommonUtil.createPattern(DEFAULT_ON_FORMAT);
074
075    /** The check format to suppress. */
076    private String checkFormat = DEFAULT_CHECK_FORMAT;
077
078    /** The message format to suppress.*/
079    private String messageFormat;
080
081    /**
082     * Sets an off comment format pattern.
083     * @param pattern off comment format pattern.
084     */
085    public final void setOffCommentFormat(Pattern pattern) {
086        offCommentFormat = pattern;
087    }
088
089    /**
090     * Sets an on comment format pattern.
091     * @param pattern  on comment format pattern.
092     */
093    public final void setOnCommentFormat(Pattern pattern) {
094        onCommentFormat = pattern;
095    }
096
097    /**
098     * Sets a pattern for check format.
099     * @param format pattern for check format.
100     */
101    public final void setCheckFormat(String format) {
102        checkFormat = format;
103    }
104
105    /**
106     * Sets a pattern for message format.
107     * @param format pattern for message format.
108     */
109    public final void setMessageFormat(String format) {
110        messageFormat = format;
111    }
112
113    @Override
114    public boolean accept(AuditEvent event) {
115        boolean accepted = true;
116        if (event.getLocalizedMessage() != null) {
117            final FileText fileText = getFileText(event.getFileName());
118            if (fileText != null) {
119                final List<Suppression> suppressions = getSuppressions(fileText);
120                accepted = getNearestSuppression(suppressions, event) == null;
121            }
122        }
123        return accepted;
124    }
125
126    @Override
127    protected void finishLocalSetup() {
128        // No code by default
129    }
130
131    /**
132     * Returns {@link FileText} instance created based on the given file name.
133     * @param fileName the name of the file.
134     * @return {@link FileText} instance.
135     */
136    private static FileText getFileText(String fileName) {
137        final File file = new File(fileName);
138        FileText result = null;
139
140        // some violations can be on a directory, instead of a file
141        if (!file.isDirectory()) {
142            try {
143                result = new FileText(file, StandardCharsets.UTF_8.name());
144            }
145            catch (IOException ex) {
146                throw new IllegalStateException("Cannot read source file: " + fileName, ex);
147            }
148        }
149
150        return result;
151    }
152
153    /**
154     * Returns the list of {@link Suppression} instances retrieved from the given {@link FileText}.
155     * @param fileText {@link FileText} instance.
156     * @return list of {@link Suppression} instances.
157     */
158    private List<Suppression> getSuppressions(FileText fileText) {
159        final List<Suppression> suppressions = new ArrayList<>();
160        for (int lineNo = 0; lineNo < fileText.size(); lineNo++) {
161            final Optional<Suppression> suppression = getSuppression(fileText, lineNo);
162            suppression.ifPresent(suppressions::add);
163        }
164        return suppressions;
165    }
166
167    /**
168     * Tries to extract the suppression from the given line.
169     * @param fileText {@link FileText} instance.
170     * @param lineNo line number.
171     * @return {@link Optional} of {@link Suppression}.
172     */
173    private Optional<Suppression> getSuppression(FileText fileText, int lineNo) {
174        final String line = fileText.get(lineNo);
175        final Matcher onCommentMatcher = onCommentFormat.matcher(line);
176        final Matcher offCommentMatcher = offCommentFormat.matcher(line);
177
178        Suppression suppression = null;
179        if (onCommentMatcher.find()) {
180            suppression = new Suppression(onCommentMatcher.group(0),
181                lineNo + 1, onCommentMatcher.start(), SuppressionType.ON, this);
182        }
183        if (offCommentMatcher.find()) {
184            suppression = new Suppression(offCommentMatcher.group(0),
185                lineNo + 1, offCommentMatcher.start(), SuppressionType.OFF, this);
186        }
187
188        return Optional.ofNullable(suppression);
189    }
190
191    /**
192     * Finds the nearest {@link Suppression} instance which can suppress
193     * the given {@link AuditEvent}. The nearest suppression is the suppression which scope
194     * is before the line and column of the event.
195     * @param suppressions {@link Suppression} instance.
196     * @param event {@link AuditEvent} instance.
197     * @return {@link Suppression} instance.
198     */
199    private static Suppression getNearestSuppression(List<Suppression> suppressions,
200                                                     AuditEvent event) {
201        return suppressions
202            .stream()
203            .filter(suppression -> suppression.isMatch(event))
204            .reduce((first, second) -> second)
205            .filter(suppression -> suppression.suppressionType != SuppressionType.ON)
206            .orElse(null);
207    }
208
209    /** Enum which represents the type of the suppression. */
210    private enum SuppressionType {
211
212        /** On suppression type. */
213        ON,
214        /** Off suppression type. */
215        OFF,
216
217    }
218
219    /** The class which represents the suppression. */
220    public static class Suppression {
221
222        /** The regexp which is used to match the event source.*/
223        private final Pattern eventSourceRegexp;
224        /** The regexp which is used to match the event message.*/
225        private final Pattern eventMessageRegexp;
226
227        /** Suppression text.*/
228        private final String text;
229        /** Suppression line.*/
230        private final int lineNo;
231        /** Suppression column number.*/
232        private final int columnNo;
233        /** Suppression type. */
234        private final SuppressionType suppressionType;
235
236        /**
237         * Creates new suppression instance.
238         * @param text suppression text.
239         * @param lineNo suppression line number.
240         * @param columnNo suppression column number.
241         * @param suppressionType suppression type.
242         * @param filter the {@link SuppressWithPlainTextCommentFilter} with the context.
243         */
244        protected Suppression(
245            String text,
246            int lineNo,
247            int columnNo,
248            SuppressionType suppressionType,
249            SuppressWithPlainTextCommentFilter filter
250        ) {
251            this.text = text;
252            this.lineNo = lineNo;
253            this.columnNo = columnNo;
254            this.suppressionType = suppressionType;
255
256            //Expand regexp for check and message
257            //Does not intern Patterns with Utils.getPattern()
258            String format = "";
259            try {
260                if (this.suppressionType == SuppressionType.ON) {
261                    format = CommonUtil.fillTemplateWithStringsByRegexp(
262                            filter.checkFormat, text, filter.onCommentFormat);
263                    eventSourceRegexp = Pattern.compile(format);
264                    if (filter.messageFormat == null) {
265                        eventMessageRegexp = null;
266                    }
267                    else {
268                        format = CommonUtil.fillTemplateWithStringsByRegexp(
269                                filter.messageFormat, text, filter.onCommentFormat);
270                        eventMessageRegexp = Pattern.compile(format);
271                    }
272                }
273                else {
274                    format = CommonUtil.fillTemplateWithStringsByRegexp(
275                            filter.checkFormat, text, filter.offCommentFormat);
276                    eventSourceRegexp = Pattern.compile(format);
277                    if (filter.messageFormat == null) {
278                        eventMessageRegexp = null;
279                    }
280                    else {
281                        format = CommonUtil.fillTemplateWithStringsByRegexp(
282                                filter.messageFormat, text, filter.offCommentFormat);
283                        eventMessageRegexp = Pattern.compile(format);
284                    }
285                }
286            }
287            catch (final PatternSyntaxException ex) {
288                throw new IllegalArgumentException(
289                    "unable to parse expanded comment " + format, ex);
290            }
291        }
292
293        /**
294         * Indicates whether some other object is "equal to" this one.
295         * Suppression on enumeration is needed so code stays consistent.
296         * @noinspection EqualsCalledOnEnumConstant
297         */
298        @Override
299        public boolean equals(Object other) {
300            if (this == other) {
301                return true;
302            }
303            if (other == null || getClass() != other.getClass()) {
304                return false;
305            }
306            final Suppression suppression = (Suppression) other;
307            return Objects.equals(lineNo, suppression.lineNo)
308                    && Objects.equals(columnNo, suppression.columnNo)
309                    && Objects.equals(suppressionType, suppression.suppressionType)
310                    && Objects.equals(text, suppression.text)
311                    && Objects.equals(eventSourceRegexp, suppression.eventSourceRegexp)
312                    && Objects.equals(eventMessageRegexp, suppression.eventMessageRegexp);
313        }
314
315        @Override
316        public int hashCode() {
317            return Objects.hash(
318                text, lineNo, columnNo, suppressionType, eventSourceRegexp, eventMessageRegexp);
319        }
320
321        /**
322         * Checks whether the suppression matches the given {@link AuditEvent}.
323         * @param event {@link AuditEvent} instance.
324         * @return true if the suppression matches {@link AuditEvent}.
325         */
326        private boolean isMatch(AuditEvent event) {
327            boolean match = false;
328            if (isInScopeOfSuppression(event)) {
329                final Matcher sourceNameMatcher = eventSourceRegexp.matcher(event.getSourceName());
330                if (sourceNameMatcher.find()) {
331                    match = eventMessageRegexp == null
332                        || eventMessageRegexp.matcher(event.getMessage()).find();
333                }
334                else {
335                    match = event.getModuleId() != null
336                        && eventSourceRegexp.matcher(event.getModuleId()).find();
337                }
338            }
339            return match;
340        }
341
342        /**
343         * Checks whether {@link AuditEvent} is in the scope of the suppression.
344         * @param event {@link AuditEvent} instance.
345         * @return true if {@link AuditEvent} is in the scope of the suppression.
346         */
347        private boolean isInScopeOfSuppression(AuditEvent event) {
348            return lineNo <= event.getLine();
349        }
350
351    }
352
353}