/*
* Copyright 2015 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.BugPattern.Category.JDK;
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import static com.google.errorprone.bugpatterns.threadsafety.ImmutableAnalysis.getImmutableAnnotation;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.annotations.Immutable;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.threadsafety.ImmutableAnalysis.Violation;
import com.google.errorprone.fixes.Fix;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.TypeParameterTree;
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.Type;
import java.util.HashSet;
import java.util.Set;
/** @author cushon@google.com (Liam Miller-Cushon) */
@BugPattern(
name = "Immutable",
summary = "Type declaration annotated with @Immutable is not immutable",
category = JDK,
severity = ERROR
)
public class ImmutableChecker extends BugChecker implements BugChecker.ClassTreeMatcher {
@Override
public Description matchClass(ClassTree tree, VisitorState state) {
ImmutableAnalysis analysis =
new ImmutableAnalysis(
this,
state,
"@Immutable classes cannot have non-final fields",
"@Immutable class has mutable field");
if (tree.getSimpleName().length() == 0) {
// anonymous classes have empty names
// TODO(cushon): once Java 8 happens, require @Immutable on anonymous classes
return handleAnonymousClass(tree, state, analysis);
}
ImmutableAnnotationInfo annotation = getImmutableAnnotation(tree, state);
if (annotation == null) {
// If the type isn't annotated we don't check for immutability, but we do
// report an error if it extends/implements any @Immutable-annotated types.
return checkSubtype(tree, state);
}
// Special-case visiting declarations of known-immutable types; these uses
// of the annotation are "trusted".
if (WellKnownMutability.KNOWN_IMMUTABLE.containsValue(annotation)) {
return Description.NO_MATCH;
}
// Check that the types in containerOf actually exist
Set<String> typarams = new HashSet<>();
for (TypeParameterTree typaram : tree.getTypeParameters()) {
typarams.add(typaram.getName().toString());
}
SetView<String> difference = Sets.difference(annotation.containerOf(), typarams);
if (!difference.isEmpty()) {
String message =
String.format(
"could not find type(s) referenced by containerOf: %s",
Joiner.on("', '").join(difference));
return buildDescription(tree).setMessage(message).build();
}
// Main path for @Immutable-annotated types:
//
// Check that the fields (including inherited fields) are immutable, and
// validate the type hierarchy superclass.
Violation info =
analysis.checkForImmutability(
Optional.of(tree),
immutableTypeParametersInScope(ASTHelpers.getSymbol(tree), state),
ASTHelpers.getType(tree));
if (!info.isPresent()) {
return Description.NO_MATCH;
}
String message =
"type annotated with @Immutable could not be proven immutable: " + info.message();
return buildDescription(tree).setMessage(message).build();
}
// Anonymous classes
/** Check anonymous implementations of {@code @Immutable} types. */
private Description handleAnonymousClass(
ClassTree tree, VisitorState state, ImmutableAnalysis analysis) {
ClassSymbol sym = ASTHelpers.getSymbol(tree);
if (sym == null) {
return Description.NO_MATCH;
}
Type superType = immutableSupertype(sym, state);
if (superType == null) {
return Description.NO_MATCH;
}
// We don't need to check that the superclass has an immutable instantiation.
// The anonymous instance can only be referred to using a superclass type, so
// the type arguments will be validated at any type use site where we care about
// the instance's immutability.
//
// Also, we have no way to express something like:
//
// public static <@Immutable T> ImmutableBox<T> create(T t) {
// return new ImmutableBox<>(t);
// }
ImmutableSet<String> typarams = immutableTypeParametersInScope(sym, state);
Violation info =
analysis.areFieldsImmutable(Optional.of(tree), typarams, ASTHelpers.getType(tree));
if (!info.isPresent()) {
return Description.NO_MATCH;
}
String reason = Joiner.on(", ").join(info.path());
String message =
String.format(
"Class extends @Immutable type %s, but is not immutable: %s", superType, reason);
return buildDescription(tree).setMessage(message).build();
}
// Strong behavioural subtyping
/** Check for classes without {@code @Immutable} that have immutable supertypes. */
private Description checkSubtype(ClassTree tree, VisitorState state) {
ClassSymbol sym = ASTHelpers.getSymbol(tree);
if (sym == null) {
return Description.NO_MATCH;
}
Type superType = immutableSupertype(sym, state);
if (superType == null) {
return Description.NO_MATCH;
}
String message =
String.format(
"Class extends @Immutable type %s, but is not annotated as immutable", superType);
Fix fix =
SuggestedFix.builder()
.prefixWith(tree, "@Immutable ")
.addImport(Immutable.class.getName())
.build();
return buildDescription(tree).setMessage(message).addFix(fix).build();
}
/**
* Returns the type of the first superclass or superinterface in the hierarchy annotated with
* {@code @Immutable}, or {@code null} if no such super type exists.
*/
private static Type immutableSupertype(Symbol sym, VisitorState state) {
for (Type superType : state.getTypes().closure(sym.type)) {
if (superType.equals(sym.type)) {
continue;
}
// Don't use getImmutableAnnotation here: subtypes of trusted types are
// also trusted, only check for explicitly annotated supertypes.
if (ASTHelpers.hasAnnotation(superType.tsym, Immutable.class, state)) {
return superType;
}
// We currently trust that @interface annotations are immutable, but don't enforce that
// custom interface implementations are also immutable. That means the check can be
// defeated by writing a custom mutable annotation implementation, and passing it around
// using the superclass type.
//
// TODO(b/25630189): fix this
//
// if (superType.tsym.getKind() == ElementKind.ANNOTATION_TYPE) {
// return superType;
// }
}
return null;
}
/**
* Gets the set of in-scope immutable type parameters from the containerOf specs on
* {@code @Immutable} annotations.
*
* <p>Usually only the immediately enclosing declaration is searched, but it's possible to have
* cases like:
*
* <pre>
* @Immutable(containerOf="T") class C<T> {
* class Inner extends ImmutableCollection<T> {}
* }
* </pre>
*/
private static ImmutableSet<String> immutableTypeParametersInScope(
Symbol sym, VisitorState state) {
if (sym == null) {
return ImmutableSet.of();
}
ImmutableSet.Builder<String> 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;
}
ImmutableAnnotationInfo annotation = getImmutableAnnotation(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();
}
}