Add 'did you mean' suggestion when accessing an undefined variable.

--
PiperOrigin-RevId: 143373605
MOS_MIGRATED_REVID=143373605
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Environment.java b/src/main/java/com/google/devtools/build/lib/syntax/Environment.java
index 11515ef..37fcdd2 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Environment.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Environment.java
@@ -14,7 +14,6 @@
 
 package com.google.devtools.build.lib.syntax;
 
-import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.devtools.build.lib.cmdline.Label;
@@ -723,11 +722,7 @@
     eventHandler.handle(event);
   }
 
-  /**
-   * @return the (immutable) set of names of all variables defined in this
-   * Environment. Exposed for testing.
-   */
-  @VisibleForTesting
+  /** @return the (immutable) set of names of all variables defined in this Environment. */
   public Set<String> getVariableNames() {
     Set<String> vars = new HashSet<>();
     if (lexicalFrame != null) {
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/FuncallExpression.java b/src/main/java/com/google/devtools/build/lib/syntax/FuncallExpression.java
index b7c2d59..30b9417 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/FuncallExpression.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/FuncallExpression.java
@@ -832,15 +832,13 @@
 
   @Override
   void validate(ValidationEnvironment env) throws EvalException {
-    for (Argument.Passed arg : args) {
-      arg.getValue().validate(env);
-    }
-
     if (obj != null) {
       obj.validate(env);
-    } else if (!env.hasSymbolInEnvironment(func.getName())) {
-      throw new EvalException(getLocation(),
-          String.format("function '%s' does not exist", func.getName()));
+    } else {
+      func.validate(env);
+    }
+    for (Argument.Passed arg : args) {
+      arg.getValue().validate(env);
     }
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Identifier.java b/src/main/java/com/google/devtools/build/lib/syntax/Identifier.java
index f953b05..83335e2 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Identifier.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Identifier.java
@@ -17,10 +17,10 @@
 import com.google.devtools.build.lib.syntax.compiler.DebugInfo;
 import com.google.devtools.build.lib.syntax.compiler.Variable.SkylarkVariable;
 import com.google.devtools.build.lib.syntax.compiler.VariableScope;
-
-import net.bytebuddy.implementation.bytecode.ByteCodeAppender;
-
+import com.google.devtools.build.lib.util.SpellChecker;
+import java.util.Set;
 import javax.annotation.Nullable;
+import net.bytebuddy.implementation.bytecode.ByteCodeAppender;
 
 // TODO(bazel-team): for extra performance:
 // (1) intern the strings, so we can use == to compare, and have .equals use the assumption.
@@ -74,7 +74,7 @@
   Object doEval(Environment env) throws EvalException {
     Object value = env.lookup(name);
     if (value == null) {
-      throw createInvalidIdentifierException();
+      throw createInvalidIdentifierException(env.getVariableNames());
     }
     return value;
   }
@@ -87,14 +87,23 @@
   @Override
   void validate(ValidationEnvironment env) throws EvalException {
     if (!env.hasSymbolInEnvironment(name)) {
-      throw createInvalidIdentifierException();
+      throw createInvalidIdentifierException(env.getAllSymbols());
     }
   }
 
-  private EvalException createInvalidIdentifierException() {
-    return name.equals("$error$")
-        ? new EvalException(getLocation(), "contains syntax error(s)", true)
-        : new EvalException(getLocation(), "name '" + name + "' is not defined");
+  private EvalException createInvalidIdentifierException(Set<String> symbols) {
+    if (name.equals("$error$")) {
+      return new EvalException(getLocation(), "contains syntax error(s)", true);
+    }
+
+    String suggestion = SpellChecker.suggest(name, symbols);
+    if (suggestion == null) {
+      suggestion = "";
+    } else {
+      suggestion = " (did you mean '" + suggestion + "'?)";
+    }
+
+    return new EvalException(getLocation(), "name '" + name + "' is not defined" + suggestion);
   }
 
   @Override
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 6aa5a7d..b0e766d 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
@@ -105,6 +105,16 @@
         || (parent != null && topLevel().variables.contains(varname));
   }
 
+  /** Returns the set of all accessible symbols (both local and global) */
+  public Set<String> getAllSymbols() {
+    Set<String> all = new HashSet<>();
+    all.addAll(variables);
+    if (parent != null) {
+      all.addAll(parent.getAllSymbols());
+    }
+    return all;
+  }
+
   private ValidationEnvironment topLevel() {
     return Preconditions.checkNotNull(parent == null ? this : parent);
   }
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/MethodLibraryTest.java b/src/test/java/com/google/devtools/build/lib/syntax/MethodLibraryTest.java
index cde97c1..ae6b8fb 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/MethodLibraryTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/MethodLibraryTest.java
@@ -518,7 +518,7 @@
 
     new SkylarkTest()
         .testIfErrorContains(
-            "ERROR 1:1: function 'join' does not exist", "join(' ', [ 'a', 'b', 'c' ])");
+            "ERROR 1:1: name 'join' is not defined", "join(' ', [ 'a', 'b', 'c' ])");
 
     new BothModesTest().testStatement("' '.join([ 'a', 'b', 'c' ])", "a b c");
   }
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/ParserTest.java b/src/test/java/com/google/devtools/build/lib/syntax/ParserTest.java
index 8cd35f2..c527fd2 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/ParserTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/ParserTest.java
@@ -1204,7 +1204,7 @@
   public void testLoadNotAtTopLevel() throws Exception {
     setFailFast(false);
     parseFileForSkylark("if 1: load(8)\n");
-    assertContainsError("function 'load' does not exist");
+    assertContainsError("name 'load' is not defined");
   }
 
   @Test
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java
index f6493ba..38874b8 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java
@@ -1044,6 +1044,15 @@
   }
 
   @Test
+  public void testTypo() throws Exception {
+    new SkylarkTest()
+        .testIfErrorContains(
+            "name 'my_variable' is not defined (did you mean 'myVariable'?)",
+            "myVariable = 2",
+            "x = my_variable + 1");
+  }
+
+  @Test
   public void testNoneTrueFalseInSkylark() throws Exception {
     new SkylarkTest().setUp("a = None",
       "b = True",
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/ValidationTest.java b/src/test/java/com/google/devtools/build/lib/syntax/ValidationTest.java
index b420ffd..2e4ae53 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/ValidationTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/ValidationTest.java
@@ -103,7 +103,7 @@
 
   @Test
   public void testFunctionDoesNotExist() {
-    checkError("function 'foo' does not exist", "def bar(): a = foo() + 'a'");
+    checkError("name 'foo' is not defined", "def bar(): a = foo() + 'a'");
   }
 
   @Test