aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/com/puppycrawl/tools/checkstyle/filters/SuppressWithPlainTextCommentFilter.java
blob: 3345fd4628eb368b6f618a5e05898792337d6811 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code for adherence to a set of rules.
// Copyright (C) 2001-2017 the original author or authors.
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
////////////////////////////////////////////////////////////////////////////////

package com.puppycrawl.tools.checkstyle.filters;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import com.puppycrawl.tools.checkstyle.api.AuditEvent;
import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
import com.puppycrawl.tools.checkstyle.api.FileText;
import com.puppycrawl.tools.checkstyle.api.Filter;
import com.puppycrawl.tools.checkstyle.utils.CommonUtils;

/**
 * <p>
 *     A filter that uses comments to suppress audit events.
 *     The filter can be used only to suppress audit events received from
 *     {@link com.puppycrawl.tools.checkstyle.api.FileSetCheck} checks.
 *     SuppressWithPlainTextCommentFilter knows nothing about AST,
 *     it treats only plain text comments and extracts the information required for suppression from
 *     the plain text comments. Currently the filter supports only single line comments.
 * </p>
 * <p>
 *     Rationale:
 *     Sometimes there are legitimate reasons for violating a check. When
 *     this is a matter of the code in question and not personal
 *     preference, the best place to override the policy is in the code
 *     itself.  Semi-structured comments can be associated with the check.
 *     This is sometimes superior to a separate suppressions file, which
 *     must be kept up-to-date as the source file is edited.
 * </p>
 * @author Andrei Selkin
 */
public class SuppressWithPlainTextCommentFilter extends AutomaticBean implements Filter {

    /** Comment format which turns checkstyle reporting off. */
    private static final String DEFAULT_OFF_FORMAT = "// CHECKSTYLE:OFF";

    /** Comment format which turns checkstyle reporting on. */
    private static final String DEFAULT_ON_FORMAT = "// CHECKSTYLE:ON";

    /** Default check format to suppress. By default the filter suppress all checks. */
    private static final String DEFAULT_CHECK_FORMAT = ".*";

    /** Regexp which turns checkstyle reporting off. */
    private Pattern offCommentFormat = CommonUtils.createPattern(DEFAULT_OFF_FORMAT);

    /** Regexp which turns checkstyle reporting on. */
    private Pattern onCommentFormat = CommonUtils.createPattern(DEFAULT_ON_FORMAT);

    /** The check format to suppress. */
    private String checkFormat = DEFAULT_CHECK_FORMAT;

    /** The message format to suppress.*/
    private String messageFormat;

    /**
     * Sets an off comment format pattern.
     * @param pattern off comment format pattern.
     */
    public final void setOffCommentFormat(Pattern pattern) {
        offCommentFormat = pattern;
    }

    /**
     * Sets an on comment format pattern.
     * @param pattern  on comment format pattern.
     */
    public final void setOnCommentFormat(Pattern pattern) {
        onCommentFormat = pattern;
    }

    /**
     * Sets a pattern for check format.
     * @param format pattern for check format.
     */
    public final void setCheckFormat(String format) {
        checkFormat = format;
    }

    /**
     * Sets a pattern for message format.
     * @param format pattern for message format.
     */
    public final void setMessageFormat(String format) {
        messageFormat = format;
    }

    @Override
    public boolean accept(AuditEvent event) {
        boolean accepted = true;
        if (event.getLocalizedMessage() != null) {
            final FileText fileText = getFileText(event.getFileName());
            final List<Suppression> suppressions = getSuppressions(fileText);
            accepted = getNearestSuppression(suppressions, event) == null;
        }
        return accepted;
    }

    @Override
    protected void finishLocalSetup() throws CheckstyleException {
        // No code by default
    }

    /**
     * Returns {@link FileText} instance created based on the given file name.
     * @param fileName the name of the file.
     * @return {@link FileText} instance.
     */
    private static FileText getFileText(String fileName) {
        try {
            return new FileText(new File(fileName), StandardCharsets.UTF_8.name());
        }
        catch (IOException ex) {
            throw new IllegalStateException("Cannot read source file: " + fileName, ex);
        }
    }

    /**
     * Returns the list of {@link Suppression} instances retrieved from the given {@link FileText}.
     * @param fileText {@link FileText} instance.
     * @return list of {@link Suppression} instances.
     */
    private List<Suppression> getSuppressions(FileText fileText) {
        final List<Suppression> suppressions = new ArrayList<>();
        for (int lineNo = 0; lineNo < fileText.size(); lineNo++) {
            final Optional<Suppression> suppression = getSuppression(fileText, lineNo);
            suppression.ifPresent(suppressions::add);
        }
        return suppressions;
    }

    /**
     * Tries to extract the suppression from the given line.
     * @param fileText {@link FileText} instance.
     * @param lineNo line number.
     * @return {@link Optional} of {@link Suppression}.
     */
    private Optional<Suppression> getSuppression(FileText fileText, int lineNo) {
        final String line = fileText.get(lineNo);
        final Matcher onCommentMatcher = onCommentFormat.matcher(line);
        final Matcher offCommentMatcher = offCommentFormat.matcher(line);

        Suppression suppression = null;
        if (onCommentMatcher.find()) {
            suppression = new Suppression(onCommentMatcher.group(0),
                lineNo + 1, onCommentMatcher.start(), SuppressionType.ON, this);
        }
        if (offCommentMatcher.find()) {
            suppression = new Suppression(offCommentMatcher.group(0),
                lineNo + 1, offCommentMatcher.start(), SuppressionType.OFF, this);
        }

        return Optional.ofNullable(suppression);
    }

    /**
     * Finds the nearest {@link Suppression} instance which can suppress
     * the given {@link AuditEvent}. The nearest suppression is the suppression which scope
     * is before the line and column of the event.
     * @param suppressions {@link Suppression} instance.
     * @param event {@link AuditEvent} instance.
     * @return {@link Suppression} instance.
     */
    private static Suppression getNearestSuppression(List<Suppression> suppressions,
                                                     AuditEvent event) {
        return suppressions
            .stream()
            .filter(suppression -> suppression.isMatch(event))
            .reduce((first, second) -> second)
            .filter(suppression -> suppression.suppressionType != SuppressionType.ON)
            .orElse(null);
    }

    /** Enum which represents the type of the suppression. */
    private enum SuppressionType {

        /** On suppression type. */
        ON,
        /** Off suppression type. */
        OFF

    }

    /** The class which represents the suppression. */
    public static class Suppression {

        /** The regexp which is used to match the event source.*/
        private final Pattern eventSourceRegexp;
        /** The regexp which is used to match the event message.*/
        private final Pattern eventMessageRegexp;

        /** Suppression text.*/
        private final String text;
        /** Suppression line.*/
        private final int lineNo;
        /** Suppression column number.*/
        private final int columnNo;
        /** Suppression type. */
        private final SuppressionType suppressionType;

        /**
         * Creates new suppression instance.
         * @param text suppression text.
         * @param lineNo suppression line number.
         * @param columnNo suppression column number.
         * @param suppressionType suppression type.
         * @param filter the {@link SuppressWithPlainTextCommentFilter} with the context.
         */
        protected Suppression(
            String text,
            int lineNo,
            int columnNo,
            SuppressionType suppressionType,
            SuppressWithPlainTextCommentFilter filter
        ) {
            this.text = text;
            this.lineNo = lineNo;
            this.columnNo = columnNo;
            this.suppressionType = suppressionType;

            //Expand regexp for check and message
            //Does not intern Patterns with Utils.getPattern()
            String format = "";
            try {
                if (this.suppressionType == SuppressionType.ON) {
                    format = CommonUtils.fillTemplateWithStringsByRegexp(
                            filter.checkFormat, text, filter.onCommentFormat);
                    eventSourceRegexp = Pattern.compile(format);
                    if (filter.messageFormat == null) {
                        eventMessageRegexp = null;
                    }
                    else {
                        format = CommonUtils.fillTemplateWithStringsByRegexp(
                                filter.messageFormat, text, filter.onCommentFormat);
                        eventMessageRegexp = Pattern.compile(format);
                    }
                }
                else {
                    format = CommonUtils.fillTemplateWithStringsByRegexp(
                            filter.checkFormat, text, filter.offCommentFormat);
                    eventSourceRegexp = Pattern.compile(format);
                    if (filter.messageFormat == null) {
                        eventMessageRegexp = null;
                    }
                    else {
                        format = CommonUtils.fillTemplateWithStringsByRegexp(
                                filter.messageFormat, text, filter.offCommentFormat);
                        eventMessageRegexp = Pattern.compile(format);
                    }
                }
            }
            catch (final PatternSyntaxException ex) {
                throw new IllegalArgumentException(
                    "unable to parse expanded comment " + format, ex);
            }
        }

        @Override
        public boolean equals(Object other) {
            if (this == other) {
                return true;
            }
            if (other == null || getClass() != other.getClass()) {
                return false;
            }
            final Suppression suppression = (Suppression) other;
            return Objects.equals(lineNo, suppression.lineNo)
                    && Objects.equals(columnNo, suppression.columnNo)
                    && Objects.equals(suppressionType, suppression.suppressionType)
                    && Objects.equals(text, suppression.text)
                    && Objects.equals(eventSourceRegexp, suppression.eventSourceRegexp)
                    && Objects.equals(eventMessageRegexp, suppression.eventMessageRegexp);
        }

        @Override
        public int hashCode() {
            return Objects.hash(
                text, lineNo, columnNo, suppressionType, eventSourceRegexp, eventMessageRegexp);
        }

        /**
         * Checks whether the suppression matches the given {@link AuditEvent}.
         * @param event {@link AuditEvent} instance.
         * @return true if the suppression matches {@link AuditEvent}.
         */
        private boolean isMatch(AuditEvent event) {
            boolean match = false;
            if (isInScopeOfSuppression(event)) {
                final Matcher sourceNameMatcher = eventSourceRegexp.matcher(event.getSourceName());
                if (sourceNameMatcher.find()) {
                    match = eventMessageRegexp == null
                        || eventMessageRegexp.matcher(event.getMessage()).find();
                }
                else {
                    match = event.getModuleId() != null
                        && eventSourceRegexp.matcher(event.getModuleId()).find();
                }
            }
            return match;
        }

        /**
         * Checks whether {@link AuditEvent} is in the scope of the suppression.
         * @param event {@link AuditEvent} instance.
         * @return true if {@link AuditEvent} is in the scope of the suppression.
         */
        private boolean isInScopeOfSuppression(AuditEvent event) {
            return lineNo <= event.getLine();
        }
    }

}