/* * Copyright 2016 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 com.google.auto.value.AutoValue; import com.google.common.base.Joiner; import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.errorprone.VisitorState; import com.google.errorprone.annotations.Immutable; import com.google.errorprone.annotations.concurrent.LazyInit; import com.google.errorprone.bugpatterns.BugChecker; import com.google.errorprone.bugpatterns.CanBeStaticAnalyzer; import com.google.errorprone.fixes.SuggestedFixes; import com.google.errorprone.util.ASTHelpers; import com.sun.source.tree.ClassTree; import com.sun.source.tree.Tree; import com.sun.tools.javac.code.Attribute; import com.sun.tools.javac.code.Attribute.Compound; import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Symbol.ClassSymbol; import com.sun.tools.javac.code.Symbol.TypeVariableSymbol; import com.sun.tools.javac.code.Symbol.VarSymbol; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.code.Type.ArrayType; import com.sun.tools.javac.code.Type.ClassType; import com.sun.tools.javac.code.Type.TypeVar; import com.sun.tools.javac.code.Type.WildcardType; import com.sun.tools.javac.code.Types; import com.sun.tools.javac.util.Filter; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.ElementKind; import javax.lang.model.element.Modifier; import javax.lang.model.type.TypeKind; import javax.lang.model.util.SimpleAnnotationValueVisitor8; import org.pcollections.ConsPStack; /** Analyzes types for deep immutability. */ public class ImmutableAnalysis { /** * A human-friendly explanation of an immutability violations. * * <p>An absent explanation indicates either an @Immutable-annotated type with no violations, or a * type without the annotation. */ @AutoValue abstract static class Violation { private static Violation create(ConsPStack<String> path) { return new AutoValue_ImmutableAnalysis_Violation(path); } /** @return true if a violation was found */ boolean isPresent() { return !path().isEmpty(); } /** @return the explanation */ String message() { return Joiner.on(", ").join(path()); } /** * The list of steps in the explanation. * * <p>Example: ["Foo has field 'xs' of type 'int[]'", "arrays are mutable"] */ abstract ConsPStack<String> path(); /** Adds a step. */ Violation plus(String edge) { return create(path().plus(edge)); } /** Creates an explanation with one step. */ static Violation of(String reason) { return create(ConsPStack.singleton(reason)); } /** An empty explanation. */ static Violation absent() { return create(ConsPStack.<String>empty()); } } private final BugChecker bugChecker; private final VisitorState state; private final String nonFinalFieldMessage; private final String mutableFieldMessage; public ImmutableAnalysis( BugChecker bugChecker, VisitorState state, String nonFinalFieldMessage, String mutableFieldMessage) { this.bugChecker = bugChecker; this.state = state; this.nonFinalFieldMessage = nonFinalFieldMessage; this.mutableFieldMessage = mutableFieldMessage; } /** * Check that an {@code @Immutable}-annotated class: * * <ul> * <li>does not declare or inherit any mutable fields, * <li>any immutable supertypes are instantiated with immutable type arguments as required by * their containerOf spec, and * <li>any enclosing instances are immutable. * </ul> * * requiring supertypes to be annotated immutable would be too restrictive. */ Violation checkForImmutability( Optional<ClassTree> tree, ImmutableSet<String> immutableTyParams, ClassType type) { Violation info = areFieldsImmutable(tree, immutableTyParams, type); if (info.isPresent()) { return info; } for (Type interfaceType : state.getTypes().interfaces(type)) { ImmutableAnnotationInfo interfaceAnnotation = getImmutableAnnotation(interfaceType.tsym, state); if (interfaceAnnotation == null) { continue; } info = immutableInstantiation(immutableTyParams, interfaceAnnotation, interfaceType); if (info.isPresent()) { return info.plus( String.format( "'%s' extends '%s'", getPrettyName(type.tsym), getPrettyName(interfaceType.tsym))); } } info = checkSuper(immutableTyParams, type); if (info.isPresent()) { return info; } Type mutableEnclosing = mutableEnclosingInstance(tree, type); if (mutableEnclosing != null) { return info.plus( String.format( "'%s' has mutable enclosing instance '%s'", getPrettyName(type.tsym), mutableEnclosing)); } return Violation.absent(); } private Type mutableEnclosingInstance(Optional<ClassTree> tree, ClassType type) { if (tree.isPresent() && !CanBeStaticAnalyzer.referencesOuter( tree.get(), ASTHelpers.getSymbol(tree.get()), state)) { return null; } Type enclosing = type.getEnclosingType(); while (!Type.noType.equals(enclosing)) { if (getImmutableAnnotation(enclosing.tsym, state) == null && isImmutableType(ImmutableSet.of(), enclosing).isPresent()) { return enclosing; } enclosing = enclosing.getEnclosingType(); } return null; } private Violation checkSuper(ImmutableSet<String> immutableTyParams, ClassType type) { ClassType superType = (ClassType) state.getTypes().supertype(type); if (superType.getKind() == TypeKind.NONE || state.getTypes().isSameType(state.getSymtab().objectType, superType)) { return Violation.absent(); } if (WellKnownMutability.isAnnotation(state, type)) { // TODO(b/25630189): add enforcement return Violation.absent(); } ImmutableAnnotationInfo superannotation = getImmutableAnnotation(superType.tsym, state); if (superannotation != null) { // If the superclass does happen to be immutable, we don't need to recursively // inspect it. We just have to check that it's instantiated correctly: Violation info = immutableInstantiation(immutableTyParams, superannotation, superType); if (!info.isPresent()) { return Violation.absent(); } return info.plus( String.format( "'%s' extends '%s'", getPrettyName(type.tsym), getPrettyName(superType.tsym))); } // Recursive case: check if the supertype is 'effectively' immutable. Violation info = checkForImmutability(Optional.<ClassTree>absent(), immutableTyParams, superType); if (!info.isPresent()) { return Violation.absent(); } return info.plus( String.format( "'%s' extends '%s'", getPrettyName(type.tsym), getPrettyName(superType.tsym))); } /** * Check a single class' fields for immutability. * * @param immutableTyParams the in-scope immutable type parameters * @param classType the type to check the fields of */ Violation areFieldsImmutable( Optional<ClassTree> tree, ImmutableSet<String> immutableTyParams, ClassType classType) { ClassSymbol classSym = (ClassSymbol) classType.tsym; if (classSym.members() == null) { return Violation.absent(); } Filter<Symbol> instanceFieldFilter = new Filter<Symbol>() { @Override public boolean accepts(Symbol symbol) { return symbol.getKind() == ElementKind.FIELD && !symbol.isStatic(); } }; Map<Symbol, Tree> declarations = new HashMap<>(); if (tree.isPresent()) { for (Tree member : tree.get().getMembers()) { Symbol sym = ASTHelpers.getSymbol(member); if (sym != null) { declarations.put(sym, member); } } } // javac gives us members in reverse declaration order // handling them in declaration order leads to marginally better diagnostics List<Symbol> members = ImmutableList.copyOf(classSym.members().getSymbols(instanceFieldFilter)).reverse(); for (Symbol member : members) { Optional<Tree> memberTree = Optional.fromNullable(declarations.get(member)); Violation info = isFieldImmutable(memberTree, immutableTyParams, classSym, classType, (VarSymbol) member); if (info.isPresent()) { return info; } } return Violation.absent(); } /** Check a single field for immutability. */ private Violation isFieldImmutable( Optional<Tree> tree, ImmutableSet<String> immutableTyParams, ClassSymbol classSym, ClassType classType, VarSymbol var) { if (bugChecker.isSuppressed(var)) { return Violation.absent(); } if (ASTHelpers.hasAnnotation(var, LazyInit.class, state)) { return Violation.absent(); } if (!var.getModifiers().contains(Modifier.FINAL)) { if (tree.isPresent()) { // If we have a tree to attach diagnostics to, report the error immediately instead of // accumulating the path to the error from the top-level class being checked state.reportMatch( BugChecker.buildDescriptionFromChecker(tree.get(), bugChecker) .setMessage(nonFinalFieldMessage) .addFix(SuggestedFixes.addModifiers(tree.get(), state, Modifier.FINAL)) .build()); return Violation.absent(); } return Violation.of( String.format( "'%s' has non-final field '%s'", getPrettyName(classSym), var.getSimpleName())); } Type varType = state.getTypes().memberType(classType, var); Violation info = isImmutableType(immutableTyParams, varType); if (info.isPresent()) { if (tree.isPresent()) { // If we have a tree to attach diagnostics to, report the error immediately instead of // accumulating the path to the error from the top-level class being checked state.reportMatch( BugChecker.buildDescriptionFromChecker(tree.get(), bugChecker) .setMessage(info.plus(mutableFieldMessage).message()) .build()); return Violation.absent(); } return info.plus( String.format( "'%s' has field '%s' of type '%s'", getPrettyName(classSym), var.getSimpleName(), varType)); } return Violation.absent(); } /** * Check that a type-use of an {@code @Immutable}-annotated type is instantiated with immutable * type arguments where required by its annotation's containerOf element. * * @param immutableTyParams the in-scope immutable type parameters, declared on some enclosing * class. * @param annotation the type's {@code @Immutable} info * @param type the type to check */ Violation immutableInstantiation( ImmutableSet<String> immutableTyParams, ImmutableAnnotationInfo annotation, Type type) { if (!annotation.containerOf().isEmpty() && type.tsym.getTypeParameters().size() != type.getTypeArguments().size()) { return Violation.of( String.format( "'%s' required immutable instantiation of '%s', but was raw", getPrettyName(type.tsym), Joiner.on(", ").join(annotation.containerOf()))); } for (int i = 0; i < type.tsym.getTypeParameters().size(); i++) { TypeVariableSymbol typaram = type.tsym.getTypeParameters().get(i); if (annotation.containerOf().contains(typaram.getSimpleName().toString())) { Type tyarg = type.getTypeArguments().get(i); Violation info = isImmutableType(immutableTyParams, tyarg); if (info.isPresent()) { return info.plus( String.format( "'%s' was instantiated with mutable type for '%s'", getPrettyName(type.tsym), typaram.getSimpleName())); } } } return Violation.absent(); } /** Returns an {@link Violation} explaining whether the type is immutable. */ Violation isImmutableType(ImmutableSet<String> immutableTyParams, Type type) { return type.accept(new ImmutableTypeVisitor(immutableTyParams), null); } private class ImmutableTypeVisitor extends Types.SimpleVisitor<Violation, Void> { private final ImmutableSet<String> immutableTyParams; private ImmutableTypeVisitor(ImmutableSet<String> immutableTyParams) { this.immutableTyParams = immutableTyParams; } @Override public Violation visitWildcardType(WildcardType type, Void s) { return state.getTypes().wildUpperBound(type).accept(this, null); } @Override public Violation visitArrayType(ArrayType t, Void s) { return Violation.of(String.format("arrays are mutable")); } @Override public Violation visitTypeVar(TypeVar type, Void s) { TypeVariableSymbol tyvar = (TypeVariableSymbol) type.tsym; if (immutableTyParams != null && immutableTyParams.contains(tyvar.getSimpleName().toString())) { return Violation.absent(); } String message; if (immutableTyParams.isEmpty()) { message = String.format("'%s' is a mutable type variable", tyvar.getSimpleName()); } else { message = String.format( "'%s' is a mutable type variable (not in '%s')", tyvar.getSimpleName(), Joiner.on(", ").join(immutableTyParams)); } return Violation.of(message); } @Override public Violation visitType(Type type, Void s) { switch (type.tsym.getKind()) { case ANNOTATION_TYPE: // assume annotations are always immutable // TODO(b/25630189): add enforcement return Violation.absent(); case ENUM: // assume enums are always immutable // TODO(b/25630186): add enforcement return Violation.absent(); case INTERFACE: case CLASS: break; default: throw new AssertionError(String.format("Unexpected type kind %s", type.tsym.getKind())); } if (WellKnownMutability.isAnnotation(state, type)) { // annotation implementations may not have ANNOTATION_TYPE kind, assume they are immutable // TODO(b/25630189): add enforcement return Violation.absent(); } ImmutableAnnotationInfo annotation = getImmutableAnnotation(type.tsym, state); if (annotation != null) { return immutableInstantiation(immutableTyParams, annotation, type); } String nameStr = type.tsym.flatName().toString(); if (WellKnownMutability.KNOWN_UNSAFE.contains(nameStr)) { return Violation.of(String.format("'%s' is mutable", type.tsym.getSimpleName())); } if (WellKnownMutability.isProto2MessageClass(state, type)) { if (WellKnownMutability.isProto2MutableMessageClass(state, type)) { return Violation.of( String.format("'%s' is a mutable proto message", type.tsym.getSimpleName())); } return Violation.absent(); } return Violation.of( String.format("the declaration of type '%s' is not annotated @Immutable", type)); } } /** * Gets the {@link Symbol}'s {@code @Immutable} annotation info, either from an annotation on the * symbol or from the list of well-known immutable types. */ static ImmutableAnnotationInfo getImmutableAnnotation(Symbol sym, VisitorState state) { String nameStr = sym.flatName().toString(); ImmutableAnnotationInfo known = WellKnownMutability.KNOWN_IMMUTABLE.get(nameStr); if (known != null) { return known; } Compound attr = sym.attribute(state.getSymbolFromString(Immutable.class.getName())); if (attr == null) { return null; } ImmutableList.Builder<String> containerOf = ImmutableList.builder(); Attribute m = attr.member(state.getName("containerOf")); if (m != null) { m.accept( new SimpleAnnotationValueVisitor8<Void, Void>() { @Override public Void visitString(String s, Void unused) { containerOf.add(s); return null; } @Override public Void visitArray(List<? extends AnnotationValue> list, Void unused) { for (AnnotationValue value : list) { value.accept(this, null); } return null; } }, null); } return ImmutableAnnotationInfo.create(sym.getQualifiedName().toString(), containerOf.build()); } /** * Gets the {@link Tree}'s {@code @Immutable} annotation info, either from an annotation on the * symbol or from the list of well-known immutable types. */ static ImmutableAnnotationInfo getImmutableAnnotation(Tree tree, VisitorState state) { Symbol sym = ASTHelpers.getSymbol(tree); return sym == null ? null : getImmutableAnnotation(sym, state); } /** Gets a human-friendly name for the given {@link Symbol} to use in diagnostics. */ private String getPrettyName(Symbol sym) { if (!sym.getSimpleName().isEmpty()) { return sym.getSimpleName().toString(); } if (sym.getKind() == ElementKind.ENUM) { // anonymous classes for enum constants are identified by the enclosing constant // declaration return sym.owner.getSimpleName().toString(); } // anonymous classes have an empty name, but a recognizable superclass or interface // e.g. refer to `new Runnable() { ... }` as "Runnable" Type superType = state.getTypes().supertype(sym.type); if (state.getTypes().isSameType(superType, state.getSymtab().objectType)) { superType = Iterables.getFirst(state.getTypes().interfaces(sym.type), superType); } return superType.tsym.getSimpleName().toString(); } }