/*
* Copyright 2017 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;
import static com.google.errorprone.BugPattern.Category.JDK;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableBiMap;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker.VariableTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.fixes.SuggestedFixes;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.ParameterizedTypeTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.SimpleTreeVisitor;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Type;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
/** @author dorir@google.com (Dori Reuveni) */
@BugPattern(
name = "MutableConstantField",
category = JDK,
summary =
"Constant field declarations should use the immutable type (such as ImmutableList) instead of"
+ " the general collection interface type (such as List)",
severity = WARNING
)
public final class MutableConstantField extends BugChecker implements VariableTreeMatcher {
@VisibleForTesting
static final ImmutableBiMap<String, String> MUTABLE_TO_IMMUTABLE_CLASS_NAME_MAP =
ImmutableBiMap.<String, String>builder()
.put("com.google.common.collect.BiMap", "com.google.common.collect.ImmutableBiMap")
.put(
"com.google.common.collect.ListMultimap",
"com.google.common.collect.ImmutableListMultimap")
.put("com.google.common.collect.Multimap", "com.google.common.collect.ImmutableMultimap")
.put("com.google.common.collect.Multiset", "com.google.common.collect.ImmutableMultiset")
.put("com.google.common.collect.RangeMap", "com.google.common.collect.ImmutableRangeMap")
.put("com.google.common.collect.RangeSet", "com.google.common.collect.ImmutableRangeSet")
.put(
"com.google.common.collect.SetMultimap",
"com.google.common.collect.ImmutableSetMultimap")
.put(
"com.google.common.collect.SortedMultiset",
"com.google.common.collect.ImmutableSortedMultiset")
.put("com.google.common.collect.Table", "com.google.common.collect.ImmutableTable")
.put("java.util.Collection", "com.google.common.collect.ImmutableCollection")
.put("java.util.List", "com.google.common.collect.ImmutableList")
.put("java.util.Map", "com.google.common.collect.ImmutableMap")
.put("java.util.NavigableMap", "com.google.common.collect.ImmutableSortedMap")
.put("java.util.NavigableSet", "com.google.common.collect.ImmutableSortedSet")
.put("java.util.Set", "com.google.common.collect.ImmutableSet")
.build();
@Override
public Description matchVariable(VariableTree tree, VisitorState state) {
if (!isConstantField(ASTHelpers.getSymbol(tree))) {
return Description.NO_MATCH;
}
Tree rhsTree = tree.getInitializer();
Type rhsType = ASTHelpers.getType(rhsTree);
if (rhsType == null || !isImmutableType(rhsType)) {
return Description.NO_MATCH;
}
Tree lhsTree = tree.getType();
Type lhsType = ASTHelpers.getType(lhsTree);
if (lhsType == null || isImmutableType(lhsType)) {
return Description.NO_MATCH;
}
String newLhsTypeQualifiedName;
String lhsTypeQualifiedName = getTypeQualifiedName(lhsType);
if (MUTABLE_TO_IMMUTABLE_CLASS_NAME_MAP.containsKey(lhsTypeQualifiedName)) {
newLhsTypeQualifiedName = MUTABLE_TO_IMMUTABLE_CLASS_NAME_MAP.get(lhsTypeQualifiedName);
} else {
newLhsTypeQualifiedName = getTypeQualifiedName(rhsType);
}
Type newLhsType = state.getTypeFromString(newLhsTypeQualifiedName);
SuggestedFix.Builder fixBuilder = SuggestedFix.builder();
fixBuilder.replace(
getTypeTree(lhsTree),
SuggestedFixes.qualifyType(state, fixBuilder, newLhsType.asElement()));
SuggestedFix fix = fixBuilder.build();
return describeMatch(lhsTree, fix);
}
private static boolean isImmutableType(Type type) {
return MUTABLE_TO_IMMUTABLE_CLASS_NAME_MAP.values().contains(getTypeQualifiedName(type));
}
private static String getTypeQualifiedName(Type type) {
return type.tsym.getQualifiedName().toString();
}
private static boolean isConstantField(Symbol sym) {
return sym.getKind() == ElementKind.FIELD
&& isStaticFinalField(sym)
&& isConstantFieldName(sym.getSimpleName().toString());
}
private static boolean isStaticFinalField(Symbol sym) {
return sym.isStatic() && sym.getModifiers().contains(Modifier.FINAL);
}
private static boolean isConstantFieldName(String fieldName) {
return fieldName.toUpperCase().equals(fieldName);
}
private static Tree getTypeTree(Tree tree) {
return tree.accept(GET_TYPE_TREE_VISITOR, null /* unused */);
}
private static final SimpleTreeVisitor<Tree, Void> GET_TYPE_TREE_VISITOR =
new SimpleTreeVisitor<Tree, Void>() {
@Override
public Tree visitIdentifier(IdentifierTree tree, Void unused) {
return tree;
}
@Override
public Tree visitParameterizedType(ParameterizedTypeTree tree, Void unused) {
return tree.getType();
}
};
}