/*
 * Copyright 2012 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;

import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
import static com.google.errorprone.fixes.SuggestedFixes.qualifyType;
import static com.google.errorprone.matchers.Description.NO_MATCH;
import static com.google.errorprone.util.ASTHelpers.getSymbol;
import static com.google.errorprone.util.ASTHelpers.isStatic;

import com.google.errorprone.BugPattern;
import com.google.errorprone.BugPattern.StandardTags;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker.MemberSelectTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.StatementTree;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.ClassSymbol;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import java.util.Objects;

/**
 * @author eaftan@google.com (Eddie Aftandilian)
 */
@BugPattern(
    summary = "A static variable or method should be qualified with a class name, not expression",
    severity = ERROR,
    altNames = {"static", "static-access", "StaticAccessedFromInstance"},
    tags = StandardTags.FRAGILE_CODE)
public class StaticQualifiedUsingExpression extends BugChecker implements MemberSelectTreeMatcher {

  @Override
  public Description matchMemberSelect(MemberSelectTree tree, VisitorState state) {
    Symbol sym = getSymbol(tree);
    if (sym == null) {
      return NO_MATCH;
    }
    switch (sym.getKind()) {
      case FIELD:
        if (sym.getSimpleName().contentEquals("class")
            || sym.getSimpleName().contentEquals("super")) {
          return NO_MATCH;
        }
      // fall through
      case ENUM_CONSTANT:
      case METHOD:
        if (!isStatic(sym)) {
          return NO_MATCH;
        }
        break; // continue below
      default:
        return NO_MATCH;
    }
    ClassSymbol owner = sym.owner.enclClass();
    ExpressionTree expression = tree.getExpression();
    switch (expression.getKind()) {
      case MEMBER_SELECT, IDENTIFIER -> {
        // References to static variables should be qualified by the type name of the owning type,
        // or a sub-type. e.g.: if CONST is declared in Foo, and SubFoo extends Foo,
        // allow `Foo.CONST` and `SubFoo.CONST` (but not, say, `new Foo().CONST`.
        Symbol base = getSymbol(expression);
        if (base instanceof ClassSymbol && base.isSubClass(owner, state.getTypes())) {
          return NO_MATCH;
        }
      }
      default -> {
        // continue below
      }
    }
    SuggestedFix.Builder fix = SuggestedFix.builder();
    String replacement;
    boolean isMethod = sym instanceof MethodSymbol;
    if (isMethod && Objects.equals(getSymbol(state.findEnclosing(ClassTree.class)), owner)) {
      replacement = sym.getSimpleName().toString();
    } else {
      replacement = qualifyType(state, fix, sym);
    }
    fix.replace(tree, replacement);

    // Spill possibly side-effectful qualifier expressions to the top level.
    // This doesn't preserve order of operations for non-trivial expressions, but we don't have
    // letexprs and hopefully it'll call attention to the fact that just deleting the qualifier
    // might not always be the right fix.
    if (expression instanceof MethodInvocationTree || expression instanceof NewClassTree) {
      StatementTree statement = state.findEnclosing(StatementTree.class);
      if (statement != null) {
        fix.prefixWith(statement, state.getSourceForNode(expression) + ";");
      }
    }

    return buildDescription(tree)
        .setMessage(
            String.format(
                "Static %s %s should not be accessed from an object instance; instead use %s",
                isMethod ? "method" : "variable", sym.getSimpleName(), replacement))
        .addFix(fix.build())
        .build();
  }
}
