1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2025 the original author or authors.
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 ///////////////////////////////////////////////////////////////////////////////////////////////
19
20 package com.puppycrawl.tools.checkstyle.filters;
21
22 import java.io.File;
23 import java.io.IOException;
24 import java.nio.charset.StandardCharsets;
25 import java.util.ArrayList;
26 import java.util.Collection;
27 import java.util.Objects;
28 import java.util.Optional;
29 import java.util.regex.Matcher;
30 import java.util.regex.Pattern;
31 import java.util.regex.PatternSyntaxException;
32
33 import com.puppycrawl.tools.checkstyle.AbstractAutomaticBean;
34 import com.puppycrawl.tools.checkstyle.PropertyType;
35 import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
36 import com.puppycrawl.tools.checkstyle.api.AuditEvent;
37 import com.puppycrawl.tools.checkstyle.api.FileText;
38 import com.puppycrawl.tools.checkstyle.api.Filter;
39 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
40
41 /**
42 * <div>
43 * Filter {@code SuppressWithPlainTextCommentFilter} uses plain text to suppress
44 * audit events. The filter can be used only to suppress audit events received
45 * from the checks which implement FileSetCheck interface. In other words, the
46 * checks which have Checker as a parent module. The filter knows nothing about
47 * AST, it treats only plain text comments and extracts the information required
48 * for suppression from the plain text comments. Currently, the filter supports
49 * only single-line comments.
50 * </div>
51 *
52 * <p>
53 * Please, be aware of the fact that, it is not recommended to use the filter
54 * for Java code anymore, however you still are able to use it to suppress audit
55 * events received from the checks which implement FileSetCheck interface.
56 * </p>
57 *
58 * <p>
59 * Rationale: Sometimes there are legitimate reasons for violating a check.
60 * When this is a matter of the code in question and not personal preference,
61 * the best place to override the policy is in the code itself. Semi-structured
62 * comments can be associated with the check. This is sometimes superior to
63 * a separate suppressions file, which must be kept up-to-date as the source
64 * file is edited.
65 * </p>
66 *
67 * <p>
68 * Note that the suppression comment should be put before the violation.
69 * You can use more than one suppression comment each on separate line.
70 * </p>
71 *
72 * <p>
73 * Notes:
74 * Properties {@code offCommentFormat} and {@code onCommentFormat} must have equal
75 * <a href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/regex/Matcher.html#groupCount()">
76 * paren counts</a>.
77 * </p>
78 *
79 * <p>
80 * SuppressionWithPlainTextCommentFilter can suppress Checks that have Treewalker or
81 * Checker as parent module.
82 * </p>
83 *
84 * @since 8.6
85 */
86 public class SuppressWithPlainTextCommentFilter extends AbstractAutomaticBean implements Filter {
87
88 /** Comment format which turns checkstyle reporting off. */
89 private static final String DEFAULT_OFF_FORMAT = "// CHECKSTYLE:OFF";
90
91 /** Comment format which turns checkstyle reporting on. */
92 private static final String DEFAULT_ON_FORMAT = "// CHECKSTYLE:ON";
93
94 /** Default check format to suppress. By default, the filter suppress all checks. */
95 private static final String DEFAULT_CHECK_FORMAT = ".*";
96
97 /** List of suppressions from the file. By default, Its null. */
98 private final Collection<Suppression> currentFileSuppressionCache = new ArrayList<>();
99
100 /** File name that was suppressed. By default, Its empty. */
101 private String currentFileName = "";
102
103 /** Specify comment pattern to trigger filter to begin suppression. */
104 private Pattern offCommentFormat = CommonUtil.createPattern(DEFAULT_OFF_FORMAT);
105
106 /** Specify comment pattern to trigger filter to end suppression. */
107 private Pattern onCommentFormat = CommonUtil.createPattern(DEFAULT_ON_FORMAT);
108
109 /** Specify check pattern to suppress. */
110 @XdocsPropertyType(PropertyType.PATTERN)
111 private String checkFormat = DEFAULT_CHECK_FORMAT;
112
113 /** Specify message pattern to suppress. */
114 @XdocsPropertyType(PropertyType.PATTERN)
115 private String messageFormat;
116
117 /** Specify check ID pattern to suppress. */
118 @XdocsPropertyType(PropertyType.PATTERN)
119 private String idFormat;
120
121 /**
122 * Setter to specify comment pattern to trigger filter to begin suppression.
123 *
124 * @param pattern off comment format pattern.
125 * @since 8.6
126 */
127 public final void setOffCommentFormat(Pattern pattern) {
128 offCommentFormat = pattern;
129 }
130
131 /**
132 * Setter to specify comment pattern to trigger filter to end suppression.
133 *
134 * @param pattern on comment format pattern.
135 * @since 8.6
136 */
137 public final void setOnCommentFormat(Pattern pattern) {
138 onCommentFormat = pattern;
139 }
140
141 /**
142 * Setter to specify check pattern to suppress.
143 *
144 * @param format pattern for check format.
145 * @since 8.6
146 */
147 public final void setCheckFormat(String format) {
148 checkFormat = format;
149 }
150
151 /**
152 * Setter to specify message pattern to suppress.
153 *
154 * @param format pattern for message format.
155 * @since 8.6
156 */
157 public final void setMessageFormat(String format) {
158 messageFormat = format;
159 }
160
161 /**
162 * Setter to specify check ID pattern to suppress.
163 *
164 * @param format pattern for check ID format
165 * @since 8.24
166 */
167 public final void setIdFormat(String format) {
168 idFormat = format;
169 }
170
171 @Override
172 public boolean accept(AuditEvent event) {
173 boolean accepted = true;
174 if (event.getViolation() != null) {
175 final String eventFileName = event.getFileName();
176
177 if (!currentFileName.equals(eventFileName)) {
178 currentFileName = eventFileName;
179 final FileText fileText = getFileText(eventFileName);
180 currentFileSuppressionCache.clear();
181 if (fileText != null) {
182 cacheSuppressions(fileText);
183 }
184 }
185
186 accepted = getNearestSuppression(currentFileSuppressionCache, event) == null;
187 }
188 return accepted;
189 }
190
191 @Override
192 protected void finishLocalSetup() {
193 // No code by default
194 }
195
196 /**
197 * Caches {@link FileText} instance created based on the given file name.
198 *
199 * @param fileName the name of the file.
200 * @return {@link FileText} instance.
201 * @throws IllegalStateException if the file could not be read.
202 */
203 private static FileText getFileText(String fileName) {
204 final File file = new File(fileName);
205 FileText result = null;
206
207 // some violations can be on a directory, instead of a file
208 if (!file.isDirectory()) {
209 try {
210 result = new FileText(file, StandardCharsets.UTF_8.name());
211 }
212 catch (IOException exc) {
213 throw new IllegalStateException("Cannot read source file: " + fileName, exc);
214 }
215 }
216
217 return result;
218 }
219
220 /**
221 * Collects the list of {@link Suppression} instances retrieved from the given {@link FileText}.
222 *
223 * @param fileText {@link FileText} instance.
224 */
225 private void cacheSuppressions(FileText fileText) {
226 for (int lineNo = 0; lineNo < fileText.size(); lineNo++) {
227 final Optional<Suppression> suppression = getSuppression(fileText, lineNo);
228 suppression.ifPresent(currentFileSuppressionCache::add);
229 }
230 }
231
232 /**
233 * Tries to extract the suppression from the given line.
234 *
235 * @param fileText {@link FileText} instance.
236 * @param lineNo line number.
237 * @return {@link Optional} of {@link Suppression}.
238 */
239 private Optional<Suppression> getSuppression(FileText fileText, int lineNo) {
240 final String line = fileText.get(lineNo);
241 final Matcher onCommentMatcher = onCommentFormat.matcher(line);
242 final Matcher offCommentMatcher = offCommentFormat.matcher(line);
243
244 Suppression suppression = null;
245 if (onCommentMatcher.find()) {
246 suppression = new Suppression(onCommentMatcher.group(0),
247 lineNo + 1, SuppressionType.ON, this);
248 }
249 if (offCommentMatcher.find()) {
250 suppression = new Suppression(offCommentMatcher.group(0),
251 lineNo + 1, SuppressionType.OFF, this);
252 }
253
254 return Optional.ofNullable(suppression);
255 }
256
257 /**
258 * Finds the nearest {@link Suppression} instance which can suppress
259 * the given {@link AuditEvent}. The nearest suppression is the suppression which scope
260 * is before the line and column of the event.
261 *
262 * @param suppressions collection of {@link Suppression} instances.
263 * @param event {@link AuditEvent} instance.
264 * @return {@link Suppression} instance.
265 */
266 private static Suppression getNearestSuppression(Collection<Suppression> suppressions,
267 AuditEvent event) {
268 return suppressions
269 .stream()
270 .filter(suppression -> suppression.isMatch(event))
271 .reduce((first, second) -> second)
272 .filter(suppression -> suppression.suppressionType != SuppressionType.ON)
273 .orElse(null);
274 }
275
276 /** Enum which represents the type of the suppression. */
277 private enum SuppressionType {
278
279 /** On suppression type. */
280 ON,
281 /** Off suppression type. */
282 OFF,
283
284 }
285
286 /** The class which represents the suppression. */
287 private static final class Suppression {
288
289 /** The regexp which is used to match the event source.*/
290 private final Pattern eventSourceRegexp;
291 /** The regexp which is used to match the event message.*/
292 private final Pattern eventMessageRegexp;
293 /** The regexp which is used to match the event ID.*/
294 private final Pattern eventIdRegexp;
295
296 /** Suppression line.*/
297 private final int lineNo;
298
299 /** Suppression type. */
300 private final SuppressionType suppressionType;
301
302 /**
303 * Creates new suppression instance.
304 *
305 * @param text suppression text.
306 * @param lineNo suppression line number.
307 * @param suppressionType suppression type.
308 * @param filter the {@link SuppressWithPlainTextCommentFilter} with the context.
309 * @throws IllegalArgumentException if there is an error in the filter regex syntax.
310 */
311 private Suppression(
312 String text,
313 int lineNo,
314 SuppressionType suppressionType,
315 SuppressWithPlainTextCommentFilter filter
316 ) {
317 this.lineNo = lineNo;
318 this.suppressionType = suppressionType;
319
320 final Pattern commentFormat;
321 if (this.suppressionType == SuppressionType.ON) {
322 commentFormat = filter.onCommentFormat;
323 }
324 else {
325 commentFormat = filter.offCommentFormat;
326 }
327
328 // Expand regexp for check and message
329 // Does not intern Patterns with Utils.getPattern()
330 String format = "";
331 try {
332 format = CommonUtil.fillTemplateWithStringsByRegexp(
333 filter.checkFormat, text, commentFormat);
334 eventSourceRegexp = Pattern.compile(format);
335 if (filter.messageFormat == null) {
336 eventMessageRegexp = null;
337 }
338 else {
339 format = CommonUtil.fillTemplateWithStringsByRegexp(
340 filter.messageFormat, text, commentFormat);
341 eventMessageRegexp = Pattern.compile(format);
342 }
343 if (filter.idFormat == null) {
344 eventIdRegexp = null;
345 }
346 else {
347 format = CommonUtil.fillTemplateWithStringsByRegexp(
348 filter.idFormat, text, commentFormat);
349 eventIdRegexp = Pattern.compile(format);
350 }
351 }
352 catch (final PatternSyntaxException exc) {
353 throw new IllegalArgumentException(
354 "unable to parse expanded comment " + format, exc);
355 }
356 }
357
358 /**
359 * Indicates whether some other object is "equal to" this one.
360 *
361 * @noinspection EqualsCalledOnEnumConstant
362 * @noinspectionreason EqualsCalledOnEnumConstant - enumeration is needed to keep
363 * code consistent
364 */
365 @Override
366 public boolean equals(Object other) {
367 if (this == other) {
368 return true;
369 }
370 if (other == null || getClass() != other.getClass()) {
371 return false;
372 }
373 final Suppression suppression = (Suppression) other;
374 return Objects.equals(lineNo, suppression.lineNo)
375 && Objects.equals(suppressionType, suppression.suppressionType)
376 && Objects.equals(eventSourceRegexp, suppression.eventSourceRegexp)
377 && Objects.equals(eventMessageRegexp, suppression.eventMessageRegexp)
378 && Objects.equals(eventIdRegexp, suppression.eventIdRegexp);
379 }
380
381 @Override
382 public int hashCode() {
383 return Objects.hash(
384 lineNo, suppressionType, eventSourceRegexp, eventMessageRegexp,
385 eventIdRegexp);
386 }
387
388 /**
389 * Checks whether the suppression matches the given {@link AuditEvent}.
390 *
391 * @param event {@link AuditEvent} instance.
392 * @return true if the suppression matches {@link AuditEvent}.
393 */
394 private boolean isMatch(AuditEvent event) {
395 return isInScopeOfSuppression(event)
396 && isCheckMatch(event)
397 && isIdMatch(event)
398 && isMessageMatch(event);
399 }
400
401 /**
402 * Checks whether {@link AuditEvent} is in the scope of the suppression.
403 *
404 * @param event {@link AuditEvent} instance.
405 * @return true if {@link AuditEvent} is in the scope of the suppression.
406 */
407 private boolean isInScopeOfSuppression(AuditEvent event) {
408 return lineNo <= event.getLine();
409 }
410
411 /**
412 * Checks whether {@link AuditEvent} source name matches the check format.
413 *
414 * @param event {@link AuditEvent} instance.
415 * @return true if the {@link AuditEvent} source name matches the check format.
416 */
417 private boolean isCheckMatch(AuditEvent event) {
418 final Matcher checkMatcher = eventSourceRegexp.matcher(event.getSourceName());
419 return checkMatcher.find();
420 }
421
422 /**
423 * Checks whether the {@link AuditEvent} module ID matches the ID format.
424 *
425 * @param event {@link AuditEvent} instance.
426 * @return true if the {@link AuditEvent} module ID matches the ID format.
427 */
428 private boolean isIdMatch(AuditEvent event) {
429 boolean match = true;
430 if (eventIdRegexp != null) {
431 if (event.getModuleId() == null) {
432 match = false;
433 }
434 else {
435 final Matcher idMatcher = eventIdRegexp.matcher(event.getModuleId());
436 match = idMatcher.find();
437 }
438 }
439 return match;
440 }
441
442 /**
443 * Checks whether the {@link AuditEvent} message matches the message format.
444 *
445 * @param event {@link AuditEvent} instance.
446 * @return true if the {@link AuditEvent} message matches the message format.
447 */
448 private boolean isMessageMatch(AuditEvent event) {
449 boolean match = true;
450 if (eventMessageRegexp != null) {
451 final Matcher messageMatcher = eventMessageRegexp.matcher(event.getMessage());
452 match = messageMatcher.find();
453 }
454 return match;
455 }
456 }
457
458 }