All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.sourceforge.pmd.lang.java.ast.AstDisambiguationPass Maven / Gradle / Ivy

There is a newer version: 7.7.0
Show newest version
/*
 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
 */


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

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

import java.util.Iterator;

import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;

import net.sourceforge.pmd.lang.ast.Node;
import net.sourceforge.pmd.lang.ast.NodeStream;
import net.sourceforge.pmd.lang.ast.impl.javacc.JavaccToken;
import net.sourceforge.pmd.lang.java.ast.ASTAssignableExpr.ASTNamedReferenceExpr;
import net.sourceforge.pmd.lang.java.symbols.JClassSymbol;
import net.sourceforge.pmd.lang.java.symbols.JTypeDeclSymbol;
import net.sourceforge.pmd.lang.java.symbols.table.JSymbolTable;
import net.sourceforge.pmd.lang.java.symbols.table.internal.JavaSemanticErrors;
import net.sourceforge.pmd.lang.java.symbols.table.internal.ReferenceCtx;
import net.sourceforge.pmd.lang.java.types.JClassType;
import net.sourceforge.pmd.lang.java.types.JTypeMirror;
import net.sourceforge.pmd.lang.java.types.JVariableSig;
import net.sourceforge.pmd.lang.java.types.JVariableSig.FieldSig;
import net.sourceforge.pmd.lang.java.types.ast.internal.LazyTypeResolver;

/**
 * This implements name disambiguation following JLS§6.5.2.
 * (see also JLS§6.4.2 - Obscuring)
 */
final class AstDisambiguationPass {

    private AstDisambiguationPass() {
        // façade
    }

    /**
     * Disambiguate the subtrees rooted at the given nodes. After this:
     * 
    *
  • All ClassOrInterfaceTypes either see their ambiguous LHS * promoted to a ClassOrInterfaceType, or demoted to a package * name (removed from the tree) *
  • All ClassOrInterfaceTypes have a non-null symbol, even if * it is unresolved EXCEPT the ones of a qualified constructor call. * Those references are resolved lazily by {@link LazyTypeResolver}, * because they depend on the full type resolution of the qualifier * expression, and that resolution may use things that are not yet * disambiguated *
  • There may still be AmbiguousNames, but only in expressions, * for the worst kind of ambiguity *
*/ public static void disambigWithCtx(NodeStream nodes, ReferenceCtx ctx) { assert ctx != null : "Null context"; nodes.forEach(it -> it.acceptVisitor(DisambigVisitor.INSTANCE, ctx)); } // those ignore JTypeParameterSymbol, for error handling logic to be uniform private static void checkParentIsMember(ReferenceCtx ctx, ASTClassType resolvedType, ASTClassType parent) { JTypeDeclSymbol sym = resolvedType.getReferencedSym(); JClassSymbol parentClass = ctx.findTypeMember(sym, parent.getSimpleName(), parent); if (parentClass == null) { ctx.reportUnresolvedMember(parent, ReferenceCtx.Fallback.TYPE, parent.getSimpleName(), sym); int numTypeArgs = ASTList.sizeOrZero(parent.getTypeArguments()); parentClass = ctx.makeUnresolvedReference(sym, parent.getSimpleName(), numTypeArgs); } parent.setSymbol(parentClass); } private static @Nullable JClassType enclosingType(JTypeMirror typeResult) { return typeResult instanceof JClassType ? ((JClassType) typeResult).getEnclosingType() : null; } private static final class DisambigVisitor extends JavaVisitorBase { public static final DisambigVisitor INSTANCE = new DisambigVisitor(); @Override protected Void visitChildren(Node node, ReferenceCtx data) { // note that this differs from the default impl, because // the default declares last = node.getNumChildren() // at the beginning of the loop, but in this visitor the // number of children may change. for (int i = 0; i < node.getNumChildren(); i++) { node.getChild(i).acceptVisitor(this, data); } return null; } @Override public Void visitTypeDecl(ASTTypeDeclaration node, ReferenceCtx data) { // since type headers are disambiguated early it doesn't matter // if the context is inaccurate in type headers return visitChildren(node, data.scopeDownToNested(node.getSymbol())); } @Override public Void visit(ASTAmbiguousName name, ReferenceCtx processor) { if (name.wasProcessed()) { // don't redo analysis return null; } JSymbolTable symbolTable = name.getSymbolTable(); assert symbolTable != null : "Symbol tables haven't been set yet??"; boolean isPackageOrTypeOnly; if (name.getParent() instanceof ASTClassType) { isPackageOrTypeOnly = true; } else if (name.getParent() instanceof ASTExpression) { isPackageOrTypeOnly = false; } else { throw new AssertionError("Unrecognised context for ambiguous name: " + name.getParent()); } // do resolve JavaNode resolved = startResolve(name, processor, isPackageOrTypeOnly); // finish assert !isPackageOrTypeOnly || resolved instanceof ASTTypeExpression || resolved instanceof ASTAmbiguousName : "Unexpected result " + resolved + " for PackageOrTypeName resolution"; if (isPackageOrTypeOnly && resolved instanceof ASTTypeExpression) { // unambiguous, we just have to check that the parent is a member of the enclosing type ASTClassType resolvedType = (ASTClassType) ((ASTTypeExpression) resolved).getTypeNode(); resolved = resolvedType; ASTClassType parent = (ASTClassType) name.getParent(); checkParentIsMember(processor, resolvedType, parent); } if (resolved != name) { // NOPMD - intentional check for reference equality ((AbstractJavaNode) name.getParent()).setChild((AbstractJavaNode) resolved, name.getIndexInParent()); } return null; } @Override public Void visit(ASTClassType type, ReferenceCtx ctx) { if (type.getReferencedSym() != null) { return null; } if (type.getFirstChild() instanceof ASTAmbiguousName) { type.getFirstChild().acceptVisitor(this, ctx); } // revisit children, which may have changed visitChildren(type, ctx); if (type.getReferencedSym() != null) { postProcess(type, ctx); return null; } ASTClassType lhsType = type.getQualifier(); if (lhsType != null) { JTypeDeclSymbol lhsSym = lhsType.getReferencedSym(); assert lhsSym != null : "Unresolved LHS for " + type; checkParentIsMember(ctx, lhsType, type); } else { if (type.getParent() instanceof ASTConstructorCall && ((ASTConstructorCall) type.getParent()).isQualifiedInstanceCreation()) { // Leave the reference null, this is handled lazily, // because the interaction it depends on the type of // the qualifier return null; } if (type.getReferencedSym() == null) { setClassSymbolIfNoQualifier(type, ctx); } } assert type.getReferencedSym() != null : "Null symbol for " + type; postProcess(type, ctx); return null; } private static void setClassSymbolIfNoQualifier(ASTClassType type, ReferenceCtx ctx) { final JTypeMirror resolved = ctx.resolveSingleTypeName(type.getSymbolTable(), type.getSimpleName(), type); JTypeDeclSymbol sym; if (resolved == null) { ctx.reportCannotResolveSymbol(type, type.getSimpleName()); sym = setArity(type, ctx, type.getSimpleName()); } else { sym = resolved.getSymbol(); if (sym.isUnresolved()) { sym = setArity(type, ctx, ((JClassSymbol) sym).getCanonicalName()); } } type.setSymbol(sym); type.setImplicitEnclosing(enclosingType(resolved)); } private void postProcess(ASTClassType type, ReferenceCtx ctx) { JTypeDeclSymbol sym = type.getReferencedSym(); if (type.getParent() instanceof ASTAnnotation) { if (!(sym instanceof JClassSymbol && (sym.isUnresolved() || ((JClassSymbol) sym).isAnnotation()))) { ctx.getLogger().warning(type, JavaSemanticErrors.EXPECTED_ANNOTATION_TYPE); } return; } int actualArity = ASTList.sizeOrZero(type.getTypeArguments()); int expectedArity = sym instanceof JClassSymbol ? ((JClassSymbol) sym).getTypeParameterCount() : 0; if (actualArity != 0 && actualArity != expectedArity) { ctx.getLogger().warning(type, JavaSemanticErrors.MALFORMED_GENERIC_TYPE, expectedArity, actualArity); } } private static @NonNull JTypeDeclSymbol setArity(ASTClassType type, ReferenceCtx ctx, String canonicalName) { int arity = ASTList.sizeOrZero(type.getTypeArguments()); return ctx.makeUnresolvedReference(canonicalName, arity); } /* This is implemented as a set of mutually recursive methods that act as a kind of automaton. State transitions: +-----+ +--+ +--+ | | | | | | +-----+ + v + v + v |START+----> PACKAGE +---> TYPE +----> EXPR +-----+ ^ ^ | | | +-------------------------------------+ Not pictured are the error transitions. Only Type & Expr are valid exit states. */ /** * Resolve an ambiguous name occurring in an expression context. * Returns the expression to which the name was resolved. If the * name is a type, this is a {@link ASTTypeExpression}, otherwise * it could be a {@link ASTFieldAccess} or {@link ASTVariableAccess}, * and in the worst case, the original {@link ASTAmbiguousName}. */ private static ASTExpression startResolve(ASTAmbiguousName name, ReferenceCtx ctx, boolean isPackageOrTypeOnly) { Iterator tokens = name.tokens().iterator(); JavaccToken firstIdent = tokens.next(); TokenUtils.expectKind(firstIdent, JavaTokenKinds.IDENTIFIER); JSymbolTable symTable = name.getSymbolTable(); if (!isPackageOrTypeOnly) { // first test if the leftmost segment is an expression JVariableSig varResult = symTable.variables().resolveFirst(firstIdent.getImage()); if (varResult != null) { return resolveExpr(null, varResult, firstIdent, tokens, ctx); } } // otherwise, test if it is a type name JTypeMirror typeResult = ctx.resolveSingleTypeName(symTable, firstIdent.getImage(), name); if (typeResult != null) { JClassType enclosing = enclosingType(typeResult); return resolveType(null, enclosing, typeResult.getSymbol(), false, firstIdent, tokens, name, isPackageOrTypeOnly, ctx); } // otherwise, first is reclassified as package name. return resolvePackage(firstIdent, new StringBuilder(firstIdent.getImage()), tokens, name, isPackageOrTypeOnly, ctx); } /** * Classify the given [identifier] as an expression name. This * produces a FieldAccess/VariableAccess, depending on whether there is a qualifier. * The remaining token chain is reclassified as a sequence of * field accesses. * * TODO Check the field accesses are legal * Also must filter by visibility */ private static ASTExpression resolveExpr(@Nullable ASTExpression qualifier, // lhs @Nullable JVariableSig varSym, // signature, only set if this is the leftmost access JavaccToken identifier, // identifier for the field/var name Iterator remaining, // rest of tokens, starting with following '.' ReferenceCtx ctx) { TokenUtils.expectKind(identifier, JavaTokenKinds.IDENTIFIER); ASTNamedReferenceExpr var; if (qualifier == null) { ASTVariableAccess varAccess = new ASTVariableAccess(identifier); varAccess.setTypedSym(varSym); var = varAccess; } else { ASTFieldAccess fieldAccess = new ASTFieldAccess(qualifier, identifier); fieldAccess.setTypedSym((FieldSig) varSym); var = fieldAccess; } if (!remaining.hasNext()) { // done return var; } JavaccToken nextIdent = skipToNextIdent(remaining); // following must also be expressions (field accesses) // we can't assert that for now, as symbols lack type information return resolveExpr(var, null, nextIdent, remaining, ctx); } /** * Classify the given [identifier] as a reference to the [sym]. * This produces a ClassOrInterfaceType with the given [image] (which * may be prepended by a package name, or otherwise is just a simple name). * We then lookup the following identifier, and take a decision: *
    *
  • If there is a field with the given name in [classSym], * then the remaining tokens are reclassified as expression names *
  • Otherwise, if there is a member type with the given name * in [classSym], then the remaining segment is classified as a * type name (recursive call to this procedure) *
  • Otherwise, normally a compile-time error occurs. We instead * log a warning and treat it as a field access. *
* * @param isPackageOrTypeOnly If true, expressions are disallowed by the context, so we don't check fields */ private static ASTExpression resolveType(final @Nullable ASTClassType qualifier, // lhs final @Nullable JClassType implicitEnclosing, // enclosing type, if it is implicitly inherited final JTypeDeclSymbol sym, // symbol for the type final boolean isFqcn, // whether this is a fully-qualified name final JavaccToken identifier, // ident of the simple name of the symbol final Iterator remaining, // rest of tokens, starting with following '.' final ASTAmbiguousName ambig, // original ambiguous name final boolean isPackageOrTypeOnly, final ReferenceCtx ctx) { TokenUtils.expectKind(identifier, JavaTokenKinds.IDENTIFIER); final ASTClassType type = new ASTClassType(qualifier, isFqcn, ambig.getFirstToken(), identifier); type.setSymbol(sym); type.setImplicitEnclosing(implicitEnclosing); if (!remaining.hasNext()) { // done return new ASTTypeExpression(type); } final JavaccToken nextIdent = skipToNextIdent(remaining); final String nextSimpleName = nextIdent.getImage(); if (!isPackageOrTypeOnly) { @Nullable FieldSig field = ctx.findStaticField(sym, nextSimpleName); if (field != null) { // todo check field is static ASTTypeExpression typeExpr = new ASTTypeExpression(type); return resolveExpr(typeExpr, field, nextIdent, remaining, ctx); } } JClassSymbol inner = ctx.findTypeMember(sym, nextSimpleName, ambig); if (inner == null && isPackageOrTypeOnly) { // normally compile-time error, continue by considering it an unresolved inner type ctx.reportUnresolvedMember(ambig, ReferenceCtx.Fallback.TYPE, nextSimpleName, sym); inner = ctx.makeUnresolvedReference(sym, nextSimpleName, 0); } if (inner != null) { return resolveType(type, null, inner, false, nextIdent, remaining, ambig, isPackageOrTypeOnly, ctx); } // no inner type, yet we have a lhs that is a type... // this is normally a compile-time error // treat as unresolved field accesses, this is the smoothest for later type res // todo report on the specific token failing ctx.reportUnresolvedMember(ambig, ReferenceCtx.Fallback.FIELD_ACCESS, nextSimpleName, sym); ASTTypeExpression typeExpr = new ASTTypeExpression(type); return resolveExpr(typeExpr, null, nextIdent, remaining, ctx); // this will chain for the rest of the name } /** * Classify the given [identifier] as a package name. This means, that * we look ahead into the [remaining] tokens, and try to find a class * by that name in the given package. Then: *
    *
  • If such a class exists, continue the classification with resolveType *
  • Otherwise, the looked ahead segment is itself reclassified as a package name *
* *

If we consumed the entire name without finding a suitable * class, then we report it and return the original ambiguous name. */ private static ASTExpression resolvePackage(JavaccToken identifier, StringBuilder packageImage, Iterator remaining, ASTAmbiguousName ambig, boolean isPackageOrTypeOnly, ReferenceCtx ctx) { TokenUtils.expectKind(identifier, JavaTokenKinds.IDENTIFIER); if (!remaining.hasNext()) { if (isPackageOrTypeOnly) { // There's one last segment to try, the parent of the ambiguous name // This may only be because this ambiguous name is the package qualification of the parent type forceResolveAsFullPackageNameOfParent(packageImage, ambig, ctx); return ambig; // returning ambig makes the outer routine not replace } // then this name is unresolved, leave the ambiguous name in the tree // this only happens inside expressions ctx.getLogger().warning(ambig, CANNOT_RESOLVE_AMBIGUOUS_NAME, packageImage, ReferenceCtx.Fallback.AMBIGUOUS); ambig.setProcessed(); // signal that we don't want to retry resolving this return ambig; } JavaccToken nextIdent = skipToNextIdent(remaining); packageImage.append('.').append(nextIdent.getImage()); String canonical = packageImage.toString(); // Don't interpret periods as nested class separators (this will be handled by resolveType). // Otherwise lookup of a fully qualified name would be quadratic JClassSymbol nextClass = ctx.resolveClassFromBinaryName(canonical); if (nextClass != null) { return resolveType(null, null, nextClass, true, nextIdent, remaining, ambig, isPackageOrTypeOnly, ctx); } else { return resolvePackage(nextIdent, packageImage, remaining, ambig, isPackageOrTypeOnly, ctx); } } /** * Force resolution of the ambiguous name as a package name. * The parent type's image is set to a package name + simple name. */ private static void forceResolveAsFullPackageNameOfParent(StringBuilder packageImage, ASTAmbiguousName ambig, ReferenceCtx ctx) { ASTClassType parent = (ASTClassType) ambig.getParent(); packageImage.append('.').append(parent.getSimpleName()); String fullName = packageImage.toString(); JClassSymbol parentClass = ctx.resolveClassFromBinaryName(fullName); if (parentClass == null) { ctx.getLogger().warning(parent, CANNOT_RESOLVE_AMBIGUOUS_NAME, fullName, ReferenceCtx.Fallback.TYPE); parentClass = ctx.makeUnresolvedReference(fullName, ASTList.sizeOrZero(parent.getTypeArguments())); } parent.setSymbol(parentClass); parent.setFullyQualified(); ambig.deleteInParent(); } private static JavaccToken skipToNextIdent(Iterator remaining) { JavaccToken dot = remaining.next(); TokenUtils.expectKind(dot, JavaTokenKinds.DOT); assert remaining.hasNext() : "Ambiguous name must end with an identifier"; return remaining.next(); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy