[clang] [llvm] [plugins] Let plugins put diagnostics in user-controllable warning groups (PR #208538)
Vassil Vassilev via cfe-commits
cfe-commits at lists.llvm.org
Sun Sep 13 05:38:57 PDT 2026
https://github.com/vgvassilev updated https://github.com/llvm/llvm-project/pull/208538
>From 895ac75b8d37e4917fd5c590502135f12e01ce86 Mon Sep 17 00:00:00 2001
From: Vassil Vassilev <v.g.vassilev at gmail.com>
Date: Thu, 9 Jul 2026 16:09:56 +0000
Subject: [PATCH 1/9] [plugins] Let plugins put diagnostics in
user-controllable warning groups
Diagnostics created through DiagnosticsEngine::getCustomDiagID() are not
members of any diagnostic group, so -- unlike built-in diagnostics --
users cannot turn them off, turn them into errors, or discover how to
control them. A plugin such as clad that reports its own diagnostics
therefore has no way to let users silence them short of -w, which
disables every warning.
Add runtime-registered diagnostic groups for this. getCustomDiagID()
gains an overload taking a group name; the diagnostic then participates
in -W<group> / -Wno-<group> / -Werror=<group> (and -R<group> for
remarks) and prints "[-W<group>]" like a built-in diagnostic. The groups
live in a name-keyed registry in DiagnosticIDs next to the static
TableGen'd table; getDiagnosticsInGroup, setGroupSeverity and
initCustomDiagMapping learn to resolve them, and the per-diagnostic
DiagState machinery is reused as-is.
A group may hold warnings, errors and remarks at once: -W flags control
its warnings, -R flags its remarks independently, a grouped remark is
off until -R<group> enables it (like built-in remarks), and an error
keeps its severity -- a group flag can never silence an error, mirroring
the "cannot map errors into warnings" rule. By convention a plugin names
its group "<plugin>-plugin", and CompilerInstance registers that group
for every loaded plugin, with "plugin" as an umbrella (so -Wno-plugin
silences every plugin). A plugin may further split its diagnostics into
"<plugin>-plugin-<sub>" subgroups, controlled by their own name, the
parent group, or the umbrella, with the most specific one winning. Only
a command-line flag sets a group's default severity; a `#pragma clang
diagnostic` naming a group is location-scoped and applies through the
per-diagnostic DiagState instead. The command line is parsed before
plugins load, so a -Wno-<plugin>-plugin flag names a group that does not
exist yet; such a flag creates the group on demand and the mapping is
applied once the plugin registers its diagnostics. After all plugins
have loaded, a flag that named a plugin group no loaded plugin claimed
is reported as an unknown warning option, so typos are still caught.
The PrintFunctionNames example plugin gains -warn-decls / -remark-decls
/ -error-decls arguments that register such grouped diagnostics up front
(so a pragma can reach them) and emit them, exercised by a new lit test
alongside DiagnosticTest unit tests for the group and subgroup mechanics.
---
clang/docs/ReleaseNotes.md | 7 +
.../PrintFunctionNames/PrintFunctionNames.cpp | 59 +++-
clang/include/clang/Basic/Diagnostic.h | 22 +-
clang/include/clang/Basic/DiagnosticIDs.h | 118 +++++++-
clang/lib/Basic/Diagnostic.cpp | 12 +-
clang/lib/Basic/DiagnosticIDs.cpp | 132 ++++++++-
clang/lib/Frontend/CompilerInstance.cpp | 16 ++
.../test/Frontend/plugin-diagnostic-group.cpp | 72 +++++
clang/unittests/Basic/DiagnosticTest.cpp | 253 ++++++++++++++++++
9 files changed, 671 insertions(+), 20 deletions(-)
create mode 100644 clang/test/Frontend/plugin-diagnostic-group.cpp
diff --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md
index 043a0ddae2a6c..0dc0b5ce1143a 100644
--- a/clang/docs/ReleaseNotes.md
+++ b/clang/docs/ReleaseNotes.md
@@ -292,6 +292,13 @@ features cannot lower the translation-unit ABI level;
`-pedantic` or when that group is enabled explicitly, matching how the `_BitInt`
type itself is already handled.
+- Plugin diagnostics can now be placed in a warning group. `getCustomDiagID`
+ accepts a group name, so a plugin's diagnostics can be controlled with `-W`
+ and `-R` flags like built-in ones: by convention a plugin uses
+ `<plugin>-plugin`, silenced with `-Wno-<plugin>-plugin` (or the `-Wno-plugin`
+ umbrella over every loaded plugin), promoted with `-Werror=<plugin>-plugin`,
+ and remarks controlled with `-R<plugin>-plugin`. Errors keep their severity.
+ See [ClangPlugins](ClangPlugins.rst).
- Fixed bug in `-Wdocumentation` so that it correctly handles explicit
function template instantiations (#64087).
diff --git a/clang/examples/PrintFunctionNames/PrintFunctionNames.cpp b/clang/examples/PrintFunctionNames/PrintFunctionNames.cpp
index b2b785b87c25c..9843c8b23a579 100644
--- a/clang/examples/PrintFunctionNames/PrintFunctionNames.cpp
+++ b/clang/examples/PrintFunctionNames/PrintFunctionNames.cpp
@@ -25,17 +25,53 @@ namespace {
class PrintFunctionsConsumer : public ASTConsumer {
CompilerInstance &Instance;
std::set<std::string> ParsedTemplates;
+ // Diagnostics in the plugin's own "print-fns-plugin" group, or 0 if the
+ // corresponding argument was not passed. Registering the IDs up front (rather
+ // than lazily on first use) makes them members of the group before the source
+ // is parsed, so a `#pragma clang diagnostic` referring to the group can be
+ // applied to them.
+ unsigned WarnID = 0;
+ unsigned RemarkID = 0;
+ unsigned ErrorID = 0;
public:
PrintFunctionsConsumer(CompilerInstance &Instance,
- std::set<std::string> ParsedTemplates)
- : Instance(Instance), ParsedTemplates(ParsedTemplates) {}
+ std::set<std::string> ParsedTemplates,
+ bool WarnOnDecls, bool RemarkOnDecls,
+ bool ErrorOnDecls)
+ : Instance(Instance), ParsedTemplates(ParsedTemplates) {
+ DiagnosticsEngine &Diags = Instance.getDiagnostics();
+ if (RemarkOnDecls)
+ RemarkID = Diags.getCustomDiagID(DiagnosticsEngine::Remark,
+ "saw top-level declaration '%0'",
+ "print-fns-plugin");
+ if (WarnOnDecls)
+ WarnID = Diags.getCustomDiagID(DiagnosticsEngine::Warning,
+ "suspicious top-level declaration '%0'",
+ "print-fns-plugin");
+ if (ErrorOnDecls)
+ ErrorID = Diags.getCustomDiagID(DiagnosticsEngine::Error,
+ "forbidden top-level declaration '%0'",
+ "print-fns-plugin");
+ }
bool HandleTopLevelDecl(DeclGroupRef DG) override {
for (DeclGroupRef::iterator i = DG.begin(), e = DG.end(); i != e; ++i) {
const Decl *D = *i;
- if (const NamedDecl *ND = dyn_cast<NamedDecl>(D))
- llvm::errs() << "top-level-decl: \"" << ND->getNameAsString() << "\"\n";
+ const NamedDecl *ND = dyn_cast<NamedDecl>(D);
+ if (!ND)
+ continue;
+ llvm::errs() << "top-level-decl: \"" << ND->getNameAsString() << "\"\n";
+ // A user controls the warning with -Wno-print-fns-plugin and the remark
+ // with -Rno-print-fns-plugin (or the -Wplugin / -Wno-plugin umbrella),
+ // while the error keeps its severity -- group flags never silence errors.
+ DiagnosticsEngine &Diags = Instance.getDiagnostics();
+ if (RemarkID)
+ Diags.Report(ND->getLocation(), RemarkID) << ND->getNameAsString();
+ if (WarnID)
+ Diags.Report(ND->getLocation(), WarnID) << ND->getNameAsString();
+ if (ErrorID)
+ Diags.Report(ND->getLocation(), ErrorID) << ND->getNameAsString();
}
return true;
@@ -78,10 +114,15 @@ class PrintFunctionsConsumer : public ASTConsumer {
class PrintFunctionNamesAction : public PluginASTAction {
std::set<std::string> ParsedTemplates;
+ bool WarnOnDecls = false;
+ bool RemarkOnDecls = false;
+ bool ErrorOnDecls = false;
+
protected:
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,
llvm::StringRef) override {
- return std::make_unique<PrintFunctionsConsumer>(CI, ParsedTemplates);
+ return std::make_unique<PrintFunctionsConsumer>(
+ CI, ParsedTemplates, WarnOnDecls, RemarkOnDecls, ErrorOnDecls);
}
bool ParseArgs(const CompilerInstance &CI,
@@ -91,7 +132,13 @@ class PrintFunctionNamesAction : public PluginASTAction {
// Example error handling.
DiagnosticsEngine &D = CI.getDiagnostics();
- if (args[i] == "-an-error") {
+ if (args[i] == "-warn-decls") {
+ WarnOnDecls = true;
+ } else if (args[i] == "-remark-decls") {
+ RemarkOnDecls = true;
+ } else if (args[i] == "-error-decls") {
+ ErrorOnDecls = true;
+ } else if (args[i] == "-an-error") {
unsigned DiagID = D.getCustomDiagID(DiagnosticsEngine::Error,
"invalid argument '%0'");
D.Report(DiagID) << args[i];
diff --git a/clang/include/clang/Basic/Diagnostic.h b/clang/include/clang/Basic/Diagnostic.h
index 834f026aff62d..8e48a76e15a40 100644
--- a/clang/include/clang/Basic/Diagnostic.h
+++ b/clang/include/clang/Basic/Diagnostic.h
@@ -918,16 +918,28 @@ class DiagnosticsEngine : public RefCountedBase<DiagnosticsEngine> {
/// \param FormatString A fixed diagnostic format string that will be hashed
/// and mapped to a unique DiagID.
template <unsigned N>
- // FIXME: this API should almost never be used; custom diagnostics do not
- // have an associated diagnostic group and thus cannot be controlled by users
- // like other diagnostics. The number of times this API is used in Clang
- // should only ever be reduced, not increased.
- // [[deprecated("Use a CustomDiagDesc instead of a Level")]]
+ // A diagnostic created here belongs to no diagnostic group, so users cannot
+ // control it with -W flags. Prefer the overload below that takes a group
+ // name whenever the diagnostic should be user-controllable; uses of this
+ // ungrouped form in Clang should only ever be reduced, not increased.
+ // [[deprecated("Pass a group name, or use a CustomDiagDesc instead of a "
+ // "Level")]]
unsigned getCustomDiagID(Level L, const char (&FormatString)[N]) {
return Diags->getCustomDiagID((DiagnosticIDs::Level)L,
StringRef(FormatString, N - 1));
}
+ /// Compute the diagnostic ID for a plugin diagnostic in the runtime warning
+ /// group \p Group (by convention "<plugin>-plugin"). Unlike the overload
+ /// above, the diagnostic can be controlled by the user with -W<group> /
+ /// -Wno-<group> / -Werror=<group>, like a built-in warning.
+ template <unsigned N>
+ unsigned getCustomDiagID(Level L, const char (&FormatString)[N],
+ StringRef Group) {
+ return Diags->getCustomDiagID((DiagnosticIDs::Level)L,
+ StringRef(FormatString, N - 1), Group);
+ }
+
/// Converts a diagnostic argument (as an intptr_t) into the string
/// that represents it.
void ConvertArgToString(ArgumentKind Kind, intptr_t Val, StringRef Modifier,
diff --git a/clang/include/clang/Basic/DiagnosticIDs.h b/clang/include/clang/Basic/DiagnosticIDs.h
index 148d772a9e593..8a85e6815b281 100644
--- a/clang/include/clang/Basic/DiagnosticIDs.h
+++ b/clang/include/clang/Basic/DiagnosticIDs.h
@@ -17,6 +17,8 @@
#include "clang/Basic/DiagnosticCategories.h"
#include "clang/Basic/LLVM.h"
#include "llvm/ADT/IntrusiveRefCntPtr.h"
+#include "llvm/ADT/SmallVector.h"
+#include "llvm/ADT/StringMap.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/ErrorHandling.h"
#include <optional>
@@ -214,11 +216,16 @@ class DiagnosticIDs : public RefCountedBase<DiagnosticIDs> {
unsigned HasGroup : 1;
diag::Group Group;
std::string Description;
+ // Runtime-named diagnostic group for plugin diagnostics (by convention
+ // "<plugin>-plugin"), empty when the diagnostic is not part of one. Unlike
+ // Group above, this is not a member of the static (TableGen'd) group table.
+ std::string DynGroupName;
auto get_as_tuple() const {
return std::tuple(DefaultSeverity, DiagClass, ShowInSystemHeader,
ShowInSystemMacro, HasGroup, Group,
- std::string_view{Description});
+ std::string_view{Description},
+ std::string_view{DynGroupName});
}
public:
@@ -226,12 +233,14 @@ class DiagnosticIDs : public RefCountedBase<DiagnosticIDs> {
unsigned Class = CLASS_WARNING,
bool ShowInSystemHeader = false,
bool ShowInSystemMacro = false,
- std::optional<diag::Group> Group = std::nullopt)
+ std::optional<diag::Group> Group = std::nullopt,
+ std::string DynGroupName = {})
: DefaultSeverity(static_cast<unsigned>(DefaultSeverity)),
DiagClass(Class), ShowInSystemHeader(ShowInSystemHeader),
ShowInSystemMacro(ShowInSystemMacro), HasGroup(Group != std::nullopt),
Group(Group.value_or(diag::Group{})),
- Description(std::move(Description)) {}
+ Description(std::move(Description)),
+ DynGroupName(std::move(DynGroupName)) {}
std::optional<diag::Group> GetGroup() const {
if (HasGroup)
@@ -239,6 +248,8 @@ class DiagnosticIDs : public RefCountedBase<DiagnosticIDs> {
return std::nullopt;
}
+ StringRef GetDynGroup() const { return DynGroupName; }
+
diag::Severity GetDefaultSeverity() const {
return static_cast<diag::Severity>(DefaultSeverity);
}
@@ -276,6 +287,48 @@ class DiagnosticIDs : public RefCountedBase<DiagnosticIDs> {
return GIs;
}();
+ /// State for a runtime-registered plugin diagnostic group. Plugins name a
+ /// group "<plugin>-plugin"; it is not in the static (TableGen'd) table, so it
+ /// is kept here keyed by name. A group can be named by a -W flag before the
+ /// plugin that owns it is loaded, so severity is recorded here and applied to
+ /// members as they register (see getCustomDiagID, setGroupSeverity).
+ struct DynamicGroupInfo {
+ // Severity a -W/-Werror flag set for this group's warnings and errors, and
+ // the one a -R flag set for its remarks; a group may hold both. A
+ // default-constructed severity means the flavor was left untouched.
+ diag::Severity WarnSeverity = diag::Severity();
+ diag::Severity RemarkSeverity = diag::Severity();
+ bool CreatedByFlag = false; // named by a -W/-R flag
+ bool ClaimedByPlugin = false; // declared or used by a loaded plugin
+ llvm::SmallVector<unsigned, 4> Members; // custom diag IDs in this group
+
+ diag::Severity severityFor(diag::Flavor F) const {
+ return F == diag::Flavor::Remark ? RemarkSeverity : WarnSeverity;
+ }
+ };
+ llvm::StringMap<DynamicGroupInfo> DynamicGroups;
+
+ /// A plugin group name is the umbrella "plugin", a "<plugin>-plugin" group,
+ /// or a "<plugin>-plugin-<sub>..." subgroup of one.
+ static bool isPluginGroupName(StringRef Name) {
+ return Name == "plugin" || Name.ends_with("-plugin") ||
+ Name.contains("-plugin-");
+ }
+
+ /// Whether the plugin group named \p Ctrl controls diagnostics in plugin
+ /// group \p G: "plugin" controls every plugin group; otherwise \p Ctrl
+ /// controls itself and every dash-separated subgroup ("example-plugin"
+ /// controls "example-plugin-loop"). Used for both -Wno-<group> suppression
+ /// and a grouped diagnostic's default mapping.
+ static bool pluginGroupControls(StringRef Ctrl, StringRef G) {
+ if (Ctrl == "plugin")
+ return true;
+ if (Ctrl == G)
+ return true;
+ return G.starts_with(Ctrl) && G.size() > Ctrl.size() &&
+ G[Ctrl.size()] == '-';
+ }
+
public:
DiagnosticIDs();
~DiagnosticIDs();
@@ -327,6 +380,61 @@ class DiagnosticIDs : public RefCountedBase<DiagnosticIDs> {
}());
}
+ /// Return an ID for a plugin diagnostic that belongs to the runtime warning
+ /// group \p Group (by convention "<plugin>-plugin"). Unlike the two-argument
+ /// form above, the resulting diagnostic *can* be controlled by the user with
+ /// -W<group> / -Wno-<group> / -Werror=<group>, just like a built-in warning.
+ unsigned getCustomDiagID(Level Level, StringRef Message, StringRef Group) {
+ unsigned Class = CLASS_WARNING;
+ diag::Severity Sev = diag::Severity::Warning;
+ switch (Level) {
+ case DiagnosticIDs::Level::Ignored:
+ Sev = diag::Severity::Ignored;
+ break;
+ case DiagnosticIDs::Level::Note:
+ Sev = diag::Severity::Fatal;
+ Class = CLASS_NOTE;
+ break;
+ case DiagnosticIDs::Level::Remark:
+ Sev = diag::Severity::Remark;
+ Class = CLASS_REMARK;
+ break;
+ case DiagnosticIDs::Level::Warning:
+ Sev = diag::Severity::Warning;
+ break;
+ case DiagnosticIDs::Level::Error:
+ Sev = diag::Severity::Error;
+ Class = CLASS_ERROR;
+ break;
+ case DiagnosticIDs::Level::Fatal:
+ Sev = diag::Severity::Fatal;
+ Class = CLASS_ERROR;
+ break;
+ }
+ // If Group names a built-in (static) group, join it directly so it is
+ // controlled like any other member of that group; otherwise it is a runtime
+ // plugin group kept in the dynamic registry.
+ std::optional<diag::Group> StaticGroup = getGroupForWarningOption(Group);
+ return getCustomDiagID(CustomDiagDesc(
+ Sev, std::string(Message), Class,
+ /*ShowInSystemHeader=*/false, /*ShowInSystemMacro=*/false, StaticGroup,
+ StaticGroup ? std::string() : std::string(Group)));
+ }
+
+ /// Ensure a runtime plugin group named \p Name exists, so a -W flag can
+ /// target it before the plugin that owns it is loaded. Returns true if
+ /// \p Name is a plugin group name (and is now registered); false for any
+ /// other option, which the caller should treat as unknown.
+ bool ensureDynamicPluginGroup(StringRef Name);
+
+ /// Record that a loaded plugin owns the group \p Name, so a -W<name> that
+ /// referenced it is not later reported as an unknown warning option.
+ void registerPluginGroup(StringRef Name);
+
+ /// After all plugins have loaded, report every plugin group that was named by
+ /// a -W flag but that no plugin ever claimed -- i.e. a misspelled option.
+ void reportUnclaimedPluginGroups(DiagnosticsEngine &Diags) const;
+
//===--------------------------------------------------------------------===//
// Diagnostic classification and reporting interfaces.
//
@@ -386,7 +494,9 @@ class DiagnosticIDs : public RefCountedBase<DiagnosticIDs> {
/// Given a diagnostic group ID, return its documentation.
static StringRef getWarningOptionDocumentation(diag::Group GroupID);
- void setGroupSeverity(StringRef Group, diag::Severity);
+ void setGroupSeverity(StringRef Group, diag::Severity,
+ diag::Flavor Flavor = diag::Flavor::WarningOrError,
+ bool CommandLine = true);
void setGroupNoWarningsAsError(StringRef Group, bool);
/// Given a group ID, returns the flag that toggles the group.
diff --git a/clang/lib/Basic/Diagnostic.cpp b/clang/lib/Basic/Diagnostic.cpp
index 48dd9559ab8e6..b2a5b39aaddac 100644
--- a/clang/lib/Basic/Diagnostic.cpp
+++ b/clang/lib/Basic/Diagnostic.cpp
@@ -404,10 +404,16 @@ bool DiagnosticsEngine::setSeverityForGroup(diag::Flavor Flavor,
SourceLocation Loc) {
// Get the diagnostics in this group.
SmallVector<diag::kind, 256> GroupDiags;
- if (Diags->getDiagnosticsInGroup(Flavor, Group, GroupDiags))
- return true;
+ if (Diags->getDiagnosticsInGroup(Flavor, Group, GroupDiags)) {
+ // Not a known static group or an already-registered plugin group. A plugin
+ // group name is created on demand so a -W flag can precede the plugin that
+ // owns it; anything else is a genuinely unknown option.
+ if (!Diags->ensureDynamicPluginGroup(Group))
+ return true;
+ Diags->getDiagnosticsInGroup(Flavor, Group, GroupDiags);
+ }
- Diags->setGroupSeverity(Group, Map);
+ Diags->setGroupSeverity(Group, Map, Flavor, /*CommandLine=*/Loc.isInvalid());
// Set the mapping.
for (diag::kind Diag : GroupDiags)
diff --git a/clang/lib/Basic/DiagnosticIDs.cpp b/clang/lib/Basic/DiagnosticIDs.cpp
index 3709528e497d2..40c2a5f9cbb0f 100644
--- a/clang/lib/Basic/DiagnosticIDs.cpp
+++ b/clang/lib/Basic/DiagnosticIDs.cpp
@@ -352,6 +352,39 @@ void DiagnosticIDs::initCustomDiagMapping(DiagnosticMapping &Mapping,
unsigned DiagID) {
assert(IsCustomDiag(DiagID));
const auto &Diag = CustomDiagInfo->getDescription(DiagID);
+ if (StringRef DynGroup = Diag.GetDynGroup(); !DynGroup.empty()) {
+ // A plugin diagnostic in a runtime group. A grouped remark is off until a
+ // -R<group> flag enables it (like built-in remarks); a warning or error
+ // starts from its default severity.
+ Mapping.setSeverity(Diag.GetClass() == CLASS_REMARK
+ ? diag::Severity::Ignored
+ : Diag.GetDefaultSeverity());
+ // A diagnostic that is an error by default keeps its severity: -W/-R flags
+ // control warnings and remarks only, and cannot silence or downgrade an
+ // error (mirroring the "cannot map errors into warnings" rule for built-in
+ // diagnostics). For a warning or remark, apply the severity a flag recorded
+ // for the most specific group controlling it -- the umbrella "plugin", the
+ // "<plugin>-plugin" group, and any "<plugin>-plugin-<sub>" it lives in are
+ // candidates; the longest (most specific) name wins. The flag may have been
+ // seen before this plugin loaded.
+ if (Diag.GetClass() != CLASS_ERROR) {
+ diag::Flavor DiagFlavor = Diag.GetClass() == CLASS_REMARK
+ ? diag::Flavor::Remark
+ : diag::Flavor::WarningOrError;
+ const DynamicGroupInfo *Best = nullptr;
+ size_t BestLen = 0;
+ for (const auto &Entry : DynamicGroups)
+ if (Entry.second.severityFor(DiagFlavor) != diag::Severity() &&
+ pluginGroupControls(Entry.first(), DynGroup) &&
+ (!Best || Entry.first().size() > BestLen)) {
+ Best = &Entry.second;
+ BestLen = Entry.first().size();
+ }
+ if (Best)
+ Mapping.setSeverity(Best->severityFor(DiagFlavor));
+ }
+ return;
+ }
if (auto Group = Diag.GetGroup()) {
GroupInfo GroupInfo = GroupInfos[static_cast<size_t>(*Group)];
if (static_cast<diag::Severity>(GroupInfo.Severity) != diag::Severity())
@@ -438,7 +471,20 @@ DiagnosticIDs::~DiagnosticIDs() {}
unsigned DiagnosticIDs::getCustomDiagID(CustomDiagDesc Diag) {
if (!CustomDiagInfo)
CustomDiagInfo.reset(new diag::CustomDiagInfo());
- return CustomDiagInfo->getOrCreateDiagID(Diag);
+ StringRef DynGroup = Diag.GetDynGroup();
+ unsigned ID = CustomDiagInfo->getOrCreateDiagID(Diag);
+ // A plugin diagnostic joins its runtime group. This both lets -W<group> reach
+ // it and, if a -W flag named the group before this plugin was loaded, marks
+ // the group as owned so it is not later reported as an unknown option. The
+ // recorded group severity (if any) is picked up as this diag's default
+ // mapping in initCustomDiagMapping.
+ if (!DynGroup.empty()) {
+ DynamicGroupInfo &Info = DynamicGroups[DynGroup];
+ Info.ClaimedByPlugin = true;
+ if (!llvm::is_contained(Info.Members, ID))
+ Info.Members.push_back(ID);
+ }
+ return ID;
}
bool DiagnosticIDs::isWarningOrExtension(unsigned DiagID) const {
@@ -741,6 +787,13 @@ DiagnosticIDs::getGroupForDiag(unsigned DiagID) const {
/// enables the specified diagnostic. If there is no -Wfoo flag that controls
/// the diagnostic, this returns null.
StringRef DiagnosticIDs::getWarningOptionForDiag(unsigned DiagID) {
+ // A plugin diagnostic advertises its runtime group so the printed
+ // "[-W<plugin>-plugin]" tells the user how to control it.
+ if (IsCustomDiag(DiagID) && CustomDiagInfo) {
+ StringRef DynGroup = CustomDiagInfo->getDescription(DiagID).GetDynGroup();
+ if (!DynGroup.empty())
+ return DynGroup;
+ }
if (auto G = getGroupForDiag(DiagID))
return getWarningOptionForGroup(*G);
return StringRef();
@@ -806,6 +859,37 @@ DiagnosticIDs::getDiagnosticsInGroup(diag::Flavor Flavor, StringRef Group,
&OptionTable[static_cast<unsigned>(*G)],
Diags, CustomDiagInfo.get());
}
+ // A runtime plugin group. "plugin" is the umbrella over every plugin group;
+ // "<plugin>-plugin" also controls its "<plugin>-plugin-<sub>" subgroups. Add
+ // the members of every registered group this name controls that match the
+ // requested flavor -- a plugin group may hold warnings, errors (both
+ // WarningOrError) and remarks. The group counts as "known" once it (or a
+ // subgroup) is registered -- by a -W/-R flag or by a plugin -- so it is not
+ // reported as an unknown option; genuine typos are caught later by
+ // reportUnclaimedPluginGroups.
+ if (isPluginGroupName(Group)) {
+ bool Known = DynamicGroups.contains(Group);
+ for (const auto &Entry : DynamicGroups)
+ if (pluginGroupControls(Group, Entry.first())) {
+ Known = true;
+ for (unsigned ID : Entry.second.Members) {
+ if (!CustomDiagInfo)
+ continue;
+ DiagnosticIDs::Class Class =
+ CustomDiagInfo->getDescription(ID).GetClass();
+ // Errors are not controllable by -W/-R group flags; leave them out so
+ // the mapping is never asked to downgrade an error.
+ if (Class == CLASS_ERROR)
+ continue;
+ diag::Flavor MemberFlavor = Class == CLASS_REMARK
+ ? diag::Flavor::Remark
+ : diag::Flavor::WarningOrError;
+ if (MemberFlavor == Flavor)
+ Diags.push_back(ID);
+ }
+ }
+ return !Known;
+ }
return true;
}
@@ -825,11 +909,27 @@ static void forEachSubGroup(diag::Group Group, Func func) {
::forEachSubGroupImpl(WarningOpt, std::move(func));
}
-void DiagnosticIDs::setGroupSeverity(StringRef Group, diag::Severity Sev) {
+void DiagnosticIDs::setGroupSeverity(StringRef Group, diag::Severity Sev,
+ diag::Flavor Flavor, bool CommandLine) {
if (std::optional<diag::Group> G = getGroupForWarningOption(Group)) {
::forEachSubGroup(*G, [&](size_t SubGroup) {
GroupInfos[SubGroup].Severity = static_cast<unsigned>(Sev);
});
+ return;
+ }
+ // Record the severity of a runtime plugin group so diagnostics registering
+ // into it later inherit it (see initCustomDiagMapping). A -W/-Werror flag and
+ // a -R flag control different flavors of the same group independently. Only a
+ // command-line flag sets this group-wide default; a `#pragma clang
+ // diagnostic` is location-scoped and applies through the per-diagnostic
+ // DiagState instead, so it must not overwrite the group's global severity.
+ if (!CommandLine)
+ return;
+ if (auto It = DynamicGroups.find(Group); It != DynamicGroups.end()) {
+ if (Flavor == diag::Flavor::Remark)
+ It->second.RemarkSeverity = Sev;
+ else
+ It->second.WarnSeverity = Sev;
}
}
@@ -841,6 +941,34 @@ void DiagnosticIDs::setGroupNoWarningsAsError(StringRef Group, bool Val) {
}
}
+bool DiagnosticIDs::ensureDynamicPluginGroup(StringRef Name) {
+ if (!isPluginGroupName(Name))
+ return false;
+ // Create the group (StringMap::operator[]) so a -W flag can target it before
+ // the plugin that owns it is loaded. Flag-created but never-claimed groups
+ // are flagged as typos by reportUnclaimedPluginGroups.
+ DynamicGroups[Name].CreatedByFlag = true;
+ return true;
+}
+
+void DiagnosticIDs::registerPluginGroup(StringRef Name) {
+ DynamicGroups[Name].ClaimedByPlugin = true;
+}
+
+void DiagnosticIDs::reportUnclaimedPluginGroups(
+ DiagnosticsEngine &Diags) const {
+ for (const auto &Entry : DynamicGroups) {
+ const DynamicGroupInfo &Info = Entry.second;
+ // The umbrella "plugin" is always valid; a specific group named by a flag
+ // that no loaded plugin ever claimed is a misspelled option.
+ if (Entry.first() != "plugin" && Info.CreatedByFlag &&
+ !Info.ClaimedByPlugin)
+ Diags.Report(diag::warn_unknown_diag_option)
+ << /*flavor=warning*/ 0 << (Twine("-W") + Entry.first()).str()
+ << /*has suggestion=*/false << StringRef();
+ }
+}
+
void DiagnosticIDs::getAllDiagnostics(diag::Flavor Flavor,
std::vector<diag::kind> &Diags) {
for (unsigned i = 0; i != StaticDiagInfoSize; ++i)
diff --git a/clang/lib/Frontend/CompilerInstance.cpp b/clang/lib/Frontend/CompilerInstance.cpp
index 87abcd38c1a92..fc3431e8b6a0d 100644
--- a/clang/lib/Frontend/CompilerInstance.cpp
+++ b/clang/lib/Frontend/CompilerInstance.cpp
@@ -1123,6 +1123,17 @@ void CompilerInstance::LoadRequestedPlugins() {
}
}
+ // Give every loaded plugin a diagnostic group named "<plugin>-plugin", so its
+ // diagnostics can be controlled with -W<plugin>-plugin / -Wno-<plugin>-plugin
+ // (and -Wplugin as an umbrella) like a built-in warning. The command line was
+ // already parsed in createDiagnostics, before any plugin was loaded, so a
+ // -W flag naming such a group was deferred; registering the group here marks
+ // it as owned so it is not reported as unknown.
+ for (const FrontendPluginRegistry::entry &Plugin :
+ FrontendPluginRegistry::entries())
+ getDiagnostics().getDiagnosticIDs()->registerPluginGroup(
+ (Plugin.getName() + "-plugin").str());
+
// Check if any of the loaded plugins replaces the main AST action
for (const FrontendPluginRegistry::entry &Plugin :
FrontendPluginRegistry::entries()) {
@@ -1133,6 +1144,11 @@ void CompilerInstance::LoadRequestedPlugins() {
break;
}
}
+
+ // Every plugin group is now known, so a -W<plugin>-plugin flag that named a
+ // group no loaded plugin owns is a misspelled option -- report it.
+ getDiagnostics().getDiagnosticIDs()->reportUnclaimedPluginGroups(
+ getDiagnostics());
}
/// Determine the appropriate source input kind based on language
diff --git a/clang/test/Frontend/plugin-diagnostic-group.cpp b/clang/test/Frontend/plugin-diagnostic-group.cpp
new file mode 100644
index 0000000000000..cc9a4909fba5d
--- /dev/null
+++ b/clang/test/Frontend/plugin-diagnostic-group.cpp
@@ -0,0 +1,72 @@
+// Tests that plugin diagnostics emitted in the plugin's own group
+// ("<plugin>-plugin") can be controlled by the user like built-in diagnostics:
+// a warning silenced with -Wno-<group> / the -Wplugin umbrella and promoted
+// with -Werror=<group>; a remark that is off until -R<group>, independent of
+// -W; an error that a group flag cannot silence; a group `#pragma clang
+// diagnostic`; and a misspelled group reported as unknown.
+
+// RUN: split-file %s %t
+
+// A warning is on by default and prints its group.
+// RUN: %clang_cc1 -load %llvmshlibdir/PrintFunctionNames%pluginext \
+// RUN: -plugin print-fns -plugin-arg-print-fns -warn-decls %t/simple.cpp 2>&1 \
+// RUN: | FileCheck --check-prefix=WARN %s
+// A warning is silenced by -Wno-<group> and by the -Wno-plugin umbrella.
+// RUN: %clang_cc1 -load %llvmshlibdir/PrintFunctionNames%pluginext \
+// RUN: -plugin print-fns -plugin-arg-print-fns -warn-decls \
+// RUN: -Wno-print-fns-plugin %t/simple.cpp 2>&1 | FileCheck --check-prefix=SILENT %s
+// RUN: %clang_cc1 -load %llvmshlibdir/PrintFunctionNames%pluginext \
+// RUN: -plugin print-fns -plugin-arg-print-fns -warn-decls \
+// RUN: -Wno-plugin %t/simple.cpp 2>&1 | FileCheck --check-prefix=SILENT %s
+// A warning is promoted by -Werror=<group>.
+// RUN: not %clang_cc1 -load %llvmshlibdir/PrintFunctionNames%pluginext \
+// RUN: -plugin print-fns -plugin-arg-print-fns -warn-decls \
+// RUN: -Werror=print-fns-plugin %t/simple.cpp 2>&1 | FileCheck --check-prefix=WERROR %s
+
+// A remark is off by default, enabled by -R<group>, and unaffected by -W.
+// RUN: %clang_cc1 -load %llvmshlibdir/PrintFunctionNames%pluginext \
+// RUN: -plugin print-fns -plugin-arg-print-fns -remark-decls %t/simple.cpp 2>&1 \
+// RUN: | FileCheck --check-prefix=SILENT %s
+// RUN: %clang_cc1 -load %llvmshlibdir/PrintFunctionNames%pluginext \
+// RUN: -plugin print-fns -plugin-arg-print-fns -remark-decls \
+// RUN: -Rprint-fns-plugin -Wno-print-fns-plugin %t/simple.cpp 2>&1 \
+// RUN: | FileCheck --check-prefix=REMARK %s
+
+// An error cannot be silenced by a group flag.
+// RUN: not %clang_cc1 -load %llvmshlibdir/PrintFunctionNames%pluginext \
+// RUN: -plugin print-fns -plugin-arg-print-fns -error-decls \
+// RUN: -Wno-print-fns-plugin %t/simple.cpp 2>&1 | FileCheck --check-prefix=ERROR %s
+
+// A `#pragma clang diagnostic` resolves the group and scopes the change.
+// RUN: %clang_cc1 -load %llvmshlibdir/PrintFunctionNames%pluginext \
+// RUN: -plugin print-fns -plugin-arg-print-fns -warn-decls %t/pragma.cpp 2>&1 \
+// RUN: | FileCheck --check-prefix=PRAGMA %s
+
+// A -Wno-<x>-plugin that no loaded plugin claims is a misspelled option.
+// RUN: %clang_cc1 -load %llvmshlibdir/PrintFunctionNames%pluginext \
+// RUN: -plugin print-fns -Wno-bogus-plugin %t/simple.cpp 2>&1 \
+// RUN: | FileCheck --check-prefix=UNKNOWN %s
+
+// REQUIRES: plugins, examples
+
+//--- simple.cpp
+void f();
+
+// WARN: warning: suspicious top-level declaration 'f' [-Wprint-fns-plugin]
+// SILENT-NOT: top-level declaration 'f'
+// WERROR: error: suspicious top-level declaration 'f'
+// REMARK: remark: saw top-level declaration 'f' [-Rprint-fns-plugin]
+// ERROR: error: forbidden top-level declaration 'f'
+// UNKNOWN: unknown warning option
+
+//--- pragma.cpp
+void before();
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wprint-fns-plugin"
+void during();
+#pragma clang diagnostic pop
+void after();
+
+// PRAGMA: warning: suspicious top-level declaration 'before'
+// PRAGMA-NOT: declaration 'during'
+// PRAGMA: warning: suspicious top-level declaration 'after'
diff --git a/clang/unittests/Basic/DiagnosticTest.cpp b/clang/unittests/Basic/DiagnosticTest.cpp
index 793431bbbe154..54b724bd05cd7 100644
--- a/clang/unittests/Basic/DiagnosticTest.cpp
+++ b/clang/unittests/Basic/DiagnosticTest.cpp
@@ -471,4 +471,257 @@ TEST(EscapeSingleCodepointForDiagnosticTest, nonScalarValues) {
EXPECT_EQ(EscapeSingleCodepointForDiagnostic(0x110000), "<0x110000>");
}
+// Plugins register their diagnostics under a warning group they name at runtime
+// (by convention "<plugin>-plugin"), so users can
+// silence them with -Wno-<group> exactly like a built-in warning -- even though
+// the group name is unknown to the compiler until the plugin is loaded.
+class PluginWarningGroupTest : public testing::Test {
+public:
+ PluginWarningGroupTest() {
+ Diags.setClient(&CaptureConsumer, /*ShouldOwnClient=*/false);
+ }
+
+protected:
+ llvm::IntrusiveRefCntPtr<llvm::vfs::InMemoryFileSystem> FS =
+ llvm::makeIntrusiveRefCnt<llvm::vfs::InMemoryFileSystem>();
+ DiagnosticOptions DiagOpts;
+ DiagnosticsEngine Diags{DiagnosticIDs::create(), DiagOpts};
+
+ llvm::ArrayRef<StoredDiagnostic> diags() {
+ return CaptureConsumer.StoredDiags;
+ }
+
+private:
+ class CaptureDiagnosticConsumer : public DiagnosticConsumer {
+ public:
+ std::vector<StoredDiagnostic> StoredDiags;
+ void HandleDiagnostic(DiagnosticsEngine::Level Level,
+ const Diagnostic &Info) override {
+ StoredDiags.push_back(StoredDiagnostic(Level, Info));
+ }
+ };
+ CaptureDiagnosticConsumer CaptureConsumer;
+};
+
+// -Wno-<group> silences a plugin diagnostic tagged with that group.
+TEST_F(PluginWarningGroupTest, WnoSuppressesPluginGroup) {
+ unsigned ID =
+ Diags.getCustomDiagID(DiagnosticsEngine::Warning,
+ "plugin reused an AST node", "example-plugin");
+ EXPECT_FALSE(Diags.isIgnored(ID, SourceLocation()));
+
+ DiagOpts.Warnings = {"no-example-plugin"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+ EXPECT_TRUE(Diags.isIgnored(ID, SourceLocation()));
+}
+
+// The command line is parsed before the plugin loads, so the group name is
+// still unknown when -Wno-<group> is processed. The mapping must be remembered
+// and applied once the plugin registers the diagnostic -- and the flag must not
+// be reported as an unknown warning option in the meantime.
+TEST_F(PluginWarningGroupTest, WnoBeforePluginRegistersGroup) {
+ DiagOpts.Warnings = {"no-example-plugin"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+ EXPECT_THAT(diags(), IsEmpty());
+
+ unsigned ID =
+ Diags.getCustomDiagID(DiagnosticsEngine::Warning,
+ "plugin reused an AST node", "example-plugin");
+ EXPECT_TRUE(Diags.isIgnored(ID, SourceLocation()));
+}
+
+// -Wplugin is an umbrella over every plugin group.
+TEST_F(PluginWarningGroupTest, WnoPluginUmbrellaSuppressesEveryPluginGroup) {
+ DiagOpts.Warnings = {"no-plugin"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+
+ unsigned First = Diags.getCustomDiagID(DiagnosticsEngine::Warning,
+ "first diag", "example-plugin");
+ unsigned Other = Diags.getCustomDiagID(DiagnosticsEngine::Warning,
+ "other diag", "othertool-plugin");
+ EXPECT_TRUE(Diags.isIgnored(First, SourceLocation()));
+ EXPECT_TRUE(Diags.isIgnored(Other, SourceLocation()));
+}
+
+// -Wno-<group> only touches diagnostics in that group.
+TEST_F(PluginWarningGroupTest, LeavesOtherDiagnosticsAlone) {
+ unsigned Grouped = Diags.getCustomDiagID(DiagnosticsEngine::Warning,
+ "grouped", "example-plugin");
+ unsigned Ungrouped =
+ Diags.getCustomDiagID(DiagnosticsEngine::Warning, "ungrouped");
+
+ DiagOpts.Warnings = {"no-example-plugin"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+ EXPECT_TRUE(Diags.isIgnored(Grouped, SourceLocation()));
+ EXPECT_FALSE(Diags.isIgnored(Ungrouped, SourceLocation()));
+}
+
+// -Werror=<group> promotes a plugin warning to an error.
+TEST_F(PluginWarningGroupTest, WerrorPromotesPluginGroup) {
+ unsigned ID = Diags.getCustomDiagID(DiagnosticsEngine::Warning, "plugin diag",
+ "example-plugin");
+ DiagOpts.Warnings = {"error=example-plugin"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+ EXPECT_EQ(Diags.getDiagnosticLevel(ID, SourceLocation()),
+ DiagnosticsEngine::Error);
+}
+
+// Global -Werror promotes a plugin warning like any other warning.
+TEST_F(PluginWarningGroupTest, GlobalWerrorPromotesPluginWarning) {
+ unsigned ID = Diags.getCustomDiagID(DiagnosticsEngine::Warning, "plugin diag",
+ "example-plugin");
+ DiagOpts.Warnings = {"error"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+ EXPECT_EQ(Diags.getDiagnosticLevel(ID, SourceLocation()),
+ DiagnosticsEngine::Error);
+}
+
+// A -Wno-<x>-plugin naming a group that no plugin ever claims is deferred (not
+// reported immediately as unknown), then flagged once all plugins have loaded.
+TEST_F(PluginWarningGroupTest, UnclaimedPluginGroupReportedAfterLoad) {
+ DiagOpts.Warnings = {"no-bogus-plugin"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+ EXPECT_THAT(diags(), IsEmpty());
+
+ Diags.getDiagnosticIDs()->reportUnclaimedPluginGroups(Diags);
+ EXPECT_THAT(diags(), ElementsAre(WithMessage(
+ "unknown warning option '-Wbogus-plugin'")));
+}
+
+// A plugin group claimed by a loaded plugin is not reported, even though the
+// -W flag named it before the plugin registered its diagnostic.
+TEST_F(PluginWarningGroupTest, ClaimedPluginGroupNotReported) {
+ DiagOpts.Warnings = {"no-example-plugin"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+ Diags.getCustomDiagID(DiagnosticsEngine::Warning, "diag", "example-plugin");
+
+ Diags.getDiagnosticIDs()->reportUnclaimedPluginGroups(Diags);
+ EXPECT_THAT(diags(), IsEmpty());
+}
+
+// A diagnostic in a "<plugin>-plugin-<sub>" subgroup is silenced by its own
+// name.
+TEST_F(PluginWarningGroupTest, SubgroupSilencedByOwnName) {
+ unsigned ID = Diags.getCustomDiagID(DiagnosticsEngine::Warning, "reuse",
+ "example-plugin-loop");
+ EXPECT_FALSE(Diags.isIgnored(ID, SourceLocation()));
+
+ DiagOpts.Warnings = {"no-example-plugin-loop"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+ EXPECT_TRUE(Diags.isIgnored(ID, SourceLocation()));
+}
+
+// A subgroup is also silenced by its parent "<plugin>-plugin" group, even when
+// the flag is seen before the plugin registers the subgroup.
+TEST_F(PluginWarningGroupTest, SubgroupSilencedByParentGroup) {
+ DiagOpts.Warnings = {"no-example-plugin"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+
+ unsigned ID = Diags.getCustomDiagID(DiagnosticsEngine::Warning, "reuse",
+ "example-plugin-loop");
+ EXPECT_TRUE(Diags.isIgnored(ID, SourceLocation()));
+}
+
+// -Wno-<subgroup> does not affect a sibling subgroup of the same plugin.
+TEST_F(PluginWarningGroupTest, SiblingSubgroupUnaffected) {
+ unsigned Reuse = Diags.getCustomDiagID(DiagnosticsEngine::Warning, "reuse",
+ "example-plugin-loop");
+ unsigned Other = Diags.getCustomDiagID(DiagnosticsEngine::Warning, "other",
+ "example-plugin-cast");
+
+ DiagOpts.Warnings = {"no-example-plugin-loop"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+ EXPECT_TRUE(Diags.isIgnored(Reuse, SourceLocation()));
+ EXPECT_FALSE(Diags.isIgnored(Other, SourceLocation()));
+}
+
+// When several controlling groups are set, the most specific one wins:
+// -Wno-<parent> does not silence a subgroup a more specific -W<subgroup> keeps
+// on.
+TEST_F(PluginWarningGroupTest, MostSpecificGroupWins) {
+ DiagOpts.Warnings = {"no-example-plugin", "example-plugin-loop"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+
+ unsigned ID = Diags.getCustomDiagID(DiagnosticsEngine::Warning, "reuse",
+ "example-plugin-loop");
+ EXPECT_FALSE(Diags.isIgnored(ID, SourceLocation()));
+}
+
+// -Werror=plugin promotes every plugin diagnostic to an error.
+TEST_F(PluginWarningGroupTest, WerrorUmbrellaPromotesAllPlugins) {
+ DiagOpts.Warnings = {"error=plugin"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+
+ unsigned ID = Diags.getCustomDiagID(DiagnosticsEngine::Warning, "diag",
+ "example-plugin-loop");
+ EXPECT_EQ(Diags.getDiagnosticLevel(ID, SourceLocation()),
+ DiagnosticsEngine::Error);
+}
+
+// A plugin that (unconventionally) names a built-in group joins that group
+// directly, so the built-in flag controls it.
+TEST_F(PluginWarningGroupTest, BuiltinGroupNameJoinsBuiltinGroup) {
+ unsigned ID = Diags.getCustomDiagID(DiagnosticsEngine::Warning,
+ "unused thing", "unused");
+ DiagOpts.Warnings = {"no-unused"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+ EXPECT_TRUE(Diags.isIgnored(ID, SourceLocation()));
+}
+
+// A plugin remark is off by default and enabled by -R<group>; a -W flag on the
+// same group does not touch it, so warnings, errors and remarks can share a
+// group namespace.
+TEST_F(PluginWarningGroupTest, RemarkGroupControlledByROption) {
+ unsigned ID = Diags.getCustomDiagID(DiagnosticsEngine::Remark, "spent %0 ms",
+ "example-plugin-perf");
+ EXPECT_TRUE(Diags.isIgnored(ID, SourceLocation()));
+
+ // A -W flag on the group does not enable the remark.
+ DiagOpts.Warnings = {"example-plugin-perf"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+ EXPECT_TRUE(Diags.isIgnored(ID, SourceLocation()));
+
+ // -R<group> enables it.
+ DiagOpts.Remarks = {"example-plugin-perf"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+ EXPECT_FALSE(Diags.isIgnored(ID, SourceLocation()));
+}
+
+// A plugin error can be placed in a group too (for organization and the printed
+// "[-W<group>]"), but a group flag must not silence it: -W/-R control warnings
+// and remarks, never errors.
+TEST_F(PluginWarningGroupTest, ErrorDiagnosticJoinsGroupButIsNotSuppressible) {
+ unsigned ID = Diags.getCustomDiagID(DiagnosticsEngine::Error,
+ "unsupported '%0'", "example-plugin");
+ EXPECT_EQ(Diags.getDiagnosticIDs()->getWarningOptionForDiag(ID),
+ "example-plugin");
+
+ DiagOpts.Warnings = {"no-example-plugin"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+ EXPECT_FALSE(Diags.isIgnored(ID, SourceLocation()));
+ EXPECT_EQ(Diags.getDiagnosticLevel(ID, SourceLocation()),
+ DiagnosticsEngine::Error);
+}
+
+// Error protection also holds when the flag is seen before the plugin registers
+// the error (exercises the mapping path, not the member-exclusion path above).
+TEST_F(PluginWarningGroupTest,
+ ErrorNotSuppressibleWhenFlagPrecedesRegistration) {
+ DiagOpts.Warnings = {"no-example-plugin"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+
+ unsigned ID = Diags.getCustomDiagID(DiagnosticsEngine::Error,
+ "unsupported '%0'", "example-plugin");
+ EXPECT_EQ(Diags.getDiagnosticLevel(ID, SourceLocation()),
+ DiagnosticsEngine::Error);
+}
+
+// A -R flag does not touch a warning in the same group (flavor separation).
+TEST_F(PluginWarningGroupTest, RemarkFlagDoesNotAffectWarning) {
+ unsigned ID =
+ Diags.getCustomDiagID(DiagnosticsEngine::Warning, "w", "example-plugin");
+ DiagOpts.Remarks = {"no-example-plugin"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+ EXPECT_FALSE(Diags.isIgnored(ID, SourceLocation()));
+}
} // namespace
>From d4d516c6f6b2d670f8bc3c9ca658909a7bdd53b4 Mon Sep 17 00:00:00 2001
From: Vassil Vassilev <v.g.vassilev at gmail.com>
Date: Fri, 10 Jul 2026 17:03:17 +0000
Subject: [PATCH 2/9] [plugins] Add a per-plugin convenience and a
-Wuser-defined-warnings root
The previous commit let a custom diagnostic join a warning group by passing a
group string to getCustomDiagID(Level, Message, Group): the group may be an
existing built-in group (so a diagnostic can extend, say, -Wdeprecated) or a
runtime-registered group whose name is not known at build time. Keep that as the
general primitive and build a plugin convenience and a user-facing root on top,
so plugin diagnostics are controllable without every call site assembling group
names or leaking defaults into the top-level -W namespace.
Add getCustomPluginDiagID(Level, Message, PluginName, Subgroup), a thin wrapper
that derives the group "<PluginName>-plugin" (or "<PluginName>-plugin-<Subgroup>")
from the plugin's own name, the naming convention the -Wplugin umbrella is built
on. A plugin that instead wants to join an existing group calls
getCustomDiagID(Level, Message, Group) directly.
Classify every runtime plugin group under -Wuser-defined-warnings, the static
group that already collects diagnose_if warnings, making it the root over the
-Wplugin umbrella. -Wno-user-defined-warnings now silences every plugin
diagnostic and -Werror=user-defined-warnings promotes them, with a more specific
plugin-group flag still winning. A shared appendPluginGroupDiags helper collects
a group's members for both the umbrella and the root, and initCustomDiagMapping
seeds a plugin warning's default from the root before applying the more specific
plugin-group flags.
Migrate the PrintFunctionNames example and the unit tests to the convenience,
add tests for joining a built-in group and for the -Wuser-defined-warnings root,
and update the ClangPlugins docs, the release note and the lit test.
---
clang/docs/ReleaseNotes.md | 18 ++-
.../PrintFunctionNames/PrintFunctionNames.cpp | 21 +--
clang/include/clang/Basic/Diagnostic.h | 30 +++-
clang/include/clang/Basic/DiagnosticIDs.h | 65 +++-----
clang/lib/Basic/DiagnosticIDs.cpp | 133 ++++++++++++---
.../test/Frontend/plugin-diagnostic-group.cpp | 4 +
clang/unittests/Basic/DiagnosticTest.cpp | 152 ++++++++++++------
7 files changed, 288 insertions(+), 135 deletions(-)
diff --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md
index 0dc0b5ce1143a..13e49fad98c68 100644
--- a/clang/docs/ReleaseNotes.md
+++ b/clang/docs/ReleaseNotes.md
@@ -292,13 +292,17 @@ features cannot lower the translation-unit ABI level;
`-pedantic` or when that group is enabled explicitly, matching how the `_BitInt`
type itself is already handled.
-- Plugin diagnostics can now be placed in a warning group. `getCustomDiagID`
- accepts a group name, so a plugin's diagnostics can be controlled with `-W`
- and `-R` flags like built-in ones: by convention a plugin uses
- `<plugin>-plugin`, silenced with `-Wno-<plugin>-plugin` (or the `-Wno-plugin`
- umbrella over every loaded plugin), promoted with `-Werror=<plugin>-plugin`,
- and remarks controlled with `-R<plugin>-plugin`. Errors keep their severity.
- See [ClangPlugins](ClangPlugins.rst).
+- Custom diagnostics can now be placed in a warning group. A new
+ `getCustomDiagID(Level, Message, Group)` overload puts a diagnostic in any
+ warning group named by `Group`, whether an existing built-in group or a
+ runtime-registered one whose name is not known at build time, so it can be
+ controlled with `-W` and `-R` flags like a built-in diagnostic. Plugins get a
+ thin convenience, `getCustomPluginDiagID`, that derives the group from the
+ plugin's name as `<plugin>-plugin`: silenced with `-Wno-<plugin>-plugin` (the
+ `-Wno-plugin` umbrella over every loaded plugin, or `-Wno-user-defined-warnings`
+ over every runtime group), promoted with `-Werror=<plugin>-plugin`, and remarks
+ controlled with `-R<plugin>-plugin`. Errors keep their severity. See
+ [ClangPlugins](ClangPlugins.rst).
- Fixed bug in `-Wdocumentation` so that it correctly handles explicit
function template instantiations (#64087).
diff --git a/clang/examples/PrintFunctionNames/PrintFunctionNames.cpp b/clang/examples/PrintFunctionNames/PrintFunctionNames.cpp
index 9843c8b23a579..978592a03c29f 100644
--- a/clang/examples/PrintFunctionNames/PrintFunctionNames.cpp
+++ b/clang/examples/PrintFunctionNames/PrintFunctionNames.cpp
@@ -41,18 +41,21 @@ class PrintFunctionsConsumer : public ASTConsumer {
bool ErrorOnDecls)
: Instance(Instance), ParsedTemplates(ParsedTemplates) {
DiagnosticsEngine &Diags = Instance.getDiagnostics();
+ // The plugin is registered under "print-fns", so its group is
+ // "print-fns-plugin"; passing the plugin's own name keeps the diagnostics
+ // in that namespace automatically.
if (RemarkOnDecls)
- RemarkID = Diags.getCustomDiagID(DiagnosticsEngine::Remark,
- "saw top-level declaration '%0'",
- "print-fns-plugin");
+ RemarkID = Diags.getCustomPluginDiagID(DiagnosticsEngine::Remark,
+ "saw top-level declaration '%0'",
+ "print-fns");
if (WarnOnDecls)
- WarnID = Diags.getCustomDiagID(DiagnosticsEngine::Warning,
- "suspicious top-level declaration '%0'",
- "print-fns-plugin");
+ WarnID = Diags.getCustomPluginDiagID(
+ DiagnosticsEngine::Warning, "suspicious top-level declaration '%0'",
+ "print-fns");
if (ErrorOnDecls)
- ErrorID = Diags.getCustomDiagID(DiagnosticsEngine::Error,
- "forbidden top-level declaration '%0'",
- "print-fns-plugin");
+ ErrorID = Diags.getCustomPluginDiagID(
+ DiagnosticsEngine::Error, "forbidden top-level declaration '%0'",
+ "print-fns");
}
bool HandleTopLevelDecl(DeclGroupRef DG) override {
diff --git a/clang/include/clang/Basic/Diagnostic.h b/clang/include/clang/Basic/Diagnostic.h
index 8e48a76e15a40..cb7f0109cfbcf 100644
--- a/clang/include/clang/Basic/Diagnostic.h
+++ b/clang/include/clang/Basic/Diagnostic.h
@@ -919,9 +919,9 @@ class DiagnosticsEngine : public RefCountedBase<DiagnosticsEngine> {
/// and mapped to a unique DiagID.
template <unsigned N>
// A diagnostic created here belongs to no diagnostic group, so users cannot
- // control it with -W flags. Prefer the overload below that takes a group
- // name whenever the diagnostic should be user-controllable; uses of this
- // ungrouped form in Clang should only ever be reduced, not increased.
+ // control it with -W flags. Prefer the group-taking overload below whenever
+ // the diagnostic should be user-controllable; uses of this ungrouped form in
+ // Clang should only ever be reduced, not increased.
// [[deprecated("Pass a group name, or use a CustomDiagDesc instead of a "
// "Level")]]
unsigned getCustomDiagID(Level L, const char (&FormatString)[N]) {
@@ -929,10 +929,12 @@ class DiagnosticsEngine : public RefCountedBase<DiagnosticsEngine> {
StringRef(FormatString, N - 1));
}
- /// Compute the diagnostic ID for a plugin diagnostic in the runtime warning
- /// group \p Group (by convention "<plugin>-plugin"). Unlike the overload
- /// above, the diagnostic can be controlled by the user with -W<group> /
- /// -Wno-<group> / -Werror=<group>, like a built-in warning.
+ /// Compute the diagnostic ID for a custom diagnostic placed in the warning
+ /// group \p Group. The group may be an existing (TableGen) group, so the
+ /// diagnostic can join a built-in group such as -Wdeprecated, or a
+ /// runtime-registered group whose name is not known at build time. Either way
+ /// it is controllable with -W<group> / -Wno-<group> / -Werror=<group> (and
+ /// -R<group> for a remark), like a built-in warning.
template <unsigned N>
unsigned getCustomDiagID(Level L, const char (&FormatString)[N],
StringRef Group) {
@@ -940,6 +942,20 @@ class DiagnosticsEngine : public RefCountedBase<DiagnosticsEngine> {
StringRef(FormatString, N - 1), Group);
}
+ /// Convenience over the group-taking getCustomDiagID that places a plugin's
+ /// diagnostic in its own runtime group "<PluginName>-plugin" (or the subgroup
+ /// "<PluginName>-plugin-<Subgroup>"), the naming convention behind the
+ /// -Wplugin umbrella. A plugin that instead wants to join an existing group
+ /// can call getCustomDiagID(L, FormatString, Group) directly.
+ template <unsigned N>
+ unsigned getCustomPluginDiagID(Level L, const char (&FormatString)[N],
+ StringRef PluginName,
+ StringRef Subgroup = {}) {
+ return Diags->getCustomPluginDiagID((DiagnosticIDs::Level)L,
+ StringRef(FormatString, N - 1),
+ PluginName, Subgroup);
+ }
+
/// Converts a diagnostic argument (as an intptr_t) into the string
/// that represents it.
void ConvertArgToString(ArgumentKind Kind, intptr_t Val, StringRef Modifier,
diff --git a/clang/include/clang/Basic/DiagnosticIDs.h b/clang/include/clang/Basic/DiagnosticIDs.h
index 8a85e6815b281..2e6bc563fe6c8 100644
--- a/clang/include/clang/Basic/DiagnosticIDs.h
+++ b/clang/include/clang/Basic/DiagnosticIDs.h
@@ -329,6 +329,14 @@ class DiagnosticIDs : public RefCountedBase<DiagnosticIDs> {
G[Ctrl.size()] == '-';
}
+ /// Append to \p Diags the runtime plugin-group diagnostics of \p Flavor that
+ /// the plugin group named \p Ctrl controls (errors excluded, since -W/-R
+ /// group flags never remap an error). Returns whether \p Ctrl names a
+ /// registered plugin group. Shared by the "plugin" umbrella and the
+ /// -Wuser-defined-warnings root.
+ bool appendPluginGroupDiags(diag::Flavor Flavor, StringRef Ctrl,
+ SmallVectorImpl<diag::kind> &Diags) const;
+
public:
DiagnosticIDs();
~DiagnosticIDs();
@@ -380,46 +388,23 @@ class DiagnosticIDs : public RefCountedBase<DiagnosticIDs> {
}());
}
- /// Return an ID for a plugin diagnostic that belongs to the runtime warning
- /// group \p Group (by convention "<plugin>-plugin"). Unlike the two-argument
- /// form above, the resulting diagnostic *can* be controlled by the user with
- /// -W<group> / -Wno-<group> / -Werror=<group>, just like a built-in warning.
- unsigned getCustomDiagID(Level Level, StringRef Message, StringRef Group) {
- unsigned Class = CLASS_WARNING;
- diag::Severity Sev = diag::Severity::Warning;
- switch (Level) {
- case DiagnosticIDs::Level::Ignored:
- Sev = diag::Severity::Ignored;
- break;
- case DiagnosticIDs::Level::Note:
- Sev = diag::Severity::Fatal;
- Class = CLASS_NOTE;
- break;
- case DiagnosticIDs::Level::Remark:
- Sev = diag::Severity::Remark;
- Class = CLASS_REMARK;
- break;
- case DiagnosticIDs::Level::Warning:
- Sev = diag::Severity::Warning;
- break;
- case DiagnosticIDs::Level::Error:
- Sev = diag::Severity::Error;
- Class = CLASS_ERROR;
- break;
- case DiagnosticIDs::Level::Fatal:
- Sev = diag::Severity::Fatal;
- Class = CLASS_ERROR;
- break;
- }
- // If Group names a built-in (static) group, join it directly so it is
- // controlled like any other member of that group; otherwise it is a runtime
- // plugin group kept in the dynamic registry.
- std::optional<diag::Group> StaticGroup = getGroupForWarningOption(Group);
- return getCustomDiagID(CustomDiagDesc(
- Sev, std::string(Message), Class,
- /*ShowInSystemHeader=*/false, /*ShowInSystemMacro=*/false, StaticGroup,
- StaticGroup ? std::string() : std::string(Group)));
- }
+ /// Return an ID for a custom diagnostic placed in the warning group \p Group.
+ /// \p Group may name an existing (TableGen) group, so a caller can put a
+ /// custom diagnostic into a built-in group such as -Wdeprecated; or a
+ /// runtime-registered group whose name is not known at build time (e.g. a
+ /// plugin's). Either way the diagnostic participates in -W<group> /
+ /// -Wno-<group> / -Werror=<group> (and -R<group> for a remark), just like a
+ /// built-in warning. Two callers may share a group name; that simply groups
+ /// their diagnostics together.
+ unsigned getCustomDiagID(Level Level, StringRef Message, StringRef Group);
+
+ /// Convenience over getCustomDiagID(Level, Message, Group) that places a
+ /// plugin's diagnostic in its own runtime group "<PluginName>-plugin" (or the
+ /// subgroup "<PluginName>-plugin-<Subgroup>"), the naming convention that the
+ /// -Wplugin umbrella is built on. A plugin that instead wants to join an
+ /// existing group can call getCustomDiagID(Level, Message, Group) directly.
+ unsigned getCustomPluginDiagID(Level Level, StringRef Message,
+ StringRef PluginName, StringRef Subgroup = {});
/// Ensure a runtime plugin group named \p Name exists, so a -W flag can
/// target it before the plugin that owns it is loaded. Returns true if
diff --git a/clang/lib/Basic/DiagnosticIDs.cpp b/clang/lib/Basic/DiagnosticIDs.cpp
index 40c2a5f9cbb0f..4c3d1544f5560 100644
--- a/clang/lib/Basic/DiagnosticIDs.cpp
+++ b/clang/lib/Basic/DiagnosticIDs.cpp
@@ -371,6 +371,20 @@ void DiagnosticIDs::initCustomDiagMapping(DiagnosticMapping &Mapping,
diag::Flavor DiagFlavor = Diag.GetClass() == CLASS_REMARK
? diag::Flavor::Remark
: diag::Flavor::WarningOrError;
+ // -Wuser-defined-warnings is the least-specific control over every runtime
+ // plugin group: it is the static root the "plugin" umbrella nests under,
+ // so -Wno-user-defined-warnings / -Werror=user-defined-warnings reach
+ // plugin diagnostics too. Seed a warning's mapping from a flag on it; a
+ // more specific plugin-group flag below overrides. Remarks are not
+ // warnings and follow -R flags only, so they do not inherit it.
+ if (DiagFlavor == diag::Flavor::WarningOrError)
+ if (std::optional<diag::Group> UDW =
+ getGroupForWarningOption("user-defined-warnings")) {
+ auto Sev = static_cast<diag::Severity>(
+ GroupInfos[static_cast<size_t>(*UDW)].Severity);
+ if (Sev != diag::Severity())
+ Mapping.setSeverity(Sev);
+ }
const DynamicGroupInfo *Best = nullptr;
size_t BestLen = 0;
for (const auto &Entry : DynamicGroups)
@@ -487,6 +501,58 @@ unsigned DiagnosticIDs::getCustomDiagID(CustomDiagDesc Diag) {
return ID;
}
+unsigned DiagnosticIDs::getCustomDiagID(Level Level, StringRef Message,
+ StringRef Group) {
+ unsigned Class = CLASS_WARNING;
+ diag::Severity Sev = diag::Severity::Warning;
+ switch (Level) {
+ case DiagnosticIDs::Level::Ignored:
+ Sev = diag::Severity::Ignored;
+ break;
+ case DiagnosticIDs::Level::Note:
+ Sev = diag::Severity::Fatal;
+ Class = CLASS_NOTE;
+ break;
+ case DiagnosticIDs::Level::Remark:
+ Sev = diag::Severity::Remark;
+ Class = CLASS_REMARK;
+ break;
+ case DiagnosticIDs::Level::Warning:
+ Sev = diag::Severity::Warning;
+ break;
+ case DiagnosticIDs::Level::Error:
+ Sev = diag::Severity::Error;
+ Class = CLASS_ERROR;
+ break;
+ case DiagnosticIDs::Level::Fatal:
+ Sev = diag::Severity::Fatal;
+ Class = CLASS_ERROR;
+ break;
+ }
+ // If Group names a built-in (static) group, join it directly so the
+ // diagnostic is controlled like any other member of that group (e.g. a
+ // -Wdeprecated addition). Otherwise it is a runtime-registered group kept in
+ // the dynamic registry, whose name need not be known at build time.
+ std::optional<diag::Group> StaticGroup = getGroupForWarningOption(Group);
+ return getCustomDiagID(CustomDiagDesc(
+ Sev, std::string(Message), Class,
+ /*ShowInSystemHeader=*/false, /*ShowInSystemMacro=*/false, StaticGroup,
+ StaticGroup ? std::string() : std::string(Group)));
+}
+
+unsigned DiagnosticIDs::getCustomPluginDiagID(Level Level, StringRef Message,
+ StringRef PluginName,
+ StringRef Subgroup) {
+ // A thin convention over getCustomDiagID: place the diagnostic in the
+ // plugin's own runtime group "<plugin>-plugin[-<sub>]" that the -Wplugin
+ // umbrella controls. A plugin that wants to join an existing group instead
+ // calls getCustomDiagID(Level, Message, Group) with that group's name.
+ std::string Group = (Twine(PluginName) + "-plugin").str();
+ if (!Subgroup.empty())
+ Group = (Twine(Group) + "-" + Subgroup).str();
+ return getCustomDiagID(Level, Message, Group);
+}
+
bool DiagnosticIDs::isWarningOrExtension(unsigned DiagID) const {
return DiagID < diag::DIAG_UPPER_LIMIT
? getDiagClass(DiagID) != CLASS_ERROR
@@ -848,6 +914,36 @@ static bool getDiagnosticsInGroup(diag::Flavor Flavor,
return NotFound;
}
+bool DiagnosticIDs::appendPluginGroupDiags(
+ diag::Flavor Flavor, StringRef Ctrl,
+ SmallVectorImpl<diag::kind> &Diags) const {
+ // Add the members of every registered plugin group that \p Ctrl controls and
+ // that match the requested flavor -- a plugin group may hold warnings, errors
+ // (both WarningOrError) and remarks. Returns whether any group matched, i.e.
+ // whether \p Ctrl names a known (registered) plugin group.
+ bool Any = false;
+ for (const auto &Entry : DynamicGroups)
+ if (pluginGroupControls(Ctrl, Entry.first())) {
+ Any = true;
+ if (!CustomDiagInfo)
+ continue;
+ for (unsigned ID : Entry.second.Members) {
+ DiagnosticIDs::Class Class =
+ CustomDiagInfo->getDescription(ID).GetClass();
+ // Errors are not controllable by -W/-R group flags; leave them out so
+ // the mapping is never asked to downgrade an error.
+ if (Class == CLASS_ERROR)
+ continue;
+ diag::Flavor MemberFlavor = Class == CLASS_REMARK
+ ? diag::Flavor::Remark
+ : diag::Flavor::WarningOrError;
+ if (MemberFlavor == Flavor)
+ Diags.push_back(ID);
+ }
+ }
+ return Any;
+}
+
bool
DiagnosticIDs::getDiagnosticsInGroup(diag::Flavor Flavor, StringRef Group,
SmallVectorImpl<diag::kind> &Diags) const {
@@ -855,39 +951,24 @@ DiagnosticIDs::getDiagnosticsInGroup(diag::Flavor Flavor, StringRef Group,
if (CustomDiagInfo)
llvm::copy(CustomDiagInfo->getDiagsInGroup(*G),
std::back_inserter(Diags));
+ // -Wuser-defined-warnings is the static root every runtime plugin group
+ // nests under, so a flag on it reaches plugin diagnostics too, just like the
+ // "plugin" umbrella does. Add their members before descending the static
+ // subgroups.
+ if (Group == "user-defined-warnings")
+ appendPluginGroupDiags(Flavor, "plugin", Diags);
return ::getDiagnosticsInGroup(Flavor,
&OptionTable[static_cast<unsigned>(*G)],
Diags, CustomDiagInfo.get());
}
// A runtime plugin group. "plugin" is the umbrella over every plugin group;
- // "<plugin>-plugin" also controls its "<plugin>-plugin-<sub>" subgroups. Add
- // the members of every registered group this name controls that match the
- // requested flavor -- a plugin group may hold warnings, errors (both
- // WarningOrError) and remarks. The group counts as "known" once it (or a
- // subgroup) is registered -- by a -W/-R flag or by a plugin -- so it is not
- // reported as an unknown option; genuine typos are caught later by
- // reportUnclaimedPluginGroups.
+ // "<plugin>-plugin" also controls its "<plugin>-plugin-<sub>" subgroups. The
+ // group counts as "known" once it (or a subgroup) is registered -- by a -W/-R
+ // flag or by a plugin -- so it is not reported as an unknown option; genuine
+ // typos are caught later by reportUnclaimedPluginGroups.
if (isPluginGroupName(Group)) {
bool Known = DynamicGroups.contains(Group);
- for (const auto &Entry : DynamicGroups)
- if (pluginGroupControls(Group, Entry.first())) {
- Known = true;
- for (unsigned ID : Entry.second.Members) {
- if (!CustomDiagInfo)
- continue;
- DiagnosticIDs::Class Class =
- CustomDiagInfo->getDescription(ID).GetClass();
- // Errors are not controllable by -W/-R group flags; leave them out so
- // the mapping is never asked to downgrade an error.
- if (Class == CLASS_ERROR)
- continue;
- diag::Flavor MemberFlavor = Class == CLASS_REMARK
- ? diag::Flavor::Remark
- : diag::Flavor::WarningOrError;
- if (MemberFlavor == Flavor)
- Diags.push_back(ID);
- }
- }
+ Known |= appendPluginGroupDiags(Flavor, Group, Diags);
return !Known;
}
return true;
diff --git a/clang/test/Frontend/plugin-diagnostic-group.cpp b/clang/test/Frontend/plugin-diagnostic-group.cpp
index cc9a4909fba5d..53005342ba035 100644
--- a/clang/test/Frontend/plugin-diagnostic-group.cpp
+++ b/clang/test/Frontend/plugin-diagnostic-group.cpp
@@ -18,6 +18,10 @@
// RUN: %clang_cc1 -load %llvmshlibdir/PrintFunctionNames%pluginext \
// RUN: -plugin print-fns -plugin-arg-print-fns -warn-decls \
// RUN: -Wno-plugin %t/simple.cpp 2>&1 | FileCheck --check-prefix=SILENT %s
+// A warning is silenced by the -Wno-user-defined-warnings root over every group.
+// RUN: %clang_cc1 -load %llvmshlibdir/PrintFunctionNames%pluginext \
+// RUN: -plugin print-fns -plugin-arg-print-fns -warn-decls \
+// RUN: -Wno-user-defined-warnings %t/simple.cpp 2>&1 | FileCheck --check-prefix=SILENT %s
// A warning is promoted by -Werror=<group>.
// RUN: not %clang_cc1 -load %llvmshlibdir/PrintFunctionNames%pluginext \
// RUN: -plugin print-fns -plugin-arg-print-fns -warn-decls \
diff --git a/clang/unittests/Basic/DiagnosticTest.cpp b/clang/unittests/Basic/DiagnosticTest.cpp
index 54b724bd05cd7..2fd9eaa946b6e 100644
--- a/clang/unittests/Basic/DiagnosticTest.cpp
+++ b/clang/unittests/Basic/DiagnosticTest.cpp
@@ -505,9 +505,8 @@ class PluginWarningGroupTest : public testing::Test {
// -Wno-<group> silences a plugin diagnostic tagged with that group.
TEST_F(PluginWarningGroupTest, WnoSuppressesPluginGroup) {
- unsigned ID =
- Diags.getCustomDiagID(DiagnosticsEngine::Warning,
- "plugin reused an AST node", "example-plugin");
+ unsigned ID = Diags.getCustomPluginDiagID(
+ DiagnosticsEngine::Warning, "plugin reused an AST node", "example");
EXPECT_FALSE(Diags.isIgnored(ID, SourceLocation()));
DiagOpts.Warnings = {"no-example-plugin"};
@@ -524,9 +523,8 @@ TEST_F(PluginWarningGroupTest, WnoBeforePluginRegistersGroup) {
ProcessWarningOptions(Diags, DiagOpts, *FS);
EXPECT_THAT(diags(), IsEmpty());
- unsigned ID =
- Diags.getCustomDiagID(DiagnosticsEngine::Warning,
- "plugin reused an AST node", "example-plugin");
+ unsigned ID = Diags.getCustomPluginDiagID(
+ DiagnosticsEngine::Warning, "plugin reused an AST node", "example");
EXPECT_TRUE(Diags.isIgnored(ID, SourceLocation()));
}
@@ -535,18 +533,18 @@ TEST_F(PluginWarningGroupTest, WnoPluginUmbrellaSuppressesEveryPluginGroup) {
DiagOpts.Warnings = {"no-plugin"};
ProcessWarningOptions(Diags, DiagOpts, *FS);
- unsigned First = Diags.getCustomDiagID(DiagnosticsEngine::Warning,
- "first diag", "example-plugin");
- unsigned Other = Diags.getCustomDiagID(DiagnosticsEngine::Warning,
- "other diag", "othertool-plugin");
+ unsigned First = Diags.getCustomPluginDiagID(DiagnosticsEngine::Warning,
+ "first diag", "example");
+ unsigned Other = Diags.getCustomPluginDiagID(DiagnosticsEngine::Warning,
+ "other diag", "othertool");
EXPECT_TRUE(Diags.isIgnored(First, SourceLocation()));
EXPECT_TRUE(Diags.isIgnored(Other, SourceLocation()));
}
// -Wno-<group> only touches diagnostics in that group.
TEST_F(PluginWarningGroupTest, LeavesOtherDiagnosticsAlone) {
- unsigned Grouped = Diags.getCustomDiagID(DiagnosticsEngine::Warning,
- "grouped", "example-plugin");
+ unsigned Grouped = Diags.getCustomPluginDiagID(DiagnosticsEngine::Warning,
+ "grouped", "example");
unsigned Ungrouped =
Diags.getCustomDiagID(DiagnosticsEngine::Warning, "ungrouped");
@@ -558,8 +556,8 @@ TEST_F(PluginWarningGroupTest, LeavesOtherDiagnosticsAlone) {
// -Werror=<group> promotes a plugin warning to an error.
TEST_F(PluginWarningGroupTest, WerrorPromotesPluginGroup) {
- unsigned ID = Diags.getCustomDiagID(DiagnosticsEngine::Warning, "plugin diag",
- "example-plugin");
+ unsigned ID = Diags.getCustomPluginDiagID(DiagnosticsEngine::Warning,
+ "plugin diag", "example");
DiagOpts.Warnings = {"error=example-plugin"};
ProcessWarningOptions(Diags, DiagOpts, *FS);
EXPECT_EQ(Diags.getDiagnosticLevel(ID, SourceLocation()),
@@ -568,8 +566,8 @@ TEST_F(PluginWarningGroupTest, WerrorPromotesPluginGroup) {
// Global -Werror promotes a plugin warning like any other warning.
TEST_F(PluginWarningGroupTest, GlobalWerrorPromotesPluginWarning) {
- unsigned ID = Diags.getCustomDiagID(DiagnosticsEngine::Warning, "plugin diag",
- "example-plugin");
+ unsigned ID = Diags.getCustomPluginDiagID(DiagnosticsEngine::Warning,
+ "plugin diag", "example");
DiagOpts.Warnings = {"error"};
ProcessWarningOptions(Diags, DiagOpts, *FS);
EXPECT_EQ(Diags.getDiagnosticLevel(ID, SourceLocation()),
@@ -593,7 +591,7 @@ TEST_F(PluginWarningGroupTest, UnclaimedPluginGroupReportedAfterLoad) {
TEST_F(PluginWarningGroupTest, ClaimedPluginGroupNotReported) {
DiagOpts.Warnings = {"no-example-plugin"};
ProcessWarningOptions(Diags, DiagOpts, *FS);
- Diags.getCustomDiagID(DiagnosticsEngine::Warning, "diag", "example-plugin");
+ Diags.getCustomPluginDiagID(DiagnosticsEngine::Warning, "diag", "example");
Diags.getDiagnosticIDs()->reportUnclaimedPluginGroups(Diags);
EXPECT_THAT(diags(), IsEmpty());
@@ -602,8 +600,8 @@ TEST_F(PluginWarningGroupTest, ClaimedPluginGroupNotReported) {
// A diagnostic in a "<plugin>-plugin-<sub>" subgroup is silenced by its own
// name.
TEST_F(PluginWarningGroupTest, SubgroupSilencedByOwnName) {
- unsigned ID = Diags.getCustomDiagID(DiagnosticsEngine::Warning, "reuse",
- "example-plugin-loop");
+ unsigned ID = Diags.getCustomPluginDiagID(DiagnosticsEngine::Warning, "reuse",
+ "example", "loop");
EXPECT_FALSE(Diags.isIgnored(ID, SourceLocation()));
DiagOpts.Warnings = {"no-example-plugin-loop"};
@@ -617,17 +615,17 @@ TEST_F(PluginWarningGroupTest, SubgroupSilencedByParentGroup) {
DiagOpts.Warnings = {"no-example-plugin"};
ProcessWarningOptions(Diags, DiagOpts, *FS);
- unsigned ID = Diags.getCustomDiagID(DiagnosticsEngine::Warning, "reuse",
- "example-plugin-loop");
+ unsigned ID = Diags.getCustomPluginDiagID(DiagnosticsEngine::Warning, "reuse",
+ "example", "loop");
EXPECT_TRUE(Diags.isIgnored(ID, SourceLocation()));
}
// -Wno-<subgroup> does not affect a sibling subgroup of the same plugin.
TEST_F(PluginWarningGroupTest, SiblingSubgroupUnaffected) {
- unsigned Reuse = Diags.getCustomDiagID(DiagnosticsEngine::Warning, "reuse",
- "example-plugin-loop");
- unsigned Other = Diags.getCustomDiagID(DiagnosticsEngine::Warning, "other",
- "example-plugin-cast");
+ unsigned Reuse = Diags.getCustomPluginDiagID(DiagnosticsEngine::Warning,
+ "reuse", "example", "loop");
+ unsigned Other = Diags.getCustomPluginDiagID(DiagnosticsEngine::Warning,
+ "other", "example", "cast");
DiagOpts.Warnings = {"no-example-plugin-loop"};
ProcessWarningOptions(Diags, DiagOpts, *FS);
@@ -642,8 +640,8 @@ TEST_F(PluginWarningGroupTest, MostSpecificGroupWins) {
DiagOpts.Warnings = {"no-example-plugin", "example-plugin-loop"};
ProcessWarningOptions(Diags, DiagOpts, *FS);
- unsigned ID = Diags.getCustomDiagID(DiagnosticsEngine::Warning, "reuse",
- "example-plugin-loop");
+ unsigned ID = Diags.getCustomPluginDiagID(DiagnosticsEngine::Warning, "reuse",
+ "example", "loop");
EXPECT_FALSE(Diags.isIgnored(ID, SourceLocation()));
}
@@ -652,28 +650,18 @@ TEST_F(PluginWarningGroupTest, WerrorUmbrellaPromotesAllPlugins) {
DiagOpts.Warnings = {"error=plugin"};
ProcessWarningOptions(Diags, DiagOpts, *FS);
- unsigned ID = Diags.getCustomDiagID(DiagnosticsEngine::Warning, "diag",
- "example-plugin-loop");
+ unsigned ID = Diags.getCustomPluginDiagID(DiagnosticsEngine::Warning, "diag",
+ "example", "loop");
EXPECT_EQ(Diags.getDiagnosticLevel(ID, SourceLocation()),
DiagnosticsEngine::Error);
}
-// A plugin that (unconventionally) names a built-in group joins that group
-// directly, so the built-in flag controls it.
-TEST_F(PluginWarningGroupTest, BuiltinGroupNameJoinsBuiltinGroup) {
- unsigned ID = Diags.getCustomDiagID(DiagnosticsEngine::Warning,
- "unused thing", "unused");
- DiagOpts.Warnings = {"no-unused"};
- ProcessWarningOptions(Diags, DiagOpts, *FS);
- EXPECT_TRUE(Diags.isIgnored(ID, SourceLocation()));
-}
-
// A plugin remark is off by default and enabled by -R<group>; a -W flag on the
// same group does not touch it, so warnings, errors and remarks can share a
// group namespace.
TEST_F(PluginWarningGroupTest, RemarkGroupControlledByROption) {
- unsigned ID = Diags.getCustomDiagID(DiagnosticsEngine::Remark, "spent %0 ms",
- "example-plugin-perf");
+ unsigned ID = Diags.getCustomPluginDiagID(DiagnosticsEngine::Remark,
+ "spent %0 ms", "example", "perf");
EXPECT_TRUE(Diags.isIgnored(ID, SourceLocation()));
// A -W flag on the group does not enable the remark.
@@ -691,8 +679,8 @@ TEST_F(PluginWarningGroupTest, RemarkGroupControlledByROption) {
// "[-W<group>]"), but a group flag must not silence it: -W/-R control warnings
// and remarks, never errors.
TEST_F(PluginWarningGroupTest, ErrorDiagnosticJoinsGroupButIsNotSuppressible) {
- unsigned ID = Diags.getCustomDiagID(DiagnosticsEngine::Error,
- "unsupported '%0'", "example-plugin");
+ unsigned ID = Diags.getCustomPluginDiagID(DiagnosticsEngine::Error,
+ "unsupported '%0'", "example");
EXPECT_EQ(Diags.getDiagnosticIDs()->getWarningOptionForDiag(ID),
"example-plugin");
@@ -710,8 +698,8 @@ TEST_F(PluginWarningGroupTest,
DiagOpts.Warnings = {"no-example-plugin"};
ProcessWarningOptions(Diags, DiagOpts, *FS);
- unsigned ID = Diags.getCustomDiagID(DiagnosticsEngine::Error,
- "unsupported '%0'", "example-plugin");
+ unsigned ID = Diags.getCustomPluginDiagID(DiagnosticsEngine::Error,
+ "unsupported '%0'", "example");
EXPECT_EQ(Diags.getDiagnosticLevel(ID, SourceLocation()),
DiagnosticsEngine::Error);
}
@@ -719,9 +707,81 @@ TEST_F(PluginWarningGroupTest,
// A -R flag does not touch a warning in the same group (flavor separation).
TEST_F(PluginWarningGroupTest, RemarkFlagDoesNotAffectWarning) {
unsigned ID =
- Diags.getCustomDiagID(DiagnosticsEngine::Warning, "w", "example-plugin");
+ Diags.getCustomPluginDiagID(DiagnosticsEngine::Warning, "w", "example");
DiagOpts.Remarks = {"no-example-plugin"};
ProcessWarningOptions(Diags, DiagOpts, *FS);
EXPECT_FALSE(Diags.isIgnored(ID, SourceLocation()));
}
+
+// A custom diagnostic may join an existing (built-in) group by naming it, and
+// is then controlled by that group's flag like any other member.
+TEST_F(PluginWarningGroupTest, CustomDiagJoinsBuiltinGroup) {
+ unsigned ID = Diags.getCustomDiagID(DiagnosticsEngine::Warning,
+ "custom deprecation", "deprecated");
+ EXPECT_FALSE(Diags.isIgnored(ID, SourceLocation()));
+
+ DiagOpts.Warnings = {"no-deprecated"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+ EXPECT_TRUE(Diags.isIgnored(ID, SourceLocation()));
+}
+
+// -Werror on a built-in group also promotes a custom diagnostic that joined it.
+TEST_F(PluginWarningGroupTest, WerrorPromotesCustomDiagInBuiltinGroup) {
+ unsigned ID = Diags.getCustomDiagID(DiagnosticsEngine::Warning,
+ "custom deprecation", "deprecated");
+ DiagOpts.Warnings = {"error=deprecated"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+ EXPECT_EQ(Diags.getDiagnosticLevel(ID, SourceLocation()),
+ DiagnosticsEngine::Error);
+}
+
+// -Wno-user-defined-warnings is the root over every runtime plugin group, so it
+// silences a plugin diagnostic even though the flag names neither the plugin
+// group nor the -Wplugin umbrella. The flag is parsed before the plugin loads.
+TEST_F(PluginWarningGroupTest, UserDefinedWarningsSilencesPluginGroup) {
+ DiagOpts.Warnings = {"no-user-defined-warnings"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+ EXPECT_THAT(diags(), IsEmpty());
+
+ unsigned ID = Diags.getCustomPluginDiagID(DiagnosticsEngine::Warning,
+ "plugin warning", "example");
+ EXPECT_TRUE(Diags.isIgnored(ID, SourceLocation()));
+}
+
+// A more specific plugin-group flag wins over -Wuser-defined-warnings: the root
+// silences the plugin, but re-enabling the plugin's own group brings it back.
+TEST_F(PluginWarningGroupTest, PluginGroupOverridesUserDefinedWarningsRoot) {
+ DiagOpts.Warnings = {"no-user-defined-warnings", "example-plugin"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+
+ unsigned ID = Diags.getCustomPluginDiagID(DiagnosticsEngine::Warning,
+ "plugin warning", "example");
+ EXPECT_FALSE(Diags.isIgnored(ID, SourceLocation()));
+}
+
+// -Werror=user-defined-warnings promotes a plugin warning through the root.
+TEST_F(PluginWarningGroupTest, UserDefinedWarningsPromotesPluginGroup) {
+ DiagOpts.Warnings = {"error=user-defined-warnings"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+
+ unsigned ID = Diags.getCustomPluginDiagID(DiagnosticsEngine::Warning,
+ "plugin warning", "example");
+ EXPECT_EQ(Diags.getDiagnosticLevel(ID, SourceLocation()),
+ DiagnosticsEngine::Error);
+}
+
+// The root controls warnings only: -Rno-user-defined-warnings does not exist as
+// a remark root, and a -W root flag must not touch a grouped remark.
+TEST_F(PluginWarningGroupTest, UserDefinedWarningsRootLeavesRemarksAlone) {
+ DiagOpts.Warnings = {"no-user-defined-warnings"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+
+ unsigned ID = Diags.getCustomPluginDiagID(DiagnosticsEngine::Remark,
+ "plugin remark", "example");
+ // A grouped remark is off by default regardless; enabling it via -R shows the
+ // -W root did not force it off in a way -R cannot revert.
+ DiagOpts.Remarks = {"example-plugin"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+ EXPECT_FALSE(Diags.isIgnored(ID, SourceLocation()));
+}
} // namespace
>From 7d519cbd332aedec7d04ed9d3aefdc31b7f78072 Mon Sep 17 00:00:00 2001
From: Vassil Vassilev <v.g.vassilev at gmail.com>
Date: Mon, 20 Jul 2026 12:48:24 +0000
Subject: [PATCH 3/9] [Basic] Give custom diagnostics a stable ID for SARIF
A custom diagnostic's numeric ID is assigned in registration order, so it is not
reproducible across runs or across plugin load orders. SARIF keys its ruleId on
getStableID, which for a custom diagnostic fell back to that numeric ID, leaving
plugin diagnostics with an unstable ruleId that the tool consuming the SARIF
cannot work around.
Let a custom diagnostic carry a build-independent stable ID. CustomDiagDesc
gains a StableID field, and getStableID returns it when set, still falling back
to the numeric ID otherwise (documented as best-effort). getCustomDiagID and the
plugin convenience getCustomPluginDiagID grow a trailing StableID parameter that
forwards it, so a plugin -- in particular one that generates its diagnostics
from TableGen, whose enum names are stable -- supplies that name as its ruleId.
The PrintFunctionNames example sets a stable ID on its warning, exercised by a
new SARIF lit test that checks the ruleId, plus a DiagnosticTest unit test for
getStableID with and without a stable ID.
---
.../PrintFunctionNames/PrintFunctionNames.cpp | 7 ++++-
clang/include/clang/Basic/Diagnostic.h | 17 +++++++-----
clang/include/clang/Basic/DiagnosticIDs.h | 26 +++++++++++++++----
clang/lib/Basic/DiagnosticIDs.cpp | 20 +++++++++-----
.../test/Frontend/plugin-diagnostic-sarif.cpp | 15 +++++++++++
clang/unittests/Basic/DiagnosticTest.cpp | 15 +++++++++++
6 files changed, 80 insertions(+), 20 deletions(-)
create mode 100644 clang/test/Frontend/plugin-diagnostic-sarif.cpp
diff --git a/clang/examples/PrintFunctionNames/PrintFunctionNames.cpp b/clang/examples/PrintFunctionNames/PrintFunctionNames.cpp
index 978592a03c29f..0f8bbc7450e7f 100644
--- a/clang/examples/PrintFunctionNames/PrintFunctionNames.cpp
+++ b/clang/examples/PrintFunctionNames/PrintFunctionNames.cpp
@@ -49,9 +49,14 @@ class PrintFunctionsConsumer : public ASTConsumer {
"saw top-level declaration '%0'",
"print-fns");
if (WarnOnDecls)
+ // The trailing stable ID becomes this diagnostic's SARIF ruleId. It is
+ // independent of registration order, unlike the numeric ID SARIF would
+ // otherwise fall back to; a plugin generating its diagnostics from
+ // TableGen would pass the generated enum name here.
WarnID = Diags.getCustomPluginDiagID(
DiagnosticsEngine::Warning, "suspicious top-level declaration '%0'",
- "print-fns");
+ "print-fns", /*Subgroup=*/"",
+ /*StableID=*/"print_fns_suspicious_decl");
if (ErrorOnDecls)
ErrorID = Diags.getCustomPluginDiagID(
DiagnosticsEngine::Error, "forbidden top-level declaration '%0'",
diff --git a/clang/include/clang/Basic/Diagnostic.h b/clang/include/clang/Basic/Diagnostic.h
index cb7f0109cfbcf..9405bf40cec7a 100644
--- a/clang/include/clang/Basic/Diagnostic.h
+++ b/clang/include/clang/Basic/Diagnostic.h
@@ -934,26 +934,29 @@ class DiagnosticsEngine : public RefCountedBase<DiagnosticsEngine> {
/// diagnostic can join a built-in group such as -Wdeprecated, or a
/// runtime-registered group whose name is not known at build time. Either way
/// it is controllable with -W<group> / -Wno-<group> / -Werror=<group> (and
- /// -R<group> for a remark), like a built-in warning.
+ /// -R<group> for a remark), like a built-in warning. \p StableID, when given,
+ /// is a build-independent identifier used as the diagnostic's SARIF ruleId.
template <unsigned N>
unsigned getCustomDiagID(Level L, const char (&FormatString)[N],
- StringRef Group) {
+ StringRef Group, StringRef StableID = {}) {
return Diags->getCustomDiagID((DiagnosticIDs::Level)L,
- StringRef(FormatString, N - 1), Group);
+ StringRef(FormatString, N - 1), Group,
+ StableID);
}
/// Convenience over the group-taking getCustomDiagID that places a plugin's
/// diagnostic in its own runtime group "<PluginName>-plugin" (or the subgroup
/// "<PluginName>-plugin-<Subgroup>"), the naming convention behind the
/// -Wplugin umbrella. A plugin that instead wants to join an existing group
- /// can call getCustomDiagID(L, FormatString, Group) directly.
+ /// can call getCustomDiagID(L, FormatString, Group) directly. \p StableID,
+ /// when given, is used as the diagnostic's SARIF ruleId.
template <unsigned N>
unsigned getCustomPluginDiagID(Level L, const char (&FormatString)[N],
- StringRef PluginName,
- StringRef Subgroup = {}) {
+ StringRef PluginName, StringRef Subgroup = {},
+ StringRef StableID = {}) {
return Diags->getCustomPluginDiagID((DiagnosticIDs::Level)L,
StringRef(FormatString, N - 1),
- PluginName, Subgroup);
+ PluginName, Subgroup, StableID);
}
/// Converts a diagnostic argument (as an intptr_t) into the string
diff --git a/clang/include/clang/Basic/DiagnosticIDs.h b/clang/include/clang/Basic/DiagnosticIDs.h
index 2e6bc563fe6c8..e6d29e4430781 100644
--- a/clang/include/clang/Basic/DiagnosticIDs.h
+++ b/clang/include/clang/Basic/DiagnosticIDs.h
@@ -220,12 +220,19 @@ class DiagnosticIDs : public RefCountedBase<DiagnosticIDs> {
// "<plugin>-plugin"), empty when the diagnostic is not part of one. Unlike
// Group above, this is not a member of the static (TableGen'd) group table.
std::string DynGroupName;
+ // Stable, build-independent identifier for this diagnostic, empty when none
+ // was given. A custom diagnostic's numeric ID is assigned in registration
+ // order and so is not reproducible across runs; a caller that generates its
+ // diagnostics from TableGen (whose enum names are stable) supplies that name
+ // here so tools keying on identity -- notably SARIF's ruleId -- stay stable.
+ std::string StableID;
auto get_as_tuple() const {
return std::tuple(DefaultSeverity, DiagClass, ShowInSystemHeader,
ShowInSystemMacro, HasGroup, Group,
std::string_view{Description},
- std::string_view{DynGroupName});
+ std::string_view{DynGroupName},
+ std::string_view{StableID});
}
public:
@@ -234,13 +241,13 @@ class DiagnosticIDs : public RefCountedBase<DiagnosticIDs> {
bool ShowInSystemHeader = false,
bool ShowInSystemMacro = false,
std::optional<diag::Group> Group = std::nullopt,
- std::string DynGroupName = {})
+ std::string DynGroupName = {}, std::string StableID = {})
: DefaultSeverity(static_cast<unsigned>(DefaultSeverity)),
DiagClass(Class), ShowInSystemHeader(ShowInSystemHeader),
ShowInSystemMacro(ShowInSystemMacro), HasGroup(Group != std::nullopt),
Group(Group.value_or(diag::Group{})),
Description(std::move(Description)),
- DynGroupName(std::move(DynGroupName)) {}
+ DynGroupName(std::move(DynGroupName)), StableID(std::move(StableID)) {}
std::optional<diag::Group> GetGroup() const {
if (HasGroup)
@@ -250,6 +257,8 @@ class DiagnosticIDs : public RefCountedBase<DiagnosticIDs> {
StringRef GetDynGroup() const { return DynGroupName; }
+ StringRef GetStableID() const { return StableID; }
+
diag::Severity GetDefaultSeverity() const {
return static_cast<diag::Severity>(DefaultSeverity);
}
@@ -396,15 +405,22 @@ class DiagnosticIDs : public RefCountedBase<DiagnosticIDs> {
/// -Wno-<group> / -Werror=<group> (and -R<group> for a remark), just like a
/// built-in warning. Two callers may share a group name; that simply groups
/// their diagnostics together.
- unsigned getCustomDiagID(Level Level, StringRef Message, StringRef Group);
+ ///
+ /// \p StableID, when non-empty, is a build-independent identifier for the
+ /// diagnostic (see CustomDiagDesc::StableID) used as its SARIF ruleId; leave
+ /// it empty to fall back to the diagnostic's numeric (non-reproducible) ID.
+ unsigned getCustomDiagID(Level Level, StringRef Message, StringRef Group,
+ StringRef StableID = {});
/// Convenience over getCustomDiagID(Level, Message, Group) that places a
/// plugin's diagnostic in its own runtime group "<PluginName>-plugin" (or the
/// subgroup "<PluginName>-plugin-<Subgroup>"), the naming convention that the
/// -Wplugin umbrella is built on. A plugin that instead wants to join an
/// existing group can call getCustomDiagID(Level, Message, Group) directly.
+ /// \p StableID is forwarded as the diagnostic's SARIF ruleId (see above).
unsigned getCustomPluginDiagID(Level Level, StringRef Message,
- StringRef PluginName, StringRef Subgroup = {});
+ StringRef PluginName, StringRef Subgroup = {},
+ StringRef StableID = {});
/// Ensure a runtime plugin group named \p Name exists, so a -W flag can
/// target it before the plugin that owns it is loaded. Returns true if
diff --git a/clang/lib/Basic/DiagnosticIDs.cpp b/clang/lib/Basic/DiagnosticIDs.cpp
index 4c3d1544f5560..fa298391ce9c1 100644
--- a/clang/lib/Basic/DiagnosticIDs.cpp
+++ b/clang/lib/Basic/DiagnosticIDs.cpp
@@ -502,7 +502,7 @@ unsigned DiagnosticIDs::getCustomDiagID(CustomDiagDesc Diag) {
}
unsigned DiagnosticIDs::getCustomDiagID(Level Level, StringRef Message,
- StringRef Group) {
+ StringRef Group, StringRef StableID) {
unsigned Class = CLASS_WARNING;
diag::Severity Sev = diag::Severity::Warning;
switch (Level) {
@@ -537,12 +537,13 @@ unsigned DiagnosticIDs::getCustomDiagID(Level Level, StringRef Message,
return getCustomDiagID(CustomDiagDesc(
Sev, std::string(Message), Class,
/*ShowInSystemHeader=*/false, /*ShowInSystemMacro=*/false, StaticGroup,
- StaticGroup ? std::string() : std::string(Group)));
+ StaticGroup ? std::string() : std::string(Group), std::string(StableID)));
}
unsigned DiagnosticIDs::getCustomPluginDiagID(Level Level, StringRef Message,
StringRef PluginName,
- StringRef Subgroup) {
+ StringRef Subgroup,
+ StringRef StableID) {
// A thin convention over getCustomDiagID: place the diagnostic in the
// plugin's own runtime group "<plugin>-plugin[-<sub>]" that the -Wplugin
// umbrella controls. A plugin that wants to join an existing group instead
@@ -550,7 +551,7 @@ unsigned DiagnosticIDs::getCustomPluginDiagID(Level Level, StringRef Message,
std::string Group = (Twine(PluginName) + "-plugin").str();
if (!Subgroup.empty())
Group = (Twine(Group) + "-" + Subgroup).str();
- return getCustomDiagID(Level, Message, Group);
+ return getCustomDiagID(Level, Message, Group, StableID);
}
bool DiagnosticIDs::isWarningOrExtension(unsigned DiagID) const {
@@ -598,9 +599,14 @@ 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?
- // If we have to go through every custom diagnostic and add a stable ID, we
- // should instead just go replace them all with declared diagnostics.
+ // A custom diagnostic that was given a stable ID (e.g. a plugin diagnostic
+ // generated from TableGen, whose enum name does not change across runs) uses
+ // it verbatim, so a tool keying on identity such as SARIF's ruleId is stable.
+ // Otherwise fall back to the numeric ID, which is assigned in registration
+ // order and is therefore not reproducible -- documented as best-effort.
+ if (StringRef SID = CustomDiagInfo->getDescription(DiagID).GetStableID();
+ !SID.empty())
+ return SID.str();
return std::to_string(DiagID);
}
diff --git a/clang/test/Frontend/plugin-diagnostic-sarif.cpp b/clang/test/Frontend/plugin-diagnostic-sarif.cpp
new file mode 100644
index 0000000000000..fba313671a050
--- /dev/null
+++ b/clang/test/Frontend/plugin-diagnostic-sarif.cpp
@@ -0,0 +1,15 @@
+// Tests that a plugin diagnostic given a stable ID reports it as its SARIF
+// ruleId. The stable ID is independent of registration order, unlike the
+// numeric ID SARIF falls back to, so a tool keying on diagnostic identity stays
+// stable across runs.
+
+// RUN: %clang_cc1 -load %llvmshlibdir/PrintFunctionNames%pluginext \
+// RUN: -plugin print-fns -plugin-arg-print-fns -warn-decls \
+// RUN: -fdiagnostics-format sarif %s 2>&1 | FileCheck %s
+
+// REQUIRES: plugins, examples
+
+void f();
+
+// The stable ID the plugin passed to getCustomPluginDiagID is the rule id.
+// CHECK: "ruleId": "print_fns_suspicious_decl"
diff --git a/clang/unittests/Basic/DiagnosticTest.cpp b/clang/unittests/Basic/DiagnosticTest.cpp
index 2fd9eaa946b6e..05e942105e423 100644
--- a/clang/unittests/Basic/DiagnosticTest.cpp
+++ b/clang/unittests/Basic/DiagnosticTest.cpp
@@ -770,6 +770,21 @@ TEST_F(PluginWarningGroupTest, UserDefinedWarningsPromotesPluginGroup) {
DiagnosticsEngine::Error);
}
+// A custom diagnostic given a stable ID reports it verbatim (used as the SARIF
+// ruleId); without one it falls back to the numeric, non-reproducible ID.
+TEST_F(PluginWarningGroupTest, StableIDReportedForSarifRuleId) {
+ const DiagnosticIDs &DiagIDs = *Diags.getDiagnosticIDs();
+
+ unsigned WithID = Diags.getCustomPluginDiagID(
+ DiagnosticsEngine::Warning, "reused an AST node", "example",
+ /*Subgroup=*/"", /*StableID=*/"example_plugin_reused_node");
+ EXPECT_EQ(DiagIDs.getStableID(WithID), "example_plugin_reused_node");
+
+ unsigned Without = Diags.getCustomPluginDiagID(DiagnosticsEngine::Warning,
+ "other", "example");
+ EXPECT_EQ(DiagIDs.getStableID(Without), std::to_string(Without));
+}
+
// The root controls warnings only: -Rno-user-defined-warnings does not exist as
// a remark root, and a -W root flag must not touch a grouped remark.
TEST_F(PluginWarningGroupTest, UserDefinedWarningsRootLeavesRemarksAlone) {
>From da57361c10f58416293d124265d42a4b0ad2d7db Mon Sep 17 00:00:00 2001
From: Vassil Vassilev <v.g.vassilev at gmail.com>
Date: Mon, 20 Jul 2026 14:01:32 +0000
Subject: [PATCH 4/9] [plugins] Add a batch diagnostic-registration helper and
tighten tests
Give DiagnosticsEngine a PluginDiagnostic descriptor and getCustomPluginDiagIDs,
which registers a whole table of a plugin's diagnostics in one call: each entry
lands in the plugin's "<plugin>-plugin[-<subgroup>]" group and gets a stable
SARIF ruleId "<plugin>_<record>", both derived from the names so the plugin
spells neither. This is the runtime equivalent of #including a clang-tblgen'd
diagnostics table, and is the path a plugin such as clad would take to move its
diagnostics under its own TableGen with no churn to the group names, the -W
control, or the ruleIds users and tooling already see.
Rework the PrintFunctionNames example to use it: the diagnostics are now an
X-macro table (standing in for a generated .inc), from which a type-safe
enumeration and the PluginDiagnostic table are both derived, and a single
getCustomPluginDiagIDs call registers them. Document the graduation path in
ClangPlugins.rst.
Tighten the tests while here: add unit coverage for the batch helper (a subgroup
entry and the derived ruleIds) and for the -Wuser-defined-warnings root reaching
an already-registered plugin member (the getDiagnosticsInGroup path, distinct
from the mapping seed the other root tests exercise); and drop the redundant
-Wno-plugin lit run, whose behavior with a single plugin loaded is identical to
-Wno-<group> and whose umbrella logic is already unit-tested.
---
.../PrintFunctionNames/PrintFunctionNames.cpp | 95 ++++++++++++-------
clang/include/clang/Basic/Diagnostic.h | 27 ++++++
clang/lib/Basic/Diagnostic.cpp | 25 +++++
.../test/Frontend/plugin-diagnostic-group.cpp | 11 ++-
clang/unittests/Basic/DiagnosticTest.cpp | 59 ++++++++++++
5 files changed, 176 insertions(+), 41 deletions(-)
diff --git a/clang/examples/PrintFunctionNames/PrintFunctionNames.cpp b/clang/examples/PrintFunctionNames/PrintFunctionNames.cpp
index 0f8bbc7450e7f..637e83e6ae113 100644
--- a/clang/examples/PrintFunctionNames/PrintFunctionNames.cpp
+++ b/clang/examples/PrintFunctionNames/PrintFunctionNames.cpp
@@ -22,45 +22,65 @@ using namespace clang;
namespace {
+// A plugin that wants to organize its diagnostics the way Clang organizes its
+// own would write them as TableGen records in a .td file and run
+// clang-tblgen -gen-clang-diags-defs
+// to generate a table of DIAG(...) rows it #includes. To keep the example
+// self-contained we hand-write the equivalent table with an X-macro; a real
+// plugin would generate the PRINT_FNS_DIAGS body instead. Each row is one
+// diagnostic: a record name (which becomes both the enumerator below and, with
+// the plugin name, the SARIF ruleId), a level, a message, and an optional
+// subgroup of the plugin's "print-fns-plugin" group.
+#define PRINT_FNS_DIAGS(DIAG) \
+ DIAG(suspicious_decl, Warning, "suspicious top-level declaration '%0'", "") \
+ DIAG(forbidden_decl, Error, "forbidden top-level declaration '%0'", "") \
+ DIAG(saw_decl, Remark, "saw top-level declaration '%0'", "")
+
+// The stable enumeration, generated from the table's first column, gives the
+// plugin type-safe names for its diagnostics just like clang's diag::warn_*.
+namespace print_fns {
+enum Kind {
+#define DIAG(ENUM, LEVEL, MSG, SUBGROUP) ENUM,
+ PRINT_FNS_DIAGS(DIAG)
+#undef DIAG
+};
+} // namespace print_fns
+
+// The same table as PluginDiagnostic rows, ready for one-shot registration.
+static const DiagnosticsEngine::PluginDiagnostic PrintFnsDiagTable[] = {
+#define DIAG(ENUM, LEVEL, MSG, SUBGROUP) \
+ {#ENUM, DiagnosticsEngine::LEVEL, MSG, SUBGROUP},
+ PRINT_FNS_DIAGS(DIAG)
+#undef DIAG
+};
+
class PrintFunctionsConsumer : public ASTConsumer {
CompilerInstance &Instance;
std::set<std::string> ParsedTemplates;
- // Diagnostics in the plugin's own "print-fns-plugin" group, or 0 if the
- // corresponding argument was not passed. Registering the IDs up front (rather
- // than lazily on first use) makes them members of the group before the source
- // is parsed, so a `#pragma clang diagnostic` referring to the group can be
- // applied to them.
- unsigned WarnID = 0;
- unsigned RemarkID = 0;
- unsigned ErrorID = 0;
+ bool WarnOnDecls;
+ bool RemarkOnDecls;
+ bool ErrorOnDecls;
+ // Diagnostic IDs assigned by getCustomPluginDiagIDs, indexed by
+ // print_fns::Kind. Registering the whole table up front (rather than lazily
+ // on first use) makes every diagnostic a member of the "print-fns-plugin"
+ // group before the source is parsed, so a `#pragma clang diagnostic` naming
+ // the group can be applied to them.
+ llvm::SmallVector<unsigned> DiagIDs;
public:
PrintFunctionsConsumer(CompilerInstance &Instance,
std::set<std::string> ParsedTemplates,
bool WarnOnDecls, bool RemarkOnDecls,
bool ErrorOnDecls)
- : Instance(Instance), ParsedTemplates(ParsedTemplates) {
- DiagnosticsEngine &Diags = Instance.getDiagnostics();
- // The plugin is registered under "print-fns", so its group is
- // "print-fns-plugin"; passing the plugin's own name keeps the diagnostics
- // in that namespace automatically.
- if (RemarkOnDecls)
- RemarkID = Diags.getCustomPluginDiagID(DiagnosticsEngine::Remark,
- "saw top-level declaration '%0'",
- "print-fns");
- if (WarnOnDecls)
- // The trailing stable ID becomes this diagnostic's SARIF ruleId. It is
- // independent of registration order, unlike the numeric ID SARIF would
- // otherwise fall back to; a plugin generating its diagnostics from
- // TableGen would pass the generated enum name here.
- WarnID = Diags.getCustomPluginDiagID(
- DiagnosticsEngine::Warning, "suspicious top-level declaration '%0'",
- "print-fns", /*Subgroup=*/"",
- /*StableID=*/"print_fns_suspicious_decl");
- if (ErrorOnDecls)
- ErrorID = Diags.getCustomPluginDiagID(
- DiagnosticsEngine::Error, "forbidden top-level declaration '%0'",
- "print-fns");
+ : Instance(Instance), ParsedTemplates(ParsedTemplates),
+ WarnOnDecls(WarnOnDecls), RemarkOnDecls(RemarkOnDecls),
+ ErrorOnDecls(ErrorOnDecls) {
+ // One call registers the whole table. Each diagnostic lands in the plugin's
+ // "print-fns-plugin" group (derived from the plugin name) with a stable
+ // SARIF ruleId "print_fns_<record>" (likewise derived), so the plugin
+ // spells neither the group nor the id.
+ DiagIDs = Instance.getDiagnostics().getCustomPluginDiagIDs("print-fns",
+ PrintFnsDiagTable);
}
bool HandleTopLevelDecl(DeclGroupRef DG) override {
@@ -74,12 +94,15 @@ class PrintFunctionsConsumer : public ASTConsumer {
// with -Rno-print-fns-plugin (or the -Wplugin / -Wno-plugin umbrella),
// while the error keeps its severity -- group flags never silence errors.
DiagnosticsEngine &Diags = Instance.getDiagnostics();
- if (RemarkID)
- Diags.Report(ND->getLocation(), RemarkID) << ND->getNameAsString();
- if (WarnID)
- Diags.Report(ND->getLocation(), WarnID) << ND->getNameAsString();
- if (ErrorID)
- Diags.Report(ND->getLocation(), ErrorID) << ND->getNameAsString();
+ if (RemarkOnDecls)
+ Diags.Report(ND->getLocation(), DiagIDs[print_fns::saw_decl])
+ << ND->getNameAsString();
+ if (WarnOnDecls)
+ Diags.Report(ND->getLocation(), DiagIDs[print_fns::suspicious_decl])
+ << ND->getNameAsString();
+ if (ErrorOnDecls)
+ Diags.Report(ND->getLocation(), DiagIDs[print_fns::forbidden_decl])
+ << ND->getNameAsString();
}
return true;
diff --git a/clang/include/clang/Basic/Diagnostic.h b/clang/include/clang/Basic/Diagnostic.h
index 9405bf40cec7a..04bcf275de907 100644
--- a/clang/include/clang/Basic/Diagnostic.h
+++ b/clang/include/clang/Basic/Diagnostic.h
@@ -959,6 +959,33 @@ class DiagnosticsEngine : public RefCountedBase<DiagnosticsEngine> {
PluginName, Subgroup, StableID);
}
+ /// One entry in a plugin's diagnostic table, mirroring a single TableGen
+ /// Diagnostic record. A plugin registers a table of these with
+ /// getCustomPluginDiagIDs to organize its diagnostics the way Clang organizes
+ /// its own .td-generated ones: grouped, controllable, and stably identified.
+ struct PluginDiagnostic {
+ /// Record name. Becomes the plugin's enumerator and, together with the
+ /// plugin name, the SARIF ruleId "<plugin>_<name>".
+ StringRef Name;
+ Level DiagLevel;
+ /// Diagnostic format string, e.g. "unexpected token %0".
+ StringRef Message;
+ /// Subgroup within the plugin's "<plugin>-plugin" group, or empty for the
+ /// group itself.
+ StringRef Subgroup;
+ };
+
+ /// Register a whole plugin diagnostic table at once: a convenience over
+ /// getCustomPluginDiagID that places every entry in the plugin's own group
+ /// "<PluginName>-plugin[-<Subgroup>]" and derives each diagnostic's stable
+ /// SARIF ruleId from the plugin and record names, so the plugin spells
+ /// neither the group nor the id. This is the runtime equivalent of #including
+ /// a clang-tblgen'd diagnostics table. Returns the assigned IDs in table
+ /// order, so a caller can index them by a parallel enumeration.
+ SmallVector<unsigned>
+ getCustomPluginDiagIDs(StringRef PluginName,
+ ArrayRef<PluginDiagnostic> Table);
+
/// Converts a diagnostic argument (as an intptr_t) into the string
/// that represents it.
void ConvertArgToString(ArgumentKind Kind, intptr_t Val, StringRef Modifier,
diff --git a/clang/lib/Basic/Diagnostic.cpp b/clang/lib/Basic/Diagnostic.cpp
index b2a5b39aaddac..31f488aa2d994 100644
--- a/clang/lib/Basic/Diagnostic.cpp
+++ b/clang/lib/Basic/Diagnostic.cpp
@@ -430,6 +430,31 @@ bool DiagnosticsEngine::setSeverityForGroup(diag::Flavor Flavor,
Map, Loc);
}
+SmallVector<unsigned>
+DiagnosticsEngine::getCustomPluginDiagIDs(StringRef PluginName,
+ ArrayRef<PluginDiagnostic> Table) {
+ // Derive a stable SARIF ruleId from the plugin name and the record name,
+ // mirroring how the group "<plugin>-plugin" is derived from the plugin name,
+ // so the plugin supplies neither. Non-identifier characters (e.g. the dash in
+ // "print-fns") are mapped to '_' so the id is a portable token.
+ auto sanitize = [](StringRef S, std::string &Out) {
+ for (char C : S)
+ Out += llvm::isAlnum(C) ? C : '_';
+ };
+ SmallVector<unsigned> IDs;
+ IDs.reserve(Table.size());
+ for (const PluginDiagnostic &D : Table) {
+ std::string StableID;
+ sanitize(PluginName, StableID);
+ StableID += '_';
+ sanitize(D.Name, StableID);
+ IDs.push_back(Diags->getCustomPluginDiagID((DiagnosticIDs::Level)D.DiagLevel,
+ D.Message, PluginName, D.Subgroup,
+ StableID));
+ }
+ return IDs;
+}
+
bool DiagnosticsEngine::setDiagnosticGroupWarningAsError(StringRef Group,
bool Enabled) {
// If we are enabling this feature, just set the diagnostic mappings to map to
diff --git a/clang/test/Frontend/plugin-diagnostic-group.cpp b/clang/test/Frontend/plugin-diagnostic-group.cpp
index 53005342ba035..9f7b17157b30f 100644
--- a/clang/test/Frontend/plugin-diagnostic-group.cpp
+++ b/clang/test/Frontend/plugin-diagnostic-group.cpp
@@ -11,14 +11,15 @@
// RUN: %clang_cc1 -load %llvmshlibdir/PrintFunctionNames%pluginext \
// RUN: -plugin print-fns -plugin-arg-print-fns -warn-decls %t/simple.cpp 2>&1 \
// RUN: | FileCheck --check-prefix=WARN %s
-// A warning is silenced by -Wno-<group> and by the -Wno-plugin umbrella.
+// A warning is silenced by -Wno-<group>. (The -Wplugin umbrella and the
+// per-group / most-specific-wins mechanics are covered by DiagnosticTest unit
+// tests; with a single plugin loaded -Wno-plugin behaves identically to
+// -Wno-<group>, so it is not re-tested here.)
// RUN: %clang_cc1 -load %llvmshlibdir/PrintFunctionNames%pluginext \
// RUN: -plugin print-fns -plugin-arg-print-fns -warn-decls \
// RUN: -Wno-print-fns-plugin %t/simple.cpp 2>&1 | FileCheck --check-prefix=SILENT %s
-// RUN: %clang_cc1 -load %llvmshlibdir/PrintFunctionNames%pluginext \
-// RUN: -plugin print-fns -plugin-arg-print-fns -warn-decls \
-// RUN: -Wno-plugin %t/simple.cpp 2>&1 | FileCheck --check-prefix=SILENT %s
-// A warning is silenced by the -Wno-user-defined-warnings root over every group.
+// A warning is silenced by the -Wno-user-defined-warnings root over every group
+// (a static group reaching runtime plugin members -- a distinct code path).
// RUN: %clang_cc1 -load %llvmshlibdir/PrintFunctionNames%pluginext \
// RUN: -plugin print-fns -plugin-arg-print-fns -warn-decls \
// RUN: -Wno-user-defined-warnings %t/simple.cpp 2>&1 | FileCheck --check-prefix=SILENT %s
diff --git a/clang/unittests/Basic/DiagnosticTest.cpp b/clang/unittests/Basic/DiagnosticTest.cpp
index 05e942105e423..e4d188f64b6d6 100644
--- a/clang/unittests/Basic/DiagnosticTest.cpp
+++ b/clang/unittests/Basic/DiagnosticTest.cpp
@@ -770,6 +770,21 @@ TEST_F(PluginWarningGroupTest, UserDefinedWarningsPromotesPluginGroup) {
DiagnosticsEngine::Error);
}
+// The other -Wuser-defined-warnings tests process the flag before the plugin
+// registers (the command-line ordering), which routes through the mapping seed.
+// When the plugin diagnostic is already registered, the flag instead reaches it
+// by enumerating the root's runtime members -- the distinct getDiagnosticsInGroup
+// path where the static "user-defined-warnings" group collects plugin members.
+TEST_F(PluginWarningGroupTest, UserDefinedWarningsRootReachesRegisteredMember) {
+ unsigned ID = Diags.getCustomPluginDiagID(DiagnosticsEngine::Warning,
+ "plugin warning", "example");
+ EXPECT_FALSE(Diags.isIgnored(ID, SourceLocation()));
+
+ DiagOpts.Warnings = {"no-user-defined-warnings"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+ EXPECT_TRUE(Diags.isIgnored(ID, SourceLocation()));
+}
+
// A custom diagnostic given a stable ID reports it verbatim (used as the SARIF
// ruleId); without one it falls back to the numeric, non-reproducible ID.
TEST_F(PluginWarningGroupTest, StableIDReportedForSarifRuleId) {
@@ -799,4 +814,48 @@ TEST_F(PluginWarningGroupTest, UserDefinedWarningsRootLeavesRemarksAlone) {
ProcessWarningOptions(Diags, DiagOpts, *FS);
EXPECT_FALSE(Diags.isIgnored(ID, SourceLocation()));
}
+
+// getCustomPluginDiagIDs registers a whole table at once: each entry lands in
+// the plugin's group, controllable together, and gets a stable ruleId derived
+// from the plugin and record names (so a -Wno-<plugin>-plugin silences the
+// warnings, the error keeps its severity, and the ruleIds are as derived).
+TEST_F(PluginWarningGroupTest, PluginDiagTableRegistration) {
+ const DiagnosticIDs &DiagIDs = *Diags.getDiagnosticIDs();
+ static const DiagnosticsEngine::PluginDiagnostic Table[] = {
+ {"suspicious_decl", DiagnosticsEngine::Warning, "suspicious %0", ""},
+ {"forbidden_decl", DiagnosticsEngine::Error, "forbidden %0", ""},
+ // A row with a subgroup lands in "my-plugin-plugin-loop".
+ {"loop_warn", DiagnosticsEngine::Warning, "loop %0", "loop"},
+ };
+ llvm::SmallVector<unsigned> IDs =
+ Diags.getCustomPluginDiagIDs("my-plugin", Table);
+ ASSERT_EQ(IDs.size(), 3u);
+
+ // ruleIds are "<sanitized-plugin>_<record>"; the dash in "my-plugin" maps to
+ // '_'. The subgroup does not affect the ruleId.
+ EXPECT_EQ(DiagIDs.getStableID(IDs[0]), "my_plugin_suspicious_decl");
+ EXPECT_EQ(DiagIDs.getStableID(IDs[1]), "my_plugin_forbidden_decl");
+ EXPECT_EQ(DiagIDs.getStableID(IDs[2]), "my_plugin_loop_warn");
+
+ // The table shares the "my-plugin-plugin" group: -Wno silences both warnings
+ // (the subgroup entry too, since the group controls its subgroups); the error
+ // keeps its severity.
+ DiagOpts.Warnings = {"no-my-plugin-plugin"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+ EXPECT_TRUE(Diags.isIgnored(IDs[0], SourceLocation()));
+ EXPECT_TRUE(Diags.isIgnored(IDs[2], SourceLocation()));
+ EXPECT_EQ(Diags.getDiagnosticLevel(IDs[1], SourceLocation()),
+ DiagnosticsEngine::Error);
+
+ // The subgroup entry is also reachable by its own subgroup flag, proving the
+ // helper threaded the Subgroup field through to the group name.
+ DiagnosticsEngine Fresh{DiagnosticIDs::create(), DiagOpts};
+ llvm::SmallVector<unsigned> FreshIDs =
+ Fresh.getCustomPluginDiagIDs("my-plugin", Table);
+ DiagnosticOptions SubOpts;
+ SubOpts.Warnings = {"no-my-plugin-plugin-loop"};
+ ProcessWarningOptions(Fresh, SubOpts, *FS);
+ EXPECT_FALSE(Fresh.isIgnored(FreshIDs[0], SourceLocation()));
+ EXPECT_TRUE(Fresh.isIgnored(FreshIDs[2], SourceLocation()));
+}
} // namespace
>From 80b4e1194dbe540ff7a026028410f62518e195d8 Mon Sep 17 00:00:00 2001
From: Vassil Vassilev <v.g.vassilev at gmail.com>
Date: Mon, 20 Jul 2026 13:00:26 +0000
Subject: [PATCH 5/9] [CodeGen] Route grouped backend-plugin diagnostics
through their warning group
Every unrecognized IR-layer DiagnosticInfo -- including all plugin diagnostics,
whose kinds come from getNextAvailablePluginDiagnosticKind() -- reaches the
default case of BackendConsumer::DiagnosticHandlerImpl and collapses into the
single -Wbackend-plugin umbrella. A backend plugin therefore cannot give its
diagnostics per-plugin control the way a frontend plugin can, because a generic
DiagnosticInfo carries no group.
Add DiagnosticInfo::getWarningGroup(), the IR-layer companion to
getNextAvailablePluginDiagnosticKind(): that gives a plugin a runtime
diagnostic kind, this gives it a runtime group. When a backend diagnostic names
a group, route it through getCustomDiagID(Level, "%0", Group), the same
runtime-group primitive frontend plugins use, so a backend plugin naming its
group "<plugin>-plugin" gets -W<plugin>-plugin control that nests under -Wplugin
and -Wuser-defined-warnings. A diagnostic that names no group falls under
-Wbackend-plugin as before, so existing plugins are unchanged.
Add a DiagnosticInfoTest for the accessor's default and an override seen through
a DiagnosticInfo base reference, the path the bridge takes. An end-to-end test
needs a pass plugin that emits a grouped diagnostic and is left as a follow-up.
---
clang/lib/CodeGen/CodeGenAction.cpp | 28 ++++++++++++++++++++++--
llvm/include/llvm/IR/DiagnosticInfo.h | 9 ++++++++
llvm/unittests/IR/DiagnosticInfoTest.cpp | 25 +++++++++++++++++++++
3 files changed, 60 insertions(+), 2 deletions(-)
diff --git a/clang/lib/CodeGen/CodeGenAction.cpp b/clang/lib/CodeGen/CodeGenAction.cpp
index 6911cab379fdc..3875f1f4b1061 100644
--- a/clang/lib/CodeGen/CodeGenAction.cpp
+++ b/clang/lib/CodeGen/CodeGenAction.cpp
@@ -925,8 +925,32 @@ void BackendConsumer::DiagnosticHandlerImpl(const DiagnosticInfo &DI) {
MisExpectDiagHandler(cast<DiagnosticInfoMisExpect>(DI));
return;
default:
- // Plugin IDs are not bound to any value as they are set dynamically.
- ComputeDiagRemarkID(Severity, backend_plugin, DiagID);
+ // Plugin IDs are not bound to any value as they are set dynamically. If the
+ // plugin put its diagnostic in a warning group, route it through that
+ // runtime group so it is user-controllable with -W<group> exactly like a
+ // frontend plugin diagnostic (nesting under -Wplugin and
+ // -Wuser-defined-warnings). Otherwise it falls under the coarse
+ // -Wbackend-plugin umbrella, as before.
+ if (StringRef Group = DI.getWarningGroup(); !Group.empty()) {
+ DiagnosticsEngine::Level DiagLevel = DiagnosticsEngine::Warning;
+ switch (Severity) {
+ case llvm::DS_Error:
+ DiagLevel = DiagnosticsEngine::Error;
+ break;
+ case llvm::DS_Warning:
+ DiagLevel = DiagnosticsEngine::Warning;
+ break;
+ case llvm::DS_Remark:
+ DiagLevel = DiagnosticsEngine::Remark;
+ break;
+ case llvm::DS_Note:
+ DiagLevel = DiagnosticsEngine::Note;
+ break;
+ }
+ DiagID = Diags.getCustomDiagID(DiagLevel, "%0", Group);
+ } else {
+ ComputeDiagRemarkID(Severity, backend_plugin, DiagID);
+ }
break;
}
std::string MsgStorage;
diff --git a/llvm/include/llvm/IR/DiagnosticInfo.h b/llvm/include/llvm/IR/DiagnosticInfo.h
index da62b62bd8c74..a18ebb315c063 100644
--- a/llvm/include/llvm/IR/DiagnosticInfo.h
+++ b/llvm/include/llvm/IR/DiagnosticInfo.h
@@ -129,6 +129,15 @@ class LLVM_ABI DiagnosticInfo {
/* DiagnosticKind */ int getKind() const { return Kind; }
DiagnosticSeverity getSeverity() const { return Severity; }
+ /// The name of the warning group this diagnostic belongs to, or empty if it
+ /// is not in a user-controllable group. A plugin (its kind obtained from
+ /// getNextAvailablePluginDiagnosticKind) can override this so its backend
+ /// diagnostic is controlled with -W<group> like a frontend plugin's, instead
+ /// of only through the coarse -Wbackend-plugin umbrella. The frontend resolves
+ /// the name against its own diagnostic-group registry; by convention it is
+ /// "<plugin>-plugin", which nests under -Wplugin and -Wuser-defined-warnings.
+ virtual StringRef getWarningGroup() const { return StringRef(); }
+
/// Print using the given \p DP a user-friendly message.
/// This is the default message that will be printed to the user.
/// It is used when the frontend does not directly take advantage
diff --git a/llvm/unittests/IR/DiagnosticInfoTest.cpp b/llvm/unittests/IR/DiagnosticInfoTest.cpp
index 9726e2fcf76ce..bcff17037f104 100644
--- a/llvm/unittests/IR/DiagnosticInfoTest.cpp
+++ b/llvm/unittests/IR/DiagnosticInfoTest.cpp
@@ -33,4 +33,29 @@ TEST(DiagnosticInfoTest, DebugMetadataKindsMatchClassof) {
EXPECT_FALSE(isa<DiagnosticInfoDebugMetadataVersion>(InvalidInfo));
}
+// A plugin diagnostic can override getWarningGroup() so the frontend routes it
+// into a user-controllable warning group. The base default is empty, and the
+// override is visible through a DiagnosticInfo base reference -- the path the
+// clang backend bridge takes.
+TEST(DiagnosticInfoTest, WarningGroup) {
+ LLVMContext C;
+ Module M("M", C);
+
+ DiagnosticInfoDebugMetadataVersion Version(M, 1);
+ EXPECT_TRUE(
+ static_cast<const DiagnosticInfo &>(Version).getWarningGroup().empty());
+
+ class GroupedDiag : public DiagnosticInfo {
+ public:
+ GroupedDiag()
+ : DiagnosticInfo(getNextAvailablePluginDiagnosticKind(), DS_Warning) {}
+ void print(DiagnosticPrinter &) const override {}
+ StringRef getWarningGroup() const override { return "example-plugin"; }
+ };
+
+ GroupedDiag Grouped;
+ const DiagnosticInfo &Base = Grouped;
+ EXPECT_EQ(Base.getWarningGroup(), "example-plugin");
+}
+
} // end anonymous namespace
>From 5dc3b6674b0f59b0e6d1d94c47013dac0096e27f Mon Sep 17 00:00:00 2001
From: Vassil Vassilev <v.g.vassilev at gmail.com>
Date: Mon, 20 Jul 2026 14:38:31 +0000
Subject: [PATCH 6/9] [plugins] Auto-scope ungrouped plugin diagnostics into
the plugin's group
The plugin diagnostic-group machinery only helps a plugin that reaches for the
grouped API; a plugin that calls the ungrouped getCustomDiagID(Level, Message)
still produces a warning no -W flag can reach, which is the problem the groups
exist to solve. Nothing stops that, so the invariant "a plugin's diagnostics are
user-controllable" rests on the plugin author's discipline.
Make it hold by construction. DiagnosticsEngine gains a PluginDiagnosticScope
RAII that records the plugin currently registering diagnostics, and the frontend
brackets each plugin's ParseArgs and CreateASTConsumer with one (the add-plugin
loop and the main-plugin paths, the latter keyed on FrontendOpts.ActionName).
While a scope is active, a warning or remark created through the ungrouped
getCustomDiagID(Level, FormatString) overload is redirected into the plugin's
"<plugin>-plugin" group, so it becomes controllable exactly as if the plugin had
called getCustomPluginDiagID. An error is left untouched -- errors are not
controllable by group flags, so there is nothing to scope and existing plugin
error reporting is unchanged. Outside any scope the overload is unchanged.
Add DiagnosticTest coverage for the scope's nesting and restore behavior, the
auto-scoping of an ungrouped warning, the pass-through of an ungrouped error,
and that the overload is inert outside a scope.
---
clang/include/clang/Basic/Diagnostic.h | 37 ++++++++++++
clang/lib/Frontend/FrontendAction.cpp | 19 +++++-
.../ExecuteCompilerInvocation.cpp | 6 ++
clang/unittests/Basic/DiagnosticTest.cpp | 59 +++++++++++++++++++
4 files changed, 120 insertions(+), 1 deletion(-)
diff --git a/clang/include/clang/Basic/Diagnostic.h b/clang/include/clang/Basic/Diagnostic.h
index 04bcf275de907..9b522118a9697 100644
--- a/clang/include/clang/Basic/Diagnostic.h
+++ b/clang/include/clang/Basic/Diagnostic.h
@@ -559,6 +559,11 @@ class DiagnosticsEngine : public RefCountedBase<DiagnosticsEngine> {
/// Number of errors reported
unsigned NumErrors;
+ /// Name of the plugin whose diagnostics are currently being registered, set
+ /// by a PluginDiagnosticScope; empty when no plugin is registering. Used to
+ /// auto-scope ungrouped plugin warnings and remarks into "<plugin>-plugin".
+ std::string ActivePluginName;
+
/// A function pointer that converts an opaque diagnostic
/// argument to a strings.
///
@@ -925,6 +930,14 @@ class DiagnosticsEngine : public RefCountedBase<DiagnosticsEngine> {
// [[deprecated("Pass a group name, or use a CustomDiagDesc instead of a "
// "Level")]]
unsigned getCustomDiagID(Level L, const char (&FormatString)[N]) {
+ // While a plugin is registering diagnostics (a PluginDiagnosticScope is
+ // active), a warning or remark created through this ungrouped overload
+ // would be uncontrollable by the user -- the very problem plugin groups
+ // exist to solve. Place it in the plugin's own "<plugin>-plugin" group
+ // instead, so it behaves like one registered through getCustomPluginDiagID.
+ // An error is not controllable by group flags, so it is left untouched.
+ if (!ActivePluginName.empty() && (L == Warning || L == Remark))
+ return getCustomPluginDiagID(L, FormatString, ActivePluginName);
return Diags->getCustomDiagID((DiagnosticIDs::Level)L,
StringRef(FormatString, N - 1));
}
@@ -986,6 +999,30 @@ class DiagnosticsEngine : public RefCountedBase<DiagnosticsEngine> {
getCustomPluginDiagIDs(StringRef PluginName,
ArrayRef<PluginDiagnostic> Table);
+ /// The plugin whose diagnostics are currently being registered, or empty when
+ /// none is (see PluginDiagnosticScope).
+ StringRef getActivePluginName() const { return ActivePluginName; }
+
+ /// While an instance is alive, a warning or remark registered through the
+ /// ungrouped getCustomDiagID(Level, FormatString) overload is redirected into
+ /// \p PluginName's "<plugin>-plugin" group, so a plugin cannot accidentally
+ /// create a diagnostic no -W flag can reach. The frontend brackets each
+ /// plugin's ParseArgs and CreateASTConsumer with one of these. Scopes nest;
+ /// the innermost active name wins, and the previous name is restored on exit.
+ class PluginDiagnosticScope {
+ DiagnosticsEngine &Diags;
+ std::string Saved;
+
+ public:
+ PluginDiagnosticScope(DiagnosticsEngine &Diags, StringRef PluginName)
+ : Diags(Diags), Saved(std::move(Diags.ActivePluginName)) {
+ Diags.ActivePluginName = PluginName.str();
+ }
+ ~PluginDiagnosticScope() { Diags.ActivePluginName = std::move(Saved); }
+ PluginDiagnosticScope(const PluginDiagnosticScope &) = delete;
+ PluginDiagnosticScope &operator=(const PluginDiagnosticScope &) = delete;
+ };
+
/// Converts a diagnostic argument (as an intptr_t) into the string
/// that represents it.
void ConvertArgToString(ArgumentKind Kind, intptr_t Val, StringRef Modifier,
diff --git a/clang/lib/Frontend/FrontendAction.cpp b/clang/lib/Frontend/FrontendAction.cpp
index 877ef662fe8da..407784ef4247c 100644
--- a/clang/lib/Frontend/FrontendAction.cpp
+++ b/clang/lib/Frontend/FrontendAction.cpp
@@ -408,7 +408,20 @@ Module *FrontendAction::getCurrentModule() const {
std::unique_ptr<ASTConsumer>
FrontendAction::CreateWrappedASTConsumer(CompilerInstance &CI,
StringRef InFile) {
- std::unique_ptr<ASTConsumer> Consumer = CreateASTConsumer(CI, InFile);
+ std::unique_ptr<ASTConsumer> Consumer;
+ {
+ // When the main action is itself a plugin (-plugin <name>), its own
+ // CreateASTConsumer is where it registers diagnostics; scope it so an
+ // ungrouped plugin warning/remark is placed in "<name>-plugin". The name is
+ // FrontendOpts.ActionName for a PluginAction; for any other action the name
+ // is empty and the scope is inert.
+ StringRef MainPluginName;
+ if (CI.getFrontendOpts().ProgramAction == frontend::PluginAction)
+ MainPluginName = CI.getFrontendOpts().ActionName;
+ DiagnosticsEngine::PluginDiagnosticScope DiagScope(CI.getDiagnostics(),
+ MainPluginName);
+ Consumer = CreateASTConsumer(CI, InFile);
+ }
if (!Consumer)
return nullptr;
@@ -471,6 +484,10 @@ FrontendAction::CreateWrappedASTConsumer(CompilerInstance &CI,
ActionType = PluginASTAction::AddAfterMainAction;
}
}
+ // Scope the plugin's ParseArgs and CreateASTConsumer so an ungrouped
+ // warning or remark it registers there is placed in "<plugin>-plugin".
+ DiagnosticsEngine::PluginDiagnosticScope DiagScope(CI.getDiagnostics(),
+ Plugin.getName());
if ((ActionType == PluginASTAction::AddBeforeMainAction ||
ActionType == PluginASTAction::AddAfterMainAction) &&
P->ParseArgs(
diff --git a/clang/lib/FrontendTool/ExecuteCompilerInvocation.cpp b/clang/lib/FrontendTool/ExecuteCompilerInvocation.cpp
index e872f0823f23e..3e0c7c6df9aaa 100644
--- a/clang/lib/FrontendTool/ExecuteCompilerInvocation.cpp
+++ b/clang/lib/FrontendTool/ExecuteCompilerInvocation.cpp
@@ -130,6 +130,12 @@ CreateFrontendBaseAction(CompilerInstance &CI) {
FrontendPluginRegistry::entries()) {
if (Plugin.getName() == CI.getFrontendOpts().ActionName) {
std::unique_ptr<PluginASTAction> P(Plugin.instantiate());
+ // Scope ParseArgs so an ungrouped warning/remark the plugin registers
+ // while parsing its arguments is placed in "<plugin>-plugin". The
+ // plugin's CreateASTConsumer is scoped later, in
+ // FrontendAction::CreateWrappedASTConsumer.
+ DiagnosticsEngine::PluginDiagnosticScope DiagScope(CI.getDiagnostics(),
+ Plugin.getName());
if ((P->getActionType() != PluginASTAction::ReplaceAction &&
P->getActionType() != PluginASTAction::CmdlineAfterMainAction) ||
!P->ParseArgs(
diff --git a/clang/unittests/Basic/DiagnosticTest.cpp b/clang/unittests/Basic/DiagnosticTest.cpp
index e4d188f64b6d6..71f20a5fe4055 100644
--- a/clang/unittests/Basic/DiagnosticTest.cpp
+++ b/clang/unittests/Basic/DiagnosticTest.cpp
@@ -858,4 +858,63 @@ TEST_F(PluginWarningGroupTest, PluginDiagTableRegistration) {
EXPECT_FALSE(Fresh.isIgnored(FreshIDs[0], SourceLocation()));
EXPECT_TRUE(Fresh.isIgnored(FreshIDs[2], SourceLocation()));
}
+
+// A PluginDiagnosticScope records the active plugin and restores the previous
+// one on exit; scopes nest with the innermost name winning.
+TEST_F(PluginWarningGroupTest, PluginDiagnosticScopeNesting) {
+ EXPECT_TRUE(Diags.getActivePluginName().empty());
+ {
+ DiagnosticsEngine::PluginDiagnosticScope Outer(Diags, "outer");
+ EXPECT_EQ(Diags.getActivePluginName(), "outer");
+ {
+ DiagnosticsEngine::PluginDiagnosticScope Inner(Diags, "inner");
+ EXPECT_EQ(Diags.getActivePluginName(), "inner");
+ }
+ EXPECT_EQ(Diags.getActivePluginName(), "outer");
+ }
+ EXPECT_TRUE(Diags.getActivePluginName().empty());
+}
+
+// Within a plugin scope, a warning or remark created through the ungrouped
+// getCustomDiagID(Level, FormatString) overload is auto-scoped into the
+// plugin's "<plugin>-plugin" group, so a -Wno-<plugin>-plugin silences it even
+// though the plugin never named a group.
+TEST_F(PluginWarningGroupTest, UngroupedPluginWarningIsAutoScoped) {
+ unsigned ID;
+ {
+ DiagnosticsEngine::PluginDiagnosticScope Scope(Diags, "example");
+ ID = Diags.getCustomDiagID(DiagnosticsEngine::Warning, "ungrouped warning");
+ }
+ EXPECT_FALSE(Diags.isIgnored(ID, SourceLocation()));
+
+ DiagOpts.Warnings = {"no-example-plugin"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+ EXPECT_TRUE(Diags.isIgnored(ID, SourceLocation()));
+}
+
+// An error created through the ungrouped overload during a plugin scope is left
+// ungrouped: errors are not controllable by group flags, so there is nothing to
+// auto-scope, and existing plugin error-reporting is unchanged. A -Wno on the
+// plugin group must not reach it.
+TEST_F(PluginWarningGroupTest, UngroupedPluginErrorNotAutoScoped) {
+ unsigned ID;
+ {
+ DiagnosticsEngine::PluginDiagnosticScope Scope(Diags, "example");
+ ID = Diags.getCustomDiagID(DiagnosticsEngine::Error, "ungrouped error");
+ }
+ DiagOpts.Warnings = {"no-example-plugin"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+ EXPECT_EQ(Diags.getDiagnosticLevel(ID, SourceLocation()),
+ DiagnosticsEngine::Error);
+}
+
+// Outside any plugin scope the ungrouped overload is unchanged: the diagnostic
+// belongs to no group and no plugin-group flag reaches it.
+TEST_F(PluginWarningGroupTest, UngroupedWarningOutsideScopeUnaffected) {
+ unsigned ID =
+ Diags.getCustomDiagID(DiagnosticsEngine::Warning, "plain warning");
+ DiagOpts.Warnings = {"no-example-plugin", "no-user-defined-warnings"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+ EXPECT_FALSE(Diags.isIgnored(ID, SourceLocation()));
+}
} // namespace
>From 2980974e580093834c553082ac1c0f68e054dee1 Mon Sep 17 00:00:00 2001
From: Vassil Vassilev <v.g.vassilev at gmail.com>
Date: Mon, 20 Jul 2026 14:41:17 +0000
Subject: [PATCH 7/9] [examples] Demonstrate a controllable backend-plugin
diagnostic end to end
The routing that lets a backend (pass) plugin place its diagnostics in its own
warning group was added without an in-tree plugin that exercises it, so the
CodeGenAction path that maps a grouped llvm::DiagnosticInfo onto a frontend
diagnostic had no end-to-end coverage.
Give the Bye example pass a -bye-warn option that emits, per function, a
DiagnosticInfo overriding getWarningGroup() to name "bye-plugin". A new
CodeGen lit test loads Bye through -fpass-plugin and checks the warning prints
"[-Wbye-plugin]" and is controlled exactly like a frontend plugin's: silenced by
-Wno-bye-plugin, the -Wno-plugin umbrella and the -Wno-user-defined-warnings
root, and promoted by -Werror=bye-plugin.
Writing the test surfaced a timing gap: reportUnclaimedPluginGroups runs after
the frontend plugins load but before codegen, whereas a pass plugin claims its
group only when it emits a diagnostic during codegen. A -Wno-bye-plugin flag
would therefore be reported as an unknown warning option. Skip the unclaimed
report when any pass plugin is loaded, since such a flag may still be claimed
later; a frontend-only compilation is unaffected.
---
clang/lib/Frontend/CompilerInstance.cpp | 13 +++++--
.../CodeGen/backend-plugin-diagnostic-group.c | 39 +++++++++++++++++++
llvm/examples/Bye/Bye.cpp | 27 +++++++++++++
3 files changed, 75 insertions(+), 4 deletions(-)
create mode 100644 clang/test/CodeGen/backend-plugin-diagnostic-group.c
diff --git a/clang/lib/Frontend/CompilerInstance.cpp b/clang/lib/Frontend/CompilerInstance.cpp
index fc3431e8b6a0d..12469c00d058c 100644
--- a/clang/lib/Frontend/CompilerInstance.cpp
+++ b/clang/lib/Frontend/CompilerInstance.cpp
@@ -1145,10 +1145,15 @@ void CompilerInstance::LoadRequestedPlugins() {
}
}
- // Every plugin group is now known, so a -W<plugin>-plugin flag that named a
- // group no loaded plugin owns is a misspelled option -- report it.
- getDiagnostics().getDiagnosticIDs()->reportUnclaimedPluginGroups(
- getDiagnostics());
+ // Every frontend plugin group is now known, so a -W<plugin>-plugin flag that
+ // named a group no loaded plugin owns is a misspelled option -- report it. A
+ // backend (pass) plugin, however, registers its group only when it emits a
+ // diagnostic during codegen, which is after this point; so when any pass
+ // plugin is loaded a flag may still be claimed later, and reporting here would
+ // risk a false "unknown warning option". Skip the report in that case.
+ if (getCodeGenOpts().PassPlugins.empty())
+ getDiagnostics().getDiagnosticIDs()->reportUnclaimedPluginGroups(
+ getDiagnostics());
}
/// Determine the appropriate source input kind based on language
diff --git a/clang/test/CodeGen/backend-plugin-diagnostic-group.c b/clang/test/CodeGen/backend-plugin-diagnostic-group.c
new file mode 100644
index 0000000000000..b36d326a0c728
--- /dev/null
+++ b/clang/test/CodeGen/backend-plugin-diagnostic-group.c
@@ -0,0 +1,39 @@
+// Tests that a backend (pass) plugin diagnostic which names its own warning
+// group via llvm::DiagnosticInfo::getWarningGroup() is controlled by the user
+// exactly like a frontend plugin's diagnostic: it prints "[-W<group>]", is
+// silenced by -Wno-<group>, the -Wno-plugin umbrella and the
+// -Wno-user-defined-warnings root, and is promoted by -Werror=<group>. The Bye
+// example pass emits such a diagnostic under -bye-warn.
+
+// The warning is on by default and prints its group.
+// RUN: %clang_cc1 -emit-llvm -o /dev/null -O2 \
+// RUN: -fpass-plugin=%llvmshlibdir/Bye%pluginext -mllvm -bye-warn %s 2>&1 \
+// RUN: | FileCheck --check-prefix=WARN %s
+
+// It is silenced by -Wno-<group>, by the -Wno-plugin umbrella, and by the
+// -Wno-user-defined-warnings root over every runtime group.
+// RUN: %clang_cc1 -emit-llvm -o /dev/null -O2 \
+// RUN: -fpass-plugin=%llvmshlibdir/Bye%pluginext -mllvm -bye-warn \
+// RUN: -Wno-bye-plugin %s 2>&1 | FileCheck --allow-empty --check-prefix=SILENT %s
+// RUN: %clang_cc1 -emit-llvm -o /dev/null -O2 \
+// RUN: -fpass-plugin=%llvmshlibdir/Bye%pluginext -mllvm -bye-warn \
+// RUN: -Wno-plugin %s 2>&1 | FileCheck --allow-empty --check-prefix=SILENT %s
+// RUN: %clang_cc1 -emit-llvm -o /dev/null -O2 \
+// RUN: -fpass-plugin=%llvmshlibdir/Bye%pluginext -mllvm -bye-warn \
+// RUN: -Wno-user-defined-warnings %s 2>&1 | FileCheck --allow-empty --check-prefix=SILENT %s
+
+// It is promoted by -Werror=<group>.
+// RUN: not %clang_cc1 -emit-llvm -o /dev/null -O2 \
+// RUN: -fpass-plugin=%llvmshlibdir/Bye%pluginext -mllvm -bye-warn \
+// RUN: -Werror=bye-plugin %s 2>&1 | FileCheck --check-prefix=WERROR %s
+
+// REQUIRES: plugins, llvm-examples
+// UNSUPPORTED: target={{.*windows.*}}
+// Plugins are currently broken on AIX, at least in the CI.
+// XFAIL: target={{.*}}-aix{{.*}}
+
+void f(void) {}
+
+// WARN: warning: Bye saw function 'f' [-Wbye-plugin]
+// SILENT-NOT: Bye saw function
+// WERROR: error: Bye saw function 'f'
diff --git a/llvm/examples/Bye/Bye.cpp b/llvm/examples/Bye/Bye.cpp
index fbe3b75920679..913631d42bb14 100644
--- a/llvm/examples/Bye/Bye.cpp
+++ b/llvm/examples/Bye/Bye.cpp
@@ -1,4 +1,7 @@
+#include "llvm/IR/DiagnosticInfo.h"
+#include "llvm/IR/DiagnosticPrinter.h"
#include "llvm/IR/Function.h"
+#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/LegacyPassManager.h"
#include "llvm/Pass.h"
#include "llvm/Passes/PassBuilder.h"
@@ -14,13 +17,37 @@ static cl::opt<bool> Wave("wave-goodbye", cl::init(false),
static cl::opt<bool> LastWords("last-words", cl::init(false),
cl::desc("say last words (suppress codegen)"));
+static cl::opt<bool>
+ ByeWarn("bye-warn", cl::init(false),
+ cl::desc("emit a warning per function through the frontend, in the "
+ "backend plugin's own warning group"));
+
namespace {
+// A backend (IR-layer) diagnostic that names its own warning group. Overriding
+// getWarningGroup() lets the frontend control it with -W<group> just like a
+// frontend plugin's diagnostic, instead of the coarse -Wbackend-plugin
+// umbrella. Using a plugin diagnostic kind routes it through the frontend's
+// generic backend-diagnostic path. By convention the group is "<plugin>-plugin".
+class DiagnosticInfoBye : public DiagnosticInfo {
+ const Twine &Msg;
+
+public:
+ DiagnosticInfoBye(const Twine &Msg LLVM_LIFETIME_BOUND)
+ : DiagnosticInfo(getNextAvailablePluginDiagnosticKind(), DS_Warning),
+ Msg(Msg) {}
+ void print(DiagnosticPrinter &DP) const override { DP << Msg; }
+ StringRef getWarningGroup() const override { return "bye-plugin"; }
+};
+
bool runBye(Function &F) {
if (Wave) {
errs() << "Bye: ";
errs().write_escaped(F.getName()) << '\n';
}
+ if (ByeWarn)
+ F.getContext().diagnose(
+ DiagnosticInfoBye("Bye saw function '" + F.getName() + "'"));
return false;
}
>From 9c4f4322a6afea94c1ff2e502892184fff2551f5 Mon Sep 17 00:00:00 2001
From: Vassil Vassilev <v.g.vassilev at gmail.com>
Date: Mon, 20 Jul 2026 20:17:08 +0000
Subject: [PATCH 8/9] [docs] Document plugin diagnostic groups in
ClangPlugins.md
Upstream migrated ClangPlugins.rst to ClangPlugins.md while this series was in
review, so the plugin-diagnostic documentation from the earlier commits did not
carry over the rename. Add it to the new Markdown file: how a plugin places a
diagnostic in its "<plugin>-plugin" group, the -Wplugin umbrella and the
-Wuser-defined-warnings root, joining a built-in group, the backend-plugin path
through getWarningGroup(), and the TableGen table registration via
getCustomPluginDiagIDs.
---
clang/docs/ClangPlugins.md | 90 ++++++++++++++++++++++++++++++++++++++
1 file changed, 90 insertions(+)
diff --git a/clang/docs/ClangPlugins.md b/clang/docs/ClangPlugins.md
index 10820833f4e4b..0a8482ec4ee71 100644
--- a/clang/docs/ClangPlugins.md
+++ b/clang/docs/ClangPlugins.md
@@ -111,6 +111,96 @@ attribute, are:
To see a working example of an attribute plugin, see [the Attribute.cpp example](https://github.com/llvm/llvm-project/blob/main/clang/examples/Attribute/Attribute.cpp).
+## Emitting diagnostics
+
+A plugin emits diagnostics through the `DiagnosticsEngine` obtained from the
+`CompilerInstance`. Calling `getCustomPluginDiagID` with the plugin's own name
+places the diagnostic in that plugin's warning group, so users can control it
+with `-W` flags exactly like a built-in warning. The group is named
+`<plugin>-plugin` (for the plugin registered under `<plugin>`); clang registers
+that group for every loaded plugin. Deriving the group from the plugin's name
+keeps every plugin in its own namespace, so two plugins can never collide on a
+group and a plugin diagnostic is never left in a name no `-W` flag reaches.
+
+```c++
+ DiagnosticsEngine &D = CI.getDiagnostics();
+ unsigned ID = D.getCustomPluginDiagID(
+ DiagnosticsEngine::Warning, "my plugin found something odd about '%0'",
+ "print-fns");
+ D.Report(Loc, ID) << Name;
+```
+
+The warning is on by default, prints `[-Wprint-fns-plugin]` so users can see how
+to control it, and can be:
+
+- silenced with `-Wno-print-fns-plugin`, with the `-Wno-plugin` umbrella that
+ covers every loaded plugin, or with `-Wno-user-defined-warnings` which is the
+ root over every runtime group (it also covers `diagnose_if`), and
+- turned into an error with `-Werror=print-fns-plugin`.
+
+The same grouping applies to remarks and errors. A remark
+(`DiagnosticsEngine::Remark`) placed in a group is off by default, like a
+built-in remark, and is opted into with `-R<group>`; `-W` flags do not affect
+it. An error keeps its severity regardless of any group flag, so a
+`-Wno-<group>` can never silence a grouped error.
+
+A plugin may further split its diagnostics into subgroups by passing a subgroup
+name as the final argument to `getCustomPluginDiagID`, producing
+`<plugin>-plugin-<subgroup>`; each subgroup is controlled by its own name, by the
+`<plugin>-plugin` group, and by the `-Wplugin` umbrella, with the most specific
+flag winning. A `-W<plugin>-plugin` flag that no loaded plugin claims is reported
+as an unknown warning option once all plugins have loaded, so misspellings are
+still caught.
+
+By convention plugin diagnostics live in their own `<plugin>-plugin` namespace
+under `-Wplugin`, which in turn nests under `-Wuser-defined-warnings`.
+`getCustomPluginDiagID` is a thin convenience over the general primitive
+
+```c++
+ unsigned getCustomDiagID(Level, StringRef Message, StringRef Group);
+```
+
+which places a diagnostic in any warning group named by `Group`. `Group` may
+also be an existing built-in group, so a plugin that deliberately wants to
+extend, say, `-Wdeprecated` can do so; the diagnostic is then controlled by that
+group's flag like any other member. Prefer the `<plugin>-plugin` convention
+unless there is a specific reason to join a built-in group.
+
+A backend (IR-layer) plugin controls its diagnostics the same way. An
+`llvm::DiagnosticInfo` that overrides `getWarningGroup()` to name a group is
+routed by clang through the same mechanism, so a pass plugin naming
+`<plugin>-plugin` gets the same `-W` control and nests under `-Wplugin` and
+`-Wuser-defined-warnings`. A backend diagnostic that names no group falls under
+`-Wbackend-plugin` as before.
+
+## Organizing diagnostics like the compiler does
+
+A plugin with more than a handful of diagnostics can organize them the way Clang
+organizes its own: as a table, with a stable name per diagnostic that doubles as
+its SARIF `ruleId`. Clang's diagnostics are defined as TableGen records in `.td`
+files, from which `clang-tblgen -gen-clang-diags-defs` generates a table of
+`DIAG(...)` rows. A plugin can do the same and register the generated table at
+runtime with `getCustomPluginDiagIDs`:
+
+```c++
+ static const DiagnosticsEngine::PluginDiagnostic Table[] = {
+ {"suspicious_decl", DiagnosticsEngine::Warning, "suspicious %0", ""},
+ {"forbidden_decl", DiagnosticsEngine::Error, "forbidden %0", ""},
+ };
+ llvm::SmallVector<unsigned> IDs =
+ CI.getDiagnostics().getCustomPluginDiagIDs("my-plugin", Table);
+```
+
+Each entry lands in the plugin's `my-plugin-plugin` group and gets a stable
+`ruleId` `my_plugin_<record>` (both derived from the names, so the plugin spells
+neither). The returned IDs are in table order, so a parallel enumeration
+generated from the same table gives type-safe call sites, exactly like Clang's
+`diag::warn_*`. The `PrintFunctionNames` example shows this end to end,
+hand-writing the table with an X-macro that a real plugin would instead generate
+from a `.td`. Moving a plugin's diagnostics under its own TableGen this way is a
+drop-in: the group names, the `-W` control, and the SARIF `ruleId` all stay the
+same, so users and tooling see no churn.
+
## Putting it all together
Let's look at an example plugin that prints top-level function names. This
>From d7e70ca0fdb5cf3e333491042a65e744fcff1c8c Mon Sep 17 00:00:00 2001
From: Vassil Vassilev <v.g.vassilev at gmail.com>
Date: Fri, 11 Sep 2026 07:51:32 +0000
Subject: [PATCH 9/9] [plugins] Nest plugin diagnostic groups under built-in
groups instead of joining them
The Clang Area Team asked that a plugin diagnostic never be a direct member of
a built-in group, so a user can always tell it from Clang's own. Let a plugin
group declare a built-in parent instead: -Wno-deprecated then reaches
"<plugin>-plugin-<sub>" while the diagnostic still prints its own flag. The
string overload of getCustomDiagID no longer accepts a built-in group name.
The -Wuser-defined-warnings root becomes the default parent, which replaces
the previous special case.
---
clang/docs/ClangPlugins.md | 26 +++-
clang/docs/ReleaseNotes.md | 22 +--
.../PrintFunctionNames/PrintFunctionNames.cpp | 31 +++-
clang/include/clang/Basic/Diagnostic.h | 29 ++--
clang/include/clang/Basic/DiagnosticIDs.h | 60 ++++++--
clang/lib/Basic/Diagnostic.cpp | 11 ++
clang/lib/Basic/DiagnosticIDs.cpp | 145 ++++++++++++------
.../test/Frontend/plugin-diagnostic-group.cpp | 22 +++
clang/unittests/Basic/DiagnosticTest.cpp | 106 +++++++++++--
llvm/include/llvm/IR/DiagnosticInfo.h | 7 +-
10 files changed, 349 insertions(+), 110 deletions(-)
diff --git a/clang/docs/ClangPlugins.md b/clang/docs/ClangPlugins.md
index 0a8482ec4ee71..3dc5a6c46542a 100644
--- a/clang/docs/ClangPlugins.md
+++ b/clang/docs/ClangPlugins.md
@@ -160,11 +160,27 @@ under `-Wplugin`, which in turn nests under `-Wuser-defined-warnings`.
unsigned getCustomDiagID(Level, StringRef Message, StringRef Group);
```
-which places a diagnostic in any warning group named by `Group`. `Group` may
-also be an existing built-in group, so a plugin that deliberately wants to
-extend, say, `-Wdeprecated` can do so; the diagnostic is then controlled by that
-group's flag like any other member. Prefer the `<plugin>-plugin` convention
-unless there is a specific reason to join a built-in group.
+which places a diagnostic in any runtime-registered warning group named by
+`Group`. `Group` must not be one of Clang's built-in groups: a plugin
+diagnostic never joins a built-in group directly, so a user can always tell a
+plugin's warning from Clang's own by the `[-W...]` it prints.
+
+A plugin that wants to extend a built-in group instead nests one of its own
+groups under it:
+
+```c++
+ D.registerPluginGroup("print-fns", "deprecation", /*Parent=*/"deprecated");
+ unsigned ID = D.getCustomPluginDiagID(
+ DiagnosticsEngine::Warning, "'%0' is old-fashioned", "print-fns",
+ "deprecation");
+```
+
+The diagnostic prints `[-Wprint-fns-plugin-deprecation]`, and `-Wno-deprecated`
+and `-Werror=deprecated` reach it like any other deprecation warning, as do
+`-Wno-print-fns-plugin` and `-Wno-plugin`; the most specific flag wins. A group
+with a declared parent nests under that parent instead of the
+`-Wuser-defined-warnings` root. Registering with a name that is not a built-in
+group returns false and registers nothing.
A backend (IR-layer) plugin controls its diagnostics the same way. An
`llvm::DiagnosticInfo` that overrides `getWarningGroup()` to name a group is
diff --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md
index 13e49fad98c68..89a386afab5fd 100644
--- a/clang/docs/ReleaseNotes.md
+++ b/clang/docs/ReleaseNotes.md
@@ -293,16 +293,18 @@ features cannot lower the translation-unit ABI level;
type itself is already handled.
- Custom diagnostics can now be placed in a warning group. A new
- `getCustomDiagID(Level, Message, Group)` overload puts a diagnostic in any
- warning group named by `Group`, whether an existing built-in group or a
- runtime-registered one whose name is not known at build time, so it can be
- controlled with `-W` and `-R` flags like a built-in diagnostic. Plugins get a
- thin convenience, `getCustomPluginDiagID`, that derives the group from the
- plugin's name as `<plugin>-plugin`: silenced with `-Wno-<plugin>-plugin` (the
- `-Wno-plugin` umbrella over every loaded plugin, or `-Wno-user-defined-warnings`
- over every runtime group), promoted with `-Werror=<plugin>-plugin`, and remarks
- controlled with `-R<plugin>-plugin`. Errors keep their severity. See
- [ClangPlugins](ClangPlugins.rst).
+ `getCustomDiagID(Level, Message, Group)` overload puts a diagnostic in a
+ runtime-registered warning group whose name is not known at build time, so it
+ can be controlled with `-W` and `-R` flags like a built-in diagnostic. Plugins
+ get a thin convenience, `getCustomPluginDiagID`, that derives the group from
+ the plugin's name as `<plugin>-plugin`: silenced with `-Wno-<plugin>-plugin`
+ (the `-Wno-plugin` umbrella over every loaded plugin, or
+ `-Wno-user-defined-warnings` over every runtime group), promoted with
+ `-Werror=<plugin>-plugin`, and remarks controlled with `-R<plugin>-plugin`.
+ Errors keep their severity. A plugin group can nest under a built-in group
+ with `registerPluginGroup`, so for example `-Wno-deprecated` reaches it while
+ it still prints its own `[-W<plugin>-plugin-...]`; a plugin diagnostic never
+ joins a built-in group directly. See [ClangPlugins](ClangPlugins.rst).
- Fixed bug in `-Wdocumentation` so that it correctly handles explicit
function template instantiations (#64087).
diff --git a/clang/examples/PrintFunctionNames/PrintFunctionNames.cpp b/clang/examples/PrintFunctionNames/PrintFunctionNames.cpp
index 637e83e6ae113..1ab402cb4442a 100644
--- a/clang/examples/PrintFunctionNames/PrintFunctionNames.cpp
+++ b/clang/examples/PrintFunctionNames/PrintFunctionNames.cpp
@@ -30,11 +30,16 @@ namespace {
// plugin would generate the PRINT_FNS_DIAGS body instead. Each row is one
// diagnostic: a record name (which becomes both the enumerator below and, with
// the plugin name, the SARIF ruleId), a level, a message, and an optional
-// subgroup of the plugin's "print-fns-plugin" group.
+// subgroup of the plugin's "print-fns-plugin" group. The "deprecation" subgroup
+// is nested under Clang's -Wdeprecated (see the consumer's constructor), which
+// is how a plugin extends a built-in group: through a group of its own that
+// still names the plugin, never by adding to the built-in group directly.
#define PRINT_FNS_DIAGS(DIAG) \
DIAG(suspicious_decl, Warning, "suspicious top-level declaration '%0'", "") \
DIAG(forbidden_decl, Error, "forbidden top-level declaration '%0'", "") \
- DIAG(saw_decl, Remark, "saw top-level declaration '%0'", "")
+ DIAG(saw_decl, Remark, "saw top-level declaration '%0'", "") \
+ DIAG(old_decl, Warning, "top-level declaration '%0' is old-fashioned", \
+ "deprecation")
// The stable enumeration, generated from the table's first column, gives the
// plugin type-safe names for its diagnostics just like clang's diag::warn_*.
@@ -60,6 +65,7 @@ class PrintFunctionsConsumer : public ASTConsumer {
bool WarnOnDecls;
bool RemarkOnDecls;
bool ErrorOnDecls;
+ bool DeprecateDecls;
// Diagnostic IDs assigned by getCustomPluginDiagIDs, indexed by
// print_fns::Kind. Registering the whole table up front (rather than lazily
// on first use) makes every diagnostic a member of the "print-fns-plugin"
@@ -71,16 +77,20 @@ class PrintFunctionsConsumer : public ASTConsumer {
PrintFunctionsConsumer(CompilerInstance &Instance,
std::set<std::string> ParsedTemplates,
bool WarnOnDecls, bool RemarkOnDecls,
- bool ErrorOnDecls)
+ bool ErrorOnDecls, bool DeprecateDecls)
: Instance(Instance), ParsedTemplates(ParsedTemplates),
WarnOnDecls(WarnOnDecls), RemarkOnDecls(RemarkOnDecls),
- ErrorOnDecls(ErrorOnDecls) {
+ ErrorOnDecls(ErrorOnDecls), DeprecateDecls(DeprecateDecls) {
+ DiagnosticsEngine &Diags = Instance.getDiagnostics();
+ // Nest the "print-fns-plugin-deprecation" subgroup under -Wdeprecated, so
+ // -Wno-deprecated and -Werror=deprecated reach its diagnostics while they
+ // still print their own [-Wprint-fns-plugin-deprecation].
+ Diags.registerPluginGroup("print-fns", "deprecation", "deprecated");
// One call registers the whole table. Each diagnostic lands in the plugin's
// "print-fns-plugin" group (derived from the plugin name) with a stable
// SARIF ruleId "print_fns_<record>" (likewise derived), so the plugin
// spells neither the group nor the id.
- DiagIDs = Instance.getDiagnostics().getCustomPluginDiagIDs("print-fns",
- PrintFnsDiagTable);
+ DiagIDs = Diags.getCustomPluginDiagIDs("print-fns", PrintFnsDiagTable);
}
bool HandleTopLevelDecl(DeclGroupRef DG) override {
@@ -103,6 +113,9 @@ class PrintFunctionsConsumer : public ASTConsumer {
if (ErrorOnDecls)
Diags.Report(ND->getLocation(), DiagIDs[print_fns::forbidden_decl])
<< ND->getNameAsString();
+ if (DeprecateDecls)
+ Diags.Report(ND->getLocation(), DiagIDs[print_fns::old_decl])
+ << ND->getNameAsString();
}
return true;
@@ -148,12 +161,14 @@ class PrintFunctionNamesAction : public PluginASTAction {
bool WarnOnDecls = false;
bool RemarkOnDecls = false;
bool ErrorOnDecls = false;
+ bool DeprecateDecls = false;
protected:
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,
llvm::StringRef) override {
return std::make_unique<PrintFunctionsConsumer>(
- CI, ParsedTemplates, WarnOnDecls, RemarkOnDecls, ErrorOnDecls);
+ CI, ParsedTemplates, WarnOnDecls, RemarkOnDecls, ErrorOnDecls,
+ DeprecateDecls);
}
bool ParseArgs(const CompilerInstance &CI,
@@ -169,6 +184,8 @@ class PrintFunctionNamesAction : public PluginASTAction {
RemarkOnDecls = true;
} else if (args[i] == "-error-decls") {
ErrorOnDecls = true;
+ } else if (args[i] == "-deprecate-decls") {
+ DeprecateDecls = true;
} else if (args[i] == "-an-error") {
unsigned DiagID = D.getCustomDiagID(DiagnosticsEngine::Error,
"invalid argument '%0'");
diff --git a/clang/include/clang/Basic/Diagnostic.h b/clang/include/clang/Basic/Diagnostic.h
index 9b522118a9697..3076f00f5e422 100644
--- a/clang/include/clang/Basic/Diagnostic.h
+++ b/clang/include/clang/Basic/Diagnostic.h
@@ -942,13 +942,13 @@ class DiagnosticsEngine : public RefCountedBase<DiagnosticsEngine> {
StringRef(FormatString, N - 1));
}
- /// Compute the diagnostic ID for a custom diagnostic placed in the warning
- /// group \p Group. The group may be an existing (TableGen) group, so the
- /// diagnostic can join a built-in group such as -Wdeprecated, or a
- /// runtime-registered group whose name is not known at build time. Either way
- /// it is controllable with -W<group> / -Wno-<group> / -Werror=<group> (and
- /// -R<group> for a remark), like a built-in warning. \p StableID, when given,
- /// is a build-independent identifier used as the diagnostic's SARIF ruleId.
+ /// Compute the diagnostic ID for a custom diagnostic placed in the
+ /// runtime-registered warning group \p Group, whose name need not be known at
+ /// build time. It is controllable with -W<group> / -Wno-<group> /
+ /// -Werror=<group> (and -R<group> for a remark), like a built-in warning.
+ /// \p Group must not name a built-in group; to extend one, nest the runtime
+ /// group under it with registerPluginGroup. \p StableID, when given, is a
+ /// build-independent identifier used as the diagnostic's SARIF ruleId.
template <unsigned N>
unsigned getCustomDiagID(Level L, const char (&FormatString)[N],
StringRef Group, StringRef StableID = {}) {
@@ -960,9 +960,8 @@ class DiagnosticsEngine : public RefCountedBase<DiagnosticsEngine> {
/// Convenience over the group-taking getCustomDiagID that places a plugin's
/// diagnostic in its own runtime group "<PluginName>-plugin" (or the subgroup
/// "<PluginName>-plugin-<Subgroup>"), the naming convention behind the
- /// -Wplugin umbrella. A plugin that instead wants to join an existing group
- /// can call getCustomDiagID(L, FormatString, Group) directly. \p StableID,
- /// when given, is used as the diagnostic's SARIF ruleId.
+ /// -Wplugin umbrella. \p StableID, when given, is used as the diagnostic's
+ /// SARIF ruleId.
template <unsigned N>
unsigned getCustomPluginDiagID(Level L, const char (&FormatString)[N],
StringRef PluginName, StringRef Subgroup = {},
@@ -999,6 +998,16 @@ class DiagnosticsEngine : public RefCountedBase<DiagnosticsEngine> {
getCustomPluginDiagIDs(StringRef PluginName,
ArrayRef<PluginDiagnostic> Table);
+ /// Nest the plugin group "<PluginName>-plugin-<Subgroup>" (or
+ /// "<PluginName>-plugin" when \p Subgroup is empty) under the built-in group
+ /// \p Parent, so a flag on the parent, say -Wno-deprecated, reaches the
+ /// diagnostics in it while their printed "[-W...]" still names the plugin's
+ /// group. This is how a plugin extends a built-in group: its diagnostics
+ /// never join one directly, so a user can tell them from Clang's own. Returns
+ /// false, registering nothing, if \p Parent is not a built-in group.
+ bool registerPluginGroup(StringRef PluginName, StringRef Subgroup,
+ StringRef Parent);
+
/// The plugin whose diagnostics are currently being registered, or empty when
/// none is (see PluginDiagnosticScope).
StringRef getActivePluginName() const { return ActivePluginName; }
diff --git a/clang/include/clang/Basic/DiagnosticIDs.h b/clang/include/clang/Basic/DiagnosticIDs.h
index e6d29e4430781..6570acee2c0cf 100644
--- a/clang/include/clang/Basic/DiagnosticIDs.h
+++ b/clang/include/clang/Basic/DiagnosticIDs.h
@@ -310,6 +310,14 @@ class DiagnosticIDs : public RefCountedBase<DiagnosticIDs> {
bool CreatedByFlag = false; // named by a -W/-R flag
bool ClaimedByPlugin = false; // declared or used by a loaded plugin
llvm::SmallVector<unsigned, 4> Members; // custom diag IDs in this group
+ // Built-in (TableGen) group this runtime group nests under, when the plugin
+ // declared one with registerPluginGroup; a flag on that group then reaches
+ // this group's members. Without one the group nests under the
+ // -Wuser-defined-warnings root (see staticParentOf). A plugin extends a
+ // built-in group this way -- through a group that still carries the
+ // plugin's name -- rather than by adding a diagnostic to the built-in group
+ // directly, so a user can always tell a plugin's warning from Clang's own.
+ std::optional<diag::Group> Parent;
diag::Severity severityFor(diag::Flavor F) const {
return F == diag::Flavor::Remark ? RemarkSeverity : WarnSeverity;
@@ -338,14 +346,24 @@ class DiagnosticIDs : public RefCountedBase<DiagnosticIDs> {
G[Ctrl.size()] == '-';
}
+ /// Append to \p Diags the members of runtime group \p Info that are of
+ /// \p Flavor. Errors are excluded, since -W/-R group flags never remap an
+ /// error.
+ void appendDynamicGroupMembers(diag::Flavor Flavor,
+ const DynamicGroupInfo &Info,
+ SmallVectorImpl<diag::kind> &Diags) const;
+
/// Append to \p Diags the runtime plugin-group diagnostics of \p Flavor that
- /// the plugin group named \p Ctrl controls (errors excluded, since -W/-R
- /// group flags never remap an error). Returns whether \p Ctrl names a
- /// registered plugin group. Shared by the "plugin" umbrella and the
- /// -Wuser-defined-warnings root.
+ /// the plugin group named \p Ctrl controls. Returns whether \p Ctrl names a
+ /// registered plugin group.
bool appendPluginGroupDiags(diag::Flavor Flavor, StringRef Ctrl,
SmallVectorImpl<diag::kind> &Diags) const;
+ /// The built-in group the runtime group \p Name nests under: the Parent
+ /// declared on it or on the nearest enclosing "<plugin>-plugin..." group,
+ /// else the -Wuser-defined-warnings root.
+ diag::Group staticParentOf(StringRef Name) const;
+
public:
DiagnosticIDs();
~DiagnosticIDs();
@@ -397,14 +415,19 @@ class DiagnosticIDs : public RefCountedBase<DiagnosticIDs> {
}());
}
- /// Return an ID for a custom diagnostic placed in the warning group \p Group.
- /// \p Group may name an existing (TableGen) group, so a caller can put a
- /// custom diagnostic into a built-in group such as -Wdeprecated; or a
- /// runtime-registered group whose name is not known at build time (e.g. a
- /// plugin's). Either way the diagnostic participates in -W<group> /
- /// -Wno-<group> / -Werror=<group> (and -R<group> for a remark), just like a
- /// built-in warning. Two callers may share a group name; that simply groups
- /// their diagnostics together.
+ /// Return an ID for a custom diagnostic placed in the runtime-registered
+ /// warning group \p Group, whose name need not be known at build time (e.g. a
+ /// plugin's). The diagnostic participates in -W<group> / -Wno-<group> /
+ /// -Werror=<group> (and -R<group> for a remark), just like a built-in
+ /// warning. Two callers may share a group name; that simply groups their
+ /// diagnostics together.
+ ///
+ /// \p Group must not name a built-in (TableGen) group: a custom diagnostic
+ /// never joins one directly, so a user can always tell it apart from Clang's
+ /// own. To extend a built-in group, put the diagnostic in a runtime group
+ /// and give that group the built-in one as its parent with
+ /// registerPluginGroup. In-tree callers with a diag::Group in hand use the
+ /// CustomDiagDesc overload.
///
/// \p StableID, when non-empty, is a build-independent identifier for the
/// diagnostic (see CustomDiagDesc::StableID) used as its SARIF ruleId; leave
@@ -415,9 +438,8 @@ class DiagnosticIDs : public RefCountedBase<DiagnosticIDs> {
/// Convenience over getCustomDiagID(Level, Message, Group) that places a
/// plugin's diagnostic in its own runtime group "<PluginName>-plugin" (or the
/// subgroup "<PluginName>-plugin-<Subgroup>"), the naming convention that the
- /// -Wplugin umbrella is built on. A plugin that instead wants to join an
- /// existing group can call getCustomDiagID(Level, Message, Group) directly.
- /// \p StableID is forwarded as the diagnostic's SARIF ruleId (see above).
+ /// -Wplugin umbrella is built on. \p StableID is forwarded as the
+ /// diagnostic's SARIF ruleId (see above).
unsigned getCustomPluginDiagID(Level Level, StringRef Message,
StringRef PluginName, StringRef Subgroup = {},
StringRef StableID = {});
@@ -429,8 +451,12 @@ class DiagnosticIDs : public RefCountedBase<DiagnosticIDs> {
bool ensureDynamicPluginGroup(StringRef Name);
/// Record that a loaded plugin owns the group \p Name, so a -W<name> that
- /// referenced it is not later reported as an unknown warning option.
- void registerPluginGroup(StringRef Name);
+ /// referenced it is not later reported as an unknown warning option. When
+ /// \p Parent names a built-in group, \p Name nests under it: a flag on the
+ /// parent (say -Wno-deprecated) reaches \p Name's members, and their printed
+ /// "[-W...]" still names \p Name, so the plugin's origin stays visible.
+ /// Returns false, registering nothing, if \p Parent is not a built-in group.
+ bool registerPluginGroup(StringRef Name, StringRef Parent = {});
/// After all plugins have loaded, report every plugin group that was named by
/// a -W flag but that no plugin ever claimed -- i.e. a misspelled option.
diff --git a/clang/lib/Basic/Diagnostic.cpp b/clang/lib/Basic/Diagnostic.cpp
index 31f488aa2d994..88a07848c741e 100644
--- a/clang/lib/Basic/Diagnostic.cpp
+++ b/clang/lib/Basic/Diagnostic.cpp
@@ -455,6 +455,17 @@ DiagnosticsEngine::getCustomPluginDiagIDs(StringRef PluginName,
return IDs;
}
+bool DiagnosticsEngine::registerPluginGroup(StringRef PluginName,
+ StringRef Subgroup,
+ StringRef Parent) {
+ // Same naming as getCustomPluginDiagID, so the group a diagnostic is placed
+ // in and the group given a parent here are the same one.
+ std::string Group = (Twine(PluginName) + "-plugin").str();
+ if (!Subgroup.empty())
+ Group = (Twine(Group) + "-" + Subgroup).str();
+ return Diags->registerPluginGroup(Group, Parent);
+}
+
bool DiagnosticsEngine::setDiagnosticGroupWarningAsError(StringRef Group,
bool Enabled) {
// If we are enabling this feature, just set the diagnostic mappings to map to
diff --git a/clang/lib/Basic/DiagnosticIDs.cpp b/clang/lib/Basic/DiagnosticIDs.cpp
index fa298391ce9c1..abb4c015d443d 100644
--- a/clang/lib/Basic/DiagnosticIDs.cpp
+++ b/clang/lib/Basic/DiagnosticIDs.cpp
@@ -15,6 +15,7 @@
#include "clang/Basic/DiagnosticCategories.h"
#include "clang/Basic/LangOptions.h"
#include "clang/Basic/SourceManager.h"
+#include "llvm/ADT/DenseSet.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/StringTable.h"
@@ -371,20 +372,18 @@ void DiagnosticIDs::initCustomDiagMapping(DiagnosticMapping &Mapping,
diag::Flavor DiagFlavor = Diag.GetClass() == CLASS_REMARK
? diag::Flavor::Remark
: diag::Flavor::WarningOrError;
- // -Wuser-defined-warnings is the least-specific control over every runtime
- // plugin group: it is the static root the "plugin" umbrella nests under,
- // so -Wno-user-defined-warnings / -Werror=user-defined-warnings reach
- // plugin diagnostics too. Seed a warning's mapping from a flag on it; a
- // more specific plugin-group flag below overrides. Remarks are not
- // warnings and follow -R flags only, so they do not inherit it.
- if (DiagFlavor == diag::Flavor::WarningOrError)
- if (std::optional<diag::Group> UDW =
- getGroupForWarningOption("user-defined-warnings")) {
- auto Sev = static_cast<diag::Severity>(
- GroupInfos[static_cast<size_t>(*UDW)].Severity);
- if (Sev != diag::Severity())
- Mapping.setSeverity(Sev);
- }
+ // The built-in group this runtime group nests under -- the parent the
+ // plugin declared, else the -Wuser-defined-warnings root -- is the least
+ // specific control over it, so a flag on that group (e.g.
+ // -Wno-deprecated for a group parented there) seeds the mapping; a more
+ // specific plugin-group flag below overrides. Remarks are not warnings
+ // and follow -R flags only, so they do not inherit it.
+ if (DiagFlavor == diag::Flavor::WarningOrError) {
+ auto Sev = static_cast<diag::Severity>(
+ GroupInfos[static_cast<size_t>(staticParentOf(DynGroup))].Severity);
+ if (Sev != diag::Severity())
+ Mapping.setSeverity(Sev);
+ }
const DynamicGroupInfo *Best = nullptr;
size_t BestLen = 0;
for (const auto &Entry : DynamicGroups)
@@ -529,15 +528,18 @@ unsigned DiagnosticIDs::getCustomDiagID(Level Level, StringRef Message,
Class = CLASS_ERROR;
break;
}
- // If Group names a built-in (static) group, join it directly so the
- // diagnostic is controlled like any other member of that group (e.g. a
- // -Wdeprecated addition). Otherwise it is a runtime-registered group kept in
- // the dynamic registry, whose name need not be known at build time.
- std::optional<diag::Group> StaticGroup = getGroupForWarningOption(Group);
+ // Group is a runtime-registered group kept in the dynamic registry, whose
+ // name need not be known at build time. A custom diagnostic never joins a
+ // built-in group directly: that would make it indistinguishable from Clang's
+ // own. A caller that wants a built-in flag to reach it nests its runtime
+ // group under that built-in group with registerPluginGroup instead.
+ assert(!getGroupForWarningOption(Group) &&
+ "custom diagnostics do not join built-in groups; give the runtime "
+ "group a parent with registerPluginGroup");
return getCustomDiagID(CustomDiagDesc(
Sev, std::string(Message), Class,
- /*ShowInSystemHeader=*/false, /*ShowInSystemMacro=*/false, StaticGroup,
- StaticGroup ? std::string() : std::string(Group), std::string(StableID)));
+ /*ShowInSystemHeader=*/false, /*ShowInSystemMacro=*/false,
+ /*Group=*/std::nullopt, std::string(Group), std::string(StableID)));
}
unsigned DiagnosticIDs::getCustomPluginDiagID(Level Level, StringRef Message,
@@ -546,8 +548,7 @@ unsigned DiagnosticIDs::getCustomPluginDiagID(Level Level, StringRef Message,
StringRef StableID) {
// A thin convention over getCustomDiagID: place the diagnostic in the
// plugin's own runtime group "<plugin>-plugin[-<sub>]" that the -Wplugin
- // umbrella controls. A plugin that wants to join an existing group instead
- // calls getCustomDiagID(Level, Message, Group) with that group's name.
+ // umbrella controls.
std::string Group = (Twine(PluginName) + "-plugin").str();
if (!Subgroup.empty())
Group = (Twine(Group) + "-" + Subgroup).str();
@@ -920,36 +921,63 @@ static bool getDiagnosticsInGroup(diag::Flavor Flavor,
return NotFound;
}
+void DiagnosticIDs::appendDynamicGroupMembers(
+ diag::Flavor Flavor, const DynamicGroupInfo &Info,
+ SmallVectorImpl<diag::kind> &Diags) const {
+ // A runtime group may hold warnings, errors (both WarningOrError) and
+ // remarks; add the members of the requested flavor.
+ if (!CustomDiagInfo)
+ return;
+ for (unsigned ID : Info.Members) {
+ DiagnosticIDs::Class Class = CustomDiagInfo->getDescription(ID).GetClass();
+ // Errors are not controllable by -W/-R group flags; leave them out so the
+ // mapping is never asked to downgrade an error.
+ if (Class == CLASS_ERROR)
+ continue;
+ diag::Flavor MemberFlavor = Class == CLASS_REMARK
+ ? diag::Flavor::Remark
+ : diag::Flavor::WarningOrError;
+ if (MemberFlavor == Flavor)
+ Diags.push_back(ID);
+ }
+}
+
bool DiagnosticIDs::appendPluginGroupDiags(
diag::Flavor Flavor, StringRef Ctrl,
SmallVectorImpl<diag::kind> &Diags) const {
- // Add the members of every registered plugin group that \p Ctrl controls and
- // that match the requested flavor -- a plugin group may hold warnings, errors
- // (both WarningOrError) and remarks. Returns whether any group matched, i.e.
- // whether \p Ctrl names a known (registered) plugin group.
+ // Add the members of every registered plugin group that \p Ctrl controls.
+ // Returns whether any group matched, i.e. whether \p Ctrl names a known
+ // (registered) plugin group.
bool Any = false;
for (const auto &Entry : DynamicGroups)
if (pluginGroupControls(Ctrl, Entry.first())) {
Any = true;
- if (!CustomDiagInfo)
- continue;
- for (unsigned ID : Entry.second.Members) {
- DiagnosticIDs::Class Class =
- CustomDiagInfo->getDescription(ID).GetClass();
- // Errors are not controllable by -W/-R group flags; leave them out so
- // the mapping is never asked to downgrade an error.
- if (Class == CLASS_ERROR)
- continue;
- diag::Flavor MemberFlavor = Class == CLASS_REMARK
- ? diag::Flavor::Remark
- : diag::Flavor::WarningOrError;
- if (MemberFlavor == Flavor)
- Diags.push_back(ID);
- }
+ appendDynamicGroupMembers(Flavor, Entry.second, Diags);
}
return Any;
}
+diag::Group DiagnosticIDs::staticParentOf(StringRef Name) const {
+ // Walk from the group up through the enclosing "<plugin>-plugin..." groups
+ // (dash-separated, the same nesting pluginGroupControls uses) and take the
+ // first declared parent, so a subgroup inherits the parent of the group it
+ // lives in. A group that is not a plugin group has no enclosing groups.
+ for (StringRef G = Name;; G = G.rsplit('-').first) {
+ if (auto It = DynamicGroups.find(G); It != DynamicGroups.end())
+ if (It->second.Parent)
+ return *It->second.Parent;
+ if (!isPluginGroupName(G) || !G.contains('-'))
+ break;
+ }
+ std::optional<diag::Group> Root =
+ getGroupForWarningOption("user-defined-warnings");
+ assert(Root && "the user-defined-warnings group is defined in TableGen");
+ return *Root;
+}
+
+template <typename Func>
+static void forEachSubGroup(diag::Group Group, Func func);
+
bool
DiagnosticIDs::getDiagnosticsInGroup(diag::Flavor Flavor, StringRef Group,
SmallVectorImpl<diag::kind> &Diags) const {
@@ -957,12 +985,19 @@ DiagnosticIDs::getDiagnosticsInGroup(diag::Flavor Flavor, StringRef Group,
if (CustomDiagInfo)
llvm::copy(CustomDiagInfo->getDiagsInGroup(*G),
std::back_inserter(Diags));
- // -Wuser-defined-warnings is the static root every runtime plugin group
- // nests under, so a flag on it reaches plugin diagnostics too, just like the
- // "plugin" umbrella does. Add their members before descending the static
- // subgroups.
- if (Group == "user-defined-warnings")
- appendPluginGroupDiags(Flavor, "plugin", Diags);
+ // A runtime group nests under a built-in group: the parent its plugin
+ // declared, else the -Wuser-defined-warnings root. So a flag on this group
+ // reaches every runtime group parented on it or on one of its static
+ // subgroups, just like the "plugin" umbrella does. Add their members before
+ // descending the static subgroups.
+ if (!DynamicGroups.empty()) {
+ llvm::SmallDenseSet<size_t, 8> Subtree;
+ ::forEachSubGroup(*G, [&](size_t SubGroup) { Subtree.insert(SubGroup); });
+ for (const auto &Entry : DynamicGroups)
+ if (Subtree.contains(
+ static_cast<size_t>(staticParentOf(Entry.first()))))
+ appendDynamicGroupMembers(Flavor, Entry.second, Diags);
+ }
return ::getDiagnosticsInGroup(Flavor,
&OptionTable[static_cast<unsigned>(*G)],
Diags, CustomDiagInfo.get());
@@ -1038,8 +1073,18 @@ bool DiagnosticIDs::ensureDynamicPluginGroup(StringRef Name) {
return true;
}
-void DiagnosticIDs::registerPluginGroup(StringRef Name) {
- DynamicGroups[Name].ClaimedByPlugin = true;
+bool DiagnosticIDs::registerPluginGroup(StringRef Name, StringRef Parent) {
+ std::optional<diag::Group> ParentGroup;
+ if (!Parent.empty()) {
+ ParentGroup = getGroupForWarningOption(Parent);
+ if (!ParentGroup)
+ return false;
+ }
+ DynamicGroupInfo &Info = DynamicGroups[Name];
+ Info.ClaimedByPlugin = true;
+ if (ParentGroup)
+ Info.Parent = ParentGroup;
+ return true;
}
void DiagnosticIDs::reportUnclaimedPluginGroups(
diff --git a/clang/test/Frontend/plugin-diagnostic-group.cpp b/clang/test/Frontend/plugin-diagnostic-group.cpp
index 9f7b17157b30f..5ea53f55b04d8 100644
--- a/clang/test/Frontend/plugin-diagnostic-group.cpp
+++ b/clang/test/Frontend/plugin-diagnostic-group.cpp
@@ -47,6 +47,26 @@
// RUN: -plugin print-fns -plugin-arg-print-fns -warn-decls %t/pragma.cpp 2>&1 \
// RUN: | FileCheck --check-prefix=PRAGMA %s
+// A plugin subgroup nested under a built-in group prints its own name, so the
+// plugin's origin is visible, and is reached by the built-in group's flags as
+// well as by the plugin's own; the most specific flag wins.
+// RUN: %clang_cc1 -load %llvmshlibdir/PrintFunctionNames%pluginext \
+// RUN: -plugin print-fns -plugin-arg-print-fns -deprecate-decls %t/simple.cpp 2>&1 \
+// RUN: | FileCheck --check-prefix=NESTED %s
+// RUN: %clang_cc1 -load %llvmshlibdir/PrintFunctionNames%pluginext \
+// RUN: -plugin print-fns -plugin-arg-print-fns -deprecate-decls \
+// RUN: -Wno-deprecated %t/simple.cpp 2>&1 | FileCheck --check-prefix=SILENT %s
+// RUN: %clang_cc1 -load %llvmshlibdir/PrintFunctionNames%pluginext \
+// RUN: -plugin print-fns -plugin-arg-print-fns -deprecate-decls \
+// RUN: -Wno-print-fns-plugin %t/simple.cpp 2>&1 | FileCheck --check-prefix=SILENT %s
+// RUN: not %clang_cc1 -load %llvmshlibdir/PrintFunctionNames%pluginext \
+// RUN: -plugin print-fns -plugin-arg-print-fns -deprecate-decls \
+// RUN: -Werror=deprecated %t/simple.cpp 2>&1 | FileCheck --check-prefix=NESTED-WERROR %s
+// RUN: %clang_cc1 -load %llvmshlibdir/PrintFunctionNames%pluginext \
+// RUN: -plugin print-fns -plugin-arg-print-fns -deprecate-decls \
+// RUN: -Wno-deprecated -Wprint-fns-plugin-deprecation %t/simple.cpp 2>&1 \
+// RUN: | FileCheck --check-prefix=NESTED %s
+
// A -Wno-<x>-plugin that no loaded plugin claims is a misspelled option.
// RUN: %clang_cc1 -load %llvmshlibdir/PrintFunctionNames%pluginext \
// RUN: -plugin print-fns -Wno-bogus-plugin %t/simple.cpp 2>&1 \
@@ -62,6 +82,8 @@ void f();
// WERROR: error: suspicious top-level declaration 'f'
// REMARK: remark: saw top-level declaration 'f' [-Rprint-fns-plugin]
// ERROR: error: forbidden top-level declaration 'f'
+// NESTED: warning: top-level declaration 'f' is old-fashioned [-Wprint-fns-plugin-deprecation]
+// NESTED-WERROR: error: top-level declaration 'f' is old-fashioned [-Werror,-Wprint-fns-plugin-deprecation]
// UNKNOWN: unknown warning option
//--- pragma.cpp
diff --git a/clang/unittests/Basic/DiagnosticTest.cpp b/clang/unittests/Basic/DiagnosticTest.cpp
index 71f20a5fe4055..f27077dd7b3ae 100644
--- a/clang/unittests/Basic/DiagnosticTest.cpp
+++ b/clang/unittests/Basic/DiagnosticTest.cpp
@@ -713,11 +713,30 @@ TEST_F(PluginWarningGroupTest, RemarkFlagDoesNotAffectWarning) {
EXPECT_FALSE(Diags.isIgnored(ID, SourceLocation()));
}
-// A custom diagnostic may join an existing (built-in) group by naming it, and
-// is then controlled by that group's flag like any other member.
-TEST_F(PluginWarningGroupTest, CustomDiagJoinsBuiltinGroup) {
- unsigned ID = Diags.getCustomDiagID(DiagnosticsEngine::Warning,
- "custom deprecation", "deprecated");
+// A plugin extends a built-in group by nesting one of its own groups under it,
+// never by adding a diagnostic to the built-in group directly. A flag on the
+// parent then reaches the plugin's diagnostic. Here the flag is parsed before
+// the plugin loads, so the parent's severity is picked up at registration.
+TEST_F(PluginWarningGroupTest, BuiltinParentFlagReachesPluginGroup) {
+ DiagOpts.Warnings = {"no-deprecated"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+
+ ASSERT_TRUE(
+ Diags.registerPluginGroup("example", "deprecation", "deprecated"));
+ unsigned ID = Diags.getCustomPluginDiagID(DiagnosticsEngine::Warning,
+ "custom deprecation", "example",
+ "deprecation");
+ EXPECT_TRUE(Diags.isIgnored(ID, SourceLocation()));
+}
+
+// The same when the flag comes after registration (a `#pragma clang
+// diagnostic`, say): the built-in group collects the nested group's members.
+TEST_F(PluginWarningGroupTest, BuiltinParentFlagReachesRegisteredMember) {
+ ASSERT_TRUE(
+ Diags.registerPluginGroup("example", "deprecation", "deprecated"));
+ unsigned ID = Diags.getCustomPluginDiagID(DiagnosticsEngine::Warning,
+ "custom deprecation", "example",
+ "deprecation");
EXPECT_FALSE(Diags.isIgnored(ID, SourceLocation()));
DiagOpts.Warnings = {"no-deprecated"};
@@ -725,16 +744,85 @@ TEST_F(PluginWarningGroupTest, CustomDiagJoinsBuiltinGroup) {
EXPECT_TRUE(Diags.isIgnored(ID, SourceLocation()));
}
-// -Werror on a built-in group also promotes a custom diagnostic that joined it.
-TEST_F(PluginWarningGroupTest, WerrorPromotesCustomDiagInBuiltinGroup) {
- unsigned ID = Diags.getCustomDiagID(DiagnosticsEngine::Warning,
- "custom deprecation", "deprecated");
+// -Werror on the built-in parent promotes the nested plugin diagnostic.
+TEST_F(PluginWarningGroupTest, WerrorOnBuiltinParentPromotesPluginGroup) {
+ ASSERT_TRUE(
+ Diags.registerPluginGroup("example", "deprecation", "deprecated"));
+ unsigned ID = Diags.getCustomPluginDiagID(DiagnosticsEngine::Warning,
+ "custom deprecation", "example",
+ "deprecation");
DiagOpts.Warnings = {"error=deprecated"};
ProcessWarningOptions(Diags, DiagOpts, *FS);
EXPECT_EQ(Diags.getDiagnosticLevel(ID, SourceLocation()),
DiagnosticsEngine::Error);
}
+// The printed flag names the plugin's own group, not the parent, so a user can
+// tell the plugin's warning from Clang's.
+TEST_F(PluginWarningGroupTest, NestedGroupPrintsItsOwnName) {
+ ASSERT_TRUE(
+ Diags.registerPluginGroup("example", "deprecation", "deprecated"));
+ unsigned ID = Diags.getCustomPluginDiagID(DiagnosticsEngine::Warning,
+ "custom deprecation", "example",
+ "deprecation");
+ EXPECT_EQ(Diags.getDiagnosticIDs()->getWarningOptionForDiag(ID),
+ "example-plugin-deprecation");
+}
+
+// The plugin group's own flag is more specific than the parent's and wins.
+TEST_F(PluginWarningGroupTest, PluginGroupFlagOverridesBuiltinParent) {
+ DiagOpts.Warnings = {"no-deprecated", "example-plugin-deprecation"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+
+ ASSERT_TRUE(
+ Diags.registerPluginGroup("example", "deprecation", "deprecated"));
+ unsigned ID = Diags.getCustomPluginDiagID(DiagnosticsEngine::Warning,
+ "custom deprecation", "example",
+ "deprecation");
+ EXPECT_FALSE(Diags.isIgnored(ID, SourceLocation()));
+}
+
+// A subgroup inherits the parent declared on the group it lives in.
+TEST_F(PluginWarningGroupTest, SubgroupInheritsBuiltinParent) {
+ ASSERT_TRUE(
+ Diags.registerPluginGroup("example", "deprecation", "deprecated"));
+ unsigned ID = Diags.getCustomPluginDiagID(DiagnosticsEngine::Warning,
+ "custom deprecation", "example",
+ "deprecation-old");
+ DiagOpts.Warnings = {"no-deprecated"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+ EXPECT_TRUE(Diags.isIgnored(ID, SourceLocation()));
+}
+
+// A group with a declared parent nests under that parent instead of the
+// -Wuser-defined-warnings root; the -Wplugin umbrella still covers it.
+TEST_F(PluginWarningGroupTest, BuiltinParentReplacesUserDefinedWarningsRoot) {
+ ASSERT_TRUE(
+ Diags.registerPluginGroup("example", "deprecation", "deprecated"));
+ unsigned ID = Diags.getCustomPluginDiagID(DiagnosticsEngine::Warning,
+ "custom deprecation", "example",
+ "deprecation");
+ DiagOpts.Warnings = {"no-user-defined-warnings"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+ EXPECT_FALSE(Diags.isIgnored(ID, SourceLocation()));
+
+ DiagOpts.Warnings = {"no-plugin"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+ EXPECT_TRUE(Diags.isIgnored(ID, SourceLocation()));
+}
+
+// A parent that is not a built-in group is refused and nothing is nested.
+TEST_F(PluginWarningGroupTest, UnknownBuiltinParentRefused) {
+ EXPECT_FALSE(
+ Diags.registerPluginGroup("example", "deprecation", "no-such-group"));
+ unsigned ID = Diags.getCustomPluginDiagID(DiagnosticsEngine::Warning,
+ "custom deprecation", "example",
+ "deprecation");
+ DiagOpts.Warnings = {"no-deprecated"};
+ ProcessWarningOptions(Diags, DiagOpts, *FS);
+ EXPECT_FALSE(Diags.isIgnored(ID, SourceLocation()));
+}
+
// -Wno-user-defined-warnings is the root over every runtime plugin group, so it
// silences a plugin diagnostic even though the flag names neither the plugin
// group nor the -Wplugin umbrella. The flag is parsed before the plugin loads.
diff --git a/llvm/include/llvm/IR/DiagnosticInfo.h b/llvm/include/llvm/IR/DiagnosticInfo.h
index a18ebb315c063..b146db351a4f5 100644
--- a/llvm/include/llvm/IR/DiagnosticInfo.h
+++ b/llvm/include/llvm/IR/DiagnosticInfo.h
@@ -133,9 +133,12 @@ class LLVM_ABI DiagnosticInfo {
/// is not in a user-controllable group. A plugin (its kind obtained from
/// getNextAvailablePluginDiagnosticKind) can override this so its backend
/// diagnostic is controlled with -W<group> like a frontend plugin's, instead
- /// of only through the coarse -Wbackend-plugin umbrella. The frontend resolves
- /// the name against its own diagnostic-group registry; by convention it is
+ /// of only through the coarse -Wbackend-plugin umbrella. The frontend
+ /// resolves the name against its own diagnostic-group registry; by convention
+ /// it is
/// "<plugin>-plugin", which nests under -Wplugin and -Wuser-defined-warnings.
+ /// It must be a runtime group of the plugin's own, never the name of one of
+ /// the frontend's built-in groups.
virtual StringRef getWarningGroup() const { return StringRef(); }
/// Print using the given \p DP a user-friendly message.
More information about the cfe-commits
mailing list