com.google.errorprone.bugpatterns.threadsafety.ThreadSafety Maven / Gradle / Ivy
/*
* Copyright 2016 The Error Prone Authors.
*
* 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.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.auto.value.AutoValue;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.CanBeStaticAnalyzer;
import com.google.errorprone.util.ASTHelpers;
import com.google.errorprone.util.MoreAnnotations;
import com.sun.source.tree.ClassTree;
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.TypeSymbol;
import com.sun.tools.javac.code.Symbol.TypeVariableSymbol;
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.TypeTag;
import com.sun.tools.javac.code.Types;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.ElementKind;
import javax.lang.model.type.TypeKind;
import org.pcollections.ConsPStack;
/**
* A class which gives information about the annotation of types; if a type isn't annotated, {@link
* Violation} gives information as to why it is not.
*/
public final class ThreadSafety {
private final VisitorState state;
private final Purpose purpose;
private final KnownTypes knownTypes;
private final ImmutableSet markerAnnotations;
private final ImmutableSet acceptedAnnotations;
private final Class containerOfAnnotation;
@Nullable private final Class suppressAnnotation;
@Nullable private final Class typeParameterAnnotation;
/** Stores recursive invocations of {@link #isTypeParameterThreadSafe} */
private final Set recursiveThreadSafeTypeParameter = new HashSet<>();
public static Builder builder() {
return new Builder();
}
/**
* The {@link ThreadSafety} utility class can be used by either the bug checker that enforces
* immutability or by the bug checker that enforces thread-safety. Depending on which of these bug
* checkers is using this utility, different messages become appropriate.
*/
public enum Purpose {
/** This is being used by the immutability bug checker */
FOR_IMMUTABLE_CHECKER {
@Override
String immutableOrThreadSafe() {
return "immutable";
}
@Override
String mutableOrNonThreadSafe() {
return "mutable";
}
@Override
String mutableOrNotThreadSafe() {
return "mutable";
}
},
/** This is being used by the thread-safety bug checker */
FOR_THREAD_SAFE_CHECKER {
@Override
String immutableOrThreadSafe() {
return "thread-safe";
}
@Override
String mutableOrNonThreadSafe() {
return "non-thread-safe";
}
@Override
String mutableOrNotThreadSafe() {
return "not thread-safe";
}
},
;
/**
* Returns either the string {@code "immutable"} or {@code "thread-safe"} depending on the
* purpose.
*/
abstract String immutableOrThreadSafe();
/**
* Returns either the string {@code "mutable"} or {@code "non-thread-safe"} depending on the
* purpose.
*/
abstract String mutableOrNonThreadSafe();
/**
* Returns either the string {@code "mutable"} or {@code "not thread-safe"} depending on the
* purpose.
*/
abstract String mutableOrNotThreadSafe();
}
/** {@link ThreadSafety}Builder */
public static class Builder {
private Builder() {}
private Purpose purpose = Purpose.FOR_IMMUTABLE_CHECKER;
private KnownTypes knownTypes;
private ImmutableSet markerAnnotations;
private ImmutableSet acceptedAnnotations = ImmutableSet.of();
@Nullable private Class containerOfAnnotation;
@Nullable private Class suppressAnnotation;
@Nullable private Class typeParameterAnnotation;
/** See {@link Purpose}. */
public Builder setPurpose(Purpose purpose) {
this.purpose = purpose;
return this;
}
/** Information about known types and whether they're known to be safe or unsafe. */
public Builder knownTypes(KnownTypes knownTypes) {
this.knownTypes = knownTypes;
return this;
}
/**
* Annotations that will cause a class to be tested with this {@link ThreadSafety} instance; for
* example, when testing a class for immutability, this should be @Immutable.
*/
public Builder markerAnnotations(Set markerAnnotations) {
return markerAnnotations(ImmutableSet.copyOf(markerAnnotations));
}
// TODO(ringwalt): Remove this constructor. We need it for binary compatibility.
public Builder markerAnnotations(ImmutableSet markerAnnotations) {
checkNotNull(markerAnnotations);
this.markerAnnotations = markerAnnotations;
return this;
}
/**
* Annotations that do *not* cause a class to be tested, but which are treated as valid
* annotations to pass the test; for example, if @ThreadSafe is the marker
* annotation, @Immutable would be included in this list, as an immutable class is by definition
* thread-safe.
*/
public Builder acceptedAnnotations(Set acceptedAnnotations) {
return acceptedAnnotations(ImmutableSet.copyOf(acceptedAnnotations));
}
// TODO(ringwalt): Remove this constructor. We need it for binary compatibility.
public Builder acceptedAnnotations(ImmutableSet acceptedAnnotations) {
checkNotNull(acceptedAnnotations);
this.acceptedAnnotations = acceptedAnnotations;
return this;
}
/** An annotation which marks a generic parameter as a container type. */
public Builder containerOfAnnotation(Class containerOfAnnotation) {
checkNotNull(containerOfAnnotation);
this.containerOfAnnotation = containerOfAnnotation;
return this;
}
/** An annotation which, when found on a class, should suppress the test */
public Builder suppressAnnotation(Class suppressAnnotation) {
checkNotNull(suppressAnnotation);
this.suppressAnnotation = suppressAnnotation;
return this;
}
/**
* An annotation which, when found on a type parameter, indicates that the type parameter may
* only be instantiated with thread-safe types.
*/
public Builder typeParameterAnnotation(Class typeParameterAnnotation) {
checkNotNull(typeParameterAnnotation);
checkArgument(
Arrays.stream(typeParameterAnnotation.getAnnotation(Target.class).value())
.anyMatch(ElementType.TYPE_PARAMETER::equals),
"%s must be applicable to type parameters",
typeParameterAnnotation);
this.typeParameterAnnotation = typeParameterAnnotation;
return this;
}
public ThreadSafety build(VisitorState state) {
checkNotNull(knownTypes);
checkNotNull(markerAnnotations);
return new ThreadSafety(
state,
purpose,
knownTypes,
markerAnnotations,
acceptedAnnotations,
containerOfAnnotation,
suppressAnnotation,
typeParameterAnnotation);
}
}
/** Use {@link #builder()} instead. */
@Deprecated
public ThreadSafety(
VisitorState state,
KnownTypes knownTypes,
Set markerAnnotations,
Set acceptedAnnotations,
@Nullable Class containerOfAnnotation,
@Nullable Class suppressAnnotation) {
this(
state,
knownTypes,
markerAnnotations,
acceptedAnnotations,
containerOfAnnotation,
suppressAnnotation,
/* typeParameterAnnotation= */ null);
}
/** Use {@link #builder()} instead. */
@Deprecated
public ThreadSafety(
VisitorState state,
KnownTypes knownTypes,
Set markerAnnotations,
Set acceptedAnnotations,
@Nullable Class containerOfAnnotation,
@Nullable Class suppressAnnotation,
@Nullable Class typeParameterAnnotation) {
this(
state,
Purpose.FOR_IMMUTABLE_CHECKER,
knownTypes,
markerAnnotations,
acceptedAnnotations,
containerOfAnnotation,
suppressAnnotation,
typeParameterAnnotation);
}
private ThreadSafety(
VisitorState state,
Purpose purpose,
KnownTypes knownTypes,
Set markerAnnotations,
Set acceptedAnnotations,
@Nullable Class containerOfAnnotation,
@Nullable Class suppressAnnotation,
@Nullable Class typeParameterAnnotation) {
this.state = checkNotNull(state);
this.purpose = purpose;
this.knownTypes = checkNotNull(knownTypes);
this.markerAnnotations = ImmutableSet.copyOf(checkNotNull(markerAnnotations));
this.acceptedAnnotations = ImmutableSet.copyOf(checkNotNull(acceptedAnnotations));
this.containerOfAnnotation = containerOfAnnotation;
this.suppressAnnotation = suppressAnnotation;
this.typeParameterAnnotation = typeParameterAnnotation;
}
/** Information about known types and whether they're known to be safe or unsafe. */
public interface KnownTypes {
/**
* Types that are known to be safe even if they're not annotated with an expected annotation.
*/
Map getKnownSafeClasses();
/** Types that are known to be unsafe and don't need testing. */
Set getKnownUnsafeClasses();
}
/**
* A human-friendly explanation of a thread safety violations.
*
* An absent explanation indicates either an annotated type with no violations, or a type
* without the annotation.
*/
@AutoValue
public abstract static class Violation {
public static Violation create(ConsPStack path) {
return new AutoValue_ThreadSafety_Violation(path);
}
/** @return true if a violation was found */
public boolean isPresent() {
return !path().isEmpty();
}
/** @return the explanation */
public String message() {
return Joiner.on(", ").join(path());
}
/**
* The list of steps in the explanation.
*
* Example: ["Foo has field 'xs' of type 'int[]'", "arrays are not thread-safe"]
*/
public abstract ConsPStack path();
/** Adds a step. */
public Violation plus(String edge) {
return create(path().plus(edge));
}
/** Creates an explanation with one step. */
public static Violation of(String reason) {
return create(ConsPStack.singleton(reason));
}
/** An empty explanation. */
public static Violation absent() {
return create(ConsPStack.empty());
}
}
/**
* Check that a type-use of an {@code @ThreadSafe}-annotated type is instantiated with threadsafe
* type arguments where required by its annotation's containerOf element.
*
* @param containerTypeParameters the in-scope threadsafe type parameters, declared on some
* enclosing class.
* @param annotation the type's {@code @ThreadSafe} info
* @param type the type to check
*/
public Violation threadSafeInstantiation(
Set containerTypeParameters, AnnotationInfo annotation, Type type) {
for (int i = 0; i < type.tsym.getTypeParameters().size(); i++) {
TypeVariableSymbol typaram = type.tsym.getTypeParameters().get(i);
boolean immutableTypeParameter = hasThreadSafeTypeParameterAnnotation(typaram);
if (annotation.containerOf().contains(typaram.getSimpleName().toString())
|| immutableTypeParameter) {
if (type.getTypeArguments().isEmpty()) {
return Violation.of(
String.format(
"'%s' required instantiation of '%s' with type parameters, but was raw",
getPrettyName(type.tsym), typaram));
}
Type tyarg = type.getTypeArguments().get(i);
if (suppressAnnotation != null
&& tyarg.getAnnotationMirrors().stream()
.anyMatch(
a ->
((ClassSymbol) a.getAnnotationType().asElement())
.flatName()
.contentEquals(suppressAnnotation.getName()))) {
continue;
}
Violation info = isThreadSafeType(!immutableTypeParameter, containerTypeParameters, tyarg);
if (info.isPresent()) {
return info.plus(
String.format(
"'%s' was instantiated with %s type for '%s'",
getPrettyName(type.tsym),
purpose.mutableOrNonThreadSafe(),
typaram.getSimpleName()));
}
}
}
return Violation.absent();
}
/**
* Check that the super-type of a {@code @ThreadSafe}-annotated type is instantiated with
* threadsafe type arguments where required by its annotation's containerOf element, and that any
* type arguments that correspond to containerOf type parameters on the sub-type are also in the
* super-type's containerOf spec.
*
* @param containerTypeParameters the in-scope threadsafe type parameters, declared on some
* enclosing class.
* @param annotation the type's {@code @ThreadSafe} info
* @param type the type to check
*/
public Violation checkSuperInstantiation(
Set containerTypeParameters, AnnotationInfo annotation, Type type) {
Violation info = threadSafeInstantiation(containerTypeParameters, annotation, type);
if (info.isPresent()) {
return info;
}
return Streams.zip(
type.asElement().getTypeParameters().stream(),
type.getTypeArguments().stream(),
(typaram, argument) -> {
if (containerOfSubtyping(containerTypeParameters, annotation, typaram, argument)) {
return Violation.of(
String.format(
"'%s' is not a container of '%s'", annotation.typeName(), typaram));
}
return Violation.absent();
})
.filter(Violation::isPresent)
.findFirst()
.orElse(Violation.absent());
}
// Enforce strong behavioral subtyping for containers.
// If:
// (1) a generic super type is instantiated with a type argument that is a type variable
// declared by the current class, and
// (2) the current class is a container of that type parameter, then
// (3) require the super-class to also be a container of its corresponding type parameter.
private boolean containerOfSubtyping(
Set containerTypeParameters,
AnnotationInfo annotation,
TypeVariableSymbol typaram,
Type tyargument) {
// (1)
if (!tyargument.hasTag(TypeTag.TYPEVAR)) {
return false;
}
// (2)
if (!containerTypeParameters.contains(tyargument.asElement().getSimpleName().toString())
|| isTypeParameterThreadSafe(
(TypeVariableSymbol) tyargument.asElement(), containerTypeParameters)) {
return false;
}
// (3)
if (annotation.containerOf().contains(typaram.getSimpleName().toString())) {
return false;
}
return true;
}
/** @deprecated use {@link #isThreadSafeType(boolean, Set, Type)} instead. */
@Deprecated
public Violation isThreadSafeType(Set containerTypeParameters, Type type) {
return isThreadSafeType(
/* allowContainerTypeParameters= */ true, containerTypeParameters, type);
}
/**
* Returns an {@link Violation} explaining whether the type is threadsafe.
*
* @param allowContainerTypeParameters true when checking the instantiation of an {@code
* typeParameterAnnotation}-annotated type parameter; indicates that {@code
* containerTypeParameters} should be ignored
* @param containerTypeParameters type parameters in enclosing elements' containerOf
* specifications
* @param type to check for thread-safety
*/
public Violation isThreadSafeType(
boolean allowContainerTypeParameters, Set containerTypeParameters, Type type) {
return type.accept(
new ThreadSafeTypeVisitor(allowContainerTypeParameters, containerTypeParameters), null);
}
private class ThreadSafeTypeVisitor extends Types.SimpleVisitor {
private final boolean allowContainerTypeParameters;
private final Set containerTypeParameters;
private ThreadSafeTypeVisitor(
boolean allowContainerTypeParameters, Set containerTypeParameters) {
this.allowContainerTypeParameters = allowContainerTypeParameters;
this.containerTypeParameters =
!allowContainerTypeParameters ? ImmutableSet.of() : containerTypeParameters;
}
@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 %s", purpose.mutableOrNotThreadSafe()));
}
@Override
public Violation visitTypeVar(TypeVar type, Void s) {
TypeVariableSymbol tyvar = (TypeVariableSymbol) type.tsym;
if (containerTypeParameters.contains(tyvar.getSimpleName().toString())) {
return Violation.absent();
}
if (isTypeParameterThreadSafe(tyvar, containerTypeParameters)) {
return Violation.absent();
}
String message;
if (!allowContainerTypeParameters) {
message =
String.format("'%s' is not annotated @ImmutableTypeParameter", tyvar.getSimpleName());
} else if (!containerTypeParameters.isEmpty()) {
message =
String.format(
"'%s' is a %s type variable (not in '%s')",
tyvar.getSimpleName(),
purpose.mutableOrNonThreadSafe(),
Joiner.on(", ").join(containerTypeParameters));
} else {
message =
String.format(
"'%s' is a %s type variable",
tyvar.getSimpleName(), purpose.mutableOrNonThreadSafe());
}
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:
if (type.tsym.getKind().name().equals("RECORD")) {
break;
}
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();
}
AnnotationInfo annotation = getMarkerOrAcceptedAnnotation(type.tsym, state);
if (annotation != null) {
return threadSafeInstantiation(containerTypeParameters, annotation, type);
}
String nameStr = type.tsym.flatName().toString();
if (knownTypes.getKnownUnsafeClasses().contains(nameStr)) {
return Violation.of(
String.format(
"'%s' is %s", type.tsym.getSimpleName(), purpose.mutableOrNotThreadSafe()));
}
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 with %s",
type,
Streams.concat(markerAnnotations.stream(), acceptedAnnotations.stream())
.map(a -> "@" + a)
.collect(Collectors.joining(" or "))));
}
}
/**
* Returns true if the given type parameter's declaration is annotated with {@link
* #typeParameterAnnotation} indicated it will only ever be instantiated with thread-safe types.
*/
public boolean hasThreadSafeTypeParameterAnnotation(TypeVariableSymbol symbol) {
return typeParameterAnnotation != null
&& symbol.getAnnotationMirrors().stream()
.anyMatch(t -> t.type.tsym.flatName().contentEquals(typeParameterAnnotation.getName()));
}
/**
* Returns whether a type parameter is thread-safe.
*
* This is true if either the type parameter's declaration is annotated with {@link
* #typeParameterAnnotation} (indicating it can only be instantiated with thread-safe types), or
* the type parameter has a thread-safe upper bound (sub-classes of thread-safe types are also
* thread-safe).
*
*
If a type has a recursive bound, we recursively assume that this type satisfies all
* thread-safety constraints. Recursion can only happen with type variables that have recursive
* type bounds. These type variables do not need to be called out in the "containerOf" attribute
* or annotated with {@link #typeParameterAnnotation}.
*
*
Recursion does not apply to other kinds of types because all declared types must be
* annotated thread-safe, which means that thread-safety checkers don't need to analyze all
* referenced types recursively.
*/
private boolean isTypeParameterThreadSafe(
TypeVariableSymbol symbol, Set containerTypeParameters) {
if (!recursiveThreadSafeTypeParameter.add(symbol)) {
return true;
}
// TODO(b/77695285): Prevent type variables that are immutable because of an immutable upper
// bound to be marked thread-safe via containerOf or typeParameterAnnotation.
try {
for (Type bound : symbol.getBounds()) {
if (!isThreadSafeType(true, containerTypeParameters, bound).isPresent()) {
// A type variable is thread-safe if any upper bound is thread-safe.
return true;
}
}
return hasThreadSafeTypeParameterAnnotation(symbol);
} finally {
recursiveThreadSafeTypeParameter.remove(symbol);
}
}
/**
* Gets the {@link Symbol}'s annotation info, either from a marker annotation on the symbol, from
* an accepted annotation on the symbol, or from the list of well-known types.
*/
public AnnotationInfo getMarkerOrAcceptedAnnotation(Symbol sym, VisitorState state) {
String nameStr = sym.flatName().toString();
AnnotationInfo known = knownTypes.getKnownSafeClasses().get(nameStr);
if (known != null) {
return known;
}
return getAnnotation(
sym, ImmutableSet.copyOf(Sets.union(markerAnnotations, acceptedAnnotations)), state);
}
/** Returns an enclosing instance for the specified type if it is thread-safe. */
public Type mutableEnclosingInstance(Optional tree, ClassType type) {
if (tree.isPresent()
&& !CanBeStaticAnalyzer.referencesOuter(
tree.get(), ASTHelpers.getSymbol(tree.get()), state)) {
return null;
}
Type enclosing = type.getEnclosingType();
while (!enclosing.getKind().equals(TypeKind.NONE)) {
if (getMarkerOrAcceptedAnnotation(enclosing.tsym, state) == null
&& isThreadSafeType(
/* allowContainerTypeParameters= */ false,
/* containerTypeParameters= */ ImmutableSet.of(),
enclosing)
.isPresent()) {
return enclosing;
}
enclosing = enclosing.getEnclosingType();
}
return null;
}
/**
* Gets the set of in-scope threadsafe type parameters from the containerOf specs on annotations.
*
* Usually only the immediately enclosing declaration is searched, but it's possible to have
* cases like:
*
*
* {@literal @}MarkerAnnotation(containerOf="T") class C<T> {
* class Inner extends ThreadSafeCollection<T> {}
* }
*
*/
public Set threadSafeTypeParametersInScope(Symbol sym) {
if (sym == null) {
return ImmutableSet.of();
}
ImmutableSet.Builder result = ImmutableSet.builder();
OUTER:
for (Symbol s = sym; s.owner != null; s = s.owner) {
switch (s.getKind()) {
case INSTANCE_INIT:
continue;
case PACKAGE:
break OUTER;
default:
break;
}
AnnotationInfo annotation = getMarkerOrAcceptedAnnotation(s, state);
if (annotation == null) {
continue;
}
for (TypeVariableSymbol typaram : s.getTypeParameters()) {
String name = typaram.getSimpleName().toString();
if (annotation.containerOf().contains(name)) {
result.add(name);
}
}
if (s.isStatic()) {
break;
}
}
return result.build();
}
private AnnotationInfo getAnnotation(
Symbol sym, ImmutableSet annotationsToCheck, VisitorState state) {
for (String annotation : annotationsToCheck) {
AnnotationInfo info = getAnnotation(sym, state, annotation, containerOfAnnotation);
if (info != null) {
return info;
}
}
return null;
}
private AnnotationInfo getAnnotation(
Symbol sym,
VisitorState state,
String annotation,
@Nullable Class elementAnnotation) {
if (sym == null) {
return null;
}
Optional attr =
sym.getRawAttributes().stream()
.filter(a -> a.type.tsym.getQualifiedName().contentEquals(annotation))
.findAny();
if (attr.isPresent()) {
ImmutableList containerElements = containerOf(state, attr.get());
if (elementAnnotation != null && containerElements.isEmpty()) {
containerElements =
sym.getTypeParameters().stream()
.filter(p -> p.getAnnotation(elementAnnotation) != null)
.map(p -> p.getSimpleName().toString())
.collect(toImmutableList());
}
return AnnotationInfo.create(sym.getQualifiedName().toString(), containerElements);
}
// @ThreadSafe is inherited from supertypes
if (!(sym instanceof ClassSymbol)) {
return null;
}
Type superClass = ((ClassSymbol) sym).getSuperclass();
AnnotationInfo superAnnotation = getInheritedAnnotation(superClass.asElement(), state);
if (superAnnotation == null) {
return null;
}
// If an annotated super-type was found, look for any type arguments to the super-type that
// are in the super-type's containerOf spec, and where the arguments are type parameters
// of the current class.
// E.g. for `Foo extends Super` if `Super` is annotated
// `@ThreadSafeContainerAnnotation Y`
// then `Foo` is has X implicitly annotated `@ThreadSafeContainerAnnotation X`
ImmutableList.Builder containerOf = ImmutableList.builder();
for (int i = 0; i < superClass.getTypeArguments().size(); i++) {
Type arg = superClass.getTypeArguments().get(i);
TypeVariableSymbol formal = superClass.asElement().getTypeParameters().get(i);
if (!arg.hasTag(TypeTag.TYPEVAR)) {
continue;
}
TypeSymbol argSym = arg.asElement();
if (argSym.owner == sym
&& superAnnotation.containerOf().contains(formal.getSimpleName().toString())) {
containerOf.add(argSym.getSimpleName().toString());
}
}
return AnnotationInfo.create(superAnnotation.typeName(), containerOf.build());
}
/**
* Gets the possibly inherited marker annotation on the given symbol, and reverse-propagates
* containerOf spec's from super-classes.
*/
public AnnotationInfo getInheritedAnnotation(Symbol sym, VisitorState state) {
return getAnnotation(sym, markerAnnotations, state);
}
private static ImmutableList containerOf(VisitorState state, Compound attr) {
Attribute m = attr.member(state.getName("containerOf"));
if (m == null) {
return ImmutableList.of();
}
return MoreAnnotations.asStrings((AnnotationValue) m).collect(toImmutableList());
}
/** Gets a human-friendly name for the given {@link Symbol} to use in diagnostics. */
public 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();
}
public Violation checkInstantiation(
Collection typeParameters, Collection typeArguments) {
return Streams.zip(
typeParameters.stream(),
typeArguments.stream(),
(sym, type) -> checkInstantiation(sym, ImmutableList.of(type)))
.filter(Violation::isPresent)
.findFirst()
.orElse(Violation.absent());
}
/** Checks that any thread-safe type parameters are instantiated with thread-safe types. */
public Violation checkInstantiation(
TypeVariableSymbol typeParameter, Collection instantiations) {
if (!hasThreadSafeTypeParameterAnnotation(typeParameter)) {
return Violation.absent();
}
for (Type instantiation : instantiations) {
Violation info =
isThreadSafeType(
/* allowContainerTypeParameters= */ true,
/* containerTypeParameters= */ ImmutableSet.of(),
instantiation);
if (info.isPresent()) {
return info.plus(
String.format(
"instantiation of '%s' is %s", typeParameter, purpose.mutableOrNotThreadSafe()));
}
}
return Violation.absent();
}
/** Checks the instantiation of any thread-safe type parameters in the current invocation. */
public Violation checkInvocation(Type methodType, Symbol symbol) {
if (methodType == null) {
return Violation.absent();
}
List typeParameters = symbol.getTypeParameters();
if (typeParameters.stream().noneMatch(this::hasThreadSafeTypeParameterAnnotation)) {
// fast path
return Violation.absent();
}
ImmutableMultimap instantiation = getInstantiation(methodType);
for (TypeVariableSymbol typeParameter : typeParameters) {
Violation violation = checkInstantiation(typeParameter, instantiation.get(typeParameter));
if (violation.isPresent()) {
return violation;
}
}
return Violation.absent();
}
private static ImmutableMultimap getInstantiation(Type methodType) {
List to = new ArrayList<>();
ArrayList from = new ArrayList<>();
getSubst(getMapping(methodType), from, to);
ImmutableMultimap.Builder mapping = ImmutableMultimap.builder();
Streams.forEachPair(
from.stream(), to.stream(), (f, t) -> mapping.put((TypeVariableSymbol) f.asElement(), t));
return mapping.build();
}
private static Type getMapping(Type type) {
if (type == null) {
return null;
}
try {
// Reflectively extract the mapping from Type.createMethodTypeWithReturn
Field valField = type.getClass().getDeclaredField("val$t");
valField.setAccessible(true);
return (Type) valField.get(type);
} catch (ReflectiveOperationException e) {
return type;
}
}
@SuppressWarnings("unchecked")
private static void getSubst(Type m, List from, List to) {
try {
// Reflectively extract the mapping from an enclosing instance of Types.Subst
Field substField = m.getClass().getDeclaredField("this$0");
substField.setAccessible(true);
Object subst = substField.get(m);
Field fromField = subst.getClass().getDeclaredField("from");
Field toField = subst.getClass().getDeclaredField("to");
fromField.setAccessible(true);
toField.setAccessible(true);
from.addAll((Collection) fromField.get(subst));
to.addAll((Collection) toField.get(subst));
} catch (ReflectiveOperationException e) {
return;
}
}
}