blob: cca66fc09668b8766402e451615fe57703a65108 [file] [log] [blame]
// 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 String NAME_WITH_WRONG_CASE_CATEGORY = "name-with-wrong-case";
private static final String PROVIDER_NAME_ENDS_IN_INFO_CATEGORY = "provider-name-suffix";
private static final String CONFUSING_NAME_CATEGORY = "confusing-name";
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(
Issue.create(
NAME_WITH_WRONG_CASE_CATEGORY,
"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(
Issue.create(
NAME_WITH_WRONG_CASE_CATEGORY,
"identifier '" + name + "' should be lower_snake_case",
location));
}
}
private void checkProviderName(String name, Location location) {
if (!isUpperCamelCase(name)) {
issues.add(
Issue.create(
NAME_WITH_WRONG_CASE_CATEGORY,
"provider name '" + name + "' should be UpperCamelCase",
location));
}
if (!name.endsWith("Info")) {
issues.add(
Issue.create(
PROVIDER_NAME_ENDS_IN_INFO_CATEGORY,
"provider name '" + name + "' should end in the suffix 'Info'",
location)
);
}
}
private void checkNameNotConfusing(String name, Location location) {
if (CONFUSING_NAMES.contains(name)) {
issues.add(
Issue.create(
CONFUSING_NAME_CATEGORY,
"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(
Issue.create(
CONFUSING_NAME_CATEGORY,
"identifier '" + name + "' shadows a builtin; please pick a different name",
location));
}
if (name.chars().allMatch(c -> c == '_') && name.length() >= 2) {
issues.add(
Issue.create(
CONFUSING_NAME_CATEGORY,
"identifier '"
+ name
+ "' consists only of underscores; please pick a different name",
location));
}
}
@Override
void use(Identifier identifier) {
if (identifier.getName().equals("_")) {
issues.add(
Issue.create(
CONFUSING_NAME_CATEGORY,
"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);
if (nameInfo.kind == Kind.IMPORTED) {
// Users may not have control over imported names, so ignore them:
return;
}
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) {
if (name.startsWith("_")) {
name = name.substring(1); // private providers are allowed
}
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());
}
}