1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package com.puppycrawl.tools.checkstyle.checks.coding;
21
22 import java.util.ArrayDeque;
23 import java.util.ArrayList;
24 import java.util.Deque;
25 import java.util.HashMap;
26 import java.util.HashSet;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Set;
30 import java.util.regex.Pattern;
31 import java.util.stream.Collectors;
32
33 import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
34 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
35 import com.puppycrawl.tools.checkstyle.api.DetailAST;
36 import com.puppycrawl.tools.checkstyle.api.FullIdent;
37 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
38 import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil;
39 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
40
41
42
43
44
45
46
47
48
49
50 @FileStatefulCheck
51 public class UnusedPrivateFieldCheck extends AbstractCheck {
52
53
54
55
56 public static final String MSG_PRIVATE_FIELD = "unused.private.field";
57
58
59
60
61 private final Deque<Map<String, DetailAST>> privateFields = new ArrayDeque<>();
62
63
64
65
66 private final Deque<String> enclosingTypeNames = new ArrayDeque<>();
67
68
69
70
71 private final List<FieldUsage> fieldUsages = new ArrayList<>();
72
73
74
75
76 private final Set<String> globalUsedFields = new HashSet<>();
77
78
79
80
81 private final List<PendingField> pendingFields = new ArrayList<>();
82
83
84
85
86 private final Deque<Map<String, String>> scopeStack = new ArrayDeque<>();
87
88
89
90
91
92 private final Deque<Deque<Map<String, String>>> scopeStackSnapshots = new ArrayDeque<>();
93
94
95
96
97 private final List<TypedUsage> typedUsages = new ArrayList<>();
98
99
100
101
102
103 private final Map<String, Map<String, DetailAST>> privateFieldsByType = new HashMap<>();
104
105
106
107
108
109
110
111 private Set<String> ignoreAnnotationCanonicalNames = new HashSet<>(Set.of("java.io.Serial"));
112
113
114
115
116 private Pattern ignoredFieldPattern = Pattern.compile("serialVersionUID");
117
118
119
120
121 private Set<String> ignoreAnnotationShortNames = new HashSet<>();
122
123
124
125
126
127 public UnusedPrivateFieldCheck() {
128
129 }
130
131
132
133
134
135
136
137 public void setIgnoreAnnotationCanonicalNames(String... annotationNames) {
138 ignoreAnnotationCanonicalNames = Set.of(annotationNames);
139 }
140
141
142
143
144
145
146
147
148
149
150
151 public void setIgnoredFieldPattern(Pattern pattern) {
152 ignoredFieldPattern = pattern;
153 }
154
155 @Override
156 public int[] getAcceptableTokens() {
157 return new int[] {
158 TokenTypes.IMPORT,
159 TokenTypes.OBJBLOCK,
160 TokenTypes.VARIABLE_DEF,
161 TokenTypes.PARAMETER_DEF,
162 TokenTypes.PARAMETERS,
163 TokenTypes.SLIST,
164 TokenTypes.IDENT,
165 TokenTypes.METHOD_DEF,
166 TokenTypes.CTOR_DEF,
167 TokenTypes.LAMBDA,
168 TokenTypes.LITERAL_FOR,
169 TokenTypes.LITERAL_CATCH,
170 };
171 }
172
173 @Override
174 public int[] getDefaultTokens() {
175 return getAcceptableTokens();
176 }
177
178 @Override
179 public int[] getRequiredTokens() {
180 return getAcceptableTokens();
181 }
182
183 @Override
184 public void beginTree(DetailAST rootAST) {
185 privateFields.clear();
186 enclosingTypeNames.clear();
187 fieldUsages.clear();
188 globalUsedFields.clear();
189 pendingFields.clear();
190 scopeStack.clear();
191 scopeStackSnapshots.clear();
192 typedUsages.clear();
193 privateFieldsByType.clear();
194 ignoreAnnotationShortNames = ignoreAnnotationCanonicalNames.stream()
195 .map(CommonUtil::baseClassName)
196 .collect(Collectors.toCollection(HashSet::new));
197 }
198
199 @Override
200 public void visitToken(DetailAST ast) {
201 switch (ast.getType()) {
202 case TokenTypes.OBJBLOCK -> {
203 privateFields.push(new HashMap<>());
204 scopeStackSnapshots.push(new ArrayDeque<>(scopeStack));
205 scopeStack.clear();
206 final DetailAST typeDef = ast.getParent();
207 final DetailAST nameIdent = typeDef.findFirstToken(TokenTypes.IDENT);
208 if (nameIdent == null) {
209 enclosingTypeNames.push("");
210 }
211 else {
212 enclosingTypeNames.push(nameIdent.getText());
213 }
214 }
215 case TokenTypes.PARAMETERS, TokenTypes.SLIST, TokenTypes.LITERAL_FOR,
216 TokenTypes.LITERAL_CATCH -> scopeStack.push(new HashMap<>());
217 case TokenTypes.PARAMETER_DEF -> {
218 final DetailAST ident = ast.findFirstToken(TokenTypes.IDENT);
219 if (ident != null) {
220 scopeStack.peek().put(ident.getText(), resolveDeclaredTypeName(ast));
221 }
222 }
223 case TokenTypes.VARIABLE_DEF -> handleVariableDef(ast);
224 case TokenTypes.IDENT -> handleIdent(ast);
225 default -> {
226
227 }
228 }
229 }
230
231 @Override
232 public void leaveToken(DetailAST ast) {
233 switch (ast.getType()) {
234 case TokenTypes.OBJBLOCK -> {
235 final Map<String, DetailAST> classFields = privateFields.pop();
236 privateFieldsByType.put(enclosingTypeNames.peek(), classFields);
237 for (final Map.Entry<String, DetailAST> entry : classFields.entrySet()) {
238 pendingFields.add(new PendingField(entry));
239 }
240 final Deque<Map<String, String>> snapshot = scopeStackSnapshots.pop();
241 snapshot.forEach(scopeStack::push);
242 enclosingTypeNames.pop();
243 }
244 case TokenTypes.LAMBDA -> {
245 if (ast.findFirstToken(TokenTypes.PARAMETERS) != null) {
246 scopeStack.pop();
247 }
248 }
249 case TokenTypes.METHOD_DEF, TokenTypes.CTOR_DEF,
250 TokenTypes.SLIST, TokenTypes.LITERAL_FOR,
251 TokenTypes.LITERAL_CATCH -> scopeStack.pop();
252 default -> {
253
254 }
255 }
256 }
257
258 @Override
259 public void finishTree(final DetailAST rootAST) {
260 final Set<DetailAST> usedFieldIdents = new HashSet<>();
261 fieldUsages.stream()
262 .map(UnusedPrivateFieldCheck::resolveUsage)
263 .forEach(usedFieldIdents::add);
264 for (final TypedUsage typedUsage : typedUsages) {
265 final Map<String, DetailAST> fields = privateFieldsByType.get(typedUsage.typeName());
266 if (fields != null) {
267 final DetailAST ident = fields.get(typedUsage.fieldName());
268 usedFieldIdents.add(ident);
269
270 }
271 }
272 for (final PendingField pending : pendingFields) {
273 final Map.Entry<String, DetailAST> entry = pending.entry();
274 final DetailAST ident = entry.getValue();
275 final String name = entry.getKey();
276 if (!usedFieldIdents.contains(ident) && !globalUsedFields.contains(name)) {
277 log(ident, MSG_PRIVATE_FIELD, name);
278 }
279 }
280 }
281
282
283
284
285
286
287
288
289 private static DetailAST resolveUsage(FieldUsage usage) {
290 DetailAST result = null;
291 if (usage.qualifierTypeName() != null) {
292 final int index = usage.ancestorTypeNames().indexOf(usage.qualifierTypeName());
293 if (index != -1) {
294 result = usage.ancestorFieldMaps().get(index).get(usage.name());
295 }
296 }
297 else {
298 for (final Map<String, DetailAST> level : usage.ancestorFieldMaps()) {
299 result = level.get(usage.name());
300 if (result != null) {
301 break;
302 }
303 }
304 }
305 return result;
306 }
307
308
309
310
311
312
313 private void handleVariableDef(DetailAST ast) {
314 final DetailAST parent = ast.getParent();
315
316 if (parent.getType() == TokenTypes.OBJBLOCK) {
317 final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
318 final boolean isPrivateField = isPrivate(modifiers);
319 final DetailAST ident = ast.findFirstToken(TokenTypes.IDENT);
320 final boolean isIgnoredName =
321 ignoredFieldPattern.matcher(ident.getText()).matches();
322 final boolean isIgnored = isIgnoredName || hasIgnoredAnnotation(ast);
323 if (isPrivateField && !isIgnored) {
324 privateFields.peek().put(ident.getText(), ident);
325 }
326 }
327 else if (!scopeStack.isEmpty()) {
328 final String localName =
329 ast.findFirstToken(TokenTypes.IDENT).getText();
330 scopeStack.peek().put(localName, resolveDeclaredTypeName(ast));
331 }
332 }
333
334
335
336
337
338
339
340
341
342 private boolean hasIgnoredAnnotation(final DetailAST variableDef) {
343 boolean result = isAnnotatedWithIgnoredAnnotation(variableDef);
344 if (!result) {
345 final DetailAST classDef = variableDef.getParent().getParent();
346 result = isAnnotatedWithIgnoredAnnotation(classDef);
347 }
348 return result;
349 }
350
351
352
353
354
355
356
357
358
359
360 private boolean isAnnotatedWithIgnoredAnnotation(final DetailAST ast) {
361 boolean result = false;
362 final DetailAST holder = AnnotationUtil.getAnnotationHolder(ast);
363 if (holder != null) {
364 DetailAST child = holder.getFirstChild();
365 while (child != null) {
366 if (child.getType() == TokenTypes.ANNOTATION) {
367 final String name =
368 FullIdent.createFullIdent(
369 child.getFirstChild().getNextSibling()).getText();
370 if (ignoreAnnotationCanonicalNames.contains(name)
371 || ignoreAnnotationShortNames.contains(name)) {
372 result = true;
373 break;
374 }
375 }
376 child = child.getNextSibling();
377 }
378 }
379 return result;
380 }
381
382
383
384
385
386
387
388
389 private void handleIdent(DetailAST ast) {
390 final DetailAST parent = ast.getParent();
391 if (!isDeclarationParent(parent)) {
392 final String name = ast.getText();
393 final boolean shadowed =
394 scopeStack.stream().anyMatch(scope -> scope.containsKey(name));
395 if (parent.getType() == TokenTypes.DOT) {
396 handleDotAccess(parent, name);
397 }
398 else if (!shadowed) {
399 recordUsage(name, null, false);
400 }
401 }
402 }
403
404
405
406
407
408
409
410
411
412
413
414 private void handleDotAccess(DetailAST dot, String name) {
415 final DetailAST qualifier = dot.getFirstChild();
416 if (qualifier.getType() == TokenTypes.LITERAL_THIS) {
417 recordUsage(name, null, true);
418 }
419 else if (qualifier.getType() == TokenTypes.DOT
420 && qualifier.getLastChild().getType() == TokenTypes.LITERAL_THIS) {
421 final String qualifiedName =
422 FullIdent.createFullIdent(qualifier.getFirstChild()).getText();
423 final String simpleName =
424 qualifiedName.substring(qualifiedName.lastIndexOf('.') + 1);
425 recordUsage(name, simpleName, false);
426 }
427 else if (findDeclaredType(qualifier.getText()) != null) {
428 typedUsages.add(new TypedUsage(findDeclaredType(qualifier.getText()), name));
429 }
430 else {
431 globalUsedFields.add(name);
432 }
433 }
434
435
436
437
438
439
440
441
442
443 private String findDeclaredType(String name) {
444 String result = null;
445 for (final Map<String, String> scope : scopeStack) {
446 final String type = scope.get(name);
447 if (type != null) {
448 result = type;
449 break;
450 }
451 }
452 return result;
453 }
454
455
456
457
458
459
460
461
462
463 private static String resolveDeclaredTypeName(DetailAST varOrParamDef) {
464 final DetailAST typeAst = varOrParamDef.findFirstToken(TokenTypes.TYPE);
465 String result = null;
466 final DetailAST identChild = typeAst.findFirstToken(TokenTypes.IDENT);
467 if (identChild != null) {
468 result = identChild.getText();
469 }
470 return result;
471 }
472
473
474
475
476
477
478
479
480
481
482 private void recordUsage(String name, String qualifierTypeName, boolean bareThisQualified) {
483 fieldUsages.add(new FieldUsage(name,
484 new ArrayList<>(privateFields),
485 new ArrayList<>(enclosingTypeNames),
486 qualifierTypeName,
487 bareThisQualified));
488 }
489
490
491
492
493
494
495
496
497
498 private static boolean isDeclarationParent(DetailAST parent) {
499 final int type = parent.getType();
500 return type == TokenTypes.VARIABLE_DEF
501 || type == TokenTypes.METHOD_DEF
502 || type == TokenTypes.CLASS_DEF
503 || type == TokenTypes.INTERFACE_DEF
504 || type == TokenTypes.ENUM_DEF
505 || type == TokenTypes.RECORD_DEF
506 || type == TokenTypes.ANNOTATION_DEF;
507 }
508
509
510
511
512
513
514
515 private static boolean isPrivate(final DetailAST modifiers) {
516 return modifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE) != null;
517 }
518
519
520
521
522
523
524 private record PendingField(Map.Entry<String, DetailAST> entry) {
525 }
526
527
528
529
530
531
532
533
534 private record TypedUsage(String typeName, String fieldName) {
535 }
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550 private record FieldUsage(String name, List<Map<String, DetailAST>> ancestorFieldMaps,
551 List<String> ancestorTypeNames, String qualifierTypeName,
552 boolean bareThisQualified) {
553 }
554
555 }