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