• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pmd / pmd / 415

27 Feb 2026 12:39PM UTC coverage: 79.038% (+0.03%) from 79.004%
415

push

github

adangel
[release] Prepare next development version

18604 of 24437 branches covered (76.13%)

Branch coverage included in aggregate %.

40598 of 50466 relevant lines covered (80.45%)

0.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.64
/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/symbols/internal/ast/AstClassSym.java
1
/*
2
 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
3
 */
4

5
package net.sourceforge.pmd.lang.java.symbols.internal.ast;
6

7
import static net.sourceforge.pmd.util.CollectionUtil.setOf;
8

9
import java.lang.reflect.Modifier;
10
import java.util.ArrayList;
11
import java.util.Collections;
12
import java.util.List;
13
import java.util.Objects;
14

15
import org.checkerframework.checker.nullness.qual.NonNull;
16
import org.checkerframework.checker.nullness.qual.Nullable;
17
import org.pcollections.HashTreePSet;
18
import org.pcollections.PSet;
19

20
import net.sourceforge.pmd.lang.ast.NodeStream;
21
import net.sourceforge.pmd.lang.java.ast.ASTAnnotation;
22
import net.sourceforge.pmd.lang.java.ast.ASTAssignableExpr.ASTNamedReferenceExpr;
23
import net.sourceforge.pmd.lang.java.ast.ASTBodyDeclaration;
24
import net.sourceforge.pmd.lang.java.ast.ASTClassDeclaration;
25
import net.sourceforge.pmd.lang.java.ast.ASTClassType;
26
import net.sourceforge.pmd.lang.java.ast.ASTConstructorCall;
27
import net.sourceforge.pmd.lang.java.ast.ASTConstructorDeclaration;
28
import net.sourceforge.pmd.lang.java.ast.ASTEnumConstant;
29
import net.sourceforge.pmd.lang.java.ast.ASTFieldDeclaration;
30
import net.sourceforge.pmd.lang.java.ast.ASTMemberValuePair;
31
import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclaration;
32
import net.sourceforge.pmd.lang.java.ast.ASTPermitsList;
33
import net.sourceforge.pmd.lang.java.ast.ASTRecordComponent;
34
import net.sourceforge.pmd.lang.java.ast.ASTRecordComponentList;
35
import net.sourceforge.pmd.lang.java.ast.ASTTypeDeclaration;
36
import net.sourceforge.pmd.lang.java.ast.ASTVariableId;
37
import net.sourceforge.pmd.lang.java.ast.InternalApiBridge;
38
import net.sourceforge.pmd.lang.java.ast.JModifier;
39
import net.sourceforge.pmd.lang.java.ast.internal.JavaAstUtils;
40
import net.sourceforge.pmd.lang.java.internal.JavaAstProcessor;
41
import net.sourceforge.pmd.lang.java.symbols.JClassSymbol;
42
import net.sourceforge.pmd.lang.java.symbols.JConstructorSymbol;
43
import net.sourceforge.pmd.lang.java.symbols.JElementSymbol;
44
import net.sourceforge.pmd.lang.java.symbols.JExecutableSymbol;
45
import net.sourceforge.pmd.lang.java.symbols.JFieldSymbol;
46
import net.sourceforge.pmd.lang.java.symbols.JMethodSymbol;
47
import net.sourceforge.pmd.lang.java.symbols.JRecordComponentSymbol;
48
import net.sourceforge.pmd.lang.java.symbols.JTypeDeclSymbol;
49
import net.sourceforge.pmd.lang.java.symbols.JTypeParameterOwnerSymbol;
50
import net.sourceforge.pmd.lang.java.symbols.internal.ImplicitMemberSymbols;
51
import net.sourceforge.pmd.lang.java.types.JClassType;
52
import net.sourceforge.pmd.lang.java.types.JTypeMirror;
53
import net.sourceforge.pmd.lang.java.types.Substitution;
54
import net.sourceforge.pmd.lang.java.types.TypeOps;
55
import net.sourceforge.pmd.lang.java.types.TypeSystem;
56
import net.sourceforge.pmd.util.CollectionUtil;
57

58

59
final class AstClassSym
1✔
60
    extends AbstractAstTParamOwner<ASTTypeDeclaration>
61
    implements JClassSymbol {
62
    private final @Nullable JTypeParameterOwnerSymbol enclosing;
63
    private final List<JClassSymbol> declaredClasses;
64
    private final List<JMethodSymbol> declaredMethods;
65
    private final List<JConstructorSymbol> declaredCtors;
66
    private final List<JFieldSymbol> declaredFields;
67
    private final List<JFieldSymbol> enumConstants; // subset of declaredFields
68
    private final List<JRecordComponentSymbol> recordComponents;
69
    private final PSet<String> annotAttributes;
70

71
    private List<JClassSymbol> permittedSubclasses;
72

73
    AstClassSym(ASTTypeDeclaration node,
74
                AstSymFactory factory,
75
                @Nullable JTypeParameterOwnerSymbol enclosing) {
76
        super(node, factory);
1✔
77
        this.enclosing = enclosing;
1✔
78

79
        // evaluate everything strictly
80
        // this populates symbols on the relevant AST nodes
81

82
        final List<JClassSymbol> myClasses = new ArrayList<>();
1✔
83
        final List<JMethodSymbol> myMethods = new ArrayList<>();
1✔
84
        final List<JConstructorSymbol> myCtors = new ArrayList<>();
1✔
85
        final List<JFieldSymbol> myFields = new ArrayList<>();
1✔
86
        final List<JFieldSymbol> enumConstants;
87
        final List<JRecordComponentSymbol> recordComponents;
88

89
        if (isRecord()) {
1✔
90
            ASTRecordComponentList components = Objects.requireNonNull(node.getRecordComponents(),
1✔
91
                                                                       "Null component list for " + node);
92
            recordComponents = mapComponentsToMutableList(factory, components, myFields);
1✔
93

94
            JConstructorSymbol canonicalRecordCtor = ImplicitMemberSymbols.recordConstructor(this, recordComponents, components.isVarargs());
1✔
95
            myCtors.add(canonicalRecordCtor);
1✔
96
            InternalApiBridge.setSymbol(components, canonicalRecordCtor);
1✔
97

98
        } else {
1✔
99
            recordComponents = Collections.emptyList();
1✔
100
        }
101

102
        if (isEnum()) {
1✔
103
            enumConstants = new ArrayList<>();
1✔
104
            node.getEnumConstants()
1✔
105
                .forEach(constant -> {
1✔
106
                    AstFieldSym fieldSym = new AstFieldSym(constant.getVarId(), factory, this);
1✔
107
                    enumConstants.add(fieldSym);
1✔
108
                    myFields.add(fieldSym);
1✔
109
                });
1✔
110
        } else {
111
            enumConstants = null;
1✔
112
        }
113

114
        for (ASTBodyDeclaration dnode : node.getDeclarations()) {
1✔
115

116
            if (dnode instanceof ASTTypeDeclaration) {
1✔
117
                myClasses.add(new AstClassSym((ASTTypeDeclaration) dnode, factory, this));
1✔
118
            } else if (dnode instanceof ASTMethodDeclaration) {
1✔
119
                if (!recordComponents.isEmpty() && ((ASTMethodDeclaration) dnode).getArity() == 0) {
1✔
120
                    // filter out record component, so that the accessor is not generated
121
                    recordComponents.removeIf(f -> f.nameEquals(((ASTMethodDeclaration) dnode).getName()));
1✔
122
                }
123
                myMethods.add(new AstMethodSym((ASTMethodDeclaration) dnode, factory, this));
1✔
124
            } else if (dnode instanceof ASTConstructorDeclaration) {
1✔
125
                myCtors.add(new AstCtorSym((ASTConstructorDeclaration) dnode, factory, this));
1✔
126
            } else if (dnode instanceof ASTFieldDeclaration) {
1✔
127
                for (ASTVariableId varId : ((ASTFieldDeclaration) dnode).getVarIds()) {
1✔
128
                    myFields.add(new AstFieldSym(varId, factory, this));
1✔
129
                }
1✔
130
            }
131
        }
1✔
132
        
133

134
        if (!recordComponents.isEmpty()) {
1✔
135
            // then the recordsComponents contains all record components
136
            // for which we must synthesize an accessor (explicitly declared
137
            // accessors have been filtered out)
138
            for (JRecordComponentSymbol component : recordComponents) {
1✔
139
                myMethods.add(ImplicitMemberSymbols.recordAccessor(this, component));
1✔
140
            }
1✔
141
        }
142

143
        if (myCtors.isEmpty() && isClass() && !isAnonymousClass()) {
1✔
144
            myCtors.add(ImplicitMemberSymbols.defaultCtor(this));
1✔
145
        }
146

147
        if (this.isEnum()) {
1✔
148
            myMethods.add(ImplicitMemberSymbols.enumValues(this));
1✔
149
            myMethods.add(ImplicitMemberSymbols.enumValueOf(this));
1✔
150
        }
151

152

153
        this.declaredClasses = myClasses;
1✔
154
        this.declaredMethods = myMethods;
1✔
155
        this.declaredCtors = myCtors;
1✔
156
        this.declaredFields = myFields;
1✔
157
        this.enumConstants = CollectionUtil.makeUnmodifiableAndNonNull(enumConstants);
1✔
158
        this.recordComponents = CollectionUtil.makeUnmodifiableAndNonNull(recordComponents);
1✔
159
        this.annotAttributes = isAnnotation()
1✔
160
                               ? getDeclaredMethods().stream().filter(JMethodSymbol::isAnnotationAttribute).map(JElementSymbol::getSimpleName).collect(CollectionUtil.toPersistentSet())
1✔
161
                               : HashTreePSet.empty();
1✔
162
    }
1✔
163

164
    public void processLombok(JavaAstProcessor processor) {
165
        final String LOMBOK_SLF4J = "lombok.extern.slf4j.Slf4j";
1✔
166
        final String LOMBOK_GETTER = "lombok.Getter";
1✔
167
        final String LOMBOK_DATA = "lombok.Data";
1✔
168
        final String LOMBOK_VALUE = "lombok.Value";
1✔
169

170
        if (node.isAnnotationPresent(LOMBOK_SLF4J)) {
1✔
171
            declaredFields.add(ImplicitMemberSymbols.lombokSlf4jLoggerField(this, processor));
1✔
172
        }
173

174
        declaredFields.stream()
1✔
175
                .filter(f -> !f.isStatic())
1✔
176
                .map(f -> f.tryGetNode())
1✔
177
                .filter(Objects::nonNull)
1✔
178
                .filter(varId -> !varId.isRecordComponent())
1✔
179
                .forEach(varId -> {
1✔
180
                    ASTAnnotation fieldGetter = varId.getAnnotation(LOMBOK_GETTER);
1✔
181
                    int accessModifier = -1;
1✔
182

183
                    if (fieldGetter != null) {
1✔
184
                        accessModifier = getLombokAccessModifier(fieldGetter);
1✔
185
                    } else {
186
                        ASTAnnotation classGetter = node.getAnnotation(LOMBOK_GETTER);
1✔
187
                        if (classGetter != null) {
1✔
188
                            accessModifier = getLombokAccessModifier(classGetter);
1✔
189
                        } else if (JavaAstUtils.hasAnyAnnotation(node, setOf(LOMBOK_DATA, LOMBOK_VALUE))) {
1✔
190
                            accessModifier = Modifier.PUBLIC;
1✔
191
                        }
192
                    }
193

194
                    if (accessModifier != -1) {
1✔
195
                        JMethodSymbol getter = ImplicitMemberSymbols.lombokGetter(this,
1✔
196
                                (JFieldSymbol) varId.getSymbol(), accessModifier);
1✔
197
                        String getterName = getter.getSimpleName();
1✔
198
                        if (declaredMethods.stream().noneMatch(m -> m.getSimpleName().equals(getterName) && m.getArity() == 0)) {
1!
199
                            declaredMethods.add(getter);
1✔
200
                        }
201
                    }
202
                });
1✔
203
    }
1✔
204

205
    private int getLombokAccessModifier(ASTAnnotation annot) {
206
        return annot.getFlatValue(ASTMemberValuePair.VALUE_ATTR)
1✔
207
                .filter(v -> v instanceof ASTNamedReferenceExpr)
1✔
208
                .toStream()
1✔
209
                .map(ASTNamedReferenceExpr.class::cast)
1✔
210
                .map(ASTNamedReferenceExpr::getName)
1✔
211
                .findFirst()
1✔
212
                .map(accessLevel -> {
1✔
213
                    switch (accessLevel) {
1!
214
                    case "PROTECTED":
215
                        return Modifier.PROTECTED;
×
216
                    case "PACKAGE":
217
                        return 0;
×
218
                    case "PRIVATE":
219
                        return Modifier.PRIVATE;
1✔
220
                    case "NONE":
221
                        return -1; // special value for NONE, we'll use -1 as a flag or handle it
1✔
222
                    case "PUBLIC":
223
                    default:
224
                        return Modifier.PUBLIC;
1✔
225
                    }
226
                })
227
                .orElse(Modifier.PUBLIC); // lombok generates by default public getters
1✔
228
    }
229

230

231
    private List<JRecordComponentSymbol> mapComponentsToMutableList(AstSymFactory factory,
232
                                                          ASTRecordComponentList components,
233
                                                          List<JFieldSymbol> fieldSyms) {
234
        List<JRecordComponentSymbol> list = new ArrayList<>();
1✔
235
        for (ASTRecordComponent comp : components) {
1✔
236
            list.add(new AstRecordComponentSym(comp, factory, this));
1✔
237
            fieldSyms.add(new AstFieldSym(comp.getVarId(), factory, this));
1✔
238
        }
1✔
239
        return list;
1✔
240
    }
241

242
    @Override
243
    public @NonNull String getSimpleName() {
244
        return node.getSimpleName();
1✔
245
    }
246

247

248
    @Override
249
    public @NonNull String getBinaryName() {
250
        return node.getBinaryName();
1✔
251
    }
252

253
    @Override
254
    public @Nullable String getCanonicalName() {
255
        return node.getCanonicalName();
1✔
256
    }
257

258
    @Override
259
    public boolean isUnresolved() {
260
        return false;
1✔
261
    }
262

263
    @Override
264
    public @Nullable JClassSymbol getEnclosingClass() {
265
        if (enclosing instanceof JClassSymbol) {
1✔
266
            return (JClassSymbol) enclosing;
1✔
267
        } else if (enclosing instanceof JExecutableSymbol) {
1✔
268
            return enclosing.getEnclosingClass();
1✔
269
        }
270
        assert enclosing == null;
1!
271
        return null;
1✔
272
    }
273

274
    @Override
275
    public @Nullable JExecutableSymbol getEnclosingMethod() {
276
        return enclosing instanceof JExecutableSymbol ? (JExecutableSymbol) enclosing : null;
1✔
277
    }
278

279
    @Override
280
    public List<JClassSymbol> getDeclaredClasses() {
281
        return Collections.unmodifiableList(declaredClasses);
1✔
282
    }
283

284
    @Override
285
    public List<JMethodSymbol> getDeclaredMethods() {
286
        return Collections.unmodifiableList(declaredMethods);
1✔
287
    }
288

289
    @Override
290
    public List<JConstructorSymbol> getConstructors() {
291
        return Collections.unmodifiableList(declaredCtors);
1✔
292
    }
293

294
    @Override
295
    public List<JFieldSymbol> getDeclaredFields() {
296
        return Collections.unmodifiableList(declaredFields);
1✔
297
    }
298

299
    @Override
300
    public @NonNull List<JFieldSymbol> getEnumConstants() {
301
        return enumConstants;
1✔
302
    }
303

304
    @Override
305
    public @NonNull List<JRecordComponentSymbol> getRecordComponents() {
306
        return recordComponents;
1✔
307
    }
308

309

310
    @Override
311
    public List<JClassSymbol> getPermittedSubtypes() {
312
        // permitted subclasses are populated lazily because they require
313
        // symbol and type resolution to determine which types are sealed.
314
        if (permittedSubclasses == null) {
1✔
315
            ASTPermitsList permits = node.getPermitsClause();
1✔
316
            if (permits != null) {
1✔
317
                this.permittedSubclasses = permits.toList().stream().map(it -> {
1✔
318
                    JTypeDeclSymbol symbol = it.getTypeMirror().getSymbol();
1✔
319
                    if (symbol instanceof JClassSymbol) {
1!
320
                        return (JClassSymbol) symbol;
1✔
321
                    } else {
322
                        return null;
×
323
                    }
324
                }).filter(Objects::nonNull).collect(CollectionUtil.toUnmodifiableList());
1✔
325
            } else if (isSealed()) {
1✔
326
                // sealed with no permits clause: infer permitted
327
                this.permittedSubclasses = inferPermittedSubclasses();
1✔
328
            } else {
329
                this.permittedSubclasses = Collections.emptyList();
1✔
330
            }
331
        }
332
        return permittedSubclasses;
1✔
333
    }
334

335
    private List<JClassSymbol> inferPermittedSubclasses() {
336
        /*
337
         *  If the declaration of a sealed class C lacks a permits clause,
338
         * then the permitted direct subclasses of C are as follows:
339
         *
340
         *  1. If C is not an enum class, then its permitted direct subclasses
341
         *     are those classes declared in the same compilation unit as C (§7.3)
342
         *     which have a canonical name (§6.7) and whose direct superclass is C.
343
         *
344
         *     That is, the permitted direct subclasses are inferred as the classes
345
         *     in the same compilation unit that specify C as their direct superclass.
346
         *     The requirement for a canonical name means that no local classes or
347
         *     anonymous classes will be considered.
348
         *
349
         *     It is a compile-time error if the declaration of a sealed class C lacks
350
         *     a permits clause and C has no permitted direct subclasses.
351
         *
352
         *  2. If C is an enum class, then its permitted direct subclasses, if any,
353
         *     are specified in §8.9.
354
         */
355
        if (!isEnum()) {
1!
356
            boolean isInterface = isInterface();
1✔
357
            List<JClassSymbol> list = node
1✔
358
                .getRoot().descendants(ASTTypeDeclaration.class).crossFindBoundaries()
1✔
359
                .filter(it -> it.getCanonicalName() != null)
1✔
360
                .filter(it -> {
1✔
361
                    if (isInterface) {
1✔
362
                        return it.getSuperInterfaceTypeNodes().any(ty -> Objects.equals(ty.getTypeMirror().getSymbol(), this));
1✔
363
                    }
364
                    return NodeStream.of(it.getSuperClassTypeNode()).any(ty -> Objects.equals(ty.getTypeMirror().getSymbol(), this));
1✔
365
                }).toList(ASTTypeDeclaration::getSymbol);
1✔
366
            return Collections.unmodifiableList(list);
1✔
367
        }
368
        return Collections.emptyList();
×
369
    }
370

371
    @Override
372
    public boolean isSealed() {
373
        return node.hasModifiers(JModifier.SEALED);
1✔
374
    }
375

376
    @Override
377
    public @Nullable JClassType getSuperclassType(Substitution substitution) {
378
        TypeSystem ts = getTypeSystem();
1✔
379

380
        if (node.isEnum()) {
1✔
381

382
            return factory.enumSuperclass(this);
1✔
383

384
        } else if (node instanceof ASTClassDeclaration) {
1✔
385

386
            ASTClassType superClass = node.getSuperClassTypeNode();
1✔
387
            return superClass == null
1✔
388
                   ? ts.OBJECT
1✔
389
                   // this cast relies on the fact that the superclass is not a type variable
390
                   : (JClassType) TypeOps.subst(superClass.getTypeMirror(), substitution);
1✔
391

392
        } else if (isAnonymousClass()) {
1✔
393

394
            if (node.getParent() instanceof ASTEnumConstant) {
1✔
395

396
                return node.getEnclosingType().getTypeMirror().subst(substitution);
1✔
397

398
            } else if (node.getParent() instanceof ASTConstructorCall) {
1!
399

400
                @NonNull JTypeMirror sym = ((ASTConstructorCall) node.getParent()).getTypeMirror();
1✔
401

402
                return sym instanceof JClassType && !sym.isInterface()
1!
403
                       ? (JClassType) sym
1✔
404
                       : factory.types().OBJECT;
1✔
405
            }
406

407
        } else if (isRecord()) {
1✔
408

409
            return factory.recordSuperclass();
1✔
410

411
        } else if (isAnnotation()) {
1✔
412

413
            return ts.OBJECT;
1✔
414

415
        }
416

417
        return null;
1✔
418
    }
419

420
    @Override
421
    public @Nullable JClassSymbol getSuperclass() {
422
        // notice this relies on the fact that the extends clause
423
        // (or the type node of the constructor call, for an anonymous class),
424
        // was disambiguated early
425

426
        // We special case anonymous classes so as not to trigger overload resolution
427
        if (isAnonymousClass() && node.getParent() instanceof ASTConstructorCall) {
1✔
428

429
            @NonNull JTypeMirror sym = ((ASTConstructorCall) node.getParent()).getTypeNode().getTypeMirror();
1✔
430

431
            return sym instanceof JClassType && !sym.isInterface()
1!
432
                   ? ((JClassType) sym).getSymbol()
1✔
433
                   : factory.types().OBJECT.getSymbol();
1✔
434

435
        }
436

437
        JClassType sup = getSuperclassType(Substitution.EMPTY);
1✔
438
        return sup == null ? null : sup.getSymbol();
1!
439
    }
440

441
    @Override
442
    public List<JClassSymbol> getSuperInterfaces() {
443
        List<JClassSymbol> itfs = CollectionUtil.mapNotNull(
1✔
444
            node.getSuperInterfaceTypeNodes(),
1✔
445
            n -> {
446
                // we play safe here, but the symbol is either a JClassSymbol
447
                // or a JTypeParameterSymbol, with the latter case being a
448
                // compile-time error
449
                JTypeDeclSymbol sym = n.getTypeMirror().getSymbol();
1✔
450
                return sym instanceof JClassSymbol ? (JClassSymbol) sym : null;
1!
451
            }
452
        );
453
        if (isAnnotation()) {
1✔
454
            itfs = CollectionUtil.concatView(Collections.singletonList(factory.annotationSym()), itfs);
1✔
455
        }
456
        return itfs;
1✔
457
    }
458

459
    @Override
460
    public List<JClassType> getSuperInterfaceTypes(Substitution subst) {
461
        List<JClassType> itfs = CollectionUtil.map(node.getSuperInterfaceTypeNodes(), n -> (JClassType) TypeOps.subst(n.getTypeMirror(), subst));
1✔
462
        if (isAnnotation()) {
1✔
463
            itfs = CollectionUtil.concatView(Collections.singletonList(factory.annotationType()), itfs);
1✔
464
        }
465
        return itfs;
1✔
466
    }
467

468
    @Override
469
    public @Nullable JTypeDeclSymbol getArrayComponent() {
470
        return null;
×
471
    }
472

473
    @Override
474
    public boolean isArray() {
475
        return false;
1✔
476
    }
477

478
    @Override
479
    public boolean isPrimitive() {
480
        return false;
1✔
481
    }
482

483
    @Override
484
    public boolean isInterface() {
485
        return node.isInterface();
1✔
486
    }
487

488
    @Override
489
    public boolean isEnum() {
490
        return node.isEnum();
1✔
491
    }
492

493
    @Override
494
    public boolean isRecord() {
495
        return node.isRecord();
1✔
496
    }
497

498
    @Override
499
    public boolean isAnnotation() {
500
        return node.isAnnotation();
1✔
501
    }
502

503
    @Override
504
    public boolean isLocalClass() {
505
        return node.isLocal();
1✔
506
    }
507

508
    @Override
509
    public boolean isAnonymousClass() {
510
        return node.isAnonymous();
1✔
511
    }
512

513
    @Override
514
    public PSet<String> getAnnotationAttributeNames() {
515
        return annotAttributes;
×
516
    }
517
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc