// Copyright 2017 The Bazel Authors. 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.devtools.skylark.skylint;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.events.Location;
import com.google.devtools.build.lib.syntax.ASTNode;
import com.google.devtools.build.lib.syntax.AssignmentStatement;
import com.google.devtools.build.lib.syntax.BuildFileAST;
import com.google.devtools.build.lib.syntax.Expression;
import com.google.devtools.build.lib.syntax.FuncallExpression;
import com.google.devtools.build.lib.syntax.Identifier;
import com.google.devtools.skylark.skylint.Environment.NameInfo;
import com.google.devtools.skylark.skylint.Environment.NameInfo.Kind;
import java.util.ArrayList;
import java.util.List;

/**
 * Checks the adherence to Skylark naming conventions.
 *
 * <ul>
 *   <li>Functions and parameters should be lower_snake_case and all other identifiers should be
 *       lower_snake_case (for variables) or UPPER_SNAKE_CASE (for constants).
 *   <li>Providers are required to be UpperCamelCase. A variable FooBar is considered a provider if
 *       it appears in an assignment of the form "FooBar = provider(...)".
 *   <li>Shadowing of builtins (e.g. "True = False", "def fail()") is not allowed.
 *   <li>The single-letter variable names 'O', 'l', 'I' are disallowed since they're easy to
 *       confuse.
 *   <li>Multi-underscore names ('__', '___', etc.) are disallowed.
 *   <li>Single-underscore names may only be written to, as in "a, _ = tuple". They may not be read,
 *       as in "f(_)".
 * </ul>
 */
// TODO(skylark-team): Check that UPPERCASE_VARIABLES are never mutated
public class NamingConventionsChecker extends AstVisitorWithNameResolution {
  private static final ImmutableList<String> CONFUSING_NAMES = ImmutableList.of("O", "I", "l");
  private static final ImmutableSet<String> BUILTIN_NAMES;
  private final List<Issue> issues = new ArrayList<>();

  static {
    Environment env = Environment.defaultBazel();
    BUILTIN_NAMES =
        env.getNameIdsInCurrentBlock()
            .stream()
            .map(id -> env.getNameInfo(id).name)
            .collect(ImmutableSet.toImmutableSet());
  }

  public static List<Issue> check(BuildFileAST ast) {
    NamingConventionsChecker checker = new NamingConventionsChecker();
    checker.visit(ast);
    return checker.issues;
  }

  @Override
  public void visit(AssignmentStatement node) {
    // Check for the pattern "FooBar = provider(...)" because CamelCase for provider names is OK
    Expression lvalue = node.getLValue().getExpression();
    Expression rhs = node.getExpression();
    if (lvalue instanceof Identifier && rhs instanceof FuncallExpression) {
      Expression function = ((FuncallExpression) rhs).getFunction();
      if (function instanceof Identifier && ((Identifier) function).getName().equals("provider")) {
        checkProviderName(((Identifier) lvalue).getName(), lvalue.getLocation());
        visit(rhs);
        return;
      }
    }
    super.visit(node);
  }

  private void checkSnakeCase(String name, Location location) {
    if (!isSnakeCase(name)) {
      issues.add(
          new Issue(
              "identifier '"
                  + name
                  + "' should be lower_snake_case (for variables)"
                  + " or UPPER_SNAKE_CASE (for constants)",
              location));
    }
  }

  private void checkLowerSnakeCase(String name, Location location) {
    if (!isLowerSnakeCase(name)) {
      issues.add(new Issue("identifier '" + name + "' should be lower_snake_case", location));
    }
  }

  private void checkProviderName(String name, Location location) {
    if (!isUpperCamelCase(name)) {
      issues.add(new Issue("provider name '" + name + "' should be UpperCamelCase", location));
    }
  }

  private void checkNameNotConfusing(String name, Location location) {
    if (CONFUSING_NAMES.contains(name)) {
      issues.add(
          new Issue(
              "never use 'l', 'I', or 'O' as names "
                  + "(they're too easily confused with 'I', 'l', or '0')",
              location));
    }
    if (BUILTIN_NAMES.contains(name)) {
      issues.add(
          new Issue(
              "identifier '" + name + "' shadows a builtin; please pick a different name",
              location));
    }
    if (name.chars().allMatch(c -> c == '_') && name.length() >= 2) {
      issues.add(
          new Issue(
              "identifier '"
                  + name
                  + "' consists only of underscores; please pick a different name",
              location));
    }
  }

  @Override
  void use(Identifier identifier) {
    if (identifier.getName().equals("_")) {
      issues.add(
          new Issue(
              "don't use '_' as an identifier, only to ignore the result in an assignment",
              identifier.getLocation()));
    }
  }

  @Override
  void declare(String name, ASTNode node) {
    NameInfo nameInfo = env.resolveExistingName(name);
    checkNameNotConfusing(name, node.getLocation());
    if (nameInfo.kind == Kind.PARAMETER || nameInfo.kind == Kind.FUNCTION) {
      checkLowerSnakeCase(nameInfo.name, node.getLocation());
    } else {
      checkSnakeCase(nameInfo.name, node.getLocation());
    }
  }

  private static boolean isUpperCamelCase(String name) {
    return !name.contains("_") && Character.isUpperCase(name.charAt(0));
  }

  private static boolean isSnakeCase(String name) {
    return isUpperSnakeCase(name) || isLowerSnakeCase(name);
  }

  private static boolean isUpperSnakeCase(String name) {
    return name.equals(name.toUpperCase());
  }

  private static boolean isLowerSnakeCase(String name) {
    return name.equals(name.toLowerCase());
  }
}
