[clang] [llvm] [Clang] Show inlining hints for __attribute__((warning/error)) (PR #174892)

Justin Stitt via cfe-commits cfe-commits at lists.llvm.org
Thu Mar 26 09:17:06 PDT 2026


https://github.com/JustinStitt updated https://github.com/llvm/llvm-project/pull/174892

>From 47fc2001f3026cb9ba9ca7b373e295c2974bf2b8 Mon Sep 17 00:00:00 2001
From: Justin Stitt <justinstitt at google.com>
Date: Tue, 6 Jan 2026 15:36:33 -0800
Subject: [PATCH] [Clang] Add -fdiagnostics-show-inlining-chain for
 warning/error attributes

When functions marked with __attribute__((warning/error)) are called
through inlined functions, Clang now optionally shows the inlining chain
that led to the call.

Three modes are supported:

- ``none`` is the default and results in no inline notes being shown
  (matches the behavior before this change).

- ``heuristic`` tries to guess which functions will be inlined to
  minimize the amount of source locations kept in memory and visible in
  LLVM IR.

- ``debug`` leverages minimal debug directive tracking infrastructure to
  get more reliable source locations over the heuristic mode while
  having more compile-time overhead.

Fixes: https://github.com/ClangBuiltLinux/linux/issues/1571
Based-on: https://github.com/llvm/llvm-project/pull/73552
Signed-off-by: Justin Stitt <justinstitt at google.com>
---
 clang/docs/ReleaseNotes.rst                   |   8 ++
 clang/include/clang/Basic/AttrDocs.td         |  12 ++
 clang/include/clang/Basic/CodeGenOptions.def  |   2 +
 .../clang/Basic/DiagnosticFrontendKinds.td    |   6 +
 clang/include/clang/Options/Options.td        |   7 ++
 clang/lib/CodeGen/CGCall.cpp                  |  25 ++--
 clang/lib/CodeGen/CodeGenAction.cpp           |  41 +++++++
 clang/lib/Driver/ToolChains/Clang.cpp         |   9 ++
 .../backend-attribute-inlining-cross-tu.c     |  67 +++++++++++
 ...-attribute-inlining-debug-vs-heuristic.cpp |  92 ++++++++++++++
 .../backend-attribute-inlining-modes.c        |  42 +++++++
 .../Frontend/backend-attribute-inlining.c     | 112 ++++++++++++++++++
 llvm/include/llvm/IR/DiagnosticInfo.h         |  26 +++-
 llvm/lib/IR/DiagnosticInfo.cpp                |  38 +++++-
 llvm/lib/Transforms/Utils/InlineFunction.cpp  |  44 +++++++
 15 files changed, 521 insertions(+), 10 deletions(-)
 create mode 100644 clang/test/Frontend/backend-attribute-inlining-cross-tu.c
 create mode 100644 clang/test/Frontend/backend-attribute-inlining-debug-vs-heuristic.cpp
 create mode 100644 clang/test/Frontend/backend-attribute-inlining-modes.c
 create mode 100644 clang/test/Frontend/backend-attribute-inlining.c

diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index a8897c707b9e6..f41d2a531bef3 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -187,6 +187,14 @@ New Compiler Flags
   Control Flow Guard (CFG) is enabled by other options, it will instruct Clang
   to emit the CFG metadata, but disable adding checks.
 
+- New option ``-fdiagnostics-show-inlining-chain`` added to show inlining chain
+  notes for ``[[gnu::warning]]`` and ``[[gnu::error]]`` diagnostics. When a
+  function with these attributes is called from an inlined context, Clang can
+  now show which functions were inlined to reach the call. When debug info is
+  available (``-gline-directives-only`` (implicitly enabled at ``-g1``) or
+  higher), accurate source locations are used; otherwise, a heuristic fallback
+  is used with a note suggesting how to enable debug info for better accuracy.
+
 Deprecated Compiler Flags
 -------------------------
 
diff --git a/clang/include/clang/Basic/AttrDocs.td b/clang/include/clang/Basic/AttrDocs.td
index 555a54fd51a89..5383264be5f30 100644
--- a/clang/include/clang/Basic/AttrDocs.td
+++ b/clang/include/clang/Basic/AttrDocs.td
@@ -8579,6 +8579,18 @@ pointing to precise locations of the call site in the source.
       dontcall(); // No Warning
     sizeof(dontcall()); // No Warning
   }
+
+When the call occurs through inlined functions, the
+``-fdiagnostics-show-inlining-chain`` option can be used to show the
+inlining chain that led to the call. This helps identify which call site
+triggered the diagnostic when the attributed function is called from
+multiple locations through inline functions.
+
+When enabled, this option automatically uses debug info for accurate source
+locations if available (``-gline-directives-only`` (implicitly enabled at
+``-g1``) or higher), or falls back to a heuristic based on metadata tracking.
+When falling back, a note is emitted suggesting ``-gline-directives-only`` for
+more accurate locations.
   }];
 }
 
diff --git a/clang/include/clang/Basic/CodeGenOptions.def b/clang/include/clang/Basic/CodeGenOptions.def
index d883552ea2198..6cee3e8acda3c 100644
--- a/clang/include/clang/Basic/CodeGenOptions.def
+++ b/clang/include/clang/Basic/CodeGenOptions.def
@@ -78,6 +78,8 @@ CODEGENOPT(CallGraphSection, 1, 0, Benign) ///< Emit a call graph section into t
                                            ///< object file.
 CODEGENOPT(EmitCallSiteInfo, 1, 0, Benign) ///< Emit call site info only in the case of
                                            ///< '-g' + 'O>0' level.
+/// Show inlining chain notes for [[gnu::warning/error]] diagnostics.
+CODEGENOPT(ShowInliningChain, 1, 0, Benign)
 CODEGENOPT(IndirectTlsSegRefs, 1, 0, Benign) ///< Set when -mno-tls-direct-seg-refs
                                              ///< is specified.
 CODEGENOPT(DisableTailCalls  , 1, 0, Benign) ///< Do not emit tail calls.
diff --git a/clang/include/clang/Basic/DiagnosticFrontendKinds.td b/clang/include/clang/Basic/DiagnosticFrontendKinds.td
index b86caeb7714e9..62b74574102e4 100644
--- a/clang/include/clang/Basic/DiagnosticFrontendKinds.td
+++ b/clang/include/clang/Basic/DiagnosticFrontendKinds.td
@@ -94,6 +94,12 @@ def err_fe_backend_error_attr :
 def warn_fe_backend_warning_attr :
   Warning<"call to '%0' declared with 'warning' attribute: %1">, BackendInfo,
   InGroup<BackendWarningAttributes>;
+def note_fe_backend_in : Note<"called by function '%0'">, BackendInfo;
+def note_fe_backend_inlined : Note<"inlined by function '%0'">, BackendInfo;
+def note_fe_backend_inlining_debug_info
+    : Note<"use '-gline-directives-only' (implied by '-g1') or higher for "
+           "more accurate inlining chain locations">,
+      BackendInfo;
 def warn_toc_unsupported_type : Warning<"-mtocdata option is ignored "
   "for %0 because %1">, InGroup<BackendWarningAttributes>;
 
diff --git a/clang/include/clang/Options/Options.td b/clang/include/clang/Options/Options.td
index 8b0c701521728..de8f6ccc19ae2 100644
--- a/clang/include/clang/Options/Options.td
+++ b/clang/include/clang/Options/Options.td
@@ -2216,6 +2216,13 @@ defm diagnostics_show_note_include_stack : BoolFOption<"diagnostics-show-note-in
     DiagnosticOpts<"ShowNoteIncludeStack">, DefaultFalse,
     PosFlag<SetTrue, [], [ClangOption], "Display include stacks for diagnostic notes">,
     NegFlag<SetFalse>, BothFlags<[], [ClangOption, CC1Option]>>;
+defm diagnostics_show_inlining_chain
+    : BoolFOption<"diagnostics-show-inlining-chain",
+                  CodeGenOpts<"ShowInliningChain">, DefaultFalse,
+                  PosFlag<SetTrue, [], [ClangOption, CC1Option],
+                          "Show inlining chain notes for "
+                          "[[gnu::warning/error]] diagnostics">,
+                  NegFlag<SetFalse, [], [ClangOption, CC1Option]>>;
 def fdiagnostics_format_EQ : Joined<["-"], "fdiagnostics-format=">, Group<f_clang_Group>;
 def fdiagnostics_show_category_EQ : Joined<["-"], "fdiagnostics-show-category=">, Group<f_clang_Group>;
 def fdiagnostics_show_template_tree : Flag<["-"], "fdiagnostics-show-template-tree">,
diff --git a/clang/lib/CodeGen/CGCall.cpp b/clang/lib/CodeGen/CGCall.cpp
index 4c0ea9ec3ea9c..48e48168ba5e7 100644
--- a/clang/lib/CodeGen/CGCall.cpp
+++ b/clang/lib/CodeGen/CGCall.cpp
@@ -6159,13 +6159,24 @@ RValue CodeGenFunction::EmitCall(const CGFunctionInfo &CallInfo,
   if (getDebugInfo() && TargetDecl && TargetDecl->hasAttr<MSAllocatorAttr>())
     getDebugInfo()->addHeapAllocSiteMetadata(CI, RetTy->getPointeeType(), Loc);
 
-  // Add metadata if calling an __attribute__((error(""))) or warning fn.
-  if (TargetDecl && TargetDecl->hasAttr<ErrorAttr>()) {
-    llvm::ConstantInt *Line =
-        llvm::ConstantInt::get(Int64Ty, Loc.getRawEncoding());
-    llvm::ConstantAsMetadata *MD = llvm::ConstantAsMetadata::get(Line);
-    llvm::MDTuple *MDT = llvm::MDNode::get(getLLVMContext(), {MD});
-    CI->setMetadata("srcloc", MDT);
+  // Add srcloc metadata for [[gnu::error/warning]] diagnostics. When
+  // ShowInliningChain is enabled, also track inline/static calls for the
+  // heuristic fallback when debug info is not available. This heuristic is
+  // conservative and best-effort since static or inline-annotated functions
+  // are still not guaranteed to be inlined.
+  if (TargetDecl) {
+    bool NeedSrcLoc = TargetDecl->hasAttr<ErrorAttr>();
+    if (!NeedSrcLoc && CGM.getCodeGenOpts().ShowInliningChain) {
+      if (const auto *FD = dyn_cast<FunctionDecl>(TargetDecl))
+        NeedSrcLoc = FD->isInlined() || FD->hasAttr<AlwaysInlineAttr>() ||
+                     FD->getStorageClass() == SC_Static ||
+                     FD->isInAnonymousNamespace();
+    }
+    if (NeedSrcLoc) {
+      auto *Line = llvm::ConstantInt::get(Int64Ty, Loc.getRawEncoding());
+      auto *MD = llvm::ConstantAsMetadata::get(Line);
+      CI->setMetadata("srcloc", llvm::MDNode::get(getLLVMContext(), {MD}));
+    }
   }
 
   // 4. Finish the call.
diff --git a/clang/lib/CodeGen/CodeGenAction.cpp b/clang/lib/CodeGen/CodeGenAction.cpp
index 29dcabd1b0971..8a82cd057376d 100644
--- a/clang/lib/CodeGen/CodeGenAction.cpp
+++ b/clang/lib/CodeGen/CodeGenAction.cpp
@@ -732,6 +732,47 @@ void BackendConsumer::DontCallDiagHandler(const DiagnosticInfoDontCall &D) {
                               ? diag::err_fe_backend_error_attr
                               : diag::warn_fe_backend_warning_attr)
       << llvm::demangle(D.getFunctionName()) << D.getNote();
+
+  if (!CodeGenOpts.ShowInliningChain)
+    return;
+
+  auto EmitNote = [&](SourceLocation Loc, StringRef FuncName, bool IsFirst) {
+    if (!Loc.isValid())
+      Loc = LocCookie;
+    unsigned DiagID =
+        IsFirst ? diag::note_fe_backend_in : diag::note_fe_backend_inlined;
+    Diags.Report(Loc, DiagID) << llvm::demangle(FuncName.str());
+  };
+
+  // Try debug info first for accurate source locations.
+  if (!D.getDebugInlineChain().empty()) {
+    SourceManager &SM = Context->getSourceManager();
+    FileManager &FM = SM.getFileManager();
+    for (const auto &[I, Info] : llvm::enumerate(D.getDebugInlineChain())) {
+      SourceLocation Loc;
+      if (Info.Line > 0)
+        if (auto FE = FM.getOptionalFileRef(Info.Filename))
+          Loc = SM.translateFileLineCol(*FE, Info.Line,
+                                        Info.Column ? Info.Column : 1);
+      EmitNote(Loc, Info.FuncName, I == 0);
+    }
+    return;
+  }
+
+  // Fall back to heuristic (srcloc metadata) when debug info is unavailable.
+  auto InliningDecisions = D.getInliningDecisions();
+  if (InliningDecisions.empty())
+    return;
+
+  for (const auto &[I, Entry] : llvm::enumerate(InliningDecisions)) {
+    SourceLocation Loc =
+        I == 0 ? LocCookie : SourceLocation::getFromRawEncoding(Entry.second);
+    EmitNote(Loc, Entry.first, I == 0);
+  }
+
+  // Suggest enabling debug info (at least -gline-directives-only) for more
+  // accurate locations.
+  Diags.Report(LocCookie, diag::note_fe_backend_inlining_debug_info);
 }
 
 void BackendConsumer::MisExpectDiagHandler(
diff --git a/clang/lib/Driver/ToolChains/Clang.cpp b/clang/lib/Driver/ToolChains/Clang.cpp
index 6416baf9126ff..e80409bf1e874 100644
--- a/clang/lib/Driver/ToolChains/Clang.cpp
+++ b/clang/lib/Driver/ToolChains/Clang.cpp
@@ -4316,6 +4316,15 @@ static void RenderDiagnosticsOptions(const Driver &D, const ArgList &Args,
     CmdArgs.push_back(Args.MakeArgString(Opt));
   }
 
+  if (const Arg *A =
+          Args.getLastArg(options::OPT_fdiagnostics_show_inlining_chain,
+                          options::OPT_fno_diagnostics_show_inlining_chain)) {
+    if (A->getOption().matches(options::OPT_fdiagnostics_show_inlining_chain))
+      CmdArgs.push_back("-fdiagnostics-show-inlining-chain");
+    else
+      CmdArgs.push_back("-fno-diagnostics-show-inlining-chain");
+  }
+
   if (const Arg *A = Args.getLastArg(options::OPT_fdiagnostics_format_EQ)) {
     CmdArgs.push_back("-fdiagnostics-format");
     CmdArgs.push_back(A->getValue());
diff --git a/clang/test/Frontend/backend-attribute-inlining-cross-tu.c b/clang/test/Frontend/backend-attribute-inlining-cross-tu.c
new file mode 100644
index 0000000000000..3d67ae946a230
--- /dev/null
+++ b/clang/test/Frontend/backend-attribute-inlining-cross-tu.c
@@ -0,0 +1,67 @@
+// RUN: rm -rf %t
+// RUN: split-file %s %t
+// RUN: not %clang -O2 -fdiagnostics-show-inlining-chain -S %t/main.c -I%t -o /dev/null 2>&1 | FileCheck %s
+
+// Cross-TU inlining: header functions inlined into source file.
+
+//--- overflow.h
+[[gnu::warning("write overflow")]]
+void __write_overflow(void);
+
+[[gnu::error("read overflow")]]
+void __read_overflow(void);
+
+static inline void check_write(int size) {
+  if (size > 100)
+    __write_overflow();
+}
+
+static inline void check_read(int size) {
+  if (size > 50)
+    __read_overflow();
+}
+
+static inline void check_both(int size) {
+  check_write(size);
+  check_read(size);
+}
+
+//--- main.c
+#include "overflow.h"
+
+void test_simple_cross_tu(void) {
+  check_write(200);
+}
+// CHECK: warning: call to '__write_overflow' declared with 'warning' attribute: write overflow
+// CHECK: note: called by function 'check_write'
+// CHECK: main.c:{{.*}}: note: inlined by function 'test_simple_cross_tu'
+
+// Nested cross-TU inlining (header -> header -> source).
+static inline void local_wrapper(int x) {
+  check_both(x);
+}
+
+void test_nested_cross_tu(void) {
+  local_wrapper(200);
+}
+// CHECK: warning: call to '__write_overflow' declared with 'warning' attribute: write overflow
+// CHECK: note: called by function 'check_write'
+// CHECK: overflow.h:{{.*}}: note: inlined by function 'check_both'
+// CHECK: main.c:{{.*}}: note: inlined by function 'local_wrapper'
+// CHECK: main.c:{{.*}}: note: inlined by function 'test_nested_cross_tu'
+
+// CHECK: error: call to '__read_overflow' declared with 'error' attribute: read overflow
+// CHECK: note: called by function 'check_read'
+// CHECK: overflow.h:{{.*}}: note: inlined by function 'check_both'
+// CHECK: main.c:{{.*}}: note: inlined by function 'local_wrapper'
+// CHECK: main.c:{{.*}}: note: inlined by function 'test_nested_cross_tu'
+
+void test_error_cross_tu(void) {
+  check_read(100);
+}
+// CHECK: error: call to '__read_overflow' declared with 'error' attribute: read overflow
+// CHECK: note: called by function 'check_read'
+// CHECK: main.c:{{.*}}: note: inlined by function 'test_error_cross_tu'
+
+// Fallback note should appear (no debug info).
+// CHECK: note: use '-gline-directives-only' (implied by '-g1') or higher for more accurate inlining chain locations
diff --git a/clang/test/Frontend/backend-attribute-inlining-debug-vs-heuristic.cpp b/clang/test/Frontend/backend-attribute-inlining-debug-vs-heuristic.cpp
new file mode 100644
index 0000000000000..3a58be1327844
--- /dev/null
+++ b/clang/test/Frontend/backend-attribute-inlining-debug-vs-heuristic.cpp
@@ -0,0 +1,92 @@
+// RUN: %clang_cc1 -O2 -emit-obj -fdiagnostics-show-inlining-chain %s -o /dev/null 2>&1 | FileCheck %s --check-prefix=HEURISTIC
+// RUN: %clang_cc1 -O2 -emit-obj -fdiagnostics-show-inlining-chain -debug-info-kind=line-directives-only %s -o /dev/null 2>&1 | FileCheck %s --check-prefix=DEBUG
+
+// Verify auto-selection works between debug info and heuristic fallback. When
+// we have at least -gline-directives-only we can use DILocation for accurate
+// inline locations.
+
+// Without that debug info we fall back to a heuristic approach using srcloc
+// metadata.
+
+[[gnu::warning("dangerous function")]]
+void dangerous();
+
+// Non-static, non-inline functions that get inlined at -O2.
+void wrapper() {
+    dangerous();
+}
+
+void middle() {
+    wrapper();
+}
+
+void caller() {
+    middle();
+}
+
+
+// HEURISTIC: :16:{{.*}}: warning: call to '{{.*}}dangerous{{.*}}'
+// HEURISTIC: :16:{{.*}}: note: called by function '{{.*}}wrapper{{.*}}'
+// HEURISTIC: :16:{{.*}}: note: inlined by function '{{.*}}middle{{.*}}'
+// HEURISTIC: :16:{{.*}}: note: inlined by function '{{.*}}caller{{.*}}'
+// HEURISTIC: note: use '-gline-directives-only' (implied by '-g1') or higher for more accurate inlining chain locations
+
+// DEBUG: :16:{{.*}}: warning: call to '{{.*}}dangerous{{.*}}'
+// DEBUG: :16:{{.*}}: note: called by function '{{.*}}wrapper{{.*}}'
+// DEBUG: :20:{{.*}}: note: inlined by function '{{.*}}middle{{.*}}'
+// DEBUG: :24:{{.*}}: note: inlined by function '{{.*}}caller{{.*}}'
+// DEBUG-NOT: note: use '-gline-directives-only'
+
+// Test that functions in anonymous namespaces are properly tracked for
+// inlining chain diagnostics. Anonymous namespace functions have internal
+// linkage and are prime candidates for inlining.
+
+[[gnu::warning("do not call")]]
+void bad_func();
+
+namespace {
+void anon_helper() {
+    bad_func();
+}
+
+void anon_middle() {
+    anon_helper();
+}
+} // namespace
+
+void public_caller() {
+    anon_middle();
+}
+
+// HEURISTIC: :49:{{.*}}: warning: call to '{{.*}}bad_func{{.*}}'
+// HEURISTIC: :49:{{.*}}: note: called by function '{{.*}}anon_helper{{.*}}'
+// HEURISTIC: :53:{{.*}}: note: inlined by function '{{.*}}anon_middle{{.*}}'
+// HEURISTIC: :58:{{.*}}: note: inlined by function '{{.*}}public_caller{{.*}}'
+
+// DEBUG: :49:{{.*}}: warning: call to '{{.*}}bad_func{{.*}}'
+// DEBUG: :49:{{.*}}: note: called by function '{{.*}}anon_helper{{.*}}'
+// DEBUG: :53:{{.*}}: note: inlined by function '{{.*}}anon_middle{{.*}}'
+// DEBUG: :58:{{.*}}: note: inlined by function '{{.*}}public_caller{{.*}}'
+
+// always_inline forces inlining but doesn't imply
+// isInlined() in the language sense.
+
+[[gnu::warning("always inline warning")]]
+void always_inline_target();
+
+__attribute__((always_inline))
+void always_inline_wrapper() {
+    always_inline_target();
+}
+
+void always_inline_caller() {
+    always_inline_wrapper();
+}
+
+// HEURISTIC: :79:{{.*}}: warning: call to '{{.*}}always_inline_target{{.*}}'
+// HEURISTIC: :79:{{.*}}: note: called by function '{{.*}}always_inline_wrapper{{.*}}'
+// HEURISTIC: :83:{{.*}}: note: inlined by function '{{.*}}always_inline_caller{{.*}}'
+
+// DEBUG: :79:{{.*}}: warning: call to '{{.*}}always_inline_target{{.*}}'
+// DEBUG: :79:{{.*}}: note: called by function '{{.*}}always_inline_wrapper{{.*}}'
+// DEBUG: :83:{{.*}}: note: inlined by function '{{.*}}always_inline_caller{{.*}}'
diff --git a/clang/test/Frontend/backend-attribute-inlining-modes.c b/clang/test/Frontend/backend-attribute-inlining-modes.c
new file mode 100644
index 0000000000000..adc010671fffd
--- /dev/null
+++ b/clang/test/Frontend/backend-attribute-inlining-modes.c
@@ -0,0 +1,42 @@
+// RUN: %clang_cc1 -O2 -emit-obj %s -o /dev/null 2>&1 | FileCheck %s --check-prefix=DISABLED
+// RUN: %clang_cc1 -O2 -emit-obj -fdiagnostics-show-inlining-chain %s -o /dev/null 2>&1 | FileCheck %s --check-prefix=ENABLED-HEURISTIC
+// RUN: %clang_cc1 -O2 -emit-obj -fdiagnostics-show-inlining-chain -debug-info-kind=line-directives-only %s -o /dev/null 2>&1 | FileCheck %s --check-prefix=ENABLED-DEBUG
+
+// Test -fdiagnostics-show-inlining-chain behavior:
+// - Disabled (default): warning only, no inlining notes.
+// - Enabled without debug info: heuristic fallback + suggestion note.
+// - Enabled with debug info: accurate locations from DILocation.
+
+[[gnu::warning("do not call")]]
+void bad_func(void);
+
+static inline void level1(void) {
+    bad_func();
+}
+
+static inline void level2(void) {
+    level1();
+}
+
+void entry(void) {
+    level2();
+}
+
+// Disabled (default): warning only, no inlining notes.
+// DISABLED: warning: call to 'bad_func'
+// DISABLED-NOT: note:
+
+// Enabled without debug info: heuristic fallback.
+// All notes point to original call site (:14).
+// ENABLED-HEURISTIC: :14:{{.*}}: warning: call to 'bad_func'
+// ENABLED-HEURISTIC: :14:{{.*}}: note: called by function 'level1'
+// ENABLED-HEURISTIC: :18:{{.*}}: note: inlined by function 'level2'
+// ENABLED-HEURISTIC: :22:{{.*}}: note: inlined by function 'entry'
+// ENABLED-HEURISTIC: note: use '-gline-directives-only' (implied by '-g1') or higher for more accurate inlining chain locations
+
+// Enabled with debug info: accurate locations.
+// ENABLED-DEBUG: :14:{{.*}}: warning: call to 'bad_func'
+// ENABLED-DEBUG: :14:{{.*}}: note: called by function 'level1'
+// ENABLED-DEBUG: :18:{{.*}}: note: inlined by function 'level2'
+// ENABLED-DEBUG: :22:{{.*}}: note: inlined by function 'entry'
+// ENABLED-DEBUG-NOT: note: use '-gline-directives-only'
diff --git a/clang/test/Frontend/backend-attribute-inlining.c b/clang/test/Frontend/backend-attribute-inlining.c
new file mode 100644
index 0000000000000..734ce0fc30bba
--- /dev/null
+++ b/clang/test/Frontend/backend-attribute-inlining.c
@@ -0,0 +1,112 @@
+// RUN: not %clang -O2 -fdiagnostics-show-inlining-chain -S %s -o /dev/null 2>&1 | FileCheck %s
+
+// Single-level inlining with warning attribute.
+[[gnu::warning("do not call directly")]]
+void __warn_single(void);
+
+static inline void warn_wrapper(void) {
+  __warn_single();
+}
+
+void test_single_level(void) {
+  warn_wrapper();
+}
+// CHECK: warning: call to '__warn_single' declared with 'warning' attribute: do not call directly
+// CHECK: note: called by function 'warn_wrapper'
+// CHECK: :12:{{.*}}: note: inlined by function 'test_single_level'
+
+// Error attribute with inlining.
+[[gnu::error("never call this")]]
+void __error_func(void);
+
+static inline void error_wrapper(void) {
+  __error_func();
+}
+
+void test_error_inlined(void) {
+  error_wrapper();
+}
+// CHECK: error: call to '__error_func' declared with 'error' attribute: never call this
+// CHECK: note: called by function 'error_wrapper'
+// CHECK: :27:{{.*}}: note: inlined by function 'test_error_inlined'
+
+// Deep nesting (5 levels).
+[[gnu::warning("deep call")]]
+void __warn_deep(void);
+
+static inline void deep1(void) { __warn_deep(); }
+static inline void deep2(void) { deep1(); }
+static inline void deep3(void) { deep2(); }
+static inline void deep4(void) { deep3(); }
+static inline void deep5(void) { deep4(); }
+
+void test_deep_nesting(void) {
+  deep5();
+}
+// CHECK: warning: call to '__warn_deep' declared with 'warning' attribute: deep call
+// CHECK: note: called by function 'deep1'
+// CHECK: :38:{{.*}}: note: inlined by function 'deep2'
+// CHECK: :39:{{.*}}: note: inlined by function 'deep3'
+// CHECK: :40:{{.*}}: note: inlined by function 'deep4'
+// CHECK: :41:{{.*}}: note: inlined by function 'deep5'
+// CHECK: :44:{{.*}}: note: inlined by function 'test_deep_nesting'
+
+// Multiple call sites produce distinct diagnostics.
+[[gnu::warning("deprecated")]]
+void __warn_multi(void);
+
+static inline void multi_wrapper(void) {
+  __warn_multi();
+}
+
+void call_site_a(void) { multi_wrapper(); }
+void call_site_b(void) { multi_wrapper(); }
+void call_site_c(void) { multi_wrapper(); }
+
+// CHECK: warning: call to '__warn_multi' declared with 'warning' attribute: deprecated
+// CHECK: note: called by function 'multi_wrapper'
+// CHECK: :62:{{.*}}: note: inlined by function 'call_site_a'
+
+// CHECK: warning: call to '__warn_multi' declared with 'warning' attribute: deprecated
+// CHECK: note: called by function 'multi_wrapper'
+// CHECK: :63:{{.*}}: note: inlined by function 'call_site_b'
+
+// CHECK: warning: call to '__warn_multi' declared with 'warning' attribute: deprecated
+// CHECK: note: called by function 'multi_wrapper'
+// CHECK: :64:{{.*}}: note: inlined by function 'call_site_c'
+
+// Different nesting depths from same inner function.
+[[gnu::warning("mixed depth")]]
+void __warn_mixed(void);
+
+static inline void mixed_inner(void) { __warn_mixed(); }
+static inline void mixed_middle(void) { mixed_inner(); }
+
+void shallow(void) { mixed_inner(); }
+void deep(void) { mixed_middle(); }
+
+// CHECK: warning: call to '__warn_mixed' declared with 'warning' attribute: mixed depth
+// CHECK: note: called by function 'mixed_inner'
+// CHECK: :85:{{.*}}: note: inlined by function 'shallow'
+
+// CHECK: warning: call to '__warn_mixed' declared with 'warning' attribute: mixed depth
+// CHECK: note: called by function 'mixed_inner'
+// CHECK: :83:{{.*}}: note: inlined by function 'mixed_middle'
+// CHECK: :86:{{.*}}: note: inlined by function 'deep'
+
+// Incidental inlining (function not marked inline/static).
+// The "inlined by" note has no location since heuristic mode doesn't track it.
+[[gnu::warning("incidental")]]
+void __warn_incidental(void);
+
+void not_marked_inline(void) { __warn_incidental(); }
+
+void test_incidental(void) { not_marked_inline(); }
+
+// CHECK: warning: call to '__warn_incidental' declared with 'warning' attribute: incidental
+// CHECK: note: called by function 'not_marked_inline'
+// CHECK: note: inlined by function 'test_incidental'
+// CHECK-NOT: :{{.*}}: note: inlined by function 'test_incidental'
+
+// Fallback note should appear (no debug info).
+// CHECK: note: use '-gline-directives-only' (implied by '-g1') or higher for more accurate inlining chain locations
diff --git a/llvm/include/llvm/IR/DiagnosticInfo.h b/llvm/include/llvm/IR/DiagnosticInfo.h
index 8f6fb4da0c839..0daaa462ba715 100644
--- a/llvm/include/llvm/IR/DiagnosticInfo.h
+++ b/llvm/include/llvm/IR/DiagnosticInfo.h
@@ -1192,19 +1192,41 @@ class LLVM_ABI DiagnosticInfoSrcMgr : public DiagnosticInfo {
 
 LLVM_ABI void diagnoseDontCall(const CallInst &CI);
 
+/// Inlining location extracted from debug info.
+struct DebugInlineInfo {
+  StringRef FuncName;
+  StringRef Filename;
+  unsigned Line;
+  unsigned Column;
+};
+
 class LLVM_ABI DiagnosticInfoDontCall : public DiagnosticInfo {
   StringRef CalleeName;
   StringRef Note;
   uint64_t LocCookie;
+  MDNode *InlinedFromMD = nullptr;
+  SmallVector<DebugInlineInfo, 4> DebugInlineChain;
 
 public:
   DiagnosticInfoDontCall(StringRef CalleeName, StringRef Note,
-                         DiagnosticSeverity DS, uint64_t LocCookie)
+                         DiagnosticSeverity DS, uint64_t LocCookie,
+                         MDNode *InlinedFromMD = nullptr)
       : DiagnosticInfo(DK_DontCall, DS), CalleeName(CalleeName), Note(Note),
-        LocCookie(LocCookie) {}
+        LocCookie(LocCookie), InlinedFromMD(InlinedFromMD) {}
+
   StringRef getFunctionName() const { return CalleeName; }
   StringRef getNote() const { return Note; }
   uint64_t getLocCookie() const { return LocCookie; }
+  MDNode *getInlinedFromMD() const { return InlinedFromMD; }
+  SmallVector<std::pair<StringRef, uint64_t>> getInliningDecisions() const;
+
+  void setDebugInlineChain(SmallVector<DebugInlineInfo, 4> &&Chain) {
+    DebugInlineChain = std::move(Chain);
+  }
+  ArrayRef<DebugInlineInfo> getDebugInlineChain() const {
+    return DebugInlineChain;
+  }
+
   void print(DiagnosticPrinter &DP) const override;
   static bool classof(const DiagnosticInfo *DI) {
     return DI->getKind() == DK_DontCall;
diff --git a/llvm/lib/IR/DiagnosticInfo.cpp b/llvm/lib/IR/DiagnosticInfo.cpp
index e48016fc4165f..73bf0eca0b964 100644
--- a/llvm/lib/IR/DiagnosticInfo.cpp
+++ b/llvm/lib/IR/DiagnosticInfo.cpp
@@ -484,8 +484,27 @@ void llvm::diagnoseDontCall(const CallInst &CI) {
       if (MDNode *MD = CI.getMetadata("srcloc"))
         LocCookie =
             mdconst::extract<ConstantInt>(MD->getOperand(0))->getZExtValue();
+      MDNode *InlinedFromMD = CI.getMetadata("inlined.from");
       DiagnosticInfoDontCall D(F->getName(), A.getValueAsString(), Sev,
-                               LocCookie);
+                               LocCookie, InlinedFromMD);
+
+      if (const DebugLoc &DL = CI.getDebugLoc()) {
+        SmallVector<DebugInlineInfo, 4> DebugChain;
+        auto AddLocation = [&](const DILocation *Loc) {
+          if (auto *Scope = Loc->getScope())
+            if (auto *SP = Scope->getSubprogram())
+              DebugChain.push_back({SP->getName(), Loc->getFilename(),
+                                    Loc->getLine(), Loc->getColumn()});
+        };
+        if (const DILocation *Loc = DL.get()) {
+          AddLocation(Loc);
+          for (const DILocation *InlinedAt = Loc->getInlinedAt(); InlinedAt;
+               InlinedAt = InlinedAt->getInlinedAt())
+            AddLocation(InlinedAt);
+        }
+        D.setDebugInlineChain(std::move(DebugChain));
+      }
+
       F->getContext().diagnose(D);
     }
   }
@@ -500,3 +519,20 @@ void DiagnosticInfoDontCall::print(DiagnosticPrinter &DP) const {
   if (!getNote().empty())
     DP << ": " << getNote();
 }
+
+SmallVector<std::pair<StringRef, uint64_t>>
+DiagnosticInfoDontCall::getInliningDecisions() const {
+  SmallVector<std::pair<StringRef, uint64_t>> Chain;
+  if (!InlinedFromMD)
+    return Chain;
+
+  for (unsigned I = 0, E = InlinedFromMD->getNumOperands(); I + 1 < E; I += 2) {
+    auto *NameMD = dyn_cast<MDString>(InlinedFromMD->getOperand(I));
+    auto *LocMD =
+        mdconst::dyn_extract<ConstantInt>(InlinedFromMD->getOperand(I + 1));
+    if (NameMD && !NameMD->getString().empty())
+      Chain.emplace_back(NameMD->getString(),
+                         LocMD ? LocMD->getZExtValue() : 0);
+  }
+  return Chain;
+}
diff --git a/llvm/lib/Transforms/Utils/InlineFunction.cpp b/llvm/lib/Transforms/Utils/InlineFunction.cpp
index 0c0d0f22d0367..1c4df54b60f46 100644
--- a/llvm/lib/Transforms/Utils/InlineFunction.cpp
+++ b/llvm/lib/Transforms/Utils/InlineFunction.cpp
@@ -974,6 +974,46 @@ static void PropagateCallSiteMetadata(CallBase &CB, Function::iterator FStart,
   }
 }
 
+/// Track inlining chain via inlined.from metadata for dontcall diagnostics.
+static void PropagateInlinedFromMetadata(CallBase &CB, StringRef CalledFuncName,
+                                         StringRef CallerFuncName,
+                                         Function::iterator FStart,
+                                         Function::iterator FEnd) {
+  LLVMContext &Ctx = CB.getContext();
+  uint64_t InlineSiteLoc = 0;
+  if (auto *MD = CB.getMetadata("srcloc"))
+    if (auto *CI = mdconst::dyn_extract<ConstantInt>(MD->getOperand(0)))
+      InlineSiteLoc = CI->getZExtValue();
+
+  auto *I64Ty = Type::getInt64Ty(Ctx);
+  auto MakeMDInt = [&](uint64_t V) {
+    return ConstantAsMetadata::get(ConstantInt::get(I64Ty, V));
+  };
+
+  for (BasicBlock &BB : make_range(FStart, FEnd)) {
+    for (Instruction &I : BB) {
+      auto *CI = dyn_cast<CallInst>(&I);
+      if (!CI || !CI->getMetadata("srcloc"))
+        continue;
+      auto *Callee = CI->getCalledFunction();
+      if (!Callee || (!Callee->hasFnAttribute("dontcall-error") &&
+                      !Callee->hasFnAttribute("dontcall-warn")))
+        continue;
+
+      SmallVector<Metadata *, 8> Ops;
+      if (MDNode *Existing = CI->getMetadata("inlined.from"))
+        append_range(Ops, Existing->operands());
+      else {
+        Ops.push_back(MDString::get(Ctx, CalledFuncName));
+        Ops.push_back(MakeMDInt(0));
+      }
+      Ops.push_back(MDString::get(Ctx, CallerFuncName));
+      Ops.push_back(MakeMDInt(InlineSiteLoc));
+      CI->setMetadata("inlined.from", MDNode::get(Ctx, Ops));
+    }
+  }
+}
+
 /// Bundle operands of the inlined function must be added to inlined call sites.
 static void PropagateOperandBundles(Function::iterator InlinedBB,
                                     Instruction *CallSiteEHPad) {
@@ -2851,6 +2891,10 @@ void llvm::InlineFunctionImpl(CallBase &CB, InlineFunctionInfo &IFI,
       }
     }
 
+    // Propagate inlined.from metadata for dontcall diagnostics.
+    PropagateInlinedFromMetadata(CB, CalledFunc->getName(), Caller->getName(),
+                                 FirstNewBlock, Caller->end());
+
     // Register any cloned assumptions.
     if (IFI.GetAssumptionCache)
       for (BasicBlock &NewBlock :



More information about the cfe-commits mailing list