/* * Copyright 2014 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.errorprone.bugpatterns.threadsafety; import static com.google.errorprone.bugpatterns.threadsafety.IllegalGuardedBy.checkGuardedBy; import com.google.common.base.Optional; import com.google.errorprone.VisitorState; import com.google.errorprone.bugpatterns.threadsafety.GuardedByExpression.Kind; import com.google.errorprone.util.ASTHelpers; import com.sun.source.tree.ClassTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.IdentifierTree; import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.ParenthesizedTree; import com.sun.source.util.SimpleTreeVisitor; import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Symbol.ClassSymbol; import com.sun.tools.javac.code.Types; import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.util.Names; import javax.lang.model.element.Name; /** * A binder from {@code @GuardedBy} annotations to {@link GuardedByExpression}s. * * @author cushon@google.com (Liam Miller-Cushon) */ public class GuardedByBinder { /** * Creates a {@link GuardedByExpression} from a bound AST node, or returns * {@code Optional.absent()} if the AST node doesn't correspond to a 'simple' * lock expression. */ public static Optional<GuardedByExpression> bindExpression(JCTree.JCExpression exp, VisitorState visitorState) { try { return Optional.of(bind( exp, BinderContext.of( ALREADY_BOUND_RESOLVER, ASTHelpers.getSymbol(visitorState.findEnclosing(ClassTree.class)), visitorState.getTypes(), Names.instance(visitorState.context)))); } catch (IllegalGuardedBy expected) { return Optional.absent(); } } /** * Creates a {@link GuardedByExpression} from a string, given the resolution context. */ static Optional<GuardedByExpression> bindString(String string, GuardedBySymbolResolver resolver) { try { return Optional.of(bind( GuardedByUtils.parseString(string, resolver.context()), BinderContext.of( resolver, resolver.enclosingClass(), Types.instance(resolver.context()), Names.instance(resolver.context())))); } catch (IllegalGuardedBy expected) { return Optional.absent(); } } private static class BinderContext { final Resolver resolver; final ClassSymbol thisClass; final Types types; final Names names; public BinderContext(Resolver resolver, ClassSymbol thisClass, Types types, Names names) { this.resolver = resolver; this.thisClass = thisClass; this.types = types; this.names = names; } public static BinderContext of(Resolver resolver, ClassSymbol thisClass, Types types, Names names) { return new BinderContext(resolver, thisClass, types, names); } } private static GuardedByExpression bind(JCTree.JCExpression exp, BinderContext context) { GuardedByExpression expr = BINDER.visit(exp, context); checkGuardedBy(expr != null, String.valueOf(exp)); checkGuardedBy(expr.kind() != Kind.TYPE_LITERAL, "Raw type literal: %s", exp); return expr; } /** * A context containing the information necessary to resolve a * {@link com.sun.tools.javac.code.Symbol} from an AST node. * * <p>Guard expressions can be bound from the string value of an {@code @GuardedBy} annotation, or * from an actual java expression. In the first case, the string is parsed into an AST which will * not have any semantic information attached. */ public interface Resolver { Symbol resolveIdentifier(IdentifierTree node); Symbol.MethodSymbol resolveMethod(MethodInvocationTree node, Name name); Symbol.MethodSymbol resolveMethod(MethodInvocationTree node, GuardedByExpression base, Name identifier); Symbol resolveSelect(GuardedByExpression base, MemberSelectTree node); Symbol resolveTypeLiteral(ExpressionTree expression); Symbol resolveEnclosingClass(ExpressionTree expression); } /** * A resolver for AST nodes that have already been bound by javac. */ static final Resolver ALREADY_BOUND_RESOLVER = new Resolver() { @Override public Symbol resolveIdentifier(IdentifierTree node) { return ASTHelpers.getSymbol(node); } @Override public Symbol.MethodSymbol resolveMethod(MethodInvocationTree node, Name name) { return ASTHelpers.getSymbol(node); } @Override public Symbol.MethodSymbol resolveMethod(MethodInvocationTree node, GuardedByExpression base, Name identifier) { return ASTHelpers.getSymbol(node); } @Override public Symbol resolveSelect(GuardedByExpression base, MemberSelectTree node) { return ASTHelpers.getSymbol(node); } @Override public Symbol resolveTypeLiteral(ExpressionTree expression) { return ASTHelpers.getSymbol(expression); } @Override public Symbol resolveEnclosingClass(ExpressionTree expression) { return ASTHelpers.getSymbol(expression); } }; private static final GuardedByExpression.Factory F = new GuardedByExpression.Factory(); private static final SimpleTreeVisitor<GuardedByExpression, BinderContext> BINDER = new SimpleTreeVisitor<GuardedByExpression, BinderContext>() { @Override public GuardedByExpression visitMethodInvocation( MethodInvocationTree node, BinderContext context) { checkGuardedBy( node.getArguments().isEmpty() && node.getTypeArguments().isEmpty(), "Only nullary methods are allowed."); ExpressionTree methodSelect = node.getMethodSelect(); switch (methodSelect.getKind()) { case IDENTIFIER: { IdentifierTree identifier = (IdentifierTree) methodSelect; Symbol.MethodSymbol method = context.resolver.resolveMethod(node, identifier.getName()); checkGuardedBy(method != null, identifier.toString()); return bindSelect(computeBase(context, method), method); } case MEMBER_SELECT: { MemberSelectTree select = (MemberSelectTree) methodSelect; GuardedByExpression base = visit(select.getExpression(), context); checkGuardedBy(base != null, select.getExpression().toString()); Symbol.MethodSymbol method = context.resolver.resolveMethod(node, base, select.getIdentifier()); checkGuardedBy(method != null, select.toString()); return bindSelect(normalizeBase(context, method, base), method); } default: throw new IllegalGuardedBy(methodSelect.getKind().toString()); } } @Override public GuardedByExpression visitMemberSelect(MemberSelectTree node, BinderContext context) { String name = node.getIdentifier().toString(); if (name.equals("this")) { Symbol base = context.resolver.resolveEnclosingClass(node.getExpression()); if (context.thisClass == base) { return F.thisliteral(); } return F.qualifiedThis(context.names, context.thisClass, base); } if (name.equals("class")) { Symbol base = context.resolver.resolveTypeLiteral(node.getExpression()); return F.classLiteral(base); } GuardedByExpression base = visit(node.getExpression(), context); checkGuardedBy(base != null, "Bad expression: %s", node.getExpression()); Symbol sym = context.resolver.resolveSelect(base, node); checkGuardedBy(sym != null, "Could not resolve: %s", node); // TODO(cushon): allow MethodSymbol here once clean-up is done checkGuardedBy( sym instanceof Symbol.VarSymbol /* || sym instanceof Symbol.MethodSymbol*/, "Bad member symbol: %s", sym.getClass()); return bindSelect(normalizeBase(context, sym, base), sym); } private GuardedByExpression bindSelect(GuardedByExpression base, Symbol sym) { if (base.kind().equals(Kind.TYPE_LITERAL) && !sym.isStatic()) { throw new IllegalGuardedBy("Instance access on static: " + base + ", " + sym); } // TODO(cushon) - forbid static access on instance? return F.select(base, sym); } @Override public GuardedByExpression visitIdentifier(IdentifierTree node, BinderContext context) { Symbol symbol = context.resolver.resolveIdentifier(node); checkGuardedBy(symbol != null, "Could not resolve %s", node); if (symbol instanceof Symbol.VarSymbol) { Symbol.VarSymbol varSymbol = (Symbol.VarSymbol) symbol; switch (varSymbol.getKind()) { case LOCAL_VARIABLE: case PARAMETER: return F.localVariable(varSymbol); case FIELD: { if (symbol.name.contentEquals("this")) { return F.thisliteral(); } return F.select(computeBase(context, varSymbol), varSymbol); } default: throw new IllegalGuardedBy(varSymbol.getKind().toString()); } } else if (symbol instanceof Symbol.MethodSymbol) { Symbol.MethodSymbol methodSymbol = (Symbol.MethodSymbol) symbol; return F.select(computeBase(context, symbol), methodSymbol); } else if (symbol instanceof Symbol.ClassSymbol) { if (node.getName().contentEquals("this")) { return F.thisliteral(); } else { return F.typeLiteral(symbol); } } throw new IllegalGuardedBy(symbol.getClass().toString()); } @Override public GuardedByExpression visitParenthesized( ParenthesizedTree node, BinderContext context) { return node.getExpression().accept(this, context); } /** * Determines the implicit receiver of a select expression that accesses the given symbol by * simple name in the given resolution context. */ private GuardedByExpression computeBase(BinderContext context, Symbol symbol) { return normalizeBase(context, symbol, null); } /** * Normalizes the receiver of a select expression so that accesses on 'this' are divided * into type names (for static accesses), qualified this accesses (for members of a * lexically enclosing scope), or simple this accesses for members of the current class. */ private GuardedByExpression normalizeBase( BinderContext context, Symbol symbol, GuardedByExpression base) { if (symbol.isStatic()) { return F.typeLiteral(symbol.owner.enclClass()); } if (base != null && base.kind() != GuardedByExpression.Kind.THIS) { return base; } if (symbol.isMemberOf(context.thisClass.type.tsym, context.types)) { return F.thisliteral(); } Symbol lexicalOwner = isEnclosedIn(context.thisClass, symbol, context.types); if (lexicalOwner != null) { return F.qualifiedThis(context.names, context.thisClass, lexicalOwner); } throw new IllegalGuardedBy("Could not find the implicit receiver."); } /** * Returns the owner if the given member is declared in a lexically enclosing scope, and * * @{code null} otherwise. */ private ClassSymbol isEnclosedIn(ClassSymbol startingClass, Symbol member, Types types) { for (ClassSymbol scope = startingClass.owner.enclClass(); scope != null; scope = scope.owner.enclClass()) { if (member.isMemberOf(scope.type.tsym, types)) { return scope; } } return null; } }; }