/*
* 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;
}
};
}