Skip to content

[clang][diagnostics] Stable IDs for Clang diagnostics#168153

Merged
dbartol merged 8 commits into
llvm:mainfrom
dbartol:sarif/stable-ids
Mar 6, 2026
Merged

[clang][diagnostics] Stable IDs for Clang diagnostics#168153
dbartol merged 8 commits into
llvm:mainfrom
dbartol:sarif/stable-ids

Conversation

@dbartol
Copy link
Copy Markdown
Contributor

@dbartol dbartol commented Nov 14, 2025

Part of the implementation of [RFC] Emitting Auditable SARIF Logs from Clang

SARIF diagnostics require that each rule have a stable id property to identify that rule across runs, even when the compiler or analysis tool has changed. We were previously setting the id property to the numeric value of the enum value for that diagnostic within the Clang implementation; this value changes whenever an unrelated diagnostic is inserted or removed earlier in the list.

This change sets the id property to the text of that same enum value. This value would only change if someone renames the enum value for that diagnostic, which should happen much less frequently than renumbering.

For now, we will just assume that renaming happens infrequently enough that existing consumers of SARIF will not notice. In the future, we could take advantage of SARIF's support for deprecatedIds, which let a rule specify the IDs by which it was previously known. This would let us rename, split, or combine diagnostics while still being able to correlate the new diagnostic IDs with older SARIF logs and/or suppressions.

Nothing in this change affects how warnings are configured on the command line or in #pragma clang diagnostic. Those still use warning groups, not the stable IDs.

Potential discussion topics

From @AaronBallman on the RFC:

We believe some open questions remain (things like whether a unique ID is on the per-diagnostic level or on the diagnostic group level, whether the ID is explicitly spelled in the .td file or implicitly generated, whether we document the IDs, etc), but we think those questions are best decided in PR discussions with interested parties rather than an RFC.

As a starting point, this PR proposes the following answers to those open questions:

  • whether a unique ID is on the per-diagnostic level or on the diagnostic group level - per-diagnostic level. For my justification, see this portion of the RFC discussion.
  • whether the ID is explicitly spelled in the .td file or implicitly generated - Implicitly generated, but I'd be happy to have a way to explicitly specify it. I just think that the in-code identifier is a reasonable default, and manually reviewing the IDs of thousands of existing diagnostics would add little benefit.
  • whether we document the IDs - For now, the IDs are only exposed to the user (and other tools) in the SARIF file, so I don't think we need to document these. We could certainly add this information to the output of diagtool in the future if users find it relevant.

SARIF diagnostics require that each rule have a stable `id` property to identify that rule across runs, even when the compiler or analysis tool has changed. We were previously setting the `id` property to the numeric value of the enum value for that diagnostic within the Clang implementation; this value changes whenever an unrelated diagnostic is inserted or removed earlier in the list.

This change sets the `id` property to the _text_ of that same enum value. This value would only change if someone renames the enum value for that diagnostic, which should happen much less frequently than renumbering.

For now, we will just assume that renaming happens infrequently enough that existing consumers of SARIF will not notice. In the future, we could take advantage of SARIF's support for `deprecatedIds`, which let a rule specify the IDs by which it was previously known. This would let us rename, split, or combine diagnostics while still being able to correlate the new diagnostic IDs with older SARIF logs and/or suppressions.

Nothing in this change affects how warnings are configured on the command line or in `#pragma clang diagnostic`. Those still use warning groups, not the stable IDs.
@dbartol dbartol added the clang:diagnostics New/improved warning or error message in Clang, but not in clang-tidy or static analyzer label Nov 14, 2025
@llvmbot llvmbot added clang Clang issues not falling into any other category clang:frontend Language frontend issues, e.g. anything involving "Sema" labels Nov 14, 2025
@llvmbot
Copy link
Copy Markdown
Member

llvmbot commented Nov 14, 2025

@llvm/pr-subscribers-clangd
@llvm/pr-subscribers-clang-static-analyzer-1

@llvm/pr-subscribers-clang

Author: Dave Bartolomeo (dbartol)

Changes

SARIF diagnostics require that each rule have a stable id property to identify that rule across runs, even when the compiler or analysis tool has changed. We were previously setting the id property to the numeric value of the enum value for that diagnostic within the Clang implementation; this value changes whenever an unrelated diagnostic is inserted or removed earlier in the list.

This change sets the id property to the text of that same enum value. This value would only change if someone renames the enum value for that diagnostic, which should happen much less frequently than renumbering.

For now, we will just assume that renaming happens infrequently enough that existing consumers of SARIF will not notice. In the future, we could take advantage of SARIF's support for deprecatedIds, which let a rule specify the IDs by which it was previously known. This would let us rename, split, or combine diagnostics while still being able to correlate the new diagnostic IDs with older SARIF logs and/or suppressions.

Nothing in this change affects how warnings are configured on the command line or in #pragma clang diagnostic. Those still use warning groups, not the stable IDs.


Full diff: https://github.com/llvm/llvm-project/pull/168153.diff

4 Files Affected:

  • (modified) clang/include/clang/Basic/DiagnosticIDs.h (+3)
  • (modified) clang/lib/Basic/DiagnosticIDs.cpp (+44-1)
  • (modified) clang/lib/Frontend/SARIFDiagnostic.cpp (+3-1)
  • (modified) clang/test/Frontend/sarif-diagnostics.cpp (+18-18)
diff --git a/clang/include/clang/Basic/DiagnosticIDs.h b/clang/include/clang/Basic/DiagnosticIDs.h
index 06446cf580389..9fc49325205a2 100644
--- a/clang/include/clang/Basic/DiagnosticIDs.h
+++ b/clang/include/clang/Basic/DiagnosticIDs.h
@@ -332,6 +332,9 @@ class DiagnosticIDs : public RefCountedBase<DiagnosticIDs> {
   /// Given a diagnostic ID, return a description of the issue.
   StringRef getDescription(unsigned DiagID) const;
 
+  /// Given a diagnostic ID, return the stable ID of the diagnostic.
+  std::string getStableID(unsigned DiagID) const;
+
   /// Return true if the unmapped diagnostic levelof the specified
   /// diagnostic ID is a Warning or Extension.
   ///
diff --git a/clang/lib/Basic/DiagnosticIDs.cpp b/clang/lib/Basic/DiagnosticIDs.cpp
index a1d9d0f34d20d..e3903a3edadfd 100644
--- a/clang/lib/Basic/DiagnosticIDs.cpp
+++ b/clang/lib/Basic/DiagnosticIDs.cpp
@@ -51,6 +51,22 @@ const StaticDiagInfoDescriptionStringTable StaticDiagInfoDescriptions = {
 #undef DIAG
 };
 
+struct StaticDiagInfoStableIDStringTable {
+#define DIAG(ENUM, CLASS, DEFAULT_SEVERITY, DESC, GROUP, SFINAE, NOWERROR,     \
+             SHOWINSYSHEADER, SHOWINSYSMACRO, DEFERRABLE, CATEGORY)            \
+  char ENUM##_id[sizeof(#ENUM)];
+#include "clang/Basic/AllDiagnosticKinds.inc"
+#undef DIAG
+};
+
+const StaticDiagInfoStableIDStringTable StaticDiagInfoStableIDs = {
+#define DIAG(ENUM, CLASS, DEFAULT_SEVERITY, DESC, GROUP, SFINAE, NOWERROR,     \
+             SHOWINSYSHEADER, SHOWINSYSMACRO, DEFERRABLE, CATEGORY)            \
+  #ENUM,
+#include "clang/Basic/AllDiagnosticKinds.inc"
+#undef DIAG
+};
+
 extern const StaticDiagInfoRec StaticDiagInfo[];
 
 // Stored separately from StaticDiagInfoRec to pack better.  Otherwise,
@@ -63,6 +79,14 @@ const uint32_t StaticDiagInfoDescriptionOffsets[] = {
 #undef DIAG
 };
 
+const uint32_t StaticDiagInfoStableIDOffsets[] = {
+#define DIAG(ENUM, CLASS, DEFAULT_SEVERITY, DESC, GROUP, SFINAE, NOWERROR,     \
+             SHOWINSYSHEADER, SHOWINSYSMACRO, DEFERRABLE, CATEGORY)            \
+  offsetof(StaticDiagInfoStableIDStringTable, ENUM##_id),
+#include "clang/Basic/AllDiagnosticKinds.inc"
+#undef DIAG
+};
+
 enum DiagnosticClass {
   CLASS_NOTE = DiagnosticIDs::CLASS_NOTE,
   CLASS_REMARK = DiagnosticIDs::CLASS_REMARK,
@@ -95,6 +119,7 @@ struct StaticDiagInfoRec {
   uint16_t Deferrable : 1;
 
   uint16_t DescriptionLen;
+  uint16_t StableIDLen;
 
   unsigned getOptionGroupIndex() const {
     return OptionGroupIndex;
@@ -107,6 +132,14 @@ struct StaticDiagInfoRec {
     return StringRef(&Table[StringOffset], DescriptionLen);
   }
 
+  StringRef getStableID() const {
+    size_t MyIndex = this - &StaticDiagInfo[0];
+    uint32_t StringOffset = StaticDiagInfoStableIDOffsets[MyIndex];
+    const char *Table =
+        reinterpret_cast<const char *>(&StaticDiagInfoStableIDs);
+    return StringRef(&Table[StringOffset], StableIDLen);
+  }
+
   diag::Flavor getFlavor() const {
     return Class == CLASS_REMARK ? diag::Flavor::Remark
                                  : diag::Flavor::WarningOrError;
@@ -159,7 +192,8 @@ const StaticDiagInfoRec StaticDiagInfo[] = {
       SHOWINSYSMACRO,                                                          \
       GROUP,                                                                   \
 	    DEFERRABLE,                                                              \
-      STR_SIZE(DESC, uint16_t)},
+      STR_SIZE(DESC, uint16_t),                                               \
+      STR_SIZE(#ENUM, uint16_t)},
 #include "clang/Basic/DiagnosticCommonKinds.inc"
 #include "clang/Basic/DiagnosticDriverKinds.inc"
 #include "clang/Basic/DiagnosticFrontendKinds.inc"
@@ -434,6 +468,15 @@ StringRef DiagnosticIDs::getDescription(unsigned DiagID) const {
   return CustomDiagInfo->getDescription(DiagID).GetDescription();
 }
 
+/// getIDString - Given a diagnostic ID, return the stable ID of the diagnostic.
+std::string DiagnosticIDs::getStableID(unsigned DiagID) const {
+  if (const StaticDiagInfoRec *Info = GetDiagInfo(DiagID))
+    return Info->getStableID().str();
+  assert(CustomDiagInfo && "Invalid CustomDiagInfo");
+  // TODO: Stable IDs for custom diagnostics?
+  return std::to_string(DiagID);
+}
+
 static DiagnosticIDs::Level toLevel(diag::Severity SV) {
   switch (SV) {
   case diag::Severity::Ignored:
diff --git a/clang/lib/Frontend/SARIFDiagnostic.cpp b/clang/lib/Frontend/SARIFDiagnostic.cpp
index ac27d7480de3e..ac72a7af05429 100644
--- a/clang/lib/Frontend/SARIFDiagnostic.cpp
+++ b/clang/lib/Frontend/SARIFDiagnostic.cpp
@@ -46,7 +46,9 @@ void SARIFDiagnostic::emitDiagnosticMessage(
   if (!Diag)
     return;
 
-  SarifRule Rule = SarifRule::create().setRuleId(std::to_string(Diag->getID()));
+  std::string StableID =
+      Diag->getDiags()->getDiagnosticIDs()->getStableID(Diag->getID());
+  SarifRule Rule = SarifRule::create().setRuleId(StableID);
 
   Rule = addDiagnosticLevelToRule(Rule, Level);
 
diff --git a/clang/test/Frontend/sarif-diagnostics.cpp b/clang/test/Frontend/sarif-diagnostics.cpp
index 767c5802ca13d..3f7adb80c67fd 100644
--- a/clang/test/Frontend/sarif-diagnostics.cpp
+++ b/clang/test/Frontend/sarif-diagnostics.cpp
@@ -34,35 +34,35 @@ void f1(t1 x, t1 y) {
 // Omit filepath to llvm project directory
 // CHECK: test/Frontend/sarif-diagnostics.cpp"},"mimeType":"text/plain","roles":["resultFile"]}],"columnKind":"unicodeCodePoints","results":
 // CHECK: [{"level":"error","locations":[{"physicalLocation":{"artifactLocation":{"index":0,"uri":"file://
-// CHECK: {"endColumn":1,"startColumn":1,"startLine":12}}}],"message":{"text":"'main' must return 'int'"},"ruleId":"{{[0-9]+}}","ruleIndex":0},
+// CHECK: {"endColumn":1,"startColumn":1,"startLine":12}}}],"message":{"text":"'main' must return 'int'"},"ruleId":"{{[A-Za-z0-9_]+}}","ruleIndex":0},
 // CHECK: {"level":"error","locations":[{"physicalLocation":{"artifactLocation":{"index":0,"uri":"file://
 // CHECK: {"endColumn":11,"startColumn":11,"startLine":13}}}],"message":{"text":"use of undeclared identifier
-// CHECK: 'hello'"},"ruleId":"{{[0-9]+}}","ruleIndex":1},{"level":"error","locations":[{"physicalLocation":{"artifactLocation":
+// CHECK: 'hello'"},"ruleId":"{{[A-Za-z0-9_]+}}","ruleIndex":1},{"level":"error","locations":[{"physicalLocation":{"artifactLocation":
 // CHECK: {"index":0,"uri":"file://{{.+}}"},"region":{"endColumn":17,"startColumn":17,"startLine":15}}}],"message":{"text":"invalid digit 'a' in decimal
-// CHECK: constant"},"ruleId":"{{[0-9]+}}","ruleIndex":2},{"level":"warning","locations":[{"physicalLocation":{"artifactLocation":
+// CHECK: constant"},"ruleId":"{{[A-Za-z0-9_]+}}","ruleIndex":2},{"level":"warning","locations":[{"physicalLocation":{"artifactLocation":
 // CHECK: {"index":0,"uri":"file://{{.+}}"},"region":{"endColumn":5,"startColumn":5,"startLine":19}}}],"message":{"text":"misleading indentation; statement is not part
-// CHECK: of the previous 'if'"},"ruleId":"{{[0-9]+}}","ruleIndex":3},{"level":"note","locations":[{"physicalLocation":{"artifactLocation":
+// CHECK: of the previous 'if'"},"ruleId":"{{[A-Za-z0-9_]+}}","ruleIndex":3},{"level":"note","locations":[{"physicalLocation":{"artifactLocation":
 // CHECK: {"index":0,"uri":"file://{{.+}}"},"region":{"endColumn":3,"startColumn":3,"startLine":17}}}],"message":{"text":"previous statement is
-// CHECK: here"},"ruleId":"{{[0-9]+}}","ruleIndex":4},{"level":"warning","locations":[{"physicalLocation":{"artifactLocation":
+// CHECK: here"},"ruleId":"{{[A-Za-z0-9_]+}}","ruleIndex":4},{"level":"warning","locations":[{"physicalLocation":{"artifactLocation":
 // CHECK: {"index":0,"uri":"file://{{.+}}"},"region":{"endColumn":10,"startColumn":10,"startLine":18}}}],"message":{"text":"unused variable
-// CHECK: 'Yes'"},"ruleId":"{{[0-9]+}}","ruleIndex":5},{"level":"error","locations":[{"physicalLocation":{"artifactLocation":{"index":0,"uri":"file://
+// CHECK: 'Yes'"},"ruleId":"{{[A-Za-z0-9_]+}}","ruleIndex":5},{"level":"error","locations":[{"physicalLocation":{"artifactLocation":{"index":0,"uri":"file://
 // CHECK: {"endColumn":12,"startColumn":12,"startLine":21}}}],"message":{"text":"use of undeclared identifier
-// CHECK: 'hi'"},"ruleId":"{{[0-9]+}}","ruleIndex":6},{"level":"error","locations":[{"physicalLocation":{"artifactLocation":{"index":0,"uri":"file://
+// CHECK: 'hi'"},"ruleId":"{{[A-Za-z0-9_]+}}","ruleIndex":6},{"level":"error","locations":[{"physicalLocation":{"artifactLocation":{"index":0,"uri":"file://
 // CHECK: {"endColumn":1,"startColumn":1,"startLine":23}}}],"message":{"text":"extraneous closing brace
-// CHECK: ('}')"},"ruleId":"{{[0-9]+}}","ruleIndex":7},{"level":"error","locations":[{"physicalLocation":{"artifactLocation":{"index":0,"uri":"file://
+// CHECK: ('}')"},"ruleId":"{{[A-Za-z0-9_]+}}","ruleIndex":7},{"level":"error","locations":[{"physicalLocation":{"artifactLocation":{"index":0,"uri":"file://
 // CHECK: {"endColumn":6,"endLine":27,"startColumn":5,"startLine":27}}},{"physicalLocation":{"artifactLocation":{"index":0,"uri":"file://
 // CHECK: {"endColumn":10,"endLine":27,"startColumn":9,"startLine":27}}},{"physicalLocation":{"artifactLocation":{"index":0,"uri":"file://
 // CHECK: {"endColumn":7,"startColumn":7,"startLine":27}}}],"message":{"text":"invalid operands to binary expression ('t1' and
-// CHECK: 't1')"},"ruleId":"{{[0-9]+}}","ruleIndex":8}],"tool":{"driver":{"fullName":"","informationUri":"https://clang.llvm.org/docs/
+// CHECK: 't1')"},"ruleId":"{{[A-Za-z0-9_]+}}","ruleIndex":8}],"tool":{"driver":{"fullName":"","informationUri":"https://clang.llvm.org/docs/
 // CHECK: UsersManual.html","language":"en-US","name":"clang","rules":[{"defaultConfiguration":
-// CHECK: {"enabled":true,"level":"error","rank":50},"fullDescription":{"text":""},"id":"{{[0-9]+}}","name":""},{"defaultConfiguration":
-// CHECK: {"enabled":true,"level":"error","rank":50},"fullDescription":{"text":""},"id":"{{[0-9]+}}","name":""},{"defaultConfiguration":
-// CHECK: {"enabled":true,"level":"error","rank":50},"fullDescription":{"text":""},"id":"{{[0-9]+}}","name":""},{"defaultConfiguration":
-// CHECK: {"enabled":true,"level":"warning","rank":-1},"fullDescription":{"text":""},"id":"{{[0-9]+}}","name":""},{"defaultConfiguration":
-// CHECK: {"enabled":true,"level":"note","rank":-1},"fullDescription":{"text":""},"id":"{{[0-9]+}}","name":""},{"defaultConfiguration":
-// CHECK: {"enabled":true,"level":"warning","rank":-1},"fullDescription":{"text":""},"id":"{{[0-9]+}}","name":""},{"defaultConfiguration":
-// CHECK: {"enabled":true,"level":"error","rank":50},"fullDescription":{"text":""},"id":"{{[0-9]+}}","name":""},{"defaultConfiguration":
-// CHECK: {"enabled":true,"level":"error","rank":50},"fullDescription":{"text":""},"id":"{{[0-9]+}}","name":""},{"defaultConfiguration":
+// CHECK: {"enabled":true,"level":"error","rank":50},"fullDescription":{"text":""},"id":"{{[A-Za-z0-9_]+}}","name":""},{"defaultConfiguration":
+// CHECK: {"enabled":true,"level":"error","rank":50},"fullDescription":{"text":""},"id":"{{[A-Za-z0-9_]+}}","name":""},{"defaultConfiguration":
+// CHECK: {"enabled":true,"level":"error","rank":50},"fullDescription":{"text":""},"id":"{{[A-Za-z0-9_]+}}","name":""},{"defaultConfiguration":
+// CHECK: {"enabled":true,"level":"warning","rank":-1},"fullDescription":{"text":""},"id":"{{[A-Za-z0-9_]+}}","name":""},{"defaultConfiguration":
+// CHECK: {"enabled":true,"level":"note","rank":-1},"fullDescription":{"text":""},"id":"{{[A-Za-z0-9_]+}}","name":""},{"defaultConfiguration":
+// CHECK: {"enabled":true,"level":"warning","rank":-1},"fullDescription":{"text":""},"id":"{{[A-Za-z0-9_]+}}","name":""},{"defaultConfiguration":
+// CHECK: {"enabled":true,"level":"error","rank":50},"fullDescription":{"text":""},"id":"{{[A-Za-z0-9_]+}}","name":""},{"defaultConfiguration":
+// CHECK: {"enabled":true,"level":"error","rank":50},"fullDescription":{"text":""},"id":"{{[A-Za-z0-9_]+}}","name":""},{"defaultConfiguration":
 // CHECK: {"enabled":true,"level":"error","rank":50},"fullDescription":
-// CHECK: {"text":""},"id":"{{[0-9]+}}","name":""}],"version":"{{[0-9]+\.[0-9]+\.[0-9]+[^" ]*}}"}}}],"version":"2.1.0"}
+// CHECK: {"text":""},"id":"{{[A-Za-z0-9_]+}}","name":""}],"version":"{{[0-9]+\.[0-9]+\.[0-9]+[^" ]*}}"}}}],"version":"2.1.0"}
 // CHECK: 2 warnings and 6 errors generated.

Copy link
Copy Markdown
Member

@Sirraide Sirraide left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation of this seems fine to me, but the RFC has to be accepted first before this can be merged.

I don’t think there is a good way to deal w/ custom diag ids. Those shouldn’t be used anywhere in Clang anyway (I know there are a few places that use them, but those should all be updated to use proper diagnostics instead).

@dbartol
Copy link
Copy Markdown
Contributor Author

dbartol commented Nov 19, 2025

The RFC has been approved to proceed by the Clang area team. I've updated the PR description to note a few open questions from the RFC, and how this PR chooses to answer each of those questions.

@dbartol
Copy link
Copy Markdown
Contributor Author

dbartol commented Nov 19, 2025

From the Area Team meeting notes, regarding diagnostic IDs:

They start with err_/warn_/ext_, and we often move diagnostics between them, very unstable

Should we automatically strip the prefix when generating the stable ID (either in the SARIF emitter, or in tablegen)?

@AaronBallman
Copy link
Copy Markdown
Contributor

I don’t think there is a good way to deal w/ custom diag ids. Those shouldn’t be used anywhere in Clang anyway (I know there are a few places that use them, but those should all be updated to use proper diagnostics instead).

+1, those custom diagnostics should be using real diagnostics instead.

Should we automatically strip the prefix when generating the stable ID (either in the SARIF emitter, or in tablegen)?

I think we may want an actual mapping instead; the problem is, we want the ability to rename any part of the diagnostic identifier because it is most readable when it matches the text of the diagnostic to some extent. The prefix happens to be something we change somewhat often as an example. But we sometimes reword diagnostics either because the diagnostic is now being used for more circumstances or because someone just found much better phrasing and it would be bad if our "stable" IDs either 1) weren't actually stable, or 2) were stable but prevented us from improving maintenance costs. In the PR summary you discuss this a bit with an idea of how we could improve it in the future, but I think that's going to be a problem sooner rather than later and we could consider addressing it up front.

Because these IDs are not being exposed to the user (either documented on a webpage, displayed as part of the diagnostic text to the user, etc), should we auto-generate a UUID for the diagnostic entries which don't have one already associated with them yet (which is all of them at first), and use that as the stable ID? Then it doesn't matter how the diagnostic changes, the underlying UUID is still the same. I'm not certain of how we would persist this across builds, though, so it might be a terrible idea. But I'm hoping we can find something that 1) doesn't require the developer to manually pick an ID and put it in the .td file, 2) allows us to modify anything about the diagnostic we want without impacting the stable ID. WDYT?

@dbartol
Copy link
Copy Markdown
Contributor Author

dbartol commented Nov 19, 2025

@AaronBallman

should we auto-generate a UUID...

To persist across builds, the IDs would either have to be in the .td files, or they'd need to be generated by some hash of stable inputs. I don't think we have any stable inputs to hash - the message, internal ID, and integer value could all change.

The SARIF stable ID doesn't have to be meaningful. If we went with UUIDs, someone adding a new diagnostic could just generate one easily enough. We'd just need a check somewhere to prevent duplicates in case of copy-paste mistakes.

We could also just use explicitly-assigned, dense-ish stable integer IDs, MSVC-style. Those are easier to use in both written and oral conversation than UUIDs, but they might require slightly more process. We'd do an initial pass to add them to the .td file, just like for UUIDs, and add the same check for duplicates. Adding a new diagnostic would require picking an unused integer ID. We'd probably have to keep a list of "retired" IDs in the code, though, to prevent accidental reuse of a no-longer-generated diagnostic's ID for a new diagnostic. The main problem with this approach would be dealing with forks and merge conflicts. If two forks both add warning 1234 and then try to push upstream, we'd have to have a rule like "first one into llvm-project/main wins, everyone else has to renumber".

I suppose we could split the difference and use one of those code phrase generators to make diagnostic IDs. Easier to use in conversation than UUIDs, but less likely to collide than manually-assigned dense integers. The downside is that at some point, we'll find ourselves writing something like "We're getting too many false positive reports for purple-monkey-dishwasher, so we'll need to tune the heuristcs."

Does any one of the above approaches seem workable?

@AaronBallman
Copy link
Copy Markdown
Contributor

@AaronBallman

should we auto-generate a UUID...

To persist across builds, the IDs would either have to be in the .td files, or they'd need to be generated by some hash of stable inputs. I don't think we have any stable inputs to hash - the message, internal ID, and integer value could all change.

Yeah, and we don't have a way to persist information across builds that I'm aware of.

The SARIF stable ID doesn't have to be meaningful. If we went with UUIDs, someone adding a new diagnostic could just generate one easily enough. We'd just need a check somewhere to prevent duplicates in case of copy-paste mistakes.

We could also just use explicitly-assigned, dense-ish stable integer IDs, MSVC-style. Those are easier to use in both written and oral conversation than UUIDs, but they might require slightly more process. We'd do an initial pass to add them to the .td file, just like for UUIDs, and add the same check for duplicates. Adding a new diagnostic would require picking an unused integer ID. We'd probably have to keep a list of "retired" IDs in the code, though, to prevent accidental reuse of a no-longer-generated diagnostic's ID for a new diagnostic. The main problem with this approach would be dealing with forks and merge conflicts. If two forks both add warning 1234 and then try to push upstream, we'd have to have a rule like "first one into llvm-project/main wins, everyone else has to renumber".

Yeah, that's why I didn't think we'd want to use dense integers. With UUIDs, downstreams have far less risk of conflicts.

I suppose we could split the difference and use one of those code phrase generators to make diagnostic IDs. Easier to use in conversation than UUIDs, but less likely to collide than manually-assigned dense integers. The downside is that at some point, we'll find ourselves writing something like "We're getting too many false positive reports for purple-monkey-dishwasher, so we'll need to tune the heuristcs."

Yeah, that does split the middle, but seems like a nightmare for people adding new diagnostics because they have to figure out a code phrase for each one, and I'm a bit worried about new contributor experience from that too.

Does any one of the above approaches seem workable?

Maybe, but what about this as an idea: we have a script that generates a UUID and gives it a constexpr name that the script automatically adds to a standalone .td file. When you want to add a new diagnostic, you have to run the script and it tells you "your diagnostic's ID is " and that gets added in the Diagnostic*Kind.td file with something like StableID<Whatever>. We could then decide that a StableID marking can go either on an individual diagnostic OR on a diagnostic group level; if on the diagnostic group level, then all diagnostics in the group are assumed to be "the same diagnostic", if we wanted that kind of control. The script would have to have some sort of reasonable naming scheme for the ID variables, but because those aren't user-facing, they can be kind of ugly but still better than a UUID; e.g., StableID<Sema12344> or something. We can help downstreams out by the script having a knob that downstreams can use to set their own starting point for IDs to help reduce conflicts. And the diagnostics tablegenerator can give an error if someone adds a diagnostic which doesn't have an ID (perhaps inherited from the group) or duplicates a UUID, to help catch mistakes.

What do you think of something along those lines? It's a bit heavier of a process to add a diagnostic than it is today, but we could actually make this a nicer script that does more than just SARIF IDs. e.g., maybe this script generates the full diagnostic for you. Like:

> gen-diag -part=sema -warning -default-error -name=do_not_do_bad_thing -msg="this is the diagnostic message, isn't it %select{great|terrible}0?" -group=bad-thing

def warn_do_not_do_bad_thing : Warning<
  "this is the diagnostic message, isn't it %select{great|terrible}0">,
  InGroup<DiagGroup<"bad-thing">>, DefaultError, StableID<Sema1234>;

(the tool could even automatically add the diagnostic text to the correct .td files directly instead of making people copy and paste it, or have a --dry-run option to show you what it would add to the file, etc.)

Maybe I'm scope-creeping too. :-D

@dbartol
Copy link
Copy Markdown
Contributor Author

dbartol commented Nov 23, 2025

@AaronBallman Your idea of having a tool to add a diagnostic got me thinking. Please bear with me...

I'll start with a couple of claims:

Warnings only

We should only really care about stable IDs for warnings, not errors/fatals/notes. While SARIF is capable of representing errors, I don't expect anyone to try to track instances of errors over time. Either your code compiles successfully or it doesn't. If it has errors, you have to fix them. The only reason why I think we should emit errors to the SARIF log at all is that having both errors and warnings in the same file makes it easier to use SARIF as a common structured diagnostic format to drive IDE support, CI reports, etc. So, while SARIF requires us to emit a ruleId property for each result, whether warning or error, I don't think we should make any effort to make the error IDs stable for now.

We seem to use notes only to annotate other warnings/errors. In SARIF, those are supposed to show up as relatedLocations on the root warning/error, rather than as their own result object. Thus, notes don't need SARIF IDs at all, stable or otherwise.

Focus on stability and evolution

The SARIF ID doesn't need to meaningful; it just needs to be unique and stable. So, instead of focusing on how to choose a stable ID for a warning, we should focus on how to ensure that the ID remains stable. We should make creation of a stable ID trivial, but then detect when new warnings are added or removed, and ensure that the developer has taken any necessary steps to preserve the stable IDs or to record how the stable IDs have evolved.

Proposal

For errors, fatals, and notes, we use the in-source identifier of the diagnostic as its SARIF ID. We do not expect this to be stable, but we do not expect SARIF tools to care about the stability of error IDs over time.

For warnings, we use the in-source identifier of the diagnostic as its SARIF ID by default. We also allow two additional properties: StableId<id> and OldStableIds<[id, ...]. StableId<id> specifies id as the stable ID of the diagnostic, instead of the default. OldStableIds<[id, ...]> adds the listed IDs to the deprecatedIds property of the diagnostic's SARIF rule metadata.

The effect on common contributor scenarios is as follows:

  • Adding a new diagnostic (either warning or non-warning): Same as today. Just add it, and we'll use the in-source identifier as its stable ID.
  • Deleting an existing diagnostic (either warning or non-warning): Same as today. Just delete it.
  • Modifying a property of an existing warning other than its in-source identifier: Same as today.
  • Changing the in-source identifier of a warning: Change the in-source identifier, but need to either explicitly specify StableId<original-name> (to keep the original stable ID) or OldStableIds<[original-name]> (to use the new in-source identifier as the stable ID while preserving history).
  • Splitting a warning into multiple warnings: Create the new warnings as today, but add OldStableIds<[original-name]> to each of them to preserve history.
  • Combining multiple warnings into a single warning: As today, but add OldStableIds<[original-name-1, original-name-2...]> to the combined warning to preserve history.

Enforcement

It should be easy enough to create a CI job that compares the set of diagnostics between the base commit of a PR and the head commit of that PR, and, if anything suspicious changes have occurred, posts instructions for how to deal with renaming/merging/splitting warnings.

To give you an idea of how often this would trigger, I wrote a simple script to crawl our Git history, run llvm-tblgen Diagnostic.td -dump-json on each commit and its parent, and identify whether that commit added a new warning, deleted an existing warning, both, or neither. Crawling back from current main to the beginning of 2024, there were:

  • 96 commits that added one or more new warnings without deleting any existing warnings. These could be splitting an existing warning, but are usually just new warnings.
  • 15 commits that deleted one or more existing warnings without adding any new warnings. These could be merging existing warnings into one, but are usually just deleting warnings.
  • 13 commits that both added and deleted warnings. These often appear to be doing something complex involving renaming, merging, or splitting.
  • 240 commits that modified one of the Diagnostic*.td files without adding or deleting any warnings.

(details here)

So, the bot would fire ~5 times a month, and actually provoke additional work from the contributor maybe once every month or two. Other than that, the whole diagnostic process would remain the same as today, which still seems like less work than managing GUIDs for every diagnostic.

This change modifies the one existing Clang test for SARIF output to use the same `normalize_sarif` sed script that we use for diffing SARIF in the Clang Static Analyzer tests, rather than using a complicated set of `CHECK` statements. This did require changing the SARIF output to include line breaks.
@dbartol
Copy link
Copy Markdown
Contributor Author

dbartol commented Dec 4, 2025

Pushed an additional commit to reuse the Static Analyzer's normalize_sarif script for testing SARIF output.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Dec 4, 2025

✅ With the latest revision this PR passed the Python code formatter.

@dbartol
Copy link
Copy Markdown
Contributor Author

dbartol commented Dec 5, 2025

@AaronBallman I went ahead and implemented the support for explicitly specifying the stable ID, and for listing previous stable IDs, in a separate PR against this PR. Any thoughts about this approach to managing stable IDs over time?

@AaronBallman
Copy link
Copy Markdown
Contributor

@AaronBallman Your idea of having a tool to add a diagnostic got me thinking. Please bear with me...

I'll start with a couple of claims:

Warnings only

We should only really care about stable IDs for warnings, not errors/fatals/notes. While SARIF is capable of representing errors, I don't expect anyone to try to track instances of errors over time.

I'm not certain I agree with this; there are reasons to track instances of errors over time (for example, gathering statistics about how often users run into a particular error, automatic reverts for certain kinds of errors, etc).

Either your code compiles successfully or it doesn't. If it has errors, you have to fix them. The only reason why I think we should emit errors to the SARIF log at all is that having both errors and warnings in the same file makes it easier to use SARIF as a common structured diagnostic format to drive IDE support, CI reports, etc. So, while SARIF requires us to emit a ruleId property for each result, whether warning or error, I don't think we should make any effort to make the error IDs stable for now.

I think we should make the error IDs stable from the start, along with warnings. Notes and remarks, however, don't seem to really have any kind of ID that I think would make sense to track. At least, I can't think of anything!

We seem to use notes only to annotate other warnings/errors. In SARIF, those are supposed to show up as relatedLocations on the root warning/error, rather than as their own result object. Thus, notes don't need SARIF IDs at all, stable or otherwise.

+1

Focus on stability and evolution

The SARIF ID doesn't need to meaningful; it just needs to be unique and stable. So, instead of focusing on how to choose a stable ID for a warning, we should focus on how to ensure that the ID remains stable. We should make creation of a stable ID trivial, but then detect when new warnings are added or removed, and ensure that the developer has taken any necessary steps to preserve the stable IDs or to record how the stable IDs have evolved.

+1

Proposal

For errors, fatals, and notes, we use the in-source identifier of the diagnostic as its SARIF ID. We do not expect this to be stable, but we do not expect SARIF tools to care about the stability of error IDs over time.

For warnings, we use the in-source identifier of the diagnostic as its SARIF ID by default. We also allow two additional properties: StableId<id> and OldStableIds<[id, ...]. StableId<id> specifies id as the stable ID of the diagnostic, instead of the default. OldStableIds<[id, ...]> adds the listed IDs to the deprecatedIds property of the diagnostic's SARIF rule metadata.

I think this should apply to warnings and errors both.

The effect on common contributor scenarios is as follows:

* **Adding a new diagnostic (either warning or non-warning):** Same as today. Just add it, and we'll use the in-source identifier as its stable ID.

* **Deleting an existing diagnostic (either warning or non-warning):** Same as today. Just delete it.

* **Modifying a property of an existing warning _other than its in-source identifier_:** Same as today.

* **Changing the in-source identifier of a warning:** Change the in-source identifier, but need to either explicitly specify `StableId<original-name>` (to keep the original stable ID) or `OldStableIds<[original-name]>` (to use the new in-source identifier as the stable ID while preserving history).

* **Splitting a warning into multiple warnings:** Create the new warnings as today, but add `OldStableIds<[original-name]>` to each of them to preserve history.

* **Combining multiple warnings into a single warning:** As today, but add `OldStableIds<[original-name-1, original-name-2...]>` to the combined warning to preserve history.

I think this makes sense. One question I have is: are these stable IDs usually displayed to the user in tools consuming SARIF? If so, are we sure these default IDs are reasonable for those cases? Or do we have to worry about tools expecting stable IDs to be short (so our long identifiers might get cut off such that the user only sees warn_foo_w and not warn_foo_was_bad_because_bar?

Enforcement

It should be easy enough to create a CI job that compares the set of diagnostics between the base commit of a PR and the head commit of that PR, and, if anything suspicious changes have occurred, posts instructions for how to deal with renaming/merging/splitting warnings.

To give you an idea of how often this would trigger, I wrote a simple script to crawl our Git history, run llvm-tblgen Diagnostic.td -dump-json on each commit and its parent, and identify whether that commit added a new warning, deleted an existing warning, both, or neither. Crawling back from current main to the beginning of 2024, there were:

* 96 commits that added one or more new warnings without deleting any existing warnings. These could be splitting an existing warning, but are usually just new warnings.

* 15 commits that deleted one or more existing warnings without adding any new warnings. These could be merging existing warnings into one, but are usually just deleting warnings.

* 13 commits that both added and deleted warnings. These often appear to be doing something complex involving renaming, merging, or splitting.

* 240 commits that modified one of the `Diagnostic*.td` files without adding or deleting any warnings.

(details here)

So, the bot would fire ~5 times a month, and actually provoke additional work from the contributor maybe once every month or two. Other than that, the whole diagnostic process would remain the same as today, which still seems like less work than managing GUIDs for every diagnostic.

Thank you, that's a fantastic analysis of the impacts! I think that's quite reasonable.

@AaronBallman
Copy link
Copy Markdown
Contributor

@AaronBallman I went ahead and implemented the support for explicitly specifying the stable ID, and for listing previous stable IDs, in a separate PR against this PR. Any thoughts about this approach to managing stable IDs over time?

I didn't do a full review, but based on what I saw, I think this is heading in a reasonable direction. But because diagnostics are so central to the compiler, I'd love some second opinions about the design here. I can't quite tell whether we're in RFC territory for this or not, too. CC @erichkeane @Sirraide @cor3ntin @rnk @Xazax-hun for some additional opinions (feel free to tag others if you think they'd interested too).

@erichkeane
Copy link
Copy Markdown
Contributor

Just catching up to this, sorry if I'm repeating above conversation:

As a starting point, this PR proposes the following answers to those open questions:

* _whether a unique ID is on the per-diagnostic level or on the diagnostic group level_ - per-diagnostic level. For my justification, see [this portion of the RFC discussion](https://discourse.llvm.org/t/rfc-emitting-auditable-sarif-logs-from-clang/88624/11?u=dbartol.).

* _whether the ID is explicitly spelled in the .td file or implicitly generated_ - Implicitly generated, but I'd be happy to have a way to explicitly specify it. I just think that the in-code identifier is a reasonable default, and manually reviewing the IDs of thousands of existing diagnostics would add little benefit.

* _whether we document the IDs_ - For now, the IDs are only exposed to the user (and other tools) in the SARIF file, so I don't think we need to document these. We could certainly add this information to the output of `diagtool` in the future if users find it relevant.

Honestly, this seems like a reasonable approach to these. All 3 answers are appropriate i think.

From the Area Team meeting notes, regarding diagnostic IDs:

They start with err_/warn_/ext_, and we often move diagnostics between them, very unstable

Should we automatically strip the prefix when generating the stable ID (either in the SARIF emitter, or in tablegen)?

We often have error/warning/ext versions of the same thing, so stripping it might be problematic.

@AaronBallman Your idea of having a tool to add a diagnostic got me thinking. Please bear with me...

I'll start with a couple of claims:

Warnings only

We should only really care about stable IDs for warnings, not errors/fatals/notes. While SARIF is capable of representing errors, I don't expect anyone to try to track instances of errors over time. Either your code compiles successfully or it doesn't. If it has errors, you have to fix them. The only reason why I think we should emit errors to the SARIF log at all is that having both errors and warnings in the same file makes it easier to use SARIF as a common structured diagnostic format to drive IDE support, CI reports, etc. So, while SARIF requires us to emit a ruleId property for each result, whether warning or error, I don't think we should make any effort to make the error IDs stable for now.

I don't think I agree with this. One of the uses of SARIF I think is that you could use it to keep track of a project that you know DOESN'T compile (like, for example, Clang-Linux, which failed for a very long time before it was successful).

We seem to use notes only to annotate other warnings/errors. In SARIF, those are supposed to show up as relatedLocations on the root warning/error, rather than as their own result object. Thus, notes don't need SARIF IDs at all, stable or otherwise.

This I agree on.

Focus on stability and evolution

The SARIF ID doesn't need to meaningful; it just needs to be unique and stable. So, instead of focusing on how to choose a stable ID for a warning, we should focus on how to ensure that the ID remains stable. We should make creation of a stable ID trivial, but then detect when new warnings are added or removed, and ensure that the developer has taken any necessary steps to preserve the stable IDs or to record how the stable IDs have evolved.

Proposal

For errors, fatals, and notes, we use the in-source identifier of the diagnostic as its SARIF ID. We do not expect this to be stable, but we do not expect SARIF tools to care about the stability of error IDs over time.

I disagree a bit here? See above.

For warnings, we use the in-source identifier of the diagnostic as its SARIF ID by default. We also allow two additional properties: StableId<id> and OldStableIds<[id, ...]. StableId<id> specifies id as the stable ID of the diagnostic, instead of the default. OldStableIds<[id, ...]> adds the listed IDs to the deprecatedIds property of the diagnostic's SARIF rule metadata.

The effect on common contributor scenarios is as follows:

* **Adding a new diagnostic (either warning or non-warning):** Same as today. Just add it, and we'll use the in-source identifier as its stable ID.

I think I'd be ok making Tablegen enforce adding a new 'tag' to each diagnostic that has its StableId. Not sure I like the name OldStableIds, but that is bikeshedding. You'd just have to do an initial population of that, and a script to name them.... something would be acceptable.

* **Deleting an existing diagnostic (either warning or non-warning):** Same as today. Just delete it.

* **Modifying a property of an existing warning _other than its in-source identifier_:** Same as today.

* **Changing the in-source identifier of a warning:** Change the in-source identifier, but need to either explicitly specify `StableId<original-name>` (to keep the original stable ID) or `OldStableIds<[original-name]>` (to use the new in-source identifier as the stable ID while preserving history).

I think this approach is a little subtle. And unless we have a way of enforcing when this happened, it'll be missed. IMO, we should just bite the bullet, require StableId via tablegen, and just come up iwth a heuristic for the 'initial set' of errors+ warnings.

* **Splitting a warning into multiple warnings:** Create the new warnings as today, but add `OldStableIds<[original-name]>` to each of them to preserve history.

* **Combining multiple warnings into a single warning:** As today, but add `OldStableIds<[original-name-1, original-name-2...]>` to the combined warning to preserve history.

Enforcement

It should be easy enough to create a CI job that compares the set of diagnostics between the base commit of a PR and the head commit of that PR, and, if anything suspicious changes have occurred, posts instructions for how to deal with renaming/merging/splitting warnings.

I'd rather tablegen enforce the existence, a CI job/etc to do it would be acceptable.

To give you an idea of how often this would trigger, I wrote a simple script to crawl our Git history, run llvm-tblgen Diagnostic.td -dump-json on each commit and its parent, and identify whether that commit added a new warning, deleted an existing warning, both, or neither. Crawling back from current main to the beginning of 2024, there were:

* 96 commits that added one or more new warnings without deleting any existing warnings. These could be splitting an existing warning, but are usually just new warnings.

* 15 commits that deleted one or more existing warnings without adding any new warnings. These could be merging existing warnings into one, but are usually just deleting warnings.

* 13 commits that both added and deleted warnings. These often appear to be doing something complex involving renaming, merging, or splitting.

* 240 commits that modified one of the `Diagnostic*.td` files without adding or deleting any warnings.

(details here)

So, the bot would fire ~5 times a month, and actually provoke additional work from the contributor maybe once every month or two. Other than that, the whole diagnostic process would remain the same as today, which still seems like less work than managing GUIDs for every diagnostic.

@dbartol
Copy link
Copy Markdown
Contributor Author

dbartol commented Dec 5, 2025

@AaronBallman @erichkeane

Both of you make good arguments for keeping error IDs stable, so I'm on board with that. I don't think that decision changes this PR or the tblgen PR I linked to, since the new properties are supported on any Diagnostic object already. It does change the hypothetical enforcement bot, though.

I think I'd be ok making Tablegen enforce adding a new 'tag' to each diagnostic that has its StableId. Not sure I like the name OldStableIds, but that is bikeshedding. You'd just have to do an initial population of that, and a script to name them.... something would be acceptable.

If the initial StableId tag is just auto-generated from the existing definition of the diagnostic, is there any point in actually adding all those initial StableId tags to the .td files? Even if we add these, we'd still need some kind of PR-time (not build-time, see below) enforcement to spot devs renaming/splitting/merging diagnostics and remind them to update OldStableIds. Having all StableIds be explicit might make it more likely for a dev who renames a diagnostic to keep its stable ID the same, but they'd still probably need the PR bot to remind them to update OldStableIds.

I think this approach is a little subtle. And unless we have a way of enforcing when this happened, it'll be missed. IMO, we should just bite the bullet, require StableId via tablegen, and just come up iwth a heuristic for the 'initial set' of errors+ warnings.
...
I'd rather tablegen enforce the existence, a CI job/etc to do it would be acceptable.

Tblgen can only enforce rules that only depend on the current version of the .td files. For example, we can and should enforce a "no duplicate StableIds" rule in tblgen at build time. The PR bot would be needed to catch cases where someone renamed a diagnostic and didn't keep the StableId (implicit or explicit) stable. It would also be needed to catch the more complex "merged two diagnostics" or "split a diagnostic" cases. Those cases require looking at the .td files before and after the change.

@dbartol
Copy link
Copy Markdown
Contributor Author

dbartol commented Dec 5, 2025

@AaronBallman

are these stable IDs usually displayed to the user in tools consuming SARIF? If so, are we sure these default IDs are reasonable for those cases? Or do we have to worry about tools expecting stable IDs to be short (so our long identifiers might get cut off such that the user only sees warn_foo_w and not warn_foo_was_bad_because_bar?

The SARIF consumer I'm most familiar with is GitHub Advanced Security, which does display the rule ID, but not particularly prominently. Screenshot attached, with rule ID highlighted.

GHAS Alert

As far as ID length goes, MSVC keeps it short (e.g., C4101), but that's mostly because MSVC has had those stable 4-or-5-digit warning IDs since at least the mid-90's. However, CodeQL has significantly longer IDs, like cpp/local-variable-hides-global-variable or cpp/comma-before-misleading-indentation, which seem comparable to Clang's existing warning enum names. I think we're OK using the IDs we've already got.

@erichkeane
Copy link
Copy Markdown
Contributor

@AaronBallman @erichkeane

Both of you make good arguments for keeping error IDs stable, so I'm on board with that. I don't think that decision changes this PR or the tblgen PR I linked to, since the new properties are supported on any Diagnostic object already. It does change the hypothetical enforcement bot, though.

I think I'd be ok making Tablegen enforce adding a new 'tag' to each diagnostic that has its StableId. Not sure I like the name OldStableIds, but that is bikeshedding. You'd just have to do an initial population of that, and a script to name them.... something would be acceptable.

If the initial StableId tag is just auto-generated from the existing definition of the diagnostic, is there any point in actually adding all those initial StableId tags to the .td files? Even if we add these, we'd still need some kind of PR-time (not build-time, see below) enforcement to spot devs renaming/splitting/merging diagnostics and remind them to update OldStableIds. Having all StableIds be explicit might make it more likely for a dev who renames a diagnostic to keep its stable ID the same, but they'd still probably need the PR bot to remind them to update OldStableIds.

Yes, there IS. This removes an entire class of 'rename problems' (AND the most likely ones!) to the point that PR-time enforcement will catch orders-of-magnitude fewer things. OF your list above, 96 + 15 + 240 all seem like 'not a problem'. This would enforce that breaking the SARIF name is an actual, active, intentional thing, which would remove all of the 13. I would still want periodic checking for these, but it would remove our risk of breaking them by almost 100%.

I think this approach is a little subtle. And unless we have a way of enforcing when this happened, it'll be missed. IMO, we should just bite the bullet, require StableId via tablegen, and just come up iwth a heuristic for the 'initial set' of errors+ warnings.
...
I'd rather tablegen enforce the existence, a CI job/etc to do it would be acceptable.

Tblgen can only enforce rules that only depend on the current version of the .td files. For example, we can and should enforce a "no duplicate StableIds" rule in tblgen at build time. The PR bot would be needed to catch cases where someone renamed a diagnostic and didn't keep the StableId (implicit or explicit) stable. It would also be needed to catch the more complex "merged two diagnostics" or "split a diagnostic" cases. Those cases require looking at the .td files before and after the change.

Right, I'm aware. Bur enforcing that we have those initial values means that the 'implicit' names don't cause an accidental breakage, and catch errors 'earlier' (even before the person submits their patch!). IMO, this will result in a much more stable set of names with less work on our parts.

@dbartol
Copy link
Copy Markdown
Contributor Author

dbartol commented Dec 5, 2025

@erichkeane

should all errors/warnings have explicit stable IDs?

Yes, there IS. This removes an entire class of 'rename problems' (AND the most likely ones!) to the point that PR-time enforcement will catch orders-of-magnitude fewer things. OF your list above, 96 + 15 + 240 all seem like 'not a problem'. This would enforce that breaking the SARIF name is an actual, active, intentional thing, which would remove all of the 13. I would still want periodic checking for these, but it would remove our risk of breaking them by almost 100%.

OK, let's do that. A couple details:
First, what's the best way to ensure that a developer changing or adding a diagnostic understands that the stable ID needs to be stable? Using the StableId<> class from my tblgen PR makes it more obvious, rather than just adding a second unnamed parameter to the Warning<> and Error<> classes. Does that sound OK?

Second, can I add those explicit stable IDs in a follow-up PR? So the PR sequence would be something like:

  1. The current PR (implicit stable IDs)
  2. Add tblgen support for (optional) explicit stable IDs PR here
  3. Add explicit stable IDs for all warnings and errors.
  4. Make explicit stable IDs required for warnings and errors.

I'm kind of dreading when PRs 3 and 4 hit my (or anyone else's) downstream fork, but I think having them as separate PRs will make that easier. It would be possible to merge PR 3 (which will probably have some merge conflicts), then go add stable IDs to all the downstream-only diagnostics, then merge PR 4.

@erichkeane
Copy link
Copy Markdown
Contributor

@erichkeane

should all errors/warnings have explicit stable IDs?

Yes, there IS. This removes an entire class of 'rename problems' (AND the most likely ones!) to the point that PR-time enforcement will catch orders-of-magnitude fewer things. OF your list above, 96 + 15 + 240 all seem like 'not a problem'. This would enforce that breaking the SARIF name is an actual, active, intentional thing, which would remove all of the 13. I would still want periodic checking for these, but it would remove our risk of breaking them by almost 100%.

OK, let's do that. A couple details: First, what's the best way to ensure that a developer changing or adding a diagnostic understands that the stable ID needs to be stable? Using the StableId<> class from my tblgen PR makes it more obvious, rather than just adding a second unnamed parameter to the Warning<> and Error<> classes. Does that sound OK?

Yep, agreed. I suspect changing StableId is something that any human programmer would think twice about, and hopefully any reviewer would notice. I think an implicit value is likely to not do a good job here.

Second, can I add those explicit stable IDs in a follow-up PR? So the PR sequence would be something like:

1. The current PR (implicit stable IDs)

2. Add tblgen support for (optional) explicit stable IDs [PR here](https://github.com/dbartol/llvm-project/pull/2)

3. Add explicit stable IDs for all warnings and errors.

4. Make explicit stable IDs required for warnings and errors.

I think a better order: Add support for StableId, add explicit IDs, make explicit StableId required for warnings/errors. I don't see any value to the 'implicit' version in the meantime.

I'm kind of dreading when PRs 3 and 4 hit my (or anyone else's) downstream fork, but I think having them as separate PRs will make that easier. It would be possible to merge PR 3 (which will probably have some merge conflicts), then go add stable IDs to all the downstream-only diagnostics, then merge PR 4.

Yes, downstreams are going to have some pain here :) Perhaps share the script you use for the explicit stable-id values/adding them would be a friendly thing to do, but that is the risk of being a downstream :)

@dbartol
Copy link
Copy Markdown
Contributor Author

dbartol commented Dec 5, 2025

Separate bikeshed thread on what the right name for OldStableIds is:

  1. OldStableIds - Pro: Short. Con: Not very clear what it means.
  2. DeprecatedStableIds - Pro: Matches the SARIF property name. Cons: Longer, still not super clear (but probably more clear than OldStableIds).
  3. PreviousStableIds - Pro: Slightly more clear, YMMV.
  4. Other suggestions?

My preference: Either DeprecatedStableIds or PreviousStableIds.

@dbartol
Copy link
Copy Markdown
Contributor Author

dbartol commented Dec 5, 2025

My preference: Either DeprecatedStableIds or PreviousStableIds.

LegacyStableIds? Whichever we do is going to need extensive documentation, so I don't have a strong preference. Previous strikes me the 'least', and Old is kinda bad. Deprecated has the advantage of actually being what is in SARIF, so I think I'm between Legacy and Deprecated, but I'm a touch of a coin-flip.

OK, I like Legacy better than either Previous or Old, so maybe a third voter can help us choose between LegacyStableIds and DeprecatedStableIds?

@AaronBallman
Copy link
Copy Markdown
Contributor

My preference: Either DeprecatedStableIds or PreviousStableIds.

LegacyStableIds? Whichever we do is going to need extensive documentation, so I don't have a strong preference. Previous strikes me the 'least', and Old is kinda bad. Deprecated has the advantage of actually being what is in SARIF, so I think I'm between Legacy and Deprecated, but I'm a touch of a coin-flip.

OK, I like Legacy better than either Previous or Old, so maybe a third voter can help us choose between LegacyStableIds and DeprecatedStableIds?

I slightly prefer Legacy over Deprecated; deprecated tends to have a bit of a value judgement associated with it that I don't think exists here. But I don't have a strong preference, either is perfectly fine by me.

@dbartol
Copy link
Copy Markdown
Contributor Author

dbartol commented Dec 8, 2025

I slightly prefer Legacy over Deprecated; deprecated tends to have a bit of a value judgement associated with it that I don't think exists here. But I don't have a strong preference, either is perfectly fine by me.

That's enough of a tie-breaker for me. Let's go with Legacy.

I'll merge my tblgen changes into this PR today, and then I'll merge once the implementation has been reviewed.

… diagnostics

This change adds tblgen support for two new properties on `Diagnostic` objects:

- `StableId<id>` - Explicitly specifies the stable ID for a diagnostic. If not specified, the stable ID defaults to the name of the diagnostic's enum member as before. The stable ID is what gets emitted as the `id` property of the diagnostic's `Rule` object in SARIF.
- `LegacyStableIds<[id1, id2, ...]> - Specifies a list of stable IDs by which this diagnostic was previously known. These are emitted in the `deprecatedIds` property of the diagnostic's `Rule` object in SARIF. This property is used when the diagnostic's stable ID is renamed, when a diagnostic is split into multiple diagnostics, or when multiple diagnostics are merged into a single diagnostic.

Most of the code changes are in tblgen, in the code that loads the new properties from their static data tables, and in the SARIF emitter code. There were some further mechanical changes wherever the `DIAG` macro was invoked, since it now has two new parameters.

To test these changes, I found a warning that was recently renamed, and added an `LegacyStableIds` list with the previous name, which is what we would have to do for any renamed warning in the future. I then added a test that ensures that when this warning is emitted, the SARIF has the right `id` and `deprecatedIds` properties.
@dbartol dbartol requested a review from steakhal December 12, 2025 17:46
Copy link
Copy Markdown
Contributor

@steakhal steakhal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, I made it! I left a couple of tiny comments inline, but overall the PR looks very solid and well thought through! I was pleased to review this.

Comment thread clang/include/clang/Basic/Diagnostic.td Outdated
Comment thread clang/include/clang/Basic/DiagnosticSemaKinds.td
Comment thread clang/lib/Basic/DiagnosticIDs.cpp Outdated
Comment thread clang/lib/Basic/DiagnosticIDs.cpp
Comment thread clang/test/Frontend/sarif-legacy-stable-ids.c Outdated
Comment thread clang/test/lit.cfg.py
Comment thread clang/utils/TableGen/ClangDiagnosticsEmitter.cpp Outdated
Comment thread clang/utils/TableGen/ClangDiagnosticsEmitter.cpp Outdated
Comment thread clang/utils/TableGen/ClangDiagnosticsEmitter.cpp Outdated
dbartol added a commit to dbartol/llvm-project that referenced this pull request Dec 15, 2025
This PR adds support to `clang-tblgen` to enforce that all warnings and extensions have explicitly-specified stable IDs, based on [discussion](llvm#168153 (comment)) in a previous PR. The clang-tblgen option to control this is `-enforce-stable-ids=<error|warning|none>`. To control this as part of the Clang build, specify `CLANG_ENFORCE_STABLE_IDS=<error|warning>` to CMake.

The default, for now, is to not enforce explicit stable IDs at all. I'll ramp this up to a warning in an upcoming PR, where I will also add stable IDs for all existing upstream warnings and extensions. We should give downstream forks some time to fix up their private diagnostics before we start enforcing explicit stable IDs with an error.

Clang-tblgen will emit fix suggestions for missing stable IDs, both as fixits to the console and as a clang-tidy-style YAML file to a path specified by `-export-fixes=`. The fixes from the entire Clang build can be bulk-applied via clang-apply-replacements; clang-tblgen will emit an additional warning specifying the exact command to run.

Re-using the existing YAML diagnostic/fixit support required a bit of minor refactoring. The `clang::tooling` library that contains that support also depends on parts of Clang itself, which would introduce a circular dependency if used in clang-tblgen. The cleanest way I could find to make this work was to ensure that the subset of the tooling library that is now needed in clang-tblgen does not `#include` any Clang headers. Those tooling headers _do_ still depend on some Clang types, but not for the subset of functionality used by clang-tblgen. So, I just used forward declarations of those Clang types, and as long as clang-tblgen doesn't call the affected functions, clang-tblgen will still compile. To make the consumption from clang-tblgen header-only, I had to move a few simple functions to be `inline` in the header as well.

The actual changes to clang-tblgen are pretty straightforward, although I did have to plumb the support for YAML fixits through from the command line.

Most of the remaining changes are adding explicit `llvm::` qualifications that are now needed because one of the Clang headers that is no longer being included must have had a stray `using namespace llvm;`.
Copy link
Copy Markdown
Contributor

@steakhal steakhal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me. Thanks for addressing my feedback.
@AaronBallman how to you feel about this?
(I am also affiliated by the same company as Dave; I think we usually wait for external approvals around here)

@steakhal steakhal changed the title [clang] [diagnostics] Stable IDs for Clang diagnostics [clang][diagnostics] Stable IDs for Clang diagnostics Dec 15, 2025
@erichkeane
Copy link
Copy Markdown
Contributor

This looks good to me. Thanks for addressing my feedback. @AaronBallman how to you feel about this? (I am also affiliated by the same company as Dave; I think we usually wait for external approvals around here)

Aaron is away until the new year, but I think it is good idea to wait for him here. I did a quick review and don't see anything I'm concerned about, but he's much more familiar with Sarif/diagnostics.

@dbartol
Copy link
Copy Markdown
Contributor Author

dbartol commented Dec 16, 2025

Sneak preview of the PR to add enforcement of explicit stable IDs, including auto-fixes.

@dbartol
Copy link
Copy Markdown
Contributor Author

dbartol commented Mar 2, 2026

@AaronBallman, could you take a look at this, please? If I can land this PR, I've got several less controversial SARIF changes stacked up behind it.

@AaronBallman
Copy link
Copy Markdown
Contributor

@AaronBallman, could you take a look at this, please? If I can land this PR, I've got several less controversial SARIF changes stacked up behind it.

Whoops, this completely fell off my radar, thank you for the ping! I'll review shortly.

std::string DiagnosticIDs::getStableID(unsigned DiagID) const {
if (const StaticDiagInfoRec *Info = GetDiagInfo(DiagID))
return Info->getStableID().str();
assert(CustomDiagInfo && "Invalid CustomDiagInfo");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this assert going to trigger on all custom diagnostics? (Same below.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a custom diagnostic, CustomDiagInfo will always be non-null, so the assert should not trigger. See DiagnosticIDs::getDescription() immediately above (not changed in this PR) for an existing example handling the exact same case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a colleague double-check my logic here, and he concurred with my reasoning. I'm going to merge based on your conditional approval, but do ping me if there's something you want me to follow up on.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for digging into that, I think I had my internal logic backwards. LG to land!

@AaronBallman
Copy link
Copy Markdown
Contributor

The only concern I had was regarding the behavior with custom diagnostics; so long as we don't trip the assert on every custom diagnostic, I think this is in good shape.

@dbartol dbartol requested a review from AaronBallman March 3, 2026 21:04
@dbartol dbartol merged commit 3da28bf into llvm:main Mar 6, 2026
10 checks passed
@llvm-ci
Copy link
Copy Markdown

llvm-ci commented Mar 6, 2026

LLVM Buildbot has detected a new failure on builder lldb-aarch64-ubuntu running on linaro-lldb-aarch64-ubuntu while building clang-tools-extra,clang at step 6 "test".

Full details are available at: https://lab.llvm.org/buildbot/#/builders/59/builds/32553

Here is the relevant piece of the build log for the reference
Step 6 (test) failure: build (failure)
...
PASS: lldb-api :: commands/log/basic/TestLogHandlers.py (193 of 2491)
PASS: lldb-api :: commands/log/invalid-args/TestInvalidArgsLog.py (194 of 2491)
PASS: lldb-api :: commands/help/TestHelp.py (195 of 2491)
PASS: lldb-api :: commands/memory/write/TestMemoryWrite.py (196 of 2491)
PASS: lldb-api :: commands/platform/basic/TestPlatformPython.py (197 of 2491)
PASS: lldb-api :: commands/platform/basic/TestPlatformCommand.py (198 of 2491)
PASS: lldb-api :: commands/platform/file/close/TestPlatformFileClose.py (199 of 2491)
PASS: lldb-api :: commands/platform/file/read/TestPlatformFileRead.py (200 of 2491)
PASS: lldb-api :: commands/memory/read/TestMemoryRead.py (201 of 2491)
UNRESOLVED: lldb-api :: commands/gui/spawn-threads/TestGuiSpawnThreads.py (202 of 2491)
******************** TEST 'lldb-api :: commands/gui/spawn-threads/TestGuiSpawnThreads.py' FAILED ********************
Script:
--
/usr/bin/python3.10 /home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/llvm-project/lldb/test/API/dotest.py -u CXXFLAGS -u CFLAGS --env LLVM_LIBS_DIR=/home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/build/./lib --env LLVM_INCLUDE_DIR=/home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/build/include --env LLVM_TOOLS_DIR=/home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/build/./bin --arch aarch64 --build-dir /home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/build/lldb-test-build.noindex --lldb-module-cache-dir /home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/build/lldb-test-build.noindex/module-cache-lldb/lldb-api --clang-module-cache-dir /home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/build/lldb-test-build.noindex/module-cache-clang/lldb-api --executable /home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/build/./bin/lldb --compiler /home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/build/./bin/clang --dsymutil /home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/build/./bin/dsymutil --make /usr/bin/gmake --llvm-tools-dir /home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/build/./bin --lldb-obj-root /home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/build/tools/lldb --lldb-libs-dir /home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/build/./lib --cmake-build-type Release /home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/llvm-project/lldb/test/API/commands/gui/spawn-threads -p TestGuiSpawnThreads.py
--
Exit Code: 1

Command Output (stdout):
--
lldb version 23.0.0git (https://github.com/llvm/llvm-project.git revision 3da28bfbce4d7ac8eaea6b8489031d01748d4fc5)
  clang revision 3da28bfbce4d7ac8eaea6b8489031d01748d4fc5
  llvm revision 3da28bfbce4d7ac8eaea6b8489031d01748d4fc5
Skipping the following test categories: libc++, msvcstl, dsym, pdb, gmodules, debugserver, objc

--
Command Output (stderr):
--
FAIL: LLDB (/home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/build/bin/clang-aarch64) :: test_gui (TestGuiSpawnThreads.TestGuiSpawnThreadsTest)
======================================================================
ERROR: test_gui (TestGuiSpawnThreads.TestGuiSpawnThreadsTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/llvm-project/lldb/packages/Python/lldbsuite/test/decorators.py", line 160, in wrapper
    return func(*args, **kwargs)
  File "/home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/llvm-project/lldb/test/API/commands/gui/spawn-threads/TestGuiSpawnThreads.py", line 44, in test_gui
    self.child.expect_exact(f"thread #{i + 2}: tid =")
  File "/usr/local/lib/python3.10/dist-packages/pexpect/spawnbase.py", line 432, in expect_exact
    return exp.expect_loop(timeout)
  File "/usr/local/lib/python3.10/dist-packages/pexpect/expect.py", line 179, in expect_loop
    return self.eof(e)
  File "/usr/local/lib/python3.10/dist-packages/pexpect/expect.py", line 122, in eof
    raise exc
pexpect.exceptions.EOF: End Of File (EOF). Exception style platform.
<pexpect.pty_spawn.spawn object at 0xff1d126f86a0>
command: /home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/build/bin/lldb
args: ['/home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/build/bin/lldb', '--no-lldbinit', '--no-use-colors', '-O', 'settings clear --all', '-O', 'settings set symbols.enable-external-lookup false', '-O', 'settings set target.inherit-tcc true', '-O', 'settings set target.disable-aslr false', '-O', 'settings set target.detach-on-error false', '-O', 'settings set target.auto-apply-fixits false', '-O', 'settings set plugin.process.gdb-remote.packet-timeout 60', '-O', 'settings set symbols.clang-modules-cache-path "/home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/build/lldb-test-build.noindex/module-cache-lldb/lldb-api"', '-O', 'settings set use-color false', '-O', 'settings set show-statusline false', '--file', '/home/tcwg-buildbot/worker/lldb-aarch64-ubuntu/build/lldb-test-build.noindex/commands/gui/spawn-threads/TestGuiSpawnThreads/a.out']
buffer (last 100 chars): b''
before (last 100 chars): b'thread_create.c:442:8\n#29 0x0000ede22d5c9e9c ./misc/../sysdeps/unix/sysv/linux/aarch64/clone.S:82:0\n'
after: <class 'pexpect.exceptions.EOF'>

@llvm-ci
Copy link
Copy Markdown

llvm-ci commented Mar 6, 2026

LLVM Buildbot has detected a new failure on builder ppc64le-flang-rhel-clang running on ppc64le-flang-rhel-test while building clang-tools-extra,clang at step 3 "clean-build-dir".

Full details are available at: https://lab.llvm.org/buildbot/#/builders/157/builds/45066

Here is the relevant piece of the build log for the reference
Step 3 (clean-build-dir) failure: Delete failed. (failure) (timed out)
Step 4 (cmake-configure) failure: cmake (failure) (timed out)
command timed out: 1200 seconds without output running [b'cmake', b'-DLLVM_TARGETS_TO_BUILD=PowerPC', b'-DLLVM_INSTALL_UTILS=ON', b'-DCMAKE_CXX_STANDARD=17', b'-DLLVM_LIT_ARGS=-vj 256', b'-DFLANG_ENABLE_WERROR=ON', b'-DLLVM_ENABLE_ASSERTIONS=ON', b'-DCMAKE_C_COMPILER_LAUNCHER=ccache', b'-DCMAKE_CXX_COMPILER_LAUNCHER=ccache', b'-DLLVM_ENABLE_PROJECTS=mlir;llvm;flang;clang', b'-DLLVM_ENABLE_RUNTIMES=openmp;flang-rt', b'-DCMAKE_BUILD_TYPE=Release', b'-GNinja', b'../llvm-project/llvm'], attempting to kill
process killed by signal 9
program finished with exit code -1
elapsedTime=1200.311801

swift-ci pushed a commit to swiftlang/llvm-project that referenced this pull request Mar 6, 2026
dbartol added a commit to dbartol/llvm-project that referenced this pull request Mar 10, 2026
This PR adds support to `clang-tblgen` to enforce that all warnings and extensions have explicitly-specified stable IDs, based on [discussion](llvm#168153 (comment)) in a previous PR. The clang-tblgen option to control this is `-enforce-stable-ids=<error|warning|none>`. To control this as part of the Clang build, specify `CLANG_ENFORCE_STABLE_IDS=<error|warning>` to CMake.

The default, for now, is to not enforce explicit stable IDs at all. I'll ramp this up to a warning in an upcoming PR, where I will also add stable IDs for all existing upstream warnings and extensions. We should give downstream forks some time to fix up their private diagnostics before we start enforcing explicit stable IDs with an error.

Clang-tblgen will emit fix suggestions for missing stable IDs, both as fixits to the console and as a clang-tidy-style YAML file to a path specified by `-export-fixes=`. The fixes from the entire Clang build can be bulk-applied via clang-apply-replacements; clang-tblgen will emit an additional warning specifying the exact command to run.

Re-using the existing YAML diagnostic/fixit support required a bit of minor refactoring. The `clang::tooling` library that contains that support also depends on parts of Clang itself, which would introduce a circular dependency if used in clang-tblgen. The cleanest way I could find to make this work was to ensure that the subset of the tooling library that is now needed in clang-tblgen does not `#include` any Clang headers. Those tooling headers _do_ still depend on some Clang types, but not for the subset of functionality used by clang-tblgen. So, I just used forward declarations of those Clang types, and as long as clang-tblgen doesn't call the affected functions, clang-tblgen will still compile. To make the consumption from clang-tblgen header-only, I had to move a few simple functions to be `inline` in the header as well.

The actual changes to clang-tblgen are pretty straightforward, although I did have to plumb the support for YAML fixits through from the command line.

Most of the remaining changes are adding explicit `llvm::` qualifications that are now needed because one of the Clang headers that is no longer being included must have had a stray `using namespace llvm;`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clang:diagnostics New/improved warning or error message in Clang, but not in clang-tidy or static analyzer clang:frontend Language frontend issues, e.g. anything involving "Sema" clang:static analyzer clang Clang issues not falling into any other category clang-tools-extra clangd

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants