/*
 * 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.collect.ImmutableMap;
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 = "Field is constant (named CONSTANT_CASE), but field type is mutable",
  severity = WARNING
)
public final class MutableConstantField extends BugChecker implements VariableTreeMatcher {

  private static final ImmutableMap<String, String> MUTABLE_TO_IMMUTABLE_CLASS_NAME_MAP =
      ImmutableMap.<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.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.List", "com.google.common.collect.ImmutableList")
          .put("java.util.Map", "com.google.common.collect.ImmutableMap")
          .put("java.util.Multimap", "com.google.common.collect.ImmutableMultimap")
          .put("java.util.Multiset", "com.google.common.collect.ImmutableMultiset")
          .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 lhsTree = tree.getType();
    Symbol lhsSymbol = ASTHelpers.getSymbol(lhsTree);
    if (lhsSymbol == null) {
      return Description.NO_MATCH;
    }
    String lhsTypeQualifiedName = lhsSymbol.getQualifiedName().toString();
    if (!MUTABLE_TO_IMMUTABLE_CLASS_NAME_MAP.containsKey(lhsTypeQualifiedName)) {
      return Description.NO_MATCH;
    }

    String immutableClassName = MUTABLE_TO_IMMUTABLE_CLASS_NAME_MAP.get(lhsTypeQualifiedName);
    Type immutableType = state.getTypeFromString(immutableClassName);
    Tree rhsTree = tree.getInitializer();
    Type rhsType = ASTHelpers.getType(rhsTree);
    if (!ASTHelpers.isSameType(rhsType, immutableType, state)) {
      return Description.NO_MATCH;
    }

    SuggestedFix.Builder fixBuilder = SuggestedFix.builder();
    fixBuilder.replace(
        getTypeTree(lhsTree),
        SuggestedFixes.qualifyType(state, fixBuilder, immutableType.asElement()));
    SuggestedFix fix = fixBuilder.build();

    return describeMatch(lhsTree, fix);
  }

  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();
        }
      };
}

@@ -1740,16 +1740,27 @@
 ut field
+'s declared
  type is

Clarify error message for MutableConstantField, to emphasize the _declared_ type.