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