001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.coding;
021
022import java.util.ArrayDeque;
023import java.util.ArrayList;
024import java.util.Deque;
025import java.util.HashMap;
026import java.util.HashSet;
027import java.util.List;
028import java.util.Map;
029import java.util.Set;
030import java.util.regex.Pattern;
031import java.util.stream.Collectors;
032
033import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
034import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
035import com.puppycrawl.tools.checkstyle.api.DetailAST;
036import com.puppycrawl.tools.checkstyle.api.FullIdent;
037import com.puppycrawl.tools.checkstyle.api.TokenTypes;
038import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil;
039import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
040
041/**
042 * <div>
043 * Check that a private field is declared, but never used. Fields with any other
044 * visibility (package-private, protected, or public), including those declared in
045 * an implicitly declared class (compact source file), are not checked.
046 * </div>
047 *
048 * @since 14.1.0
049 */
050@FileStatefulCheck
051public class UnusedPrivateFieldCheck extends AbstractCheck {
052
053    /**
054     * A key is pointing to the warning message text in "messages.properties".
055     */
056    public static final String MSG_PRIVATE_FIELD = "unused.private.field";
057
058    /**
059     * Stack of private field maps, one per class nesting level.
060     */
061    private final Deque<Map<String, DetailAST>> privateFields = new ArrayDeque<>();
062
063    /**
064     * Stack of currently open enclosing type names.
065     */
066    private final Deque<String> enclosingTypeNames = new ArrayDeque<>();
067
068    /**
069     * Recorded field-name usage occurrences.
070     */
071    private final List<FieldUsage> fieldUsages = new ArrayList<>();
072
073    /**
074     * Global set of field names accessed.
075     */
076    private final Set<String> globalUsedFields = new HashSet<>();
077
078    /**
079     * Accumulated pending fields, reported at finishTree.
080     */
081    private final List<PendingField> pendingFields = new ArrayList<>();
082
083    /**
084     * Scope stack tracking local variable and parameter names per block.
085     */
086    private final Deque<Map<String, String>> scopeStack = new ArrayDeque<>();
087
088    /**
089     * Snapshots of scope stack saved when entering a nested class,
090     * restored when leaving it.
091     */
092    private final Deque<Deque<Map<String, String>>> scopeStackSnapshots = new ArrayDeque<>();
093
094    /**
095     * Recorded field-name accesses on a qualifier.
096     */
097    private final List<TypedUsage> typedUsages = new ArrayList<>();
098
099    /**
100     * Each class's private fields, keyed by simple type name, populated once
101     * a class's OBJBLOCK closes. Used to resolve {@link #typedUsages}.
102     */
103    private final Map<String, Map<String, DetailAST>> privateFieldsByType = new HashMap<>();
104
105    /**
106     * Specify annotations canonical names which ignore variables in consideration.
107     * A field is ignored either when the field itself carries a matching annotation,
108     * or when its enclosing class, interface, enum, or record carries one (e.g. a
109     * class-level Lombok {@code @Getter}).
110     */
111    private Set<String> ignoreAnnotationCanonicalNames = new HashSet<>(Set.of("java.io.Serial"));
112
113    /**
114     * Specify a regular expression pattern for field names to ignore.
115     */
116    private Pattern ignoredFieldPattern = Pattern.compile("serialVersionUID");
117
118    /**
119     * Set of ignore annotations short names.
120     */
121    private Set<String> ignoreAnnotationShortNames = new HashSet<>();
122
123    /**
124     * Creates a new {@code UnusedPrivateFieldCheck} instance with default values.
125     *
126     */
127    public UnusedPrivateFieldCheck() {
128        // default constructor
129    }
130
131    /**
132     * Setter to specify annotations canonical names which ignore variables in consideration.
133     *
134     * @param annotationNames array of ignore annotations canonical names.
135     * @since 14.1.0
136     */
137    public void setIgnoreAnnotationCanonicalNames(String... annotationNames) {
138        ignoreAnnotationCanonicalNames = Set.of(annotationNames);
139    }
140
141    /**
142     * Setter to specify a regular expression pattern for field names to ignore, even
143     * if they otherwise satisfy this check's detection of an unused private field.
144     * Note this replaces the default value entirely — to keep {@code serialVersionUID}
145     * ignored alongside your own pattern, include it explicitly, e.g.
146     * {@code ^(serialVersionUID|LOG|LOGGER)$}.
147     *
148     * @param pattern regular expression pattern for field names to ignore.
149     * @since 14.1.0
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                // no action needed for other token types
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                // no action needed for other token types
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     * Resolves a recorded usage to the exact field declaration it refers to,
284     * following the same shadowing rules Java itself applies.
285     *
286     * @param usage the recorded usage to resolve.
287     * @return the DetailAST of the field it resolves to, or null if none.
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     * Collects private field declarations.
310     *
311     * @param ast for this method.
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     * Checks whether a field should be ignored because either the field itself or its
336     * enclosing type (class, interface, enum, or record) carries an annotation present
337     * in {@link #ignoreAnnotationCanonicalNames} (matched by canonical or short name).
338     *
339     * @param variableDef the VARIABLE_DEF node.
340     * @return true if the field or its enclosing type has a matching ignore annotation.
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     * Checks whether the given AST node (a field or a type definition) carries an
353     * annotation present in {@link #ignoreAnnotationCanonicalNames}, matched either
354     * by canonical name or by short name.
355     *
356     * @param ast the VARIABLE_DEF, CLASS_DEF, INTERFACE_DEF, ENUM_DEF, RECORD_DEF, or
357     *            ANNOTATION_DEF node to inspect.
358     * @return true if a matching ignore annotation is present directly on {@code ast}.
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     * Records field usage, respecting local variable and parameter shadowing.
384     * Resolution to a specific field declaration happens later, at
385     * {@link #finishTree}.
386     *
387     * @param ast for handleIdent
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     * Classifies a dot-qualified reference: {@code this.field} and
406     * {@code ClassName.this.field} are resolved precisely; any other qualifier
407     * (an arbitrary object or type reference) cannot be resolved without type
408     * information, so it falls back to name-only matching via
409     * {@link #globalUsedFields}.
410     *
411     * @param dot  the DOT node whose last child is the field IDENT.
412     * @param name the field name being accessed.
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     * Looks up the declared simple type name of a local variable or parameter
437     * currently in scope.
438     *
439     * @param name the variable or parameter name.
440     * @return its declared simple type name, or null if the name is not a
441     *         tracked local/parameter, or its type could not be determined.
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     * Extracts the declared simple type name of a VARIABLE_DEF or
457     * PARAMETER_DEF, if determinable.
458     *
459     * @param varOrParamDef the VARIABLE_DEF or PARAMETER_DEF node.
460     * @return the simple type name, or null if it could not be determined
461     *         (e.g. primitive types, inferred/generic-only forms).
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     * Snapshots the currently open class chain (field maps and type names) so the
475     * usage can be resolved later, once every class in the file is fully populated.
476     *
477     * @param name              the field name referenced.
478     * @param qualifierTypeName the enclosing type name named in a
479     *                          {@code ClassName.this.field} reference, or null.
480     * @param bareThisQualified true for a {@code this.field} reference.
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     * Checks whether the given parent node is a declaration site whose IDENT child
492     * names the declared element itself (a variable, method, constructor, or type),
493     * rather than referencing some other field.
494     *
495     * @param parent the parent of the IDENT being inspected.
496     * @return true if the IDENT is a declaration name, not a usage.
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     * Checks whether a field is private.
511     *
512     * @param modifiers for isPrivate method.
513     * @return modifiers of literal_private.
514     */
515    private static boolean isPrivate(final DetailAST modifiers) {
516        return modifiers.findFirstToken(TokenTypes.LITERAL_PRIVATE) != null;
517    }
518
519    /**
520     * Holds a private field entry, reported if never resolved to by any usage.
521     *
522     * @param entry The field name and its AST node.
523     */
524    private record PendingField(Map.Entry<String, DetailAST> entry) {
525    }
526
527    /**
528     * A recorded field-name access on a qualifier whose declared type is
529     * known precisely.
530     *
531     * @param typeName  the qualifier's declared simple type name.
532     * @param fieldName the field name accessed on it.
533     */
534    private record TypedUsage(String typeName, String fieldName) {
535    }
536
537    /**
538     * A recorded, not-yet-resolved reference to a field name, with enough context
539     * to resolve it precisely once the whole file has been visited.
540     *
541     * @param name                the field name referenced.
542     * @param ancestorFieldMaps   the currently open field maps at the time of the
543     *                            reference, innermost first (index 0).
544     * @param ancestorTypeNames   the currently open type names, parallel to
545     *                            {@code ancestorFieldMaps}.
546     * @param qualifierTypeName   the type name in a {@code ClassName.this.field}
547     *                            reference, or null if not that form.
548     * @param bareThisQualified   true for a {@code this.field} reference.
549     */
550    private record FieldUsage(String name, List<Map<String, DetailAST>> ancestorFieldMaps,
551                              List<String> ancestorTypeNames, String qualifierTypeName,
552                              boolean bareThisQualified) {
553    }
554
555}