tint: Move "suggest alternatives" logic to utils

This will be used for suggesting alternative diagnostic rule names in
the Resolver.

Bug: tint:1809
Change-Id: Icc9af02937326f6f774fbaf2aeaa9314c88fdea6
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/117565
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
diff --git a/src/tint/reader/wgsl/parser_impl.cc b/src/tint/reader/wgsl/parser_impl.cc
index 37c3ac3..f7aa4b8 100644
--- a/src/tint/reader/wgsl/parser_impl.cc
+++ b/src/tint/reader/wgsl/parser_impl.cc
@@ -1240,40 +1240,15 @@
     }
 
     /// Create a sensible error message
-    std::stringstream err;
+    std::ostringstream err;
     err << "expected " << name;
 
     if (!use.empty()) {
         err << " for " << use;
     }
+    err << "\n";
 
-    // If the string typed was within kSuggestionDistance of one of the possible enum values,
-    // suggest that. Don't bother with suggestions if the string was extremely long.
-    constexpr size_t kSuggestionDistance = 5;
-    constexpr size_t kSuggestionMaxLength = 64;
-    if (auto got = t.to_str(); !got.empty() && got.size() < kSuggestionMaxLength) {
-        size_t candidate_dist = kSuggestionDistance;
-        const char* candidate = nullptr;
-        for (auto* str : strings) {
-            auto dist = utils::Distance(str, got);
-            if (dist < candidate_dist) {
-                candidate = str;
-                candidate_dist = dist;
-            }
-        }
-        if (candidate) {
-            err << ". Did you mean '" << candidate << "'?";
-        }
-    }
-
-    // List all the possible enumerator values
-    err << "\nPossible values: ";
-    for (auto* str : strings) {
-        if (str != strings[0]) {
-            err << ", ";
-        }
-        err << "'" << str << "'";
-    }
+    utils::SuggestAlternatives(t.to_str(), strings, err);
 
     synchronized_ = false;
     return add_error(t.source(), err.str());
diff --git a/src/tint/reader/wgsl/parser_impl_enable_directive_test.cc b/src/tint/reader/wgsl/parser_impl_enable_directive_test.cc
index 23a8cb2..260175d 100644
--- a/src/tint/reader/wgsl/parser_impl_enable_directive_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_enable_directive_test.cc
@@ -74,7 +74,8 @@
     p->enable_directive();
     // Error when unknown extension found
     EXPECT_TRUE(p->has_error());
-    EXPECT_EQ(p->error(), R"(1:8: expected extension. Did you mean 'f16'?
+    EXPECT_EQ(p->error(), R"(1:8: expected extension
+Did you mean 'f16'?
 Possible values: 'chromium_disable_uniformity_analysis', 'chromium_experimental_dp4a', 'chromium_experimental_full_ptr_parameters', 'chromium_experimental_push_constant', 'f16')");
     auto program = p->program();
     auto& ast = program.AST();
diff --git a/src/tint/reader/wgsl/parser_impl_error_msg_test.cc b/src/tint/reader/wgsl/parser_impl_error_msg_test.cc
index f458834..5337c3c 100644
--- a/src/tint/reader/wgsl/parser_impl_error_msg_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_error_msg_test.cc
@@ -1110,7 +1110,8 @@
 
 TEST_F(ParserImplErrorTest, GlobalDeclVarAttrBuiltinInvalidValue) {
     EXPECT("@builtin(frag_d3pth) var i : i32;",
-           R"(test.wgsl:1:10 error: expected builtin. Did you mean 'frag_depth'?
+           R"(test.wgsl:1:10 error: expected builtin
+Did you mean 'frag_depth'?
 Possible values: 'frag_depth', 'front_facing', 'global_invocation_id', 'instance_index', 'local_invocation_id', 'local_invocation_index', 'num_workgroups', 'position', 'sample_index', 'sample_mask', 'vertex_index', 'workgroup_id'
 @builtin(frag_d3pth) var i : i32;
          ^^^^^^^^^^
diff --git a/src/tint/reader/wgsl/parser_impl_texture_sampler_test.cc b/src/tint/reader/wgsl/parser_impl_texture_sampler_test.cc
index 299bd33..7b051d3 100644
--- a/src/tint/reader/wgsl/parser_impl_texture_sampler_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_texture_sampler_test.cc
@@ -230,7 +230,8 @@
     EXPECT_FALSE(t.matched);
     EXPECT_TRUE(t.errored);
     EXPECT_EQ(p->error(),
-              R"(1:20: expected texel format for storage texture type. Did you mean 'rg32float'?
+              R"(1:20: expected texel format for storage texture type
+Did you mean 'rg32float'?
 Possible values: 'bgra8unorm', 'r32float', 'r32sint', 'r32uint', 'rg32float', 'rg32sint', 'rg32uint', 'rgba16float', 'rgba16sint', 'rgba16uint', 'rgba32float', 'rgba32sint', 'rgba32uint', 'rgba8sint', 'rgba8snorm', 'rgba8uint', 'rgba8unorm')");
 }
 
@@ -241,7 +242,8 @@
     EXPECT_FALSE(t.matched);
     EXPECT_TRUE(t.errored);
     EXPECT_EQ(p->error(),
-              R"(1:30: expected access control for storage texture type. Did you mean 'read'?
+              R"(1:30: expected access control for storage texture type
+Did you mean 'read'?
 Possible values: 'read', 'read_write', 'write')");
 }
 
diff --git a/src/tint/reader/wgsl/parser_impl_type_decl_test.cc b/src/tint/reader/wgsl/parser_impl_type_decl_test.cc
index 5e18b00..0a4198e 100644
--- a/src/tint/reader/wgsl/parser_impl_type_decl_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_type_decl_test.cc
@@ -316,7 +316,8 @@
     ASSERT_EQ(t.value, nullptr);
     ASSERT_TRUE(p->has_error());
     ASSERT_EQ(p->error(),
-              R"(1:5: expected address space for ptr declaration. Did you mean 'uniform'?
+              R"(1:5: expected address space for ptr declaration
+Did you mean 'uniform'?
 Possible values: 'function', 'private', 'push_constant', 'storage', 'uniform', 'workgroup')");
 }
 
diff --git a/src/tint/reader/wgsl/parser_impl_type_decl_without_ident_test.cc b/src/tint/reader/wgsl/parser_impl_type_decl_without_ident_test.cc
index ea146c9..409062a 100644
--- a/src/tint/reader/wgsl/parser_impl_type_decl_without_ident_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_type_decl_without_ident_test.cc
@@ -307,7 +307,8 @@
     ASSERT_EQ(t.value, nullptr);
     ASSERT_TRUE(p->has_error());
     ASSERT_EQ(p->error(),
-              R"(1:5: expected address space for ptr declaration. Did you mean 'uniform'?
+              R"(1:5: expected address space for ptr declaration
+Did you mean 'uniform'?
 Possible values: 'function', 'private', 'push_constant', 'storage', 'uniform', 'workgroup')");
 }
 
diff --git a/src/tint/reader/wgsl/parser_impl_variable_attribute_list_test.cc b/src/tint/reader/wgsl/parser_impl_variable_attribute_list_test.cc
index 79574a8..9de2a3d 100644
--- a/src/tint/reader/wgsl/parser_impl_variable_attribute_list_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_variable_attribute_list_test.cc
@@ -69,7 +69,8 @@
     EXPECT_TRUE(attrs.errored);
     EXPECT_FALSE(attrs.matched);
     EXPECT_TRUE(attrs.value.IsEmpty());
-    EXPECT_EQ(p->error(), R"(1:10: expected builtin. Did you mean 'instance_index'?
+    EXPECT_EQ(p->error(), R"(1:10: expected builtin
+Did you mean 'instance_index'?
 Possible values: 'frag_depth', 'front_facing', 'global_invocation_id', 'instance_index', 'local_invocation_id', 'local_invocation_index', 'num_workgroups', 'position', 'sample_index', 'sample_mask', 'vertex_index', 'workgroup_id')");
 }
 }  // namespace
diff --git a/src/tint/reader/wgsl/parser_impl_variable_attribute_test.cc b/src/tint/reader/wgsl/parser_impl_variable_attribute_test.cc
index 1b538a2..602cc20 100644
--- a/src/tint/reader/wgsl/parser_impl_variable_attribute_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_variable_attribute_test.cc
@@ -323,7 +323,8 @@
     EXPECT_TRUE(attr.errored);
     EXPECT_EQ(attr.value, nullptr);
     EXPECT_TRUE(p->has_error());
-    EXPECT_EQ(p->error(), R"(1:9: expected builtin. Did you mean 'front_facing'?
+    EXPECT_EQ(p->error(), R"(1:9: expected builtin
+Did you mean 'front_facing'?
 Possible values: 'frag_depth', 'front_facing', 'global_invocation_id', 'instance_index', 'local_invocation_id', 'local_invocation_index', 'num_workgroups', 'position', 'sample_index', 'sample_mask', 'vertex_index', 'workgroup_id')");
 }
 
@@ -494,7 +495,8 @@
     EXPECT_TRUE(attr.errored);
     EXPECT_EQ(attr.value, nullptr);
     EXPECT_TRUE(p->has_error());
-    EXPECT_EQ(p->error(), R"(1:26: expected interpolation sampling. Did you mean 'sample'?
+    EXPECT_EQ(p->error(), R"(1:26: expected interpolation sampling
+Did you mean 'sample'?
 Possible values: 'center', 'centroid', 'sample')");
 }
 
diff --git a/src/tint/reader/wgsl/parser_impl_variable_decl_test.cc b/src/tint/reader/wgsl/parser_impl_variable_decl_test.cc
index ba4a872..7e29203 100644
--- a/src/tint/reader/wgsl/parser_impl_variable_decl_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_variable_decl_test.cc
@@ -106,7 +106,8 @@
     EXPECT_TRUE(v.errored);
     EXPECT_TRUE(p->has_error());
     EXPECT_EQ(p->error(),
-              R"(1:5: expected address space for variable declaration. Did you mean 'uniform'?
+              R"(1:5: expected address space for variable declaration
+Did you mean 'uniform'?
 Possible values: 'function', 'private', 'push_constant', 'storage', 'uniform', 'workgroup')");
 }
 
diff --git a/src/tint/utils/string.h b/src/tint/utils/string.h
index abfe51a..5c05ebf 100644
--- a/src/tint/utils/string.h
+++ b/src/tint/utils/string.h
@@ -66,6 +66,43 @@
 /// @returns the Levenshtein distance between @p a and @p b
 size_t Distance(std::string_view a, std::string_view b);
 
+/// Suggest alternatives for an unrecognized string from a list of expected values.
+/// @param got the unrecognized string
+/// @param strings the list of expected values
+/// @param ss the stream to write the suggest and list of possible values to
+template <size_t N>
+void SuggestAlternatives(std::string_view got,
+                         const char* const (&strings)[N],
+                         std::ostringstream& ss) {
+    // If the string typed was within kSuggestionDistance of one of the possible enum values,
+    // suggest that. Don't bother with suggestions if the string was extremely long.
+    constexpr size_t kSuggestionDistance = 5;
+    constexpr size_t kSuggestionMaxLength = 64;
+    if (!got.empty() && got.size() < kSuggestionMaxLength) {
+        size_t candidate_dist = kSuggestionDistance;
+        const char* candidate = nullptr;
+        for (auto* str : strings) {
+            auto dist = utils::Distance(str, got);
+            if (dist < candidate_dist) {
+                candidate = str;
+                candidate_dist = dist;
+            }
+        }
+        if (candidate) {
+            ss << "Did you mean '" << candidate << "'?\n";
+        }
+    }
+
+    // List all the possible enumerator values
+    ss << "Possible values: ";
+    for (auto* str : strings) {
+        if (str != strings[0]) {
+            ss << ", ";
+        }
+        ss << "'" << str << "'";
+    }
+}
+
 }  // namespace tint::utils
 
 #endif  // SRC_TINT_UTILS_STRING_H_
diff --git a/src/tint/utils/string_test.cc b/src/tint/utils/string_test.cc
index bbf8e0f..6b17dfb 100644
--- a/src/tint/utils/string_test.cc
+++ b/src/tint/utils/string_test.cc
@@ -58,5 +58,19 @@
     EXPECT_EQ(Distance("", "Hello world"), 11u);
 }
 
+TEST(StringTest, SuggestAlternatives) {
+    {
+        std::ostringstream ss;
+        SuggestAlternatives("hello wordl", {"hello world", "Hello World"}, ss);
+        EXPECT_EQ(ss.str(), R"(Did you mean 'hello world'?
+Possible values: 'hello world', 'Hello World')");
+    }
+    {
+        std::ostringstream ss;
+        SuggestAlternatives("hello world", {"foobar", "something else"}, ss);
+        EXPECT_EQ(ss.str(), R"(Possible values: 'foobar', 'something else')");
+    }
+}
+
 }  // namespace
 }  // namespace tint::utils