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