| // Copyright 2023 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.build.lib.packages; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadHostile; | |
| import net.starlark.java.syntax.Argument; | |
| import net.starlark.java.syntax.CallExpression; | |
| import net.starlark.java.syntax.DefStatement; | |
| import net.starlark.java.syntax.DictExpression; | |
| import net.starlark.java.syntax.ForStatement; | |
| import net.starlark.java.syntax.IfStatement; | |
| import net.starlark.java.syntax.LambdaExpression; | |
| import net.starlark.java.syntax.LoadStatement; | |
| import net.starlark.java.syntax.Location; | |
| import net.starlark.java.syntax.NodeVisitor; | |
| import net.starlark.java.syntax.StarlarkFile; | |
| import net.starlark.java.syntax.SyntaxError; | |
| /** | |
| * A {@link NodeVisitor} that can be used to check that a Starlark AST conforms to the restricted | |
| * syntax that BUILD, WORKSPACE, REPO.bazel, and MODULE.bazel files use. This restricted syntax | |
| * disallows: | |
| * | |
| * <ul> | |
| * <li>control-flow statements ({@code for} and {@code if}, but not comprehensions and {@code if} | |
| * expressions), | |
| * <li>function definitions ({@code def} and {@code lambda}), | |
| * <li>variadic arguments ({@code *args} and {@code **kwargs}) in function call sites, and | |
| * <li>optionally, {@code load()} statements. | |
| * </ul> | |
| */ | |
| @ThreadHostile | |
| public class DotBazelFileSyntaxChecker extends NodeVisitor { | |
| private final String where; | |
| private final boolean canLoadBzl; | |
| private final boolean allowLiteralStarStarArgs; | |
| private ImmutableList.Builder<SyntaxError> errors = ImmutableList.builder(); | |
| /** | |
| * @param where describes the type of file being checked. | |
| * @param canLoadBzl whether the file type being check supports load statements. This is used to | |
| * generate more informative error messages. | |
| * @param allowLiteralStarStarArgs whether to allow **kwargs in function calls if the dict is a | |
| * literal. This is needed for some functions that take arbitrary keyword arguments whose keys | |
| * may have to contain non-identifier characters. | |
| */ | |
| public DotBazelFileSyntaxChecker( | |
| String where, boolean canLoadBzl, boolean allowLiteralStarStarArgs) { | |
| this.where = where; | |
| this.canLoadBzl = canLoadBzl; | |
| this.allowLiteralStarStarArgs = allowLiteralStarStarArgs; | |
| } | |
| public DotBazelFileSyntaxChecker(String where, boolean canLoadBzl) { | |
| this(where, canLoadBzl, /* allowLiteralStarStarArgs= */ false); | |
| } | |
| public final void check(StarlarkFile file) throws SyntaxError.Exception { | |
| this.errors = ImmutableList.builder(); | |
| visit(file); | |
| ImmutableList<SyntaxError> errors = this.errors.build(); | |
| if (!errors.isEmpty()) { | |
| throw new SyntaxError.Exception(errors); | |
| } | |
| } | |
| protected void error(Location loc, String message) { | |
| errors.add(new SyntaxError(loc, message)); | |
| } | |
| // Reject f(*args) and f(**kwargs) calls. | |
| private void rejectStarArgs(CallExpression call) { | |
| for (Argument arg : call.getArguments()) { | |
| if (arg instanceof Argument.StarStar starStar) { | |
| if (!allowLiteralStarStarArgs) { | |
| error( | |
| arg.getStartLocation(), | |
| "**kwargs arguments are not allowed in " | |
| + where | |
| + ". Pass the arguments in explicitly."); | |
| } | |
| if (!(starStar.getValue() instanceof DictExpression)) { | |
| error( | |
| arg.getStartLocation(), | |
| "**kwargs arguments must be a literal dict in " + where + "."); | |
| } | |
| } else if (arg instanceof Argument.Star) { | |
| error( | |
| arg.getStartLocation(), | |
| "*args arguments are not allowed in " + where + ". Pass the arguments in explicitly."); | |
| } | |
| } | |
| } | |
| @Override | |
| public void visit(LoadStatement node) { | |
| if (!canLoadBzl) { | |
| error(node.getStartLocation(), "`load` statements may not be used in " + where); | |
| } | |
| } | |
| // We prune the traversal if we encounter disallowed keywords, as we have already reported the | |
| // root error and there's no point reporting more. | |
| @Override | |
| public void visit(DefStatement node) { | |
| error( | |
| node.getStartLocation(), | |
| "functions may not be defined in " | |
| + where | |
| + (canLoadBzl ? ". You may move the function to a .bzl file and load it." : ".")); | |
| } | |
| @Override | |
| public void visit(LambdaExpression node) { | |
| error( | |
| node.getStartLocation(), | |
| "functions may not be defined in " | |
| + where | |
| + (canLoadBzl ? ". You may move the function to a .bzl file and load it." : ".")); | |
| } | |
| @Override | |
| public void visit(ForStatement node) { | |
| error( | |
| node.getStartLocation(), | |
| "`for` statements are not allowed in " | |
| + where | |
| + ". You may inline the loop" | |
| + (canLoadBzl ? ", move it to a function definition (in a .bzl file)," : "") | |
| + " or as a last resort use a list comprehension."); | |
| } | |
| @Override | |
| public void visit(IfStatement node) { | |
| error( | |
| node.getStartLocation(), | |
| "`if` statements are not allowed in " | |
| + where | |
| + ". You may" | |
| + (canLoadBzl | |
| ? " move conditional logic to a function definition (in a .bzl file), or" | |
| : "") | |
| + " use an `if` expression for simple cases."); | |
| } | |
| @Override | |
| public void visit(CallExpression node) { | |
| rejectStarArgs(node); | |
| // Continue traversal so as not to miss nested calls | |
| // like cc_binary(..., f(**kwargs), ...). | |
| super.visit(node); | |
| } | |
| } |