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

pmd / pmd / 4502

13 Mar 2025 12:14PM UTC coverage: 83.089% (+3.4%) from 79.659%
4502

push

github

adangel
[java] Fix crash when parsing class for anonymous class (#5588)

Merge pull request #5588 from oowekyala:fix-anon-class-loading

1912 of 2411 branches covered (79.3%)

Branch coverage included in aggregate %.

29 of 33 new or added lines in 2 files covered. (87.88%)

30 existing lines in 6 files now uncovered.

4559 of 5377 relevant lines covered (84.79%)

14.17 hits per line

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

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

5

6
package net.sourceforge.pmd.lang.java.ast;
7

8
import static net.sourceforge.pmd.lang.java.symbols.table.internal.JavaSemanticErrors.CANNOT_RESOLVE_AMBIGUOUS_NAME;
9

10
import java.util.Iterator;
11

12
import org.checkerframework.checker.nullness.qual.NonNull;
13
import org.checkerframework.checker.nullness.qual.Nullable;
14

15
import net.sourceforge.pmd.lang.ast.Node;
16
import net.sourceforge.pmd.lang.ast.NodeStream;
17
import net.sourceforge.pmd.lang.ast.impl.javacc.JavaccToken;
18
import net.sourceforge.pmd.lang.java.ast.ASTAssignableExpr.ASTNamedReferenceExpr;
19
import net.sourceforge.pmd.lang.java.symbols.JClassSymbol;
20
import net.sourceforge.pmd.lang.java.symbols.JTypeDeclSymbol;
21
import net.sourceforge.pmd.lang.java.symbols.table.JSymbolTable;
22
import net.sourceforge.pmd.lang.java.symbols.table.internal.JavaSemanticErrors;
23
import net.sourceforge.pmd.lang.java.symbols.table.internal.ReferenceCtx;
24
import net.sourceforge.pmd.lang.java.types.JClassType;
25
import net.sourceforge.pmd.lang.java.types.JTypeMirror;
26
import net.sourceforge.pmd.lang.java.types.JVariableSig;
27
import net.sourceforge.pmd.lang.java.types.JVariableSig.FieldSig;
28
import net.sourceforge.pmd.lang.java.types.ast.internal.LazyTypeResolver;
29

30
/**
31
 * This implements name disambiguation following <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-6.html#jls-6.5.2">JLS§6.5.2</a>.
32
 * (see also <a href="https://docs.oracle.com/javase/specs/jls/se13/html/jls-6.html#jls-6.4.2">JLS§6.4.2 - Obscuring</a>)
33
 */
34
final class AstDisambiguationPass {
2✔
35

36
    private AstDisambiguationPass() {
37
        // façade
38
    }
39

40
    /**
41
     * Disambiguate the subtrees rooted at the given nodes. After this:
42
     * <ul>
43
     * <li>All ClassOrInterfaceTypes either see their ambiguous LHS
44
     * promoted to a ClassOrInterfaceType, or demoted to a package
45
     * name (removed from the tree)
46
     * <li>All ClassOrInterfaceTypes have a non-null symbol, even if
47
     * it is unresolved EXCEPT the ones of a qualified constructor call.
48
     * Those references are resolved lazily by {@link LazyTypeResolver},
49
     * because they depend on the full type resolution of the qualifier
50
     * expression, and that resolution may use things that are not yet
51
     * disambiguated
52
     * <li>There may still be AmbiguousNames, but only in expressions,
53
     * for the worst kind of ambiguity
54
     * </ul>
55
     */
56
    public static void disambigWithCtx(NodeStream<? extends JavaNode> nodes, ReferenceCtx ctx) {
57
        assert ctx != null : "Null context";
2!
58
        nodes.forEach(it -> it.acceptVisitor(DisambigVisitor.INSTANCE, ctx));
2✔
59
    }
2✔
60

61

62
    // those ignore JTypeParameterSymbol, for error handling logic to be uniform
63

64

65
    private static void checkParentIsMember(ReferenceCtx ctx, ASTClassType resolvedType, ASTClassType parent) {
66
        JTypeDeclSymbol sym = resolvedType.getReferencedSym();
2✔
67
        JClassSymbol parentClass = ctx.findTypeMember(sym, parent.getSimpleName(), parent);
2✔
68
        if (parentClass == null) {
2✔
69
            ctx.reportUnresolvedMember(parent, ReferenceCtx.Fallback.TYPE, parent.getSimpleName(), sym);
2✔
70
            int numTypeArgs = ASTList.sizeOrZero(parent.getTypeArguments());
2✔
71
            parentClass = ctx.makeUnresolvedReference(sym, parent.getSimpleName(), numTypeArgs);
2✔
72
        }
73
        parent.setSymbol(parentClass);
2✔
74
    }
2✔
75

76
    private static @Nullable JClassType enclosingType(JTypeMirror typeResult) {
77
        return typeResult instanceof JClassType ? ((JClassType) typeResult).getEnclosingType() : null;
2✔
78
    }
79

80
    private static final class DisambigVisitor extends JavaVisitorBase<ReferenceCtx, Void> {
81

82
        public static final DisambigVisitor INSTANCE = new DisambigVisitor();
2✔
83

84

85
        @Override
86
        protected Void visitChildren(Node node, ReferenceCtx data) {
87
            // note that this differs from the default impl, because
88
            // the default declares last = node.getNumChildren()
89
            // at the beginning of the loop, but in this visitor the
90
            // number of children may change.
91
            for (int i = 0; i < node.getNumChildren(); i++) {
2✔
92
                node.getChild(i).acceptVisitor(this, data);
2✔
93
            }
94
            return null;
2✔
95
        }
96

97
        @Override
98
        public Void visitTypeDecl(ASTTypeDeclaration node, ReferenceCtx data) {
99
            // since type headers are disambiguated early it doesn't matter
100
            // if the context is inaccurate in type headers
101
            return visitChildren(node, data.scopeDownToNested(node.getSymbol()));
2✔
102
        }
103

104
        @Override
105
        public Void visit(ASTAmbiguousName name, ReferenceCtx processor) {
106
            if (name.wasProcessed()) {
2✔
107
                // don't redo analysis
108
                return null;
2✔
109
            }
110

111
            JSymbolTable symbolTable = name.getSymbolTable();
2✔
112
            assert symbolTable != null : "Symbol tables haven't been set yet??";
2!
113

114
            boolean isPackageOrTypeOnly;
115
            if (name.getParent() instanceof ASTClassType) {
2✔
116
                isPackageOrTypeOnly = true;
2✔
117
            } else if (name.getParent() instanceof ASTExpression) {
2!
118
                isPackageOrTypeOnly = false;
2✔
119
            } else {
120
                throw new AssertionError("Unrecognised context for ambiguous name: " + name.getParent());
×
121
            }
122

123
            // do resolve
124
            JavaNode resolved = startResolve(name, processor, isPackageOrTypeOnly);
2✔
125

126
            // finish
127
            assert !isPackageOrTypeOnly
2!
128
                || resolved instanceof ASTTypeExpression
129
                || resolved instanceof ASTAmbiguousName
130
                : "Unexpected result " + resolved + " for PackageOrTypeName resolution";
131

132
            if (isPackageOrTypeOnly && resolved instanceof ASTTypeExpression) {
2✔
133
                // unambiguous, we just have to check that the parent is a member of the enclosing type
134

135
                ASTClassType resolvedType = (ASTClassType) ((ASTTypeExpression) resolved).getTypeNode();
2✔
136
                resolved = resolvedType;
2✔
137
                ASTClassType parent = (ASTClassType) name.getParent();
2✔
138

139
                checkParentIsMember(processor, resolvedType, parent);
2✔
140
            }
141

142
            if (resolved != name) { // NOPMD - intentional check for reference equality
2✔
143
                ((AbstractJavaNode) name.getParent()).setChild((AbstractJavaNode) resolved, name.getIndexInParent());
2✔
144
            }
145

146
            return null;
2✔
147
        }
148

149
        @Override
150
        public Void visit(ASTClassType type, ReferenceCtx ctx) {
151

152
            if (type.getReferencedSym() != null) {
2✔
153
                return null;
2✔
154
            }
155

156
            if (type.getFirstChild() instanceof ASTAmbiguousName) {
2✔
157
                type.getFirstChild().acceptVisitor(this, ctx);
2✔
158
            }
159

160
            // revisit children, which may have changed
161
            visitChildren(type, ctx);
2✔
162

163
            if (type.getReferencedSym() != null) {
2✔
164
                postProcess(type, ctx);
2✔
165
                return null;
2✔
166
            }
167

168
            ASTClassType lhsType = type.getQualifier();
2✔
169
            if (lhsType != null) {
2✔
170
                JTypeDeclSymbol lhsSym = lhsType.getReferencedSym();
2✔
171
                assert lhsSym != null : "Unresolved LHS for " + type;
2!
172
                checkParentIsMember(ctx, lhsType, type);
2✔
173
            } else {
2✔
174
                if (type.getParent() instanceof ASTConstructorCall
2✔
175
                    && ((ASTConstructorCall) type.getParent()).isQualifiedInstanceCreation()) {
2✔
176
                    // Leave the reference null, this is handled lazily,
177
                    // because the interaction it depends on the type of
178
                    // the qualifier
179
                    return null;
2✔
180
                }
181

182
                if (type.getReferencedSym() == null) {
2!
183
                    setClassSymbolIfNoQualifier(type, ctx);
2✔
184
                }
185
            }
186

187
            assert type.getReferencedSym() != null : "Null symbol for " + type;
2!
188

189
            postProcess(type, ctx);
2✔
190
            return null;
2✔
191
        }
192

193
        private static void setClassSymbolIfNoQualifier(ASTClassType type, ReferenceCtx ctx) {
194
            final JTypeMirror resolved = ctx.resolveSingleTypeName(type.getSymbolTable(), type.getSimpleName(), type);
2✔
195
            JTypeDeclSymbol sym;
196
            if (resolved == null) {
2✔
197
                ctx.reportCannotResolveSymbol(type, type.getSimpleName());
2✔
198
                sym = setArity(type, ctx, type.getSimpleName());
2✔
199
            } else {
200
                sym = resolved.getSymbol();
2✔
201
                if (sym.isUnresolved()) {
2✔
202
                    sym = setArity(type, ctx, ((JClassSymbol) sym).getCanonicalName());
2✔
203
                }
204
            }
205
            type.setSymbol(sym);
2✔
206
            type.setImplicitEnclosing(enclosingType(resolved));
2✔
207
        }
2✔
208

209
        private void postProcess(ASTClassType type, ReferenceCtx ctx) {
210
            JTypeDeclSymbol sym = type.getReferencedSym();
2✔
211
            if (type.getParent() instanceof ASTAnnotation) {
2✔
212
                if (!(sym instanceof JClassSymbol && (sym.isUnresolved() || ((JClassSymbol) sym).isAnnotation()))) {
2✔
213
                    ctx.getLogger().warning(type, JavaSemanticErrors.EXPECTED_ANNOTATION_TYPE);
2✔
214
                }
215
                return;
2✔
216
            }
217

218
            int actualArity = ASTList.sizeOrZero(type.getTypeArguments());
2✔
219
            int expectedArity = sym instanceof JClassSymbol ? ((JClassSymbol) sym).getTypeParameterCount() : 0;
2✔
220
            if (actualArity != 0 && actualArity != expectedArity) {
2✔
221
                ctx.getLogger().warning(type, JavaSemanticErrors.MALFORMED_GENERIC_TYPE, expectedArity, actualArity);
2✔
222
            }
223
        }
2✔
224

225
        private static @NonNull JTypeDeclSymbol setArity(ASTClassType type, ReferenceCtx ctx, String canonicalName) {
226
            int arity = ASTList.sizeOrZero(type.getTypeArguments());
2✔
227
            return ctx.makeUnresolvedReference(canonicalName, arity);
2✔
228
        }
229

230
        /*
231

232
           This is implemented as a set of mutually recursive methods
233
           that act as a kind of automaton. State transitions:
234

235
                        +-----+       +--+        +--+
236
                        |     |       |  |        |  |
237
           +-----+      +     v       +  v        +  v
238
           |START+----> PACKAGE +---> TYPE +----> EXPR
239
           +-----+                     ^           ^
240
             |                         |           |
241
             +-------------------------------------+
242

243
           Not pictured are the error transitions.
244
           Only Type & Expr are valid exit states.
245
         */
246

247
        /**
248
         * Resolve an ambiguous name occurring in an expression context.
249
         * Returns the expression to which the name was resolved. If the
250
         * name is a type, this is a {@link ASTTypeExpression}, otherwise
251
         * it could be a {@link ASTFieldAccess} or {@link ASTVariableAccess},
252
         * and in the worst case, the original {@link ASTAmbiguousName}.
253
         */
254
        private static ASTExpression startResolve(ASTAmbiguousName name, ReferenceCtx ctx, boolean isPackageOrTypeOnly) {
255
            Iterator<JavaccToken> tokens = name.tokens().iterator();
2✔
256
            JavaccToken firstIdent = tokens.next();
2✔
257
            TokenUtils.expectKind(firstIdent, JavaTokenKinds.IDENTIFIER);
2✔
258

259
            JSymbolTable symTable = name.getSymbolTable();
2✔
260

261
            String firstImage = firstIdent.getImage();
2✔
262

263
            if (!isPackageOrTypeOnly) {
2✔
264
                // first test if the leftmost segment is an expression
265
                JVariableSig varResult = symTable.variables().resolveFirst(firstImage);
2✔
266

267
                if (varResult != null) {
2✔
268
                    return resolveExpr(null, varResult, firstIdent, tokens, ctx);
2✔
269
                }
270
            }
271

272
            // otherwise, test if it is a type name
273

274
            JTypeMirror typeResult = ctx.resolveSingleTypeName(symTable, firstImage, name);
2✔
275

276
            if (typeResult != null) {
2✔
277
                JClassType enclosing = enclosingType(typeResult);
2✔
278
                return resolveType(null, enclosing, typeResult.getSymbol(), false, firstIdent, tokens, name, isPackageOrTypeOnly, ctx);
2✔
279
            }
280

281
            // otherwise, first is reclassified as package name.
282
            return resolvePackage(firstIdent, new StringBuilder(firstImage), tokens, name, isPackageOrTypeOnly, ctx);
2✔
283
        }
284

285

286
        /**
287
         * Classify the given [identifier] as an expression name. This
288
         * produces a FieldAccess/VariableAccess, depending on whether there is a qualifier.
289
         * The remaining token chain is reclassified as a sequence of
290
         * field accesses.
291
         *
292
         * TODO Check the field accesses are legal
293
         *  Also must filter by visibility
294
         */
295
        private static ASTExpression resolveExpr(@Nullable ASTExpression qualifier, // lhs
296
                                                 @Nullable JVariableSig varSym,     // signature, only set if this is the leftmost access
297
                                                 JavaccToken identifier,            // identifier for the field/var name
298
                                                 Iterator<JavaccToken> remaining,   // rest of tokens, starting with following '.'
299
                                                 ReferenceCtx ctx) {
300

301
            TokenUtils.expectKind(identifier, JavaTokenKinds.IDENTIFIER);
2✔
302

303
            ASTNamedReferenceExpr var;
304
            if (qualifier == null) {
2✔
305
                ASTVariableAccess varAccess = new ASTVariableAccess(identifier);
2✔
306
                varAccess.setTypedSym(varSym);
2✔
307
                var = varAccess;
2✔
308
            } else {
2✔
309
                ASTFieldAccess fieldAccess = new ASTFieldAccess(qualifier, identifier);
2✔
310
                fieldAccess.setTypedSym((FieldSig) varSym);
2✔
311
                var = fieldAccess;
2✔
312
            }
313

314

315
            if (!remaining.hasNext()) { // done
2✔
316
                return var;
2✔
317
            }
318

319
            JavaccToken nextIdent = skipToNextIdent(remaining);
2✔
320

321
            // following must also be expressions (field accesses)
322
            // we can't assert that for now, as symbols lack type information
323

324
            return resolveExpr(var, null, nextIdent, remaining, ctx);
2✔
325
        }
326

327
        /**
328
         * Classify the given [identifier] as a reference to the [sym].
329
         * This produces a ClassOrInterfaceType with the given [image] (which
330
         * may be prepended by a package name, or otherwise is just a simple name).
331
         * We then lookup the following identifier, and take a decision:
332
         * <ul>
333
         * <li>If there is a field with the given name in [classSym],
334
         * then the remaining tokens are reclassified as expression names
335
         * <li>Otherwise, if there is a member type with the given name
336
         * in [classSym], then the remaining segment is classified as a
337
         * type name (recursive call to this procedure)
338
         * <li>Otherwise, normally a compile-time error occurs. We instead
339
         * log a warning and treat it as a field access.
340
         * </ul>
341
         *
342
         * @param isPackageOrTypeOnly If true, expressions are disallowed by the context, so we don't check fields
343
         */
344
        private static ASTExpression resolveType(final @Nullable ASTClassType qualifier, // lhs
345
                                                 final @Nullable JClassType implicitEnclosing,      // enclosing type, if it is implicitly inherited
346
                                                 final JTypeDeclSymbol sym,                         // symbol for the type
347
                                                 final boolean isFqcn,                              // whether this is a fully-qualified name
348
                                                 final JavaccToken identifier,                      // ident of the simple name of the symbol
349
                                                 final Iterator<JavaccToken> remaining,             // rest of tokens, starting with following '.'
350
                                                 final ASTAmbiguousName ambig,                      // original ambiguous name
351
                                                 final boolean isPackageOrTypeOnly,
352
                                                 final ReferenceCtx ctx) {
353

354
            TokenUtils.expectKind(identifier, JavaTokenKinds.IDENTIFIER);
2✔
355

356
            final ASTClassType type = new ASTClassType(qualifier, isFqcn, ambig.getFirstToken(), identifier);
2✔
357
            type.setSymbol(sym);
2✔
358
            type.setImplicitEnclosing(implicitEnclosing);
2✔
359

360
            if (!remaining.hasNext()) { // done
2✔
361
                return new ASTTypeExpression(type);
2✔
362
            }
363

364
            final JavaccToken nextIdent = skipToNextIdent(remaining);
2✔
365
            final String nextSimpleName = nextIdent.getImage();
2✔
366

367
            if (!isPackageOrTypeOnly) {
2!
368
                @Nullable FieldSig field = ctx.findStaticField(sym, nextSimpleName);
2✔
369
                if (field != null) {
2✔
370
                    // todo check field is static
371
                    ASTTypeExpression typeExpr = new ASTTypeExpression(type);
2✔
372
                    return resolveExpr(typeExpr, field, nextIdent, remaining, ctx);
2✔
373
                }
374
            }
375

376
            JClassSymbol inner = ctx.findTypeMember(sym, nextSimpleName, ambig);
2✔
377

378
            if (inner == null && isPackageOrTypeOnly) {
2!
379
                // normally compile-time error, continue by considering it an unresolved inner type
UNCOV
380
                ctx.reportUnresolvedMember(ambig, ReferenceCtx.Fallback.TYPE, nextSimpleName, sym);
×
UNCOV
381
                inner = ctx.makeUnresolvedReference(sym, nextSimpleName, 0);
×
382
            }
383

384
            if (inner != null) {
2✔
385
                return resolveType(type, null, inner, false, nextIdent, remaining, ambig, isPackageOrTypeOnly, ctx);
2✔
386
            }
387

388
            // no inner type, yet we have a lhs that is a type...
389
            // this is normally a compile-time error
390
            // treat as unresolved field accesses, this is the smoothest for later type res
391

392
            // todo report on the specific token failing
393
            ctx.reportUnresolvedMember(ambig, ReferenceCtx.Fallback.FIELD_ACCESS, nextSimpleName, sym);
2✔
394
            ASTTypeExpression typeExpr = new ASTTypeExpression(type);
2✔
395
            return resolveExpr(typeExpr, null, nextIdent, remaining, ctx); // this will chain for the rest of the name
2✔
396
        }
397

398
        /**
399
         * Classify the given [identifier] as a package name. This means, that
400
         * we look ahead into the [remaining] tokens, and try to find a class
401
         * by that name in the given package. Then:
402
         * <ul>
403
         * <li>If such a class exists, continue the classification with resolveType
404
         * <li>Otherwise, the looked ahead segment is itself reclassified as a package name
405
         * </ul>
406
         *
407
         * <p>If we consumed the entire name without finding a suitable
408
         * class, then we report it and return the original ambiguous name.
409
         */
410
        private static ASTExpression resolvePackage(JavaccToken identifier,
411
                                                    StringBuilder packageImage,
412
                                                    Iterator<JavaccToken> remaining,
413
                                                    ASTAmbiguousName ambig,
414
                                                    boolean isPackageOrTypeOnly,
415
                                                    ReferenceCtx ctx) {
416

417
            TokenUtils.expectKind(identifier, JavaTokenKinds.IDENTIFIER);
2✔
418

419
            if (!remaining.hasNext()) {
2✔
420
                if (isPackageOrTypeOnly) {
2✔
421
                    // There's one last segment to try, the parent of the ambiguous name
422
                    // This may only be because this ambiguous name is the package qualification of the parent type
423
                    forceResolveAsFullPackageNameOfParent(packageImage, ambig, ctx);
2✔
424
                    return ambig; // returning ambig makes the outer routine not replace
2✔
425
                }
426

427
                // then this name is unresolved, leave the ambiguous name in the tree
428
                // this only happens inside expressions
429
                ctx.getLogger().warning(ambig, CANNOT_RESOLVE_AMBIGUOUS_NAME, packageImage, ReferenceCtx.Fallback.AMBIGUOUS);
2✔
430
                ambig.setProcessed(); // signal that we don't want to retry resolving this
2✔
431
                return ambig;
2✔
432
            }
433

434
            JavaccToken nextIdent = skipToNextIdent(remaining);
2✔
435

436

437
            packageImage.append('.').append(nextIdent.getImage());
2✔
438
            String canonical = packageImage.toString();
2✔
439

440
            // Don't interpret periods as nested class separators (this will be handled by resolveType).
441
            // Otherwise lookup of a fully qualified name would be quadratic
442
            JClassSymbol nextClass = ctx.resolveClassFromBinaryName(canonical);
2✔
443

444
            if (nextClass != null) {
2✔
445
                return resolveType(null, null, nextClass, true, nextIdent, remaining, ambig, isPackageOrTypeOnly, ctx);
2✔
446
            } else {
447
                return resolvePackage(nextIdent, packageImage, remaining, ambig, isPackageOrTypeOnly, ctx);
2✔
448
            }
449
        }
450

451
        /**
452
         * Force resolution of the ambiguous name as a package name.
453
         * The parent type's image is set to a package name + simple name.
454
         */
455
        private static void forceResolveAsFullPackageNameOfParent(StringBuilder packageImage, ASTAmbiguousName ambig, ReferenceCtx ctx) {
456
            ASTClassType parent = (ASTClassType) ambig.getParent();
2✔
457

458
            packageImage.append('.').append(parent.getSimpleName());
2✔
459
            String fullName = packageImage.toString();
2✔
460
            JClassSymbol parentClass = ctx.resolveClassFromBinaryName(fullName);
2✔
461
            if (parentClass == null) {
2✔
462
                ctx.getLogger().warning(parent, CANNOT_RESOLVE_AMBIGUOUS_NAME, fullName, ReferenceCtx.Fallback.TYPE);
2✔
463
                parentClass = ctx.makeUnresolvedReference(fullName, ASTList.sizeOrZero(parent.getTypeArguments()));
2✔
464
            }
465
            parent.setSymbol(parentClass);
2✔
466
            parent.setFullyQualified();
2✔
467
            ambig.deleteInParent();
2✔
468
        }
2✔
469

470
        private static JavaccToken skipToNextIdent(Iterator<JavaccToken> remaining) {
471
            JavaccToken dot = remaining.next();
2✔
472
            TokenUtils.expectKind(dot, JavaTokenKinds.DOT);
2✔
473
            assert remaining.hasNext() : "Ambiguous name must end with an identifier";
2!
474
            return remaining.next();
2✔
475
        }
476
    }
477

478
}
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