tint: Add two-token form for diagnostics rule names.

Fixed: tint:1891
Change-Id: Ia3737c29b111d7b6e6b00fbd68da7f85a5a49bca
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/128301
Commit-Queue: Ben Clayton <bclayton@google.com>
Reviewed-by: Dan Sinclair <dsinclair@chromium.org>
Reviewed-by: James Price <jrprice@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index 663ba05..f4526ff 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -472,6 +472,7 @@
     "ast/diagnostic_attribute.h",
     "ast/diagnostic_control.h",
     "ast/diagnostic_directive.h",
+    "ast/diagnostic_rule_name.h",
     "ast/disable_validation_attribute.h",
     "ast/discard_statement.h",
     "ast/enable.h",
@@ -559,6 +560,7 @@
     "ast/diagnostic_attribute.cc",
     "ast/diagnostic_control.cc",
     "ast/diagnostic_directive.cc",
+    "ast/diagnostic_rule_name.cc",
     "ast/disable_validation_attribute.cc",
     "ast/discard_statement.cc",
     "ast/enable.cc",
@@ -1291,6 +1293,7 @@
       "ast/diagnostic_attribute_test.cc",
       "ast/diagnostic_control_test.cc",
       "ast/diagnostic_directive_test.cc",
+      "ast/diagnostic_rule_name_test.cc",
       "ast/discard_statement_test.cc",
       "ast/enable_test.cc",
       "ast/float_literal_expression_test.cc",
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index 323ed79..7bac6ed 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -126,6 +126,8 @@
   ast/diagnostic_control.h
   ast/diagnostic_directive.cc
   ast/diagnostic_directive.h
+  ast/diagnostic_rule_name.cc
+  ast/diagnostic_rule_name.h
   ast/disable_validation_attribute.cc
   ast/disable_validation_attribute.h
   ast/discard_statement.cc
@@ -845,6 +847,7 @@
     ast/diagnostic_attribute_test.cc
     ast/diagnostic_control_test.cc
     ast/diagnostic_directive_test.cc
+    ast/diagnostic_rule_name_test.cc
     ast/discard_statement_test.cc
     ast/enable_test.cc
     ast/float_literal_expression_test.cc
diff --git a/src/tint/ast/diagnostic_attribute_test.cc b/src/tint/ast/diagnostic_attribute_test.cc
index ec6ec26..a77c025 100644
--- a/src/tint/ast/diagnostic_attribute_test.cc
+++ b/src/tint/ast/diagnostic_attribute_test.cc
@@ -20,12 +20,20 @@
 using namespace tint::number_suffixes;  // NOLINT
 using DiagnosticAttributeTest = TestHelper;
 
-TEST_F(DiagnosticAttributeTest, Creation) {
-    auto* name = Ident("foo");
-    auto* d = DiagnosticAttribute(builtin::DiagnosticSeverity::kWarning, name);
+TEST_F(DiagnosticAttributeTest, Name) {
+    auto* d = DiagnosticAttribute(builtin::DiagnosticSeverity::kWarning, "foo");
     EXPECT_EQ(d->Name(), "diagnostic");
     EXPECT_EQ(d->control.severity, builtin::DiagnosticSeverity::kWarning);
-    EXPECT_EQ(d->control.rule_name, name);
+    EXPECT_EQ(d->control.rule_name->category, nullptr);
+    CheckIdentifier(d->control.rule_name->name, "foo");
+}
+
+TEST_F(DiagnosticAttributeTest, CategoryAndName) {
+    auto* d = DiagnosticAttribute(builtin::DiagnosticSeverity::kWarning, "foo", "bar");
+    EXPECT_EQ(d->Name(), "diagnostic");
+    EXPECT_EQ(d->control.severity, builtin::DiagnosticSeverity::kWarning);
+    CheckIdentifier(d->control.rule_name->category, "foo");
+    CheckIdentifier(d->control.rule_name->name, "bar");
 }
 
 }  // namespace
diff --git a/src/tint/ast/diagnostic_control.cc b/src/tint/ast/diagnostic_control.cc
index a2bdd46..27ec3c1 100644
--- a/src/tint/ast/diagnostic_control.cc
+++ b/src/tint/ast/diagnostic_control.cc
@@ -24,13 +24,10 @@
 
 DiagnosticControl::DiagnosticControl() = default;
 
-DiagnosticControl::DiagnosticControl(builtin::DiagnosticSeverity sev, const Identifier* rule)
+DiagnosticControl::DiagnosticControl(builtin::DiagnosticSeverity sev,
+                                     const DiagnosticRuleName* rule)
     : severity(sev), rule_name(rule) {
     TINT_ASSERT(AST, rule != nullptr);
-    if (rule) {
-        // It is invalid for a diagnostic rule name to be templated
-        TINT_ASSERT(AST, !rule->Is<TemplatedIdentifier>());
-    }
 }
 
 DiagnosticControl::DiagnosticControl(DiagnosticControl&&) = default;
diff --git a/src/tint/ast/diagnostic_control.h b/src/tint/ast/diagnostic_control.h
index f99d002..2b504e4 100644
--- a/src/tint/ast/diagnostic_control.h
+++ b/src/tint/ast/diagnostic_control.h
@@ -23,7 +23,7 @@
 
 // Forward declarations
 namespace tint::ast {
-class Identifier;
+class DiagnosticRuleName;
 }  // namespace tint::ast
 
 namespace tint::ast {
@@ -37,16 +37,16 @@
     /// Constructor
     /// @param sev the diagnostic severity
     /// @param rule the diagnostic rule name
-    DiagnosticControl(builtin::DiagnosticSeverity sev, const Identifier* rule);
+    DiagnosticControl(builtin::DiagnosticSeverity sev, const DiagnosticRuleName* rule);
 
     /// Move constructor
     DiagnosticControl(DiagnosticControl&&);
 
     /// The diagnostic severity control.
-    builtin::DiagnosticSeverity severity;
+    builtin::DiagnosticSeverity severity = builtin::DiagnosticSeverity::kUndefined;
 
     /// The diagnostic rule name.
-    const Identifier* rule_name;
+    const DiagnosticRuleName* rule_name = nullptr;
 };
 
 }  // namespace tint::ast
diff --git a/src/tint/ast/diagnostic_control_test.cc b/src/tint/ast/diagnostic_control_test.cc
index 53afe53..d118677 100644
--- a/src/tint/ast/diagnostic_control_test.cc
+++ b/src/tint/ast/diagnostic_control_test.cc
@@ -24,12 +24,11 @@
 
 using DiagnosticControlTest = TestHelper;
 
-TEST_F(DiagnosticControlTest, Assert_RuleNotTemplated) {
+TEST_F(DiagnosticControlTest, Assert_RuleNotNull) {
     EXPECT_FATAL_FAILURE(
         {
             ProgramBuilder b;
-            DiagnosticControl control(builtin::DiagnosticSeverity::kWarning,
-                                      b.Ident("name", "a", "b", "c"));
+            DiagnosticControl control(builtin::DiagnosticSeverity::kWarning, nullptr);
         },
         "internal compiler error");
 }
diff --git a/src/tint/ast/diagnostic_directive.cc b/src/tint/ast/diagnostic_directive.cc
index f0ef041..8783901 100644
--- a/src/tint/ast/diagnostic_directive.cc
+++ b/src/tint/ast/diagnostic_directive.cc
@@ -34,4 +34,5 @@
     DiagnosticControl dc(control.severity, rule);
     return ctx->dst->create<DiagnosticDirective>(src, std::move(dc));
 }
+
 }  // namespace tint::ast
diff --git a/src/tint/ast/diagnostic_directive_test.cc b/src/tint/ast/diagnostic_directive_test.cc
index 721a545..c423ae9 100644
--- a/src/tint/ast/diagnostic_directive_test.cc
+++ b/src/tint/ast/diagnostic_directive_test.cc
@@ -21,15 +21,28 @@
 
 using DiagnosticDirectiveTest = TestHelper;
 
-TEST_F(DiagnosticDirectiveTest, Creation) {
-    auto* diag = DiagnosticDirective(Source{{{10, 5}, {10, 15}}},
-                                     builtin::DiagnosticSeverity::kWarning, "foo");
-    EXPECT_EQ(diag->source.range.begin.line, 10u);
-    EXPECT_EQ(diag->source.range.begin.column, 5u);
-    EXPECT_EQ(diag->source.range.end.line, 10u);
-    EXPECT_EQ(diag->source.range.end.column, 15u);
-    EXPECT_EQ(diag->control.severity, builtin::DiagnosticSeverity::kWarning);
-    CheckIdentifier(diag->control.rule_name, "foo");
+TEST_F(DiagnosticDirectiveTest, Name) {
+    auto* d = DiagnosticDirective(Source{{{10, 5}, {10, 15}}},
+                                  builtin::DiagnosticSeverity::kWarning, "foo");
+    EXPECT_EQ(d->source.range.begin.line, 10u);
+    EXPECT_EQ(d->source.range.begin.column, 5u);
+    EXPECT_EQ(d->source.range.end.line, 10u);
+    EXPECT_EQ(d->source.range.end.column, 15u);
+    EXPECT_EQ(d->control.severity, builtin::DiagnosticSeverity::kWarning);
+    EXPECT_EQ(d->control.rule_name->category, nullptr);
+    CheckIdentifier(d->control.rule_name->name, "foo");
+}
+
+TEST_F(DiagnosticDirectiveTest, CategoryAndName) {
+    auto* d = DiagnosticDirective(Source{{{10, 5}, {10, 15}}},
+                                  builtin::DiagnosticSeverity::kWarning, "foo", "bar");
+    EXPECT_EQ(d->source.range.begin.line, 10u);
+    EXPECT_EQ(d->source.range.begin.column, 5u);
+    EXPECT_EQ(d->source.range.end.line, 10u);
+    EXPECT_EQ(d->source.range.end.column, 15u);
+    EXPECT_EQ(d->control.severity, builtin::DiagnosticSeverity::kWarning);
+    CheckIdentifier(d->control.rule_name->category, "foo");
+    CheckIdentifier(d->control.rule_name->name, "bar");
 }
 
 }  // namespace
diff --git a/src/tint/ast/diagnostic_rule_name.cc b/src/tint/ast/diagnostic_rule_name.cc
new file mode 100644
index 0000000..856e054
--- /dev/null
+++ b/src/tint/ast/diagnostic_rule_name.cc
@@ -0,0 +1,74 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/ast/diagnostic_rule_name.h"
+
+#include <string>
+
+#include "src/tint/program_builder.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::DiagnosticRuleName);
+
+namespace tint::ast {
+
+DiagnosticRuleName::DiagnosticRuleName(ProgramID pid,
+                                       NodeID nid,
+                                       const Source& src,
+                                       const Identifier* n)
+    : Base(pid, nid, src), name(n) {
+    TINT_ASSERT(AST, name != nullptr);
+    TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, name, program_id);
+    if (name) {
+        // It is invalid for a diagnostic rule name to be templated
+        TINT_ASSERT(AST, !name->Is<TemplatedIdentifier>());
+    }
+}
+
+DiagnosticRuleName::DiagnosticRuleName(ProgramID pid,
+                                       NodeID nid,
+                                       const Source& src,
+                                       const Identifier* c,
+                                       const Identifier* n)
+    : Base(pid, nid, src), category(c), name(n) {
+    TINT_ASSERT(AST, name != nullptr);
+    TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, name, program_id);
+    if (name) {
+        // It is invalid for a diagnostic rule name to be templated
+        TINT_ASSERT(AST, !name->Is<TemplatedIdentifier>());
+    }
+    if (category) {
+        TINT_ASSERT_PROGRAM_IDS_EQUAL_IF_VALID(AST, category, program_id);
+        // It is invalid for a diagnostic rule category to be templated
+        TINT_ASSERT(AST, !category->Is<TemplatedIdentifier>());
+    }
+}
+
+const DiagnosticRuleName* DiagnosticRuleName::Clone(CloneContext* ctx) const {
+    auto src = ctx->Clone(source);
+    auto n = ctx->Clone(name);
+    if (auto c = ctx->Clone(category)) {
+        return ctx->dst->create<DiagnosticRuleName>(src, c, n);
+    }
+    return ctx->dst->create<DiagnosticRuleName>(src, n);
+}
+
+std::string DiagnosticRuleName::String() const {
+    if (category) {
+        return category->symbol.Name() + "." + name->symbol.Name();
+    } else {
+        return name->symbol.Name();
+    }
+}
+
+}  // namespace tint::ast
diff --git a/src/tint/ast/diagnostic_rule_name.h b/src/tint/ast/diagnostic_rule_name.h
new file mode 100644
index 0000000..a3d1189
--- /dev/null
+++ b/src/tint/ast/diagnostic_rule_name.h
@@ -0,0 +1,68 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_AST_DIAGNOSTIC_RULE_NAME_H_
+#define SRC_TINT_AST_DIAGNOSTIC_RULE_NAME_H_
+
+#include <string>
+
+#include "src/tint/ast/node.h"
+
+// Forward declarations
+namespace tint::ast {
+class Identifier;
+}  // namespace tint::ast
+
+namespace tint::ast {
+
+/// A diagnostic rule name used for diagnostic directives and attributes.
+class DiagnosticRuleName final : public utils::Castable<DiagnosticRuleName, Node> {
+  public:
+    /// Constructor
+    /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
+    /// @param src the source of this node
+    /// @param name the rule name
+    DiagnosticRuleName(ProgramID pid, NodeID nid, const Source& src, const Identifier* name);
+
+    /// Constructor
+    /// @param pid the identifier of the program that owns this node
+    /// @param nid the unique node identifier
+    /// @param src the source of this node
+    /// @param category the rule category.
+    /// @param name the rule name
+    DiagnosticRuleName(ProgramID pid,
+                       NodeID nid,
+                       const Source& src,
+                       const Identifier* category,
+                       const Identifier* name);
+
+    /// Clones this node and all transitive child nodes using the `CloneContext` `ctx`.
+    /// @param ctx the clone context
+    /// @return the newly cloned node
+    const DiagnosticRuleName* Clone(CloneContext* ctx) const override;
+
+    /// @return the full name of this diagnostic rule, either as `name` or `category.name`.
+    std::string String() const;
+
+    /// The diagnostic rule category (category.name)
+    Identifier const* const category = nullptr;
+
+    /// The diagnostic rule name.
+    Identifier const* const name;
+};
+
+}  // namespace tint::ast
+
+#endif  // SRC_TINT_AST_DIAGNOSTIC_RULE_NAME_H_
diff --git a/src/tint/ast/diagnostic_rule_name_test.cc b/src/tint/ast/diagnostic_rule_name_test.cc
new file mode 100644
index 0000000..40caac7
--- /dev/null
+++ b/src/tint/ast/diagnostic_rule_name_test.cc
@@ -0,0 +1,50 @@
+// Copyright 2023 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <string>
+
+#include "gtest/gtest-spi.h"
+#include "src/tint/ast/diagnostic_rule_name.h"
+#include "src/tint/ast/test_helper.h"
+
+namespace tint::ast {
+namespace {
+
+using DiagnosticRuleNameTest = TestHelper;
+
+TEST_F(DiagnosticRuleNameTest, String) {
+    EXPECT_EQ(DiagnosticRuleName("name")->String(), "name");
+    EXPECT_EQ(DiagnosticRuleName("category", "name")->String(), "category.name");
+}
+
+TEST_F(DiagnosticRuleNameTest, Assert_NameNotTemplated) {
+    EXPECT_FATAL_FAILURE(
+        {
+            ProgramBuilder b;
+            b.create<ast::DiagnosticRuleName>(b.Ident("name", "a", "b", "c"));
+        },
+        "internal compiler error");
+}
+
+TEST_F(DiagnosticRuleNameTest, Assert_CategoryNotTemplated) {
+    EXPECT_FATAL_FAILURE(
+        {
+            ProgramBuilder b;
+            b.create<ast::DiagnosticRuleName>(b.Ident("name"), b.Ident("category", "a", "b", "c"));
+        },
+        "internal compiler error");
+}
+
+}  // namespace
+}  // namespace tint::ast
diff --git a/src/tint/ast/module_clone_test.cc b/src/tint/ast/module_clone_test.cc
index eb7f775..b2407bc 100644
--- a/src/tint/ast/module_clone_test.cc
+++ b/src/tint/ast/module_clone_test.cc
@@ -25,7 +25,7 @@
     // Shader that exercises the bulk of the AST nodes and types.
     // See also fuzzers/tint_ast_clone_fuzzer.cc for further coverage of cloning.
     Source::File file("test.wgsl", R"(enable f16;
-diagnostic(off, chromium_unreachable_code);
+diagnostic(off, chromium.unreachable_code);
 
 struct S0 {
   @size(4)
@@ -65,7 +65,7 @@
   return 0.0;
 }
 
-@diagnostic(warning, chromium_unreachable_code)
+@diagnostic(warning, chromium.unreachable_code)
 fn f1(p0 : f32, p1 : i32) -> f32 {
   var l0 : i32 = 3;
   var l1 : f32 = 8.0;
diff --git a/src/tint/builtin/diagnostic_rule.cc b/src/tint/builtin/diagnostic_rule.cc
index 69650cb..334a88a 100644
--- a/src/tint/builtin/diagnostic_rule.cc
+++ b/src/tint/builtin/diagnostic_rule.cc
@@ -28,29 +28,45 @@
 
 namespace tint::builtin {
 
-/// ParseDiagnosticRule parses a DiagnosticRule from a string.
+/// ParseCoreDiagnosticRule parses a CoreDiagnosticRule from a string.
 /// @param str the string to parse
-/// @returns the parsed enum, or DiagnosticRule::kUndefined if the string could not be parsed.
-DiagnosticRule ParseDiagnosticRule(std::string_view str) {
-    if (str == "chromium_unreachable_code") {
-        return DiagnosticRule::kChromiumUnreachableCode;
-    }
+/// @returns the parsed enum, or CoreDiagnosticRule::kUndefined if the string could not be parsed.
+CoreDiagnosticRule ParseCoreDiagnosticRule(std::string_view str) {
     if (str == "derivative_uniformity") {
-        return DiagnosticRule::kDerivativeUniformity;
+        return CoreDiagnosticRule::kDerivativeUniformity;
     }
-    return DiagnosticRule::kUndefined;
+    return CoreDiagnosticRule::kUndefined;
 }
 
-utils::StringStream& operator<<(utils::StringStream& out, DiagnosticRule value) {
+utils::StringStream& operator<<(utils::StringStream& out, CoreDiagnosticRule value) {
     switch (value) {
-        case DiagnosticRule::kUndefined:
+        case CoreDiagnosticRule::kUndefined:
             return out << "undefined";
-        case DiagnosticRule::kChromiumUnreachableCode:
-            return out << "chromium_unreachable_code";
-        case DiagnosticRule::kDerivativeUniformity:
+        case CoreDiagnosticRule::kDerivativeUniformity:
             return out << "derivative_uniformity";
     }
     return out << "<unknown>";
 }
 
+/// ParseChromiumDiagnosticRule parses a ChromiumDiagnosticRule from a string.
+/// @param str the string to parse
+/// @returns the parsed enum, or ChromiumDiagnosticRule::kUndefined if the string could not be
+/// parsed.
+ChromiumDiagnosticRule ParseChromiumDiagnosticRule(std::string_view str) {
+    if (str == "unreachable_code") {
+        return ChromiumDiagnosticRule::kUnreachableCode;
+    }
+    return ChromiumDiagnosticRule::kUndefined;
+}
+
+utils::StringStream& operator<<(utils::StringStream& out, ChromiumDiagnosticRule value) {
+    switch (value) {
+        case ChromiumDiagnosticRule::kUndefined:
+            return out << "undefined";
+        case ChromiumDiagnosticRule::kUnreachableCode:
+            return out << "unreachable_code";
+    }
+    return out << "<unknown>";
+}
+
 }  // namespace tint::builtin
diff --git a/src/tint/builtin/diagnostic_rule.cc.tmpl b/src/tint/builtin/diagnostic_rule.cc.tmpl
index 8705485..8e64b09 100644
--- a/src/tint/builtin/diagnostic_rule.cc.tmpl
+++ b/src/tint/builtin/diagnostic_rule.cc.tmpl
@@ -18,8 +18,12 @@
 
 namespace tint::builtin {
 
-{{ Eval "ParseEnum" (Sem.Enum "diagnostic_rule")}}
+{{ Eval "ParseEnum" (Sem.Enum "core_diagnostic_rule")}}
 
-{{ Eval "EnumOStream" (Sem.Enum "diagnostic_rule")}}
+{{ Eval "EnumOStream" (Sem.Enum "core_diagnostic_rule")}}
+
+{{ Eval "ParseEnum" (Sem.Enum "chromium_diagnostic_rule")}}
+
+{{ Eval "EnumOStream" (Sem.Enum "chromium_diagnostic_rule")}}
 
 }  // namespace tint::builtin
diff --git a/src/tint/builtin/diagnostic_rule.h b/src/tint/builtin/diagnostic_rule.h
index 6b6d094..aa641b0 100644
--- a/src/tint/builtin/diagnostic_rule.h
+++ b/src/tint/builtin/diagnostic_rule.h
@@ -24,33 +24,56 @@
 #define SRC_TINT_BUILTIN_DIAGNOSTIC_RULE_H_
 
 #include <string>
+#include <variant>
 
 #include "src/tint/utils/string_stream.h"
 
 namespace tint::builtin {
 
-/// The diagnostic rule.
-enum class DiagnosticRule {
+/// WGSL core diagnostic rules.
+enum class CoreDiagnosticRule {
     kUndefined,
-    kChromiumUnreachableCode,
     kDerivativeUniformity,
 };
 
 /// @param out the stream to write to
-/// @param value the DiagnosticRule
+/// @param value the CoreDiagnosticRule
 /// @returns `out` so calls can be chained
-utils::StringStream& operator<<(utils::StringStream& out, DiagnosticRule value);
+utils::StringStream& operator<<(utils::StringStream& out, CoreDiagnosticRule value);
 
-/// ParseDiagnosticRule parses a DiagnosticRule from a string.
+/// ParseCoreDiagnosticRule parses a CoreDiagnosticRule from a string.
 /// @param str the string to parse
-/// @returns the parsed enum, or DiagnosticRule::kUndefined if the string could not be parsed.
-DiagnosticRule ParseDiagnosticRule(std::string_view str);
+/// @returns the parsed enum, or CoreDiagnosticRule::kUndefined if the string could not be parsed.
+CoreDiagnosticRule ParseCoreDiagnosticRule(std::string_view str);
 
-constexpr const char* kDiagnosticRuleStrings[] = {
-    "chromium_unreachable_code",
+constexpr const char* kCoreDiagnosticRuleStrings[] = {
     "derivative_uniformity",
 };
 
+/// Chromium-specific diagnostic rules.
+enum class ChromiumDiagnosticRule {
+    kUndefined,
+    kUnreachableCode,
+};
+
+/// @param out the stream to write to
+/// @param value the ChromiumDiagnosticRule
+/// @returns `out` so calls can be chained
+utils::StringStream& operator<<(utils::StringStream& out, ChromiumDiagnosticRule value);
+
+/// ParseChromiumDiagnosticRule parses a ChromiumDiagnosticRule from a string.
+/// @param str the string to parse
+/// @returns the parsed enum, or ChromiumDiagnosticRule::kUndefined if the string could not be
+/// parsed.
+ChromiumDiagnosticRule ParseChromiumDiagnosticRule(std::string_view str);
+
+constexpr const char* kChromiumDiagnosticRuleStrings[] = {
+    "unreachable_code",
+};
+
+/// All diagnostic rules understood by Tint.
+using DiagnosticRule = std::variant<CoreDiagnosticRule, ChromiumDiagnosticRule>;
+
 }  // namespace tint::builtin
 
 #endif  // SRC_TINT_BUILTIN_DIAGNOSTIC_RULE_H_
diff --git a/src/tint/builtin/diagnostic_rule.h.tmpl b/src/tint/builtin/diagnostic_rule.h.tmpl
index ba823e3..16d7f9e 100644
--- a/src/tint/builtin/diagnostic_rule.h.tmpl
+++ b/src/tint/builtin/diagnostic_rule.h.tmpl
@@ -14,13 +14,20 @@
 #define SRC_TINT_BUILTIN_DIAGNOSTIC_RULE_H_
 
 #include <string>
+#include <variant>
 
 #include "src/tint/utils/string_stream.h"
 
 namespace tint::builtin {
 
-/// The diagnostic rule.
-{{ Eval "DeclareEnum" (Sem.Enum "diagnostic_rule") }}
+/// WGSL core diagnostic rules.
+{{ Eval "DeclareEnum" (Sem.Enum "core_diagnostic_rule") }}
+
+/// Chromium-specific diagnostic rules.
+{{ Eval "DeclareEnum" (Sem.Enum "chromium_diagnostic_rule") }}
+
+/// All diagnostic rules understood by Tint.
+using DiagnosticRule = std::variant<CoreDiagnosticRule, ChromiumDiagnosticRule>;
 
 }  // namespace tint::builtin
 
diff --git a/src/tint/builtin/diagnostic_rule_bench.cc b/src/tint/builtin/diagnostic_rule_bench.cc
index c98bd46..fb60580 100644
--- a/src/tint/builtin/diagnostic_rule_bench.cc
+++ b/src/tint/builtin/diagnostic_rule_bench.cc
@@ -29,23 +29,36 @@
 namespace tint::builtin {
 namespace {
 
-void DiagnosticRuleParser(::benchmark::State& state) {
+void CoreDiagnosticRuleParser(::benchmark::State& state) {
     const char* kStrings[] = {
-        "chromium_unrachaccle_code",   "clromium_unreachab3_oe",    "chromium_unreachable_Vode",
-        "chromium_unreachable_code",   "chro1ium_unreachable_code", "chromium_unreJchableqqcde",
-        "chromium77unreallhable_code", "dqqrvatiHHe_uniforppity",   "deriatcv_nvformity",
-        "derivatbe_unGformity",        "derivative_uniformity",     "derivative_iinifvrmity",
-        "derivat8WWe_uniformity",      "drivaxxive_uniformity",
+        "deriative_unccformity",   "dlivative_3iformiy",    "derivative_uniforVity",
+        "derivative_uniformity",   "derivative_uniform1ty", "derivativeJunifqrmity",
+        "derivative_unifllrmit77",
     };
     for (auto _ : state) {
         for (auto* str : kStrings) {
-            auto result = ParseDiagnosticRule(str);
+            auto result = ParseCoreDiagnosticRule(str);
             benchmark::DoNotOptimize(result);
         }
     }
 }
 
-BENCHMARK(DiagnosticRuleParser);
+BENCHMARK(CoreDiagnosticRuleParser);
+
+void ChromiumDiagnosticRuleParser(::benchmark::State& state) {
+    const char* kStrings[] = {
+        "pqnreachableHHcode", "unrechcbe_cov",     "unreachGblecode",  "unreachable_code",
+        "vnriiachable_code",  "unreac8ablWW_code", "unreMchablxxcode",
+    };
+    for (auto _ : state) {
+        for (auto* str : kStrings) {
+            auto result = ParseChromiumDiagnosticRule(str);
+            benchmark::DoNotOptimize(result);
+        }
+    }
+}
+
+BENCHMARK(ChromiumDiagnosticRuleParser);
 
 }  // namespace
 }  // namespace tint::builtin
diff --git a/src/tint/builtin/diagnostic_rule_bench.cc.tmpl b/src/tint/builtin/diagnostic_rule_bench.cc.tmpl
index 2605aae..dab6856 100644
--- a/src/tint/builtin/diagnostic_rule_bench.cc.tmpl
+++ b/src/tint/builtin/diagnostic_rule_bench.cc.tmpl
@@ -19,7 +19,9 @@
 namespace tint::builtin {
 namespace {
 
-{{ Eval "BenchmarkParseEnum" (Sem.Enum "diagnostic_rule")}}
+{{ Eval "BenchmarkParseEnum" (Sem.Enum "core_diagnostic_rule")}}
+
+{{ Eval "BenchmarkParseEnum" (Sem.Enum "chromium_diagnostic_rule")}}
 
 }  // namespace
 }  // namespace tint::builtin
diff --git a/src/tint/builtin/diagnostic_rule_test.cc b/src/tint/builtin/diagnostic_rule_test.cc
index 9982061..78bd3f3 100644
--- a/src/tint/builtin/diagnostic_rule_test.cc
+++ b/src/tint/builtin/diagnostic_rule_test.cc
@@ -29,13 +29,13 @@
 namespace tint::builtin {
 namespace {
 
-namespace diagnostic_rule_tests {
+namespace core_diagnostic_rule_tests {
 
 namespace parse_print_tests {
 
 struct Case {
     const char* string;
-    DiagnosticRule value;
+    CoreDiagnosticRule value;
 };
 
 inline std::ostream& operator<<(std::ostream& out, Case c) {
@@ -43,43 +43,95 @@
 }
 
 static constexpr Case kValidCases[] = {
-    {"chromium_unreachable_code", DiagnosticRule::kChromiumUnreachableCode},
-    {"derivative_uniformity", DiagnosticRule::kDerivativeUniformity},
+    {"derivative_uniformity", CoreDiagnosticRule::kDerivativeUniformity},
 };
 
 static constexpr Case kInvalidCases[] = {
-    {"chromium_unrachaccle_code", DiagnosticRule::kUndefined},
-    {"clromium_unreachab3_oe", DiagnosticRule::kUndefined},
-    {"chromium_unreachable_Vode", DiagnosticRule::kUndefined},
-    {"derivative_uniform1ty", DiagnosticRule::kUndefined},
-    {"derivativeJunifqrmity", DiagnosticRule::kUndefined},
-    {"derivative_unifllrmit77", DiagnosticRule::kUndefined},
+    {"deriative_unccformity", CoreDiagnosticRule::kUndefined},
+    {"dlivative_3iformiy", CoreDiagnosticRule::kUndefined},
+    {"derivative_uniforVity", CoreDiagnosticRule::kUndefined},
 };
 
-using DiagnosticRuleParseTest = testing::TestWithParam<Case>;
+using CoreDiagnosticRuleParseTest = testing::TestWithParam<Case>;
 
-TEST_P(DiagnosticRuleParseTest, Parse) {
+TEST_P(CoreDiagnosticRuleParseTest, Parse) {
     const char* string = GetParam().string;
-    DiagnosticRule expect = GetParam().value;
-    EXPECT_EQ(expect, ParseDiagnosticRule(string));
+    CoreDiagnosticRule expect = GetParam().value;
+    EXPECT_EQ(expect, ParseCoreDiagnosticRule(string));
 }
 
-INSTANTIATE_TEST_SUITE_P(ValidCases, DiagnosticRuleParseTest, testing::ValuesIn(kValidCases));
-INSTANTIATE_TEST_SUITE_P(InvalidCases, DiagnosticRuleParseTest, testing::ValuesIn(kInvalidCases));
+INSTANTIATE_TEST_SUITE_P(ValidCases, CoreDiagnosticRuleParseTest, testing::ValuesIn(kValidCases));
+INSTANTIATE_TEST_SUITE_P(InvalidCases,
+                         CoreDiagnosticRuleParseTest,
+                         testing::ValuesIn(kInvalidCases));
 
-using DiagnosticRulePrintTest = testing::TestWithParam<Case>;
+using CoreDiagnosticRulePrintTest = testing::TestWithParam<Case>;
 
-TEST_P(DiagnosticRulePrintTest, Print) {
-    DiagnosticRule value = GetParam().value;
+TEST_P(CoreDiagnosticRulePrintTest, Print) {
+    CoreDiagnosticRule value = GetParam().value;
     const char* expect = GetParam().string;
     EXPECT_EQ(expect, utils::ToString(value));
 }
 
-INSTANTIATE_TEST_SUITE_P(ValidCases, DiagnosticRulePrintTest, testing::ValuesIn(kValidCases));
+INSTANTIATE_TEST_SUITE_P(ValidCases, CoreDiagnosticRulePrintTest, testing::ValuesIn(kValidCases));
 
 }  // namespace parse_print_tests
 
-}  // namespace diagnostic_rule_tests
+}  // namespace core_diagnostic_rule_tests
+
+namespace chromium_diagnostic_rule_tests {
+
+namespace parse_print_tests {
+
+struct Case {
+    const char* string;
+    ChromiumDiagnosticRule value;
+};
+
+inline std::ostream& operator<<(std::ostream& out, Case c) {
+    return out << "'" << std::string(c.string) << "'";
+}
+
+static constexpr Case kValidCases[] = {
+    {"unreachable_code", ChromiumDiagnosticRule::kUnreachableCode},
+};
+
+static constexpr Case kInvalidCases[] = {
+    {"unreacha1le_code", ChromiumDiagnosticRule::kUndefined},
+    {"unreachableJcqde", ChromiumDiagnosticRule::kUndefined},
+    {"unreachable77llode", ChromiumDiagnosticRule::kUndefined},
+};
+
+using ChromiumDiagnosticRuleParseTest = testing::TestWithParam<Case>;
+
+TEST_P(ChromiumDiagnosticRuleParseTest, Parse) {
+    const char* string = GetParam().string;
+    ChromiumDiagnosticRule expect = GetParam().value;
+    EXPECT_EQ(expect, ParseChromiumDiagnosticRule(string));
+}
+
+INSTANTIATE_TEST_SUITE_P(ValidCases,
+                         ChromiumDiagnosticRuleParseTest,
+                         testing::ValuesIn(kValidCases));
+INSTANTIATE_TEST_SUITE_P(InvalidCases,
+                         ChromiumDiagnosticRuleParseTest,
+                         testing::ValuesIn(kInvalidCases));
+
+using ChromiumDiagnosticRulePrintTest = testing::TestWithParam<Case>;
+
+TEST_P(ChromiumDiagnosticRulePrintTest, Print) {
+    ChromiumDiagnosticRule value = GetParam().value;
+    const char* expect = GetParam().string;
+    EXPECT_EQ(expect, utils::ToString(value));
+}
+
+INSTANTIATE_TEST_SUITE_P(ValidCases,
+                         ChromiumDiagnosticRulePrintTest,
+                         testing::ValuesIn(kValidCases));
+
+}  // namespace parse_print_tests
+
+}  // namespace chromium_diagnostic_rule_tests
 
 }  // namespace
 }  // namespace tint::builtin
diff --git a/src/tint/builtin/diagnostic_rule_test.cc.tmpl b/src/tint/builtin/diagnostic_rule_test.cc.tmpl
index 106f6fc..cc12bf7 100644
--- a/src/tint/builtin/diagnostic_rule_test.cc.tmpl
+++ b/src/tint/builtin/diagnostic_rule_test.cc.tmpl
@@ -19,11 +19,17 @@
 namespace tint::builtin {
 namespace {
 
-namespace diagnostic_rule_tests {
+namespace core_diagnostic_rule_tests {
 
-{{ Eval "TestParsePrintEnum" (Sem.Enum "diagnostic_rule")}}
+{{ Eval "TestParsePrintEnum" (Sem.Enum "core_diagnostic_rule")}}
 
-}  // namespace diagnostic_rule_tests
+}  // namespace core_diagnostic_rule_tests
+
+namespace chromium_diagnostic_rule_tests {
+
+{{ Eval "TestParsePrintEnum" (Sem.Enum "chromium_diagnostic_rule")}}
+
+}  // namespace chromium_diagnostic_rule_tests
 
 }  // namespace
 }  // namespace tint::builtin
diff --git a/src/tint/intrinsics.def b/src/tint/intrinsics.def
index fae4ee4..dacd664 100644
--- a/src/tint/intrinsics.def
+++ b/src/tint/intrinsics.def
@@ -41,11 +41,15 @@
 }
 
 // https://gpuweb.github.io/gpuweb/wgsl/#filterable-triggering-rules
-enum diagnostic_rule {
+enum core_diagnostic_rule {
   // Rules defined in the spec.
   derivative_uniformity
+}
+
+// chromium-specific diagnostics
+enum chromium_diagnostic_rule {
   // Chromium specific rules not defined in the spec.
-  chromium_unreachable_code
+  unreachable_code
 }
 
 // https://gpuweb.github.io/gpuweb/wgsl/#syntax-severity_control_name
diff --git a/src/tint/program_builder.h b/src/tint/program_builder.h
index b7fda95..b4f5eea 100644
--- a/src/tint/program_builder.h
+++ b/src/tint/program_builder.h
@@ -39,6 +39,7 @@
 #include "src/tint/ast/diagnostic_attribute.h"
 #include "src/tint/ast/diagnostic_control.h"
 #include "src/tint/ast/diagnostic_directive.h"
+#include "src/tint/ast/diagnostic_rule_name.h"
 #include "src/tint/ast/disable_validation_attribute.h"
 #include "src/tint/ast/discard_statement.h"
 #include "src/tint/ast/enable.h"
@@ -3752,57 +3753,129 @@
                                                                   validation);
     }
 
-    /// Creates an ast::DiagnosticAttribute
-    /// @param source the source information
-    /// @param severity the diagnostic severity control
-    /// @param rule_name the diagnostic rule name
-    /// @returns the diagnostic attribute pointer
+    /// Passthrough overload
+    /// @param name the diagnostic rule name
+    /// @returns @p name
+    const ast::DiagnosticRuleName* DiagnosticRuleName(const ast::DiagnosticRuleName* name) {
+        return name;
+    }
+
+    /// Creates an ast::DiagnosticRuleName
+    /// @param name the diagnostic rule name
+    /// @returns the diagnostic rule name
     template <typename NAME>
-    const ast::DiagnosticAttribute* DiagnosticAttribute(const Source& source,
-                                                        builtin::DiagnosticSeverity severity,
-                                                        NAME&& rule_name) {
+    const ast::DiagnosticRuleName* DiagnosticRuleName(NAME&& name) {
         static_assert(
             !utils::traits::IsType<utils::traits::PtrElTy<NAME>, ast::TemplatedIdentifier>,
             "it is invalid for a diagnostic rule name to be templated");
+        auto* name_ident = Ident(std::forward<NAME>(name));
+        return create<ast::DiagnosticRuleName>(name_ident->source, name_ident);
+    }
+
+    /// Creates an ast::DiagnosticRuleName
+    /// @param category the diagnostic rule category
+    /// @param name the diagnostic rule name
+    /// @returns the diagnostic rule name
+    template <typename CATEGORY, typename NAME, typename = DisableIfSource<CATEGORY>>
+    const ast::DiagnosticRuleName* DiagnosticRuleName(CATEGORY&& category, NAME&& name) {
+        static_assert(
+            !utils::traits::IsType<utils::traits::PtrElTy<NAME>, ast::TemplatedIdentifier>,
+            "it is invalid for a diagnostic rule name to be templated");
+        static_assert(
+            !utils::traits::IsType<utils::traits::PtrElTy<CATEGORY>, ast::TemplatedIdentifier>,
+            "it is invalid for a diagnostic rule category to be templated");
+        auto* category_ident = Ident(std::forward<CATEGORY>(category));
+        auto* name_ident = Ident(std::forward<NAME>(name));
+        Source source = category_ident->source;
+        source.range.end = name_ident->source.range.end;
+        return create<ast::DiagnosticRuleName>(source, category_ident, name_ident);
+    }
+
+    /// Creates an ast::DiagnosticRuleName
+    /// @param source the source information
+    /// @param name the diagnostic rule name
+    /// @returns the diagnostic rule name
+    template <typename NAME>
+    const ast::DiagnosticRuleName* DiagnosticRuleName(const Source& source, NAME&& name) {
+        static_assert(
+            !utils::traits::IsType<utils::traits::PtrElTy<NAME>, ast::TemplatedIdentifier>,
+            "it is invalid for a diagnostic rule name to be templated");
+        auto* name_ident = Ident(std::forward<NAME>(name));
+        return create<ast::DiagnosticRuleName>(source, name_ident);
+    }
+
+    /// Creates an ast::DiagnosticRuleName
+    /// @param source the source information
+    /// @param category the diagnostic rule category
+    /// @param name the diagnostic rule name
+    /// @returns the diagnostic rule name
+    template <typename CATEGORY, typename NAME>
+    const ast::DiagnosticRuleName* DiagnosticRuleName(const Source& source,
+                                                      CATEGORY&& category,
+                                                      NAME&& name) {
+        static_assert(
+            !utils::traits::IsType<utils::traits::PtrElTy<NAME>, ast::TemplatedIdentifier>,
+            "it is invalid for a diagnostic rule name to be templated");
+        static_assert(
+            !utils::traits::IsType<utils::traits::PtrElTy<CATEGORY>, ast::TemplatedIdentifier>,
+            "it is invalid for a diagnostic rule category to be templated");
+        auto* category_ident = Ident(std::forward<CATEGORY>(category));
+        auto* name_ident = Ident(std::forward<NAME>(name));
+        return create<ast::DiagnosticRuleName>(source, category_ident, name_ident);
+    }
+
+    /// Creates an ast::DiagnosticAttribute
+    /// @param source the source information
+    /// @param severity the diagnostic severity control
+    /// @param rule_args the arguments used to construct the rule name
+    /// @returns the diagnostic attribute pointer
+    template <typename... RULE_ARGS>
+    const ast::DiagnosticAttribute* DiagnosticAttribute(const Source& source,
+                                                        builtin::DiagnosticSeverity severity,
+                                                        RULE_ARGS&&... rule_args) {
         return create<ast::DiagnosticAttribute>(
-            source, ast::DiagnosticControl(severity, Ident(std::forward<NAME>(rule_name))));
+            source, ast::DiagnosticControl(
+                        severity, DiagnosticRuleName(std::forward<RULE_ARGS>(rule_args)...)));
     }
 
     /// Creates an ast::DiagnosticAttribute
     /// @param severity the diagnostic severity control
-    /// @param rule_name the diagnostic rule name
+    /// @param rule_args the arguments used to construct the rule name
     /// @returns the diagnostic attribute pointer
-    template <typename NAME>
+    template <typename... RULE_ARGS>
     const ast::DiagnosticAttribute* DiagnosticAttribute(builtin::DiagnosticSeverity severity,
-                                                        NAME&& rule_name) {
+                                                        RULE_ARGS&&... rule_args) {
         return create<ast::DiagnosticAttribute>(
-            source_, ast::DiagnosticControl(severity, Ident(std::forward<NAME>(rule_name))));
+            source_, ast::DiagnosticControl(
+                         severity, DiagnosticRuleName(std::forward<RULE_ARGS>(rule_args)...)));
     }
 
     /// Add a diagnostic directive to the module.
     /// @param source the source information
     /// @param severity the diagnostic severity control
-    /// @param rule_name the diagnostic rule name
+    /// @param rule_args the arguments used to construct the rule name
     /// @returns the diagnostic directive pointer
-    template <typename NAME>
+    template <typename... RULE_ARGS>
     const ast::DiagnosticDirective* DiagnosticDirective(const Source& source,
                                                         builtin::DiagnosticSeverity severity,
-                                                        NAME&& rule_name) {
-        auto* directive = create<ast::DiagnosticDirective>(
-            source, ast::DiagnosticControl(severity, Ident(std::forward<NAME>(rule_name))));
+                                                        RULE_ARGS&&... rule_args) {
+        auto* rule = DiagnosticRuleName(std::forward<RULE_ARGS>(rule_args)...);
+        auto* directive =
+            create<ast::DiagnosticDirective>(source, ast::DiagnosticControl(severity, rule));
         AST().AddDiagnosticDirective(directive);
         return directive;
     }
 
     /// Add a diagnostic directive to the module.
     /// @param severity the diagnostic severity control
-    /// @param rule_name the diagnostic rule name
+    /// @param rule_args the arguments used to construct the rule name
     /// @returns the diagnostic directive pointer
-    template <typename NAME>
+    template <typename... RULE_ARGS>
     const ast::DiagnosticDirective* DiagnosticDirective(builtin::DiagnosticSeverity severity,
-                                                        NAME&& rule_name) {
-        auto* directive = create<ast::DiagnosticDirective>(
-            source_, ast::DiagnosticControl(severity, Ident(std::forward<NAME>(rule_name))));
+                                                        RULE_ARGS&&... rule_args) {
+        auto* rule = DiagnosticRuleName(std::forward<RULE_ARGS>(rule_args)...);
+        auto* directive =
+            create<ast::DiagnosticDirective>(source_, ast::DiagnosticControl(severity, rule));
         AST().AddDiagnosticDirective(directive);
         return directive;
     }
diff --git a/src/tint/reader/wgsl/parser_impl.cc b/src/tint/reader/wgsl/parser_impl.cc
index e8a9bd4..514e7f8 100644
--- a/src/tint/reader/wgsl/parser_impl.cc
+++ b/src/tint/reader/wgsl/parser_impl.cc
@@ -3092,7 +3092,7 @@
 }
 
 // diagnostic_control
-// : PAREN_LEFT severity_control_name COMMA ident_pattern_token COMMA ? PAREN_RIGHT
+// : PAREN_LEFT severity_control_name COMMA diagnostic_rule_name COMMA ? PAREN_RIGHT
 Expect<ast::DiagnosticControl> ParserImpl::expect_diagnostic_control() {
     return expect_paren_block("diagnostic control", [&]() -> Expect<ast::DiagnosticControl> {
         auto severity_control = expect_severity_control_name();
@@ -3104,7 +3104,7 @@
             return Failure::kErrored;
         }
 
-        auto rule_name = expect_ident("diagnostic control");
+        auto rule_name = expect_diagnostic_rule_name();
         if (rule_name.errored) {
             return Failure::kErrored;
         }
@@ -3114,6 +3114,31 @@
     });
 }
 
+// diagnostic_rule_name :
+// | diagnostic_name_token
+// | diagnostic_name_token '.' diagnostic_name_token
+Expect<const ast::DiagnosticRuleName*> ParserImpl::expect_diagnostic_rule_name() {
+    if (peek_is(Token::Type::kPeriod, 1)) {
+        auto category = expect_ident("", "diagnostic rule category");
+        if (category.errored) {
+            return Failure::kErrored;
+        }
+        if (!expect("diagnostic rule", Token::Type::kPeriod)) {
+            return Failure::kErrored;
+        }
+        auto name = expect_ident("", "diagnostic rule name");
+        if (name.errored) {
+            return Failure::kErrored;
+        }
+        return builder_.DiagnosticRuleName(category.value, name.value);
+    }
+    auto name = expect_ident("", "diagnostic rule name");
+    if (name.errored) {
+        return Failure::kErrored;
+    }
+    return builder_.DiagnosticRuleName(name.value);
+}
+
 bool ParserImpl::match(Token::Type tok, Source* source /*= nullptr*/) {
     auto& t = peek();
 
@@ -3214,7 +3239,9 @@
     return {static_cast<uint32_t>(sint.value), sint.source};
 }
 
-Expect<const ast::Identifier*> ParserImpl::expect_ident(std::string_view use) {
+Expect<const ast::Identifier*> ParserImpl::expect_ident(
+    std::string_view use,
+    std::string_view kind /* = "identifier" */) {
     auto& t = peek();
     if (t.IsIdentifier()) {
         synchronized_ = true;
@@ -3230,7 +3257,7 @@
         return Failure::kErrored;
     }
     synchronized_ = false;
-    return add_error(t.source(), "expected identifier", use);
+    return add_error(t.source(), "expected " + std::string(kind), use);
 }
 
 template <typename F, typename T>
diff --git a/src/tint/reader/wgsl/parser_impl.h b/src/tint/reader/wgsl/parser_impl.h
index 95c4db9..c7ac96d 100644
--- a/src/tint/reader/wgsl/parser_impl.h
+++ b/src/tint/reader/wgsl/parser_impl.h
@@ -636,14 +636,17 @@
     /// Parses a single attribute, reporting an error if the next token does not
     /// represent a attribute.
     /// @see #attribute for the full list of attributes this method parses.
-    /// @return the parsed attribute, or nullptr on error.
+    /// @return the parsed attribute.
     Expect<const ast::Attribute*> expect_attribute();
     /// Parses a severity_control_name grammar element.
-    /// @return the parsed severity control name, or nullptr on error.
+    /// @return the parsed severity control name.
     Expect<builtin::DiagnosticSeverity> expect_severity_control_name();
     /// Parses a diagnostic_control grammar element.
-    /// @return the parsed diagnostic control, or nullptr on error.
+    /// @return the parsed diagnostic control.
     Expect<ast::DiagnosticControl> expect_diagnostic_control();
+    /// Parses a diagnostic_rule_name grammar element.
+    /// @return the parsed diagnostic rule name.
+    Expect<const ast::DiagnosticRuleName*> expect_diagnostic_rule_name();
 
     /// Splits a peekable token into to parts filling in the peekable fields.
     /// @param lhs the token to set in the current position
@@ -694,8 +697,11 @@
     /// Errors if the next token is not an identifier.
     /// Consumes the next token on match.
     /// @param use a description of what was being parsed if an error was raised
+    /// @param kind a string describing the kind of identifier.
+    ///             Examples: "identifier", "diagnostic name"
     /// @returns the parsed identifier.
-    Expect<const ast::Identifier*> expect_ident(std::string_view use);
+    Expect<const ast::Identifier*> expect_ident(std::string_view use,
+                                                std::string_view kind = "identifier");
     /// Parses a lexical block starting with the token `start` and ending with
     /// the token `end`. `body` is called to parse the lexical block body
     /// between the `start` and `end` tokens. If the `start` or `end` tokens
diff --git a/src/tint/reader/wgsl/parser_impl_diagnostic_attribute_test.cc b/src/tint/reader/wgsl/parser_impl_diagnostic_attribute_test.cc
index 991d5b6..69f0003 100644
--- a/src/tint/reader/wgsl/parser_impl_diagnostic_attribute_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_diagnostic_attribute_test.cc
@@ -20,7 +20,7 @@
 namespace tint::reader::wgsl {
 namespace {
 
-TEST_F(ParserImplTest, DiagnosticAttribute_Valid) {
+TEST_F(ParserImplTest, DiagnosticAttribute_Name) {
     auto p = parser("diagnostic(off, foo)");
     auto a = p->attribute();
     EXPECT_FALSE(p->has_error()) << p->error();
@@ -30,7 +30,22 @@
     EXPECT_EQ(d->control.severity, builtin::DiagnosticSeverity::kOff);
     auto* r = d->control.rule_name;
     ASSERT_NE(r, nullptr);
-    ast::CheckIdentifier(r, "foo");
+    EXPECT_EQ(r->category, nullptr);
+    ast::CheckIdentifier(r->name, "foo");
+}
+
+TEST_F(ParserImplTest, DiagnosticAttribute_CategoryName) {
+    auto p = parser("diagnostic(off, foo.bar)");
+    auto a = p->attribute();
+    EXPECT_FALSE(p->has_error()) << p->error();
+    EXPECT_TRUE(a.matched);
+    auto* d = a.value->As<ast::DiagnosticAttribute>();
+    ASSERT_NE(d, nullptr);
+    EXPECT_EQ(d->control.severity, builtin::DiagnosticSeverity::kOff);
+    auto* r = d->control.rule_name;
+    ASSERT_NE(r, nullptr);
+    ast::CheckIdentifier(r->category, "foo");
+    ast::CheckIdentifier(r->name, "bar");
 }
 
 }  // namespace
diff --git a/src/tint/reader/wgsl/parser_impl_diagnostic_control_test.cc b/src/tint/reader/wgsl/parser_impl_diagnostic_control_test.cc
index e45d1e7..bad434e 100644
--- a/src/tint/reader/wgsl/parser_impl_diagnostic_control_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_diagnostic_control_test.cc
@@ -23,7 +23,7 @@
 using SeverityPair = std::pair<std::string, builtin::DiagnosticSeverity>;
 class DiagnosticControlParserTest : public ParserImplTestWithParam<SeverityPair> {};
 
-TEST_P(DiagnosticControlParserTest, DiagnosticControl_Valid) {
+TEST_P(DiagnosticControlParserTest, DiagnosticControl_Name) {
     auto& params = GetParam();
     auto p = parser("(" + params.first + ", foo)");
     auto e = p->expect_diagnostic_control();
@@ -33,7 +33,21 @@
 
     auto* r = e->rule_name;
     ASSERT_NE(r, nullptr);
-    ast::CheckIdentifier(r, "foo");
+    EXPECT_EQ(r->category, nullptr);
+    ast::CheckIdentifier(r->name, "foo");
+}
+TEST_P(DiagnosticControlParserTest, DiagnosticControl_CategoryAndName) {
+    auto& params = GetParam();
+    auto p = parser("(" + params.first + ", foo.bar)");
+    auto e = p->expect_diagnostic_control();
+    EXPECT_FALSE(e.errored);
+    EXPECT_FALSE(p->has_error()) << p->error();
+    EXPECT_EQ(e->severity, params.second);
+
+    auto* r = e->rule_name;
+    ASSERT_NE(r, nullptr);
+    ast::CheckIdentifier(r->category, "foo");
+    ast::CheckIdentifier(r->name, "bar");
 }
 INSTANTIATE_TEST_SUITE_P(DiagnosticControlParserTest,
                          DiagnosticControlParserTest,
@@ -43,7 +57,7 @@
                                          SeverityPair{"info", builtin::DiagnosticSeverity::kInfo},
                                          SeverityPair{"off", builtin::DiagnosticSeverity::kOff}));
 
-TEST_F(ParserImplTest, DiagnosticControl_Valid_TrailingComma) {
+TEST_F(ParserImplTest, DiagnosticControl_Name_TrailingComma) {
     auto p = parser("(error, foo,)");
     auto e = p->expect_diagnostic_control();
     EXPECT_FALSE(e.errored);
@@ -52,7 +66,21 @@
 
     auto* r = e->rule_name;
     ASSERT_NE(r, nullptr);
-    ast::CheckIdentifier(r, "foo");
+    EXPECT_EQ(r->category, nullptr);
+    ast::CheckIdentifier(r->name, "foo");
+}
+
+TEST_F(ParserImplTest, DiagnosticControl_CategoryAndName_TrailingComma) {
+    auto p = parser("(error, foo.bar,)");
+    auto e = p->expect_diagnostic_control();
+    EXPECT_FALSE(e.errored);
+    EXPECT_FALSE(p->has_error()) << p->error();
+    EXPECT_EQ(e->severity, builtin::DiagnosticSeverity::kError);
+
+    auto* r = e->rule_name;
+    ASSERT_NE(r, nullptr);
+    ast::CheckIdentifier(r->category, "foo");
+    ast::CheckIdentifier(r->name, "bar");
 }
 
 TEST_F(ParserImplTest, DiagnosticControl_MissingOpenParen) {
@@ -102,7 +130,15 @@
     auto e = p->expect_diagnostic_control();
     EXPECT_TRUE(e.errored);
     EXPECT_TRUE(p->has_error());
-    EXPECT_EQ(p->error(), R"(1:6: expected identifier for diagnostic control)");
+    EXPECT_EQ(p->error(), R"(1:6: expected diagnostic rule name)");
+}
+
+TEST_F(ParserImplTest, DiagnosticControl_MissingRuleCategory) {
+    auto p = parser("(off,for.foo)");
+    auto e = p->expect_diagnostic_control();
+    EXPECT_TRUE(e.errored);
+    EXPECT_TRUE(p->has_error());
+    EXPECT_EQ(p->error(), R"(1:6: expected diagnostic rule category)");
 }
 
 TEST_F(ParserImplTest, DiagnosticControl_InvalidRuleName) {
diff --git a/src/tint/reader/wgsl/parser_impl_diagnostic_directive_test.cc b/src/tint/reader/wgsl/parser_impl_diagnostic_directive_test.cc
index 12b69f1..a965112 100644
--- a/src/tint/reader/wgsl/parser_impl_diagnostic_directive_test.cc
+++ b/src/tint/reader/wgsl/parser_impl_diagnostic_directive_test.cc
@@ -20,7 +20,7 @@
 namespace tint::reader::wgsl {
 namespace {
 
-TEST_F(ParserImplTest, DiagnosticDirective_Valid) {
+TEST_F(ParserImplTest, DiagnosticDirective_Name) {
     auto p = parser("diagnostic(off, foo);");
     p->diagnostic_directive();
     EXPECT_FALSE(p->has_error()) << p->error();
@@ -33,7 +33,25 @@
 
     auto* r = directive->control.rule_name;
     ASSERT_NE(r, nullptr);
-    ast::CheckIdentifier(r, "foo");
+    EXPECT_EQ(r->category, nullptr);
+    ast::CheckIdentifier(r->name, "foo");
+}
+
+TEST_F(ParserImplTest, DiagnosticDirective_CategoryName) {
+    auto p = parser("diagnostic(off, foo.bar);");
+    p->diagnostic_directive();
+    EXPECT_FALSE(p->has_error()) << p->error();
+    auto& ast = p->builder().AST();
+    ASSERT_EQ(ast.DiagnosticDirectives().Length(), 1u);
+    auto* directive = ast.DiagnosticDirectives()[0];
+    EXPECT_EQ(directive->control.severity, builtin::DiagnosticSeverity::kOff);
+    ASSERT_EQ(ast.GlobalDeclarations().Length(), 1u);
+    EXPECT_EQ(ast.GlobalDeclarations()[0], directive);
+
+    auto* r = directive->control.rule_name;
+    ASSERT_NE(r, nullptr);
+    ast::CheckIdentifier(r->category, "foo");
+    ast::CheckIdentifier(r->name, "bar");
 }
 
 TEST_F(ParserImplTest, DiagnosticDirective_MissingSemicolon) {
diff --git a/src/tint/resolver/attribute_validation_test.cc b/src/tint/resolver/attribute_validation_test.cc
index d6a2f0f..098ab17 100644
--- a/src/tint/resolver/attribute_validation_test.cc
+++ b/src/tint/resolver/attribute_validation_test.cc
@@ -101,7 +101,7 @@
             return {builder.Builtin(source, builtin::BuiltinValue::kPosition)};
         case AttributeKind::kDiagnostic:
             return {builder.DiagnosticAttribute(source, builtin::DiagnosticSeverity::kInfo,
-                                                "chromium_unreachable_code")};
+                                                "chromium", "unreachable_code")};
         case AttributeKind::kGroup:
             return {builder.Group(source, 1_a)};
         case AttributeKind::kId:
diff --git a/src/tint/resolver/diagnostic_control_test.cc b/src/tint/resolver/diagnostic_control_test.cc
index d7143c7..527acd1 100644
--- a/src/tint/resolver/diagnostic_control_test.cc
+++ b/src/tint/resolver/diagnostic_control_test.cc
@@ -32,7 +32,7 @@
 }
 
 TEST_F(ResolverDiagnosticControlTest, UnreachableCode_ErrorViaDirective) {
-    DiagnosticDirective(builtin::DiagnosticSeverity::kError, "chromium_unreachable_code");
+    DiagnosticDirective(builtin::DiagnosticSeverity::kError, "chromium", "unreachable_code");
 
     auto stmts = utils::Vector{Return(), Return()};
     Func("foo", {}, ty.void_(), stmts);
@@ -42,7 +42,7 @@
 }
 
 TEST_F(ResolverDiagnosticControlTest, UnreachableCode_WarningViaDirective) {
-    DiagnosticDirective(builtin::DiagnosticSeverity::kWarning, "chromium_unreachable_code");
+    DiagnosticDirective(builtin::DiagnosticSeverity::kWarning, "chromium", "unreachable_code");
 
     auto stmts = utils::Vector{Return(), Return()};
     Func("foo", {}, ty.void_(), stmts);
@@ -52,7 +52,7 @@
 }
 
 TEST_F(ResolverDiagnosticControlTest, UnreachableCode_InfoViaDirective) {
-    DiagnosticDirective(builtin::DiagnosticSeverity::kInfo, "chromium_unreachable_code");
+    DiagnosticDirective(builtin::DiagnosticSeverity::kInfo, "chromium", "unreachable_code");
 
     auto stmts = utils::Vector{Return(), Return()};
     Func("foo", {}, ty.void_(), stmts);
@@ -62,7 +62,7 @@
 }
 
 TEST_F(ResolverDiagnosticControlTest, UnreachableCode_OffViaDirective) {
-    DiagnosticDirective(builtin::DiagnosticSeverity::kOff, "chromium_unreachable_code");
+    DiagnosticDirective(builtin::DiagnosticSeverity::kOff, "chromium", "unreachable_code");
 
     auto stmts = utils::Vector{Return(), Return()};
     Func("foo", {}, ty.void_(), stmts);
@@ -73,7 +73,7 @@
 
 TEST_F(ResolverDiagnosticControlTest, UnreachableCode_ErrorViaAttribute) {
     auto* attr =
-        DiagnosticAttribute(builtin::DiagnosticSeverity::kError, "chromium_unreachable_code");
+        DiagnosticAttribute(builtin::DiagnosticSeverity::kError, "chromium", "unreachable_code");
 
     auto stmts = utils::Vector{Return(), Return()};
     Func("foo", {}, ty.void_(), stmts, utils::Vector{attr});
@@ -84,7 +84,7 @@
 
 TEST_F(ResolverDiagnosticControlTest, UnreachableCode_WarningViaAttribute) {
     auto* attr =
-        DiagnosticAttribute(builtin::DiagnosticSeverity::kWarning, "chromium_unreachable_code");
+        DiagnosticAttribute(builtin::DiagnosticSeverity::kWarning, "chromium", "unreachable_code");
 
     auto stmts = utils::Vector{Return(), Return()};
     Func("foo", {}, ty.void_(), stmts, utils::Vector{attr});
@@ -95,7 +95,7 @@
 
 TEST_F(ResolverDiagnosticControlTest, UnreachableCode_InfoViaAttribute) {
     auto* attr =
-        DiagnosticAttribute(builtin::DiagnosticSeverity::kInfo, "chromium_unreachable_code");
+        DiagnosticAttribute(builtin::DiagnosticSeverity::kInfo, "chromium", "unreachable_code");
 
     auto stmts = utils::Vector{Return(), Return()};
     Func("foo", {}, ty.void_(), stmts, utils::Vector{attr});
@@ -106,7 +106,7 @@
 
 TEST_F(ResolverDiagnosticControlTest, UnreachableCode_OffViaAttribute) {
     auto* attr =
-        DiagnosticAttribute(builtin::DiagnosticSeverity::kOff, "chromium_unreachable_code");
+        DiagnosticAttribute(builtin::DiagnosticSeverity::kOff, "chromium", "unreachable_code");
 
     auto stmts = utils::Vector{Return(), Return()};
     Func("foo", {}, ty.void_(), stmts, utils::Vector{attr});
@@ -116,15 +116,15 @@
 }
 
 TEST_F(ResolverDiagnosticControlTest, UnreachableCode_ErrorViaDirective_OverriddenViaAttribute) {
-    // diagnostic(error, chromium_unreachable_code);
+    // diagnostic(error, chromium.unreachable_code);
     //
-    // @diagnostic(off, chromium_unreachable_code) fn foo() {
+    // @diagnostic(off, chromium.unreachable_code) fn foo() {
     //   return;
     //   return; // Should produce a warning
     // }
-    DiagnosticDirective(builtin::DiagnosticSeverity::kError, "chromium_unreachable_code");
+    DiagnosticDirective(builtin::DiagnosticSeverity::kError, "chromium", "unreachable_code");
     auto* attr =
-        DiagnosticAttribute(builtin::DiagnosticSeverity::kWarning, "chromium_unreachable_code");
+        DiagnosticAttribute(builtin::DiagnosticSeverity::kWarning, "chromium", "unreachable_code");
 
     auto stmts = utils::Vector{Return(), Return()};
     Func("foo", {}, ty.void_(), stmts, utils::Vector{attr});
@@ -134,7 +134,7 @@
 }
 
 TEST_F(ResolverDiagnosticControlTest, FunctionAttributeScope) {
-    // @diagnostic(off, chromium_unreachable_code) fn foo() {
+    // @diagnostic(off, chromium.unreachable_code) fn foo() {
     //   return;
     //   return; // Should not produce a diagnostic
     // }
@@ -144,13 +144,13 @@
     //   return; // Should produce a warning (default severity)
     // }
     //
-    // @diagnostic(info, chromium_unreachable_code) fn bar() {
+    // @diagnostic(info, chromium.unreachable_code) fn bar() {
     //   return;
     //   return; // Should produce an info
     // }
     {
         auto* attr =
-            DiagnosticAttribute(builtin::DiagnosticSeverity::kOff, "chromium_unreachable_code");
+            DiagnosticAttribute(builtin::DiagnosticSeverity::kOff, "chromium", "unreachable_code");
         Func("foo", {}, ty.void_(),
              utils::Vector{
                  Return(),
@@ -167,7 +167,7 @@
     }
     {
         auto* attr =
-            DiagnosticAttribute(builtin::DiagnosticSeverity::kInfo, "chromium_unreachable_code");
+            DiagnosticAttribute(builtin::DiagnosticSeverity::kInfo, "chromium", "unreachable_code");
         Func("zoo", {}, ty.void_(),
              utils::Vector{
                  Return(),
@@ -182,17 +182,17 @@
 }
 
 TEST_F(ResolverDiagnosticControlTest, BlockAttributeScope) {
-    // fn foo() @diagnostic(off, chromium_unreachable_code) {
+    // fn foo() @diagnostic(off, chromium.unreachable_code) {
     //   {
     //     return;
     //     return; // Should not produce a diagnostic
     //   }
-    //   @diagnostic(warning, chromium_unreachable_code) {
-    //     if (true) @diagnostic(info, chromium_unreachable_code) {
+    //   @diagnostic(warning, chromium.unreachable_code) {
+    //     if (true) @diagnostic(info, chromium.unreachable_code) {
     //       return;
     //       return; // Should produce an info
     //     } else {
-    //       while (true) @diagnostic(off, chromium_unreachable_code) {
+    //       while (true) @diagnostic(off, chromium.unreachable_code) {
     //         return;
     //         return; // Should not produce a diagnostic
     //       }
@@ -203,7 +203,7 @@
     // }
 
     auto attr = [&](auto severity) {
-        return utils::Vector{DiagnosticAttribute(severity, "chromium_unreachable_code")};
+        return utils::Vector{DiagnosticAttribute(severity, "chromium", "unreachable_code")};
     };
     Func("foo", {}, ty.void_(),
          utils::Vector{
@@ -241,113 +241,157 @@
 78:87 warning: code is unreachable)");
 }
 
-TEST_F(ResolverDiagnosticControlTest, UnrecognizedRuleName_Directive) {
+TEST_F(ResolverDiagnosticControlTest, UnrecognizedCoreRuleName_Directive) {
     DiagnosticDirective(builtin::DiagnosticSeverity::kError,
-                        Ident(Source{{12, 34}}, "chromium_unreachable_cod"));
+                        DiagnosticRuleName(Source{{12, 34}}, "derivative_uniform"));
     EXPECT_TRUE(r()->Resolve()) << r()->error();
     EXPECT_EQ(r()->error(),
-              R"(12:34 warning: unrecognized diagnostic rule 'chromium_unreachable_cod'
-Did you mean 'chromium_unreachable_code'?
-Possible values: 'chromium_unreachable_code', 'derivative_uniformity')");
+              R"(12:34 warning: unrecognized diagnostic rule 'derivative_uniform'
+Did you mean 'derivative_uniformity'?
+Possible values: 'derivative_uniformity')");
 }
 
-TEST_F(ResolverDiagnosticControlTest, UnrecognizedRuleName_Attribute) {
+TEST_F(ResolverDiagnosticControlTest, UnrecognizedCoreRuleName_Attribute) {
     auto* attr = DiagnosticAttribute(builtin::DiagnosticSeverity::kError,
-                                     Ident(Source{{12, 34}}, "chromium_unreachable_cod"));
+                                     DiagnosticRuleName(Source{{12, 34}}, "derivative_uniform"));
     Func("foo", {}, ty.void_(), {}, utils::Vector{attr});
     EXPECT_TRUE(r()->Resolve()) << r()->error();
     EXPECT_EQ(r()->error(),
-              R"(12:34 warning: unrecognized diagnostic rule 'chromium_unreachable_cod'
-Did you mean 'chromium_unreachable_code'?
-Possible values: 'chromium_unreachable_code', 'derivative_uniformity')");
+              R"(12:34 warning: unrecognized diagnostic rule 'derivative_uniform'
+Did you mean 'derivative_uniformity'?
+Possible values: 'derivative_uniformity')");
+}
+
+TEST_F(ResolverDiagnosticControlTest, UnrecognizedChromiumRuleName_Directive) {
+    DiagnosticDirective(builtin::DiagnosticSeverity::kError,
+                        DiagnosticRuleName(Source{{12, 34}}, "chromium", "unreachable_cod"));
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(r()->error(),
+              R"(12:34 warning: unrecognized diagnostic rule 'chromium.unreachable_cod'
+Did you mean 'chromium.unreachable_code'?
+Possible values: 'chromium.unreachable_code')");
+}
+
+TEST_F(ResolverDiagnosticControlTest, UnrecognizedChromiumRuleName_Attribute) {
+    auto* attr =
+        DiagnosticAttribute(builtin::DiagnosticSeverity::kError,
+                            DiagnosticRuleName(Source{{12, 34}}, "chromium", "unreachable_cod"));
+    Func("foo", {}, ty.void_(), {}, utils::Vector{attr});
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(r()->error(),
+              R"(12:34 warning: unrecognized diagnostic rule 'chromium.unreachable_cod'
+Did you mean 'chromium.unreachable_code'?
+Possible values: 'chromium.unreachable_code')");
+}
+
+TEST_F(ResolverDiagnosticControlTest, UnrecognizedOtherRuleName_Directive) {
+    DiagnosticDirective(builtin::DiagnosticSeverity::kError,
+                        DiagnosticRuleName(Source{{12, 34}}, "unknown", "unreachable_cod"));
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(r()->error(), "");
+}
+
+TEST_F(ResolverDiagnosticControlTest, UnrecognizedOtherRuleName_Attribute) {
+    auto* attr =
+        DiagnosticAttribute(builtin::DiagnosticSeverity::kError,
+                            DiagnosticRuleName(Source{{12, 34}}, "unknown", "unreachable_cod"));
+    Func("foo", {}, ty.void_(), {}, utils::Vector{attr});
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+    EXPECT_EQ(r()->error(), "");
 }
 
 TEST_F(ResolverDiagnosticControlTest, Conflict_SameNameSameSeverity_Directive) {
     DiagnosticDirective(builtin::DiagnosticSeverity::kError,
-                        Ident(Source{{12, 34}}, "chromium_unreachable_code"));
+                        DiagnosticRuleName(Source{{12, 34}}, "chromium", "unreachable_code"));
     DiagnosticDirective(builtin::DiagnosticSeverity::kError,
-                        Ident(Source{{56, 78}}, "chromium_unreachable_code"));
+                        DiagnosticRuleName(Source{{56, 78}}, "chromium", "unreachable_code"));
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 }
 
 TEST_F(ResolverDiagnosticControlTest, Conflict_SameNameDifferentSeverity_Directive) {
     DiagnosticDirective(builtin::DiagnosticSeverity::kError,
-                        Ident(Source{{12, 34}}, "chromium_unreachable_code"));
+                        DiagnosticRuleName(Source{{12, 34}}, "chromium", "unreachable_code"));
     DiagnosticDirective(builtin::DiagnosticSeverity::kOff,
-                        Ident(Source{{56, 78}}, "chromium_unreachable_code"));
+                        DiagnosticRuleName(Source{{56, 78}}, "chromium", "unreachable_code"));
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
               R"(56:78 error: conflicting diagnostic directive
-12:34 note: severity of 'chromium_unreachable_code' set to 'off' here)");
+12:34 note: severity of 'chromium.unreachable_code' set to 'off' here)");
 }
 
 TEST_F(ResolverDiagnosticControlTest, Conflict_SameUnknownNameDifferentSeverity_Directive) {
     DiagnosticDirective(builtin::DiagnosticSeverity::kError,
-                        Ident(Source{{12, 34}}, "chromium_unreachable_codes"));
+                        DiagnosticRuleName(Source{{12, 34}}, "chromium", "unreachable_codes"));
     DiagnosticDirective(builtin::DiagnosticSeverity::kOff,
-                        Ident(Source{{56, 78}}, "chromium_unreachable_codes"));
+                        DiagnosticRuleName(Source{{56, 78}}, "chromium", "unreachable_codes"));
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 warning: unrecognized diagnostic rule 'chromium_unreachable_codes'
-Did you mean 'chromium_unreachable_code'?
-Possible values: 'chromium_unreachable_code', 'derivative_uniformity'
-56:78 warning: unrecognized diagnostic rule 'chromium_unreachable_codes'
-Did you mean 'chromium_unreachable_code'?
-Possible values: 'chromium_unreachable_code', 'derivative_uniformity'
+              R"(12:34 warning: unrecognized diagnostic rule 'chromium.unreachable_codes'
+Did you mean 'chromium.unreachable_code'?
+Possible values: 'chromium.unreachable_code'
+56:78 warning: unrecognized diagnostic rule 'chromium.unreachable_codes'
+Did you mean 'chromium.unreachable_code'?
+Possible values: 'chromium.unreachable_code'
 56:78 error: conflicting diagnostic directive
-12:34 note: severity of 'chromium_unreachable_codes' set to 'off' here)");
+12:34 note: severity of 'chromium.unreachable_codes' set to 'off' here)");
 }
 
 TEST_F(ResolverDiagnosticControlTest, Conflict_DifferentUnknownNameDifferentSeverity_Directive) {
-    DiagnosticDirective(builtin::DiagnosticSeverity::kError, "chromium_unreachable_codes");
-    DiagnosticDirective(builtin::DiagnosticSeverity::kOff, "chromium_unreachable_codex");
+    DiagnosticDirective(builtin::DiagnosticSeverity::kError, "chromium", "unreachable_codes");
+    DiagnosticDirective(builtin::DiagnosticSeverity::kOff, "chromium", "unreachable_codex");
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 }
 
 TEST_F(ResolverDiagnosticControlTest, Conflict_SameNameSameSeverity_Attribute) {
-    auto* attr1 = DiagnosticAttribute(builtin::DiagnosticSeverity::kError,
-                                      Ident(Source{{12, 34}}, "chromium_unreachable_code"));
-    auto* attr2 = DiagnosticAttribute(builtin::DiagnosticSeverity::kError,
-                                      Ident(Source{{56, 78}}, "chromium_unreachable_code"));
+    auto* attr1 =
+        DiagnosticAttribute(builtin::DiagnosticSeverity::kError,
+                            DiagnosticRuleName(Source{{12, 34}}, "chromium", "unreachable_code"));
+    auto* attr2 =
+        DiagnosticAttribute(builtin::DiagnosticSeverity::kError,
+                            DiagnosticRuleName(Source{{56, 78}}, "chromium", "unreachable_code"));
     Func("foo", {}, ty.void_(), {}, utils::Vector{attr1, attr2});
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 }
 
 TEST_F(ResolverDiagnosticControlTest, Conflict_SameNameDifferentSeverity_Attribute) {
-    auto* attr1 = DiagnosticAttribute(builtin::DiagnosticSeverity::kError,
-                                      Ident(Source{{12, 34}}, "chromium_unreachable_code"));
-    auto* attr2 = DiagnosticAttribute(builtin::DiagnosticSeverity::kOff,
-                                      Ident(Source{{56, 78}}, "chromium_unreachable_code"));
+    auto* attr1 =
+        DiagnosticAttribute(builtin::DiagnosticSeverity::kError,
+                            DiagnosticRuleName(Source{{12, 34}}, "chromium", "unreachable_code"));
+    auto* attr2 =
+        DiagnosticAttribute(builtin::DiagnosticSeverity::kOff,
+                            DiagnosticRuleName(Source{{56, 78}}, "chromium", "unreachable_code"));
     Func("foo", {}, ty.void_(), {}, utils::Vector{attr1, attr2});
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
               R"(56:78 error: conflicting diagnostic attribute
-12:34 note: severity of 'chromium_unreachable_code' set to 'off' here)");
+12:34 note: severity of 'chromium.unreachable_code' set to 'off' here)");
 }
 
 TEST_F(ResolverDiagnosticControlTest, Conflict_SameUnknownNameDifferentSeverity_Attribute) {
-    auto* attr1 = DiagnosticAttribute(builtin::DiagnosticSeverity::kError,
-                                      Ident(Source{{12, 34}}, "chromium_unreachable_codes"));
-    auto* attr2 = DiagnosticAttribute(builtin::DiagnosticSeverity::kOff,
-                                      Ident(Source{{56, 78}}, "chromium_unreachable_codes"));
+    auto* attr1 =
+        DiagnosticAttribute(builtin::DiagnosticSeverity::kError,
+                            DiagnosticRuleName(Source{{12, 34}}, "chromium", "unreachable_codes"));
+    auto* attr2 =
+        DiagnosticAttribute(builtin::DiagnosticSeverity::kOff,
+                            DiagnosticRuleName(Source{{56, 78}}, "chromium", "unreachable_codes"));
     Func("foo", {}, ty.void_(), {}, utils::Vector{attr1, attr2});
     EXPECT_FALSE(r()->Resolve());
     EXPECT_EQ(r()->error(),
-              R"(12:34 warning: unrecognized diagnostic rule 'chromium_unreachable_codes'
-Did you mean 'chromium_unreachable_code'?
-Possible values: 'chromium_unreachable_code', 'derivative_uniformity'
-56:78 warning: unrecognized diagnostic rule 'chromium_unreachable_codes'
-Did you mean 'chromium_unreachable_code'?
-Possible values: 'chromium_unreachable_code', 'derivative_uniformity'
+              R"(12:34 warning: unrecognized diagnostic rule 'chromium.unreachable_codes'
+Did you mean 'chromium.unreachable_code'?
+Possible values: 'chromium.unreachable_code'
+56:78 warning: unrecognized diagnostic rule 'chromium.unreachable_codes'
+Did you mean 'chromium.unreachable_code'?
+Possible values: 'chromium.unreachable_code'
 56:78 error: conflicting diagnostic attribute
-12:34 note: severity of 'chromium_unreachable_codes' set to 'off' here)");
+12:34 note: severity of 'chromium.unreachable_codes' set to 'off' here)");
 }
 
 TEST_F(ResolverDiagnosticControlTest, Conflict_DifferentUnknownNameDifferentSeverity_Attribute) {
     auto* attr1 =
-        DiagnosticAttribute(builtin::DiagnosticSeverity::kError, "chromium_unreachable_codes");
+        DiagnosticAttribute(builtin::DiagnosticSeverity::kError, "chromium", "unreachable_codes");
     auto* attr2 =
-        DiagnosticAttribute(builtin::DiagnosticSeverity::kOff, "chromium_unreachable_codex");
+        DiagnosticAttribute(builtin::DiagnosticSeverity::kOff, "chromium", "unreachable_codex");
     Func("foo", {}, ty.void_(), {}, utils::Vector{attr1, attr2});
     EXPECT_TRUE(r()->Resolve()) << r()->error();
 }
diff --git a/src/tint/resolver/resolver.cc b/src/tint/resolver/resolver.cc
index 1ec680d..19571a7 100644
--- a/src/tint/resolver/resolver.cc
+++ b/src/tint/resolver/resolver.cc
@@ -3466,15 +3466,33 @@
 
 bool Resolver::DiagnosticControl(const ast::DiagnosticControl& control) {
     Mark(control.rule_name);
+    Mark(control.rule_name->name);
+    auto name = control.rule_name->name->symbol.Name();
 
-    auto rule_name = control.rule_name->symbol.Name();
-    auto rule = builtin::ParseDiagnosticRule(rule_name);
-    if (rule != builtin::DiagnosticRule::kUndefined) {
+    if (control.rule_name->category) {
+        Mark(control.rule_name->category);
+        if (control.rule_name->category->symbol.Name() == "chromium") {
+            auto rule = builtin::ParseChromiumDiagnosticRule(name);
+            if (rule != builtin::ChromiumDiagnosticRule::kUndefined) {
+                validator_.DiagnosticFilters().Set(rule, control.severity);
+            } else {
+                utils::StringStream ss;
+                ss << "unrecognized diagnostic rule 'chromium." << name << "'\n";
+                utils::SuggestAlternatives(name, builtin::kChromiumDiagnosticRuleStrings, ss,
+                                           "chromium.");
+                AddWarning(ss.str(), control.rule_name->source);
+            }
+        }
+        return true;
+    }
+
+    auto rule = builtin::ParseCoreDiagnosticRule(name);
+    if (rule != builtin::CoreDiagnosticRule::kUndefined) {
         validator_.DiagnosticFilters().Set(rule, control.severity);
     } else {
         utils::StringStream ss;
-        ss << "unrecognized diagnostic rule '" << rule_name << "'\n";
-        utils::SuggestAlternatives(rule_name, builtin::kDiagnosticRuleStrings, ss);
+        ss << "unrecognized diagnostic rule '" << name << "'\n";
+        utils::SuggestAlternatives(name, builtin::kCoreDiagnosticRuleStrings, ss);
         AddWarning(ss.str(), control.rule_name->source);
     }
     return true;
diff --git a/src/tint/resolver/uniformity.cc b/src/tint/resolver/uniformity.cc
index fc84826..45c919e 100644
--- a/src/tint/resolver/uniformity.cc
+++ b/src/tint/resolver/uniformity.cc
@@ -1563,7 +1563,7 @@
                            builtin->Type() == builtin::Function::kTextureSampleCompare) {
                     // Get the severity of derivative uniformity violations in this context.
                     auto severity = sem_.DiagnosticSeverity(
-                        call, builtin::DiagnosticRule::kDerivativeUniformity);
+                        call, builtin::CoreDiagnosticRule::kDerivativeUniformity);
                     if (severity != builtin::DiagnosticSeverity::kOff) {
                         callsite_tag = {CallSiteTag::CallSiteRequiredToBeUniform, severity};
                     }
diff --git a/src/tint/resolver/validator.cc b/src/tint/resolver/validator.cc
index f72000b..97eae88 100644
--- a/src/tint/resolver/validator.cc
+++ b/src/tint/resolver/validator.cc
@@ -159,9 +159,9 @@
       atomic_composite_info_(atomic_composite_info),
       valid_type_storage_layouts_(valid_type_storage_layouts) {
     // Set default severities for filterable diagnostic rules.
-    diagnostic_filters_.Set(builtin::DiagnosticRule::kDerivativeUniformity,
+    diagnostic_filters_.Set(builtin::CoreDiagnosticRule::kDerivativeUniformity,
                             builtin::DiagnosticSeverity::kError);
-    diagnostic_filters_.Set(builtin::DiagnosticRule::kChromiumUnreachableCode,
+    diagnostic_filters_.Set(builtin::ChromiumDiagnosticRule::kUnreachableCode,
                             builtin::DiagnosticSeverity::kWarning);
 }
 
@@ -1417,7 +1417,7 @@
 bool Validator::Statements(utils::VectorRef<const ast::Statement*> stmts) const {
     for (auto* stmt : stmts) {
         if (!sem_.Get(stmt)->IsReachable()) {
-            if (!AddDiagnostic(builtin::DiagnosticRule::kChromiumUnreachableCode,
+            if (!AddDiagnostic(builtin::ChromiumDiagnosticRule::kUnreachableCode,
                                "code is unreachable", stmt->source)) {
                 return false;
             }
@@ -2530,9 +2530,12 @@
                                    const char* use) const {
     // Make sure that no two diagnostic controls conflict.
     // They conflict if the rule name is the same and the severity is different.
-    utils::Hashmap<Symbol, const ast::DiagnosticControl*, 8> diagnostics;
+    utils::Hashmap<std::pair<Symbol, Symbol>, const ast::DiagnosticControl*, 8> diagnostics;
     for (auto* dc : controls) {
-        auto diag_added = diagnostics.Add(dc->rule_name->symbol, dc);
+        auto category = dc->rule_name->category ? dc->rule_name->category->symbol : Symbol();
+        auto name = dc->rule_name->name->symbol;
+
+        auto diag_added = diagnostics.Add(std::make_pair(category, name), dc);
         if (!diag_added && (*diag_added.value)->severity != dc->severity) {
             {
                 utils::StringStream ss;
@@ -2541,8 +2544,8 @@
             }
             {
                 utils::StringStream ss;
-                ss << "severity of '" << dc->rule_name->symbol.Name() << "' set to '"
-                   << dc->severity << "' here";
+                ss << "severity of '" << dc->rule_name->String() << "' set to '" << dc->severity
+                   << "' here";
                 AddNote(ss.str(), (*diag_added.value)->rule_name->source);
             }
             return false;
diff --git a/src/tint/sem/diagnostic_severity_test.cc b/src/tint/sem/diagnostic_severity_test.cc
index 9d36ea2..a378add 100644
--- a/src/tint/sem/diagnostic_severity_test.cc
+++ b/src/tint/sem/diagnostic_severity_test.cc
@@ -23,27 +23,27 @@
 
 class DiagnosticSeverityTest : public TestHelper {
   protected:
-    /// Create a program with two functions, setting the severity for "chromium_unreachable_code"
+    /// Create a program with two functions, setting the severity for "chromium.unreachable_code"
     /// using an attribute. Test that we correctly track the severity of the filter for the
     /// functions and the statements with them.
-    /// @param global_severity the global severity of the "chromium_unreachable_code" filter
+    /// @param global_severity the global severity of the "chromium.unreachable_code" filter
     void Run(builtin::DiagnosticSeverity global_severity) {
-        // @diagnostic(off, chromium_unreachable_code)
+        // @diagnostic(off, chromium.unreachable_code)
         // fn foo() {
-        //   @diagnostic(info, chromium_unreachable_code) {
-        //     @diagnostic(error, chromium_unreachable_code)
-        //     if (true) @diagnostic(warning, chromium_unreachable_code) {
+        //   @diagnostic(info, chromium.unreachable_code) {
+        //     @diagnostic(error, chromium.unreachable_code)
+        //     if (true) @diagnostic(warning, chromium.unreachable_code) {
         //       return;
         //     } else if (false) {
         //       return;
-        //     } else @diagnostic(info, chromium_unreachable_code) {
+        //     } else @diagnostic(info, chromium.unreachable_code) {
         //       return;
         //     }
         //     return;
         //
-        //     @diagnostic(error, chromium_unreachable_code)
-        //     switch (42) @diagnostic(off, chromium_unreachable_code) {
-        //       case 0 @diagnostic(warning, chromium_unreachable_code) {
+        //     @diagnostic(error, chromium.unreachable_code)
+        //     switch (42) @diagnostic(off, chromium.unreachable_code) {
+        //       case 0 @diagnostic(warning, chromium.unreachable_code) {
         //         return;
         //       }
         //       default {
@@ -51,21 +51,21 @@
         //       }
         //     }
         //
-        //     @diagnostic(error, chromium_unreachable_code)
-        //     for (var i = 0; false; i++) @diagnostic(warning, chromium_unreachable_code) {
+        //     @diagnostic(error, chromium.unreachable_code)
+        //     for (var i = 0; false; i++) @diagnostic(warning, chromium.unreachable_code) {
         //       return;
         //     }
         //
-        //     @diagnostic(warning, chromium_unreachable_code)
-        //     loop @diagnostic(off, chromium_unreachable_code) {
+        //     @diagnostic(warning, chromium.unreachable_code)
+        //     loop @diagnostic(off, chromium.unreachable_code) {
         //       return;
-        //       continuing @diagnostic(info, chromium_unreachable_code) {
+        //       continuing @diagnostic(info, chromium.unreachable_code) {
         //         break if true;
         //       }
         //     }
         //
-        //     @diagnostic(error, chromium_unreachable_code)
-        //     while (false) @diagnostic(warning, chromium_unreachable_code) {
+        //     @diagnostic(error, chromium.unreachable_code)
+        //     while (false) @diagnostic(warning, chromium.unreachable_code) {
         //       return;
         //     }
         //   }
@@ -74,7 +74,7 @@
         // fn bar() {
         //   return;
         // }
-        auto rule = builtin::DiagnosticRule::kChromiumUnreachableCode;
+        auto rule = builtin::ChromiumDiagnosticRule::kUnreachableCode;
         auto func_severity = builtin::DiagnosticSeverity::kOff;
         auto block_severity = builtin::DiagnosticSeverity::kInfo;
         auto if_severity = builtin::DiagnosticSeverity::kError;
@@ -91,7 +91,7 @@
         auto while_severity = builtin::DiagnosticSeverity::kError;
         auto while_body_severity = builtin::DiagnosticSeverity::kWarning;
         auto attr = [&](auto severity) {
-            return utils::Vector{DiagnosticAttribute(severity, "chromium_unreachable_code")};
+            return utils::Vector{DiagnosticAttribute(severity, "chromium", "unreachable_code")};
         };
 
         auto* return_foo_if = Return();
@@ -123,7 +123,7 @@
                          attr(while_severity));
         auto* block_1 =
             Block(utils::Vector{if_foo, return_foo_block, swtch, fl, l, wl}, attr(block_severity));
-        auto* func_attr = DiagnosticAttribute(func_severity, "chromium_unreachable_code");
+        auto* func_attr = DiagnosticAttribute(func_severity, "chromium", "unreachable_code");
         auto* foo = Func("foo", {}, ty.void_(), utils::Vector{block_1}, utils::Vector{func_attr});
 
         auto* return_bar = Return();
@@ -173,7 +173,7 @@
 };
 
 TEST_F(DiagnosticSeverityTest, WithDirective) {
-    DiagnosticDirective(builtin::DiagnosticSeverity::kError, "chromium_unreachable_code");
+    DiagnosticDirective(builtin::DiagnosticSeverity::kError, "chromium", "unreachable_code");
     Run(builtin::DiagnosticSeverity::kError);
 }
 
diff --git a/src/tint/transform/renamer.cc b/src/tint/transform/renamer.cc
index bd74cf6..14b8c12 100644
--- a/src/tint/transform/renamer.cc
+++ b/src/tint/transform/renamer.cc
@@ -1289,10 +1289,16 @@
                 }
             },
             [&](const ast::DiagnosticAttribute* diagnostic) {
-                preserved_identifiers.Add(diagnostic->control.rule_name);
+                if (auto* category = diagnostic->control.rule_name->category) {
+                    preserved_identifiers.Add(category);
+                }
+                preserved_identifiers.Add(diagnostic->control.rule_name->name);
             },
             [&](const ast::DiagnosticDirective* diagnostic) {
-                preserved_identifiers.Add(diagnostic->control.rule_name);
+                if (auto* category = diagnostic->control.rule_name->category) {
+                    preserved_identifiers.Add(category);
+                }
+                preserved_identifiers.Add(diagnostic->control.rule_name->name);
             },
             [&](const ast::IdentifierExpression* expr) {
                 Switch(
diff --git a/src/tint/transform/renamer_test.cc b/src/tint/transform/renamer_test.cc
index fc34251..cd321fb 100644
--- a/src/tint/transform/renamer_test.cc
+++ b/src/tint/transform/renamer_test.cc
@@ -192,9 +192,9 @@
     EXPECT_THAT(data->remappings, ContainerEq(expected_remappings));
 }
 
-TEST_F(RenamerTest, PreserveDiagnosticControls) {
+TEST_F(RenamerTest, PreserveCoreDiagnosticRuleName) {
     auto* src = R"(
-diagnostic(off, unreachable_code);
+diagnostic(off, chromium.unreachable_code);
 
 @diagnostic(off, derivative_uniformity)
 @fragment
@@ -208,7 +208,7 @@
 )";
 
     auto* expect = R"(
-diagnostic(off, unreachable_code);
+diagnostic(off, chromium.unreachable_code);
 
 @diagnostic(off, derivative_uniformity) @fragment
 fn tint_symbol(@location(0) tint_symbol_1 : f32) -> @location(0) f32 {
diff --git a/src/tint/utils/hash.h b/src/tint/utils/hash.h
index 8c9dd25..a63a303 100644
--- a/src/tint/utils/hash.h
+++ b/src/tint/utils/hash.h
@@ -134,7 +134,15 @@
     }
 };
 
-/// Hasher specialization for std::tuple
+/// Hasher specialization for std::pair
+template <typename A, typename B>
+struct Hasher<std::pair<A, B>> {
+    /// @param tuple the tuple to hash
+    /// @returns a hash of the tuple
+    size_t operator()(const std::pair<A, B>& tuple) const { return std::apply(Hash<A, B>, tuple); }
+};
+
+/// Hasher specialization for std::variant
 template <typename... TYPES>
 struct Hasher<std::variant<TYPES...>> {
     /// @param variant the variant to hash
diff --git a/src/tint/utils/string.cc b/src/tint/utils/string.cc
index 2f28a3e..67eaf2b 100644
--- a/src/tint/utils/string.cc
+++ b/src/tint/utils/string.cc
@@ -50,7 +50,8 @@
 
 void SuggestAlternatives(std::string_view got,
                          Slice<char const* const> strings,
-                         utils::StringStream& ss) {
+                         utils::StringStream& ss,
+                         std::string_view prefix /* = "" */) {
     // 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;
@@ -66,7 +67,7 @@
             }
         }
         if (candidate) {
-            ss << "Did you mean '" << candidate << "'?\n";
+            ss << "Did you mean '" << prefix << candidate << "'?\n";
         }
     }
 
@@ -76,7 +77,7 @@
         if (str != strings[0]) {
             ss << ", ";
         }
-        ss << "'" << str << "'";
+        ss << "'" << prefix << str << "'";
     }
 }
 
diff --git a/src/tint/utils/string.h b/src/tint/utils/string.h
index 5c7255d..897d1a1 100644
--- a/src/tint/utils/string.h
+++ b/src/tint/utils/string.h
@@ -72,9 +72,11 @@
 /// @param got the unrecognized string
 /// @param strings the list of possible values
 /// @param ss the stream to write the suggest and list of possible values to
+/// @param prefix the prefix to apply to the strings when printing (optional)
 void SuggestAlternatives(std::string_view got,
                          Slice<char const* const> strings,
-                         utils::StringStream& ss);
+                         utils::StringStream& ss,
+                         std::string_view prefix = "");
 
 }  // namespace tint::utils
 
diff --git a/src/tint/writer/syntax_tree/generator_impl.cc b/src/tint/writer/syntax_tree/generator_impl.cc
index be30289..08ca8c5 100644
--- a/src/tint/writer/syntax_tree/generator_impl.cc
+++ b/src/tint/writer/syntax_tree/generator_impl.cc
@@ -66,7 +66,7 @@
 
 void GeneratorImpl::EmitDiagnosticControl(const ast::DiagnosticControl& diagnostic) {
     line() << "DiagnosticControl [severity: " << diagnostic.severity
-           << ", rule: " << diagnostic.rule_name->symbol.Name() << "]";
+           << ", rule: " << diagnostic.rule_name->String() << "]";
 }
 
 void GeneratorImpl::EmitEnable(const ast::Enable* enable) {
diff --git a/src/tint/writer/wgsl/generator_impl.cc b/src/tint/writer/wgsl/generator_impl.cc
index 2a2f0e7..2703e8d 100644
--- a/src/tint/writer/wgsl/generator_impl.cc
+++ b/src/tint/writer/wgsl/generator_impl.cc
@@ -82,8 +82,7 @@
 
 void GeneratorImpl::EmitDiagnosticControl(utils::StringStream& out,
                                           const ast::DiagnosticControl& diagnostic) {
-    out << "diagnostic(" << diagnostic.severity << ", " << diagnostic.rule_name->symbol.Name()
-        << ")";
+    out << "diagnostic(" << diagnostic.severity << ", " << diagnostic.rule_name->String() << ")";
 }
 
 void GeneratorImpl::EmitEnable(const ast::Enable* enable) {
diff --git a/src/tint/writer/wgsl/generator_impl_diagnostic_test.cc b/src/tint/writer/wgsl/generator_impl_diagnostic_test.cc
index 2a2f4b6..2aeab09 100644
--- a/src/tint/writer/wgsl/generator_impl_diagnostic_test.cc
+++ b/src/tint/writer/wgsl/generator_impl_diagnostic_test.cc
@@ -22,25 +22,25 @@
 using WgslGeneratorImplTest = TestHelper;
 
 TEST_F(WgslGeneratorImplTest, Emit_DiagnosticDirective) {
-    DiagnosticDirective(builtin::DiagnosticSeverity::kError, "chromium_unreachable_code");
+    DiagnosticDirective(builtin::DiagnosticSeverity::kError, "chromium", "unreachable_code");
 
     GeneratorImpl& gen = Build();
     gen.Generate();
     EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
-    EXPECT_EQ(gen.result(), R"(diagnostic(error, chromium_unreachable_code);
+    EXPECT_EQ(gen.result(), R"(diagnostic(error, chromium.unreachable_code);
 
 )");
 }
 
 TEST_F(WgslGeneratorImplTest, Emit_DiagnosticAttribute) {
     auto* attr =
-        DiagnosticAttribute(builtin::DiagnosticSeverity::kError, "chromium_unreachable_code");
+        DiagnosticAttribute(builtin::DiagnosticSeverity::kError, "chromium", "unreachable_code");
     Func("foo", {}, ty.void_(), {}, utils::Vector{attr});
 
     GeneratorImpl& gen = Build();
     gen.Generate();
     EXPECT_THAT(gen.Diagnostics(), testing::IsEmpty());
-    EXPECT_EQ(gen.result(), R"(@diagnostic(error, chromium_unreachable_code)
+    EXPECT_EQ(gen.result(), R"(@diagnostic(error, chromium.unreachable_code)
 fn foo() {
 }
 )");