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.checks;
21
22 import java.util.ArrayList;
23 import java.util.HashMap;
24 import java.util.List;
25 import java.util.Locale;
26 import java.util.Map;
27 import java.util.Optional;
28 import java.util.regex.Pattern;
29
30 import javax.annotation.Nullable;
31
32 import com.puppycrawl.tools.checkstyle.StatelessCheck;
33 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
34 import com.puppycrawl.tools.checkstyle.api.AuditEvent;
35 import com.puppycrawl.tools.checkstyle.api.DetailAST;
36 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
37 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
38
39 /**
40 * <div>
41 * Maintains a set of check suppressions from {@code @SuppressWarnings} annotations.
42 * It allows to prevent Checkstyle from reporting violations from parts of code that were
43 * annotated with {@code @SuppressWarnings} and using name of the check to be excluded.
44 * It is possible to suppress all the checkstyle warnings with the argument {@code "all"}.
45 * You can also use a {@code checkstyle:} prefix to prevent compiler
46 * from processing these annotations.
47 * You can also define aliases for check names that need to be suppressed.
48 * </div>
49 *
50 * @since 5.7
51 */
52 @StatelessCheck
53 public class SuppressWarningsHolder
54 extends AbstractCheck {
55
56 /**
57 * Optional prefix for warning suppressions that are only intended to be
58 * recognized by checkstyle. For instance, to suppress {@code
59 * FallThroughCheck} only in checkstyle (and not in javac), use the
60 * suppression {@code "checkstyle:fallthrough"} or {@code "checkstyle:FallThrough"}.
61 * To suppress the warning in both tools, just use {@code "fallthrough"}.
62 */
63 private static final String CHECKSTYLE_PREFIX = "checkstyle:";
64
65 /** Java.lang namespace prefix, which is stripped from SuppressWarnings. */
66 private static final String JAVA_LANG_PREFIX = "java.lang.";
67
68 /** Suffix to be removed from subclasses of Check. */
69 private static final String CHECK_SUFFIX = "check";
70
71 /** Special warning id for matching all the warnings. */
72 private static final String ALL_WARNING_MATCHING_ID = "all";
73
74 /** A map from check source names to suppression aliases. */
75 private static final Map<String, String> CHECK_ALIAS_MAP = new HashMap<>();
76
77 /**
78 * A thread-local holder for the list of suppression entries for the last
79 * file parsed.
80 */
81 private static final ThreadLocal<List<Entry>> ENTRIES =
82 ThreadLocal.withInitial(ArrayList::new);
83
84 /**
85 * Compiled pattern used to match whitespace in text block content.
86 */
87 private static final Pattern WHITESPACE = Pattern.compile("\\s+");
88
89 /**
90 * Compiled pattern used to match preceding newline in text block content.
91 */
92 private static final Pattern NEWLINE = Pattern.compile("\\n");
93
94 /**
95 * Creates a new {@code SuppressWarningsHolder} instance.
96 */
97 public SuppressWarningsHolder() {
98 // no code by default
99 }
100
101 /**
102 * Returns the default alias for the source name of a check, which is the
103 * source name in lower case with any dotted prefix or "Check"/"check"
104 * suffix removed.
105 *
106 * @param sourceName the source name of the check (generally the class
107 * name)
108 * @return the default alias for the given check
109 */
110 public static String getDefaultAlias(String sourceName) {
111 int endIndex = sourceName.length();
112 final String sourceNameLower = sourceName.toLowerCase(Locale.ENGLISH);
113 if (sourceNameLower.endsWith(CHECK_SUFFIX)) {
114 endIndex -= CHECK_SUFFIX.length();
115 }
116 final int startIndex = sourceNameLower.lastIndexOf('.') + 1;
117 return sourceNameLower.substring(startIndex, endIndex);
118 }
119
120 /**
121 * Returns the alias of simple check name for a check, The alias is
122 * for the form of CheckNameCheck or CheckName.
123 *
124 * @param sourceName the source name of the check (generally the class
125 * name)
126 * @return the alias of the simple check name for the given check
127 */
128 @Nullable
129 private static String getSimpleNameAlias(String sourceName) {
130 final String checkName = CommonUtil.baseClassName(sourceName);
131 final String checkNameSuffix = "Check";
132 // check alias for the CheckNameCheck
133 String checkAlias = CHECK_ALIAS_MAP.get(checkName);
134 if (checkAlias == null && checkName.endsWith(checkNameSuffix)) {
135 final int checkStartIndex = checkName.length() - checkNameSuffix.length();
136 final String checkNameWithoutSuffix = checkName.substring(0, checkStartIndex);
137 // check alias for the CheckName
138 checkAlias = CHECK_ALIAS_MAP.get(checkNameWithoutSuffix);
139 }
140
141 return checkAlias;
142 }
143
144 /**
145 * Returns the alias for the source name of a check. If an alias has been
146 * explicitly registered via {@link #setAliasList(String...)}, that
147 * alias is returned; otherwise, the default alias is used.
148 *
149 * @param sourceName the source name of the check (generally the class
150 * name)
151 * @return the current alias for the given check
152 */
153 public static String getAlias(String sourceName) {
154 String checkAlias = CHECK_ALIAS_MAP.get(sourceName);
155 if (checkAlias == null) {
156 checkAlias = getSimpleNameAlias(sourceName);
157 }
158 if (checkAlias == null) {
159 checkAlias = getDefaultAlias(sourceName);
160 }
161 return checkAlias;
162 }
163
164 /**
165 * Registers an alias for the source name of a check.
166 *
167 * @param sourceName the source name of the check (generally the class
168 * name)
169 * @param checkAlias the alias used in {@link SuppressWarnings} annotations
170 */
171 private static void registerAlias(String sourceName, String checkAlias) {
172 CHECK_ALIAS_MAP.put(sourceName, checkAlias);
173 }
174
175 /**
176 * Setter to specify aliases for check names that can be used in code
177 * within {@code SuppressWarnings} in a format of comma separated attribute=value entries.
178 * The attribute is the fully qualified name of the Check and value is its alias.
179 *
180 * @param aliasList comma-separated alias assignments
181 * @throws IllegalArgumentException when alias item does not have '='
182 * @since 5.7
183 */
184 public void setAliasList(String... aliasList) {
185 for (String sourceAlias : aliasList) {
186 final int index = sourceAlias.indexOf('=');
187 if (index > 0) {
188 registerAlias(sourceAlias.substring(0, index), sourceAlias
189 .substring(index + 1));
190 }
191 else if (!sourceAlias.isEmpty()) {
192 throw new IllegalArgumentException(
193 "'=' expected in alias list item: " + sourceAlias);
194 }
195 }
196 }
197
198 /**
199 * Checks for a suppression of a check with the given source name and
200 * location in the last file processed.
201 *
202 * @param event audit event.
203 * @return whether the check with the given name is suppressed at the given
204 * source location
205 */
206 public static boolean isSuppressed(AuditEvent event) {
207 final List<Entry> entries = ENTRIES.get();
208 final String sourceName = event.getSourceName();
209 final String checkAlias = getAlias(sourceName);
210 final int line = event.getLine();
211 final int column = event.getColumn();
212 boolean suppressed = false;
213 for (Entry entry : entries) {
214 final boolean afterStart = isSuppressedAfterEventStart(line, column, entry);
215 final boolean beforeEnd = isSuppressedBeforeEventEnd(line, column, entry);
216 final String checkName = entry.checkName();
217 final boolean nameMatches =
218 ALL_WARNING_MATCHING_ID.equals(checkName)
219 || checkName.equalsIgnoreCase(checkAlias)
220 || getDefaultAlias(checkName).equalsIgnoreCase(checkAlias)
221 || getDefaultAlias(sourceName).equalsIgnoreCase(checkName);
222 if (afterStart && beforeEnd
223 && (nameMatches || checkName.equals(event.getModuleId()))) {
224 suppressed = true;
225 break;
226 }
227 }
228 return suppressed;
229 }
230
231 /**
232 * Checks whether suppression entry position is after the audit event occurrence position
233 * in the source file.
234 *
235 * @param line the line number in the source file where the event occurred.
236 * @param column the column number in the source file where the event occurred.
237 * @param entry suppression entry.
238 * @return true if suppression entry position is after the audit event occurrence position
239 * in the source file.
240 */
241 private static boolean isSuppressedAfterEventStart(int line, int column, Entry entry) {
242 return entry.firstLine() < line
243 || entry.firstLine() == line
244 && (column == 0 || entry.firstColumn() <= column);
245 }
246
247 /**
248 * Checks whether suppression entry position is before the audit event occurrence position
249 * in the source file.
250 *
251 * @param line the line number in the source file where the event occurred.
252 * @param column the column number in the source file where the event occurred.
253 * @param entry suppression entry.
254 * @return true if suppression entry position is before the audit event occurrence position
255 * in the source file.
256 */
257 private static boolean isSuppressedBeforeEventEnd(int line, int column, Entry entry) {
258 return entry.lastLine() > line
259 || entry.lastLine() == line && entry
260 .lastColumn() >= column;
261 }
262
263 @Override
264 public int[] getDefaultTokens() {
265 return getRequiredTokens();
266 }
267
268 @Override
269 public int[] getAcceptableTokens() {
270 return getRequiredTokens();
271 }
272
273 @Override
274 public int[] getRequiredTokens() {
275 return new int[] {TokenTypes.ANNOTATION};
276 }
277
278 @Override
279 public void beginTree(DetailAST rootAST) {
280 ENTRIES.get().clear();
281 }
282
283 @Override
284 public void visitToken(DetailAST ast) {
285 // check whether annotation is SuppressWarnings
286 // expected children: AT ( IDENT | DOT ) LPAREN <values> RPAREN
287 String identifier = getIdentifier(getNthChild(ast, 1));
288 if (identifier.startsWith(JAVA_LANG_PREFIX)) {
289 identifier = identifier.substring(JAVA_LANG_PREFIX.length());
290 }
291 if ("SuppressWarnings".equals(identifier)) {
292 getAnnotationTarget(ast).ifPresent(targetAST -> {
293 addSuppressions(getAllAnnotationValues(ast), targetAST);
294 });
295 }
296 }
297
298 /**
299 * Method to populate list of suppression entries.
300 *
301 * @param values
302 * - list of check names
303 * @param targetAST
304 * - annotation target
305 */
306 private static void addSuppressions(List<String> values, DetailAST targetAST) {
307 // get text range of target
308 final int firstLine = targetAST.getLineNo();
309 final int firstColumn = targetAST.getColumnNo();
310 final DetailAST nextAST = targetAST.getNextSibling();
311 final int lastLine;
312 final int lastColumn;
313 if (nextAST == null) {
314 lastLine = Integer.MAX_VALUE;
315 lastColumn = Integer.MAX_VALUE;
316 }
317 else {
318 lastLine = nextAST.getLineNo();
319 lastColumn = nextAST.getColumnNo();
320 }
321
322 final List<Entry> entries = ENTRIES.get();
323 for (String value : values) {
324 // strip off the checkstyle-only prefix if present
325 final String checkName = removeCheckstylePrefixIfExists(value);
326 entries.add(new Entry(checkName, firstLine, firstColumn,
327 lastLine, lastColumn));
328 }
329 }
330
331 /**
332 * Method removes checkstyle prefix (checkstyle:) from check name if exists.
333 *
334 * @param checkName
335 * - name of the check
336 * @return check name without prefix
337 */
338 private static String removeCheckstylePrefixIfExists(String checkName) {
339 String result = checkName;
340 if (checkName.startsWith(CHECKSTYLE_PREFIX)) {
341 result = checkName.substring(CHECKSTYLE_PREFIX.length());
342 }
343 return result;
344 }
345
346 /**
347 * Get all annotation values.
348 *
349 * @param ast annotation token
350 * @return list values
351 * @throws IllegalArgumentException if there is an unknown annotation value type.
352 */
353 private static List<String> getAllAnnotationValues(DetailAST ast) {
354 // get values of annotation
355 List<String> values = List.of();
356 final DetailAST lparenAST = ast.findFirstToken(TokenTypes.LPAREN);
357 if (lparenAST != null) {
358 final DetailAST nextAST = lparenAST.getNextSibling();
359 final int nextType = nextAST.getType();
360 switch (nextType) {
361 case TokenTypes.EXPR, TokenTypes.ANNOTATION_ARRAY_INIT ->
362 values = getAnnotationValues(nextAST);
363 case TokenTypes.ANNOTATION_MEMBER_VALUE_PAIR ->
364 // expected children: IDENT ASSIGN ( EXPR |
365 // ANNOTATION_ARRAY_INIT )
366 values = getAnnotationValues(getNthChild(nextAST, 2));
367 case TokenTypes.RPAREN -> {
368 // no value present (not valid Java)
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 -> List.of(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 ArrayList<>();
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 /**
518 * Records a particular suppression for a region of a file.
519 *
520 * @param checkName the source name of the suppressed check
521 * @param firstLine the first line of the suppression region
522 * @param firstColumn the first column of the suppression region
523 * @param lastLine the last line of the suppression region
524 * @param lastColumn the last column of the suppression region
525 */
526 private record Entry(String checkName, int firstLine,
527 int firstColumn, int lastLine, int lastColumn) {
528
529 }
530
531 }