diff --git a/src/main/java/com/google/devtools/build/lib/cmdline/BUILD b/src/main/java/com/google/devtools/build/lib/cmdline/BUILD index c7da581a407f02..8f36f454536405 100644 --- a/src/main/java/com/google/devtools/build/lib/cmdline/BUILD +++ b/src/main/java/com/google/devtools/build/lib/cmdline/BUILD @@ -57,6 +57,7 @@ java_library( "//src/main/java/com/google/devtools/build/skyframe:skyframe-objects", "//src/main/java/net/starlark/java/annot", "//src/main/java/net/starlark/java/eval", + "//src/main/java/net/starlark/java/spelling", "//src/main/protobuf:failure_details_java_proto", "//third_party:auto_value", "//third_party:caffeine", diff --git a/src/main/java/com/google/devtools/build/lib/cmdline/RepositoryMapping.java b/src/main/java/com/google/devtools/build/lib/cmdline/RepositoryMapping.java index 478f0d24f4c8a9..e4339a6eac624e 100644 --- a/src/main/java/com/google/devtools/build/lib/cmdline/RepositoryMapping.java +++ b/src/main/java/com/google/devtools/build/lib/cmdline/RepositoryMapping.java @@ -23,6 +23,7 @@ import java.util.Map.Entry; import java.util.Optional; import javax.annotation.Nullable; +import net.starlark.java.spelling.SpellChecker; /** * Stores the mapping from apparent repo name to canonical repo name, from the viewpoint of an @@ -128,7 +129,8 @@ public RepositoryName get(String preMappingName) { if (ownerRepo() == null) { return RepositoryName.createUnvalidated(preMappingName); } else { - return RepositoryName.createUnvalidated(preMappingName).toNonVisible(ownerRepo()); + return RepositoryName.createUnvalidated(preMappingName) + .toNonVisible(ownerRepo(), SpellChecker.didYouMean(preMappingName, entries().keySet())); } } diff --git a/src/main/java/com/google/devtools/build/lib/cmdline/RepositoryName.java b/src/main/java/com/google/devtools/build/lib/cmdline/RepositoryName.java index ab1bf0dc46f2a1..e70065bced006e 100644 --- a/src/main/java/com/google/devtools/build/lib/cmdline/RepositoryName.java +++ b/src/main/java/com/google/devtools/build/lib/cmdline/RepositoryName.java @@ -16,6 +16,7 @@ import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.LoadingCache; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Throwables; import com.google.devtools.build.lib.skyframe.serialization.autocodec.SerializationConstant; @@ -132,13 +133,22 @@ public static Pair fromPathFragment( */ private final RepositoryName ownerRepoIfNotVisible; - private RepositoryName(String name, RepositoryName ownerRepoIfNotVisible) { + /** + * If ownerRepoIfNotVisible is not null, this field stores the suffix to be appended to the error + */ + @Nullable private final String didYouMeanSuffix; + + private RepositoryName( + String name, + @Nullable RepositoryName ownerRepoIfNotVisible, + @Nullable String didYouMeanSuffix) { this.name = name; this.ownerRepoIfNotVisible = ownerRepoIfNotVisible; + this.didYouMeanSuffix = didYouMeanSuffix; } private RepositoryName(String name) { - this(name, null); + this(name, /* ownerRepoIfNotVisible= */ null, /* didYouMeanSuffix= */ null); } /** @@ -192,10 +202,16 @@ public String getMarkerFileName() { * actually not visible from the owner repository and should fail in {@code * RepositoryDelegatorFunction} when fetching with this {@link RepositoryName} instance. */ - public RepositoryName toNonVisible(RepositoryName ownerRepo) { + public RepositoryName toNonVisible(RepositoryName ownerRepo, String didYouMeanSuffix) { Preconditions.checkNotNull(ownerRepo); Preconditions.checkArgument(ownerRepo.isVisible()); - return new RepositoryName(name, ownerRepo); + Preconditions.checkNotNull(didYouMeanSuffix); + return new RepositoryName(name, ownerRepo, didYouMeanSuffix); + } + + @VisibleForTesting + public RepositoryName toNonVisible(RepositoryName ownerRepo) { + return toNonVisible(ownerRepo, ""); } public boolean isVisible() { @@ -239,7 +255,9 @@ public boolean isMain() { // TODO(bazel-team): Rename to "getCanonicalForm". public String getNameWithAt() { if (!isVisible()) { - return String.format("@@[unknown repo '%s' requested from %s]", name, ownerRepoIfNotVisible); + return String.format( + "@@[unknown repo '%s' requested from %s%s]", + name, ownerRepoIfNotVisible, didYouMeanSuffix); } return "@@" + name; } @@ -339,16 +357,17 @@ public boolean equals(Object object) { if (this == object) { return true; } - if (!(object instanceof RepositoryName)) { + if (!(object instanceof RepositoryName other)) { return false; } - RepositoryName other = (RepositoryName) object; return OsPathPolicy.getFilePathOs().equals(name, other.name) - && Objects.equals(ownerRepoIfNotVisible, other.ownerRepoIfNotVisible); + && Objects.equals(ownerRepoIfNotVisible, other.ownerRepoIfNotVisible) + && Objects.equals(didYouMeanSuffix, other.didYouMeanSuffix); } @Override public int hashCode() { - return Objects.hash(OsPathPolicy.getFilePathOs().hash(name), ownerRepoIfNotVisible); + return Objects.hash( + OsPathPolicy.getFilePathOs().hash(name), ownerRepoIfNotVisible, didYouMeanSuffix); } } diff --git a/src/test/java/com/google/devtools/build/lib/cmdline/RepositoryMappingTest.java b/src/test/java/com/google/devtools/build/lib/cmdline/RepositoryMappingTest.java index 7556c3703e6e09..86017c3058220a 100644 --- a/src/test/java/com/google/devtools/build/lib/cmdline/RepositoryMappingTest.java +++ b/src/test/java/com/google/devtools/build/lib/cmdline/RepositoryMappingTest.java @@ -89,4 +89,13 @@ public void composeWith() throws Exception { assertThat(mapping.get("C")).isEqualTo(RepositoryName.create("C_mapped")); assertThat(mapping.get("D")).isEqualTo(RepositoryName.create("D")); } + + @Test + public void unknownRepoDidYouMean() throws LabelSyntaxException { + RepositoryMapping mapping = + RepositoryMapping.create( + ImmutableMap.of("foo", RepositoryName.create("foo_internal")), RepositoryName.MAIN); + assertThat(mapping.get("boo").getNameWithAt()) + .isEqualTo("@@[unknown repo 'boo' requested from @@ (did you mean 'foo'?)]"); + } }