blob: 56eede9b9e9f715baf99b7bfdad1394b43602c18 [file] [log] [blame]
Damien Martin-Guillerezf88f4d82015-09-25 13:56:55 +00001// Copyright 2014 The Bazel Authors. All rights reserved.
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +01002//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package com.google.devtools.build.lib.syntax;
16
tomlua155b532017-11-08 20:12:47 +010017import com.google.common.base.Preconditions;
Laurent Le Brun8e965b82016-08-03 11:50:24 +000018import com.google.devtools.build.lib.events.Event;
19import com.google.devtools.build.lib.events.EventHandler;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010020import com.google.devtools.build.lib.events.Location;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010021import java.util.HashSet;
Laurent Le Brun68743162015-05-13 13:18:09 +000022import java.util.List;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010023import java.util.Set;
laurentlb137e6c82017-08-16 20:16:39 +020024import javax.annotation.Nullable;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010025
laurentlb65345cd2017-08-17 19:59:59 +020026/** A class for doing static checks on files, before evaluating them. */
laurentlbaa8cc6c2017-08-17 15:39:50 +020027public final class ValidationEnvironment extends SyntaxTreeVisitor {
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010028
laurentlbf7956d62018-08-22 07:25:28 -070029 private enum Scope {
30 /** Symbols defined inside a function or a comprehension. */
31 Local,
32 /** Symbols defined at a module top-level, e.g. functions, loaded symbols. */
33 Module,
34 /** Predefined symbols (builtins) */
35 Universe,
36 }
37
laurentlb65345cd2017-08-17 19:59:59 +020038 private static class Block {
laurentlb137e6c82017-08-16 20:16:39 +020039 private final Set<String> variables = new HashSet<>();
40 private final Set<String> readOnlyVariables = new HashSet<>();
laurentlbf7956d62018-08-22 07:25:28 -070041 private final Scope scope;
laurentlb65345cd2017-08-17 19:59:59 +020042 @Nullable private final Block parent;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010043
laurentlbf7956d62018-08-22 07:25:28 -070044 Block(Scope scope, @Nullable Block parent) {
45 this.scope = scope;
laurentlb137e6c82017-08-16 20:16:39 +020046 this.parent = parent;
47 }
48 }
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010049
laurentlbaa8cc6c2017-08-17 15:39:50 +020050 /**
51 * We use an unchecked exception around EvalException because the SyntaxTreeVisitor doesn't let
52 * visit methods throw checked exceptions. We might change that later.
53 */
54 private static class ValidationException extends RuntimeException {
55 EvalException exception;
56
57 ValidationException(EvalException e) {
58 exception = e;
59 }
60
61 ValidationException(Location location, String message, String url) {
62 exception = new EvalException(location, message, url);
63 }
64
65 ValidationException(Location location, String message) {
66 exception = new EvalException(location, message);
67 }
68 }
69
brandjon3c161912017-10-05 05:06:05 +020070 private final SkylarkSemantics semantics;
laurentlb65345cd2017-08-17 19:59:59 +020071 private Block block;
laurentlba9b9aea2017-09-04 17:39:09 +020072 private int loopCount;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010073
laurentlbf7956d62018-08-22 07:25:28 -070074 /** Create a ValidationEnvironment for a given global Environment (containing builtins). */
laurentlbbde7c412017-06-12 15:22:37 +020075 ValidationEnvironment(Environment env) {
Francois-Rene Rideau6e7160d2015-08-26 17:22:35 +000076 Preconditions.checkArgument(env.isGlobal());
laurentlbf7956d62018-08-22 07:25:28 -070077 block = new Block(Scope.Universe, null);
Francois-Rene Rideau89312fb2015-09-10 18:53:03 +000078 Set<String> builtinVariables = env.getVariableNames();
laurentlb65345cd2017-08-17 19:59:59 +020079 block.variables.addAll(builtinVariables);
80 block.readOnlyVariables.addAll(builtinVariables);
laurentlba0fd7662017-05-10 12:26:15 -040081 semantics = env.getSemantics();
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010082 }
83
laurentlbaa8cc6c2017-08-17 15:39:50 +020084 @Override
85 public void visit(LoadStatement node) {
86 for (Identifier symbol : node.getSymbols()) {
87 declare(symbol.getName(), node.getLocation());
88 }
89 }
90
91 @Override
92 public void visit(Identifier node) {
93 if (!hasSymbolInEnvironment(node.getName())) {
94 throw new ValidationException(node.createInvalidIdentifierException(getAllSymbols()));
95 }
96 }
97
98 private void validateLValue(Location loc, Expression expr) {
99 if (expr instanceof Identifier) {
100 declare(((Identifier) expr).getName(), loc);
101 } else if (expr instanceof IndexExpression) {
102 visit(expr);
103 } else if (expr instanceof ListLiteral) {
104 for (Expression e : ((ListLiteral) expr).getElements()) {
105 validateLValue(loc, e);
106 }
107 } else {
108 throw new ValidationException(loc, "cannot assign to '" + expr + "'");
109 }
110 }
111
112 @Override
113 public void visit(LValue node) {
114 validateLValue(node.getLocation(), node.getExpression());
115 }
116
117 @Override
118 public void visit(ReturnStatement node) {
laurentlbf7956d62018-08-22 07:25:28 -0700119 if (block.scope != Scope.Local) {
laurentlbaa8cc6c2017-08-17 15:39:50 +0200120 throw new ValidationException(
laurentlba9b9aea2017-09-04 17:39:09 +0200121 node.getLocation(), "return statements must be inside a function");
122 }
123 super.visit(node);
124 }
125
126 @Override
127 public void visit(ForStatement node) {
128 loopCount++;
129 super.visit(node);
130 Preconditions.checkState(loopCount > 0);
131 loopCount--;
132 }
133
134 @Override
135 public void visit(FlowStatement node) {
136 if (loopCount <= 0) {
137 throw new ValidationException(
138 node.getLocation(), node.getKind().getName() + " statement must be inside a for loop");
laurentlbaa8cc6c2017-08-17 15:39:50 +0200139 }
140 super.visit(node);
141 }
142
143 @Override
144 public void visit(DotExpression node) {
145 visit(node.getObject());
146 // Do not visit the field.
147 }
148
149 @Override
150 public void visit(AbstractComprehension node) {
laurentlbf7956d62018-08-22 07:25:28 -0700151 openBlock(Scope.Local);
laurentlb2195b1c2018-02-16 04:14:46 -0800152 super.visit(node);
153 closeBlock();
laurentlbaa8cc6c2017-08-17 15:39:50 +0200154 }
155
156 @Override
157 public void visit(FunctionDefStatement node) {
158 for (Parameter<Expression, Expression> param : node.getParameters()) {
159 if (param.isOptional()) {
160 visit(param.getDefaultValue());
161 }
162 }
laurentlbf7956d62018-08-22 07:25:28 -0700163 openBlock(Scope.Local);
laurentlbaa8cc6c2017-08-17 15:39:50 +0200164 for (Parameter<Expression, Expression> param : node.getParameters()) {
165 if (param.hasName()) {
166 declare(param.getName(), param.getLocation());
167 }
168 }
laurentlb65345cd2017-08-17 19:59:59 +0200169 visitAll(node.getStatements());
170 closeBlock();
laurentlbaa8cc6c2017-08-17 15:39:50 +0200171 }
172
laurentlb65345cd2017-08-17 19:59:59 +0200173 @Override
174 public void visit(IfStatement node) {
laurentlbf7956d62018-08-22 07:25:28 -0700175 if (block.scope != Scope.Local) {
laurentlb65345cd2017-08-17 19:59:59 +0200176 throw new ValidationException(
177 node.getLocation(),
178 "if statements are not allowed at the top level. You may move it inside a function "
laurentlb2213d922018-04-12 09:27:57 -0700179 + "or use an if expression (x if condition else y).");
laurentlb65345cd2017-08-17 19:59:59 +0200180 }
181 super.visit(node);
182 }
183
laurentlbe3684492017-08-21 12:02:46 +0200184 @Override
185 public void visit(AugmentedAssignmentStatement node) {
186 if (node.getLValue().getExpression() instanceof ListLiteral) {
187 throw new ValidationException(
188 node.getLocation(), "cannot perform augmented assignment on a list or tuple expression");
189 }
190 // Other bad cases are handled when visiting the LValue node.
191 super.visit(node);
192 }
193
laurentlbbde7c412017-06-12 15:22:37 +0200194 /** Declare a variable and add it to the environment. */
laurentlbaa8cc6c2017-08-17 15:39:50 +0200195 private void declare(String varname, Location location) {
laurentlbf7956d62018-08-22 07:25:28 -0700196 boolean readOnlyViolation = false;
laurentlb65345cd2017-08-17 19:59:59 +0200197 if (block.readOnlyVariables.contains(varname)) {
laurentlbf7956d62018-08-22 07:25:28 -0700198 readOnlyViolation = true;
199 }
200 if (block.scope == Scope.Module && block.parent.readOnlyVariables.contains(varname)) {
201 // TODO(laurentlb): This behavior is buggy. Symbols in the module scope should shadow symbols
202 // from the universe. https://github.com/bazelbuild/bazel/issues/5637
203 readOnlyViolation = true;
204 }
205 if (readOnlyViolation) {
laurentlbaa8cc6c2017-08-17 15:39:50 +0200206 throw new ValidationException(
Laurent Le Brunfa407e52016-11-04 15:53:08 +0000207 location,
208 String.format("Variable %s is read only", varname),
209 "https://bazel.build/versions/master/docs/skylark/errors/read-only-variable.html");
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100210 }
laurentlbf7956d62018-08-22 07:25:28 -0700211 if (block.scope == Scope.Module) {
212 // Symbols defined in the module scope cannot be reassigned.
laurentlb65345cd2017-08-17 19:59:59 +0200213 block.readOnlyVariables.add(varname);
214 }
215 block.variables.add(varname);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100216 }
217
laurentlb9d5c0a02017-06-13 23:08:06 +0200218 /** Returns true if the symbol exists in the validation environment (or a parent). */
laurentlbaa8cc6c2017-08-17 15:39:50 +0200219 private boolean hasSymbolInEnvironment(String varname) {
laurentlb65345cd2017-08-17 19:59:59 +0200220 for (Block b = block; b != null; b = b.parent) {
221 if (b.variables.contains(varname)) {
laurentlb137e6c82017-08-16 20:16:39 +0200222 return true;
223 }
224 }
225 return false;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100226 }
227
Laurent Le Brune102a2d2017-01-02 12:06:18 +0000228 /** Returns the set of all accessible symbols (both local and global) */
laurentlbaa8cc6c2017-08-17 15:39:50 +0200229 private Set<String> getAllSymbols() {
Laurent Le Brune102a2d2017-01-02 12:06:18 +0000230 Set<String> all = new HashSet<>();
laurentlb65345cd2017-08-17 19:59:59 +0200231 for (Block b = block; b != null; b = b.parent) {
232 all.addAll(b.variables);
Laurent Le Brune102a2d2017-01-02 12:06:18 +0000233 }
234 return all;
235 }
236
laurentlbaa8cc6c2017-08-17 15:39:50 +0200237 /** Throws ValidationException if a load() appears after another kind of statement. */
238 private static void checkLoadAfterStatement(List<Statement> statements) {
laurentlba0fd7662017-05-10 12:26:15 -0400239 Location firstStatement = null;
240
241 for (Statement statement : statements) {
242 // Ignore string literals (e.g. docstrings).
243 if (statement instanceof ExpressionStatement
244 && ((ExpressionStatement) statement).getExpression() instanceof StringLiteral) {
245 continue;
246 }
247
248 if (statement instanceof LoadStatement) {
249 if (firstStatement == null) {
250 continue;
251 }
laurentlbaa8cc6c2017-08-17 15:39:50 +0200252 throw new ValidationException(
laurentlba0fd7662017-05-10 12:26:15 -0400253 statement.getLocation(),
254 "load() statements must be called before any other statement. "
255 + "First non-load() statement appears at "
256 + firstStatement
brandjonf5b8d6f2017-06-23 18:03:28 +0200257 + ". Use --incompatible_bzl_disallow_load_after_statement=false to temporarily "
258 + "disable this check.");
laurentlba0fd7662017-05-10 12:26:15 -0400259 }
260
261 if (firstStatement == null) {
262 firstStatement = statement.getLocation();
263 }
264 }
265 }
266
laurentlbbde7c412017-06-12 15:22:37 +0200267 /** Validates the AST and runs static checks. */
laurentlbaa8cc6c2017-08-17 15:39:50 +0200268 private void validateAst(List<Statement> statements) {
laurentlba0fd7662017-05-10 12:26:15 -0400269 // Check that load() statements are on top.
brandjon3c161912017-10-05 05:06:05 +0200270 if (semantics.incompatibleBzlDisallowLoadAfterStatement()) {
laurentlba0fd7662017-05-10 12:26:15 -0400271 checkLoadAfterStatement(statements);
272 }
273
laurentlbf7956d62018-08-22 07:25:28 -0700274 openBlock(Scope.Module);
275
Laurent Le Brun68743162015-05-13 13:18:09 +0000276 // Add every function in the environment before validating. This is
277 // necessary because functions may call other functions defined
278 // later in the file.
279 for (Statement statement : statements) {
280 if (statement instanceof FunctionDefStatement) {
281 FunctionDefStatement fct = (FunctionDefStatement) statement;
brandjon990622b2017-07-11 19:56:45 +0200282 declare(fct.getIdentifier().getName(), fct.getLocation());
Laurent Le Brun68743162015-05-13 13:18:09 +0000283 }
284 }
285
laurentlb65345cd2017-08-17 19:59:59 +0200286 this.visitAll(statements);
laurentlbf7956d62018-08-22 07:25:28 -0700287 closeBlock();
Laurent Le Brun68743162015-05-13 13:18:09 +0000288 }
Florian Weikert917ceaa2015-06-10 13:54:26 +0000289
laurentlbbde7c412017-06-12 15:22:37 +0200290 public static void validateAst(Environment env, List<Statement> statements) throws EvalException {
laurentlbaa8cc6c2017-08-17 15:39:50 +0200291 try {
292 ValidationEnvironment venv = new ValidationEnvironment(env);
293 venv.validateAst(statements);
laurentlb65345cd2017-08-17 19:59:59 +0200294 // Check that no closeBlock was forgotten.
295 Preconditions.checkState(venv.block.parent == null);
laurentlbaa8cc6c2017-08-17 15:39:50 +0200296 } catch (ValidationException e) {
297 throw e.exception;
298 }
laurentlbbde7c412017-06-12 15:22:37 +0200299 }
300
301 public static boolean validateAst(
302 Environment env, List<Statement> statements, EventHandler eventHandler) {
Laurent Le Brun8e965b82016-08-03 11:50:24 +0000303 try {
laurentlbbde7c412017-06-12 15:22:37 +0200304 validateAst(env, statements);
Laurent Le Brun8e965b82016-08-03 11:50:24 +0000305 return true;
306 } catch (EvalException e) {
307 if (!e.isDueToIncompleteAST()) {
308 eventHandler.handle(Event.error(e.getLocation(), e.getMessage()));
309 }
310 return false;
311 }
312 }
laurentlb137e6c82017-08-16 20:16:39 +0200313
laurentlb65345cd2017-08-17 19:59:59 +0200314 /** Open a new lexical block that will contain the future declarations. */
laurentlbf7956d62018-08-22 07:25:28 -0700315 private void openBlock(Scope scope) {
316 block = new Block(scope, block);
laurentlb137e6c82017-08-16 20:16:39 +0200317 }
318
laurentlb65345cd2017-08-17 19:59:59 +0200319 /** Close a lexical block (and lose all declarations it contained). */
320 private void closeBlock() {
321 block = Preconditions.checkNotNull(block.parent);
laurentlb137e6c82017-08-16 20:16:39 +0200322 }
laurentlbab58a922017-08-22 16:45:28 +0200323
324 /**
325 * Checks that the AST is using the restricted syntax.
326 *
327 * <p>Restricted syntax is used by Bazel BUILD files. It forbids function definitions, *args, and
328 * **kwargs. This creates a better separation between code and data.
329 */
330 public static boolean checkBuildSyntax(
331 List<Statement> statements, final EventHandler eventHandler) {
332 // Wrap the boolean inside an array so that the inner class can modify it.
333 final boolean[] success = new boolean[] {true};
334 // TODO(laurentlb): Merge with the visitor above when possible (i.e. when BUILD files use it).
335 SyntaxTreeVisitor checker =
336 new SyntaxTreeVisitor() {
brandjonaadf6602018-01-17 10:40:38 -0800337
338 private void error(ASTNode node, String message) {
339 eventHandler.handle(Event.error(node.getLocation(), message));
340 success[0] = false;
laurentlbab58a922017-08-22 16:45:28 +0200341 }
342
343 @Override
344 public void visit(FunctionDefStatement node) {
brandjonaadf6602018-01-17 10:40:38 -0800345 error(
346 node,
347 "function definitions are not allowed in BUILD files. You may move the function to "
348 + "a .bzl file and load it.");
349 }
350
351 @Override
352 public void visit(ForStatement node) {
353 error(
354 node,
355 "for statements are not allowed in BUILD files. You may inline the loop, move it "
356 + "to a function definition (in a .bzl file), or as a last resort use a list "
357 + "comprehension.");
358 }
359
360 @Override
361 public void visit(IfStatement node) {
362 error(
363 node,
364 "if statements are not allowed in BUILD files. You may move conditional logic to a "
365 + "function definition (in a .bzl file), or for simple cases use an if "
366 + "expression.");
367 }
368
369 @Override
370 public void visit(FuncallExpression node) {
371 for (Argument.Passed arg : node.getArguments()) {
372 if (arg.isStarStar()) {
373 error(
374 node,
375 "**kwargs arguments are not allowed in BUILD files. Pass the arguments in "
376 + "explicitly.");
377 } else if (arg.isStar()) {
378 error(
379 node,
380 "*args arguments are not allowed in BUILD files. Pass the arguments in "
381 + "explicitly.");
382 }
383 }
laurentlbab58a922017-08-22 16:45:28 +0200384 }
385 };
386 checker.visitAll(statements);
387 return success[0];
388 }
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100389}