Skip to content

Conversation

@timon-ul
Copy link
Contributor

@timon-ul timon-ul commented Nov 26, 2025

First step for clangd/clangd#716 , the missing part is finding the same information directly in the AST.

Not sure about the location of the new visitor and the new references collection. Also I used std::vector which I see never used so if I should have used a different data structure please tell me.

@timon-ul timon-ul changed the title Extend index references with constructor calls through forwarding clangd: Extend index references with constructor calls through forwarding Nov 26, 2025
@llvmbot
Copy link
Member

llvmbot commented Nov 26, 2025

@llvm/pr-subscribers-clang-tools-extra

@llvm/pr-subscribers-clangd

Author: None (timon-ul)

Changes

First step for clangd/clangd#716 (comment) , the missing part is finding the same information directly in the AST.

Not sure about the location of the new visitor and the new references collection. Also I used std::vector which I see never used so if I should have used a different data structure please tell me.


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

5 Files Affected:

  • (modified) clang-tools-extra/clangd/AST.cpp (+21)
  • (modified) clang-tools-extra/clangd/AST.h (+4)
  • (modified) clang-tools-extra/clangd/Preamble.cpp (+1-21)
  • (modified) clang-tools-extra/clangd/index/SymbolCollector.cpp (+38)
  • (modified) clang-tools-extra/clangd/unittests/XRefsTests.cpp (+31)
diff --git a/clang-tools-extra/clangd/AST.cpp b/clang-tools-extra/clangd/AST.cpp
index 0dcff2eae05e7..4b73bdba8aaa9 100644
--- a/clang-tools-extra/clangd/AST.cpp
+++ b/clang-tools-extra/clangd/AST.cpp
@@ -1040,5 +1040,26 @@ bool isExpandedFromParameterPack(const ParmVarDecl *D) {
   return getUnderlyingPackType(D) != nullptr;
 }
 
+bool isLikelyForwardingFunction(FunctionTemplateDecl *FT) {
+  const auto *FD = FT->getTemplatedDecl();
+  const auto NumParams = FD->getNumParams();
+  // Check whether its last parameter is a parameter pack...
+  if (NumParams > 0) {
+    const auto *LastParam = FD->getParamDecl(NumParams - 1);
+    if (const auto *PET = dyn_cast<PackExpansionType>(LastParam->getType())) {
+      // ... of the type T&&... or T...
+      const auto BaseType = PET->getPattern().getNonReferenceType();
+      if (const auto *TTPT =
+              dyn_cast<TemplateTypeParmType>(BaseType.getTypePtr())) {
+        // ... whose template parameter comes from the function directly
+        if (FT->getTemplateParameters()->getDepth() == TTPT->getDepth()) {
+          return true;
+        }
+      }
+    }
+  }
+  return false;
+}
+
 } // namespace clangd
 } // namespace clang
diff --git a/clang-tools-extra/clangd/AST.h b/clang-tools-extra/clangd/AST.h
index 2b83595e5b8e9..af45ae2d9022d 100644
--- a/clang-tools-extra/clangd/AST.h
+++ b/clang-tools-extra/clangd/AST.h
@@ -253,6 +253,10 @@ resolveForwardingParameters(const FunctionDecl *D, unsigned MaxDepth = 10);
 /// reference to one (e.g. `Args&...` or `Args&&...`).
 bool isExpandedFromParameterPack(const ParmVarDecl *D);
 
+/// Heuristic that checks if FT is forwarding a parameter pack to another
+/// function. (e.g. `make_unique`).
+bool isLikelyForwardingFunction(FunctionTemplateDecl *FT);
+
 } // namespace clangd
 } // namespace clang
 
diff --git a/clang-tools-extra/clangd/Preamble.cpp b/clang-tools-extra/clangd/Preamble.cpp
index 8af9e4649218d..09aaf3290b585 100644
--- a/clang-tools-extra/clangd/Preamble.cpp
+++ b/clang-tools-extra/clangd/Preamble.cpp
@@ -7,6 +7,7 @@
 //===----------------------------------------------------------------------===//
 
 #include "Preamble.h"
+#include "AST.h"
 #include "CollectMacros.h"
 #include "Compiler.h"
 #include "Config.h"
@@ -166,27 +167,6 @@ class CppFilePreambleCallbacks : public PreambleCallbacks {
         collectPragmaMarksCallback(*SourceMgr, Marks));
   }
 
-  static bool isLikelyForwardingFunction(FunctionTemplateDecl *FT) {
-    const auto *FD = FT->getTemplatedDecl();
-    const auto NumParams = FD->getNumParams();
-    // Check whether its last parameter is a parameter pack...
-    if (NumParams > 0) {
-      const auto *LastParam = FD->getParamDecl(NumParams - 1);
-      if (const auto *PET = dyn_cast<PackExpansionType>(LastParam->getType())) {
-        // ... of the type T&&... or T...
-        const auto BaseType = PET->getPattern().getNonReferenceType();
-        if (const auto *TTPT =
-                dyn_cast<TemplateTypeParmType>(BaseType.getTypePtr())) {
-          // ... whose template parameter comes from the function directly
-          if (FT->getTemplateParameters()->getDepth() == TTPT->getDepth()) {
-            return true;
-          }
-        }
-      }
-    }
-    return false;
-  }
-
   bool shouldSkipFunctionBody(Decl *D) override {
     // Usually we don't need to look inside the bodies of header functions
     // to understand the program. However when forwarding function like
diff --git a/clang-tools-extra/clangd/index/SymbolCollector.cpp b/clang-tools-extra/clangd/index/SymbolCollector.cpp
index 39c479b5f4d5b..11afd16e5f99d 100644
--- a/clang-tools-extra/clangd/index/SymbolCollector.cpp
+++ b/clang-tools-extra/clangd/index/SymbolCollector.cpp
@@ -29,6 +29,7 @@
 #include "clang/AST/DeclTemplate.h"
 #include "clang/AST/DeclarationName.h"
 #include "clang/AST/Expr.h"
+#include "clang/AST/RecursiveASTVisitor.h"
 #include "clang/Basic/FileEntry.h"
 #include "clang/Basic/LangOptions.h"
 #include "clang/Basic/SourceLocation.h"
@@ -576,6 +577,22 @@ SymbolCollector::getRefContainer(const Decl *Enclosing,
   return Enclosing;
 }
 
+class ForwardingToConstructorVisitor
+    : public RecursiveASTVisitor<ForwardingToConstructorVisitor> {
+public:
+  ForwardingToConstructorVisitor() {}
+
+  bool VisitCXXConstructExpr(CXXConstructExpr *E) {
+    if (auto *Callee = E->getConstructor()) {
+      Constructors.push_back(Callee);
+    }
+    return true;
+  }
+
+  // Output of this visitor
+  std::vector<CXXConstructorDecl *> Constructors{};
+};
+
 // Always return true to continue indexing.
 bool SymbolCollector::handleDeclOccurrence(
     const Decl *D, index::SymbolRoleSet Roles,
@@ -669,6 +686,27 @@ bool SymbolCollector::handleDeclOccurrence(
       addRef(ID, SymbolRef{FileLoc, FID, Roles, index::getSymbolInfo(ND).Kind,
                            getRefContainer(ASTNode.Parent, Opts),
                            isSpelled(FileLoc, *ND)});
+      // Also collect indirect constructor calls like `make_unique`
+      if (auto *FD = llvm::dyn_cast<clang::FunctionDecl>(D)) {
+        if (auto *FT = FD->getDescribedFunctionTemplate();
+            FT && isLikelyForwardingFunction(FT)) {
+          ForwardingToConstructorVisitor Visitor{};
+          for (auto *Specialized : FT->specializations()) {
+            Visitor.TraverseStmt(Specialized->getBody());
+          }
+          auto FileLoc = SM.getFileLoc(Loc);
+          auto FID = SM.getFileID(FileLoc);
+          if (Opts.RefsInHeaders || FID == SM.getMainFileID()) {
+            for (auto *Constructor : Visitor.Constructors) {
+              addRef(getSymbolIDCached(Constructor),
+                     SymbolRef{FileLoc, FID, Roles,
+                               index::getSymbolInfo(ND).Kind,
+                               getRefContainer(ASTNode.Parent, Opts),
+                               isSpelled(FileLoc, *ND)});
+            }
+          }
+        }
+      }
     }
   }
   // Don't continue indexing if this is a mere reference.
diff --git a/clang-tools-extra/clangd/unittests/XRefsTests.cpp b/clang-tools-extra/clangd/unittests/XRefsTests.cpp
index 7ed08d7cce3d3..51e29f5f1af43 100644
--- a/clang-tools-extra/clangd/unittests/XRefsTests.cpp
+++ b/clang-tools-extra/clangd/unittests/XRefsTests.cpp
@@ -13,6 +13,7 @@
 #include "SyncAPI.h"
 #include "TestFS.h"
 #include "TestTU.h"
+#include "TestWorkspace.h"
 #include "XRefs.h"
 #include "index/MemIndex.h"
 #include "clang/AST/Decl.h"
@@ -2713,6 +2714,36 @@ TEST(FindReferences, NoQueryForLocalSymbols) {
   }
 }
 
+TEST(FindReferences, ForwardingInIndex) {
+  Annotations Header(R"cpp(
+    namespace std {
+    template <class T> T &&forward(T &t);
+    template <class T, class... Args> T *make_unique(Args &&...args) {
+      return new T(std::forward<Args>(args)...);
+    }
+    }
+    struct Test {
+      [[T^est]](){}
+    };
+  )cpp");
+  Annotations Main(R"cpp(
+    #include "header.hpp"
+    int main() {
+      auto a = std::[[make_unique]]<Test>();
+    }
+  )cpp");
+  TestWorkspace TW;
+  TW.addMainFile("header.hpp", Header.code());
+  TW.addMainFile("main.cpp", Main.code());
+  auto AST = TW.openFile("header.hpp").value();
+  auto Index = TW.index();
+
+  EXPECT_THAT(findReferences(AST, Header.point(), 0, Index.get(),
+                             /*AddContext*/ true)
+                  .References,
+              ElementsAre(rangeIs(Header.range()), rangeIs(Main.range())));
+}
+
 TEST(GetNonLocalDeclRefs, All) {
   struct Case {
     llvm::StringRef AnnotatedCode;

@github-actions
Copy link

github-actions bot commented Nov 26, 2025

✅ With the latest revision this PR passed the C/C++ code formatter.

@timon-ul
Copy link
Contributor Author

Interesting side effect I noticed when applying this to a project:

using std::make_unique;

void main(){
    auto a = make_unique<Ab>();
}

the usingstatement is also now recorded as a reference for the constructor AB and I have no clue why, also not sure if this is worth fixing,

@timon-ul
Copy link
Contributor Author

Laying in bed yesterday I realized that yeah I need to fix that, basically all instantiations of such a template, even if they have the wrong type parameter will be returned now, which is definitely not intended behaviour, the code is missing a way to say "only record a reference for this specialization" and not "for all specializations".

@timon-ul timon-ul marked this pull request as draft November 27, 2025 23:17
idea is to directly at the instantiation index the template for
constructor calls. For some reason the function declaration never is an
instantiation though.
@github-actions
Copy link

github-actions bot commented Nov 28, 2025

🐧 Linux x64 Test Results

  • 3055 tests passed
  • 7 tests skipped

✅ The build succeeded and all tests passed.

@timon-ul timon-ul marked this pull request as ready for review November 30, 2025 17:23
@timon-ul timon-ul changed the title clangd: Extend index references with constructor calls through forwarding clangd: Extend reference search with constructor calls through forwarding Nov 30, 2025
Copy link
Collaborator

@HighCommander4 HighCommander4 left a comment

Choose a reason for hiding this comment

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

Nice work so far, thanks!

Some high-level thoughts:

  • General
    • What do you think about restricting the matches to constructor calls that occur in a new expression for now? That seems a bit more conservative, and would avoid accidental matches of other things (like maybe implicitly called constructors).
    • We don't need to handle it in this PR, but the Searchfox implementation (as well as clangd's parameter hint forwarding feature) handle chained forwarding functions, e.g. "make_myptr<T> calls make_myptr_impl<T> which calls new T(...)" sort of thing. We'll probably want to explore that as a follow-up as well.
  • SymbolCollector
    • As discussed on Discord, we'll want some caching. Currently, our logic will run for every occurrence of a given forwarding-function instantiation (e.g. for every use of make_unique<Concrete> in a given file). We can avoid this pretty easily, by keeping around a SmallSet<FunctionDecl*> SeenForwardingFuncInstances and only processing an instantiation if we haven't seen it already. Note that the caching needs to happen at the level of the instantiation, not the primary template (if we've seen a call to make_unique<Foo>, we still want to process a call to make_unique<Bar>).
  • AST implementation
    • This is a very clever approach, nice! But this too will need some caching. Currently, we're going to traverse the body of every forwarding-function instantiation in the AST on every find-references operation. Unlike building the index, this is an operation with "real-time" responsiveness needs (i.e. the user invoked the operation and wants their results quickly), so we should try to cache our results across different find-refs invocations. Something like a DenseMap<FunctionDecl*,CXXConstructorDecl*> that maps forwarding function instantiations to the constructors they call. This can be stored on the ParsedAST (so we get caching in between find-ref operations; note the whole ParsedAST is rebuilt when the source file is modified, so we don't need to worry about invalidating the cache), and mappings populated the first time we encounter a given forwarding-function instantiation.

@timon-ul
Copy link
Contributor Author

timon-ul commented Dec 2, 2025

restricting the matches to constructor calls that occur in a new expression for now

Yeah makes sense.

handle chained forwarding functions

I was thinking about this too, will for sure work on this as a followup task, seems a bit complicated again though, I think I now have to track where the parameter pack is going?

we'll want some caching

I can't believe I forgot about the cache again....

keeping around a SmallSet<FunctionDecl*> SeenForwardingFuncInstances

Is this right? Shouldn't it be a map mapping the declaration to the constructors? Because if I run across for example make_unique<Foo> a 2nd time I indeed do not need to traverse its body again, but I still need to record the current location as a reference to the constructor of Foo, right?

@HighCommander4
Copy link
Collaborator

keeping around a SmallSet<FunctionDecl*> SeenForwardingFuncInstances

Is this right? Shouldn't it be a map mapping the declaration to the constructors? Because if I run across for example make_unique<Foo> a 2nd time I indeed do not need to traverse its body again, but I still need to record the current location as a reference to the constructor of Foo, right?

Yes, you're right, this cache should be a map from forwarding-function instances to constructors, just like in the AST case. My bad!

@timon-ul
Copy link
Contributor Author

timon-ul commented Dec 3, 2025

Since the cache made the code uglier I also tried to clean up a bit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants