Make a 'did you mean' suggestion when referencing a non-existent label.

It works for both labels on the command-line and labels in BUILD files.

--
MOS_MIGRATED_REVID=123967347
diff --git a/src/main/java/com/google/devtools/build/lib/packages/Package.java b/src/main/java/com/google/devtools/build/lib/packages/Package.java
index 8a666b0..cbfeb5b 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/Package.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/Package.java
@@ -34,6 +34,7 @@
 import com.google.devtools.build.lib.packages.AttributeMap.AcceptsLabelAttribute;
 import com.google.devtools.build.lib.packages.License.DistributionType;
 import com.google.devtools.build.lib.util.Preconditions;
+import com.google.devtools.build.lib.util.SpellChecker;
 import com.google.devtools.build.lib.vfs.Canonicalizer;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
@@ -528,7 +529,12 @@
       suffix = "; however, a source file of this name exists.  (Perhaps add "
           + "'exports_files([\"" + targetName + "\"])' to " + name + "/BUILD?)";
     } else {
-      suffix = "";
+      String suggestion = SpellChecker.suggest(targetName, targets.keySet());
+      if (suggestion != null) {
+        suffix = " (did you mean '" + suggestion + "'?)";
+      } else {
+        suffix = "";
+      }
     }
 
     throw makeNoSuchTargetException(targetName, suffix);
diff --git a/src/main/java/com/google/devtools/build/lib/util/SpellChecker.java b/src/main/java/com/google/devtools/build/lib/util/SpellChecker.java
index e453ebc..b8fda8f 100644
--- a/src/main/java/com/google/devtools/build/lib/util/SpellChecker.java
+++ b/src/main/java/com/google/devtools/build/lib/util/SpellChecker.java
@@ -14,6 +14,8 @@
 
 package com.google.devtools.build.lib.util;
 
+import javax.annotation.Nullable;
+
 /**
  * Class that provides functions to do spell checking, i.e. detect typos
  * and make suggestions.
@@ -71,4 +73,25 @@
     int result = row[s2.length()];
     return result <= maxEditDistance ? result : -1;
   }
+
+  /**
+   * Find in words which string is the most similar to input (according to
+   * the edit distance, ignoring case) - or null if no string is similar
+   * enough. In case of equality, the first one in words wins.
+   */
+  @Nullable
+  public static String suggest(String input, Iterable<String> words) {
+    String best = null;
+    // Heuristic: the expected number of typos depends on the length of the word.
+    int bestDistance = Math.min(5, input.length() / 2 + 1);
+    input = input.toLowerCase();
+    for (String candidate : words) {
+      int d = editDistance(input, candidate.toLowerCase(), bestDistance);
+      if (d >= 0 && d < bestDistance) {
+        bestDistance = d;
+        best = candidate;
+      }
+    }
+    return best;
+  }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/packages/ExportsFilesTest.java b/src/test/java/com/google/devtools/build/lib/packages/ExportsFilesTest.java
index c8afeea..31ca540 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/ExportsFilesTest.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/ExportsFilesTest.java
@@ -75,7 +75,8 @@
       fail();
     } catch (NoSuchTargetException e) {
       assertThat(e).hasMessage("no such target '//pkg:baz.txt':"
-          + " target 'baz.txt' not declared in package 'pkg' defined by /workspace/pkg/BUILD");
+          + " target 'baz.txt' not declared in package 'pkg' (did you mean 'bar.txt'?)"
+          + " defined by /workspace/pkg/BUILD");
     }
   }
 
diff --git a/src/test/java/com/google/devtools/build/lib/packages/PackageFactoryTest.java b/src/test/java/com/google/devtools/build/lib/packages/PackageFactoryTest.java
index cefcf15..943c21f 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/PackageFactoryTest.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/PackageFactoryTest.java
@@ -500,7 +500,8 @@
       assertThat(e)
           .hasMessage(
               "no such target '//x:z.cc': "
-                  + "target 'z.cc' not declared in package 'x' defined by /x/BUILD");
+                  + "target 'z.cc' not declared in package 'x' (did you mean 'x.cc'?) "
+                  + "defined by /x/BUILD");
     }
 
     try {
diff --git a/src/test/java/com/google/devtools/build/lib/pkgcache/TargetPatternEvaluatorTest.java b/src/test/java/com/google/devtools/build/lib/pkgcache/TargetPatternEvaluatorTest.java
index c99a582..aeb297e 100644
--- a/src/test/java/com/google/devtools/build/lib/pkgcache/TargetPatternEvaluatorTest.java
+++ b/src/test/java/com/google/devtools/build/lib/pkgcache/TargetPatternEvaluatorTest.java
@@ -316,7 +316,7 @@
   @Test
   public void testUnsupportedTargets() throws Exception {
     String expectedError = "no such target '//foo:foo': target 'foo' not declared in package 'foo'"
-        + " defined by /workspace/foo/BUILD";
+        + " (did you mean 'foo1'?) defined by /workspace/foo/BUILD";
     expectError(expectedError, "foo");
     expectError("The package part of 'foo/' should not end in a slash", "foo/");
   }
@@ -1155,4 +1155,3 @@
     expectError("no such target '//:nope.cc'", "//:nope.cc");
   }
 }
-
diff --git a/src/test/java/com/google/devtools/build/lib/util/SpellCheckerTest.java b/src/test/java/com/google/devtools/build/lib/util/SpellCheckerTest.java
index d0d84e3..5b4e55d 100644
--- a/src/test/java/com/google/devtools/build/lib/util/SpellCheckerTest.java
+++ b/src/test/java/com/google/devtools/build/lib/util/SpellCheckerTest.java
@@ -15,10 +15,14 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.collect.Lists;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
+import java.util.List;
+
 /**
  * Tests for {@link SpellChecker}.
  */
@@ -76,4 +80,27 @@
 
     assertThat(SpellChecker.editDistance("abcdefg", "s", 2)).isEqualTo(-1);
   }
+
+  @Test
+  public void suggest() throws Exception {
+    List<String> dict = Lists.newArrayList(
+        "isalnum", "isalpha", "isdigit", "islower", "isupper", "find", "join",
+        "rsplit", "rstrip", "split", "splitlines", "startswith", "strip", "title", "upper");
+
+    assertThat(SpellChecker.suggest("isdfit", dict)).isEqualTo("isdigit");
+    assertThat(SpellChecker.suggest("rspit", dict)).isEqualTo("rsplit");
+    assertThat(SpellChecker.suggest("IS_LOWER", dict)).isEqualTo("islower");
+    assertThat(SpellChecker.suggest("sartwigh", dict)).isEqualTo("startswith");
+    assertThat(SpellChecker.suggest("SplitAllLines", dict)).isEqualTo("splitlines");
+    assertThat(SpellChecker.suggest("fird", dict)).isEqualTo("find");
+    assertThat(SpellChecker.suggest("stip", dict)).isEqualTo("strip");
+    assertThat(SpellChecker.suggest("isAln", dict)).isEqualTo("isalnum");
+
+    assertThat(SpellChecker.suggest("isAl", dict)).isEqualTo(null);
+    assertThat(SpellChecker.suggest("", dict)).isEqualTo(null);
+    assertThat(SpellChecker.suggest("f", dict)).isEqualTo(null);
+    assertThat(SpellChecker.suggest("fir", dict)).isEqualTo(null);
+    assertThat(SpellChecker.suggest("wqevxc", dict)).isEqualTo(null);
+    assertThat(SpellChecker.suggest("ialsnuaip", dict)).isEqualTo(null);
+  }
 }