Add reader::spv::Namer::Sanitize

Bug: tint:3
Change-Id: I4d554755dacecac0f2dacf191d85f8e339e87923
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/17420
Reviewed-by: dan sinclair <dsinclair@google.com>
diff --git a/src/reader/spirv/namer.cc b/src/reader/spirv/namer.cc
index 8f316c9..e56f395 100644
--- a/src/reader/spirv/namer.cc
+++ b/src/reader/spirv/namer.cc
@@ -14,6 +14,8 @@
 
 #include "src/reader/spirv/namer.h"
 
+#include <algorithm>
+
 namespace tint {
 namespace reader {
 namespace spirv {
@@ -22,6 +24,30 @@
 
 Namer::~Namer() = default;
 
+std::string Namer::Sanitize(const std::string& suggested_name) {
+  if (suggested_name.empty()) {
+    return "empty";
+  }
+  // Otherwise, replace invalid characters by '_'.
+  std::string result;
+  std::string invalid_as_first_char = "_0123456789";
+  std::string valid =
+      "abcdefghijklmnopqrstuvwxyz"
+      "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+      "_0123456789";
+  // If the first character is invalid for starting a WGSL identifier, then
+  // prefix the result with "x".
+  if ((std::string::npos != invalid_as_first_char.find(suggested_name[0])) ||
+      (std::string::npos == valid.find(suggested_name[0]))) {
+    result = "x";
+  }
+  std::transform(suggested_name.begin(), suggested_name.end(),
+                 std::back_inserter(result), [&valid](const char c) {
+                   return (std::string::npos == valid.find(c)) ? '_' : c;
+                 });
+  return result;
+}
+
 bool Namer::SaveName(uint32_t id, const std::string& name) {
   if (HasName(id)) {
     return Fail() << "internal error: ID " << id
diff --git a/src/reader/spirv/namer.h b/src/reader/spirv/namer.h
index 03fc531..470228e 100644
--- a/src/reader/spirv/namer.h
+++ b/src/reader/spirv/namer.h
@@ -26,6 +26,12 @@
 namespace spirv {
 
 /// A Namer maps SPIR-V IDs to strings.
+///
+/// Sanitization:
+/// Some names are user-suggested, but "sanitized" in the sense that an
+/// unusual character (e.g. invalid for use in WGSL identifiers) is remapped
+/// to a safer character such as an underscore.  Also, sanitized names
+/// never start with an underscorre.
 class Namer {
  public:
   /// Creates a new namer
@@ -34,6 +40,13 @@
   /// Destructor
   ~Namer();
 
+  /// Sanitizes the given string, to replace unusual characters with
+  /// obviously-valid idenfier characters. An empy string yields "empty".
+  /// A sanitized name never starts with an underscore.
+  /// @param suggested_name input string
+  /// @returns sanitized name, suitable for use as an identifier
+  static std::string Sanitize(const std::string& suggested_name);
+
   /// Registers a failure.
   /// @returns a fail stream to accumulate diagnostics.
   FailStream& Fail() { return fail_stream_.Fail(); }
diff --git a/src/reader/spirv/namer_test.cc b/src/reader/spirv/namer_test.cc
index aa6eeee..647706e 100644
--- a/src/reader/spirv/namer_test.cc
+++ b/src/reader/spirv/namer_test.cc
@@ -18,7 +18,7 @@
 #include <sstream>
 #include <string>
 
-#include "gtest/gtest.h"
+#include "gmock/gmock.h"
 #include "src/reader/spirv/fail_stream.h"
 
 namespace tint {
@@ -26,6 +26,8 @@
 namespace spirv {
 namespace {
 
+using ::testing::Eq;
+
 class SpvNamerTest : public testing::Test {
  public:
   SpvNamerTest() : fail_stream_(&success_, &errors_) {}
@@ -39,6 +41,26 @@
   FailStream fail_stream_;
 };
 
+TEST_F(SpvNamerTest, SanitizeEmpty) {
+  EXPECT_THAT(Namer::Sanitize(""), Eq("empty"));
+}
+
+TEST_F(SpvNamerTest, SanitizeLeadingUnderscore) {
+  EXPECT_THAT(Namer::Sanitize("_"), Eq("x_"));
+}
+
+TEST_F(SpvNamerTest, SanitizeLeadingDigit) {
+  EXPECT_THAT(Namer::Sanitize("7zip"), Eq("x7zip"));
+}
+
+TEST_F(SpvNamerTest, SanitizeOkChars) {
+  EXPECT_THAT(Namer::Sanitize("_abcdef12345"), Eq("x_abcdef12345"));
+}
+
+TEST_F(SpvNamerTest, SanitizeNonIdentifierChars) {
+  EXPECT_THAT(Namer::Sanitize("a:1.2'f\n"), "a_1_2_f_");
+}
+
 TEST_F(SpvNamerTest, NoFailureToStart) {
   Namer namer(fail_stream_);
   EXPECT_TRUE(success_);