[clang] [clang][analyzer] Forward CTU-import failure conditions (PR #189064)

Arseniy Zaostrovnykh via cfe-commits cfe-commits at lists.llvm.org
Sun Mar 29 23:08:20 PDT 2026


https://github.com/necto updated https://github.com/llvm/llvm-project/pull/189064

>From 78332b472ef9a9e3daf7337e47593efb176384c0 Mon Sep 17 00:00:00 2001
From: Arseniy Zaostrovnykh <necto.ne at gmail.com>
Date: Fri, 27 Mar 2026 16:02:14 +0100
Subject: [PATCH 1/7] Draft errors and warnings for the CTU import failures

---
 .../clang/Basic/DiagnosticCrossTUKinds.td     |  24 +++
 .../clang/CrossTU/CrossTranslationUnit.h      |  14 +-
 clang/lib/CrossTU/CrossTranslationUnit.cpp    | 144 ++++++++++++++----
 .../Analysis/ctu/diag/invlist-ambiguous.cpp   |   3 +-
 .../test/Analysis/ctu/diag/invlist-empty.cpp  |   3 +-
 .../Analysis/ctu/diag/invlist-lookup-miss.cpp |   3 +-
 .../Analysis/ctu/diag/invlist-missing.cpp     |   3 +-
 .../ctu/diag/invlist-wrong-format.cpp         |   3 +-
 .../ctu/diag/lang-dialect-mismatch.cpp        |   3 +-
 clang/test/Analysis/ctu/diag/lang-mismatch.c  |   3 +-
 clang/test/Analysis/ctu/invalid-ast.cpp       |   3 +-
 clang/test/Analysis/ctu/main.c                |   2 +
 clang/test/Analysis/ctu/missing-ast.cpp       |   3 +-
 clang/test/Analysis/ctu/on-demand-parsing.c   |   1 +
 .../test/Analysis/ctu/test-import-failure.cpp |   1 +
 15 files changed, 162 insertions(+), 51 deletions(-)

diff --git a/clang/include/clang/Basic/DiagnosticCrossTUKinds.td b/clang/include/clang/Basic/DiagnosticCrossTUKinds.td
index e6ea1956f98a6..4374e85b114bd 100644
--- a/clang/include/clang/Basic/DiagnosticCrossTUKinds.td
+++ b/clang/include/clang/Basic/DiagnosticCrossTUKinds.td
@@ -15,10 +15,34 @@ def err_extdefmap_parsing : Error<
   "error parsing index file: '%0' line: %1 '<USR-Length>:<USR> <File-Path>' "
   "format expected">;
 
+def err_invlist_parsing : Error<
+  "error parsing invocation list file: '%0' line: %1 "
+  "'<source-file>: [<compiler>, <arg1>, ...]' YAML mapping format expected">;
+
 def err_multiple_def_index : Error<
   "multiple definitions are found for the same key in index ">;
 
+def warn_multiple_entries_invlist : Warning<
+  "multiple invocations for '%0' are found in the invocation list ">;
+
+def warn_invlist_missing_file : Warning<
+  "invocation for '%0' is missing in the invocation list">, InGroup<CrossTU>;
+
 def warn_ctu_incompat_triple : Warning<
   "imported AST from '%0' had been generated for a different target, "
   "current: %1, imported: %2">, InGroup<CrossTU>;
+
+def warn_ctu_import_failure : Warning<
+  "import of an external symbol for CTU failed: %0">, InGroup<CrossTU>;
+
+def err_ctu_import_failure: Error<
+  "import of an external symbol for CTU failed: %0">;
+
+def remark_ctu_import_threshold_reached: Remark<
+  "reached a the CTU-import threshold before trying to import definition">,
+  InGroup<CrossTU>;
+
+def warn_ctu_incompat_lang : Warning<
+  "imported AST from '%0' had been generated for a different language, "
+  "current: %1, imported: %2">, InGroup<CrossTU>;
 }
diff --git a/clang/include/clang/CrossTU/CrossTranslationUnit.h b/clang/include/clang/CrossTU/CrossTranslationUnit.h
index 145bc8df27de6..54438026f29f0 100644
--- a/clang/include/clang/CrossTU/CrossTranslationUnit.h
+++ b/clang/include/clang/CrossTU/CrossTranslationUnit.h
@@ -67,22 +67,22 @@ class IndexError : public llvm::ErrorInfo<IndexError> {
   IndexError(index_error_code C, std::string FileName, std::string TripleToName,
              std::string TripleFromName)
       : Code(C), FileName(std::move(FileName)),
-        TripleToName(std::move(TripleToName)),
-        TripleFromName(std::move(TripleFromName)) {}
+        ConfigToName(std::move(TripleToName)),
+        ConfigFromName(std::move(TripleFromName)) {}
   void log(raw_ostream &OS) const override;
   std::error_code convertToErrorCode() const override;
   index_error_code getCode() const { return Code; }
   int getLineNum() const { return LineNo; }
   std::string getFileName() const { return FileName; }
-  std::string getTripleToName() const { return TripleToName; }
-  std::string getTripleFromName() const { return TripleFromName; }
+  std::string getConfigToName() const { return ConfigToName; }
+  std::string getConfigFromName() const { return ConfigFromName; }
 
 private:
   index_error_code Code;
   std::string FileName;
   int LineNo;
-  std::string TripleToName;
-  std::string TripleFromName;
+  std::string ConfigToName;
+  std::string ConfigFromName;
 };
 
 /// This function parses an index file that determines which
@@ -264,7 +264,7 @@ class CrossTranslationUnitContext {
     /// In case of on-demand parsing, the invocations for parsing the source
     /// files is stored.
     std::optional<InvocationListTy> InvocationList;
-    index_error_code PreviousParsingResult = index_error_code::success;
+    std::optional<IndexError> PreviousError;
   };
 
   /// Maintain number of AST loads and check for reaching the load limit.
diff --git a/clang/lib/CrossTU/CrossTranslationUnit.cpp b/clang/lib/CrossTU/CrossTranslationUnit.cpp
index 8dd0ef13123d1..cff9352a315fb 100644
--- a/clang/lib/CrossTU/CrossTranslationUnit.cpp
+++ b/clang/lib/CrossTU/CrossTranslationUnit.cpp
@@ -88,6 +88,29 @@ bool hasEqualKnownFields(const llvm::Triple &Lhs, const llvm::Triple &Rhs) {
   return true;
 }
 
+/// Returns a human-readable language/dialect description for diagnostics.
+/// Checks flags from highest to lowest standard since they are cumulative
+/// (e.g. CPlusPlus20 implies CPlusPlus17).
+/// This does not cover all possible languages (e.g. Obj-C or flavors of C),
+/// because CTU currently does not differentiate between them.
+static std::string getLangDescription(const LangOptions &LO) {
+  if (!LO.CPlusPlus)
+    return "non-C++";
+  if (LO.CPlusPlus26)
+    return "C++26";
+  if (LO.CPlusPlus23)
+    return "C++23";
+  if (LO.CPlusPlus20)
+    return "C++20";
+  if (LO.CPlusPlus17)
+    return "C++17";
+  if (LO.CPlusPlus14)
+    return "C++14";
+  if (LO.CPlusPlus11)
+    return "C++11";
+  return "C++98";
+}
+
 // FIXME: This class is will be removed after the transition to llvm::Error.
 class IndexErrorCategory : public std::error_category {
 public:
@@ -330,7 +353,9 @@ llvm::Expected<const T *> CrossTranslationUnitContext::getCrossTUDefinitionImpl(
   // different dialects of C++.
   if (LangTo.CPlusPlus != LangFrom.CPlusPlus) {
     ++NumLangMismatch;
-    return llvm::make_error<IndexError>(index_error_code::lang_mismatch);
+    return llvm::make_error<IndexError>(
+        index_error_code::lang_mismatch, std::string(Unit->getMainFileName()),
+        getLangDescription(LangTo), getLangDescription(LangFrom));
   }
 
   // If CPP dialects are different then return with error.
@@ -351,8 +376,10 @@ llvm::Expected<const T *> CrossTranslationUnitContext::getCrossTUDefinitionImpl(
       LangTo.CPlusPlus17 != LangFrom.CPlusPlus17 ||
       LangTo.CPlusPlus20 != LangFrom.CPlusPlus20) {
     ++NumLangDialectMismatch;
-    return llvm::make_error<IndexError>(
-        index_error_code::lang_dialect_mismatch);
+    return llvm::make_error<IndexError>(index_error_code::lang_dialect_mismatch,
+                                        std::string(Unit->getMainFileName()),
+                                        getLangDescription(LangTo),
+                                        getLangDescription(LangFrom));
   }
 
   TranslationUnitDecl *TU = Unit->getASTContext().getTranslationUnitDecl();
@@ -386,36 +413,92 @@ void CrossTranslationUnitContext::emitCrossTUDiagnostics(const IndexError &IE,
     Context.getDiagnostics().Report(Loc, diag::err_ctu_error_opening)
         << IE.getFileName();
     return;
+
   case index_error_code::invalid_index_format:
     Context.getDiagnostics().Report(Loc, diag::err_extdefmap_parsing)
         << IE.getFileName() << IE.getLineNum();
     return;
+
   case index_error_code::multiple_definitions:
     Context.getDiagnostics().Report(Loc, diag::err_multiple_def_index)
         << IE.getLineNum();
     return;
+
   case index_error_code::triple_mismatch:
     Context.getDiagnostics().Report(Loc, diag::warn_ctu_incompat_triple)
-        << IE.getFileName() << IE.getTripleToName() << IE.getTripleFromName();
-    return;
-  case index_error_code::success:
-    llvm_unreachable("There should not be a success error. This case should "
-                     "have been handled by the caller.");
+        << IE.getFileName() << IE.getConfigToName() << IE.getConfigFromName();
     return;
-  case index_error_code::unspecified:
+
   case index_error_code::missing_definition:
+    // Ignore missing definitions because it is very common to have some symbols
+    // defined outside of the analysis scope: they may be defined in 3-rd party
+    // and standard libraries, generated code, and files excluded from the
+    // analysis.
+    // Even ignoring it with Ignored diagnostic might generate too much traffic.
+    return;
+
   case index_error_code::failed_import:
-  case index_error_code::failed_to_get_external_ast:
+  case index_error_code::unspecified: // FIXME: remove?
+    // Not clear what happened exactly, but the outcome is a missing definition
+    // This is not a big deal, and is expected since ASTImporter is incomplete.
+    Context.getDiagnostics().Report(Loc, diag::warn_ctu_import_failure)
+        << Category->message(static_cast<int>(IE.getCode()));
+    return;
+
   case index_error_code::failed_to_generate_usr:
+    // This is unlikely, so it is worth looking into, hence an error.
+  case index_error_code::failed_to_get_external_ast:
+    // This is suspicious, since the external AST is mentioned in the external
+    // defmap, so it should exist.
+    Context.getDiagnostics().Report(Loc, diag::err_ctu_import_failure)
+        << Category->message(static_cast<int>(IE.getCode()));
+    return;
+
+  case index_error_code::invocation_list_file_not_found:
+    // If the external def-map refers to source files, you must provide an
+    // invocation list file. Otherwise, CTU does not work at all, so you should
+    // check your build and analysis configuration.
+    Context.getDiagnostics().Report(Loc, diag::err_ctu_error_opening)
+        << IE.getFileName();
+    return;
+
+  case index_error_code::load_threshold_reached:
+    // This is expected. It is still useful to be aware of, but it is normal operation.
+    Context.getDiagnostics().Report(Loc,
+                                    diag::remark_ctu_import_threshold_reached);
+    return;
+
   case index_error_code::lang_mismatch:
   case index_error_code::lang_dialect_mismatch:
-  case index_error_code::load_threshold_reached:
-  case index_error_code::invocation_list_ambiguous:
-  case index_error_code::invocation_list_file_not_found:
-  case index_error_code::invocation_list_empty:
+    // Similar to target triple mismatch.
+    Context.getDiagnostics().Report(Loc, diag::warn_ctu_incompat_lang)
+        << IE.getFileName() << IE.getConfigToName() << IE.getConfigFromName();
+    return;
+
   case index_error_code::invocation_list_wrong_format:
+  case index_error_code::invocation_list_empty: // FIXME: remove?
+    // Without parsable invocation list, CTU cannot function.
+    Context.getDiagnostics().Report(Loc, diag::err_invlist_parsing)
+        << IE.getFileName() << IE.getLineNum();
+    return;
+
+  case index_error_code::invocation_list_ambiguous:
+    // For automatically generated invocation lists, it is common to list multiple invocations,
+    // if a file is compiled in multiple contexts. No need to block CTU because of this.
+    Context.getDiagnostics().Report(Loc, diag::warn_multiple_entries_invlist)
+        << IE.getFileName();
+    return;
+
   case index_error_code::invocation_list_lookup_unsuccessful:
-    // FIXME: Silently dropping these errors
+    // Some files might be missing in the invocation list. It is sad but not fatal,
+    // and CTU can take advantage of the definitions in files with known invocations.
+    Context.getDiagnostics().Report(Loc, diag::warn_invlist_missing_file)
+        << IE.getFileName();
+    return;
+
+  case index_error_code::success:
+    llvm_unreachable("There should not be a success error. This case should "
+                     "have been handled by the caller.");
     return;
   }
   llvm_unreachable("Unrecognized index_error_code.");
@@ -624,7 +707,8 @@ CrossTranslationUnitContext::ASTLoader::loadFromSource(
   auto Invocation = InvocationList->find(SourceFilePath);
   if (Invocation == InvocationList->end())
     return llvm::make_error<IndexError>(
-        index_error_code::invocation_list_lookup_unsuccessful);
+        index_error_code::invocation_list_lookup_unsuccessful,
+        std::string(SourceFilePath));
 
   const InvocationListTy::mapped_type &InvocationCommand = Invocation->second;
 
@@ -695,7 +779,8 @@ parseInvocationList(StringRef FileContent, llvm::sys::path::Style PathStyle) {
 
     if (InvocationList.contains(InvocationKey))
       return llvm::make_error<IndexError>(
-          index_error_code::invocation_list_ambiguous);
+          index_error_code::invocation_list_ambiguous,
+          std::string(InvocationKey));
 
     /// The values should be sequences of strings, each representing a part of
     /// the invocation.
@@ -728,14 +813,15 @@ llvm::Error CrossTranslationUnitContext::ASTLoader::lazyInitInvocationList() {
   /// Lazily initialize the invocation list member used for on-demand parsing.
   if (InvocationList)
     return llvm::Error::success();
-  if (index_error_code::success != PreviousParsingResult)
-    return llvm::make_error<IndexError>(PreviousParsingResult);
+  if (PreviousError)
+    return llvm::make_error<IndexError>(*PreviousError);
 
   llvm::ErrorOr<std::unique_ptr<llvm::MemoryBuffer>> FileContent =
       CI.getVirtualFileSystem().getBufferForFile(InvocationListFilePath);
   if (!FileContent) {
-    PreviousParsingResult = index_error_code::invocation_list_file_not_found;
-    return llvm::make_error<IndexError>(PreviousParsingResult);
+    PreviousError = IndexError(index_error_code::invocation_list_file_not_found,
+                               std::string(InvocationListFilePath));
+    return llvm::make_error<IndexError>(*PreviousError);
   }
   std::unique_ptr<llvm::MemoryBuffer> ContentBuffer = std::move(*FileContent);
   assert(ContentBuffer && "If no error was produced after loading, the pointer "
@@ -744,12 +830,18 @@ llvm::Error CrossTranslationUnitContext::ASTLoader::lazyInitInvocationList() {
   llvm::Expected<InvocationListTy> ExpectedInvocationList =
       parseInvocationList(ContentBuffer->getBuffer(), PathStyle);
 
-  // Handle the error to store the code for next call to this function.
+  // Handle the error to store the code and filename for next call to this
+  // function.
   if (!ExpectedInvocationList) {
-    llvm::handleAllErrors(
-        ExpectedInvocationList.takeError(),
-        [&](const IndexError &E) { PreviousParsingResult = E.getCode(); });
-    return llvm::make_error<IndexError>(PreviousParsingResult);
+    llvm::handleAllErrors(ExpectedInvocationList.takeError(),
+                          [&](const IndexError &E) {
+                            PreviousError = IndexError(
+                                E.getCode(),
+                                E.getFileName().empty()
+                                    ? std::string(InvocationListFilePath)
+                                    : E.getFileName());
+                          });
+    return llvm::make_error<IndexError>(*PreviousError);
   }
 
   InvocationList = *ExpectedInvocationList;
diff --git a/clang/test/Analysis/ctu/diag/invlist-ambiguous.cpp b/clang/test/Analysis/ctu/diag/invlist-ambiguous.cpp
index 570c6c7fe2585..3c106cee78a56 100644
--- a/clang/test/Analysis/ctu/diag/invlist-ambiguous.cpp
+++ b/clang/test/Analysis/ctu/diag/invlist-ambiguous.cpp
@@ -14,6 +14,5 @@
 int foo(int);
 
 void test() {
-  // expected-no-diagnostics
-  foo(1); // no-warning. Ignoring "Invocation list file contains multiple references to the same source file."
+  foo(1); // expected-warning{{multiple invocations for '/some/path.cpp' are found in the invocation list}}
 }
diff --git a/clang/test/Analysis/ctu/diag/invlist-empty.cpp b/clang/test/Analysis/ctu/diag/invlist-empty.cpp
index b0504960b98b2..e952faad17873 100644
--- a/clang/test/Analysis/ctu/diag/invlist-empty.cpp
+++ b/clang/test/Analysis/ctu/diag/invlist-empty.cpp
@@ -18,6 +18,5 @@
 int foo(int);
 
 void test() {
-  // expected-no-diagnostics
-  foo(1); // no-warning. Ignoring "Invocation list file is in wrong format."
+  foo(1); // expected-error-re{{error parsing invocation list file: '{{.+}}invocations.yaml' line: 0 '<source-file>: [<compiler>, <arg1>, ...]' YAML mapping format expected}}
 }
diff --git a/clang/test/Analysis/ctu/diag/invlist-lookup-miss.cpp b/clang/test/Analysis/ctu/diag/invlist-lookup-miss.cpp
index 3adc035dff681..8a384e73fb005 100644
--- a/clang/test/Analysis/ctu/diag/invlist-lookup-miss.cpp
+++ b/clang/test/Analysis/ctu/diag/invlist-lookup-miss.cpp
@@ -16,6 +16,5 @@
 int foo(int);
 
 void test() {
-  // expected-no-diagnostics
-  foo(1); // no-warning. Ignoring "Invocation list file does not contain the requested source file."
+  foo(1); // expected-warning-re{{invocation for '{{.+}}diag-simple.cpp' is missing in the invocation list}}
 }
diff --git a/clang/test/Analysis/ctu/diag/invlist-missing.cpp b/clang/test/Analysis/ctu/diag/invlist-missing.cpp
index bac264ab10b4e..e4edab66ef2a1 100644
--- a/clang/test/Analysis/ctu/diag/invlist-missing.cpp
+++ b/clang/test/Analysis/ctu/diag/invlist-missing.cpp
@@ -15,6 +15,5 @@
 int foo(int);
 
 void test() {
-  // expected-no-diagnostics
-  foo(1); // no-warning. Ignoring "Invocation list file is not found."
+  foo(1); // expected-error-re{{error opening '{{.+}}nonexistent.yaml': required by the CrossTU functionality}}
 }
diff --git a/clang/test/Analysis/ctu/diag/invlist-wrong-format.cpp b/clang/test/Analysis/ctu/diag/invlist-wrong-format.cpp
index cfa3cc65e8d06..c7e42d16ca7fd 100644
--- a/clang/test/Analysis/ctu/diag/invlist-wrong-format.cpp
+++ b/clang/test/Analysis/ctu/diag/invlist-wrong-format.cpp
@@ -16,6 +16,5 @@
 int foo(int);
 
 void test() {
-  // expected-no-diagnostics
-  foo(1); // no-warning. Ignoring "Invocation list file is in wrong format."
+  foo(1); // expected-error-re{{error parsing invocation list file: '{{.+}}invocations.yaml' line: 0 '<source-file>: [<compiler>, <arg1>, ...]' YAML mapping format expected}}
 }
diff --git a/clang/test/Analysis/ctu/diag/lang-dialect-mismatch.cpp b/clang/test/Analysis/ctu/diag/lang-dialect-mismatch.cpp
index c41072d46df02..d08f69df756b8 100644
--- a/clang/test/Analysis/ctu/diag/lang-dialect-mismatch.cpp
+++ b/clang/test/Analysis/ctu/diag/lang-dialect-mismatch.cpp
@@ -16,6 +16,5 @@
 int foo(int);
 
 void test() {
-  // expected-no-diagnostics
-  foo(1); // no-warning. Ignoring "Invocation list file contains multiple references to the same source file."
+  foo(1); // expected-warning-re{{imported AST from '{{.+}}simple.cpp' had been generated for a different language, current: C++14, imported: C++17}}
 }
diff --git a/clang/test/Analysis/ctu/diag/lang-mismatch.c b/clang/test/Analysis/ctu/diag/lang-mismatch.c
index deee7077ec495..58cc659cadea7 100644
--- a/clang/test/Analysis/ctu/diag/lang-mismatch.c
+++ b/clang/test/Analysis/ctu/diag/lang-mismatch.c
@@ -18,6 +18,5 @@
 int foo(int);
 
 void test(void) {
-  // expected-no-diagnostics
-  foo(1); // no-warning. Ignoring "Language mismatch."
+  foo(1); // expected-warning-re{{imported AST from '{{.+}}simple-extern-c.cpp' had been generated for a different language, current: non-C++, imported: C++14}}
 }
diff --git a/clang/test/Analysis/ctu/invalid-ast.cpp b/clang/test/Analysis/ctu/invalid-ast.cpp
index 9d0b850233dbe..2026f88ab6a74 100644
--- a/clang/test/Analysis/ctu/invalid-ast.cpp
+++ b/clang/test/Analysis/ctu/invalid-ast.cpp
@@ -21,6 +21,5 @@
 void external();
 
 void trigger() {
-  // expected-no-diagnostics
-  external(); // no-warning
+  external(); // expected-error{{import of an external symbol for CTU failed: Failed to load external AST source.}}
 }
diff --git a/clang/test/Analysis/ctu/main.c b/clang/test/Analysis/ctu/main.c
index 928da3cbea038..b36ca4a1faa0e 100644
--- a/clang/test/Analysis/ctu/main.c
+++ b/clang/test/Analysis/ctu/main.c
@@ -101,6 +101,8 @@ void testStructDefInArgument(void) {
   // Not imported, thus remains unknown both in stu and ctu.
   clang_analyzer_eval(structInProto(&d) == 0); // newctu-warning{{UNKNOWN}}
                                                // oldctu-warning at -1{{UNKNOWN}}
+                                               // newctu-warning at -2{{import of an external symbol for CTU failed: Failed to import the definition.}}
+                                               // oldctu-warning at -3{{import of an external symbol for CTU failed: Failed to import the definition.}}
 }
 
 int switchWithoutCases(int);
diff --git a/clang/test/Analysis/ctu/missing-ast.cpp b/clang/test/Analysis/ctu/missing-ast.cpp
index d39d5d8f05bf6..190b88d3146ba 100644
--- a/clang/test/Analysis/ctu/missing-ast.cpp
+++ b/clang/test/Analysis/ctu/missing-ast.cpp
@@ -19,6 +19,5 @@
 void external();
 
 void trigger() {
-  // expected-no-diagnostics
-  external(); // no-warning
+  external(); // expected-error{{import of an external symbol for CTU failed: Failed to load external AST source.}}
 }
diff --git a/clang/test/Analysis/ctu/on-demand-parsing.c b/clang/test/Analysis/ctu/on-demand-parsing.c
index d1d490bcf9a6d..4a94690fde05c 100644
--- a/clang/test/Analysis/ctu/on-demand-parsing.c
+++ b/clang/test/Analysis/ctu/on-demand-parsing.c
@@ -85,4 +85,5 @@ void testStructDefInArgument() {
   d.a = 1;
   d.b = 0;
   clang_analyzer_eval(structInProto(&d) == 0); // expected-warning{{TRUE}} expected-warning{{FALSE}}
+  // expected-warning at -1{{import of an external symbol for CTU failed: Failed to import the definition.}}
 }
diff --git a/clang/test/Analysis/ctu/test-import-failure.cpp b/clang/test/Analysis/ctu/test-import-failure.cpp
index 89ccd297ac3a7..3b5f9e71c2570 100644
--- a/clang/test/Analysis/ctu/test-import-failure.cpp
+++ b/clang/test/Analysis/ctu/test-import-failure.cpp
@@ -32,3 +32,4 @@ extern const int RootExamples[];
 // expected-warning at Inputs/test-import-failure-import.cpp:14{{incompatible definitions}}
 // expected-note at Inputs/test-import-failure-import.cpp:14{{no corresponding field here}}
 // expected-note at Inputs/test-import-failure-import.cpp:14{{no corresponding field here}}
+// expected-warning at Inputs/test-import-failure-import.cpp:44{{import of an external symbol for CTU failed: Failed to import the definition.}}

>From 053a7041b1c5925b57b0715d2d37e3833dba5feb Mon Sep 17 00:00:00 2001
From: Arseniy Zaostrovnykh <necto.ne at gmail.com>
Date: Fri, 27 Mar 2026 17:59:56 +0100
Subject: [PATCH 2/7] Remove fixmes

---
 clang/lib/CrossTU/CrossTranslationUnit.cpp | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/clang/lib/CrossTU/CrossTranslationUnit.cpp b/clang/lib/CrossTU/CrossTranslationUnit.cpp
index cff9352a315fb..22349e0e46432 100644
--- a/clang/lib/CrossTU/CrossTranslationUnit.cpp
+++ b/clang/lib/CrossTU/CrossTranslationUnit.cpp
@@ -438,7 +438,7 @@ void CrossTranslationUnitContext::emitCrossTUDiagnostics(const IndexError &IE,
     return;
 
   case index_error_code::failed_import:
-  case index_error_code::unspecified: // FIXME: remove?
+  case index_error_code::unspecified:
     // Not clear what happened exactly, but the outcome is a missing definition
     // This is not a big deal, and is expected since ASTImporter is incomplete.
     Context.getDiagnostics().Report(Loc, diag::warn_ctu_import_failure)
@@ -476,7 +476,7 @@ void CrossTranslationUnitContext::emitCrossTUDiagnostics(const IndexError &IE,
     return;
 
   case index_error_code::invocation_list_wrong_format:
-  case index_error_code::invocation_list_empty: // FIXME: remove?
+  case index_error_code::invocation_list_empty:
     // Without parsable invocation list, CTU cannot function.
     Context.getDiagnostics().Report(Loc, diag::err_invlist_parsing)
         << IE.getFileName() << IE.getLineNum();

>From f96b668dd22304a3cff92601af9fa0c167911777 Mon Sep 17 00:00:00 2001
From: Arseniy Zaostrovnykh <necto.ne at gmail.com>
Date: Fri, 27 Mar 2026 18:01:34 +0100
Subject: [PATCH 3/7] [NFC] Forgotten renaming

---
 clang/include/clang/CrossTU/CrossTranslationUnit.h | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/clang/include/clang/CrossTU/CrossTranslationUnit.h b/clang/include/clang/CrossTU/CrossTranslationUnit.h
index 54438026f29f0..dbfb8a72f49d0 100644
--- a/clang/include/clang/CrossTU/CrossTranslationUnit.h
+++ b/clang/include/clang/CrossTU/CrossTranslationUnit.h
@@ -64,11 +64,11 @@ class IndexError : public llvm::ErrorInfo<IndexError> {
   IndexError(index_error_code C) : Code(C), LineNo(0) {}
   IndexError(index_error_code C, std::string FileName, int LineNo = 0)
       : Code(C), FileName(std::move(FileName)), LineNo(LineNo) {}
-  IndexError(index_error_code C, std::string FileName, std::string TripleToName,
-             std::string TripleFromName)
+  IndexError(index_error_code C, std::string FileName, std::string ConfigToName,
+             std::string ConfigFromName)
       : Code(C), FileName(std::move(FileName)),
-        ConfigToName(std::move(TripleToName)),
-        ConfigFromName(std::move(TripleFromName)) {}
+        ConfigToName(std::move(ConfigToName)),
+        ConfigFromName(std::move(ConfigFromName)) {}
   void log(raw_ostream &OS) const override;
   std::error_code convertToErrorCode() const override;
   index_error_code getCode() const { return Code; }

>From bf8da81c250902d1cd387984b84591c1a3daf48b Mon Sep 17 00:00:00 2001
From: Arseniy Zaostrovnykh <necto.ne at gmail.com>
Date: Fri, 27 Mar 2026 18:40:38 +0100
Subject: [PATCH 4/7] Add correct number of wrong line in inv-list

---
 .../clang/CrossTU/CrossTranslationUnit.h      |  3 +-
 clang/lib/CrossTU/CrossTranslationUnit.cpp    | 40 +++++++++----------
 .../test/Analysis/ctu/diag/invlist-empty.cpp  |  5 ++-
 .../ctu/diag/invlist-wrong-format-late.cpp    | 19 +++++++++
 .../ctu/diag/invlist-wrong-format.cpp         |  2 +-
 .../CrossTU/CrossTranslationUnitTest.cpp      | 18 +++++++++
 6 files changed, 62 insertions(+), 25 deletions(-)
 create mode 100644 clang/test/Analysis/ctu/diag/invlist-wrong-format-late.cpp

diff --git a/clang/include/clang/CrossTU/CrossTranslationUnit.h b/clang/include/clang/CrossTU/CrossTranslationUnit.h
index dbfb8a72f49d0..2be184faff819 100644
--- a/clang/include/clang/CrossTU/CrossTranslationUnit.h
+++ b/clang/include/clang/CrossTU/CrossTranslationUnit.h
@@ -107,7 +107,8 @@ using InvocationListTy = llvm::StringMap<llvm::SmallVector<std::string, 32>>;
 /// will be used to produce the AST of the TU.
 llvm::Expected<InvocationListTy> parseInvocationList(
     StringRef FileContent,
-    llvm::sys::path::Style PathStyle = llvm::sys::path::Style::posix);
+    llvm::sys::path::Style PathStyle = llvm::sys::path::Style::posix,
+    StringRef FilePath = "");
 
 /// Returns true if it makes sense to import a foreign variable definition.
 /// For instance, we don't want to import variables that have non-trivial types
diff --git a/clang/lib/CrossTU/CrossTranslationUnit.cpp b/clang/lib/CrossTU/CrossTranslationUnit.cpp
index 22349e0e46432..ee3e0de54c23c 100644
--- a/clang/lib/CrossTU/CrossTranslationUnit.cpp
+++ b/clang/lib/CrossTU/CrossTranslationUnit.cpp
@@ -733,13 +733,23 @@ CrossTranslationUnitContext::ASTLoader::loadFromSource(
 }
 
 llvm::Expected<InvocationListTy>
-parseInvocationList(StringRef FileContent, llvm::sys::path::Style PathStyle) {
+parseInvocationList(StringRef FileContent, llvm::sys::path::Style PathStyle,
+                    StringRef FilePath) {
   InvocationListTy InvocationList;
 
   /// LLVM YAML parser is used to extract information from invocation list file.
   llvm::SourceMgr SM;
   llvm::yaml::Stream InvocationFile(FileContent, SM);
 
+  auto getLine = [&SM](llvm::yaml::Node *N) -> int {
+    return N ? SM.FindLineNumber(N->getSourceRange().Start) : 0;
+  };
+  auto wrongFormat = [&](llvm::yaml::Node *N) {
+    return llvm::make_error<IndexError>(
+        index_error_code::invocation_list_wrong_format, std::string(FilePath),
+        getLine(N));
+  };
+
   /// Only the first document is processed.
   llvm::yaml::document_iterator FirstInvocationFile = InvocationFile.begin();
 
@@ -758,15 +768,13 @@ parseInvocationList(StringRef FileContent, llvm::sys::path::Style PathStyle) {
   /// parts.
   auto *Mappings = dyn_cast<llvm::yaml::MappingNode>(DocumentRoot);
   if (!Mappings)
-    return llvm::make_error<IndexError>(
-        index_error_code::invocation_list_wrong_format);
+    return wrongFormat(DocumentRoot);
 
   for (auto &NextMapping : *Mappings) {
     /// The keys should be strings, which represent a source-file path.
     auto *Key = dyn_cast<llvm::yaml::ScalarNode>(NextMapping.getKey());
     if (!Key)
-      return llvm::make_error<IndexError>(
-          index_error_code::invocation_list_wrong_format);
+      return wrongFormat(NextMapping.getKey());
 
     SmallString<32> ValueStorage;
     StringRef SourcePath = Key->getValue(ValueStorage);
@@ -786,14 +794,12 @@ parseInvocationList(StringRef FileContent, llvm::sys::path::Style PathStyle) {
     /// the invocation.
     auto *Args = dyn_cast<llvm::yaml::SequenceNode>(NextMapping.getValue());
     if (!Args)
-      return llvm::make_error<IndexError>(
-          index_error_code::invocation_list_wrong_format);
+      return wrongFormat(NextMapping.getValue());
 
     for (auto &Arg : *Args) {
       auto *CmdString = dyn_cast<llvm::yaml::ScalarNode>(&Arg);
       if (!CmdString)
-        return llvm::make_error<IndexError>(
-            index_error_code::invocation_list_wrong_format);
+        return wrongFormat(&Arg);
       /// Every conversion starts with an empty working storage, as it is not
       /// clear if this is a requirement of the YAML parser.
       ValueStorage.clear();
@@ -802,8 +808,7 @@ parseInvocationList(StringRef FileContent, llvm::sys::path::Style PathStyle) {
     }
 
     if (InvocationList[InvocationKey].empty())
-      return llvm::make_error<IndexError>(
-          index_error_code::invocation_list_wrong_format);
+      return wrongFormat(Key);
   }
 
   return InvocationList;
@@ -828,19 +833,12 @@ llvm::Error CrossTranslationUnitContext::ASTLoader::lazyInitInvocationList() {
                           "should not be nullptr.");
 
   llvm::Expected<InvocationListTy> ExpectedInvocationList =
-      parseInvocationList(ContentBuffer->getBuffer(), PathStyle);
+      parseInvocationList(ContentBuffer->getBuffer(), PathStyle,
+                          InvocationListFilePath);
 
-  // Handle the error to store the code and filename for next call to this
-  // function.
   if (!ExpectedInvocationList) {
     llvm::handleAllErrors(ExpectedInvocationList.takeError(),
-                          [&](const IndexError &E) {
-                            PreviousError = IndexError(
-                                E.getCode(),
-                                E.getFileName().empty()
-                                    ? std::string(InvocationListFilePath)
-                                    : E.getFileName());
-                          });
+                          [&](const IndexError &E) { PreviousError = E; });
     return llvm::make_error<IndexError>(*PreviousError);
   }
 
diff --git a/clang/test/Analysis/ctu/diag/invlist-empty.cpp b/clang/test/Analysis/ctu/diag/invlist-empty.cpp
index e952faad17873..53fab169da78d 100644
--- a/clang/test/Analysis/ctu/diag/invlist-empty.cpp
+++ b/clang/test/Analysis/ctu/diag/invlist-empty.cpp
@@ -3,7 +3,8 @@
 // Note: invocation_list_empty (index_error_code::invocation_list_empty) is
 // dead code: llvm::yaml::Stream::begin() always creates at least one Document,
 // so FirstInvocationFile == InvocationFile.end() is never true. An empty file
-// reaches the !DocumentRoot branch instead, producing invocation_list_wrong_format.
+// produces a NullNode as the document root, which fails the dyn_cast to
+// MappingNode, producing invocation_list_wrong_format at line 1.
 //
 // RUN: rm -rf %t && mkdir %t
 // RUN: echo '11:c:@F at foo#I# simple.cpp' > %t/externalDefMap.txt
@@ -18,5 +19,5 @@
 int foo(int);
 
 void test() {
-  foo(1); // expected-error-re{{error parsing invocation list file: '{{.+}}invocations.yaml' line: 0 '<source-file>: [<compiler>, <arg1>, ...]' YAML mapping format expected}}
+  foo(1); // expected-error-re{{error parsing invocation list file: '{{.+}}invocations.yaml' line: 1 '<source-file>: [<compiler>, <arg1>, ...]' YAML mapping format expected}}
 }
diff --git a/clang/test/Analysis/ctu/diag/invlist-wrong-format-late.cpp b/clang/test/Analysis/ctu/diag/invlist-wrong-format-late.cpp
new file mode 100644
index 0000000000000..3472c9a552c3b
--- /dev/null
+++ b/clang/test/Analysis/ctu/diag/invlist-wrong-format-late.cpp
@@ -0,0 +1,19 @@
+// Test that a malformed invocation list entry on a non-first line reports the
+// correct line number. The first mapping entry is valid; the second has a
+// scalar value instead of a sequence, triggering invocation_list_wrong_format.
+//
+// RUN: rm -rf %t && mkdir %t
+// RUN: echo '11:c:@F at foo#I# simple.cpp' > %t/externalDefMap.txt
+// RUN: printf '/tmp/valid.cpp:\n  - clang++\n/tmp/bad.cpp: not_a_sequence\n' > %t/invocations.yaml
+// RUN: %clang_analyze_cc1 -std=c++14 -triple x86_64-pc-linux-gnu \
+// RUN:   -analyzer-checker=core \
+// RUN:   -analyzer-config experimental-enable-naive-ctu-analysis=true \
+// RUN:   -analyzer-config ctu-dir=%t \
+// RUN:   -analyzer-config ctu-invocation-list=%t/invocations.yaml \
+// RUN:   -verify %s
+
+int foo(int);
+
+void test() {
+  foo(1); // expected-error-re{{error parsing invocation list file: '{{.+}}invocations.yaml' line: 3 '<source-file>: [<compiler>, <arg1>, ...]' YAML mapping format expected}}
+}
diff --git a/clang/test/Analysis/ctu/diag/invlist-wrong-format.cpp b/clang/test/Analysis/ctu/diag/invlist-wrong-format.cpp
index c7e42d16ca7fd..58ac139543823 100644
--- a/clang/test/Analysis/ctu/diag/invlist-wrong-format.cpp
+++ b/clang/test/Analysis/ctu/diag/invlist-wrong-format.cpp
@@ -16,5 +16,5 @@
 int foo(int);
 
 void test() {
-  foo(1); // expected-error-re{{error parsing invocation list file: '{{.+}}invocations.yaml' line: 0 '<source-file>: [<compiler>, <arg1>, ...]' YAML mapping format expected}}
+  foo(1); // expected-error-re{{error parsing invocation list file: '{{.+}}invocations.yaml' line: 1 '<source-file>: [<compiler>, <arg1>, ...]' YAML mapping format expected}}
 }
diff --git a/clang/unittests/CrossTU/CrossTranslationUnitTest.cpp b/clang/unittests/CrossTU/CrossTranslationUnitTest.cpp
index 28f71d61e2c57..6f045eedf0412 100644
--- a/clang/unittests/CrossTU/CrossTranslationUnitTest.cpp
+++ b/clang/unittests/CrossTU/CrossTranslationUnitTest.cpp
@@ -191,6 +191,24 @@ TEST(CrossTranslationUnit, EmptyInvocationListIsNotValid) {
   EXPECT_TRUE(IsWrongFromatError);
 }
 
+TEST(CrossTranslationUnit, WrongFormatInvocationListHasLineNumber) {
+  // The first entry is valid; the second has a scalar value instead of a
+  // sequence. The error should report the line of the malformed value.
+  auto Input = "/tmp/valid.cpp:\n  - clang++\n/tmp/bad.cpp: not_a_sequence\n";
+
+  llvm::Expected<InvocationListTy> Result = parseInvocationList(Input);
+  EXPECT_FALSE(static_cast<bool>(Result));
+  bool IsWrongFormatError = false;
+  int LineNum = 0;
+  llvm::handleAllErrors(Result.takeError(), [&](IndexError &Err) {
+    IsWrongFormatError =
+        Err.getCode() == index_error_code::invocation_list_wrong_format;
+    LineNum = Err.getLineNum();
+  });
+  EXPECT_TRUE(IsWrongFormatError);
+  EXPECT_EQ(LineNum, 3);
+}
+
 TEST(CrossTranslationUnit, AmbiguousInvocationListIsDetected) {
   // The same source file occurs twice (for two different architecture) in
   // this test case. The disambiguation is the responsibility of the user.

>From 5ffab316c0d22ae594f045c9b041bce63e262637 Mon Sep 17 00:00:00 2001
From: Arseniy Zaostrovnykh <necto.ne at gmail.com>
Date: Fri, 27 Mar 2026 18:50:32 +0100
Subject: [PATCH 5/7] Fix missing warning group

---
 clang/include/clang/Basic/DiagnosticCrossTUKinds.td | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/clang/include/clang/Basic/DiagnosticCrossTUKinds.td b/clang/include/clang/Basic/DiagnosticCrossTUKinds.td
index 4374e85b114bd..464bdbccc664b 100644
--- a/clang/include/clang/Basic/DiagnosticCrossTUKinds.td
+++ b/clang/include/clang/Basic/DiagnosticCrossTUKinds.td
@@ -23,7 +23,8 @@ def err_multiple_def_index : Error<
   "multiple definitions are found for the same key in index ">;
 
 def warn_multiple_entries_invlist : Warning<
-  "multiple invocations for '%0' are found in the invocation list ">;
+  "multiple invocations for '%0' are found in the invocation list ">,
+  InGroup<CrossTU>;
 
 def warn_invlist_missing_file : Warning<
   "invocation for '%0' is missing in the invocation list">, InGroup<CrossTU>;

>From 78a38ce7fed6ebaccdeea19822929fed540e7d9c Mon Sep 17 00:00:00 2001
From: Arseniy Zaostrovnykh <necto.ne at gmail.com>
Date: Fri, 27 Mar 2026 19:13:42 +0100
Subject: [PATCH 6/7] [NFC] reformat

---
 clang/lib/CrossTU/CrossTranslationUnit.cpp | 18 ++++++++++--------
 1 file changed, 10 insertions(+), 8 deletions(-)

diff --git a/clang/lib/CrossTU/CrossTranslationUnit.cpp b/clang/lib/CrossTU/CrossTranslationUnit.cpp
index ee3e0de54c23c..9f9eea3479532 100644
--- a/clang/lib/CrossTU/CrossTranslationUnit.cpp
+++ b/clang/lib/CrossTU/CrossTranslationUnit.cpp
@@ -463,7 +463,8 @@ void CrossTranslationUnitContext::emitCrossTUDiagnostics(const IndexError &IE,
     return;
 
   case index_error_code::load_threshold_reached:
-    // This is expected. It is still useful to be aware of, but it is normal operation.
+    // This is expected. It is still useful to be aware of, but it is normal
+    // operation.
     Context.getDiagnostics().Report(Loc,
                                     diag::remark_ctu_import_threshold_reached);
     return;
@@ -483,15 +484,17 @@ void CrossTranslationUnitContext::emitCrossTUDiagnostics(const IndexError &IE,
     return;
 
   case index_error_code::invocation_list_ambiguous:
-    // For automatically generated invocation lists, it is common to list multiple invocations,
-    // if a file is compiled in multiple contexts. No need to block CTU because of this.
+    // For automatically generated invocation lists, it is common to list
+    // multiple invocations, if a file is compiled in multiple contexts. No need
+    // to block CTU because of this.
     Context.getDiagnostics().Report(Loc, diag::warn_multiple_entries_invlist)
         << IE.getFileName();
     return;
 
   case index_error_code::invocation_list_lookup_unsuccessful:
-    // Some files might be missing in the invocation list. It is sad but not fatal,
-    // and CTU can take advantage of the definitions in files with known invocations.
+    // Some files might be missing in the invocation list. It is sad but not
+    // fatal, and CTU can take advantage of the definitions in files with known
+    // invocations.
     Context.getDiagnostics().Report(Loc, diag::warn_invlist_missing_file)
         << IE.getFileName();
     return;
@@ -832,9 +835,8 @@ llvm::Error CrossTranslationUnitContext::ASTLoader::lazyInitInvocationList() {
   assert(ContentBuffer && "If no error was produced after loading, the pointer "
                           "should not be nullptr.");
 
-  llvm::Expected<InvocationListTy> ExpectedInvocationList =
-      parseInvocationList(ContentBuffer->getBuffer(), PathStyle,
-                          InvocationListFilePath);
+  llvm::Expected<InvocationListTy> ExpectedInvocationList = parseInvocationList(
+      ContentBuffer->getBuffer(), PathStyle, InvocationListFilePath);
 
   if (!ExpectedInvocationList) {
     llvm::handleAllErrors(ExpectedInvocationList.takeError(),

>From 031aba7600c71f44fef40d7f215ab7735b46abbe Mon Sep 17 00:00:00 2001
From: Arseniy Zaostrovnykh <necto.ne at gmail.com>
Date: Mon, 30 Mar 2026 07:38:39 +0200
Subject: [PATCH 7/7] Workaround Windows limitation: relativize the defmap path

---
 clang/test/Analysis/ctu/import-type-decl-definition.c | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/clang/test/Analysis/ctu/import-type-decl-definition.c b/clang/test/Analysis/ctu/import-type-decl-definition.c
index 10910e0812f3a..60fabb50f83e1 100644
--- a/clang/test/Analysis/ctu/import-type-decl-definition.c
+++ b/clang/test/Analysis/ctu/import-type-decl-definition.c
@@ -5,7 +5,9 @@
 // RUN: %clang_cc1 -x c -emit-pch -o %t/import.c.ast %t/import.c
 
 // RUN: %clang_extdef_map %t/import.c -- -c -x c > %t/externalDefMap.tmp.txt
-// RUN: sed 's/$/.ast/' %t/externalDefMap.tmp.txt > %t/externalDefMap.txt
+// FIXME On windows, absolute path generated by extdef_map is not recognized,
+// so CSA prepends the workdir path to it. Force relative path to workaround this issue.
+// RUN: sed 's| .*import\.c| import.c.ast|' %t/externalDefMap.tmp.txt > %t/externalDefMap.txt
 
 // RUN: %clang_cc1 -analyze \
 // RUN:   -analyzer-checker=core \



More information about the cfe-commits mailing list