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.checks;
21
22 import java.util.Collections;
23 import java.util.HashMap;
24 import java.util.LinkedList;
25 import java.util.List;
26 import java.util.Locale;
27 import java.util.Map;
28 import java.util.Optional;
29 import java.util.regex.Pattern;
30
31 import javax.annotation.Nullable;
32
33 import com.puppycrawl.tools.checkstyle.StatelessCheck;
34 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
35 import com.puppycrawl.tools.checkstyle.api.AuditEvent;
36 import com.puppycrawl.tools.checkstyle.api.DetailAST;
37 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
38 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
39
40 /**
41 * <div>
42 * Maintains a set of check suppressions from {@code @SuppressWarnings} annotations.
43 * It allows to prevent Checkstyle from reporting violations from parts of code that were
44 * annotated with {@code @SuppressWarnings} and using name of the check to be excluded.
45 * It is possible to suppress all the checkstyle warnings with the argument {@code "all"}.
46 * You can also use a {@code checkstyle:} prefix to prevent compiler
47 * from processing these annotations.
48 * You can also define aliases for check names that need to be suppressed.
49 * </div>
50 *
51 * @since 5.7
52 */
53 @StatelessCheck
54 public class SuppressWarningsHolder
55 extends AbstractCheck {
56
57 /**
58 * Optional prefix for warning suppressions that are only intended to be
59 * recognized by checkstyle. For instance, to suppress {@code
60 * FallThroughCheck} only in checkstyle (and not in javac), use the
61 * suppression {@code "checkstyle:fallthrough"} or {@code "checkstyle:FallThrough"}.
62 * To suppress the warning in both tools, just use {@code "fallthrough"}.
63 */
64 private static final String CHECKSTYLE_PREFIX = "checkstyle:";
65
66 /** Java.lang namespace prefix, which is stripped from SuppressWarnings. */
67 private static final String JAVA_LANG_PREFIX = "java.lang.";
68
69 /** Suffix to be removed from subclasses of Check. */
70 private static final String CHECK_SUFFIX = "check";
71
72 /** Special warning id for matching all the warnings. */
73 private static final String ALL_WARNING_MATCHING_ID = "all";
74
75 /** A map from check source names to suppression aliases. */
76 private static final Map<String, String> CHECK_ALIAS_MAP = new HashMap<>();
77
78 /**
79 * A thread-local holder for the list of suppression entries for the last
80 * file parsed.
81 */
82 private static final ThreadLocal<List<Entry>> ENTRIES =
83 ThreadLocal.withInitial(LinkedList::new);
84
85 /**
86 * Compiled pattern used to match whitespace in text block content.
87 */
88 private static final Pattern WHITESPACE = Pattern.compile("\\s+");
89
90 /**
91 * Compiled pattern used to match preceding newline in text block content.
92 */
93 private static final Pattern NEWLINE = Pattern.compile("\\n");
94
95 /**
96 * Returns the default alias for the source name of a check, which is the
97 * source name in lower case with any dotted prefix or "Check"/"check"
98 * suffix removed.
99 *
100 * @param sourceName the source name of the check (generally the class
101 * name)
102 * @return the default alias for the given check
103 */
104 public static String getDefaultAlias(String sourceName) {
105 int endIndex = sourceName.length();
106 final String sourceNameLower = sourceName.toLowerCase(Locale.ENGLISH);
107 if (sourceNameLower.endsWith(CHECK_SUFFIX)) {
108 endIndex -= CHECK_SUFFIX.length();
109 }
110 final int startIndex = sourceNameLower.lastIndexOf('.') + 1;
111 return sourceNameLower.substring(startIndex, endIndex);
112 }
113
114 /**
115 * Returns the alias of simple check name for a check, The alias is
116 * for the form of CheckNameCheck or CheckName.
117 *
118 * @param sourceName the source name of the check (generally the class
119 * name)
120 * @return the alias of the simple check name for the given check
121 */
122 @Nullable
123 private static String getSimpleNameAlias(String sourceName) {
124 final String checkName = CommonUtil.baseClassName(sourceName);
125 final String checkNameSuffix = "Check";
126 // check alias for the CheckNameCheck
127 String checkAlias = CHECK_ALIAS_MAP.get(checkName);
128 if (checkAlias == null && checkName.endsWith(checkNameSuffix)) {
129 final int checkStartIndex = checkName.length() - checkNameSuffix.length();
130 final String checkNameWithoutSuffix = checkName.substring(0, checkStartIndex);
131 // check alias for the CheckName
132 checkAlias = CHECK_ALIAS_MAP.get(checkNameWithoutSuffix);
133 }
134
135 return checkAlias;
136 }
137
138 /**
139 * Returns the alias for the source name of a check. If an alias has been
140 * explicitly registered via {@link #setAliasList(String...)}, that
141 * alias is returned; otherwise, the default alias is used.
142 *
143 * @param sourceName the source name of the check (generally the class
144 * name)
145 * @return the current alias for the given check
146 */
147 public static String getAlias(String sourceName) {
148 String checkAlias = CHECK_ALIAS_MAP.get(sourceName);
149 if (checkAlias == null) {
150 checkAlias = getSimpleNameAlias(sourceName);
151 }
152 if (checkAlias == null) {
153 checkAlias = getDefaultAlias(sourceName);
154 }
155 return checkAlias;
156 }
157
158 /**
159 * Registers an alias for the source name of a check.
160 *
161 * @param sourceName the source name of the check (generally the class
162 * name)
163 * @param checkAlias the alias used in {@link SuppressWarnings} annotations
164 */
165 private static void registerAlias(String sourceName, String checkAlias) {
166 CHECK_ALIAS_MAP.put(sourceName, checkAlias);
167 }
168
169 /**
170 * Setter to specify aliases for check names that can be used in code
171 * within {@code SuppressWarnings} in a format of comma separated attribute=value entries.
172 * The attribute is the fully qualified name of the Check and value is its alias.
173 *
174 * @param aliasList comma-separated alias assignments
175 * @throws IllegalArgumentException when alias item does not have '='
176 * @since 5.7
177 */
178 public void setAliasList(String... aliasList) {
179 for (String sourceAlias : aliasList) {
180 final int index = sourceAlias.indexOf('=');
181 if (index > 0) {
182 registerAlias(sourceAlias.substring(0, index), sourceAlias
183 .substring(index + 1));
184 }
185 else if (!sourceAlias.isEmpty()) {
186 throw new IllegalArgumentException(
187 "'=' expected in alias list item: " + sourceAlias);
188 }
189 }
190 }
191
192 /**
193 * Checks for a suppression of a check with the given source name and
194 * location in the last file processed.
195 *
196 * @param event audit event.
197 * @return whether the check with the given name is suppressed at the given
198 * source location
199 */
200 public static boolean isSuppressed(AuditEvent event) {
201 final List<Entry> entries = ENTRIES.get();
202 final String sourceName = event.getSourceName();
203 final String checkAlias = getAlias(sourceName);
204 final int line = event.getLine();
205 final int column = event.getColumn();
206 boolean suppressed = false;
207 for (Entry entry : entries) {
208 final boolean afterStart = isSuppressedAfterEventStart(line, column, entry);
209 final boolean beforeEnd = isSuppressedBeforeEventEnd(line, column, entry);
210 final String checkName = entry.getCheckName();
211 final boolean nameMatches =
212 ALL_WARNING_MATCHING_ID.equals(checkName)
213 || checkName.equalsIgnoreCase(checkAlias)
214 || getDefaultAlias(checkName).equalsIgnoreCase(checkAlias)
215 || getDefaultAlias(sourceName).equalsIgnoreCase(checkName);
216 if (afterStart && beforeEnd
217 && (nameMatches || checkName.equals(event.getModuleId()))) {
218 suppressed = true;
219 break;
220 }
221 }
222 return suppressed;
223 }
224
225 /**
226 * Checks whether suppression entry position is after the audit event occurrence position
227 * in the source file.
228 *
229 * @param line the line number in the source file where the event occurred.
230 * @param column the column number in the source file where the event occurred.
231 * @param entry suppression entry.
232 * @return true if suppression entry position is after the audit event occurrence position
233 * in the source file.
234 */
235 private static boolean isSuppressedAfterEventStart(int line, int column, Entry entry) {
236 return entry.getFirstLine() < line
237 || entry.getFirstLine() == line
238 && (column == 0 || entry.getFirstColumn() <= column);
239 }
240
241 /**
242 * Checks whether suppression entry position is before the audit event occurrence position
243 * in the source file.
244 *
245 * @param line the line number in the source file where the event occurred.
246 * @param column the column number in the source file where the event occurred.
247 * @param entry suppression entry.
248 * @return true if suppression entry position is before the audit event occurrence position
249 * in the source file.
250 */
251 private static boolean isSuppressedBeforeEventEnd(int line, int column, Entry entry) {
252 return entry.getLastLine() > line
253 || entry.getLastLine() == line && entry
254 .getLastColumn() >= column;
255 }
256
257 @Override
258 public int[] getDefaultTokens() {
259 return getRequiredTokens();
260 }
261
262 @Override
263 public int[] getAcceptableTokens() {
264 return getRequiredTokens();
265 }
266
267 @Override
268 public int[] getRequiredTokens() {
269 return new int[] {TokenTypes.ANNOTATION};
270 }
271
272 @Override
273 public void beginTree(DetailAST rootAST) {
274 ENTRIES.get().clear();
275 }
276
277 @Override
278 public void visitToken(DetailAST ast) {
279 // check whether annotation is SuppressWarnings
280 // expected children: AT ( IDENT | DOT ) LPAREN <values> RPAREN
281 String identifier = getIdentifier(getNthChild(ast, 1));
282 if (identifier.startsWith(JAVA_LANG_PREFIX)) {
283 identifier = identifier.substring(JAVA_LANG_PREFIX.length());
284 }
285 if ("SuppressWarnings".equals(identifier)) {
286 getAnnotationTarget(ast).ifPresent(targetAST -> {
287 addSuppressions(getAllAnnotationValues(ast), targetAST);
288 });
289 }
290 }
291
292 /**
293 * Method to populate list of suppression entries.
294 *
295 * @param values
296 * - list of check names
297 * @param targetAST
298 * - annotation target
299 */
300 private static void addSuppressions(List<String> values, DetailAST targetAST) {
301 // get text range of target
302 final int firstLine = targetAST.getLineNo();
303 final int firstColumn = targetAST.getColumnNo();
304 final DetailAST nextAST = targetAST.getNextSibling();
305 final int lastLine;
306 final int lastColumn;
307 if (nextAST == null) {
308 lastLine = Integer.MAX_VALUE;
309 lastColumn = Integer.MAX_VALUE;
310 }
311 else {
312 lastLine = nextAST.getLineNo();
313 lastColumn = nextAST.getColumnNo();
314 }
315
316 final List<Entry> entries = ENTRIES.get();
317 for (String value : values) {
318 // strip off the checkstyle-only prefix if present
319 final String checkName = removeCheckstylePrefixIfExists(value);
320 entries.add(new Entry(checkName, firstLine, firstColumn,
321 lastLine, lastColumn));
322 }
323 }
324
325 /**
326 * Method removes checkstyle prefix (checkstyle:) from check name if exists.
327 *
328 * @param checkName
329 * - name of the check
330 * @return check name without prefix
331 */
332 private static String removeCheckstylePrefixIfExists(String checkName) {
333 String result = checkName;
334 if (checkName.startsWith(CHECKSTYLE_PREFIX)) {
335 result = checkName.substring(CHECKSTYLE_PREFIX.length());
336 }
337 return result;
338 }
339
340 /**
341 * Get all annotation values.
342 *
343 * @param ast annotation token
344 * @return list values
345 * @throws IllegalArgumentException if there is an unknown annotation value type.
346 */
347 private static List<String> getAllAnnotationValues(DetailAST ast) {
348 // get values of annotation
349 List<String> values = Collections.emptyList();
350 final DetailAST lparenAST = ast.findFirstToken(TokenTypes.LPAREN);
351 if (lparenAST != null) {
352 final DetailAST nextAST = lparenAST.getNextSibling();
353 final int nextType = nextAST.getType();
354 switch (nextType) {
355 case TokenTypes.EXPR:
356 case TokenTypes.ANNOTATION_ARRAY_INIT:
357 values = getAnnotationValues(nextAST);
358 break;
359
360 case TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR:
361 // expected children: IDENT ASSIGN ( EXPR |
362 // ANNOTATION_ARRAY_INIT )
363 values = getAnnotationValues(getNthChild(nextAST, 2));
364 break;
365
366 case TokenTypes.RPAREN:
367 // no value present (not valid Java)
368 break;
369
370 default:
371 // unknown annotation value type (new syntax?)
372 throw new IllegalArgumentException("Unexpected AST: " + nextAST);
373 }
374 }
375 return values;
376 }
377
378 /**
379 * Get target of annotation.
380 *
381 * @param ast the AST node to get the child of
382 * @return get target of annotation
383 * @throws IllegalArgumentException if there is an unexpected container type.
384 */
385 private static Optional<DetailAST> getAnnotationTarget(DetailAST ast) {
386 DetailAST current = ast.getParent();
387 while (current.getType() == TokenTypes.ANNOTATION_ARRAY_INIT) {
388 current = current.getParent();
389 }
390 return switch (current.getType()) {
391 case TokenTypes.MODIFIERS, TokenTypes.ANNOTATIONS, TokenTypes.ANNOTATION,
392 TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR -> Optional.of(current.getParent());
393 case TokenTypes.LITERAL_DEFAULT -> Optional.empty();
394 default -> throw new IllegalArgumentException("Unexpected container AST: " + current);
395 };
396 }
397
398 /**
399 * Returns the n'th child of an AST node.
400 *
401 * @param ast the AST node to get the child of
402 * @param index the index of the child to get
403 * @return the n'th child of the given AST node, or {@code null} if none
404 */
405 private static DetailAST getNthChild(DetailAST ast, int index) {
406 DetailAST child = ast.getFirstChild();
407 for (int i = 0; i < index && child != null; ++i) {
408 child = child.getNextSibling();
409 }
410 return child;
411 }
412
413 /**
414 * Returns the Java identifier represented by an AST.
415 *
416 * @param ast an AST node for an IDENT or DOT
417 * @return the Java identifier represented by the given AST subtree
418 * @throws IllegalArgumentException if the AST is invalid
419 */
420 private static String getIdentifier(DetailAST ast) {
421 if (ast == null) {
422 throw new IllegalArgumentException("Identifier AST expected, but get null.");
423 }
424 final String identifier;
425 if (ast.getType() == TokenTypes.IDENT) {
426 identifier = ast.getText();
427 }
428 else {
429 identifier = getIdentifier(ast.getFirstChild()) + "."
430 + getIdentifier(ast.getLastChild());
431 }
432 return identifier;
433 }
434
435 /**
436 * Returns the literal string expression represented by an AST.
437 *
438 * @param ast an AST node for an EXPR
439 * @return the Java string represented by the given AST expression
440 * or empty string if expression is too complex
441 * @throws IllegalArgumentException if the AST is invalid
442 */
443 private static String getStringExpr(DetailAST ast) {
444 final DetailAST firstChild = ast.getFirstChild();
445
446 return switch (firstChild.getType()) {
447 case TokenTypes.STRING_LITERAL -> {
448 // NOTE: escaped characters are not unescaped
449 final String quotedText = firstChild.getText();
450 yield quotedText.substring(1, quotedText.length() - 1);
451 }
452 case TokenTypes.IDENT -> firstChild.getText();
453 case TokenTypes.DOT -> firstChild.getLastChild().getText();
454 case TokenTypes.TEXT_BLOCK_LITERAL_BEGIN -> {
455 final String textBlockContent = firstChild.getFirstChild().getText();
456 yield getContentWithoutPrecedingWhitespace(textBlockContent);
457 }
458 default ->
459 // annotations with complex expressions cannot suppress warnings
460 "";
461 };
462 }
463
464 /**
465 * Returns the annotation values represented by an AST.
466 *
467 * @param ast an AST node for an EXPR or ANNOTATION_ARRAY_INIT
468 * @return the list of Java string represented by the given AST for an
469 * expression or annotation array initializer
470 * @throws IllegalArgumentException if the AST is invalid
471 */
472 private static List<String> getAnnotationValues(DetailAST ast) {
473 return switch (ast.getType()) {
474 case TokenTypes.EXPR -> Collections.singletonList(getStringExpr(ast));
475 case TokenTypes.ANNOTATION_ARRAY_INIT -> findAllExpressionsInChildren(ast);
476 default -> throw new IllegalArgumentException(
477 "Expression or annotation array initializer AST expected: " + ast);
478 };
479 }
480
481 /**
482 * Method looks at children and returns list of expressions in strings.
483 *
484 * @param parent ast, that contains children
485 * @return list of expressions in strings
486 */
487 private static List<String> findAllExpressionsInChildren(DetailAST parent) {
488 final List<String> valueList = new LinkedList<>();
489 DetailAST childAST = parent.getFirstChild();
490 while (childAST != null) {
491 if (childAST.getType() == TokenTypes.EXPR) {
492 valueList.add(getStringExpr(childAST));
493 }
494 childAST = childAST.getNextSibling();
495 }
496 return valueList;
497 }
498
499 /**
500 * Remove preceding newline and whitespace from the content of a text block.
501 *
502 * @param textBlockContent the actual text in a text block.
503 * @return content of text block with preceding whitespace and newline removed.
504 */
505 private static String getContentWithoutPrecedingWhitespace(String textBlockContent) {
506 final String contentWithNoPrecedingNewline =
507 NEWLINE.matcher(textBlockContent).replaceAll("");
508 return WHITESPACE.matcher(contentWithNoPrecedingNewline).replaceAll("");
509 }
510
511 @Override
512 public void destroy() {
513 super.destroy();
514 ENTRIES.remove();
515 }
516
517 /** Records a particular suppression for a region of a file. */
518 private static final class Entry {
519
520 /** The source name of the suppressed check. */
521 private final String checkName;
522 /** The suppression region for the check - first line. */
523 private final int firstLine;
524 /** The suppression region for the check - first column. */
525 private final int firstColumn;
526 /** The suppression region for the check - last line. */
527 private final int lastLine;
528 /** The suppression region for the check - last column. */
529 private final int lastColumn;
530
531 /**
532 * Constructs a new suppression region entry.
533 *
534 * @param checkName the source name of the suppressed check
535 * @param firstLine the first line of the suppression region
536 * @param firstColumn the first column of the suppression region
537 * @param lastLine the last line of the suppression region
538 * @param lastColumn the last column of the suppression region
539 */
540 private Entry(String checkName, int firstLine, int firstColumn,
541 int lastLine, int lastColumn) {
542 this.checkName = checkName;
543 this.firstLine = firstLine;
544 this.firstColumn = firstColumn;
545 this.lastLine = lastLine;
546 this.lastColumn = lastColumn;
547 }
548
549 /**
550 * Gets the source name of the suppressed check.
551 *
552 * @return the source name of the suppressed check
553 */
554 public String getCheckName() {
555 return checkName;
556 }
557
558 /**
559 * Gets the first line of the suppression region.
560 *
561 * @return the first line of the suppression region
562 */
563 public int getFirstLine() {
564 return firstLine;
565 }
566
567 /**
568 * Gets the first column of the suppression region.
569 *
570 * @return the first column of the suppression region
571 */
572 public int getFirstColumn() {
573 return firstColumn;
574 }
575
576 /**
577 * Gets the last line of the suppression region.
578 *
579 * @return the last line of the suppression region
580 */
581 public int getLastLine() {
582 return lastLine;
583 }
584
585 /**
586 * Gets the last column of the suppression region.
587 *
588 * @return the last column of the suppression region
589 */
590 public int getLastColumn() {
591 return lastColumn;
592 }
593
594 }
595
596 }