https://github.com/Maoyao233 updated 
https://github.com/llvm/llvm-project/pull/237067

>From 96323110e6cf29de6e983f0aa537b97d268ecef1 Mon Sep 17 00:00:00 2001
From: Maoyao Ye <[email protected]>
Date: Fri, 30 Oct 2026 11:15:24 +0800
Subject: [PATCH 1/4] [clang][AST] Visit lambda bodies whose call operator is
 not a definition

A lambda whose body is part of a variable template specialization can end up
with a closure type merged across modules. When the closure type is merged,
ASTReader's PendingBodies handling can drop the duplicate body of the call
operator and leave the closure class with a call operator declaration that has
no body, while the definition lives on another declaration of the same entity.

RecursiveASTVisitor's implicit-code model reaches a lambda body through the
closure class' call operator: TraverseLambdaExpr visits S->getLambdaClass(), and
TraverseFunctionHelper only visits the body of a declaration that is itself a
definition. The body is then missing from the traversal, so tools that rely on
it -- e.g. clang-tidy's misc-static-initialization-cycle, which builds a parent
map and reaches the body through LambdaExpr::getBody() -- hit
llvm_unreachable("declaration of ParentD should be reached") with assertions
enabled, or crash in release builds.

Traverse the body through the lambda expression when the closure type's call
operator is not a definition. LambdaExpr::getBody() resolves the body through
the redeclaration chain, so the body is visited exactly once: when the call
operator is a definition, TraverseDecl has already visited it and the new branch
is not taken; shouldVisitLambdaBody() is still honored.

Add a clang-tidy regression test and a release note.
---
 clang-tools-extra/docs/ReleaseNotes.md        |  4 ++
 .../static-initialization-cycle-modules.cppm  | 45 +++++++++++++++++++
 clang/include/clang/AST/RecursiveASTVisitor.h | 13 ++++++
 3 files changed, 62 insertions(+)
 create mode 100644 
clang-tools-extra/test/clang-tidy/checkers/misc/static-initialization-cycle-modules.cppm

diff --git a/clang-tools-extra/docs/ReleaseNotes.md 
b/clang-tools-extra/docs/ReleaseNotes.md
index 679ca9ecbb4a39..24ee2465bb2570 100644
--- a/clang-tools-extra/docs/ReleaseNotes.md
+++ b/clang-tools-extra/docs/ReleaseNotes.md
@@ -283,6 +283,10 @@ infrastructure are described first, followed by 
tool-specific sections.
   nested expressions involving different macros or a mix of macro and
   non-macro operands.
 
+- Fixed a crash in {doc}`misc-static-initialization-cycle
+  <clang-tidy/checks/misc/static-initialization-cycle>` when checking a lambda
+  body from a closure type that was merged across modules.
+
 - Fixed false positive in {doc}`modernize-make-unique
   <clang-tidy/checks/modernize/make-unique>` in presence of private, protected
   or deleted destructor.
diff --git 
a/clang-tools-extra/test/clang-tidy/checkers/misc/static-initialization-cycle-modules.cppm
 
b/clang-tools-extra/test/clang-tidy/checkers/misc/static-initialization-cycle-modules.cppm
new file mode 100644
index 00000000000000..d77de075f27578
--- /dev/null
+++ 
b/clang-tools-extra/test/clang-tidy/checkers/misc/static-initialization-cycle-modules.cppm
@@ -0,0 +1,45 @@
+// The check walks the body of a lambda that is part of a variable template
+// specialization instantiated in a module. Building the parent map must not
+// skip that body: the lambda body is reached through the call operator of the
+// closure type, and when the closure type is merged with one from another
+// module, the call operator the closure class refers to can be a declaration
+// without a body, with the definition living on another declaration of the
+// same entity.
+//
+// RUN: rm -rf %t
+// RUN: mkdir -p %t
+// RUN: split-file %s %t
+// RUN: %clang -std=c++20 -x c++-module -I%t -fmodule-output=%t/mod_a.pcm -c 
%t/mod_a.cppm -o %t/mod_a.o
+// RUN: %clang -std=c++20 -x c++-module -I%t -fmodule-output=%t/mod_b.pcm -c 
%t/mod_b.cppm -o %t/mod_b.o
+// RUN: clang-tidy -checks='-*,misc-static-initialization-cycle' %t/main.cppm 
-- \
+// RUN:   -std=c++20 -x c++-module -fmodule-file=mod_a=%t/mod_a.pcm \
+// RUN:   -fmodule-file=mod_b=%t/mod_b.pcm
+
+//--- shared.h
+#pragma once
+struct SharedType {
+  int a{};
+};
+
+template <class T>
+inline constexpr unsigned long self = sizeof(T);
+
+template <class T>
+inline constexpr auto probe = [] { return self<T>; }();
+
+//--- mod_a.cppm
+module;
+#include "shared.h"
+export module mod_a;
+export inline constexpr unsigned long kA = probe<SharedType>;
+
+//--- mod_b.cppm
+module;
+#include "shared.h"
+export module mod_b;
+export inline constexpr unsigned long kB = probe<SharedType>;
+
+//--- main.cppm
+export module main;
+import mod_a;
+import mod_b;
diff --git a/clang/include/clang/AST/RecursiveASTVisitor.h 
b/clang/include/clang/AST/RecursiveASTVisitor.h
index 13dd5eda84fa65..fbd82b86d8a1cc 100644
--- a/clang/include/clang/AST/RecursiveASTVisitor.h
+++ b/clang/include/clang/AST/RecursiveASTVisitor.h
@@ -2897,6 +2897,19 @@ DEF_TRAVERSE_STMT(LambdaExpr, {
   if (getDerived().shouldVisitImplicitCode()) {
     // The implicit model is simple: everything else is in the lambda class.
     TRY_TO(TraverseDecl(S->getLambdaClass()));
+
+    // The body is normally reached through the call operator of the closure
+    // class. If that declaration is not a definition, the traversal above did
+    // not visit the body, so visit it through the lambda expression instead.
+    // This happens when the closure type was merged with one from another
+    // module and the definition ended up on a different declaration of the
+    // same entity than the one the closure class refers to. For named 
functions
+    // and methods the declaration carrying the definition is traversed
+    // separately, so only closure types need this fallback.
+    if (CXXMethodDecl *CallOp = S->getCallOperator();
+        CallOp && !CallOp->isThisDeclarationADefinition() &&
+        getDerived().shouldVisitLambdaBody())
+      TRY_TO_TRAVERSE_OR_ENQUEUE_STMT(S->getBody());
   } else {
     // We need to poke around to find the bits that might be explicitly 
written.
     TypeLoc TL = S->getCallOperator()->getTypeSourceInfo()->getTypeLoc();

>From e0feb481e4f89b8ffdd37b7a4b99e9f1b5245ba0 Mon Sep 17 00:00:00 2001
From: Maoyao Ye <[email protected]>
Date: Fri, 30 Oct 2026 22:43:39 +0800
Subject: [PATCH 2/4] [clang][AST] Address review comments

- Add a RecursiveASTVisitor unit test
  (clang/unittests/Tooling/RecursiveASTVisitorTests/LambdaExprModules.cpp):
  builds two reduced-BMI PCMs and checks that the implicit traversal visits
  every lambda body.
- Shorten the comment in RecursiveASTVisitor.h; the clang-tidy test comment now
  links to this PR instead of restating the analysis.
- Build the module interfaces in the clang-tidy test with --precompile instead
  of producing unused object files.
- Drop the release note for now; it can be added to clang's release notes if
  reviewers prefer.
---
 clang-tools-extra/docs/ReleaseNotes.md        |   4 -
 .../static-initialization-cycle-modules.cppm  |  15 +-
 clang/include/clang/AST/RecursiveASTVisitor.h |  10 +-
 clang/unittests/Tooling/CMakeLists.txt        |   1 +
 .../LambdaExprModules.cpp                     | 156 ++++++++++++++++++
 5 files changed, 166 insertions(+), 20 deletions(-)
 create mode 100644 
clang/unittests/Tooling/RecursiveASTVisitorTests/LambdaExprModules.cpp

diff --git a/clang-tools-extra/docs/ReleaseNotes.md 
b/clang-tools-extra/docs/ReleaseNotes.md
index 24ee2465bb2570..679ca9ecbb4a39 100644
--- a/clang-tools-extra/docs/ReleaseNotes.md
+++ b/clang-tools-extra/docs/ReleaseNotes.md
@@ -283,10 +283,6 @@ infrastructure are described first, followed by 
tool-specific sections.
   nested expressions involving different macros or a mix of macro and
   non-macro operands.
 
-- Fixed a crash in {doc}`misc-static-initialization-cycle
-  <clang-tidy/checks/misc/static-initialization-cycle>` when checking a lambda
-  body from a closure type that was merged across modules.
-
 - Fixed false positive in {doc}`modernize-make-unique
   <clang-tidy/checks/modernize/make-unique>` in presence of private, protected
   or deleted destructor.
diff --git 
a/clang-tools-extra/test/clang-tidy/checkers/misc/static-initialization-cycle-modules.cppm
 
b/clang-tools-extra/test/clang-tidy/checkers/misc/static-initialization-cycle-modules.cppm
index d77de075f27578..d71a22406c7eff 100644
--- 
a/clang-tools-extra/test/clang-tidy/checkers/misc/static-initialization-cycle-modules.cppm
+++ 
b/clang-tools-extra/test/clang-tidy/checkers/misc/static-initialization-cycle-modules.cppm
@@ -1,16 +1,13 @@
-// The check walks the body of a lambda that is part of a variable template
-// specialization instantiated in a module. Building the parent map must not
-// skip that body: the lambda body is reached through the call operator of the
-// closure type, and when the closure type is merged with one from another
-// module, the call operator the closure class refers to can be a declaration
-// without a body, with the definition living on another declaration of the
-// same entity.
+// Regression test for https://github.com/llvm/llvm-project/pull/237067:
+// the parent map must contain the lambda body even when the closure type's
+// call operator is a declaration without a body (merged from another module),
+// otherwise this check hits llvm_unreachable.
 //
 // RUN: rm -rf %t
 // RUN: mkdir -p %t
 // RUN: split-file %s %t
-// RUN: %clang -std=c++20 -x c++-module -I%t -fmodule-output=%t/mod_a.pcm -c 
%t/mod_a.cppm -o %t/mod_a.o
-// RUN: %clang -std=c++20 -x c++-module -I%t -fmodule-output=%t/mod_b.pcm -c 
%t/mod_b.cppm -o %t/mod_b.o
+// RUN: %clang -std=c++20 -I%t --precompile %t/mod_a.cppm -o %t/mod_a.pcm
+// RUN: %clang -std=c++20 -I%t --precompile %t/mod_b.cppm -o %t/mod_b.pcm
 // RUN: clang-tidy -checks='-*,misc-static-initialization-cycle' %t/main.cppm 
-- \
 // RUN:   -std=c++20 -x c++-module -fmodule-file=mod_a=%t/mod_a.pcm \
 // RUN:   -fmodule-file=mod_b=%t/mod_b.pcm
diff --git a/clang/include/clang/AST/RecursiveASTVisitor.h 
b/clang/include/clang/AST/RecursiveASTVisitor.h
index fbd82b86d8a1cc..05bab54c5c85c6 100644
--- a/clang/include/clang/AST/RecursiveASTVisitor.h
+++ b/clang/include/clang/AST/RecursiveASTVisitor.h
@@ -2899,13 +2899,9 @@ DEF_TRAVERSE_STMT(LambdaExpr, {
     TRY_TO(TraverseDecl(S->getLambdaClass()));
 
     // The body is normally reached through the call operator of the closure
-    // class. If that declaration is not a definition, the traversal above did
-    // not visit the body, so visit it through the lambda expression instead.
-    // This happens when the closure type was merged with one from another
-    // module and the definition ended up on a different declaration of the
-    // same entity than the one the closure class refers to. For named 
functions
-    // and methods the declaration carrying the definition is traversed
-    // separately, so only closure types need this fallback.
+    // class. If that declaration is not a definition (which happens when the
+    // closure type was merged with one from another module), visit the body
+    // through the lambda expression.
     if (CXXMethodDecl *CallOp = S->getCallOperator();
         CallOp && !CallOp->isThisDeclarationADefinition() &&
         getDerived().shouldVisitLambdaBody())
diff --git a/clang/unittests/Tooling/CMakeLists.txt 
b/clang/unittests/Tooling/CMakeLists.txt
index 4de300fa86385f..655b39606c290c 100644
--- a/clang/unittests/Tooling/CMakeLists.txt
+++ b/clang/unittests/Tooling/CMakeLists.txt
@@ -39,6 +39,7 @@ add_clang_unittest(ToolingTests
   RecursiveASTVisitorTests/IntegerLiteral.cpp
   RecursiveASTVisitorTests/LambdaDefaultCapture.cpp
   RecursiveASTVisitorTests/LambdaExpr.cpp
+  RecursiveASTVisitorTests/LambdaExprModules.cpp
   RecursiveASTVisitorTests/LambdaTemplateParams.cpp
   RecursiveASTVisitorTests/MemberPointerTypeLoc.cpp
   RecursiveASTVisitorTests/NestedNameSpecifiers.cpp
diff --git 
a/clang/unittests/Tooling/RecursiveASTVisitorTests/LambdaExprModules.cpp 
b/clang/unittests/Tooling/RecursiveASTVisitorTests/LambdaExprModules.cpp
new file mode 100644
index 00000000000000..5a021c23d8e3bd
--- /dev/null
+++ b/clang/unittests/Tooling/RecursiveASTVisitorTests/LambdaExprModules.cpp
@@ -0,0 +1,156 @@
+//===- unittest/Tooling/RecursiveASTVisitorTests/LambdaExprModules.cpp 
----===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM 
Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/RecursiveASTVisitor.h"
+#include "clang/Driver/CreateInvocationFromArgs.h"
+#include "clang/Frontend/CompilerInstance.h"
+#include "clang/Frontend/FrontendActions.h"
+#include "clang/Frontend/Utils.h"
+#include "clang/Tooling/Tooling.h"
+#include "llvm/ADT/SmallPtrSet.h"
+#include "llvm/ADT/SmallString.h"
+#include "llvm/Support/FileSystem.h"
+#include "llvm/Support/Path.h"
+#include "llvm/Support/raw_ostream.h"
+#include "gtest/gtest.h"
+
+using namespace clang;
+
+namespace {
+
+// Counts lambdas visited in the implicit-code model (the model 
ParentMapContext
+// uses) and whether their bodies were visited too.
+class ImplicitLambdaBodyVisitor
+    : public RecursiveASTVisitor<ImplicitLambdaBodyVisitor> {
+public:
+  bool shouldVisitImplicitCode() const { return true; }
+
+  bool VisitLambdaExpr(LambdaExpr *LE) {
+    ++NumLambdas;
+    Bodies.insert(LE->getBody());
+    return true;
+  }
+
+  bool VisitStmt(Stmt *S) {
+    if (Bodies.contains(S))
+      ++NumVisitedBodies;
+    return true;
+  }
+
+  unsigned NumLambdas = 0;
+  unsigned NumVisitedBodies = 0;
+
+private:
+  llvm::SmallPtrSet<Stmt *, 8> Bodies;
+};
+
+class LambdaExprModulesTest : public ::testing::Test {
+protected:
+  void SetUp() override {
+    ASSERT_FALSE(
+        llvm::sys::fs::createUniqueDirectory("lambda-expr-modules", TestDir));
+  }
+
+  void TearDown() override { llvm::sys::fs::remove_directories(TestDir); }
+
+  void addFile(llvm::StringRef Path, llvm::StringRef Contents) {
+    ASSERT_FALSE(llvm::sys::path::is_absolute(Path));
+
+    llvm::SmallString<256> AbsPath(TestDir);
+    llvm::sys::path::append(AbsPath, Path);
+
+    ASSERT_FALSE(llvm::sys::fs::create_directories(
+        llvm::sys::path::parent_path(AbsPath)));
+
+    std::error_code EC;
+    llvm::raw_fd_ostream OS(AbsPath, EC);
+    ASSERT_FALSE(EC);
+    OS << Contents;
+  }
+
+  std::string precompile(llvm::StringRef FileName) {
+    CreateInvocationOptions CIOpts;
+    CIOpts.VFS = llvm::vfs::createPhysicalFileSystem();
+    DiagnosticOptions DiagOpts;
+    IntrusiveRefCntPtr<DiagnosticsEngine> Diags =
+        CompilerInstance::createDiagnostics(*CIOpts.VFS, DiagOpts);
+    CIOpts.Diags = Diags;
+
+    std::string File = FileName.str();
+    const char *Args[] = {"clang++",       "-std=c++20",
+                          "--precompile",  "-working-directory",
+                          TestDir.c_str(), File.c_str()};
+    std::shared_ptr<CompilerInvocation> Invocation =
+        createInvocation(Args, CIOpts);
+    EXPECT_TRUE(Invocation);
+    Invocation->getFrontendOpts().DisableFree = false;
+
+    CompilerInstance Instance(std::move(Invocation));
+    Instance.setDiagnostics(Diags);
+
+    std::string PCM = (llvm::Twine(TestDir) + "/" + File + ".pcm").str();
+    Instance.getFrontendOpts().OutputFile = PCM;
+
+    GenerateReducedModuleInterfaceAction Action;
+    EXPECT_TRUE(Instance.ExecuteAction(Action));
+    EXPECT_FALSE(Diags->hasErrorOccurred());
+    return PCM;
+  }
+
+  llvm::SmallString<256> TestDir;
+};
+
+TEST_F(LambdaExprModulesTest, VisitsMergedLambdaBodies) {
+  addFile("shared.h", R"cpp(
+#pragma once
+template <class T>
+inline constexpr unsigned long size_of = sizeof(T);
+
+template <class T>
+inline constexpr auto probe = [] { return size_of<T>; }();
+)cpp");
+  addFile("mod_a.cppm", R"cpp(
+module;
+#include "shared.h"
+export module mod_a;
+export inline constexpr unsigned long kA = probe<unsigned long>;
+)cpp");
+  addFile("mod_b.cppm", R"cpp(
+module;
+#include "shared.h"
+export module mod_b;
+export inline constexpr unsigned long kB = probe<unsigned long>;
+)cpp");
+
+  std::string PModA = precompile("mod_a.cppm");
+  std::string PModB = precompile("mod_b.cppm");
+  std::string ArgA = "-fmodule-file=mod_a=" + PModA;
+  std::string ArgB = "-fmodule-file=mod_b=" + PModB;
+
+  std::unique_ptr<ASTUnit> AST = tooling::buildASTFromCodeWithArgs(
+      R"cpp(
+import mod_a;
+import mod_b;
+unsigned long use() { return kA + kB; }
+)cpp",
+      /*Args=*/{"-std=c++20", ArgA, ArgB});
+  ASSERT_TRUE(AST);
+
+  ImplicitLambdaBodyVisitor Visitor;
+  Visitor.TraverseDecl(AST->getASTContext().getTranslationUnitDecl());
+
+  // The TU imports two modules that each instantiate the same variable
+  // template, so the closure type is merged and one call operator is left
+  // without a body. The implicit traversal must still visit every lambda body
+  // it encountered.
+  ASSERT_GT(Visitor.NumLambdas, 0u);
+  EXPECT_EQ(Visitor.NumLambdas, Visitor.NumVisitedBodies);
+}
+
+} // namespace

>From 5dc94cf896d83e40c6dd43f1bb3597c55024c317 Mon Sep 17 00:00:00 2001
From: Maoyao Ye <[email protected]>
Date: Sat, 31 Oct 2026 00:12:04 +0800
Subject: [PATCH 3/4] remove comments in test

---
 .../checkers/misc/static-initialization-cycle-modules.cppm   | 5 -----
 1 file changed, 5 deletions(-)

diff --git 
a/clang-tools-extra/test/clang-tidy/checkers/misc/static-initialization-cycle-modules.cppm
 
b/clang-tools-extra/test/clang-tidy/checkers/misc/static-initialization-cycle-modules.cppm
index d71a22406c7eff..e31ae27eceab1c 100644
--- 
a/clang-tools-extra/test/clang-tidy/checkers/misc/static-initialization-cycle-modules.cppm
+++ 
b/clang-tools-extra/test/clang-tidy/checkers/misc/static-initialization-cycle-modules.cppm
@@ -1,8 +1,3 @@
-// Regression test for https://github.com/llvm/llvm-project/pull/237067:
-// the parent map must contain the lambda body even when the closure type's
-// call operator is a declaration without a body (merged from another module),
-// otherwise this check hits llvm_unreachable.
-//
 // RUN: rm -rf %t
 // RUN: mkdir -p %t
 // RUN: split-file %s %t

>From 7d9403b60d74f4ad7b77805843feb7f719b614ce Mon Sep 17 00:00:00 2001
From: Maoyao Ye <[email protected]>
Date: Sat, 31 Oct 2026 14:53:18 +0800
Subject: [PATCH 4/4] move the fix to ParentMap

---
 clang/include/clang/AST/RecursiveASTVisitor.h |   9 -
 clang/lib/AST/ParentMapContext.cpp            |  16 ++
 .../unittests/AST/ASTContextParentMapTest.cpp | 104 ++++++++++++
 clang/unittests/Tooling/CMakeLists.txt        |   1 -
 .../LambdaExprModules.cpp                     | 156 ------------------
 5 files changed, 120 insertions(+), 166 deletions(-)
 delete mode 100644 
clang/unittests/Tooling/RecursiveASTVisitorTests/LambdaExprModules.cpp

diff --git a/clang/include/clang/AST/RecursiveASTVisitor.h 
b/clang/include/clang/AST/RecursiveASTVisitor.h
index 05bab54c5c85c6..13dd5eda84fa65 100644
--- a/clang/include/clang/AST/RecursiveASTVisitor.h
+++ b/clang/include/clang/AST/RecursiveASTVisitor.h
@@ -2897,15 +2897,6 @@ DEF_TRAVERSE_STMT(LambdaExpr, {
   if (getDerived().shouldVisitImplicitCode()) {
     // The implicit model is simple: everything else is in the lambda class.
     TRY_TO(TraverseDecl(S->getLambdaClass()));
-
-    // The body is normally reached through the call operator of the closure
-    // class. If that declaration is not a definition (which happens when the
-    // closure type was merged with one from another module), visit the body
-    // through the lambda expression.
-    if (CXXMethodDecl *CallOp = S->getCallOperator();
-        CallOp && !CallOp->isThisDeclarationADefinition() &&
-        getDerived().shouldVisitLambdaBody())
-      TRY_TO_TRAVERSE_OR_ENQUEUE_STMT(S->getBody());
   } else {
     // We need to poke around to find the bits that might be explicitly 
written.
     TypeLoc TL = S->getCallOperator()->getTypeSourceInfo()->getTypeLoc();
diff --git a/clang/lib/AST/ParentMapContext.cpp 
b/clang/lib/AST/ParentMapContext.cpp
index 7607cf27bfe60f..dce8bdbb1e674c 100644
--- a/clang/lib/AST/ParentMapContext.cpp
+++ b/clang/lib/AST/ParentMapContext.cpp
@@ -457,6 +457,22 @@ class ParentMapContext::ParentMap::ASTVisitor
         &Map.OtherParents);
   }
 
+  // Traverse the body of a lambda whose call operator is a declaration rather
+  // than a definition because the closure type was merged across modules, so
+  // that it is not missing from the parent map.
+  // See https://github.com/llvm/llvm-project/issues/236863.
+  bool TraverseCXXMethodDecl(CXXMethodDecl *MethodNode) {
+    if (!VisitorBase::TraverseCXXMethodDecl(MethodNode))
+      return false;
+    if (!shouldVisitLambdaBody() || MethodNode->isThisDeclarationADefinition())
+      return true;
+    const CXXRecordDecl *RD = MethodNode->getParent();
+    if (!RD->isLambda() ||
+        !declaresSameEntity(RD->getLambdaCallOperator(), MethodNode))
+      return true;
+    return TraverseStmt(MethodNode->getBody());
+  }
+
   // Using generic TraverseNode for Stmt would prevent data-recursion.
   bool dataTraverseStmtPre(Stmt *StmtNode) {
     addParent(StmtNode, &Map.PointerParents);
diff --git a/clang/unittests/AST/ASTContextParentMapTest.cpp 
b/clang/unittests/AST/ASTContextParentMapTest.cpp
index 545eaf36191717..37cab9c43e592a 100644
--- a/clang/unittests/AST/ASTContextParentMapTest.cpp
+++ b/clang/unittests/AST/ASTContextParentMapTest.cpp
@@ -11,12 +11,17 @@
 
//===----------------------------------------------------------------------===//
 
 #include "clang/AST/ASTContext.h"
+#include "clang/AST/RecursiveASTVisitor.h"
 #include "MatchVerifier.h"
 #include "clang/ASTMatchers/ASTMatchFinder.h"
 #include "clang/ASTMatchers/ASTMatchers.h"
 #include "clang/Tooling/Tooling.h"
+#include "llvm/ADT/ScopeExit.h"
+#include "llvm/ADT/SmallString.h"
+#include "llvm/Support/FileSystem.h"
 #include "gtest/gtest.h"
 #include "gmock/gmock.h"
+#include <vector>
 
 using testing::ElementsAre;
 
@@ -119,6 +124,105 @@ TEST(GetParents, ImplicitLambdaNodes) {
       Lang_CXX11));
 }
 
+namespace {
+
+/// Collects every LambdaExpr in the AST, including template instantiations.
+class LambdaCollector : public RecursiveASTVisitor<LambdaCollector> {
+public:
+  bool shouldVisitTemplateInstantiations() const { return true; }
+
+  bool VisitLambdaExpr(LambdaExpr *LE) {
+    Lambdas.push_back(LE);
+    return true;
+  }
+
+  std::vector<LambdaExpr *> Lambdas;
+};
+
+void collectDeclRefExprs(Stmt *S, std::vector<DeclRefExpr *> &Refs) {
+  if (!S)
+    return;
+  if (auto *DRE = dyn_cast<DeclRefExpr>(S))
+    Refs.push_back(DRE);
+  for (Stmt *Child : S->children())
+    collectDeclRefExprs(Child, Refs);
+}
+
+} // namespace
+
+TEST(GetParents, MergedLambdaClosureBody) {
+  // Two module units instantiate the same variable template specialization
+  // containing a lambda. The closure types are merged and the call operator is
+  // left without a body; see issue #236863.
+  llvm::SmallString<256> ModuleCache;
+  ASSERT_FALSE(llvm::sys::fs::createUniqueDirectory(
+      "clang-parent-map-lambda-modules", ModuleCache));
+  llvm::scope_exit CleanupModuleCache(
+      [&] { llvm::sys::fs::remove_directories(ModuleCache); });
+
+  auto AST = tooling::buildASTFromCodeWithArgs(
+      R"cpp(
+#pragma clang module build A
+module A {}
+#pragma clang module contents
+#pragma clang module begin A
+struct SharedType { int a{}; };
+template <class T> inline constexpr unsigned long self = sizeof(T);
+template <class T> inline constexpr auto probe = [] { return self<T>; }();
+inline constexpr unsigned long kA = probe<SharedType>;
+#pragma clang module end
+#pragma clang module endbuild
+
+#pragma clang module build B
+module B {}
+#pragma clang module contents
+#pragma clang module begin B
+struct SharedType { int a{}; };
+template <class T> inline constexpr unsigned long self = sizeof(T);
+template <class T> inline constexpr auto probe = [] { return self<T>; }();
+inline constexpr unsigned long kB = probe<SharedType>;
+#pragma clang module end
+#pragma clang module endbuild
+
+#pragma clang module import A
+#pragma clang module import B
+
+unsigned long use() { return kA + kB; }
+)cpp",
+      {"-std=c++20", "-fmodules",
+       "-fmodules-cache-path=" + ModuleCache.str().str()});
+  ASSERT_TRUE(AST);
+
+  auto &Ctx = AST->getASTContext();
+  // Build the parent map before anything below loads the call operator bodies:
+  // deserializing the bodies changes which traversal path reaches them.
+  (void)Ctx.getParents(*Ctx.getTranslationUnitDecl());
+
+  LambdaCollector Collector;
+  Collector.TraverseDecl(Ctx.getTranslationUnitDecl());
+
+  unsigned NumBodies = 0;
+  for (LambdaExpr *LE : Collector.Lambdas) {
+    if (!LE->getLambdaClass()->isFromASTFile())
+      continue;
+    Stmt *Body = LE->getBody();
+    ASSERT_NE(Body, nullptr);
+    ++NumBodies;
+
+    // The body is reached through the call operator, as for any other lambda.
+    DynTypedNodeList BodyParents = Ctx.getParents(*Body);
+    ASSERT_FALSE(BodyParents.empty());
+    EXPECT_NE(BodyParents[0].get<CXXMethodDecl>(), nullptr);
+
+    // These nodes' missing parents caused the clang-tidy crash.
+    std::vector<DeclRefExpr *> Refs;
+    collectDeclRefExprs(Body, Refs);
+    for (DeclRefExpr *Ref : Refs)
+      EXPECT_FALSE(Ctx.getParents(*Ref).empty());
+  }
+  EXPECT_GT(NumBodies, 0u);
+}
+
 TEST(GetParents, FriendTypeLoc) {
   auto AST = tooling::buildASTFromCode("struct A { friend struct Fr; };"
                                        "struct B { friend struct Fr; };"
diff --git a/clang/unittests/Tooling/CMakeLists.txt 
b/clang/unittests/Tooling/CMakeLists.txt
index 655b39606c290c..4de300fa86385f 100644
--- a/clang/unittests/Tooling/CMakeLists.txt
+++ b/clang/unittests/Tooling/CMakeLists.txt
@@ -39,7 +39,6 @@ add_clang_unittest(ToolingTests
   RecursiveASTVisitorTests/IntegerLiteral.cpp
   RecursiveASTVisitorTests/LambdaDefaultCapture.cpp
   RecursiveASTVisitorTests/LambdaExpr.cpp
-  RecursiveASTVisitorTests/LambdaExprModules.cpp
   RecursiveASTVisitorTests/LambdaTemplateParams.cpp
   RecursiveASTVisitorTests/MemberPointerTypeLoc.cpp
   RecursiveASTVisitorTests/NestedNameSpecifiers.cpp
diff --git 
a/clang/unittests/Tooling/RecursiveASTVisitorTests/LambdaExprModules.cpp 
b/clang/unittests/Tooling/RecursiveASTVisitorTests/LambdaExprModules.cpp
deleted file mode 100644
index 5a021c23d8e3bd..00000000000000
--- a/clang/unittests/Tooling/RecursiveASTVisitorTests/LambdaExprModules.cpp
+++ /dev/null
@@ -1,156 +0,0 @@
-//===- unittest/Tooling/RecursiveASTVisitorTests/LambdaExprModules.cpp 
----===//
-//
-// Part of the LLVM Project, under the Apache License v2.0 with LLVM 
Exceptions.
-// See https://llvm.org/LICENSE.txt for license information.
-// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
-//
-//===----------------------------------------------------------------------===//
-
-#include "clang/AST/ASTContext.h"
-#include "clang/AST/RecursiveASTVisitor.h"
-#include "clang/Driver/CreateInvocationFromArgs.h"
-#include "clang/Frontend/CompilerInstance.h"
-#include "clang/Frontend/FrontendActions.h"
-#include "clang/Frontend/Utils.h"
-#include "clang/Tooling/Tooling.h"
-#include "llvm/ADT/SmallPtrSet.h"
-#include "llvm/ADT/SmallString.h"
-#include "llvm/Support/FileSystem.h"
-#include "llvm/Support/Path.h"
-#include "llvm/Support/raw_ostream.h"
-#include "gtest/gtest.h"
-
-using namespace clang;
-
-namespace {
-
-// Counts lambdas visited in the implicit-code model (the model 
ParentMapContext
-// uses) and whether their bodies were visited too.
-class ImplicitLambdaBodyVisitor
-    : public RecursiveASTVisitor<ImplicitLambdaBodyVisitor> {
-public:
-  bool shouldVisitImplicitCode() const { return true; }
-
-  bool VisitLambdaExpr(LambdaExpr *LE) {
-    ++NumLambdas;
-    Bodies.insert(LE->getBody());
-    return true;
-  }
-
-  bool VisitStmt(Stmt *S) {
-    if (Bodies.contains(S))
-      ++NumVisitedBodies;
-    return true;
-  }
-
-  unsigned NumLambdas = 0;
-  unsigned NumVisitedBodies = 0;
-
-private:
-  llvm::SmallPtrSet<Stmt *, 8> Bodies;
-};
-
-class LambdaExprModulesTest : public ::testing::Test {
-protected:
-  void SetUp() override {
-    ASSERT_FALSE(
-        llvm::sys::fs::createUniqueDirectory("lambda-expr-modules", TestDir));
-  }
-
-  void TearDown() override { llvm::sys::fs::remove_directories(TestDir); }
-
-  void addFile(llvm::StringRef Path, llvm::StringRef Contents) {
-    ASSERT_FALSE(llvm::sys::path::is_absolute(Path));
-
-    llvm::SmallString<256> AbsPath(TestDir);
-    llvm::sys::path::append(AbsPath, Path);
-
-    ASSERT_FALSE(llvm::sys::fs::create_directories(
-        llvm::sys::path::parent_path(AbsPath)));
-
-    std::error_code EC;
-    llvm::raw_fd_ostream OS(AbsPath, EC);
-    ASSERT_FALSE(EC);
-    OS << Contents;
-  }
-
-  std::string precompile(llvm::StringRef FileName) {
-    CreateInvocationOptions CIOpts;
-    CIOpts.VFS = llvm::vfs::createPhysicalFileSystem();
-    DiagnosticOptions DiagOpts;
-    IntrusiveRefCntPtr<DiagnosticsEngine> Diags =
-        CompilerInstance::createDiagnostics(*CIOpts.VFS, DiagOpts);
-    CIOpts.Diags = Diags;
-
-    std::string File = FileName.str();
-    const char *Args[] = {"clang++",       "-std=c++20",
-                          "--precompile",  "-working-directory",
-                          TestDir.c_str(), File.c_str()};
-    std::shared_ptr<CompilerInvocation> Invocation =
-        createInvocation(Args, CIOpts);
-    EXPECT_TRUE(Invocation);
-    Invocation->getFrontendOpts().DisableFree = false;
-
-    CompilerInstance Instance(std::move(Invocation));
-    Instance.setDiagnostics(Diags);
-
-    std::string PCM = (llvm::Twine(TestDir) + "/" + File + ".pcm").str();
-    Instance.getFrontendOpts().OutputFile = PCM;
-
-    GenerateReducedModuleInterfaceAction Action;
-    EXPECT_TRUE(Instance.ExecuteAction(Action));
-    EXPECT_FALSE(Diags->hasErrorOccurred());
-    return PCM;
-  }
-
-  llvm::SmallString<256> TestDir;
-};
-
-TEST_F(LambdaExprModulesTest, VisitsMergedLambdaBodies) {
-  addFile("shared.h", R"cpp(
-#pragma once
-template <class T>
-inline constexpr unsigned long size_of = sizeof(T);
-
-template <class T>
-inline constexpr auto probe = [] { return size_of<T>; }();
-)cpp");
-  addFile("mod_a.cppm", R"cpp(
-module;
-#include "shared.h"
-export module mod_a;
-export inline constexpr unsigned long kA = probe<unsigned long>;
-)cpp");
-  addFile("mod_b.cppm", R"cpp(
-module;
-#include "shared.h"
-export module mod_b;
-export inline constexpr unsigned long kB = probe<unsigned long>;
-)cpp");
-
-  std::string PModA = precompile("mod_a.cppm");
-  std::string PModB = precompile("mod_b.cppm");
-  std::string ArgA = "-fmodule-file=mod_a=" + PModA;
-  std::string ArgB = "-fmodule-file=mod_b=" + PModB;
-
-  std::unique_ptr<ASTUnit> AST = tooling::buildASTFromCodeWithArgs(
-      R"cpp(
-import mod_a;
-import mod_b;
-unsigned long use() { return kA + kB; }
-)cpp",
-      /*Args=*/{"-std=c++20", ArgA, ArgB});
-  ASSERT_TRUE(AST);
-
-  ImplicitLambdaBodyVisitor Visitor;
-  Visitor.TraverseDecl(AST->getASTContext().getTranslationUnitDecl());
-
-  // The TU imports two modules that each instantiate the same variable
-  // template, so the closure type is merged and one call operator is left
-  // without a body. The implicit traversal must still visit every lambda body
-  // it encountered.
-  ASSERT_GT(Visitor.NumLambdas, 0u);
-  EXPECT_EQ(Visitor.NumLambdas, Visitor.NumVisitedBodies);
-}
-
-} // namespace

_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to