Distinguish between Module scope and Universe scope in ValidationEnvironment

This is just a refactoring, no behavior change intended. This change makes
obvious a previous bug we want to fix. It is also a prerequisite for other
changes related to name resolution.

https://github.com/bazelbuild/bazel/issues/5637

RELNOTES: None.
PiperOrigin-RevId: 209761775
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/ValidationEnvironment.java b/src/main/java/com/google/devtools/build/lib/syntax/ValidationEnvironment.java
index 7021b6a..56eede9 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/ValidationEnvironment.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/ValidationEnvironment.java
@@ -26,12 +26,23 @@
 /** A class for doing static checks on files, before evaluating them. */
 public final class ValidationEnvironment extends SyntaxTreeVisitor {
 
+  private enum Scope {
+    /** Symbols defined inside a function or a comprehension. */
+    Local,
+    /** Symbols defined at a module top-level, e.g. functions, loaded symbols. */
+    Module,
+    /** Predefined symbols (builtins) */
+    Universe,
+  }
+
   private static class Block {
     private final Set<String> variables = new HashSet<>();
     private final Set<String> readOnlyVariables = new HashSet<>();
+    private final Scope scope;
     @Nullable private final Block parent;
 
-    Block(@Nullable Block parent) {
+    Block(Scope scope, @Nullable Block parent) {
+      this.scope = scope;
       this.parent = parent;
     }
   }
@@ -60,10 +71,10 @@
   private Block block;
   private int loopCount;
 
-  /** Create a ValidationEnvironment for a given global Environment. */
+  /** Create a ValidationEnvironment for a given global Environment (containing builtins). */
   ValidationEnvironment(Environment env) {
     Preconditions.checkArgument(env.isGlobal());
-    block = new Block(null);
+    block = new Block(Scope.Universe, null);
     Set<String> builtinVariables = env.getVariableNames();
     block.variables.addAll(builtinVariables);
     block.readOnlyVariables.addAll(builtinVariables);
@@ -105,7 +116,7 @@
 
   @Override
   public void visit(ReturnStatement node) {
-    if (isTopLevel()) {
+    if (block.scope != Scope.Local) {
       throw new ValidationException(
           node.getLocation(), "return statements must be inside a function");
     }
@@ -137,7 +148,7 @@
 
   @Override
   public void visit(AbstractComprehension node) {
-    openBlock();
+    openBlock(Scope.Local);
     super.visit(node);
     closeBlock();
   }
@@ -149,7 +160,7 @@
         visit(param.getDefaultValue());
       }
     }
-    openBlock();
+    openBlock(Scope.Local);
     for (Parameter<Expression, Expression> param : node.getParameters()) {
       if (param.hasName()) {
         declare(param.getName(), param.getLocation());
@@ -161,7 +172,7 @@
 
   @Override
   public void visit(IfStatement node) {
-    if (isTopLevel()) {
+    if (block.scope != Scope.Local) {
       throw new ValidationException(
           node.getLocation(),
           "if statements are not allowed at the top level. You may move it inside a function "
@@ -180,20 +191,25 @@
     super.visit(node);
   }
 
-  /** Returns true if the current block is the top level i.e. has no parent. */
-  private boolean isTopLevel() {
-    return block.parent == null;
-  }
-
   /** Declare a variable and add it to the environment. */
   private void declare(String varname, Location location) {
+    boolean readOnlyViolation = false;
     if (block.readOnlyVariables.contains(varname)) {
+      readOnlyViolation = true;
+    }
+    if (block.scope == Scope.Module && block.parent.readOnlyVariables.contains(varname)) {
+      // TODO(laurentlb): This behavior is buggy. Symbols in the module scope should shadow symbols
+      // from the universe. https://github.com/bazelbuild/bazel/issues/5637
+      readOnlyViolation = true;
+    }
+    if (readOnlyViolation) {
       throw new ValidationException(
           location,
           String.format("Variable %s is read only", varname),
           "https://bazel.build/versions/master/docs/skylark/errors/read-only-variable.html");
     }
-    if (isTopLevel()) {  // top-level values are immutable
+    if (block.scope == Scope.Module) {
+      // Symbols defined in the module scope cannot be reassigned.
       block.readOnlyVariables.add(varname);
     }
     block.variables.add(varname);
@@ -255,6 +271,8 @@
       checkLoadAfterStatement(statements);
     }
 
+    openBlock(Scope.Module);
+
     // Add every function in the environment before validating. This is
     // necessary because functions may call other functions defined
     // later in the file.
@@ -266,6 +284,7 @@
     }
 
     this.visitAll(statements);
+    closeBlock();
   }
 
   public static void validateAst(Environment env, List<Statement> statements) throws EvalException {
@@ -293,8 +312,8 @@
   }
 
   /** Open a new lexical block that will contain the future declarations. */
-  private void openBlock() {
-    block = new Block(block);
+  private void openBlock(Scope scope) {
+    block = new Block(scope, block);
   }
 
   /** Close a lexical block (and lose all declarations it contained). */