[clang-tools-extra] Symbol tags in SymbolInformation, WorkspaceSymbol, CallHierarchyItem and TypeHierarchyItem (PR #170103)

Dimitri Ratz via cfe-commits cfe-commits at lists.llvm.org
Tue Apr 14 03:59:10 PDT 2026


https://github.com/ratzdi updated https://github.com/llvm/llvm-project/pull/170103

>From 910a890e96e231d19a5833c02c280a03a2283271 Mon Sep 17 00:00:00 2001
From: Dimitri Ratz <dimitri.ratz at thinkdigital.cc>
Date: Fri, 28 Nov 2025 14:01:25 +0100
Subject: [PATCH 01/17] Fill symbol tags for the response TypeHierarchyItem of
 prepareTypeHierarchy.

---
 clang-tools-extra/clangd/FindSymbols.cpp      | 14 +++++++++++++
 clang-tools-extra/clangd/FindSymbols.h        |  3 +++
 clang-tools-extra/clangd/Protocol.cpp         |  2 ++
 clang-tools-extra/clangd/XRefs.cpp            | 21 ++++++++++---------
 .../clangd/test/type-hierarchy.test           |  9 ++++++++
 5 files changed, 39 insertions(+), 10 deletions(-)

diff --git a/clang-tools-extra/clangd/FindSymbols.cpp b/clang-tools-extra/clangd/FindSymbols.cpp
index 147dd38db8a8a..40e31af5b8ce3 100644
--- a/clang-tools-extra/clangd/FindSymbols.cpp
+++ b/clang-tools-extra/clangd/FindSymbols.cpp
@@ -228,6 +228,20 @@ std::vector<SymbolTag> getSymbolTags(const NamedDecl &ND) {
   return Tags;
 }
 
+std::vector<SymbolTag> getSymbolTags(const Symbol &S) {
+  std::vector<SymbolTag> Tags;
+
+  if (S.Flags & Symbol::Deprecated)
+    Tags.push_back(SymbolTag::Deprecated);
+
+  if (S.Definition)
+    Tags.push_back(SymbolTag::Definition);
+  else
+    Tags.push_back(SymbolTag::Declaration);
+
+  return Tags;
+}
+
 namespace {
 using ScoredSymbolInfo = std::pair<float, SymbolInformation>;
 struct ScoredSymbolGreater {
diff --git a/clang-tools-extra/clangd/FindSymbols.h b/clang-tools-extra/clangd/FindSymbols.h
index 97b99af4f35e6..4847ea359e9ef 100644
--- a/clang-tools-extra/clangd/FindSymbols.h
+++ b/clang-tools-extra/clangd/FindSymbols.h
@@ -69,6 +69,9 @@ SymbolTags computeSymbolTags(const NamedDecl &ND);
 /// \p ND The declaration to get tags for.
 std::vector<SymbolTag> getSymbolTags(const NamedDecl &ND);
 
+/// Returns the symbol tags for an index `Symbol`.
+std::vector<SymbolTag> getSymbolTags(const Symbol &S);
+
 } // namespace clangd
 } // namespace clang
 
diff --git a/clang-tools-extra/clangd/Protocol.cpp b/clang-tools-extra/clangd/Protocol.cpp
index 793db7b052990..81ee6c9287ee1 100644
--- a/clang-tools-extra/clangd/Protocol.cpp
+++ b/clang-tools-extra/clangd/Protocol.cpp
@@ -1445,6 +1445,8 @@ llvm::json::Value toJSON(const TypeHierarchyItem &I) {
 
   if (I.detail)
     Result["detail"] = I.detail;
+  if(!I.tags.empty())
+    Result["tags"] = I.tags;
   return std::move(Result);
 }
 
diff --git a/clang-tools-extra/clangd/XRefs.cpp b/clang-tools-extra/clangd/XRefs.cpp
index 5b9ba1baa0705..5a9a9f3eab770 100644
--- a/clang-tools-extra/clangd/XRefs.cpp
+++ b/clang-tools-extra/clangd/XRefs.cpp
@@ -1872,6 +1872,7 @@ static std::optional<HierarchyItem> symbolToHierarchyItem(const Symbol &S,
   HI.name = std::string(S.Name);
   HI.detail = (S.Scope + S.Name).str();
   HI.kind = indexSymbolKindToSymbolKind(S.SymInfo);
+  HI.tags = getSymbolTags(S);
   HI.selectionRange = Loc->range;
   // FIXME: Populate 'range' correctly
   // (https://github.com/clangd/clangd/issues/59).
@@ -2136,15 +2137,15 @@ static QualType typeForNode(const ASTContext &Ctx, const HeuristicResolver *H,
   return QualType();
 }
 
-// Given a type targeted by the cursor, return one or more types that are more interesting
-// to target.
-static void unwrapFindType(
-    QualType T, const HeuristicResolver* H, llvm::SmallVector<QualType>& Out) {
+// Given a type targeted by the cursor, return one or more types that are more
+// interesting to target.
+static void unwrapFindType(QualType T, const HeuristicResolver *H,
+                           llvm::SmallVector<QualType> &Out) {
   if (T.isNull())
     return;
 
   // If there's a specific type alias, point at that rather than unwrapping.
-  if (const auto* TDT = T->getAs<TypedefType>())
+  if (const auto *TDT = T->getAs<TypedefType>())
     return Out.push_back(QualType(TDT, 0));
 
   // Pointers etc => pointee type.
@@ -2178,8 +2179,8 @@ static void unwrapFindType(
 }
 
 // Convenience overload, to allow calling this without the out-parameter
-static llvm::SmallVector<QualType> unwrapFindType(
-    QualType T, const HeuristicResolver* H) {
+static llvm::SmallVector<QualType> unwrapFindType(QualType T,
+                                                  const HeuristicResolver *H) {
   llvm::SmallVector<QualType> Result;
   unwrapFindType(T, H, Result);
   return Result;
@@ -2201,9 +2202,9 @@ std::vector<LocatedSymbol> findType(ParsedAST &AST, Position Pos,
     std::vector<LocatedSymbol> LocatedSymbols;
 
     // NOTE: unwrapFindType might return duplicates for something like
-    // unique_ptr<unique_ptr<T>>. Let's *not* remove them, because it gives you some
-    // information about the type you may have not known before
-    // (since unique_ptr<unique_ptr<T>> != unique_ptr<T>).
+    // unique_ptr<unique_ptr<T>>. Let's *not* remove them, because it gives you
+    // some information about the type you may have not known before (since
+    // unique_ptr<unique_ptr<T>> != unique_ptr<T>).
     for (const QualType &Type : unwrapFindType(
              typeForNode(AST.getASTContext(), AST.getHeuristicResolver(), N),
              AST.getHeuristicResolver()))
diff --git a/clang-tools-extra/clangd/test/type-hierarchy.test b/clang-tools-extra/clangd/test/type-hierarchy.test
index a5f13ab13d0b3..918a37a74098c 100644
--- a/clang-tools-extra/clangd/test/type-hierarchy.test
+++ b/clang-tools-extra/clangd/test/type-hierarchy.test
@@ -44,6 +44,9 @@
 # CHECK-NEXT:           "line": 2
 # CHECK-NEXT:         }
 # CHECK-NEXT:       },
+# CHECK-NEXT:       "tags": [
+# CHECK-NEXT:         19
+# CHECK-NEXT:       ],
 # CHECK-NEXT:       "uri": "file://{{.*}}/clangd-test/main.cpp"
 # CHECK-NEXT:     }
 # CHECK-NEXT:   ]
@@ -85,6 +88,9 @@
 # CHECK-NEXT:           "line": 1
 # CHECK-NEXT:         }
 # CHECK-NEXT:       },
+# CHECK-NEXT:       "tags": [
+# CHECK-NEXT:         19
+# CHECK-NEXT:       ],
 # CHECK-NEXT:       "uri": "file://{{.*}}/clangd-test/main.cpp"
 # CHECK-NEXT:     }
 # CHECK-NEXT:  ]
@@ -136,6 +142,9 @@
 # CHECK-NEXT:           "line": 3
 # CHECK-NEXT:         }
 # CHECK-NEXT:       },
+# CHECK-NEXT:       "tags": [
+# CHECK-NEXT:         19
+# CHECK-NEXT:       ],
 # CHECK-NEXT:       "uri": "file://{{.*}}/clangd-test/main.cpp"
 # CHECK-NEXT:     }
 # CHECK-NEXT:  ]

>From 01a022e6ea330ec0b3bcbae3997644cac733cab1 Mon Sep 17 00:00:00 2001
From: Dimitri Ratz <dimitri.ratz at thinkdigital.cc>
Date: Mon, 1 Dec 2025 11:34:51 +0100
Subject: [PATCH 02/17] Fill symbol tags into the object SymbolInformation, and
 on getting workspace symbols.

---
 clang-tools-extra/clangd/FindSymbols.cpp   | 1 +
 clang-tools-extra/clangd/Protocol.cpp      | 2 ++
 clang-tools-extra/clangd/test/symbols.test | 5 ++++-
 3 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/clang-tools-extra/clangd/FindSymbols.cpp b/clang-tools-extra/clangd/FindSymbols.cpp
index 40e31af5b8ce3..175645d4d7c39 100644
--- a/clang-tools-extra/clangd/FindSymbols.cpp
+++ b/clang-tools-extra/clangd/FindSymbols.cpp
@@ -373,6 +373,7 @@ getWorkspaceSymbols(llvm::StringRef Query, int Limit,
     Info.score = Relevance.NameMatch > std::numeric_limits<float>::epsilon()
                      ? Score / Relevance.NameMatch
                      : QualScore;
+    Info.tags = getSymbolTags(Sym);
     Top.push({Score, std::move(Info)});
   });
   for (auto &R : std::move(Top).items())
diff --git a/clang-tools-extra/clangd/Protocol.cpp b/clang-tools-extra/clangd/Protocol.cpp
index 81ee6c9287ee1..fb3919f587ff2 100644
--- a/clang-tools-extra/clangd/Protocol.cpp
+++ b/clang-tools-extra/clangd/Protocol.cpp
@@ -867,6 +867,8 @@ llvm::json::Value toJSON(const SymbolInformation &P) {
   };
   if (P.score)
     O["score"] = *P.score;
+  if(!P.tags.empty())
+    O["tags"] = P.tags;
   return std::move(O);
 }
 
diff --git a/clang-tools-extra/clangd/test/symbols.test b/clang-tools-extra/clangd/test/symbols.test
index a16a226e48c05..4f0d7b8b08569 100644
--- a/clang-tools-extra/clangd/test/symbols.test
+++ b/clang-tools-extra/clangd/test/symbols.test
@@ -24,7 +24,10 @@
 # CHECK-NEXT:          "uri": "file://{{.*}}/vector.h"
 # CHECK-NEXT:        },
 # CHECK-NEXT:        "name": "vector",
-# CHECK-NEXT:        "score": {{.*}}
+# CHECK-NEXT:        "score": {{.*}},
+# CHECK-NEXT:        "tags": [
+# CHECK-NEXT:          18
+# CHECK-NEXT:        ]
 # CHECK-NEXT:      }
 # CHECK-NEXT:    ]
 # CHECK-NEXT:}

>From c450d629f9699706eaaa51ab059683f420d2873b Mon Sep 17 00:00:00 2001
From: Dimitri Ratz <dimitri.ratz at thinkdigital.cc>
Date: Mon, 1 Dec 2025 12:03:35 +0100
Subject: [PATCH 03/17] Minor improvements.

---
 clang-tools-extra/clangd/FindSymbols.cpp | 3 +++
 clang-tools-extra/clangd/FindSymbols.h   | 6 +++++-
 clang-tools-extra/clangd/XRefs.cpp       | 2 --
 3 files changed, 8 insertions(+), 3 deletions(-)

diff --git a/clang-tools-extra/clangd/FindSymbols.cpp b/clang-tools-extra/clangd/FindSymbols.cpp
index 175645d4d7c39..5209dd82dc738 100644
--- a/clang-tools-extra/clangd/FindSymbols.cpp
+++ b/clang-tools-extra/clangd/FindSymbols.cpp
@@ -13,7 +13,10 @@
 #include "Quality.h"
 #include "SourceCode.h"
 #include "index/Index.h"
+#include "index/Symbol.h"
+#include "index/SymbolLocation.h"
 #include "support/Logger.h"
+#include "clang/AST/Decl.h"
 #include "clang/AST/DeclFriend.h"
 #include "clang/AST/DeclTemplate.h"
 #include "clang/Index/IndexSymbol.h"
diff --git a/clang-tools-extra/clangd/FindSymbols.h b/clang-tools-extra/clangd/FindSymbols.h
index 4847ea359e9ef..169efa9a84584 100644
--- a/clang-tools-extra/clangd/FindSymbols.h
+++ b/clang-tools-extra/clangd/FindSymbols.h
@@ -14,13 +14,17 @@
 
 #include "Protocol.h"
 #include "index/Symbol.h"
-#include "clang/AST/Decl.h"
 #include "llvm/ADT/StringRef.h"
+#include "clang/AST/Decl.h"
 
 namespace clang {
+class NamedDecl;
+
 namespace clangd {
 class ParsedAST;
 class SymbolIndex;
+struct Symbol;
+struct SymbolLocation;
 
 /// A bitmask type representing symbol tags supported by LSP.
 /// \see
diff --git a/clang-tools-extra/clangd/XRefs.cpp b/clang-tools-extra/clangd/XRefs.cpp
index 5a9a9f3eab770..4facc2c6c5c7d 100644
--- a/clang-tools-extra/clangd/XRefs.cpp
+++ b/clang-tools-extra/clangd/XRefs.cpp
@@ -1898,8 +1898,6 @@ symbolToCallHierarchyItem(const Symbol &S, PathRef TUPath) {
   if (!Result)
     return Result;
   Result->data = S.ID.str();
-  if (S.Flags & Symbol::Deprecated)
-    Result->tags.push_back(SymbolTag::Deprecated);
   return Result;
 }
 

>From c297add73cd10bfead6b34c7035279dabde201da Mon Sep 17 00:00:00 2001
From: Dimitri Ratz <dimitri.ratz at thinkdigital.cc>
Date: Wed, 3 Dec 2025 13:09:58 +0100
Subject: [PATCH 04/17] Set the detail field of HierarchyItem.

---
 clang-tools-extra/clangd/XRefs.cpp                    | 2 +-
 clang-tools-extra/clangd/test/call-hierarchy.test     | 1 +
 clang-tools-extra/clangd/test/type-hierarchy-ext.test | 3 +++
 clang-tools-extra/clangd/test/type-hierarchy.test     | 1 +
 4 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/clang-tools-extra/clangd/XRefs.cpp b/clang-tools-extra/clangd/XRefs.cpp
index 4facc2c6c5c7d..7483440f97c67 100644
--- a/clang-tools-extra/clangd/XRefs.cpp
+++ b/clang-tools-extra/clangd/XRefs.cpp
@@ -1818,7 +1818,7 @@ declToHierarchyItem(const NamedDecl &ND, llvm::StringRef TUPath) {
 
   HierarchyItem HI;
   HI.name = printName(Ctx, ND);
-  // FIXME: Populate HI.detail the way we do in symbolToHierarchyItem?
+  HI.detail = printQualifiedName(ND);
   HI.kind = SK;
   HI.range = Range{sourceLocToPosition(SM, DeclRange->getBegin()),
                    sourceLocToPosition(SM, DeclRange->getEnd())};
diff --git a/clang-tools-extra/clangd/test/call-hierarchy.test b/clang-tools-extra/clangd/test/call-hierarchy.test
index f0d57b60421a4..fb0766c089ee5 100644
--- a/clang-tools-extra/clangd/test/call-hierarchy.test
+++ b/clang-tools-extra/clangd/test/call-hierarchy.test
@@ -9,6 +9,7 @@
 # CHECK-NEXT:  "result": [
 # CHECK-NEXT:    {
 # CHECK-NEXT:      "data": "{{.*}}",
+# CHECK-NEXT:      "detail": "callee",
 # CHECK-NEXT:      "kind": 12,
 # CHECK-NEXT:      "name": "callee",
 # CHECK-NEXT:      "range": {
diff --git a/clang-tools-extra/clangd/test/type-hierarchy-ext.test b/clang-tools-extra/clangd/test/type-hierarchy-ext.test
index 8d1a5dc31da0f..983c7538088bf 100644
--- a/clang-tools-extra/clangd/test/type-hierarchy-ext.test
+++ b/clang-tools-extra/clangd/test/type-hierarchy-ext.test
@@ -52,6 +52,7 @@
 # CHECK-NEXT:        ],
 # CHECK-NEXT:        "symbolID": "8A991335E4E67D08"
 # CHECK-NEXT:    },
+# CHECK-NEXT:    "detail": "Child2",
 # CHECK-NEXT:    "kind": 23,
 # CHECK-NEXT:    "name": "Child2",
 # CHECK-NEXT:    "parents": [
@@ -65,6 +66,7 @@
 # CHECK-NEXT:          ],
 # CHECK-NEXT:          "symbolID": "ECDC0C46D75120F4"
 # CHECK-NEXT:        },
+# CHECK-NEXT:        "detail": "Child1",
 # CHECK-NEXT:        "kind": 23,
 # CHECK-NEXT:        "name": "Child1",
 # CHECK-NEXT:        "parents": [
@@ -73,6 +75,7 @@
 # CHECK-NEXT:             "parents": [],
 # CHECK-NEXT:             "symbolID": "FE546E7B648D69A7"
 # CHECK-NEXT:            },
+# CHECK-NEXT:            "detail": "Parent",
 # CHECK-NEXT:            "kind": 23,
 # CHECK-NEXT:            "name": "Parent",
 # CHECK-NEXT:            "parents": [],
diff --git a/clang-tools-extra/clangd/test/type-hierarchy.test b/clang-tools-extra/clangd/test/type-hierarchy.test
index 918a37a74098c..d1dda4b92c29c 100644
--- a/clang-tools-extra/clangd/test/type-hierarchy.test
+++ b/clang-tools-extra/clangd/test/type-hierarchy.test
@@ -22,6 +22,7 @@
 # CHECK-NEXT:        ],
 # CHECK-NEXT:        "symbolID": "8A991335E4E67D08"
 # CHECK-NEXT:       },
+# CHECK-NEXT:       "detail": "Child2",
 # CHECK-NEXT:       "kind": 23,
 # CHECK-NEXT:       "name": "Child2",
 # CHECK-NEXT:       "range": {

>From 35ee021c67bdb5ed50b422f7477d89330a4c8714 Mon Sep 17 00:00:00 2001
From: Dimitri Ratz <dimitri.ratz at thinkdigital.cc>
Date: Fri, 12 Dec 2025 12:17:56 +0100
Subject: [PATCH 05/17] Collect symbols tags from AST in the method
 incomingCalls.

---
 clang-tools-extra/clangd/ClangdLSPServer.cpp  |  2 +-
 clang-tools-extra/clangd/ClangdServer.cpp     | 12 ++--
 clang-tools-extra/clangd/ClangdServer.h       |  5 +-
 clang-tools-extra/clangd/XRefs.cpp            | 57 +++++++++++++++++--
 clang-tools-extra/clangd/XRefs.h              |  3 +-
 .../clangd/unittests/CallHierarchyTests.cpp   | 56 +++++++++---------
 6 files changed, 93 insertions(+), 42 deletions(-)

diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp
index ebd42abd2dd61..0a57573ecc469 100644
--- a/clang-tools-extra/clangd/ClangdLSPServer.cpp
+++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp
@@ -1389,7 +1389,7 @@ void ClangdLSPServer::onPrepareCallHierarchy(
 void ClangdLSPServer::onCallHierarchyIncomingCalls(
     const CallHierarchyIncomingCallsParams &Params,
     Callback<std::vector<CallHierarchyIncomingCall>> Reply) {
-  Server->incomingCalls(Params.item, std::move(Reply));
+  Server->incomingCalls(Params.item.uri.file(), Params.item, std::move(Reply));
 }
 
 void ClangdLSPServer::onClangdInlayHints(const InlayHintsParams &Params,
diff --git a/clang-tools-extra/clangd/ClangdServer.cpp b/clang-tools-extra/clangd/ClangdServer.cpp
index f1a87dd12d905..21d69b99ad8d4 100644
--- a/clang-tools-extra/clangd/ClangdServer.cpp
+++ b/clang-tools-extra/clangd/ClangdServer.cpp
@@ -909,12 +909,16 @@ void ClangdServer::prepareCallHierarchy(
 }
 
 void ClangdServer::incomingCalls(
+    PathRef File,
     const CallHierarchyItem &Item,
     Callback<std::vector<CallHierarchyIncomingCall>> CB) {
-  WorkScheduler->run("Incoming Calls", "",
-                     [CB = std::move(CB), Item, this]() mutable {
-                       CB(clangd::incomingCalls(Item, Index));
-                     });
+  auto Action = [Item, CB = std::move(CB), this](
+      llvm::Expected<InputsAndAST> InpAST) mutable {
+    if (!InpAST)
+      return CB(InpAST.takeError());
+    CB(clangd::incomingCalls(Item, Index, InpAST->AST));
+  };
+  WorkScheduler->runWithAST("Incoming Calls", File, std::move(Action));
 }
 
 void ClangdServer::inlayHints(PathRef File, std::optional<Range> RestrictRange,
diff --git a/clang-tools-extra/clangd/ClangdServer.h b/clang-tools-extra/clangd/ClangdServer.h
index 3ffaf67553dce..ae7e25e2b6db6 100644
--- a/clang-tools-extra/clangd/ClangdServer.h
+++ b/clang-tools-extra/clangd/ClangdServer.h
@@ -299,8 +299,9 @@ class ClangdServer {
                             Callback<std::vector<CallHierarchyItem>> CB);
 
   /// Resolve incoming calls for a given call hierarchy item.
-  void incomingCalls(const CallHierarchyItem &Item,
-                     Callback<std::vector<CallHierarchyIncomingCall>>);
+  void incomingCalls(PathRef File,
+                     const CallHierarchyItem &Item,
+                     Callback<std::vector<CallHierarchyIncomingCall>> CB);
 
   /// Resolve outgoing calls for a given call hierarchy item.
   void outgoingCalls(const CallHierarchyItem &Item,
diff --git a/clang-tools-extra/clangd/XRefs.cpp b/clang-tools-extra/clangd/XRefs.cpp
index 7483440f97c67..776e1a33e6c68 100644
--- a/clang-tools-extra/clangd/XRefs.cpp
+++ b/clang-tools-extra/clangd/XRefs.cpp
@@ -2349,6 +2349,36 @@ void resolveTypeHierarchy(TypeHierarchyItem &Item, int ResolveLevels,
                Item.uri.file());
 }
 
+// Tries to find a NamedDecl in the AST that matches the given Symbol.
+// Returns nullptr if the symbol is not found in the current AST.
+const NamedDecl *getNamedDeclFromSymbol(const Symbol &Sym,
+                                        const ParsedAST &AST) {
+  // Try to convert the symbol to a location and find the decl at that location
+  auto SymLoc = symbolToLocation(Sym, AST.tuPath());
+  if (!SymLoc)
+    return nullptr;
+
+  // Check if the symbol location is in the main file
+  if (SymLoc->uri.file() != AST.tuPath())
+    return nullptr;
+
+  // Convert LSP position to source location
+  const auto &SM = AST.getSourceManager();
+  auto CurLoc = sourceLocationInMainFile(SM, SymLoc->range.start);
+  if (!CurLoc) {
+    llvm::consumeError(CurLoc.takeError());
+    return nullptr;
+  }
+
+  // Get all decls at this location
+  auto Decls = getDeclAtPosition(const_cast<ParsedAST &>(AST), *CurLoc, {});
+  if (Decls.empty())
+    return nullptr;
+
+  // Return the first decl (usually the most specific one)
+  return Decls[0];
+}
+
 std::vector<CallHierarchyItem>
 prepareCallHierarchy(ParsedAST &AST, Position Pos, PathRef TUPath) {
   std::vector<CallHierarchyItem> Result;
@@ -2376,8 +2406,10 @@ prepareCallHierarchy(ParsedAST &AST, Position Pos, PathRef TUPath) {
 }
 
 std::vector<CallHierarchyIncomingCall>
-incomingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index) {
+incomingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index,
+              const ParsedAST &AST) {
   std::vector<CallHierarchyIncomingCall> Results;
+
   if (!Index || Item.data.empty())
     return Results;
   auto ID = SymbolID::fromStr(Item.data);
@@ -2421,14 +2453,27 @@ incomingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index) {
     Index->lookup(ContainerLookup, [&](const Symbol &Caller) {
       auto It = CallsIn.find(Caller.ID);
       assert(It != CallsIn.end());
-      if (auto CHI = symbolToCallHierarchyItem(Caller, Item.uri.file())) {
+      if (auto *ND = getNamedDeclFromSymbol(Caller, AST)) {
+        if (auto CHI = declToCallHierarchyItem(*ND, AST.tuPath())) {
+          std::vector<Range> FromRanges;
+          for (const Location &L : It->second) {
+            if (L.uri != CHI->uri) {
+              // Call location not in same file as caller.
+              // This can happen in some edge cases. There's not much we can do,
+              // since the protocol only allows returning ranges interpreted as
+              // being in the caller's file.
+              continue;
+            }
+            FromRanges.push_back(L.range);
+          }
+          Results.push_back(CallHierarchyIncomingCall{
+              std::move(*CHI), std::move(FromRanges), MightNeverCall});
+        }
+      } else if (auto CHI = symbolToCallHierarchyItem(Caller, Item.uri.file())) {
+        // Fallback to using symbol if NamedDecl is not available
         std::vector<Range> FromRanges;
         for (const Location &L : It->second) {
           if (L.uri != CHI->uri) {
-            // Call location not in same file as caller.
-            // This can happen in some edge cases. There's not much we can do,
-            // since the protocol only allows returning ranges interpreted as
-            // being in the caller's file.
             continue;
           }
           FromRanges.push_back(L.range);
diff --git a/clang-tools-extra/clangd/XRefs.h b/clang-tools-extra/clangd/XRefs.h
index 247e52314c3f9..1019fa189a613 100644
--- a/clang-tools-extra/clangd/XRefs.h
+++ b/clang-tools-extra/clangd/XRefs.h
@@ -148,7 +148,8 @@ std::vector<CallHierarchyItem>
 prepareCallHierarchy(ParsedAST &AST, Position Pos, PathRef TUPath);
 
 std::vector<CallHierarchyIncomingCall>
-incomingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index);
+incomingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index,
+              const ParsedAST &AST);
 
 std::vector<CallHierarchyOutgoingCall>
 outgoingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index);
diff --git a/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp b/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp
index 9859577c7cf7e..f5e9983aa70ec 100644
--- a/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp
+++ b/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp
@@ -89,12 +89,12 @@ TEST(CallHierarchy, IncomingOneFileCpp) {
   std::vector<CallHierarchyItem> Items =
       prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename));
   ASSERT_THAT(Items, ElementsAre(withName("callee")));
-  auto IncomingLevel1 = incomingCalls(Items[0], Index.get());
+  auto IncomingLevel1 = incomingCalls(Items[0], Index.get(), AST);
   ASSERT_THAT(
       IncomingLevel1,
       ElementsAre(AllOf(from(AllOf(withName("caller1"), withDetail("caller1"))),
                         iFromRanges(Source.range("Callee")))));
-  auto IncomingLevel2 = incomingCalls(IncomingLevel1[0].from, Index.get());
+  auto IncomingLevel2 = incomingCalls(IncomingLevel1[0].from, Index.get(), AST);
   ASSERT_THAT(
       IncomingLevel2,
       ElementsAre(AllOf(from(AllOf(withName("caller2"), withDetail("caller2"))),
@@ -103,13 +103,13 @@ TEST(CallHierarchy, IncomingOneFileCpp) {
                   AllOf(from(AllOf(withName("caller3"), withDetail("caller3"))),
                         iFromRanges(Source.range("Caller1C")))));
 
-  auto IncomingLevel3 = incomingCalls(IncomingLevel2[0].from, Index.get());
+  auto IncomingLevel3 = incomingCalls(IncomingLevel2[0].from, Index.get(), AST);
   ASSERT_THAT(
       IncomingLevel3,
       ElementsAre(AllOf(from(AllOf(withName("caller3"), withDetail("caller3"))),
                         iFromRanges(Source.range("Caller2")))));
 
-  auto IncomingLevel4 = incomingCalls(IncomingLevel3[0].from, Index.get());
+  auto IncomingLevel4 = incomingCalls(IncomingLevel3[0].from, Index.get(), AST);
   EXPECT_THAT(IncomingLevel4, IsEmpty());
 }
 
@@ -137,12 +137,12 @@ TEST(CallHierarchy, IncomingOneFileObjC) {
   std::vector<CallHierarchyItem> Items =
       prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename));
   ASSERT_THAT(Items, ElementsAre(withName("callee")));
-  auto IncomingLevel1 = incomingCalls(Items[0], Index.get());
+  auto IncomingLevel1 = incomingCalls(Items[0], Index.get(), AST);
   ASSERT_THAT(IncomingLevel1,
               ElementsAre(AllOf(from(AllOf(withName("caller1"),
                                            withDetail("MyClass::caller1"))),
                                 iFromRanges(Source.range("Callee")))));
-  auto IncomingLevel2 = incomingCalls(IncomingLevel1[0].from, Index.get());
+  auto IncomingLevel2 = incomingCalls(IncomingLevel1[0].from, Index.get(), AST);
   ASSERT_THAT(IncomingLevel2,
               ElementsAre(AllOf(from(AllOf(withName("caller2"),
                                            withDetail("MyClass::caller2"))),
@@ -152,13 +152,13 @@ TEST(CallHierarchy, IncomingOneFileObjC) {
                                            withDetail("MyClass::caller3"))),
                                 iFromRanges(Source.range("Caller1C")))));
 
-  auto IncomingLevel3 = incomingCalls(IncomingLevel2[0].from, Index.get());
+  auto IncomingLevel3 = incomingCalls(IncomingLevel2[0].from, Index.get(), AST);
   ASSERT_THAT(IncomingLevel3,
               ElementsAre(AllOf(from(AllOf(withName("caller3"),
                                            withDetail("MyClass::caller3"))),
                                 iFromRanges(Source.range("Caller2")))));
 
-  auto IncomingLevel4 = incomingCalls(IncomingLevel3[0].from, Index.get());
+  auto IncomingLevel4 = incomingCalls(IncomingLevel3[0].from, Index.get(), AST);
   EXPECT_THAT(IncomingLevel4, IsEmpty());
 }
 
@@ -184,18 +184,18 @@ TEST(CallHierarchy, IncomingIncludeOverrides) {
   std::vector<CallHierarchyItem> Items =
       prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename));
   ASSERT_THAT(Items, ElementsAre(withName("callee")));
-  auto IncomingLevel1 = incomingCalls(Items[0], Index.get());
+  auto IncomingLevel1 = incomingCalls(Items[0], Index.get(), AST);
   ASSERT_THAT(IncomingLevel1,
               ElementsAre(AllOf(from(AllOf(withName("Func"),
                                            withDetail("Implementation::Func"))),
                                 iFromRanges(Source.range("Callee")))));
-  auto IncomingLevel2 = incomingCalls(IncomingLevel1[0].from, Index.get());
+  auto IncomingLevel2 = incomingCalls(IncomingLevel1[0].from, Index.get(), AST);
   ASSERT_THAT(
       IncomingLevel2,
       ElementsAre(AllOf(from(AllOf(withName("Test"), withDetail("Test"))),
                         iFromRanges(Source.range("FuncCall")))));
 
-  auto IncomingLevel3 = incomingCalls(IncomingLevel2[0].from, Index.get());
+  auto IncomingLevel3 = incomingCalls(IncomingLevel2[0].from, Index.get(), AST);
   EXPECT_THAT(IncomingLevel3, IsEmpty());
 }
 
@@ -221,13 +221,13 @@ TEST(CallHierarchy, MainFileOnlyRef) {
   std::vector<CallHierarchyItem> Items =
       prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename));
   ASSERT_THAT(Items, ElementsAre(withName("callee")));
-  auto IncomingLevel1 = incomingCalls(Items[0], Index.get());
+  auto IncomingLevel1 = incomingCalls(Items[0], Index.get(), AST);
   ASSERT_THAT(
       IncomingLevel1,
       ElementsAre(AllOf(from(AllOf(withName("caller1"), withDetail("caller1"))),
                         iFromRanges(Source.range("Callee")))));
 
-  auto IncomingLevel2 = incomingCalls(IncomingLevel1[0].from, Index.get());
+  auto IncomingLevel2 = incomingCalls(IncomingLevel1[0].from, Index.get(), AST);
   EXPECT_THAT(
       IncomingLevel2,
       ElementsAre(AllOf(from(AllOf(withName("caller2"), withDetail("caller2"))),
@@ -256,7 +256,7 @@ TEST(CallHierarchy, IncomingQualified) {
   std::vector<CallHierarchyItem> Items =
       prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename));
   ASSERT_THAT(Items, ElementsAre(withName("Waldo::find")));
-  auto Incoming = incomingCalls(Items[0], Index.get());
+  auto Incoming = incomingCalls(Items[0], Index.get(), AST);
   EXPECT_THAT(
       Incoming,
       ElementsAre(
@@ -396,13 +396,13 @@ TEST(CallHierarchy, MultiFileCpp) {
     std::vector<CallHierarchyItem> Items =
         prepareCallHierarchy(AST, Pos, TUPath);
     ASSERT_THAT(Items, ElementsAre(withName("callee")));
-    auto IncomingLevel1 = incomingCalls(Items[0], Index.get());
+    auto IncomingLevel1 = incomingCalls(Items[0], Index.get(), AST);
     ASSERT_THAT(IncomingLevel1,
                 ElementsAre(AllOf(from(AllOf(withName("caller1"),
                                              withDetail("nsa::caller1"))),
                                   iFromRanges(Caller1C.range()))));
 
-    auto IncomingLevel2 = incomingCalls(IncomingLevel1[0].from, Index.get());
+    auto IncomingLevel2 = incomingCalls(IncomingLevel1[0].from, Index.get(), AST);
     ASSERT_THAT(
         IncomingLevel2,
         ElementsAre(
@@ -411,13 +411,13 @@ TEST(CallHierarchy, MultiFileCpp) {
             AllOf(from(AllOf(withName("caller3"), withDetail("nsa::caller3"))),
                   iFromRanges(Caller3C.range("Caller1")))));
 
-    auto IncomingLevel3 = incomingCalls(IncomingLevel2[0].from, Index.get());
+    auto IncomingLevel3 = incomingCalls(IncomingLevel2[0].from, Index.get(), AST);
     ASSERT_THAT(IncomingLevel3,
                 ElementsAre(AllOf(from(AllOf(withName("caller3"),
                                              withDetail("nsa::caller3"))),
                                   iFromRanges(Caller3C.range("Caller2")))));
 
-    auto IncomingLevel4 = incomingCalls(IncomingLevel3[0].from, Index.get());
+    auto IncomingLevel4 = incomingCalls(IncomingLevel3[0].from, Index.get(), AST);
     EXPECT_THAT(IncomingLevel4, IsEmpty());
   };
 
@@ -553,12 +553,12 @@ TEST(CallHierarchy, IncomingMultiFileObjC) {
     std::vector<CallHierarchyItem> Items =
         prepareCallHierarchy(AST, Pos, TUPath);
     ASSERT_THAT(Items, ElementsAre(withName("callee")));
-    auto IncomingLevel1 = incomingCalls(Items[0], Index.get());
+    auto IncomingLevel1 = incomingCalls(Items[0], Index.get(), AST);
     ASSERT_THAT(IncomingLevel1,
                 ElementsAre(AllOf(from(withName("caller1")),
                                   iFromRanges(Caller1C.range()))));
 
-    auto IncomingLevel2 = incomingCalls(IncomingLevel1[0].from, Index.get());
+    auto IncomingLevel2 = incomingCalls(IncomingLevel1[0].from, Index.get(), AST);
     ASSERT_THAT(IncomingLevel2,
                 ElementsAre(AllOf(from(withName("caller2")),
                                   iFromRanges(Caller2C.range("A"),
@@ -566,12 +566,12 @@ TEST(CallHierarchy, IncomingMultiFileObjC) {
                             AllOf(from(withName("caller3")),
                                   iFromRanges(Caller3C.range("Caller1")))));
 
-    auto IncomingLevel3 = incomingCalls(IncomingLevel2[0].from, Index.get());
+    auto IncomingLevel3 = incomingCalls(IncomingLevel2[0].from, Index.get(), AST);
     ASSERT_THAT(IncomingLevel3,
                 ElementsAre(AllOf(from(withName("caller3")),
                                   iFromRanges(Caller3C.range("Caller2")))));
 
-    auto IncomingLevel4 = incomingCalls(IncomingLevel3[0].from, Index.get());
+    auto IncomingLevel4 = incomingCalls(IncomingLevel3[0].from, Index.get(), AST);
     EXPECT_THAT(IncomingLevel4, IsEmpty());
   };
 
@@ -616,7 +616,7 @@ TEST(CallHierarchy, CallInLocalVarDecl) {
       prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename));
   ASSERT_THAT(Items, ElementsAre(withName("callee")));
 
-  auto Incoming = incomingCalls(Items[0], Index.get());
+  auto Incoming = incomingCalls(Items[0], Index.get(), AST);
   ASSERT_THAT(Incoming, ElementsAre(AllOf(from(withName("caller1")),
                                           iFromRanges(Source.range("call1"))),
                                     AllOf(from(withName("caller2")),
@@ -643,7 +643,7 @@ TEST(CallHierarchy, HierarchyOnField) {
   std::vector<CallHierarchyItem> Items =
       prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename));
   ASSERT_THAT(Items, ElementsAre(withName("var1")));
-  auto IncomingLevel1 = incomingCalls(Items[0], Index.get());
+  auto IncomingLevel1 = incomingCalls(Items[0], Index.get(), AST);
   ASSERT_THAT(IncomingLevel1,
               ElementsAre(AllOf(from(withName("caller")),
                                 iFromRanges(Source.range("Callee")))));
@@ -664,7 +664,7 @@ TEST(CallHierarchy, HierarchyOnVar) {
   std::vector<CallHierarchyItem> Items =
       prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename));
   ASSERT_THAT(Items, ElementsAre(withName("var")));
-  auto IncomingLevel1 = incomingCalls(Items[0], Index.get());
+  auto IncomingLevel1 = incomingCalls(Items[0], Index.get(), AST);
   ASSERT_THAT(IncomingLevel1,
               ElementsAre(AllOf(from(withName("caller")),
                                 iFromRanges(Source.range("Callee")))));
@@ -686,14 +686,14 @@ TEST(CallHierarchy, HierarchyOnEnumConstant) {
   std::vector<CallHierarchyItem> Items =
       prepareCallHierarchy(AST, Source.point("Heads"), testPath(TU.Filename));
   ASSERT_THAT(Items, ElementsAre(withName("heads")));
-  auto IncomingLevel1 = incomingCalls(Items[0], Index.get());
+  auto IncomingLevel1 = incomingCalls(Items[0], Index.get(), AST);
   ASSERT_THAT(IncomingLevel1,
               ElementsAre(AllOf(from(withName("caller")),
                                 iFromRanges(Source.range("CallerH")))));
   Items =
       prepareCallHierarchy(AST, Source.point("Tails"), testPath(TU.Filename));
   ASSERT_THAT(Items, ElementsAre(withName("tails")));
-  IncomingLevel1 = incomingCalls(Items[0], Index.get());
+  IncomingLevel1 = incomingCalls(Items[0], Index.get(), AST);
   ASSERT_THAT(IncomingLevel1,
               ElementsAre(AllOf(from(withName("caller")),
                                 iFromRanges(Source.range("CallerT")))));
@@ -718,7 +718,7 @@ TEST(CallHierarchy, CallInDifferentFileThanCaller) {
       prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename));
   ASSERT_THAT(Items, ElementsAre(withName("callee")));
 
-  auto Incoming = incomingCalls(Items[0], Index.get());
+  auto Incoming = incomingCalls(Items[0], Index.get(), AST);
 
   // The only call site is in the source file, which is a different file from
   // the declaration of the function containing the call, which is in the

>From 89237d198b323e4dd65176c25bcbbb462a586cdd Mon Sep 17 00:00:00 2001
From: Dimitri Ratz <dimitri.ratz at thinkdigital.cc>
Date: Sun, 14 Dec 2025 19:49:49 +0100
Subject: [PATCH 06/17] Collect symbols tags from AST in the method
 outgoingCalls.

---
 clang-tools-extra/clangd/ClangdLSPServer.cpp  |  2 +-
 clang-tools-extra/clangd/ClangdServer.cpp     | 12 ++++--
 clang-tools-extra/clangd/ClangdServer.h       |  2 +-
 clang-tools-extra/clangd/XRefs.cpp            | 42 ++++++++++---------
 clang-tools-extra/clangd/XRefs.h              |  3 +-
 .../clangd/unittests/CallHierarchyTests.cpp   | 20 ++++-----
 6 files changed, 45 insertions(+), 36 deletions(-)

diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp
index 0a57573ecc469..66b4ca9880c4f 100644
--- a/clang-tools-extra/clangd/ClangdLSPServer.cpp
+++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp
@@ -1434,7 +1434,7 @@ void ClangdLSPServer::onInlayHint(const InlayHintsParams &Params,
 void ClangdLSPServer::onCallHierarchyOutgoingCalls(
     const CallHierarchyOutgoingCallsParams &Params,
     Callback<std::vector<CallHierarchyOutgoingCall>> Reply) {
-  Server->outgoingCalls(Params.item, std::move(Reply));
+  Server->outgoingCalls(Params.item.uri.file(), Params.item, std::move(Reply));
 }
 
 void ClangdLSPServer::applyConfiguration(
diff --git a/clang-tools-extra/clangd/ClangdServer.cpp b/clang-tools-extra/clangd/ClangdServer.cpp
index 21d69b99ad8d4..cb26c7a799f5e 100644
--- a/clang-tools-extra/clangd/ClangdServer.cpp
+++ b/clang-tools-extra/clangd/ClangdServer.cpp
@@ -933,12 +933,16 @@ void ClangdServer::inlayHints(PathRef File, std::optional<Range> RestrictRange,
 }
 
 void ClangdServer::outgoingCalls(
+    PathRef File,
     const CallHierarchyItem &Item,
     Callback<std::vector<CallHierarchyOutgoingCall>> CB) {
-  WorkScheduler->run("Outgoing Calls", "",
-                     [CB = std::move(CB), Item, this]() mutable {
-                       CB(clangd::outgoingCalls(Item, Index));
-                     });
+  auto Action = [Item, CB = std::move(CB), this](
+      llvm::Expected<InputsAndAST> InpAST) mutable {
+    if (!InpAST)
+      return CB(InpAST.takeError());
+    CB(clangd::outgoingCalls(Item, Index, InpAST->AST));
+  };
+  WorkScheduler->runWithAST("Outgoing Calls", File, std::move(Action));
 }
 
 void ClangdServer::onFileEvent(const DidChangeWatchedFilesParams &Params) {
diff --git a/clang-tools-extra/clangd/ClangdServer.h b/clang-tools-extra/clangd/ClangdServer.h
index ae7e25e2b6db6..c28224e8bbb6b 100644
--- a/clang-tools-extra/clangd/ClangdServer.h
+++ b/clang-tools-extra/clangd/ClangdServer.h
@@ -304,7 +304,7 @@ class ClangdServer {
                      Callback<std::vector<CallHierarchyIncomingCall>> CB);
 
   /// Resolve outgoing calls for a given call hierarchy item.
-  void outgoingCalls(const CallHierarchyItem &Item,
+  void outgoingCalls(PathRef File, const CallHierarchyItem &Item,
                      Callback<std::vector<CallHierarchyOutgoingCall>>);
 
   /// Resolve inlay hints for a given document.
diff --git a/clang-tools-extra/clangd/XRefs.cpp b/clang-tools-extra/clangd/XRefs.cpp
index 776e1a33e6c68..3539a30818e0b 100644
--- a/clang-tools-extra/clangd/XRefs.cpp
+++ b/clang-tools-extra/clangd/XRefs.cpp
@@ -2453,27 +2453,21 @@ incomingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index,
     Index->lookup(ContainerLookup, [&](const Symbol &Caller) {
       auto It = CallsIn.find(Caller.ID);
       assert(It != CallsIn.end());
+
+      std::optional<CallHierarchyItem> CHI;
       if (auto *ND = getNamedDeclFromSymbol(Caller, AST)) {
-        if (auto CHI = declToCallHierarchyItem(*ND, AST.tuPath())) {
-          std::vector<Range> FromRanges;
-          for (const Location &L : It->second) {
-            if (L.uri != CHI->uri) {
-              // Call location not in same file as caller.
-              // This can happen in some edge cases. There's not much we can do,
-              // since the protocol only allows returning ranges interpreted as
-              // being in the caller's file.
-              continue;
-            }
-            FromRanges.push_back(L.range);
-          }
-          Results.push_back(CallHierarchyIncomingCall{
-              std::move(*CHI), std::move(FromRanges), MightNeverCall});
-        }
-      } else if (auto CHI = symbolToCallHierarchyItem(Caller, Item.uri.file())) {
-        // Fallback to using symbol if NamedDecl is not available
+        CHI = declToCallHierarchyItem(*ND, AST.tuPath());
+      } else {
+        CHI = symbolToCallHierarchyItem(Caller, Item.uri.file());
+      }
+      if (CHI) {
         std::vector<Range> FromRanges;
         for (const Location &L : It->second) {
           if (L.uri != CHI->uri) {
+            // Call location not in same file as caller.
+            // This can happen in some edge cases. There's not much we can do,
+            // since the protocol only allows returning ranges interpreted as
+            // being in the caller's file.
             continue;
           }
           FromRanges.push_back(L.range);
@@ -2503,7 +2497,8 @@ incomingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index,
 }
 
 std::vector<CallHierarchyOutgoingCall>
-outgoingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index) {
+outgoingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index,
+              const ParsedAST &AST) {
   std::vector<CallHierarchyOutgoingCall> Results;
   if (!Index || Item.data.empty())
     return Results;
@@ -2549,7 +2544,16 @@ outgoingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index) {
 
     auto It = CallsOut.find(Callee.ID);
     assert(It != CallsOut.end());
-    if (auto CHI = symbolToCallHierarchyItem(Callee, Item.uri.file())) {
+
+    std::optional<CallHierarchyItem> CHI;
+
+    if (auto *ND = getNamedDeclFromSymbol(Callee, AST)) {
+      CHI = declToCallHierarchyItem(*ND, AST.tuPath());
+    } else {
+      CHI = symbolToCallHierarchyItem(Callee, Item.uri.file());
+    }
+
+    if (CHI) {
       std::vector<Range> FromRanges;
       for (const Location &L : It->second) {
         if (L.uri != Item.uri) {
diff --git a/clang-tools-extra/clangd/XRefs.h b/clang-tools-extra/clangd/XRefs.h
index 1019fa189a613..6319729ba39e4 100644
--- a/clang-tools-extra/clangd/XRefs.h
+++ b/clang-tools-extra/clangd/XRefs.h
@@ -152,7 +152,8 @@ incomingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index,
               const ParsedAST &AST);
 
 std::vector<CallHierarchyOutgoingCall>
-outgoingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index);
+outgoingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index,
+              const ParsedAST &AST);
 
 /// Returns all decls that are referenced in the \p FD except local symbols.
 llvm::DenseSet<const Decl *> getNonLocalDeclRefs(ParsedAST &AST,
diff --git a/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp b/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp
index f5e9983aa70ec..2891b420427d2 100644
--- a/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp
+++ b/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp
@@ -296,29 +296,29 @@ TEST(CallHierarchy, OutgoingOneFile) {
   std::vector<CallHierarchyItem> Items =
       prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename));
   ASSERT_THAT(Items, ElementsAre(withName("caller3")));
-  auto OugoingLevel1 = outgoingCalls(Items[0], Index.get());
+  auto OugoingLevel1 = outgoingCalls(Items[0], Index.get(), AST);
   ASSERT_THAT(
       OugoingLevel1,
       ElementsAre(
-          AllOf(to(AllOf(withName("caller1"), withDetail("ns::Foo::caller1"))),
+          AllOf(to(AllOf(withName("Foo::caller1"), withDetail("ns::Foo::caller1"))),
                 oFromRanges(Source.range("Caller1C"))),
           AllOf(to(AllOf(withName("caller2"), withDetail("caller2"))),
                 oFromRanges(Source.range("Caller2")))));
 
-  auto OutgoingLevel2 = outgoingCalls(OugoingLevel1[1].to, Index.get());
+  auto OutgoingLevel2 = outgoingCalls(OugoingLevel1[1].to, Index.get(), AST);
   ASSERT_THAT(
       OutgoingLevel2,
       ElementsAre(AllOf(
-          to(AllOf(withName("caller1"), withDetail("ns::Foo::caller1"))),
+          to(AllOf(withName("Foo::caller1"), withDetail("ns::Foo::caller1"))),
           oFromRanges(Source.range("Caller1A"), Source.range("Caller1B")))));
 
-  auto OutgoingLevel3 = outgoingCalls(OutgoingLevel2[0].to, Index.get());
+  auto OutgoingLevel3 = outgoingCalls(OutgoingLevel2[0].to, Index.get(), AST);
   ASSERT_THAT(
       OutgoingLevel3,
       ElementsAre(AllOf(to(AllOf(withName("callee"), withDetail("callee"))),
                         oFromRanges(Source.range("Callee")))));
 
-  auto OutgoingLevel4 = outgoingCalls(OutgoingLevel3[0].to, Index.get());
+  auto OutgoingLevel4 = outgoingCalls(OutgoingLevel3[0].to, Index.get(), AST);
   EXPECT_THAT(OutgoingLevel4, IsEmpty());
 }
 
@@ -430,7 +430,7 @@ TEST(CallHierarchy, MultiFileCpp) {
         ElementsAre(AllOf(
             withName("caller3"),
             withFile(testPath(IsDeclaration ? "caller3.hh" : "caller3.cc")))));
-    auto OutgoingLevel1 = outgoingCalls(Items[0], Index.get());
+    auto OutgoingLevel1 = outgoingCalls(Items[0], Index.get(), AST);
     ASSERT_THAT(
         OutgoingLevel1,
         // fromRanges are interpreted in the context of Items[0]'s file.
@@ -444,19 +444,19 @@ TEST(CallHierarchy, MultiFileCpp) {
                   IsDeclaration ? oFromRanges()
                                 : oFromRanges(Caller3C.range("Caller2")))));
 
-    auto OutgoingLevel2 = outgoingCalls(OutgoingLevel1[1].to, Index.get());
+    auto OutgoingLevel2 = outgoingCalls(OutgoingLevel1[1].to, Index.get(), AST);
     ASSERT_THAT(OutgoingLevel2,
                 ElementsAre(AllOf(
                     to(AllOf(withName("caller1"), withDetail("nsa::caller1"))),
                     oFromRanges(Caller2C.range("A"), Caller2C.range("B")))));
 
-    auto OutgoingLevel3 = outgoingCalls(OutgoingLevel2[0].to, Index.get());
+    auto OutgoingLevel3 = outgoingCalls(OutgoingLevel2[0].to, Index.get(), AST);
     ASSERT_THAT(
         OutgoingLevel3,
         ElementsAre(AllOf(to(AllOf(withName("callee"), withDetail("callee"))),
                           oFromRanges(Caller1C.range()))));
 
-    auto OutgoingLevel4 = outgoingCalls(OutgoingLevel3[0].to, Index.get());
+    auto OutgoingLevel4 = outgoingCalls(OutgoingLevel3[0].to, Index.get(), AST);
     EXPECT_THAT(OutgoingLevel4, IsEmpty());
   };
 

>From 9eb72dc9f49566bf7fda0c21ea990d3ff39c2ea6 Mon Sep 17 00:00:00 2001
From: Dimitri Ratz <dimitri.ratz at thinkdigital.cc>
Date: Mon, 15 Dec 2025 11:02:01 +0100
Subject: [PATCH 07/17] Collect symbols tags from AST in the method subtypes.

---
 clang-tools-extra/clangd/ClangdLSPServer.cpp  |  4 +-
 clang-tools-extra/clangd/ClangdServer.cpp     | 36 +++++---
 clang-tools-extra/clangd/ClangdServer.h       |  4 +-
 clang-tools-extra/clangd/XRefs.cpp            | 89 +++++++++++--------
 clang-tools-extra/clangd/XRefs.h              |  5 +-
 .../clangd/test/type-hierarchy-ext.test       |  8 +-
 .../clangd/test/type-hierarchy.test           |  4 +-
 .../clangd/unittests/TypeHierarchyTests.cpp   |  4 +-
 8 files changed, 91 insertions(+), 63 deletions(-)

diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp
index 66b4ca9880c4f..39b3d58397fbc 100644
--- a/clang-tools-extra/clangd/ClangdLSPServer.cpp
+++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp
@@ -1356,7 +1356,7 @@ void ClangdLSPServer::onResolveTypeHierarchy(
         }
         Reply(serializeTHIForExtension(std::move(**Resp)));
       };
-  Server->resolveTypeHierarchy(Params.item, Params.resolve, Params.direction,
+  Server->resolveTypeHierarchy(Params.item.uri.file(), Params.item, Params.resolve, Params.direction,
                                std::move(Serialize));
 }
 
@@ -1376,7 +1376,7 @@ void ClangdLSPServer::onSuperTypes(
 void ClangdLSPServer::onSubTypes(
     const ResolveTypeHierarchyItemParams &Params,
     Callback<std::vector<TypeHierarchyItem>> Reply) {
-  Server->subTypes(Params.item, std::move(Reply));
+  Server->subTypes(Params.item.uri.file(), Params.item, std::move(Reply));
 }
 
 void ClangdLSPServer::onPrepareCallHierarchy(
diff --git a/clang-tools-extra/clangd/ClangdServer.cpp b/clang-tools-extra/clangd/ClangdServer.cpp
index cb26c7a799f5e..c210eda6bfd2e 100644
--- a/clang-tools-extra/clangd/ClangdServer.cpp
+++ b/clang-tools-extra/clangd/ClangdServer.cpp
@@ -880,21 +880,37 @@ void ClangdServer::superTypes(
                      });
 }
 
-void ClangdServer::subTypes(const TypeHierarchyItem &Item,
+void ClangdServer::subTypes(PathRef File, const TypeHierarchyItem &Item,
                             Callback<std::vector<TypeHierarchyItem>> CB) {
-  WorkScheduler->run(
-      "typeHierarchy/subTypes", /*Path=*/"",
-      [=, CB = std::move(CB)]() mutable { CB(clangd::subTypes(Item, Index)); });
+
+  auto Action = [File = File.str(), Item, CB = std::move(CB), this](
+      llvm::Expected<InputsAndAST> InpAST) mutable {
+        if (!InpAST)
+            return CB(InpAST.takeError());
+        CB(clangd::subTypes(Item, Index, InpAST->AST));
+      };
+   WorkScheduler->runWithAST("subTypes Calls", File, std::move(Action));
+  // WorkScheduler->run(
+  //     "typeHierarchy/subTypes", /*Path=*/"",
+  //     [=, CB = std::move(CB)]() mutable { CB(clangd::subTypes(Item, Index)); });
 }
 
-void ClangdServer::resolveTypeHierarchy(
+void ClangdServer::resolveTypeHierarchy(PathRef File,
     TypeHierarchyItem Item, int Resolve, TypeHierarchyDirection Direction,
     Callback<std::optional<TypeHierarchyItem>> CB) {
-  WorkScheduler->run(
-      "Resolve Type Hierarchy", "", [=, CB = std::move(CB)]() mutable {
-        clangd::resolveTypeHierarchy(Item, Resolve, Direction, Index);
-        CB(Item);
-      });
+  auto Action = [=, CB = std::move(CB), this](
+      llvm::Expected<InputsAndAST> InpAST) mutable {
+    if (!InpAST)
+      return CB(InpAST.takeError());
+    clangd::resolveTypeHierarchy(Item, Resolve, Direction, Index, InpAST->AST);
+    CB(Item);
+  };
+  WorkScheduler->runWithAST("resolveTypeHierarchy Calls", File, std::move(Action));
+  // WorkScheduler->run(
+  //     "Resolve Type Hierarchy", "", [=, CB = std::move(CB)]() mutable {
+  //       clangd::resolveTypeHierarchy(Item, Resolve, Direction, Index);
+  //       CB(Item);
+  //     });
 }
 
 void ClangdServer::prepareCallHierarchy(
diff --git a/clang-tools-extra/clangd/ClangdServer.h b/clang-tools-extra/clangd/ClangdServer.h
index c28224e8bbb6b..431856aa1fadb 100644
--- a/clang-tools-extra/clangd/ClangdServer.h
+++ b/clang-tools-extra/clangd/ClangdServer.h
@@ -286,11 +286,11 @@ class ClangdServer {
   void superTypes(const TypeHierarchyItem &Item,
                   Callback<std::optional<std::vector<TypeHierarchyItem>>> CB);
   /// Get direct children of a type hierarchy item.
-  void subTypes(const TypeHierarchyItem &Item,
+  void subTypes(PathRef File, const TypeHierarchyItem &Item,
                 Callback<std::vector<TypeHierarchyItem>> CB);
 
   /// Resolve type hierarchy item in the given direction.
-  void resolveTypeHierarchy(TypeHierarchyItem Item, int Resolve,
+  void resolveTypeHierarchy(PathRef File, TypeHierarchyItem Item, int Resolve,
                             TypeHierarchyDirection Direction,
                             Callback<std::optional<TypeHierarchyItem>> CB);
 
diff --git a/clang-tools-extra/clangd/XRefs.cpp b/clang-tools-extra/clangd/XRefs.cpp
index 3539a30818e0b..ff9a15c3372aa 100644
--- a/clang-tools-extra/clangd/XRefs.cpp
+++ b/clang-tools-extra/clangd/XRefs.cpp
@@ -1901,18 +1901,58 @@ symbolToCallHierarchyItem(const Symbol &S, PathRef TUPath) {
   return Result;
 }
 
+// Tries to find a NamedDecl in the AST that matches the given Symbol.
+// Returns nullptr if the symbol is not found in the current AST.
+const NamedDecl *getNamedDeclFromSymbol(const Symbol &Sym,
+                                        const ParsedAST &AST) {
+  // Try to convert the symbol to a location and find the decl at that location
+  auto SymLoc = symbolToLocation(Sym, AST.tuPath());
+  if (!SymLoc)
+    return nullptr;
+
+  // Check if the symbol location is in the main file
+  if (SymLoc->uri.file() != AST.tuPath())
+    return nullptr;
+
+  // Convert LSP position to source location
+  const auto &SM = AST.getSourceManager();
+  auto CurLoc = sourceLocationInMainFile(SM, SymLoc->range.start);
+  if (!CurLoc) {
+    llvm::consumeError(CurLoc.takeError());
+    return nullptr;
+  }
+
+  // Get all decls at this location
+  auto Decls = getDeclAtPosition(const_cast<ParsedAST &>(AST), *CurLoc, {});
+  if (Decls.empty())
+    return nullptr;
+
+  // Return the first decl (usually the most specific one)
+  return Decls[0];
+}
+
 static void fillSubTypes(const SymbolID &ID,
                          std::vector<TypeHierarchyItem> &SubTypes,
-                         const SymbolIndex *Index, int Levels, PathRef TUPath) {
+                         const SymbolIndex *Index, int Levels, PathRef TUPath,
+                         const ParsedAST &AST) {
   RelationsRequest Req;
   Req.Subjects.insert(ID);
   Req.Predicate = RelationKind::BaseOf;
   Index->relations(Req, [&](const SymbolID &Subject, const Symbol &Object) {
-    if (std::optional<TypeHierarchyItem> ChildSym =
-            symbolToTypeHierarchyItem(Object, TUPath)) {
+    std::optional<TypeHierarchyItem> ChildSym;
+
+    if (auto *ND = getNamedDeclFromSymbol(Object, AST)) {
+      ChildSym = declToTypeHierarchyItem(*ND, AST.tuPath());
+      elog("fillSubTypes: declToTypeHierarchyItem, {0}", ChildSym.has_value());
+    } else {
+      ChildSym = symbolToTypeHierarchyItem(Object, TUPath);
+      elog("fillSubTypes: symbolToTypeHierarchyItem, {0}", ChildSym.has_value());
+    }
+    if (ChildSym) {
       if (Levels > 1) {
         ChildSym->children.emplace();
-        fillSubTypes(Object.ID, *ChildSym->children, Index, Levels - 1, TUPath);
+        fillSubTypes(Object.ID, *ChildSym->children, Index, Levels - 1, TUPath,
+                     AST);
       }
       SubTypes.emplace_back(std::move(*ChildSym));
     }
@@ -2295,7 +2335,7 @@ getTypeHierarchy(ParsedAST &AST, Position Pos, int ResolveLevels,
 
       if (Index) {
         if (auto ID = getSymbolID(CXXRD))
-          fillSubTypes(ID, *Result->children, Index, ResolveLevels, TUPath);
+          fillSubTypes(ID, *Result->children, Index, ResolveLevels, TUPath, AST);
       }
     }
     Results.emplace_back(std::move(*Result));
@@ -2327,9 +2367,9 @@ superTypes(const TypeHierarchyItem &Item, const SymbolIndex *Index) {
 }
 
 std::vector<TypeHierarchyItem> subTypes(const TypeHierarchyItem &Item,
-                                        const SymbolIndex *Index) {
+                                        const SymbolIndex *Index, const ParsedAST &AST) {
   std::vector<TypeHierarchyItem> Results;
-  fillSubTypes(Item.data.symbolID, Results, Index, 1, Item.uri.file());
+  fillSubTypes(Item.data.symbolID, Results, Index, 1, Item.uri.file(), AST);
   for (auto &ChildSym : Results)
     ChildSym.data.parents = {Item.data};
   return Results;
@@ -2337,7 +2377,8 @@ std::vector<TypeHierarchyItem> subTypes(const TypeHierarchyItem &Item,
 
 void resolveTypeHierarchy(TypeHierarchyItem &Item, int ResolveLevels,
                           TypeHierarchyDirection Direction,
-                          const SymbolIndex *Index) {
+                          const SymbolIndex *Index,
+                          const ParsedAST &AST) {
   // We only support typeHierarchy/resolve for children, because for parents
   // we ignore ResolveLevels and return all levels of parents eagerly.
   if (!Index || Direction == TypeHierarchyDirection::Parents ||
@@ -2346,37 +2387,7 @@ void resolveTypeHierarchy(TypeHierarchyItem &Item, int ResolveLevels,
 
   Item.children.emplace();
   fillSubTypes(Item.data.symbolID, *Item.children, Index, ResolveLevels,
-               Item.uri.file());
-}
-
-// Tries to find a NamedDecl in the AST that matches the given Symbol.
-// Returns nullptr if the symbol is not found in the current AST.
-const NamedDecl *getNamedDeclFromSymbol(const Symbol &Sym,
-                                        const ParsedAST &AST) {
-  // Try to convert the symbol to a location and find the decl at that location
-  auto SymLoc = symbolToLocation(Sym, AST.tuPath());
-  if (!SymLoc)
-    return nullptr;
-
-  // Check if the symbol location is in the main file
-  if (SymLoc->uri.file() != AST.tuPath())
-    return nullptr;
-
-  // Convert LSP position to source location
-  const auto &SM = AST.getSourceManager();
-  auto CurLoc = sourceLocationInMainFile(SM, SymLoc->range.start);
-  if (!CurLoc) {
-    llvm::consumeError(CurLoc.takeError());
-    return nullptr;
-  }
-
-  // Get all decls at this location
-  auto Decls = getDeclAtPosition(const_cast<ParsedAST &>(AST), *CurLoc, {});
-  if (Decls.empty())
-    return nullptr;
-
-  // Return the first decl (usually the most specific one)
-  return Decls[0];
+               Item.uri.file(), AST);
 }
 
 std::vector<CallHierarchyItem>
diff --git a/clang-tools-extra/clangd/XRefs.h b/clang-tools-extra/clangd/XRefs.h
index 6319729ba39e4..1582c481ea5c2 100644
--- a/clang-tools-extra/clangd/XRefs.h
+++ b/clang-tools-extra/clangd/XRefs.h
@@ -137,11 +137,12 @@ std::optional<std::vector<TypeHierarchyItem>>
 superTypes(const TypeHierarchyItem &Item, const SymbolIndex *Index);
 /// Returns direct children of a TypeHierarchyItem.
 std::vector<TypeHierarchyItem> subTypes(const TypeHierarchyItem &Item,
-                                        const SymbolIndex *Index);
+                                        const SymbolIndex *Index,
+                                        const ParsedAST &AST);
 
 void resolveTypeHierarchy(TypeHierarchyItem &Item, int ResolveLevels,
                           TypeHierarchyDirection Direction,
-                          const SymbolIndex *Index);
+                          const SymbolIndex *Index, const ParsedAST &AST);
 
 /// Get call hierarchy information at \p Pos.
 std::vector<CallHierarchyItem>
diff --git a/clang-tools-extra/clangd/test/type-hierarchy-ext.test b/clang-tools-extra/clangd/test/type-hierarchy-ext.test
index 983c7538088bf..d635b199d002c 100644
--- a/clang-tools-extra/clangd/test/type-hierarchy-ext.test
+++ b/clang-tools-extra/clangd/test/type-hierarchy-ext.test
@@ -17,11 +17,11 @@
 # CHECK-NEXT:        "name": "Child3",
 # CHECK-NEXT:        "range": {
 # CHECK-NEXT:          "end": {
-# CHECK-NEXT:            "character": 13,
+# CHECK-NEXT:            "character": 25,
 # CHECK-NEXT:            "line": 3
 # CHECK-NEXT:          },
 # CHECK-NEXT:          "start": {
-# CHECK-NEXT:            "character": 7,
+# CHECK-NEXT:            "character": 0,
 # CHECK-NEXT:            "line": 3
 # CHECK-NEXT:          }
 # CHECK-NEXT:        },
@@ -162,11 +162,11 @@
 # CHECK-NEXT:        "name": "Child4",
 # CHECK-NEXT:        "range": {
 # CHECK-NEXT:          "end": {
-# CHECK-NEXT:            "character": 13,
+# CHECK-NEXT:            "character": 25,
 # CHECK-NEXT:            "line": 4
 # CHECK-NEXT:          },
 # CHECK-NEXT:          "start": {
-# CHECK-NEXT:            "character": 7,
+# CHECK-NEXT:            "character": 0,
 # CHECK-NEXT:            "line": 4
 # CHECK-NEXT:          }
 # CHECK-NEXT:        },
diff --git a/clang-tools-extra/clangd/test/type-hierarchy.test b/clang-tools-extra/clangd/test/type-hierarchy.test
index d1dda4b92c29c..065f129e05d2f 100644
--- a/clang-tools-extra/clangd/test/type-hierarchy.test
+++ b/clang-tools-extra/clangd/test/type-hierarchy.test
@@ -125,11 +125,11 @@
 # CHECK-NEXT:       "name": "Child3",
 # CHECK-NEXT:       "range": {
 # CHECK-NEXT:         "end": {
-# CHECK-NEXT:           "character": 13,
+# CHECK-NEXT:           "character": 25,
 # CHECK-NEXT:           "line": 3
 # CHECK-NEXT:         },
 # CHECK-NEXT:         "start": {
-# CHECK-NEXT:           "character": 7,
+# CHECK-NEXT:           "character": 0,
 # CHECK-NEXT:           "line": 3
 # CHECK-NEXT:         }
 # CHECK-NEXT:       },
diff --git a/clang-tools-extra/clangd/unittests/TypeHierarchyTests.cpp b/clang-tools-extra/clangd/unittests/TypeHierarchyTests.cpp
index 406a842f5a008..ae2f806d0b17e 100644
--- a/clang-tools-extra/clangd/unittests/TypeHierarchyTests.cpp
+++ b/clang-tools-extra/clangd/unittests/TypeHierarchyTests.cpp
@@ -756,7 +756,7 @@ struct Child2b : Child1 {};
                            parentsNotResolved(), childrenNotResolved()))));
 
   resolveTypeHierarchy((*Result.front().children)[0], /*ResolveLevels=*/1,
-                       TypeHierarchyDirection::Children, Index.get());
+                       TypeHierarchyDirection::Children, Index.get(), AST);
 
   EXPECT_THAT(
       (*Result.front().children)[0],
@@ -783,7 +783,7 @@ struct Child : Parent1, Parent2 {};
                                  TypeHierarchyDirection::Children, Index.get(),
                                  testPath(TU.Filename));
   ASSERT_THAT(Result, SizeIs(1));
-  auto Children = subTypes(Result.front(), Index.get());
+  auto Children = subTypes(Result.front(), Index.get(), AST);
 
   // Make sure parents are populated when getting children.
   // FIXME: This is partial.

>From 0b1c3f6635a14ae224a6446c35a6d7159cb47dfa Mon Sep 17 00:00:00 2001
From: Dimitri Ratz <dimitri.ratz at thinkdigital.cc>
Date: Mon, 15 Dec 2025 16:25:26 +0100
Subject: [PATCH 08/17] Collect symbols tags from AST in the method supertypes.

---
 clang-tools-extra/clangd/ClangdLSPServer.cpp  | 61 ++++++++---------
 clang-tools-extra/clangd/ClangdLSPServer.h    | 11 ++--
 clang-tools-extra/clangd/ClangdServer.cpp     | 65 +++++++++----------
 clang-tools-extra/clangd/ClangdServer.h       |  5 +-
 clang-tools-extra/clangd/Protocol.h           |  3 +
 clang-tools-extra/clangd/XRefs.cpp            | 28 +++++---
 clang-tools-extra/clangd/XRefs.h              |  6 +-
 .../clangd/test/call-hierarchy.test           |  3 +
 .../clangd/test/type-hierarchy.test           |  7 +-
 .../clangd/unittests/TypeHierarchyTests.cpp   |  2 +-
 10 files changed, 99 insertions(+), 92 deletions(-)

diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp
index 39b3d58397fbc..d8a1a089beb8c 100644
--- a/clang-tools-extra/clangd/ClangdLSPServer.cpp
+++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp
@@ -136,10 +136,9 @@ CodeAction toCodeAction(const Fix &F, const URIForFile &File,
     Edit.textDocument = VersionedTextDocumentIdentifier{{File}, Version};
     for (const auto &E : F.Edits)
       Edit.edits.push_back(
-          {E.range, E.newText,
-           SupportChangeAnnotation ? E.annotationId : ""});
+          {E.range, E.newText, SupportChangeAnnotation ? E.annotationId : ""});
     if (SupportChangeAnnotation) {
-      for (const auto &[AID, Annotation]: F.Annotations)
+      for (const auto &[AID, Annotation] : F.Annotations)
         Action.edit->changeAnnotations[AID] = Annotation;
     }
   }
@@ -909,24 +908,24 @@ void ClangdLSPServer::onRename(const RenameParams &Params,
   if (!Server->getDraft(File))
     return Reply(llvm::make_error<LSPError>(
         "onRename called for non-added file", ErrorCode::InvalidParams));
-  Server->rename(File, Params.position, Params.newName, Opts.Rename,
-                 [File, Params, Reply = std::move(Reply),
-                  this](llvm::Expected<RenameResult> R) mutable {
-                   if (!R)
-                     return Reply(R.takeError());
-                   if (auto Err = validateEdits(*Server, R->GlobalChanges))
-                     return Reply(std::move(Err));
-                   WorkspaceEdit Result;
-                   // FIXME: use documentChanges if SupportDocumentChanges is
-                   // true.
-                   Result.changes.emplace();
-                   for (const auto &Rep : R->GlobalChanges) {
-                     (*Result
-                           .changes)[URI::createFile(Rep.first()).toString()] =
-                         Rep.second.asTextEdits();
-                   }
-                   Reply(Result);
-                 });
+  Server->rename(
+      File, Params.position, Params.newName, Opts.Rename,
+      [File, Params, Reply = std::move(Reply),
+       this](llvm::Expected<RenameResult> R) mutable {
+        if (!R)
+          return Reply(R.takeError());
+        if (auto Err = validateEdits(*Server, R->GlobalChanges))
+          return Reply(std::move(Err));
+        WorkspaceEdit Result;
+        // FIXME: use documentChanges if SupportDocumentChanges is
+        // true.
+        Result.changes.emplace();
+        for (const auto &Rep : R->GlobalChanges) {
+          (*Result.changes)[URI::createFile(Rep.first()).toString()] =
+              Rep.second.asTextEdits();
+        }
+        Reply(Result);
+      });
 }
 
 void ClangdLSPServer::onDocumentDidClose(
@@ -1070,7 +1069,7 @@ void ClangdLSPServer::onCodeAction(const CodeActionParams &Params,
   std::map<ClangdServer::DiagRef, clangd::Diagnostic> ToLSPDiags;
   ClangdServer::CodeActionInputs Inputs;
 
-  for (const auto& LSPDiag : Params.context.diagnostics) {
+  for (const auto &LSPDiag : Params.context.diagnostics) {
     if (auto DiagRef = getDiagRef(File.file(), LSPDiag)) {
       ToLSPDiags[*DiagRef] = LSPDiag;
       Inputs.Diagnostics.push_back(*DiagRef);
@@ -1079,13 +1078,9 @@ void ClangdLSPServer::onCodeAction(const CodeActionParams &Params,
   Inputs.File = File.file();
   Inputs.Selection = Params.range;
   Inputs.RequestedActionKinds = Params.context.only;
-  Inputs.TweakFilter = [this](const Tweak &T) {
-    return Opts.TweakFilter(T);
-  };
-  auto CB = [this,
-             Reply = std::move(Reply),
-             ToLSPDiags = std::move(ToLSPDiags), File,
-             Selection = Params.range](
+  Inputs.TweakFilter = [this](const Tweak &T) { return Opts.TweakFilter(T); };
+  auto CB = [this, Reply = std::move(Reply), ToLSPDiags = std::move(ToLSPDiags),
+             File, Selection = Params.range](
                 llvm::Expected<ClangdServer::CodeActionResult> Fixits) mutable {
     if (!Fixits)
       return Reply(Fixits.takeError());
@@ -1094,8 +1089,7 @@ void ClangdLSPServer::onCodeAction(const CodeActionParams &Params,
     for (const auto &QF : Fixits->QuickFixes) {
       CAs.push_back(toCodeAction(QF.F, File, Version, SupportsDocumentChanges,
                                  SupportsChangeAnnotation));
-      if (auto It = ToLSPDiags.find(QF.Diag);
-          It != ToLSPDiags.end()) {
+      if (auto It = ToLSPDiags.find(QF.Diag); It != ToLSPDiags.end()) {
         CAs.back().diagnostics = {It->second};
       }
     }
@@ -1356,7 +1350,8 @@ void ClangdLSPServer::onResolveTypeHierarchy(
         }
         Reply(serializeTHIForExtension(std::move(**Resp)));
       };
-  Server->resolveTypeHierarchy(Params.item.uri.file(), Params.item, Params.resolve, Params.direction,
+  Server->resolveTypeHierarchy(Params.item.uri.file(), Params.item,
+                               Params.resolve, Params.direction,
                                std::move(Serialize));
 }
 
@@ -1370,7 +1365,7 @@ void ClangdLSPServer::onPrepareTypeHierarchy(
 void ClangdLSPServer::onSuperTypes(
     const ResolveTypeHierarchyItemParams &Params,
     Callback<std::optional<std::vector<TypeHierarchyItem>>> Reply) {
-  Server->superTypes(Params.item, std::move(Reply));
+  Server->superTypes(Params.item.uri.file(), Params.item, std::move(Reply));
 }
 
 void ClangdLSPServer::onSubTypes(
diff --git a/clang-tools-extra/clangd/ClangdLSPServer.h b/clang-tools-extra/clangd/ClangdLSPServer.h
index 6ada3fd9e6e47..64dcbfbc55325 100644
--- a/clang-tools-extra/clangd/ClangdLSPServer.h
+++ b/clang-tools-extra/clangd/ClangdLSPServer.h
@@ -133,7 +133,8 @@ class ClangdLSPServer : private ClangdServer::Callbacks,
                   Callback<std::vector<Location>>);
   void onGoToImplementation(const TextDocumentPositionParams &,
                             Callback<std::vector<Location>>);
-  void onReference(const ReferenceParams &, Callback<std::vector<ReferenceLocation>>);
+  void onReference(const ReferenceParams &,
+                   Callback<std::vector<ReferenceLocation>>);
   void onSwitchSourceHeader(const TextDocumentIdentifier &,
                             Callback<std::optional<URIForFile>>);
   void onDocumentHighlight(const TextDocumentPositionParams &,
@@ -243,7 +244,7 @@ class ClangdLSPServer : private ClangdServer::Callbacks,
   /// Used to indicate the ClangdLSPServer is being destroyed.
   std::atomic<bool> IsBeingDestroyed = {false};
 
-  // FIXME: The caching is a temporary solution to get corresponding clangd 
+  // FIXME: The caching is a temporary solution to get corresponding clangd
   // diagnostic from a LSP diagnostic.
   // Ideally, ClangdServer can generate an identifier for each diagnostic,
   // emit them via the LSP's data field (which was newly added in LSP 3.16).
@@ -259,11 +260,9 @@ class ClangdLSPServer : private ClangdServer::Callbacks,
     return {LSPDiag.range, LSPDiag.message};
   }
   /// A map from LSP diagnostic to clangd-naive diagnostic.
-  typedef std::map<DiagKey, ClangdServer::DiagRef>
-      DiagnosticToDiagRefMap;
+  typedef std::map<DiagKey, ClangdServer::DiagRef> DiagnosticToDiagRefMap;
   /// Caches the mapping LSP and clangd-naive diagnostics per file.
-  llvm::StringMap<DiagnosticToDiagRefMap>
-      DiagRefMap;
+  llvm::StringMap<DiagnosticToDiagRefMap> DiagRefMap;
 
   // Last semantic-tokens response, for incremental requests.
   std::mutex SemanticTokensMutex;
diff --git a/clang-tools-extra/clangd/ClangdServer.cpp b/clang-tools-extra/clangd/ClangdServer.cpp
index c210eda6bfd2e..5b6525d8f842e 100644
--- a/clang-tools-extra/clangd/ClangdServer.cpp
+++ b/clang-tools-extra/clangd/ClangdServer.cpp
@@ -62,8 +62,8 @@ namespace clangd {
 namespace {
 
 // Tracks number of times a tweak has been offered.
-static constexpr trace::Metric TweakAvailable(
-    "tweak_available", trace::Metric::Counter, "tweak_id");
+static constexpr trace::Metric
+    TweakAvailable("tweak_available", trace::Metric::Counter, "tweak_id");
 
 // Update the FileIndex with new ASTs and plumb the diagnostics responses.
 struct UpdateIndexCallbacks : public ParsingCallbacks {
@@ -872,45 +872,42 @@ void ClangdServer::typeHierarchy(PathRef File, Position Pos, int Resolve,
 }
 
 void ClangdServer::superTypes(
-    const TypeHierarchyItem &Item,
+    PathRef File, const TypeHierarchyItem &Item,
     Callback<std::optional<std::vector<TypeHierarchyItem>>> CB) {
-  WorkScheduler->run("typeHierarchy/superTypes", /*Path=*/"",
-                     [=, CB = std::move(CB)]() mutable {
-                       CB(clangd::superTypes(Item, Index));
-                     });
+  auto Action = [File = File.str(), Item, CB = std::move(CB),
+                 this](llvm::Expected<InputsAndAST> InpAST) mutable {
+    if (!InpAST)
+      return CB(InpAST.takeError());
+    CB(clangd::superTypes(Item, Index, InpAST->AST));
+  };
+  WorkScheduler->runWithAST("superTypes Calls", File, std::move(Action));
 }
 
 void ClangdServer::subTypes(PathRef File, const TypeHierarchyItem &Item,
                             Callback<std::vector<TypeHierarchyItem>> CB) {
 
-  auto Action = [File = File.str(), Item, CB = std::move(CB), this](
-      llvm::Expected<InputsAndAST> InpAST) mutable {
-        if (!InpAST)
-            return CB(InpAST.takeError());
-        CB(clangd::subTypes(Item, Index, InpAST->AST));
-      };
-   WorkScheduler->runWithAST("subTypes Calls", File, std::move(Action));
-  // WorkScheduler->run(
-  //     "typeHierarchy/subTypes", /*Path=*/"",
-  //     [=, CB = std::move(CB)]() mutable { CB(clangd::subTypes(Item, Index)); });
+  auto Action = [File = File.str(), Item, CB = std::move(CB),
+                 this](llvm::Expected<InputsAndAST> InpAST) mutable {
+    if (!InpAST)
+      return CB(InpAST.takeError());
+    CB(clangd::subTypes(Item, Index, InpAST->AST));
+  };
+  WorkScheduler->runWithAST("subTypes Calls", File, std::move(Action));
 }
 
-void ClangdServer::resolveTypeHierarchy(PathRef File,
-    TypeHierarchyItem Item, int Resolve, TypeHierarchyDirection Direction,
+void ClangdServer::resolveTypeHierarchy(
+    PathRef File, TypeHierarchyItem Item, int Resolve,
+    TypeHierarchyDirection Direction,
     Callback<std::optional<TypeHierarchyItem>> CB) {
-  auto Action = [=, CB = std::move(CB), this](
-      llvm::Expected<InputsAndAST> InpAST) mutable {
+  auto Action = [=, CB = std::move(CB),
+                 this](llvm::Expected<InputsAndAST> InpAST) mutable {
     if (!InpAST)
       return CB(InpAST.takeError());
     clangd::resolveTypeHierarchy(Item, Resolve, Direction, Index, InpAST->AST);
     CB(Item);
   };
-  WorkScheduler->runWithAST("resolveTypeHierarchy Calls", File, std::move(Action));
-  // WorkScheduler->run(
-  //     "Resolve Type Hierarchy", "", [=, CB = std::move(CB)]() mutable {
-  //       clangd::resolveTypeHierarchy(Item, Resolve, Direction, Index);
-  //       CB(Item);
-  //     });
+  WorkScheduler->runWithAST("resolveTypeHierarchy Calls", File,
+                            std::move(Action));
 }
 
 void ClangdServer::prepareCallHierarchy(
@@ -925,11 +922,10 @@ void ClangdServer::prepareCallHierarchy(
 }
 
 void ClangdServer::incomingCalls(
-    PathRef File,
-    const CallHierarchyItem &Item,
+    PathRef File, const CallHierarchyItem &Item,
     Callback<std::vector<CallHierarchyIncomingCall>> CB) {
-  auto Action = [Item, CB = std::move(CB), this](
-      llvm::Expected<InputsAndAST> InpAST) mutable {
+  auto Action = [Item, CB = std::move(CB),
+                 this](llvm::Expected<InputsAndAST> InpAST) mutable {
     if (!InpAST)
       return CB(InpAST.takeError());
     CB(clangd::incomingCalls(Item, Index, InpAST->AST));
@@ -949,11 +945,10 @@ void ClangdServer::inlayHints(PathRef File, std::optional<Range> RestrictRange,
 }
 
 void ClangdServer::outgoingCalls(
-    PathRef File,
-    const CallHierarchyItem &Item,
+    PathRef File, const CallHierarchyItem &Item,
     Callback<std::vector<CallHierarchyOutgoingCall>> CB) {
-  auto Action = [Item, CB = std::move(CB), this](
-      llvm::Expected<InputsAndAST> InpAST) mutable {
+  auto Action = [Item, CB = std::move(CB),
+                 this](llvm::Expected<InputsAndAST> InpAST) mutable {
     if (!InpAST)
       return CB(InpAST.takeError());
     CB(clangd::outgoingCalls(Item, Index, InpAST->AST));
diff --git a/clang-tools-extra/clangd/ClangdServer.h b/clang-tools-extra/clangd/ClangdServer.h
index 431856aa1fadb..c582a96691deb 100644
--- a/clang-tools-extra/clangd/ClangdServer.h
+++ b/clang-tools-extra/clangd/ClangdServer.h
@@ -283,7 +283,7 @@ class ClangdServer {
                      TypeHierarchyDirection Direction,
                      Callback<std::vector<TypeHierarchyItem>> CB);
   /// Get direct parents of a type hierarchy item.
-  void superTypes(const TypeHierarchyItem &Item,
+  void superTypes(PathRef File, const TypeHierarchyItem &Item,
                   Callback<std::optional<std::vector<TypeHierarchyItem>>> CB);
   /// Get direct children of a type hierarchy item.
   void subTypes(PathRef File, const TypeHierarchyItem &Item,
@@ -299,8 +299,7 @@ class ClangdServer {
                             Callback<std::vector<CallHierarchyItem>> CB);
 
   /// Resolve incoming calls for a given call hierarchy item.
-  void incomingCalls(PathRef File,
-                     const CallHierarchyItem &Item,
+  void incomingCalls(PathRef File, const CallHierarchyItem &Item,
                      Callback<std::vector<CallHierarchyIncomingCall>> CB);
 
   /// Resolve outgoing calls for a given call hierarchy item.
diff --git a/clang-tools-extra/clangd/Protocol.h b/clang-tools-extra/clangd/Protocol.h
index 7a99721a1e856..b1cd269e45533 100644
--- a/clang-tools-extra/clangd/Protocol.h
+++ b/clang-tools-extra/clangd/Protocol.h
@@ -1548,6 +1548,9 @@ struct TypeHierarchyItem {
   /// The kind of this item.
   SymbolKind kind;
 
+  /// The symbol tags for this item.
+  std::vector<SymbolTag> tags;
+
   /// More detail for this item, e.g. the signature of a function.
   std::optional<std::string> detail;
 
diff --git a/clang-tools-extra/clangd/XRefs.cpp b/clang-tools-extra/clangd/XRefs.cpp
index ff9a15c3372aa..ce282cb8a71d3 100644
--- a/clang-tools-extra/clangd/XRefs.cpp
+++ b/clang-tools-extra/clangd/XRefs.cpp
@@ -1820,6 +1820,7 @@ declToHierarchyItem(const NamedDecl &ND, llvm::StringRef TUPath) {
   HI.name = printName(Ctx, ND);
   HI.detail = printQualifiedName(ND);
   HI.kind = SK;
+  HI.tags = getSymbolTags(ND);
   HI.range = Range{sourceLocToPosition(SM, DeclRange->getBegin()),
                    sourceLocToPosition(SM, DeclRange->getEnd())};
   HI.selectionRange = Range{NameBegin, NameEnd};
@@ -1938,15 +1939,14 @@ static void fillSubTypes(const SymbolID &ID,
   RelationsRequest Req;
   Req.Subjects.insert(ID);
   Req.Predicate = RelationKind::BaseOf;
-  Index->relations(Req, [&](const SymbolID &Subject, const Symbol &Object) {
+  Index->relations(Req, [&Levels, &Index, &SubTypes, &TUPath,
+                         &AST](const SymbolID &Subject, const Symbol &Object) {
     std::optional<TypeHierarchyItem> ChildSym;
 
     if (auto *ND = getNamedDeclFromSymbol(Object, AST)) {
       ChildSym = declToTypeHierarchyItem(*ND, AST.tuPath());
-      elog("fillSubTypes: declToTypeHierarchyItem, {0}", ChildSym.has_value());
     } else {
       ChildSym = symbolToTypeHierarchyItem(Object, TUPath);
-      elog("fillSubTypes: symbolToTypeHierarchyItem, {0}", ChildSym.has_value());
     }
     if (ChildSym) {
       if (Levels > 1) {
@@ -2335,7 +2335,8 @@ getTypeHierarchy(ParsedAST &AST, Position Pos, int ResolveLevels,
 
       if (Index) {
         if (auto ID = getSymbolID(CXXRD))
-          fillSubTypes(ID, *Result->children, Index, ResolveLevels, TUPath, AST);
+          fillSubTypes(ID, *Result->children, Index, ResolveLevels, TUPath,
+                       AST);
       }
     }
     Results.emplace_back(std::move(*Result));
@@ -2345,7 +2346,8 @@ getTypeHierarchy(ParsedAST &AST, Position Pos, int ResolveLevels,
 }
 
 std::optional<std::vector<TypeHierarchyItem>>
-superTypes(const TypeHierarchyItem &Item, const SymbolIndex *Index) {
+superTypes(const TypeHierarchyItem &Item, const SymbolIndex *Index,
+           const ParsedAST &AST) {
   std::vector<TypeHierarchyItem> Results;
   if (!Item.data.parents)
     return std::nullopt;
@@ -2357,8 +2359,14 @@ superTypes(const TypeHierarchyItem &Item, const SymbolIndex *Index) {
     Req.IDs.insert(Parent.symbolID);
     IDToData[Parent.symbolID] = &Parent;
   }
-  Index->lookup(Req, [&Item, &Results, &IDToData](const Symbol &S) {
-    if (auto THI = symbolToTypeHierarchyItem(S, Item.uri.file())) {
+  Index->lookup(Req, [&Item, &Results, &IDToData, &AST](const Symbol &S) {
+    std::optional<TypeHierarchyItem> THI;
+    if (auto *ND = getNamedDeclFromSymbol(S, AST)) {
+      THI = declToTypeHierarchyItem(*ND, AST.tuPath());
+    } else {
+      THI = symbolToTypeHierarchyItem(S, Item.uri.file());
+    }
+    if (THI) {
       THI->data = *IDToData.lookup(S.ID);
       Results.emplace_back(std::move(*THI));
     }
@@ -2367,7 +2375,8 @@ superTypes(const TypeHierarchyItem &Item, const SymbolIndex *Index) {
 }
 
 std::vector<TypeHierarchyItem> subTypes(const TypeHierarchyItem &Item,
-                                        const SymbolIndex *Index, const ParsedAST &AST) {
+                                        const SymbolIndex *Index,
+                                        const ParsedAST &AST) {
   std::vector<TypeHierarchyItem> Results;
   fillSubTypes(Item.data.symbolID, Results, Index, 1, Item.uri.file(), AST);
   for (auto &ChildSym : Results)
@@ -2377,8 +2386,7 @@ std::vector<TypeHierarchyItem> subTypes(const TypeHierarchyItem &Item,
 
 void resolveTypeHierarchy(TypeHierarchyItem &Item, int ResolveLevels,
                           TypeHierarchyDirection Direction,
-                          const SymbolIndex *Index,
-                          const ParsedAST &AST) {
+                          const SymbolIndex *Index, const ParsedAST &AST) {
   // We only support typeHierarchy/resolve for children, because for parents
   // we ignore ResolveLevels and return all levels of parents eagerly.
   if (!Index || Direction == TypeHierarchyDirection::Parents ||
diff --git a/clang-tools-extra/clangd/XRefs.h b/clang-tools-extra/clangd/XRefs.h
index 1582c481ea5c2..f2652b5f274db 100644
--- a/clang-tools-extra/clangd/XRefs.h
+++ b/clang-tools-extra/clangd/XRefs.h
@@ -132,9 +132,11 @@ std::vector<TypeHierarchyItem> getTypeHierarchy(
     const SymbolIndex *Index = nullptr, PathRef TUPath = PathRef{});
 
 /// Returns direct parents of a TypeHierarchyItem using SymbolIDs stored inside
-/// the item.
+/// the item or using the AST.
 std::optional<std::vector<TypeHierarchyItem>>
-superTypes(const TypeHierarchyItem &Item, const SymbolIndex *Index);
+superTypes(const TypeHierarchyItem &Item, const SymbolIndex *Index,
+           const ParsedAST &AST);
+
 /// Returns direct children of a TypeHierarchyItem.
 std::vector<TypeHierarchyItem> subTypes(const TypeHierarchyItem &Item,
                                         const SymbolIndex *Index,
diff --git a/clang-tools-extra/clangd/test/call-hierarchy.test b/clang-tools-extra/clangd/test/call-hierarchy.test
index fb0766c089ee5..270b553302ce1 100644
--- a/clang-tools-extra/clangd/test/call-hierarchy.test
+++ b/clang-tools-extra/clangd/test/call-hierarchy.test
@@ -32,6 +32,9 @@
 # CHECK-NEXT:          "line": 0
 # CHECK-NEXT:        }
 # CHECK-NEXT:      },
+# CHECK-NEXT:      "tags": [
+# CHECK-NEXT:        18
+# CHECK-NEXT:      ],
 # CHECK-NEXT:      "uri": "file://{{.*}}/clangd-test/main.cpp"
 # CHECK-NEXT:    }
 ---
diff --git a/clang-tools-extra/clangd/test/type-hierarchy.test b/clang-tools-extra/clangd/test/type-hierarchy.test
index 065f129e05d2f..c7b8a16ae51e2 100644
--- a/clang-tools-extra/clangd/test/type-hierarchy.test
+++ b/clang-tools-extra/clangd/test/type-hierarchy.test
@@ -46,6 +46,7 @@
 # CHECK-NEXT:         }
 # CHECK-NEXT:       },
 # CHECK-NEXT:       "tags": [
+# CHECK-NEXT:         18,
 # CHECK-NEXT:         19
 # CHECK-NEXT:       ],
 # CHECK-NEXT:       "uri": "file://{{.*}}/clangd-test/main.cpp"
@@ -71,11 +72,11 @@
 # CHECK-NEXT:       "name": "Child1",
 # CHECK-NEXT:       "range": {
 # CHECK-NEXT:         "end": {
-# CHECK-NEXT:           "character": 13,
+# CHECK-NEXT:           "character": 25,
 # CHECK-NEXT:           "line": 1
 # CHECK-NEXT:         },
 # CHECK-NEXT:         "start": {
-# CHECK-NEXT:           "character": 7,
+# CHECK-NEXT:           "character": 0,
 # CHECK-NEXT:           "line": 1
 # CHECK-NEXT:         }
 # CHECK-NEXT:       },
@@ -90,6 +91,7 @@
 # CHECK-NEXT:         }
 # CHECK-NEXT:       },
 # CHECK-NEXT:       "tags": [
+# CHECK-NEXT:         18,
 # CHECK-NEXT:         19
 # CHECK-NEXT:       ],
 # CHECK-NEXT:       "uri": "file://{{.*}}/clangd-test/main.cpp"
@@ -144,6 +146,7 @@
 # CHECK-NEXT:         }
 # CHECK-NEXT:       },
 # CHECK-NEXT:       "tags": [
+# CHECK-NEXT:         18,
 # CHECK-NEXT:         19
 # CHECK-NEXT:       ],
 # CHECK-NEXT:       "uri": "file://{{.*}}/clangd-test/main.cpp"
diff --git a/clang-tools-extra/clangd/unittests/TypeHierarchyTests.cpp b/clang-tools-extra/clangd/unittests/TypeHierarchyTests.cpp
index ae2f806d0b17e..8696e6d4b0790 100644
--- a/clang-tools-extra/clangd/unittests/TypeHierarchyTests.cpp
+++ b/clang-tools-extra/clangd/unittests/TypeHierarchyTests.cpp
@@ -809,7 +809,7 @@ struct Chil^d : Parent {};
                                  TypeHierarchyDirection::Children, Index.get(),
                                  testPath(TU.Filename));
   ASSERT_THAT(Result, SizeIs(1));
-  auto Parents = superTypes(Result.front(), Index.get());
+  auto Parents = superTypes(Result.front(), Index.get(), AST);
 
   EXPECT_THAT(Parents, Optional(UnorderedElementsAre(
                            AllOf(withName("Parent"),

>From 7e11d6921dfb4b94b9f75b3f60382d3399fa09eb Mon Sep 17 00:00:00 2001
From: Dimitri Ratz <dimitri.ratz at thinkdigital.cc>
Date: Thu, 22 Jan 2026 13:18:40 +0100
Subject: [PATCH 09/17] Extended unit-tests to check the occurrence of symbol
 tags in call-hierarchy and type-hierarchy.

---
 .../clangd/unittests/CallHierarchyTests.cpp   | 56 +++++++++++++++++++
 .../clangd/unittests/TypeHierarchyTests.cpp   | 30 ++++++----
 2 files changed, 76 insertions(+), 10 deletions(-)

diff --git a/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp b/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp
index 2891b420427d2..1d2ae22463a08 100644
--- a/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp
+++ b/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp
@@ -48,6 +48,12 @@ MATCHER_P(withDetail, N, "") { return arg.detail == N; }
 MATCHER_P(withFile, N, "") { return arg.uri.file() == N; }
 MATCHER_P(withSelectionRange, R, "") { return arg.selectionRange == R; }
 
+template <typename... Tags>
+::testing::Matcher<CallHierarchyItem> withSymbolTags(Tags... tags) {
+  // Matches the tags vector ignoring element order.
+  return Field(&CallHierarchyItem::tags, UnorderedElementsAre(tags...));
+}
+
 template <class ItemMatcher>
 ::testing::Matcher<CallHierarchyIncomingCall> from(ItemMatcher M) {
   return Field(&CallHierarchyIncomingCall::from, M);
@@ -728,6 +734,56 @@ TEST(CallHierarchy, CallInDifferentFileThanCaller) {
               ElementsAre(AllOf(from(withName("caller")), iFromRanges())));
 }
 
+TEST(CallHierarchy, IncomingCalls) {
+  Annotations Source(R"cpp(
+    class A {
+      public:
+        void call^ee() {};
+    };
+    void caller(A &a) {
+      a.callee();
+    }
+  )cpp");
+  TestTU TU = TestTU::withCode(Source.code());
+  auto AST = TU.build();
+  auto Index = TU.index();
+
+  std::vector<CallHierarchyItem> Items =
+      prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename));
+  ASSERT_THAT(Items, ElementsAre(withName("callee")));
+
+  auto Incoming = incomingCalls(Items[0], Index.get(), AST);
+  EXPECT_THAT(
+      Incoming,
+      UnorderedElementsAre(AllOf(from(
+          AllOf(withName("caller"), withSymbolTags(SymbolTag::Declaration,
+                                                   SymbolTag::Definition))))));
+}
+
+TEST(CallHierarchy, OutgoingCalls) {
+  Annotations Source(R"cpp(
+    void callee() {}
+    class A {
+      public:
+        void call^er() {
+          callee();
+        };
+    };
+  )cpp");
+  TestTU TU = TestTU::withCode(Source.code());
+  auto AST = TU.build();
+  auto Index = TU.index();
+
+  std::vector<CallHierarchyItem> Items =
+      prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename));
+  ASSERT_THAT(Items, ElementsAre(withName("caller")));
+
+  auto Outgoing = outgoingCalls(Items[0], Index.get(), AST);
+  EXPECT_THAT(Outgoing, UnorderedElementsAre(AllOf(
+                            to(AllOf(withName("callee"),
+                                     withSymbolTags(SymbolTag::Declaration,
+                                                    SymbolTag::Definition))))));
+}
 } // namespace
 } // namespace clangd
 } // namespace clang
diff --git a/clang-tools-extra/clangd/unittests/TypeHierarchyTests.cpp b/clang-tools-extra/clangd/unittests/TypeHierarchyTests.cpp
index 8696e6d4b0790..21adbb6d0d3a3 100644
--- a/clang-tools-extra/clangd/unittests/TypeHierarchyTests.cpp
+++ b/clang-tools-extra/clangd/unittests/TypeHierarchyTests.cpp
@@ -54,6 +54,12 @@ MATCHER_P(withResolveParents, M, "") {
   return testing::ExplainMatchResult(M, arg.data.parents, result_listener);
 }
 
+template <typename... Tags>
+::testing::Matcher<TypeHierarchyItem> withSymbolTags(Tags... tags) {
+  // Matches the tags vector ignoring element order.
+  return Field(&TypeHierarchyItem::tags, UnorderedElementsAre(tags...));
+}
+
 TEST(FindRecordTypeAt, TypeOrVariable) {
   Annotations Source(R"cpp(
 struct Ch^ild2 {
@@ -770,10 +776,10 @@ struct Child2b : Child1 {};
 
 TEST(Standard, SubTypes) {
   Annotations Source(R"cpp(
-struct Pare^nt1 {};
-struct Parent2 {};
-struct Child : Parent1, Parent2 {};
-)cpp");
+    struct Pare^nt1 {};
+    struct Parent2 {};
+    struct Child final : Parent1, Parent2 {};
+  )cpp");
 
   TestTU TU = TestTU::withCode(Source.code());
   auto AST = TU.build();
@@ -791,15 +797,17 @@ struct Child : Parent1, Parent2 {};
       Children,
       UnorderedElementsAre(
           AllOf(withName("Child"),
+                withSymbolTags(SymbolTag::Declaration, SymbolTag::Definition,
+                               SymbolTag::Final),
                 withResolveParents(Optional(UnorderedElementsAre(withResolveID(
                     getSymbolID(&findDecl(AST, "Parent1")).str())))))));
 }
 
 TEST(Standard, SuperTypes) {
   Annotations Source(R"cpp(
-struct Parent {};
-struct Chil^d : Parent {};
-)cpp");
+    struct Parent {};
+    struct Chil^d : Parent {};
+  )cpp");
 
   TestTU TU = TestTU::withCode(Source.code());
   auto AST = TU.build();
@@ -811,9 +819,11 @@ struct Chil^d : Parent {};
   ASSERT_THAT(Result, SizeIs(1));
   auto Parents = superTypes(Result.front(), Index.get(), AST);
 
-  EXPECT_THAT(Parents, Optional(UnorderedElementsAre(
-                           AllOf(withName("Parent"),
-                                 withResolveParents(Optional(IsEmpty()))))));
+  EXPECT_THAT(Parents,
+              Optional(UnorderedElementsAre(AllOf(
+                  withName("Parent"),
+                  withSymbolTags(SymbolTag::Declaration, SymbolTag::Definition),
+                  withResolveParents(Optional(IsEmpty()))))));
 }
 } // namespace
 } // namespace clangd

>From 007764fa085dd099427f0503099165210e6ca871 Mon Sep 17 00:00:00 2001
From: Dimitri Ratz <dimitri.ratz at thinkdigital.cc>
Date: Fri, 23 Jan 2026 15:46:57 +0100
Subject: [PATCH 10/17] Simplify lambda capture by explicitly moving `Item` and
 parameters. Fix code format.

---
 clang-tools-extra/clangd/ClangdServer.cpp     |  2 +-
 clang-tools-extra/clangd/FindSymbols.h        |  2 +-
 clang-tools-extra/clangd/Protocol.cpp         |  6 ++--
 .../clangd/unittests/CallHierarchyTests.cpp   | 28 +++++++++++--------
 4 files changed, 22 insertions(+), 16 deletions(-)

diff --git a/clang-tools-extra/clangd/ClangdServer.cpp b/clang-tools-extra/clangd/ClangdServer.cpp
index 5b6525d8f842e..e304db2914483 100644
--- a/clang-tools-extra/clangd/ClangdServer.cpp
+++ b/clang-tools-extra/clangd/ClangdServer.cpp
@@ -899,7 +899,7 @@ void ClangdServer::resolveTypeHierarchy(
     PathRef File, TypeHierarchyItem Item, int Resolve,
     TypeHierarchyDirection Direction,
     Callback<std::optional<TypeHierarchyItem>> CB) {
-  auto Action = [=, CB = std::move(CB),
+  auto Action = [Item = std::move(Item), Resolve, Direction, CB = std::move(CB),
                  this](llvm::Expected<InputsAndAST> InpAST) mutable {
     if (!InpAST)
       return CB(InpAST.takeError());
diff --git a/clang-tools-extra/clangd/FindSymbols.h b/clang-tools-extra/clangd/FindSymbols.h
index 169efa9a84584..d85a03ebe449b 100644
--- a/clang-tools-extra/clangd/FindSymbols.h
+++ b/clang-tools-extra/clangd/FindSymbols.h
@@ -14,8 +14,8 @@
 
 #include "Protocol.h"
 #include "index/Symbol.h"
-#include "llvm/ADT/StringRef.h"
 #include "clang/AST/Decl.h"
+#include "llvm/ADT/StringRef.h"
 
 namespace clang {
 class NamedDecl;
diff --git a/clang-tools-extra/clangd/Protocol.cpp b/clang-tools-extra/clangd/Protocol.cpp
index fb3919f587ff2..7d1b06db64e2b 100644
--- a/clang-tools-extra/clangd/Protocol.cpp
+++ b/clang-tools-extra/clangd/Protocol.cpp
@@ -209,7 +209,7 @@ bool fromJSON(const llvm::json::Value &Params, ChangeAnnotation &R,
          O.map("needsConfirmation", R.needsConfirmation) &&
          O.mapOptional("description", R.description);
 }
-llvm::json::Value toJSON(const ChangeAnnotation & CA) {
+llvm::json::Value toJSON(const ChangeAnnotation &CA) {
   llvm::json::Object Result{{"label", CA.label}};
   if (CA.needsConfirmation)
     Result["needsConfirmation"] = *CA.needsConfirmation;
@@ -867,7 +867,7 @@ llvm::json::Value toJSON(const SymbolInformation &P) {
   };
   if (P.score)
     O["score"] = *P.score;
-  if(!P.tags.empty())
+  if (!P.tags.empty())
     O["tags"] = P.tags;
   return std::move(O);
 }
@@ -1447,7 +1447,7 @@ llvm::json::Value toJSON(const TypeHierarchyItem &I) {
 
   if (I.detail)
     Result["detail"] = I.detail;
-  if(!I.tags.empty())
+  if (!I.tags.empty())
     Result["tags"] = I.tags;
   return std::move(Result);
 }
diff --git a/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp b/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp
index 1d2ae22463a08..fb911ba07354a 100644
--- a/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp
+++ b/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp
@@ -305,11 +305,11 @@ TEST(CallHierarchy, OutgoingOneFile) {
   auto OugoingLevel1 = outgoingCalls(Items[0], Index.get(), AST);
   ASSERT_THAT(
       OugoingLevel1,
-      ElementsAre(
-          AllOf(to(AllOf(withName("Foo::caller1"), withDetail("ns::Foo::caller1"))),
-                oFromRanges(Source.range("Caller1C"))),
-          AllOf(to(AllOf(withName("caller2"), withDetail("caller2"))),
-                oFromRanges(Source.range("Caller2")))));
+      ElementsAre(AllOf(to(AllOf(withName("Foo::caller1"),
+                                 withDetail("ns::Foo::caller1"))),
+                        oFromRanges(Source.range("Caller1C"))),
+                  AllOf(to(AllOf(withName("caller2"), withDetail("caller2"))),
+                        oFromRanges(Source.range("Caller2")))));
 
   auto OutgoingLevel2 = outgoingCalls(OugoingLevel1[1].to, Index.get(), AST);
   ASSERT_THAT(
@@ -408,7 +408,8 @@ TEST(CallHierarchy, MultiFileCpp) {
                                              withDetail("nsa::caller1"))),
                                   iFromRanges(Caller1C.range()))));
 
-    auto IncomingLevel2 = incomingCalls(IncomingLevel1[0].from, Index.get(), AST);
+    auto IncomingLevel2 =
+        incomingCalls(IncomingLevel1[0].from, Index.get(), AST);
     ASSERT_THAT(
         IncomingLevel2,
         ElementsAre(
@@ -417,13 +418,15 @@ TEST(CallHierarchy, MultiFileCpp) {
             AllOf(from(AllOf(withName("caller3"), withDetail("nsa::caller3"))),
                   iFromRanges(Caller3C.range("Caller1")))));
 
-    auto IncomingLevel3 = incomingCalls(IncomingLevel2[0].from, Index.get(), AST);
+    auto IncomingLevel3 =
+        incomingCalls(IncomingLevel2[0].from, Index.get(), AST);
     ASSERT_THAT(IncomingLevel3,
                 ElementsAre(AllOf(from(AllOf(withName("caller3"),
                                              withDetail("nsa::caller3"))),
                                   iFromRanges(Caller3C.range("Caller2")))));
 
-    auto IncomingLevel4 = incomingCalls(IncomingLevel3[0].from, Index.get(), AST);
+    auto IncomingLevel4 =
+        incomingCalls(IncomingLevel3[0].from, Index.get(), AST);
     EXPECT_THAT(IncomingLevel4, IsEmpty());
   };
 
@@ -564,7 +567,8 @@ TEST(CallHierarchy, IncomingMultiFileObjC) {
                 ElementsAre(AllOf(from(withName("caller1")),
                                   iFromRanges(Caller1C.range()))));
 
-    auto IncomingLevel2 = incomingCalls(IncomingLevel1[0].from, Index.get(), AST);
+    auto IncomingLevel2 =
+        incomingCalls(IncomingLevel1[0].from, Index.get(), AST);
     ASSERT_THAT(IncomingLevel2,
                 ElementsAre(AllOf(from(withName("caller2")),
                                   iFromRanges(Caller2C.range("A"),
@@ -572,12 +576,14 @@ TEST(CallHierarchy, IncomingMultiFileObjC) {
                             AllOf(from(withName("caller3")),
                                   iFromRanges(Caller3C.range("Caller1")))));
 
-    auto IncomingLevel3 = incomingCalls(IncomingLevel2[0].from, Index.get(), AST);
+    auto IncomingLevel3 =
+        incomingCalls(IncomingLevel2[0].from, Index.get(), AST);
     ASSERT_THAT(IncomingLevel3,
                 ElementsAre(AllOf(from(withName("caller3")),
                                   iFromRanges(Caller3C.range("Caller2")))));
 
-    auto IncomingLevel4 = incomingCalls(IncomingLevel3[0].from, Index.get(), AST);
+    auto IncomingLevel4 =
+        incomingCalls(IncomingLevel3[0].from, Index.get(), AST);
     EXPECT_THAT(IncomingLevel4, IsEmpty());
   };
 

>From f1fdea35baedf486e3d73ca8bee0e3495be30c7f Mon Sep 17 00:00:00 2001
From: Dimitri Ratz <dimitri.ratz at protonmail.com>
Date: Mon, 16 Feb 2026 12:36:00 +0100
Subject: [PATCH 11/17] Feat: functions calculating the tags Overrides and
 Implements.

Changed the usage of SymbolTag:
- Abstract = pure virtual
- Virtual = a virtual method implemented in that class, not implementing or overriding a method from base class
- Implements = implements pure virtual method from base class
- Override = overriding a virtual method implemented in base class
- Final = final method
---
 clang-tools-extra/clangd/FindSymbols.cpp      | 116 ++++++++++++-
 clang-tools-extra/clangd/Protocol.h           |  19 ++-
 .../clangd/test/symbol-tags.test              |   2 -
 .../clangd/unittests/FindSymbolsTests.cpp     | 152 +++++++++++++++---
 4 files changed, 260 insertions(+), 29 deletions(-)

diff --git a/clang-tools-extra/clangd/FindSymbols.cpp b/clang-tools-extra/clangd/FindSymbols.cpp
index 5209dd82dc738..e980cd08a76eb 100644
--- a/clang-tools-extra/clangd/FindSymbols.cpp
+++ b/clang-tools-extra/clangd/FindSymbols.cpp
@@ -121,6 +121,16 @@ bool isAbstract(const Decl *D) {
 
 // Indicates whether declaration D is virtual in cases where D is a method.
 bool isVirtual(const Decl *D) {
+  // We want to treat a method as virtual if it is declared virtual, even if it
+  // is not implemented in this class, or if it overrides/implements a
+  // base-class method. This is because the "virtual" modifier is still relevant
+  // to the method's behavior and how it should be highlighted, even if it is
+  // not itself a virtual method in the strictest sense. For example, a method
+  // that overrides a virtual method from a base class is still considered
+  // virtual, even if it is not declared as such in the derived class.
+  // Similarly, a method that implements a pure virtual method from a base class
+  // is also considered virtual, even if it is not declared as such in the
+  // derived class.
   if (const auto *CMD = llvm::dyn_cast<CXXMethodDecl>(D))
     return CMD->isVirtual();
   return false;
@@ -162,6 +172,45 @@ SymbolTags toSymbolTagBitmask(const SymbolTag ST) {
   return (1 << static_cast<unsigned>(ST));
 }
 
+bool isOverrides(const NamedDecl *ND) {
+  if (const auto *MD = llvm::dyn_cast<CXXMethodDecl>(ND)) {
+    // A method "overrides" if:
+    // 1. It overrides at least one method
+    // 2. At least one of the overridden methods is virtual (but NOT pure
+    // virtual)
+
+    if (MD->size_overridden_methods() == 0)
+      return false;
+
+    for (const auto Overridden : MD->overridden_methods()) {
+      // Check if the overridden method is virtual but not pure virtual
+      if (Overridden->isVirtual() && !Overridden->isPureVirtual())
+        return true;
+    }
+    return false;
+  }
+  return false;
+}
+
+bool isImplements(const NamedDecl *ND) {
+  if (const auto *MD = llvm::dyn_cast<CXXMethodDecl>(ND)) {
+    // A method "implements" pure virtual methods from base classes if:
+    // 1. It overrides at least one method
+    // 2. It is NOT itself pure virtual (i.e., it has a concrete implementation)
+    // 3. ALL overridden methods are pure virtual
+
+    if (MD->size_overridden_methods() == 0 || MD->isPureVirtual())
+      return false;
+
+    for (const auto Overridden : MD->overridden_methods()) {
+      if (!Overridden->isPureVirtual())
+        return false;
+    }
+    return true;
+  }
+  return false;
+}
+
 SymbolTags computeSymbolTags(const NamedDecl &ND) {
   SymbolTags Result = 0;
   const auto IsDef = isUniqueDefinition(&ND);
@@ -184,6 +233,12 @@ SymbolTags computeSymbolTags(const NamedDecl &ND) {
   if (isFinal(&ND))
     Result |= toSymbolTagBitmask(SymbolTag::Final);
 
+  if (isOverrides(&ND))
+    Result |= toSymbolTagBitmask(SymbolTag::Overrides);
+
+  if (isImplements(&ND))
+    Result |= toSymbolTagBitmask(SymbolTag::Implements);
+
   if (not isa<UnresolvedUsingValueDecl>(ND)) {
     // Do not treat an UnresolvedUsingValueDecl as a declaration.
     // It's more common to think of it as a reference to the
@@ -211,6 +266,62 @@ SymbolTags computeSymbolTags(const NamedDecl &ND) {
   return Result;
 }
 
+// Filter symbol tags based on the presence of other tags and the kind of
+// symbol. This is needed to avoid redundant tags.
+SymbolTags filterSymbolTags(const NamedDecl &ND, const SymbolTags ST) {
+  SymbolTags Result = ST;
+
+  if (not isa<CXXMethodDecl>(ND))
+    return Result;
+
+  if (ST & toSymbolTagBitmask(SymbolTag::Overrides)) {
+    // Overrides means that ND overrides an existing implementation of a virtual
+    // method in a base class. If a symbol is marked as Overrides, the tags
+    // Virtual, Declaration and Definition should be removed, as the Overrides
+    // tag implies that the symbol has/is virtual/declaration/definition.
+    Result &= ~toSymbolTagBitmask(SymbolTag::Virtual);
+    Result &= ~toSymbolTagBitmask(SymbolTag::Declaration);
+    Result &= ~toSymbolTagBitmask(SymbolTag::Definition);
+  }
+  if (ST & toSymbolTagBitmask(SymbolTag::Implements)) {
+    // Implements means that ND implements an existing pure virtual method in a
+    // base class. If a symbol is marked as Implements, the tags Virtual,
+    // Declaration, Definition and Overrides should be removed, as the
+    // Implements tag implies that the symbol is virtual, is a declaration, is a
+    // definition, and overrides a method.
+    Result &= ~toSymbolTagBitmask(SymbolTag::Virtual);
+    Result &= ~toSymbolTagBitmask(SymbolTag::Declaration);
+    Result &= ~toSymbolTagBitmask(SymbolTag::Definition);
+    Result &= ~toSymbolTagBitmask(SymbolTag::Overrides);
+  }
+  if (ST & toSymbolTagBitmask(SymbolTag::Virtual)) {
+    // Virtual means that ND is a virtual method that does not override any
+    // method in a base class. If a symbol is marked as Virtual, the tags
+    // Declaration and Definition should be removed, as the Virtual tag implies
+    // that the symbol is a declaration/definition.
+    Result &= ~toSymbolTagBitmask(SymbolTag::Declaration);
+    Result &= ~toSymbolTagBitmask(SymbolTag::Definition);
+  }
+  if (ST & toSymbolTagBitmask(SymbolTag::Abstract)) {
+    // Abstract means that ND is a pure virtual method. If a symbol is marked as
+    // Abstract, the tags Virtual, Declaration and Definition should be removed,
+    // as the Abstract tag implies that the symbol is virtual and a
+    // declaration/definition.
+    Result &= ~toSymbolTagBitmask(SymbolTag::Virtual);
+    Result &= ~toSymbolTagBitmask(SymbolTag::Declaration);
+    Result &= ~toSymbolTagBitmask(SymbolTag::Definition);
+  }
+  if (ST & toSymbolTagBitmask(SymbolTag::Final)) {
+    // Final means that ND is a method that cannot be overridden by any method
+    // in a derived class. If a symbol is marked as Final, the tags Virtual and
+    // Overrides should be removed, as the Final tag implies that the symbol is
+    // virtual.
+    Result &= ~toSymbolTagBitmask(SymbolTag::Virtual);
+    Result &= ~toSymbolTagBitmask(SymbolTag::Overrides);
+  }
+  return Result;
+}
+
 std::vector<SymbolTag> getSymbolTags(const NamedDecl &ND) {
   const auto symbolTags = computeSymbolTags(ND);
   std::vector<SymbolTag> Tags;
@@ -218,6 +329,9 @@ std::vector<SymbolTag> getSymbolTags(const NamedDecl &ND) {
   if (symbolTags == 0)
     return Tags;
 
+  // Apply specific filter to the symbol tags.
+  const auto filteredTags = filterSymbolTags(ND, symbolTags);
+
   // Iterate through SymbolTag enum values and collect any that are present in
   // the bitmask. SymbolTag values are in the numeric range
   // [FirstTag .. LastTag].
@@ -225,7 +339,7 @@ std::vector<SymbolTag> getSymbolTags(const NamedDecl &ND) {
   constexpr unsigned MaxTag = static_cast<unsigned>(SymbolTag::LastTag);
   for (unsigned I = MinTag; I <= MaxTag; ++I) {
     auto ST = static_cast<SymbolTag>(I);
-    if (symbolTags & toSymbolTagBitmask(ST))
+    if (filteredTags & toSymbolTagBitmask(ST))
       Tags.push_back(ST);
   }
   return Tags;
diff --git a/clang-tools-extra/clangd/Protocol.h b/clang-tools-extra/clangd/Protocol.h
index b1cd269e45533..baa2d659b218f 100644
--- a/clang-tools-extra/clangd/Protocol.h
+++ b/clang-tools-extra/clangd/Protocol.h
@@ -1115,22 +1115,33 @@ enum class SymbolTag {
   Internal = 6,
   File = 7,
   Static = 8,
-  Abstract = 9,
-  Final = 10,
+  Abstract = 9, // In context of a class and method - this symbol indicates a
+                // pure virtual class or method.
+  Final = 10, // In context of a method - this symbol indicates that the method
+              // cannot be overridden in subclasses.
+              // In context of a class - this symbol indicates that the class is
+              // final and thus cannot be extended.
   Sealed = 11,
   Transient = 12,
   Volatile = 13,
   Synchronized = 14,
-  Virtual = 15,
+  Virtual =
+      15, // In context of a method - this symbol indicates a virtual
+          // method declared and implemented in same class, and thereby it is
+          // not implementing or overriding a method from any base class.
   Nullable = 16,
   NonNull = 17,
   Declaration = 18,
   Definition = 19,
   ReadOnly = 20,
+  Overrides = 21,  // In context of a method - this symbol indicates a method
+                   // overriding a virtual method, implemented in base class.
+  Implements = 22, // In context of a method - this symbol indicates a method
+                   // implementing a pure virtual method from a base class.
 
   // Update as needed
   FirstTag = Deprecated,
-  LastTag = ReadOnly
+  LastTag = Implements
 };
 llvm::json::Value toJSON(SymbolTag);
 /// Represents programming constructs like variables, classes, interfaces etc.
diff --git a/clang-tools-extra/clangd/test/symbol-tags.test b/clang-tools-extra/clangd/test/symbol-tags.test
index 6b17dc994d029..34c06c4b9ba63 100644
--- a/clang-tools-extra/clangd/test/symbol-tags.test
+++ b/clang-tools-extra/clangd/test/symbol-tags.test
@@ -41,8 +41,6 @@
 # CHECK:           "tags": [
 # CHECK:             2,
 # CHECK:             9,
-# CHECK:             15,
-# CHECK:             18,
 # CHECK:             20
 # CHECK:           ]
 # CHECK:         }
diff --git a/clang-tools-extra/clangd/unittests/FindSymbolsTests.cpp b/clang-tools-extra/clangd/unittests/FindSymbolsTests.cpp
index 2d237429ebfbf..63fb76e330d8f 100644
--- a/clang-tools-extra/clangd/unittests/FindSymbolsTests.cpp
+++ b/clang-tools-extra/clangd/unittests/FindSymbolsTests.cpp
@@ -1160,12 +1160,125 @@ TEST(DocumentSymbolsTest, PragmaMarkGroupsNoNesting) {
                                    withName("Core"), withName("coreMethod")));
 }
 
-TEST(DocumentSymbolsTest, SymbolTags) {
+TEST(DocumentSymbolsTest, SymbolTagsMustContainPublicAbstract) {
   TestTU TU;
   Annotations Main(R"cpp(
-    class AbstractClass {
+    class A {
       public:
-        virtual ~AbstractClass() = default;
+        virtual void f1() = 0;
+    };
+
+    class B : public A {
+      public:
+        virtual void f2() = 0;
+    };
+    )cpp");
+
+  TU.Code = Main.code().str();
+  auto Symbols = getSymbols(TU.build());
+  EXPECT_THAT(Symbols,
+              UnorderedElementsAre(
+                  AllOf(withName("A"),
+                        children(AllOf(withName("f1"),
+                                       withSymbolTags(SymbolTag::Public,
+                                                      SymbolTag::Abstract)))),
+                  AllOf(withName("B"),
+                        children(AllOf(withName("f2"),
+                                       withSymbolTags(SymbolTag::Public,
+                                                      SymbolTag::Abstract))))));
+}
+
+TEST(DocumentSymbolsTest, SymbolTagsMustContainPublicVirtualAndOverrides) {
+  TestTU TU;
+  Annotations Main(R"cpp(
+    class A {
+      public:
+        virtual void f1() {};
+    };
+
+    class B : public A {
+      public:
+        void f1() override {}
+    };
+
+    class C : public B {
+      public:
+        void f1() override {}
+    };
+    )cpp");
+
+  TU.Code = Main.code().str();
+  auto Symbols = getSymbols(TU.build());
+  EXPECT_THAT(
+      Symbols,
+      UnorderedElementsAre(
+          AllOf(withName("A"),
+                children(
+                    AllOf(withName("f1"), withSymbolTags(SymbolTag::Public,
+                                                         SymbolTag::Virtual)))),
+          AllOf(withName("B"),
+                children(AllOf(
+                    withName("f1"),
+                    withSymbolTags(SymbolTag::Public, SymbolTag::Overrides)))),
+          AllOf(withName("C"),
+                children(AllOf(withName("f1"),
+                               withSymbolTags(SymbolTag::Public,
+                                              SymbolTag::Overrides))))));
+}
+
+TEST(DocumentSymbolsTest,
+     SymbolTagsMustContainPublicAbstractImplementsOverridesAndFinal) {
+  TestTU TU;
+  Annotations Main(R"cpp(
+    class A {
+      public:
+        virtual void f1() = 0;
+    };
+
+    class B : public A {
+      public:
+        void f1() override {}
+    };
+
+    class C : public B {
+      public:
+        void f1() override {}
+    };
+
+    class D : public C {
+      public:
+        void f1() final override {}
+    };
+    )cpp");
+
+  TU.Code = Main.code().str();
+  auto Symbols = getSymbols(TU.build());
+  EXPECT_THAT(Symbols,
+              UnorderedElementsAre(
+                  AllOf(withName("A"),
+                        children(AllOf(withName("f1"),
+                                       withSymbolTags(SymbolTag::Public,
+                                                      SymbolTag::Abstract)))),
+                  AllOf(withName("B"),
+                        children(AllOf(withName("f1"),
+                                       withSymbolTags(SymbolTag::Public,
+                                                      SymbolTag::Implements)))),
+                  AllOf(withName("C"),
+                        children(AllOf(withName("f1"),
+                                       withSymbolTags(SymbolTag::Public,
+                                                      SymbolTag::Overrides)))),
+                  AllOf(withName("D"),
+                        children(AllOf(withName("f1"),
+                                       withSymbolTags(SymbolTag::Public,
+                                                      SymbolTag::Final))))));
+}
+
+TEST(DocumentSymbolsTest, SymbolTagsCompilation) {
+  TestTU TU;
+  Annotations Main(R"cpp(
+    class A {
+      public:
+        virtual ~A() = default;
         virtual void f1() = 0;
         void f2() const;
       protected:
@@ -1174,9 +1287,9 @@ TEST(DocumentSymbolsTest, SymbolTags) {
         static void f4(){}
     };
 
-    void AbstractClass::f2() const {}
+    void A::f2() const {}
 
-    class ImplClass final: public AbstractClass {
+    class B final: public A {
       public:
         void f1() final {}
     };
@@ -1188,18 +1301,14 @@ TEST(DocumentSymbolsTest, SymbolTags) {
       Symbols,
       UnorderedElementsAre(
           AllOf(
-              withName("AbstractClass"),
+              withName("A"),
               withSymbolTags(SymbolTag::Abstract, SymbolTag::Declaration,
                              SymbolTag::Definition),
               children(
-                  AllOf(withName("~AbstractClass"),
-                        withSymbolTags(SymbolTag::Public, SymbolTag::Virtual,
-                                       SymbolTag::Declaration,
-                                       SymbolTag::Definition)),
+                  AllOf(withName("~A"),
+                        withSymbolTags(SymbolTag::Public, SymbolTag::Virtual)),
                   AllOf(withName("f1"),
-                        withSymbolTags(SymbolTag::Public, SymbolTag::Abstract,
-                                       SymbolTag::Virtual,
-                                       SymbolTag::Declaration)),
+                        withSymbolTags(SymbolTag::Public, SymbolTag::Abstract)),
                   AllOf(withName("f2"), withSymbolTags(SymbolTag::Public,
                                                        SymbolTag::Declaration,
                                                        SymbolTag::ReadOnly)),
@@ -1210,17 +1319,16 @@ TEST(DocumentSymbolsTest, SymbolTags) {
                         withSymbolTags(SymbolTag::Private, SymbolTag::Static,
                                        SymbolTag::Declaration,
                                        SymbolTag::Definition)))),
-          AllOf(withName("AbstractClass::f2"),
+          AllOf(withName("A::f2"),
                 withSymbolTags(SymbolTag::Public, SymbolTag::Declaration,
                                SymbolTag::Definition, SymbolTag::ReadOnly)),
-          AllOf(withName("ImplClass"),
-                withSymbolTags(SymbolTag::Final, SymbolTag::Declaration,
-                               SymbolTag::Definition),
-                children(AllOf(
-                    withName("f1"),
-                    withSymbolTags(SymbolTag::Public, SymbolTag::Final,
-                                   SymbolTag::Virtual, SymbolTag::Declaration,
-                                   SymbolTag::Definition))))));
+          AllOf(
+              withName("B"),
+              withSymbolTags(SymbolTag::Final, SymbolTag::Declaration,
+                             SymbolTag::Definition),
+              children(AllOf(withName("f1"),
+                             withSymbolTags(SymbolTag::Public, SymbolTag::Final,
+                                            SymbolTag::Implements))))));
 }
 
 } // namespace

>From 2c0475f663304dea565e76780f7523aae1c70b42 Mon Sep 17 00:00:00 2001
From: Dimitri Ratz <dimitri.ratz at protonmail.com>
Date: Wed, 25 Feb 2026 12:53:13 +0100
Subject: [PATCH 12/17] Feat: compute symbol tags during the AST indexation.

---
 clang-tools-extra/clangd/FindSymbols.cpp      | 35 +++++----
 clang-tools-extra/clangd/FindSymbols.h        | 14 +---
 clang-tools-extra/clangd/XRefs.cpp            |  2 +-
 clang-tools-extra/clangd/index/Merge.cpp      |  1 +
 clang-tools-extra/clangd/index/Symbol.h       | 15 ++++
 .../clangd/index/SymbolCollector.cpp          | 14 ++--
 .../clangd/index/YAMLSerialization.cpp        |  1 +
 .../clangd/test/Inputs/symbols.test.yaml      |  3 +-
 .../clangd/unittests/FindSymbolsTests.cpp     | 74 +++++++++++++++++++
 9 files changed, 126 insertions(+), 33 deletions(-)

diff --git a/clang-tools-extra/clangd/FindSymbols.cpp b/clang-tools-extra/clangd/FindSymbols.cpp
index e980cd08a76eb..7d87885c5c60c 100644
--- a/clang-tools-extra/clangd/FindSymbols.cpp
+++ b/clang-tools-extra/clangd/FindSymbols.cpp
@@ -322,15 +322,15 @@ SymbolTags filterSymbolTags(const NamedDecl &ND, const SymbolTags ST) {
   return Result;
 }
 
-std::vector<SymbolTag> getSymbolTags(const NamedDecl &ND) {
-  const auto symbolTags = computeSymbolTags(ND);
+std::vector<SymbolTag> expandTagBitmask(const SymbolTags symbolTags) {
   std::vector<SymbolTag> Tags;
 
   if (symbolTags == 0)
     return Tags;
 
-  // Apply specific filter to the symbol tags.
-  const auto filteredTags = filterSymbolTags(ND, symbolTags);
+  // No filtering required since this function is only used for Symbols from the
+  // index, which have already been filtered in getSymbolTags(const NamedDecl
+  // &ND).
 
   // Iterate through SymbolTag enum values and collect any that are present in
   // the bitmask. SymbolTag values are in the numeric range
@@ -339,23 +339,32 @@ std::vector<SymbolTag> getSymbolTags(const NamedDecl &ND) {
   constexpr unsigned MaxTag = static_cast<unsigned>(SymbolTag::LastTag);
   for (unsigned I = MinTag; I <= MaxTag; ++I) {
     auto ST = static_cast<SymbolTag>(I);
-    if (filteredTags & toSymbolTagBitmask(ST))
+    if (symbolTags & toSymbolTagBitmask(ST))
       Tags.push_back(ST);
   }
   return Tags;
 }
 
-std::vector<SymbolTag> getSymbolTags(const Symbol &S) {
+std::vector<SymbolTag> getSymbolTags(const NamedDecl &ND) {
+  const auto symbolTags = computeSymbolTags(ND);
   std::vector<SymbolTag> Tags;
 
-  if (S.Flags & Symbol::Deprecated)
-    Tags.push_back(SymbolTag::Deprecated);
+  if (symbolTags == 0)
+    return Tags;
 
-  if (S.Definition)
-    Tags.push_back(SymbolTag::Definition);
-  else
-    Tags.push_back(SymbolTag::Declaration);
+  // Apply specific filter to the symbol tags.
+  const auto filteredTags = filterSymbolTags(ND, symbolTags);
 
+  // Iterate through SymbolTag enum values and collect any that are present in
+  // the bitmask. SymbolTag values are in the numeric range
+  // [FirstTag .. LastTag].
+  constexpr unsigned MinTag = static_cast<unsigned>(SymbolTag::FirstTag);
+  constexpr unsigned MaxTag = static_cast<unsigned>(SymbolTag::LastTag);
+  for (unsigned I = MinTag; I <= MaxTag; ++I) {
+    auto ST = static_cast<SymbolTag>(I);
+    if (filteredTags & toSymbolTagBitmask(ST))
+      Tags.push_back(ST);
+  }
   return Tags;
 }
 
@@ -490,7 +499,7 @@ getWorkspaceSymbols(llvm::StringRef Query, int Limit,
     Info.score = Relevance.NameMatch > std::numeric_limits<float>::epsilon()
                      ? Score / Relevance.NameMatch
                      : QualScore;
-    Info.tags = getSymbolTags(Sym);
+    Info.tags = expandTagBitmask(Sym.Tags);
     Top.push({Score, std::move(Info)});
   });
   for (auto &R : std::move(Top).items())
diff --git a/clang-tools-extra/clangd/FindSymbols.h b/clang-tools-extra/clangd/FindSymbols.h
index d85a03ebe449b..c13231fb644a0 100644
--- a/clang-tools-extra/clangd/FindSymbols.h
+++ b/clang-tools-extra/clangd/FindSymbols.h
@@ -12,7 +12,6 @@
 #ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_FINDSYMBOLS_H
 #define LLVM_CLANG_TOOLS_EXTRA_CLANGD_FINDSYMBOLS_H
 
-#include "Protocol.h"
 #include "index/Symbol.h"
 #include "clang/AST/Decl.h"
 #include "llvm/ADT/StringRef.h"
@@ -26,15 +25,6 @@ class SymbolIndex;
 struct Symbol;
 struct SymbolLocation;
 
-/// A bitmask type representing symbol tags supported by LSP.
-/// \see
-/// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#symbolTag
-using SymbolTags = uint32_t;
-/// Ensure we have enough bits to represent all SymbolTag values.
-static_assert(static_cast<unsigned>(SymbolTag::LastTag) <= 32,
-              "Too many SymbolTags to fit in uint32_t. Change to uint64_t if "
-              "we ever have more than 32 tags.");
-
 /// Helper function for deriving an LSP Location from an index SymbolLocation.
 llvm::Expected<Location> indexToLSPLocation(const SymbolLocation &Loc,
                                             llvm::StringRef TUPath);
@@ -73,8 +63,8 @@ SymbolTags computeSymbolTags(const NamedDecl &ND);
 /// \p ND The declaration to get tags for.
 std::vector<SymbolTag> getSymbolTags(const NamedDecl &ND);
 
-/// Returns the symbol tags for an index `Symbol`.
-std::vector<SymbolTag> getSymbolTags(const Symbol &S);
+/// Returns the symbol tags for the given declaration as a bitmask.
+std::vector<SymbolTag> expandTagBitmask(SymbolTags symbolTags);
 
 } // namespace clangd
 } // namespace clang
diff --git a/clang-tools-extra/clangd/XRefs.cpp b/clang-tools-extra/clangd/XRefs.cpp
index ce282cb8a71d3..088bd98f3b269 100644
--- a/clang-tools-extra/clangd/XRefs.cpp
+++ b/clang-tools-extra/clangd/XRefs.cpp
@@ -1873,7 +1873,7 @@ static std::optional<HierarchyItem> symbolToHierarchyItem(const Symbol &S,
   HI.name = std::string(S.Name);
   HI.detail = (S.Scope + S.Name).str();
   HI.kind = indexSymbolKindToSymbolKind(S.SymInfo);
-  HI.tags = getSymbolTags(S);
+  HI.tags = expandTagBitmask(S.Tags);
   HI.selectionRange = Loc->range;
   // FIXME: Populate 'range' correctly
   // (https://github.com/clangd/clangd/issues/59).
diff --git a/clang-tools-extra/clangd/index/Merge.cpp b/clang-tools-extra/clangd/index/Merge.cpp
index 625b1c6926a28..4a7caeb9ace60 100644
--- a/clang-tools-extra/clangd/index/Merge.cpp
+++ b/clang-tools-extra/clangd/index/Merge.cpp
@@ -319,6 +319,7 @@ Symbol mergeSymbol(const Symbol &L, const Symbol &R) {
 
   S.Origin |= O.Origin | SymbolOrigin::Merge;
   S.Flags |= O.Flags;
+  S.Tags |= O.Tags;
   return S;
 }
 
diff --git a/clang-tools-extra/clangd/index/Symbol.h b/clang-tools-extra/clangd/index/Symbol.h
index 62c47ddfc5758..45e6bebd04faa 100644
--- a/clang-tools-extra/clangd/index/Symbol.h
+++ b/clang-tools-extra/clangd/index/Symbol.h
@@ -9,6 +9,7 @@
 #ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_INDEX_SYMBOL_H
 #define LLVM_CLANG_TOOLS_EXTRA_CLANGD_INDEX_SYMBOL_H
 
+#include "Protocol.h"
 #include "index/SymbolID.h"
 #include "index/SymbolLocation.h"
 #include "index/SymbolOrigin.h"
@@ -22,6 +23,15 @@ namespace clangd {
 
 LLVM_ENABLE_BITMASK_ENUMS_IN_NAMESPACE();
 
+/// A bitmask type representing symbol tags supported by LSP.
+/// \see
+/// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#symbolTag
+using SymbolTags = uint32_t;
+/// Ensure we have enough bits to represent all SymbolTag values.
+static_assert(static_cast<unsigned>(SymbolTag::LastTag) <= 32,
+              "Too many SymbolTags to fit in uint32_t. Change to uint64_t if "
+              "we ever have more than 32 tags.");
+
 /// The class presents a C++ symbol, e.g. class, function.
 ///
 /// WARNING: Symbols do not own much of their underlying data - typically
@@ -87,6 +97,11 @@ struct Symbol {
   /// Only set when the symbol is indexed for completion.
   llvm::StringRef Type;
 
+  /// Symbol tags for LSP protocol (Deprecated, Static, Virtual, Abstract,
+  /// Final, ReadOnly, Public, Protected, Private, Declaration, Definition).
+  /// This is a bitmask where each bit represents a SymbolTag.
+  SymbolTags Tags = 0;
+
   enum IncludeDirective : uint8_t {
     Invalid = 0,
     /// `#include "header.h"`
diff --git a/clang-tools-extra/clangd/index/SymbolCollector.cpp b/clang-tools-extra/clangd/index/SymbolCollector.cpp
index bd974e4c18818..cd6fd53d310a9 100644
--- a/clang-tools-extra/clangd/index/SymbolCollector.cpp
+++ b/clang-tools-extra/clangd/index/SymbolCollector.cpp
@@ -11,6 +11,7 @@
 #include "CodeComplete.h"
 #include "CodeCompletionStrings.h"
 #include "ExpectedTypes.h"
+#include "FindSymbols.h"
 #include "SourceCode.h"
 #include "URI.h"
 #include "clang-include-cleaner/Analysis.h"
@@ -428,8 +429,7 @@ class SymbolCollector::HeaderFileURICache {
       CachePathToFrameworkSpelling.erase(Res.first);
       return std::nullopt;
     }
-    if (auto UmbrellaSpelling =
-            getFrameworkUmbrellaSpelling(HS, *HeaderPath)) {
+    if (auto UmbrellaSpelling = getFrameworkUmbrellaSpelling(HS, *HeaderPath)) {
       *CachedHeaderSpelling = *UmbrellaSpelling;
       return llvm::StringRef(*CachedHeaderSpelling);
     }
@@ -926,7 +926,7 @@ llvm::StringRef getStdHeader(const Symbol *S, const LangOptions &LangOpts) {
   tooling::stdlib::Lang Lang = tooling::stdlib::Lang::CXX;
   if (LangOpts.C11)
     Lang = tooling::stdlib::Lang::C;
-  else if(!LangOpts.CPlusPlus)
+  else if (!LangOpts.CPlusPlus)
     return "";
 
   if (S->Scope == "std::" && S->Name == "move") {
@@ -1046,8 +1046,7 @@ void SymbolCollector::finish() {
         // depending on the translation unit.
         else if (tooling::isSelfContainedHeader(H.physical(), SM,
                                                 PP->getHeaderSearchInfo()))
-          SpellingIt->second =
-              HeaderFileURIs->toURI(H.physical());
+          SpellingIt->second = HeaderFileURIs->toURI(H.physical());
       } else {
         SpellingIt->second = include_cleaner::spellHeader(
             {H, PP->getHeaderSearchInfo(),
@@ -1126,6 +1125,9 @@ const Symbol *SymbolCollector::addDeclaration(const NamedDecl &ND, SymbolID ID,
       S.Documentation = Documentation;
     }
   };
+
+  S.Tags = computeSymbolTags(ND);
+
   if (!(S.Flags & Symbol::IndexedForCodeCompletion)) {
     if (Opts.StoreAllDocumentation)
       UpdateDoc();
@@ -1192,7 +1194,7 @@ void SymbolCollector::addDefinition(const NamedDecl &ND, const Symbol &DeclSym,
       S.Flags |= Symbol::HasDocComment;
     S.Documentation = Documentation;
   }
-
+  S.Tags |= computeSymbolTags(ND);
   Symbols.insert(S);
 }
 
diff --git a/clang-tools-extra/clangd/index/YAMLSerialization.cpp b/clang-tools-extra/clangd/index/YAMLSerialization.cpp
index 495d8a2ff487a..6109761d2e8ed 100644
--- a/clang-tools-extra/clangd/index/YAMLSerialization.cpp
+++ b/clang-tools-extra/clangd/index/YAMLSerialization.cpp
@@ -246,6 +246,7 @@ template <> struct MappingTraits<Symbol> {
     IO.mapOptional("Documentation", Sym.Documentation);
     IO.mapOptional("ReturnType", Sym.ReturnType);
     IO.mapOptional("Type", Sym.Type);
+    IO.mapOptional("Tags", Sym.Tags, clang::clangd::SymbolTags(0));
     IO.mapOptional("IncludeHeaders", NIncludeHeaders->Headers);
   }
 };
diff --git a/clang-tools-extra/clangd/test/Inputs/symbols.test.yaml b/clang-tools-extra/clangd/test/Inputs/symbols.test.yaml
index 6d457ad8ad498..4ddc5d9c2ea30 100644
--- a/clang-tools-extra/clangd/test/Inputs/symbols.test.yaml
+++ b/clang-tools-extra/clangd/test/Inputs/symbols.test.yaml
@@ -6,7 +6,8 @@ Scope:           'std::'
 SymInfo:         
   Kind:            Class
   Lang:            Cpp
-CanonicalDeclaration: 
+Tags:            262144
+CanonicalDeclaration:
   FileURI:         'test:///vector.h'
   Start:           
     Line:            215
diff --git a/clang-tools-extra/clangd/unittests/FindSymbolsTests.cpp b/clang-tools-extra/clangd/unittests/FindSymbolsTests.cpp
index 63fb76e330d8f..a14c32b25282c 100644
--- a/clang-tools-extra/clangd/unittests/FindSymbolsTests.cpp
+++ b/clang-tools-extra/clangd/unittests/FindSymbolsTests.cpp
@@ -7,8 +7,10 @@
 //===----------------------------------------------------------------------===//
 #include "Annotations.h"
 #include "FindSymbols.h"
+#include "SyncAPI.h"
 #include "TestFS.h"
 #include "TestTU.h"
+#include "index/FileIndex.h"
 #include "llvm/ADT/StringRef.h"
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
@@ -1331,6 +1333,78 @@ TEST(DocumentSymbolsTest, SymbolTagsCompilation) {
                                             SymbolTag::Implements))))));
 }
 
+TEST(DocumentSymbolsTest, SymbolTagsWithIndexing) {
+  // Test that verifies symbol tags are correctly set when the AST is indexed
+  // through FileIndex, which triggers the full indexing path through
+  // SymbolCollector::addDeclaration where S.Tags = computeSymbolTags(ND)
+  TestTU TU;
+  Annotations Main(R"cpp(
+    class A {
+      public:
+        virtual ~A() = default;
+        virtual void f1() = 0;
+        void f2() const;
+      protected:
+        void f3(){}
+      private:
+        static void f4(){}
+    };
+
+    void A::f2() const {}
+
+    class B final: public A {
+      public:
+        void f1() final {}
+    };
+    )cpp");
+
+  TU.Code = Main.code().str();
+  auto AST = TU.build();
+
+  // This path goes through:
+  //   FileIndex::updateMain() -> indexMainDecls() -> indexSymbols() ->
+  //   SymbolCollector -> addDeclaration() -> S.Tags = computeSymbolTags(ND)
+
+  FileIndex Index{false};
+  Index.updateMain(testPath(TU.Filename), AST);
+  // Verify that the index contains symbols with correct tags
+  // Note: We can't directly inspect Symbol.Tags from the index in this test,
+  // but the fact that updateMain() completes successfully demonstrates that:
+  // 1. SymbolCollector::addDeclaration() was called for each decl
+  // 2. computeSymbolTags() was executed and S.Tags was set
+  // 3. The full indexing pipeline works with our tag implementation
+
+  auto Indexed = runFuzzyFind(Index, "");
+  EXPECT_FALSE(Indexed.empty());
+
+  auto FindByQName = [&](llvm::StringRef QName) -> const Symbol * {
+    for (const auto &S : Indexed) {
+      if ((S.Scope + S.Name).str() == QName.str())
+        return &S;
+    }
+    return nullptr;
+  };
+
+  const Symbol *A = FindByQName("A");
+  ASSERT_TRUE(A);
+  EXPECT_THAT(expandTagBitmask(A->Tags),
+              UnorderedElementsAre(SymbolTag::Abstract, SymbolTag::Declaration,
+                                   SymbolTag::Definition));
+
+  const Symbol *B = FindByQName("B");
+  ASSERT_TRUE(B);
+  EXPECT_THAT(expandTagBitmask(B->Tags),
+              UnorderedElementsAre(SymbolTag::Final, SymbolTag::Declaration,
+                                   SymbolTag::Definition));
+  const Symbol *Bf1 = FindByQName("B::f1");
+  ASSERT_TRUE(Bf1);
+  EXPECT_THAT(expandTagBitmask(Bf1->Tags),
+              UnorderedElementsAre(SymbolTag::Public, SymbolTag::Final,
+                                   SymbolTag::Virtual, SymbolTag::Declaration,
+                                   SymbolTag::Definition,
+                                   SymbolTag::Implements));
+}
+
 } // namespace
 } // namespace clangd
 } // namespace clang

>From d6865ccdc3669a117e6a674e8ac529d17519091b Mon Sep 17 00:00:00 2001
From: Dimitri Ratz <dimitri.ratz at protonmail.com>
Date: Thu, 19 Mar 2026 14:18:21 +0100
Subject: [PATCH 13/17] Change: removed AST usage in call/type hierarchy

---
 clang-tools-extra/clangd/ClangdLSPServer.cpp  | 67 +++++++------
 clang-tools-extra/clangd/ClangdLSPServer.h    |  9 +-
 clang-tools-extra/clangd/ClangdServer.cpp     | 73 ++++++--------
 clang-tools-extra/clangd/ClangdServer.h       | 12 +--
 clang-tools-extra/clangd/Protocol.cpp         |  2 +-
 clang-tools-extra/clangd/XRefs.cpp            | 91 ++++++------------
 clang-tools-extra/clangd/XRefs.h              | 17 ++--
 .../clangd/index/SymbolCollector.cpp          |  8 +-
 .../clangd/test/type-hierarchy-ext.test       |  8 +-
 .../clangd/test/type-hierarchy.test           |  8 +-
 .../clangd/unittests/CallHierarchyTests.cpp   | 94 +++++++++----------
 .../clangd/unittests/TypeHierarchyTests.cpp   | 20 ++--
 12 files changed, 176 insertions(+), 233 deletions(-)

diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp
index d8a1a089beb8c..ebd42abd2dd61 100644
--- a/clang-tools-extra/clangd/ClangdLSPServer.cpp
+++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp
@@ -136,9 +136,10 @@ CodeAction toCodeAction(const Fix &F, const URIForFile &File,
     Edit.textDocument = VersionedTextDocumentIdentifier{{File}, Version};
     for (const auto &E : F.Edits)
       Edit.edits.push_back(
-          {E.range, E.newText, SupportChangeAnnotation ? E.annotationId : ""});
+          {E.range, E.newText,
+           SupportChangeAnnotation ? E.annotationId : ""});
     if (SupportChangeAnnotation) {
-      for (const auto &[AID, Annotation] : F.Annotations)
+      for (const auto &[AID, Annotation]: F.Annotations)
         Action.edit->changeAnnotations[AID] = Annotation;
     }
   }
@@ -908,24 +909,24 @@ void ClangdLSPServer::onRename(const RenameParams &Params,
   if (!Server->getDraft(File))
     return Reply(llvm::make_error<LSPError>(
         "onRename called for non-added file", ErrorCode::InvalidParams));
-  Server->rename(
-      File, Params.position, Params.newName, Opts.Rename,
-      [File, Params, Reply = std::move(Reply),
-       this](llvm::Expected<RenameResult> R) mutable {
-        if (!R)
-          return Reply(R.takeError());
-        if (auto Err = validateEdits(*Server, R->GlobalChanges))
-          return Reply(std::move(Err));
-        WorkspaceEdit Result;
-        // FIXME: use documentChanges if SupportDocumentChanges is
-        // true.
-        Result.changes.emplace();
-        for (const auto &Rep : R->GlobalChanges) {
-          (*Result.changes)[URI::createFile(Rep.first()).toString()] =
-              Rep.second.asTextEdits();
-        }
-        Reply(Result);
-      });
+  Server->rename(File, Params.position, Params.newName, Opts.Rename,
+                 [File, Params, Reply = std::move(Reply),
+                  this](llvm::Expected<RenameResult> R) mutable {
+                   if (!R)
+                     return Reply(R.takeError());
+                   if (auto Err = validateEdits(*Server, R->GlobalChanges))
+                     return Reply(std::move(Err));
+                   WorkspaceEdit Result;
+                   // FIXME: use documentChanges if SupportDocumentChanges is
+                   // true.
+                   Result.changes.emplace();
+                   for (const auto &Rep : R->GlobalChanges) {
+                     (*Result
+                           .changes)[URI::createFile(Rep.first()).toString()] =
+                         Rep.second.asTextEdits();
+                   }
+                   Reply(Result);
+                 });
 }
 
 void ClangdLSPServer::onDocumentDidClose(
@@ -1069,7 +1070,7 @@ void ClangdLSPServer::onCodeAction(const CodeActionParams &Params,
   std::map<ClangdServer::DiagRef, clangd::Diagnostic> ToLSPDiags;
   ClangdServer::CodeActionInputs Inputs;
 
-  for (const auto &LSPDiag : Params.context.diagnostics) {
+  for (const auto& LSPDiag : Params.context.diagnostics) {
     if (auto DiagRef = getDiagRef(File.file(), LSPDiag)) {
       ToLSPDiags[*DiagRef] = LSPDiag;
       Inputs.Diagnostics.push_back(*DiagRef);
@@ -1078,9 +1079,13 @@ void ClangdLSPServer::onCodeAction(const CodeActionParams &Params,
   Inputs.File = File.file();
   Inputs.Selection = Params.range;
   Inputs.RequestedActionKinds = Params.context.only;
-  Inputs.TweakFilter = [this](const Tweak &T) { return Opts.TweakFilter(T); };
-  auto CB = [this, Reply = std::move(Reply), ToLSPDiags = std::move(ToLSPDiags),
-             File, Selection = Params.range](
+  Inputs.TweakFilter = [this](const Tweak &T) {
+    return Opts.TweakFilter(T);
+  };
+  auto CB = [this,
+             Reply = std::move(Reply),
+             ToLSPDiags = std::move(ToLSPDiags), File,
+             Selection = Params.range](
                 llvm::Expected<ClangdServer::CodeActionResult> Fixits) mutable {
     if (!Fixits)
       return Reply(Fixits.takeError());
@@ -1089,7 +1094,8 @@ void ClangdLSPServer::onCodeAction(const CodeActionParams &Params,
     for (const auto &QF : Fixits->QuickFixes) {
       CAs.push_back(toCodeAction(QF.F, File, Version, SupportsDocumentChanges,
                                  SupportsChangeAnnotation));
-      if (auto It = ToLSPDiags.find(QF.Diag); It != ToLSPDiags.end()) {
+      if (auto It = ToLSPDiags.find(QF.Diag);
+          It != ToLSPDiags.end()) {
         CAs.back().diagnostics = {It->second};
       }
     }
@@ -1350,8 +1356,7 @@ void ClangdLSPServer::onResolveTypeHierarchy(
         }
         Reply(serializeTHIForExtension(std::move(**Resp)));
       };
-  Server->resolveTypeHierarchy(Params.item.uri.file(), Params.item,
-                               Params.resolve, Params.direction,
+  Server->resolveTypeHierarchy(Params.item, Params.resolve, Params.direction,
                                std::move(Serialize));
 }
 
@@ -1365,13 +1370,13 @@ void ClangdLSPServer::onPrepareTypeHierarchy(
 void ClangdLSPServer::onSuperTypes(
     const ResolveTypeHierarchyItemParams &Params,
     Callback<std::optional<std::vector<TypeHierarchyItem>>> Reply) {
-  Server->superTypes(Params.item.uri.file(), Params.item, std::move(Reply));
+  Server->superTypes(Params.item, std::move(Reply));
 }
 
 void ClangdLSPServer::onSubTypes(
     const ResolveTypeHierarchyItemParams &Params,
     Callback<std::vector<TypeHierarchyItem>> Reply) {
-  Server->subTypes(Params.item.uri.file(), Params.item, std::move(Reply));
+  Server->subTypes(Params.item, std::move(Reply));
 }
 
 void ClangdLSPServer::onPrepareCallHierarchy(
@@ -1384,7 +1389,7 @@ void ClangdLSPServer::onPrepareCallHierarchy(
 void ClangdLSPServer::onCallHierarchyIncomingCalls(
     const CallHierarchyIncomingCallsParams &Params,
     Callback<std::vector<CallHierarchyIncomingCall>> Reply) {
-  Server->incomingCalls(Params.item.uri.file(), Params.item, std::move(Reply));
+  Server->incomingCalls(Params.item, std::move(Reply));
 }
 
 void ClangdLSPServer::onClangdInlayHints(const InlayHintsParams &Params,
@@ -1429,7 +1434,7 @@ void ClangdLSPServer::onInlayHint(const InlayHintsParams &Params,
 void ClangdLSPServer::onCallHierarchyOutgoingCalls(
     const CallHierarchyOutgoingCallsParams &Params,
     Callback<std::vector<CallHierarchyOutgoingCall>> Reply) {
-  Server->outgoingCalls(Params.item.uri.file(), Params.item, std::move(Reply));
+  Server->outgoingCalls(Params.item, std::move(Reply));
 }
 
 void ClangdLSPServer::applyConfiguration(
diff --git a/clang-tools-extra/clangd/ClangdLSPServer.h b/clang-tools-extra/clangd/ClangdLSPServer.h
index 64dcbfbc55325..bd9c5e6bc6954 100644
--- a/clang-tools-extra/clangd/ClangdLSPServer.h
+++ b/clang-tools-extra/clangd/ClangdLSPServer.h
@@ -133,8 +133,7 @@ class ClangdLSPServer : private ClangdServer::Callbacks,
                   Callback<std::vector<Location>>);
   void onGoToImplementation(const TextDocumentPositionParams &,
                             Callback<std::vector<Location>>);
-  void onReference(const ReferenceParams &,
-                   Callback<std::vector<ReferenceLocation>>);
+  void onReference(const ReferenceParams &, Callback<std::vector<ReferenceLocation>>);
   void onSwitchSourceHeader(const TextDocumentIdentifier &,
                             Callback<std::optional<URIForFile>>);
   void onDocumentHighlight(const TextDocumentPositionParams &,
@@ -260,9 +259,11 @@ class ClangdLSPServer : private ClangdServer::Callbacks,
     return {LSPDiag.range, LSPDiag.message};
   }
   /// A map from LSP diagnostic to clangd-naive diagnostic.
-  typedef std::map<DiagKey, ClangdServer::DiagRef> DiagnosticToDiagRefMap;
+  typedef std::map<DiagKey, ClangdServer::DiagRef>
+      DiagnosticToDiagRefMap;
   /// Caches the mapping LSP and clangd-naive diagnostics per file.
-  llvm::StringMap<DiagnosticToDiagRefMap> DiagRefMap;
+  llvm::StringMap<DiagnosticToDiagRefMap>
+      DiagRefMap;
 
   // Last semantic-tokens response, for incremental requests.
   std::mutex SemanticTokensMutex;
diff --git a/clang-tools-extra/clangd/ClangdServer.cpp b/clang-tools-extra/clangd/ClangdServer.cpp
index e304db2914483..f1a87dd12d905 100644
--- a/clang-tools-extra/clangd/ClangdServer.cpp
+++ b/clang-tools-extra/clangd/ClangdServer.cpp
@@ -62,8 +62,8 @@ namespace clangd {
 namespace {
 
 // Tracks number of times a tweak has been offered.
-static constexpr trace::Metric
-    TweakAvailable("tweak_available", trace::Metric::Counter, "tweak_id");
+static constexpr trace::Metric TweakAvailable(
+    "tweak_available", trace::Metric::Counter, "tweak_id");
 
 // Update the FileIndex with new ASTs and plumb the diagnostics responses.
 struct UpdateIndexCallbacks : public ParsingCallbacks {
@@ -872,42 +872,29 @@ void ClangdServer::typeHierarchy(PathRef File, Position Pos, int Resolve,
 }
 
 void ClangdServer::superTypes(
-    PathRef File, const TypeHierarchyItem &Item,
+    const TypeHierarchyItem &Item,
     Callback<std::optional<std::vector<TypeHierarchyItem>>> CB) {
-  auto Action = [File = File.str(), Item, CB = std::move(CB),
-                 this](llvm::Expected<InputsAndAST> InpAST) mutable {
-    if (!InpAST)
-      return CB(InpAST.takeError());
-    CB(clangd::superTypes(Item, Index, InpAST->AST));
-  };
-  WorkScheduler->runWithAST("superTypes Calls", File, std::move(Action));
+  WorkScheduler->run("typeHierarchy/superTypes", /*Path=*/"",
+                     [=, CB = std::move(CB)]() mutable {
+                       CB(clangd::superTypes(Item, Index));
+                     });
 }
 
-void ClangdServer::subTypes(PathRef File, const TypeHierarchyItem &Item,
+void ClangdServer::subTypes(const TypeHierarchyItem &Item,
                             Callback<std::vector<TypeHierarchyItem>> CB) {
-
-  auto Action = [File = File.str(), Item, CB = std::move(CB),
-                 this](llvm::Expected<InputsAndAST> InpAST) mutable {
-    if (!InpAST)
-      return CB(InpAST.takeError());
-    CB(clangd::subTypes(Item, Index, InpAST->AST));
-  };
-  WorkScheduler->runWithAST("subTypes Calls", File, std::move(Action));
+  WorkScheduler->run(
+      "typeHierarchy/subTypes", /*Path=*/"",
+      [=, CB = std::move(CB)]() mutable { CB(clangd::subTypes(Item, Index)); });
 }
 
 void ClangdServer::resolveTypeHierarchy(
-    PathRef File, TypeHierarchyItem Item, int Resolve,
-    TypeHierarchyDirection Direction,
+    TypeHierarchyItem Item, int Resolve, TypeHierarchyDirection Direction,
     Callback<std::optional<TypeHierarchyItem>> CB) {
-  auto Action = [Item = std::move(Item), Resolve, Direction, CB = std::move(CB),
-                 this](llvm::Expected<InputsAndAST> InpAST) mutable {
-    if (!InpAST)
-      return CB(InpAST.takeError());
-    clangd::resolveTypeHierarchy(Item, Resolve, Direction, Index, InpAST->AST);
-    CB(Item);
-  };
-  WorkScheduler->runWithAST("resolveTypeHierarchy Calls", File,
-                            std::move(Action));
+  WorkScheduler->run(
+      "Resolve Type Hierarchy", "", [=, CB = std::move(CB)]() mutable {
+        clangd::resolveTypeHierarchy(Item, Resolve, Direction, Index);
+        CB(Item);
+      });
 }
 
 void ClangdServer::prepareCallHierarchy(
@@ -922,15 +909,12 @@ void ClangdServer::prepareCallHierarchy(
 }
 
 void ClangdServer::incomingCalls(
-    PathRef File, const CallHierarchyItem &Item,
+    const CallHierarchyItem &Item,
     Callback<std::vector<CallHierarchyIncomingCall>> CB) {
-  auto Action = [Item, CB = std::move(CB),
-                 this](llvm::Expected<InputsAndAST> InpAST) mutable {
-    if (!InpAST)
-      return CB(InpAST.takeError());
-    CB(clangd::incomingCalls(Item, Index, InpAST->AST));
-  };
-  WorkScheduler->runWithAST("Incoming Calls", File, std::move(Action));
+  WorkScheduler->run("Incoming Calls", "",
+                     [CB = std::move(CB), Item, this]() mutable {
+                       CB(clangd::incomingCalls(Item, Index));
+                     });
 }
 
 void ClangdServer::inlayHints(PathRef File, std::optional<Range> RestrictRange,
@@ -945,15 +929,12 @@ void ClangdServer::inlayHints(PathRef File, std::optional<Range> RestrictRange,
 }
 
 void ClangdServer::outgoingCalls(
-    PathRef File, const CallHierarchyItem &Item,
+    const CallHierarchyItem &Item,
     Callback<std::vector<CallHierarchyOutgoingCall>> CB) {
-  auto Action = [Item, CB = std::move(CB),
-                 this](llvm::Expected<InputsAndAST> InpAST) mutable {
-    if (!InpAST)
-      return CB(InpAST.takeError());
-    CB(clangd::outgoingCalls(Item, Index, InpAST->AST));
-  };
-  WorkScheduler->runWithAST("Outgoing Calls", File, std::move(Action));
+  WorkScheduler->run("Outgoing Calls", "",
+                     [CB = std::move(CB), Item, this]() mutable {
+                       CB(clangd::outgoingCalls(Item, Index));
+                     });
 }
 
 void ClangdServer::onFileEvent(const DidChangeWatchedFilesParams &Params) {
diff --git a/clang-tools-extra/clangd/ClangdServer.h b/clang-tools-extra/clangd/ClangdServer.h
index c582a96691deb..3ffaf67553dce 100644
--- a/clang-tools-extra/clangd/ClangdServer.h
+++ b/clang-tools-extra/clangd/ClangdServer.h
@@ -283,14 +283,14 @@ class ClangdServer {
                      TypeHierarchyDirection Direction,
                      Callback<std::vector<TypeHierarchyItem>> CB);
   /// Get direct parents of a type hierarchy item.
-  void superTypes(PathRef File, const TypeHierarchyItem &Item,
+  void superTypes(const TypeHierarchyItem &Item,
                   Callback<std::optional<std::vector<TypeHierarchyItem>>> CB);
   /// Get direct children of a type hierarchy item.
-  void subTypes(PathRef File, const TypeHierarchyItem &Item,
+  void subTypes(const TypeHierarchyItem &Item,
                 Callback<std::vector<TypeHierarchyItem>> CB);
 
   /// Resolve type hierarchy item in the given direction.
-  void resolveTypeHierarchy(PathRef File, TypeHierarchyItem Item, int Resolve,
+  void resolveTypeHierarchy(TypeHierarchyItem Item, int Resolve,
                             TypeHierarchyDirection Direction,
                             Callback<std::optional<TypeHierarchyItem>> CB);
 
@@ -299,11 +299,11 @@ class ClangdServer {
                             Callback<std::vector<CallHierarchyItem>> CB);
 
   /// Resolve incoming calls for a given call hierarchy item.
-  void incomingCalls(PathRef File, const CallHierarchyItem &Item,
-                     Callback<std::vector<CallHierarchyIncomingCall>> CB);
+  void incomingCalls(const CallHierarchyItem &Item,
+                     Callback<std::vector<CallHierarchyIncomingCall>>);
 
   /// Resolve outgoing calls for a given call hierarchy item.
-  void outgoingCalls(PathRef File, const CallHierarchyItem &Item,
+  void outgoingCalls(const CallHierarchyItem &Item,
                      Callback<std::vector<CallHierarchyOutgoingCall>>);
 
   /// Resolve inlay hints for a given document.
diff --git a/clang-tools-extra/clangd/Protocol.cpp b/clang-tools-extra/clangd/Protocol.cpp
index 7d1b06db64e2b..778ac70584094 100644
--- a/clang-tools-extra/clangd/Protocol.cpp
+++ b/clang-tools-extra/clangd/Protocol.cpp
@@ -209,7 +209,7 @@ bool fromJSON(const llvm::json::Value &Params, ChangeAnnotation &R,
          O.map("needsConfirmation", R.needsConfirmation) &&
          O.mapOptional("description", R.description);
 }
-llvm::json::Value toJSON(const ChangeAnnotation &CA) {
+llvm::json::Value toJSON(const ChangeAnnotation & CA) {
   llvm::json::Object Result{{"label", CA.label}};
   if (CA.needsConfirmation)
     Result["needsConfirmation"] = *CA.needsConfirmation;
diff --git a/clang-tools-extra/clangd/XRefs.cpp b/clang-tools-extra/clangd/XRefs.cpp
index 95ab9d891837d..134b6a957aacc 100644
--- a/clang-tools-extra/clangd/XRefs.cpp
+++ b/clang-tools-extra/clangd/XRefs.cpp
@@ -1934,25 +1934,16 @@ const NamedDecl *getNamedDeclFromSymbol(const Symbol &Sym,
 
 static void fillSubTypes(const SymbolID &ID,
                          std::vector<TypeHierarchyItem> &SubTypes,
-                         const SymbolIndex *Index, int Levels, PathRef TUPath,
-                         const ParsedAST &AST) {
+                         const SymbolIndex *Index, int Levels, PathRef TUPath) {
   RelationsRequest Req;
   Req.Subjects.insert(ID);
   Req.Predicate = RelationKind::BaseOf;
-  Index->relations(Req, [&Levels, &Index, &SubTypes, &TUPath,
-                         &AST](const SymbolID &Subject, const Symbol &Object) {
-    std::optional<TypeHierarchyItem> ChildSym;
-
-    if (auto *ND = getNamedDeclFromSymbol(Object, AST)) {
-      ChildSym = declToTypeHierarchyItem(*ND, AST.tuPath());
-    } else {
-      ChildSym = symbolToTypeHierarchyItem(Object, TUPath);
-    }
-    if (ChildSym) {
+  Index->relations(Req, [&](const SymbolID &Subject, const Symbol &Object) {
+    if (std::optional<TypeHierarchyItem> ChildSym =
+            symbolToTypeHierarchyItem(Object, TUPath)) {
       if (Levels > 1) {
         ChildSym->children.emplace();
-        fillSubTypes(Object.ID, *ChildSym->children, Index, Levels - 1, TUPath,
-                     AST);
+        fillSubTypes(Object.ID, *ChildSym->children, Index, Levels - 1, TUPath);
       }
       SubTypes.emplace_back(std::move(*ChildSym));
     }
@@ -2175,10 +2166,10 @@ static QualType typeForNode(const ASTContext &Ctx, const HeuristicResolver *H,
   return QualType();
 }
 
-// Given a type targeted by the cursor, return one or more types that are more
-// interesting to target.
-static void unwrapFindType(QualType T, const HeuristicResolver *H,
-                           llvm::SmallVector<QualType> &Out) {
+// Given a type targeted by the cursor, return one or more types that are more interesting
+// to target.
+static void unwrapFindType(
+    QualType T, const HeuristicResolver* H, llvm::SmallVector<QualType>& Out) {
   if (T.isNull())
     return;
 
@@ -2217,8 +2208,8 @@ static void unwrapFindType(QualType T, const HeuristicResolver *H,
 }
 
 // Convenience overload, to allow calling this without the out-parameter
-static llvm::SmallVector<QualType> unwrapFindType(QualType T,
-                                                  const HeuristicResolver *H) {
+static llvm::SmallVector<QualType> unwrapFindType(
+    QualType T, const HeuristicResolver* H) {
   llvm::SmallVector<QualType> Result;
   unwrapFindType(T, H, Result);
   return Result;
@@ -2240,9 +2231,9 @@ std::vector<LocatedSymbol> findType(ParsedAST &AST, Position Pos,
     std::vector<LocatedSymbol> LocatedSymbols;
 
     // NOTE: unwrapFindType might return duplicates for something like
-    // unique_ptr<unique_ptr<T>>. Let's *not* remove them, because it gives you
-    // some information about the type you may have not known before (since
-    // unique_ptr<unique_ptr<T>> != unique_ptr<T>).
+    // unique_ptr<unique_ptr<T>>. Let's *not* remove them, because it gives you some
+    // information about the type you may have not known before
+    // (since unique_ptr<unique_ptr<T>> != unique_ptr<T>).
     for (const QualType &Type : unwrapFindType(
              typeForNode(AST.getASTContext(), AST.getHeuristicResolver(), N),
              AST.getHeuristicResolver()))
@@ -2335,8 +2326,7 @@ getTypeHierarchy(ParsedAST &AST, Position Pos, int ResolveLevels,
 
       if (Index) {
         if (auto ID = getSymbolID(CXXRD))
-          fillSubTypes(ID, *Result->children, Index, ResolveLevels, TUPath,
-                       AST);
+          fillSubTypes(ID, *Result->children, Index, ResolveLevels, TUPath);
       }
     }
     Results.emplace_back(std::move(*Result));
@@ -2346,9 +2336,10 @@ getTypeHierarchy(ParsedAST &AST, Position Pos, int ResolveLevels,
 }
 
 std::optional<std::vector<TypeHierarchyItem>>
-superTypes(const TypeHierarchyItem &Item, const SymbolIndex *Index,
-           const ParsedAST &AST) {
+superTypes(const TypeHierarchyItem &Item, const SymbolIndex *Index) {
   std::vector<TypeHierarchyItem> Results;
+  if (!Index)
+    return Results;
   if (!Item.data.parents)
     return std::nullopt;
   if (Item.data.parents->empty())
@@ -2359,14 +2350,8 @@ superTypes(const TypeHierarchyItem &Item, const SymbolIndex *Index,
     Req.IDs.insert(Parent.symbolID);
     IDToData[Parent.symbolID] = &Parent;
   }
-  Index->lookup(Req, [&Item, &Results, &IDToData, &AST](const Symbol &S) {
-    std::optional<TypeHierarchyItem> THI;
-    if (auto *ND = getNamedDeclFromSymbol(S, AST)) {
-      THI = declToTypeHierarchyItem(*ND, AST.tuPath());
-    } else {
-      THI = symbolToTypeHierarchyItem(S, Item.uri.file());
-    }
-    if (THI) {
+  Index->lookup(Req, [&Item, &Results, &IDToData](const Symbol &S) {
+    if (auto THI = symbolToTypeHierarchyItem(S, Item.uri.file())) {
       THI->data = *IDToData.lookup(S.ID);
       Results.emplace_back(std::move(*THI));
     }
@@ -2375,10 +2360,9 @@ superTypes(const TypeHierarchyItem &Item, const SymbolIndex *Index,
 }
 
 std::vector<TypeHierarchyItem> subTypes(const TypeHierarchyItem &Item,
-                                        const SymbolIndex *Index,
-                                        const ParsedAST &AST) {
+                                        const SymbolIndex *Index) {
   std::vector<TypeHierarchyItem> Results;
-  fillSubTypes(Item.data.symbolID, Results, Index, 1, Item.uri.file(), AST);
+  fillSubTypes(Item.data.symbolID, Results, Index, 1, Item.uri.file());
   for (auto &ChildSym : Results)
     ChildSym.data.parents = {Item.data};
   return Results;
@@ -2386,7 +2370,7 @@ std::vector<TypeHierarchyItem> subTypes(const TypeHierarchyItem &Item,
 
 void resolveTypeHierarchy(TypeHierarchyItem &Item, int ResolveLevels,
                           TypeHierarchyDirection Direction,
-                          const SymbolIndex *Index, const ParsedAST &AST) {
+                          const SymbolIndex *Index) {
   // We only support typeHierarchy/resolve for children, because for parents
   // we ignore ResolveLevels and return all levels of parents eagerly.
   if (!Index || Direction == TypeHierarchyDirection::Parents ||
@@ -2395,7 +2379,7 @@ void resolveTypeHierarchy(TypeHierarchyItem &Item, int ResolveLevels,
 
   Item.children.emplace();
   fillSubTypes(Item.data.symbolID, *Item.children, Index, ResolveLevels,
-               Item.uri.file(), AST);
+               Item.uri.file());
 }
 
 std::vector<CallHierarchyItem>
@@ -2425,10 +2409,8 @@ prepareCallHierarchy(ParsedAST &AST, Position Pos, PathRef TUPath) {
 }
 
 std::vector<CallHierarchyIncomingCall>
-incomingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index,
-              const ParsedAST &AST) {
+incomingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index) {
   std::vector<CallHierarchyIncomingCall> Results;
-
   if (!Index || Item.data.empty())
     return Results;
   auto ID = SymbolID::fromStr(Item.data);
@@ -2472,14 +2454,7 @@ incomingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index,
     Index->lookup(ContainerLookup, [&](const Symbol &Caller) {
       auto It = CallsIn.find(Caller.ID);
       assert(It != CallsIn.end());
-
-      std::optional<CallHierarchyItem> CHI;
-      if (auto *ND = getNamedDeclFromSymbol(Caller, AST)) {
-        CHI = declToCallHierarchyItem(*ND, AST.tuPath());
-      } else {
-        CHI = symbolToCallHierarchyItem(Caller, Item.uri.file());
-      }
-      if (CHI) {
+      if (auto CHI = symbolToCallHierarchyItem(Caller, Item.uri.file())) {
         std::vector<Range> FromRanges;
         for (const Location &L : It->second) {
           if (L.uri != CHI->uri) {
@@ -2516,8 +2491,7 @@ incomingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index,
 }
 
 std::vector<CallHierarchyOutgoingCall>
-outgoingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index,
-              const ParsedAST &AST) {
+outgoingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index) {
   std::vector<CallHierarchyOutgoingCall> Results;
   if (!Index || Item.data.empty())
     return Results;
@@ -2563,16 +2537,7 @@ outgoingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index,
 
     auto It = CallsOut.find(Callee.ID);
     assert(It != CallsOut.end());
-
-    std::optional<CallHierarchyItem> CHI;
-
-    if (auto *ND = getNamedDeclFromSymbol(Callee, AST)) {
-      CHI = declToCallHierarchyItem(*ND, AST.tuPath());
-    } else {
-      CHI = symbolToCallHierarchyItem(Callee, Item.uri.file());
-    }
-
-    if (CHI) {
+    if (auto CHI = symbolToCallHierarchyItem(Callee, Item.uri.file())) {
       std::vector<Range> FromRanges;
       for (const Location &L : It->second) {
         if (L.uri != Item.uri) {
diff --git a/clang-tools-extra/clangd/XRefs.h b/clang-tools-extra/clangd/XRefs.h
index f2652b5f274db..247e52314c3f9 100644
--- a/clang-tools-extra/clangd/XRefs.h
+++ b/clang-tools-extra/clangd/XRefs.h
@@ -132,31 +132,26 @@ std::vector<TypeHierarchyItem> getTypeHierarchy(
     const SymbolIndex *Index = nullptr, PathRef TUPath = PathRef{});
 
 /// Returns direct parents of a TypeHierarchyItem using SymbolIDs stored inside
-/// the item or using the AST.
+/// the item.
 std::optional<std::vector<TypeHierarchyItem>>
-superTypes(const TypeHierarchyItem &Item, const SymbolIndex *Index,
-           const ParsedAST &AST);
-
+superTypes(const TypeHierarchyItem &Item, const SymbolIndex *Index);
 /// Returns direct children of a TypeHierarchyItem.
 std::vector<TypeHierarchyItem> subTypes(const TypeHierarchyItem &Item,
-                                        const SymbolIndex *Index,
-                                        const ParsedAST &AST);
+                                        const SymbolIndex *Index);
 
 void resolveTypeHierarchy(TypeHierarchyItem &Item, int ResolveLevels,
                           TypeHierarchyDirection Direction,
-                          const SymbolIndex *Index, const ParsedAST &AST);
+                          const SymbolIndex *Index);
 
 /// Get call hierarchy information at \p Pos.
 std::vector<CallHierarchyItem>
 prepareCallHierarchy(ParsedAST &AST, Position Pos, PathRef TUPath);
 
 std::vector<CallHierarchyIncomingCall>
-incomingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index,
-              const ParsedAST &AST);
+incomingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index);
 
 std::vector<CallHierarchyOutgoingCall>
-outgoingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index,
-              const ParsedAST &AST);
+outgoingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index);
 
 /// Returns all decls that are referenced in the \p FD except local symbols.
 llvm::DenseSet<const Decl *> getNonLocalDeclRefs(ParsedAST &AST,
diff --git a/clang-tools-extra/clangd/index/SymbolCollector.cpp b/clang-tools-extra/clangd/index/SymbolCollector.cpp
index cd6fd53d310a9..87561f3268f0a 100644
--- a/clang-tools-extra/clangd/index/SymbolCollector.cpp
+++ b/clang-tools-extra/clangd/index/SymbolCollector.cpp
@@ -429,7 +429,8 @@ class SymbolCollector::HeaderFileURICache {
       CachePathToFrameworkSpelling.erase(Res.first);
       return std::nullopt;
     }
-    if (auto UmbrellaSpelling = getFrameworkUmbrellaSpelling(HS, *HeaderPath)) {
+    if (auto UmbrellaSpelling =
+            getFrameworkUmbrellaSpelling(HS, *HeaderPath)) {
       *CachedHeaderSpelling = *UmbrellaSpelling;
       return llvm::StringRef(*CachedHeaderSpelling);
     }
@@ -926,7 +927,7 @@ llvm::StringRef getStdHeader(const Symbol *S, const LangOptions &LangOpts) {
   tooling::stdlib::Lang Lang = tooling::stdlib::Lang::CXX;
   if (LangOpts.C11)
     Lang = tooling::stdlib::Lang::C;
-  else if (!LangOpts.CPlusPlus)
+  else if(!LangOpts.CPlusPlus)
     return "";
 
   if (S->Scope == "std::" && S->Name == "move") {
@@ -1046,7 +1047,8 @@ void SymbolCollector::finish() {
         // depending on the translation unit.
         else if (tooling::isSelfContainedHeader(H.physical(), SM,
                                                 PP->getHeaderSearchInfo()))
-          SpellingIt->second = HeaderFileURIs->toURI(H.physical());
+          SpellingIt->second =
+              HeaderFileURIs->toURI(H.physical());
       } else {
         SpellingIt->second = include_cleaner::spellHeader(
             {H, PP->getHeaderSearchInfo(),
diff --git a/clang-tools-extra/clangd/test/type-hierarchy-ext.test b/clang-tools-extra/clangd/test/type-hierarchy-ext.test
index d635b199d002c..983c7538088bf 100644
--- a/clang-tools-extra/clangd/test/type-hierarchy-ext.test
+++ b/clang-tools-extra/clangd/test/type-hierarchy-ext.test
@@ -17,11 +17,11 @@
 # CHECK-NEXT:        "name": "Child3",
 # CHECK-NEXT:        "range": {
 # CHECK-NEXT:          "end": {
-# CHECK-NEXT:            "character": 25,
+# CHECK-NEXT:            "character": 13,
 # CHECK-NEXT:            "line": 3
 # CHECK-NEXT:          },
 # CHECK-NEXT:          "start": {
-# CHECK-NEXT:            "character": 0,
+# CHECK-NEXT:            "character": 7,
 # CHECK-NEXT:            "line": 3
 # CHECK-NEXT:          }
 # CHECK-NEXT:        },
@@ -162,11 +162,11 @@
 # CHECK-NEXT:        "name": "Child4",
 # CHECK-NEXT:        "range": {
 # CHECK-NEXT:          "end": {
-# CHECK-NEXT:            "character": 25,
+# CHECK-NEXT:            "character": 13,
 # CHECK-NEXT:            "line": 4
 # CHECK-NEXT:          },
 # CHECK-NEXT:          "start": {
-# CHECK-NEXT:            "character": 0,
+# CHECK-NEXT:            "character": 7,
 # CHECK-NEXT:            "line": 4
 # CHECK-NEXT:          }
 # CHECK-NEXT:        },
diff --git a/clang-tools-extra/clangd/test/type-hierarchy.test b/clang-tools-extra/clangd/test/type-hierarchy.test
index c7b8a16ae51e2..c6079bc88662a 100644
--- a/clang-tools-extra/clangd/test/type-hierarchy.test
+++ b/clang-tools-extra/clangd/test/type-hierarchy.test
@@ -72,11 +72,11 @@
 # CHECK-NEXT:       "name": "Child1",
 # CHECK-NEXT:       "range": {
 # CHECK-NEXT:         "end": {
-# CHECK-NEXT:           "character": 25,
+# CHECK-NEXT:           "character": 13,
 # CHECK-NEXT:           "line": 1
 # CHECK-NEXT:         },
 # CHECK-NEXT:         "start": {
-# CHECK-NEXT:           "character": 0,
+# CHECK-NEXT:           "character": 7,
 # CHECK-NEXT:           "line": 1
 # CHECK-NEXT:         }
 # CHECK-NEXT:       },
@@ -127,11 +127,11 @@
 # CHECK-NEXT:       "name": "Child3",
 # CHECK-NEXT:       "range": {
 # CHECK-NEXT:         "end": {
-# CHECK-NEXT:           "character": 25,
+# CHECK-NEXT:           "character": 13,
 # CHECK-NEXT:           "line": 3
 # CHECK-NEXT:         },
 # CHECK-NEXT:         "start": {
-# CHECK-NEXT:           "character": 0,
+# CHECK-NEXT:           "character": 7,
 # CHECK-NEXT:           "line": 3
 # CHECK-NEXT:         }
 # CHECK-NEXT:       },
diff --git a/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp b/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp
index fb911ba07354a..b646ac4ce8515 100644
--- a/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp
+++ b/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp
@@ -95,12 +95,12 @@ TEST(CallHierarchy, IncomingOneFileCpp) {
   std::vector<CallHierarchyItem> Items =
       prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename));
   ASSERT_THAT(Items, ElementsAre(withName("callee")));
-  auto IncomingLevel1 = incomingCalls(Items[0], Index.get(), AST);
+  auto IncomingLevel1 = incomingCalls(Items[0], Index.get());
   ASSERT_THAT(
       IncomingLevel1,
       ElementsAre(AllOf(from(AllOf(withName("caller1"), withDetail("caller1"))),
                         iFromRanges(Source.range("Callee")))));
-  auto IncomingLevel2 = incomingCalls(IncomingLevel1[0].from, Index.get(), AST);
+  auto IncomingLevel2 = incomingCalls(IncomingLevel1[0].from, Index.get());
   ASSERT_THAT(
       IncomingLevel2,
       ElementsAre(AllOf(from(AllOf(withName("caller2"), withDetail("caller2"))),
@@ -109,13 +109,13 @@ TEST(CallHierarchy, IncomingOneFileCpp) {
                   AllOf(from(AllOf(withName("caller3"), withDetail("caller3"))),
                         iFromRanges(Source.range("Caller1C")))));
 
-  auto IncomingLevel3 = incomingCalls(IncomingLevel2[0].from, Index.get(), AST);
+  auto IncomingLevel3 = incomingCalls(IncomingLevel2[0].from, Index.get());
   ASSERT_THAT(
       IncomingLevel3,
       ElementsAre(AllOf(from(AllOf(withName("caller3"), withDetail("caller3"))),
                         iFromRanges(Source.range("Caller2")))));
 
-  auto IncomingLevel4 = incomingCalls(IncomingLevel3[0].from, Index.get(), AST);
+  auto IncomingLevel4 = incomingCalls(IncomingLevel3[0].from, Index.get());
   EXPECT_THAT(IncomingLevel4, IsEmpty());
 }
 
@@ -143,12 +143,12 @@ TEST(CallHierarchy, IncomingOneFileObjC) {
   std::vector<CallHierarchyItem> Items =
       prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename));
   ASSERT_THAT(Items, ElementsAre(withName("callee")));
-  auto IncomingLevel1 = incomingCalls(Items[0], Index.get(), AST);
+  auto IncomingLevel1 = incomingCalls(Items[0], Index.get());
   ASSERT_THAT(IncomingLevel1,
               ElementsAre(AllOf(from(AllOf(withName("caller1"),
                                            withDetail("MyClass::caller1"))),
                                 iFromRanges(Source.range("Callee")))));
-  auto IncomingLevel2 = incomingCalls(IncomingLevel1[0].from, Index.get(), AST);
+  auto IncomingLevel2 = incomingCalls(IncomingLevel1[0].from, Index.get());
   ASSERT_THAT(IncomingLevel2,
               ElementsAre(AllOf(from(AllOf(withName("caller2"),
                                            withDetail("MyClass::caller2"))),
@@ -158,13 +158,13 @@ TEST(CallHierarchy, IncomingOneFileObjC) {
                                            withDetail("MyClass::caller3"))),
                                 iFromRanges(Source.range("Caller1C")))));
 
-  auto IncomingLevel3 = incomingCalls(IncomingLevel2[0].from, Index.get(), AST);
+  auto IncomingLevel3 = incomingCalls(IncomingLevel2[0].from, Index.get());
   ASSERT_THAT(IncomingLevel3,
               ElementsAre(AllOf(from(AllOf(withName("caller3"),
                                            withDetail("MyClass::caller3"))),
                                 iFromRanges(Source.range("Caller2")))));
 
-  auto IncomingLevel4 = incomingCalls(IncomingLevel3[0].from, Index.get(), AST);
+  auto IncomingLevel4 = incomingCalls(IncomingLevel3[0].from, Index.get());
   EXPECT_THAT(IncomingLevel4, IsEmpty());
 }
 
@@ -190,18 +190,18 @@ TEST(CallHierarchy, IncomingIncludeOverrides) {
   std::vector<CallHierarchyItem> Items =
       prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename));
   ASSERT_THAT(Items, ElementsAre(withName("callee")));
-  auto IncomingLevel1 = incomingCalls(Items[0], Index.get(), AST);
+  auto IncomingLevel1 = incomingCalls(Items[0], Index.get());
   ASSERT_THAT(IncomingLevel1,
               ElementsAre(AllOf(from(AllOf(withName("Func"),
                                            withDetail("Implementation::Func"))),
                                 iFromRanges(Source.range("Callee")))));
-  auto IncomingLevel2 = incomingCalls(IncomingLevel1[0].from, Index.get(), AST);
+  auto IncomingLevel2 = incomingCalls(IncomingLevel1[0].from, Index.get());
   ASSERT_THAT(
       IncomingLevel2,
       ElementsAre(AllOf(from(AllOf(withName("Test"), withDetail("Test"))),
                         iFromRanges(Source.range("FuncCall")))));
 
-  auto IncomingLevel3 = incomingCalls(IncomingLevel2[0].from, Index.get(), AST);
+  auto IncomingLevel3 = incomingCalls(IncomingLevel2[0].from, Index.get());
   EXPECT_THAT(IncomingLevel3, IsEmpty());
 }
 
@@ -227,13 +227,13 @@ TEST(CallHierarchy, MainFileOnlyRef) {
   std::vector<CallHierarchyItem> Items =
       prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename));
   ASSERT_THAT(Items, ElementsAre(withName("callee")));
-  auto IncomingLevel1 = incomingCalls(Items[0], Index.get(), AST);
+  auto IncomingLevel1 = incomingCalls(Items[0], Index.get());
   ASSERT_THAT(
       IncomingLevel1,
       ElementsAre(AllOf(from(AllOf(withName("caller1"), withDetail("caller1"))),
                         iFromRanges(Source.range("Callee")))));
 
-  auto IncomingLevel2 = incomingCalls(IncomingLevel1[0].from, Index.get(), AST);
+  auto IncomingLevel2 = incomingCalls(IncomingLevel1[0].from, Index.get());
   EXPECT_THAT(
       IncomingLevel2,
       ElementsAre(AllOf(from(AllOf(withName("caller2"), withDetail("caller2"))),
@@ -262,7 +262,7 @@ TEST(CallHierarchy, IncomingQualified) {
   std::vector<CallHierarchyItem> Items =
       prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename));
   ASSERT_THAT(Items, ElementsAre(withName("Waldo::find")));
-  auto Incoming = incomingCalls(Items[0], Index.get(), AST);
+  auto Incoming = incomingCalls(Items[0], Index.get());
   EXPECT_THAT(
       Incoming,
       ElementsAre(
@@ -302,29 +302,29 @@ TEST(CallHierarchy, OutgoingOneFile) {
   std::vector<CallHierarchyItem> Items =
       prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename));
   ASSERT_THAT(Items, ElementsAre(withName("caller3")));
-  auto OugoingLevel1 = outgoingCalls(Items[0], Index.get(), AST);
+  auto OugoingLevel1 = outgoingCalls(Items[0], Index.get());
   ASSERT_THAT(
       OugoingLevel1,
-      ElementsAre(AllOf(to(AllOf(withName("Foo::caller1"),
-                                 withDetail("ns::Foo::caller1"))),
-                        oFromRanges(Source.range("Caller1C"))),
-                  AllOf(to(AllOf(withName("caller2"), withDetail("caller2"))),
-                        oFromRanges(Source.range("Caller2")))));
+      ElementsAre(
+          AllOf(to(AllOf(withName("caller1"), withDetail("ns::Foo::caller1"))),
+                oFromRanges(Source.range("Caller1C"))),
+          AllOf(to(AllOf(withName("caller2"), withDetail("caller2"))),
+                oFromRanges(Source.range("Caller2")))));
 
-  auto OutgoingLevel2 = outgoingCalls(OugoingLevel1[1].to, Index.get(), AST);
+  auto OutgoingLevel2 = outgoingCalls(OugoingLevel1[1].to, Index.get());
   ASSERT_THAT(
       OutgoingLevel2,
       ElementsAre(AllOf(
-          to(AllOf(withName("Foo::caller1"), withDetail("ns::Foo::caller1"))),
+          to(AllOf(withName("caller1"), withDetail("ns::Foo::caller1"))),
           oFromRanges(Source.range("Caller1A"), Source.range("Caller1B")))));
 
-  auto OutgoingLevel3 = outgoingCalls(OutgoingLevel2[0].to, Index.get(), AST);
+  auto OutgoingLevel3 = outgoingCalls(OutgoingLevel2[0].to, Index.get());
   ASSERT_THAT(
       OutgoingLevel3,
       ElementsAre(AllOf(to(AllOf(withName("callee"), withDetail("callee"))),
                         oFromRanges(Source.range("Callee")))));
 
-  auto OutgoingLevel4 = outgoingCalls(OutgoingLevel3[0].to, Index.get(), AST);
+  auto OutgoingLevel4 = outgoingCalls(OutgoingLevel3[0].to, Index.get());
   EXPECT_THAT(OutgoingLevel4, IsEmpty());
 }
 
@@ -402,14 +402,13 @@ TEST(CallHierarchy, MultiFileCpp) {
     std::vector<CallHierarchyItem> Items =
         prepareCallHierarchy(AST, Pos, TUPath);
     ASSERT_THAT(Items, ElementsAre(withName("callee")));
-    auto IncomingLevel1 = incomingCalls(Items[0], Index.get(), AST);
+    auto IncomingLevel1 = incomingCalls(Items[0], Index.get());
     ASSERT_THAT(IncomingLevel1,
                 ElementsAre(AllOf(from(AllOf(withName("caller1"),
                                              withDetail("nsa::caller1"))),
                                   iFromRanges(Caller1C.range()))));
 
-    auto IncomingLevel2 =
-        incomingCalls(IncomingLevel1[0].from, Index.get(), AST);
+    auto IncomingLevel2 = incomingCalls(IncomingLevel1[0].from, Index.get());
     ASSERT_THAT(
         IncomingLevel2,
         ElementsAre(
@@ -418,15 +417,13 @@ TEST(CallHierarchy, MultiFileCpp) {
             AllOf(from(AllOf(withName("caller3"), withDetail("nsa::caller3"))),
                   iFromRanges(Caller3C.range("Caller1")))));
 
-    auto IncomingLevel3 =
-        incomingCalls(IncomingLevel2[0].from, Index.get(), AST);
+    auto IncomingLevel3 = incomingCalls(IncomingLevel2[0].from, Index.get());
     ASSERT_THAT(IncomingLevel3,
                 ElementsAre(AllOf(from(AllOf(withName("caller3"),
                                              withDetail("nsa::caller3"))),
                                   iFromRanges(Caller3C.range("Caller2")))));
 
-    auto IncomingLevel4 =
-        incomingCalls(IncomingLevel3[0].from, Index.get(), AST);
+    auto IncomingLevel4 = incomingCalls(IncomingLevel3[0].from, Index.get());
     EXPECT_THAT(IncomingLevel4, IsEmpty());
   };
 
@@ -439,7 +436,7 @@ TEST(CallHierarchy, MultiFileCpp) {
         ElementsAre(AllOf(
             withName("caller3"),
             withFile(testPath(IsDeclaration ? "caller3.hh" : "caller3.cc")))));
-    auto OutgoingLevel1 = outgoingCalls(Items[0], Index.get(), AST);
+    auto OutgoingLevel1 = outgoingCalls(Items[0], Index.get());
     ASSERT_THAT(
         OutgoingLevel1,
         // fromRanges are interpreted in the context of Items[0]'s file.
@@ -453,19 +450,19 @@ TEST(CallHierarchy, MultiFileCpp) {
                   IsDeclaration ? oFromRanges()
                                 : oFromRanges(Caller3C.range("Caller2")))));
 
-    auto OutgoingLevel2 = outgoingCalls(OutgoingLevel1[1].to, Index.get(), AST);
+    auto OutgoingLevel2 = outgoingCalls(OutgoingLevel1[1].to, Index.get());
     ASSERT_THAT(OutgoingLevel2,
                 ElementsAre(AllOf(
                     to(AllOf(withName("caller1"), withDetail("nsa::caller1"))),
                     oFromRanges(Caller2C.range("A"), Caller2C.range("B")))));
 
-    auto OutgoingLevel3 = outgoingCalls(OutgoingLevel2[0].to, Index.get(), AST);
+    auto OutgoingLevel3 = outgoingCalls(OutgoingLevel2[0].to, Index.get());
     ASSERT_THAT(
         OutgoingLevel3,
         ElementsAre(AllOf(to(AllOf(withName("callee"), withDetail("callee"))),
                           oFromRanges(Caller1C.range()))));
 
-    auto OutgoingLevel4 = outgoingCalls(OutgoingLevel3[0].to, Index.get(), AST);
+    auto OutgoingLevel4 = outgoingCalls(OutgoingLevel3[0].to, Index.get());
     EXPECT_THAT(OutgoingLevel4, IsEmpty());
   };
 
@@ -562,13 +559,12 @@ TEST(CallHierarchy, IncomingMultiFileObjC) {
     std::vector<CallHierarchyItem> Items =
         prepareCallHierarchy(AST, Pos, TUPath);
     ASSERT_THAT(Items, ElementsAre(withName("callee")));
-    auto IncomingLevel1 = incomingCalls(Items[0], Index.get(), AST);
+    auto IncomingLevel1 = incomingCalls(Items[0], Index.get());
     ASSERT_THAT(IncomingLevel1,
                 ElementsAre(AllOf(from(withName("caller1")),
                                   iFromRanges(Caller1C.range()))));
 
-    auto IncomingLevel2 =
-        incomingCalls(IncomingLevel1[0].from, Index.get(), AST);
+    auto IncomingLevel2 = incomingCalls(IncomingLevel1[0].from, Index.get());
     ASSERT_THAT(IncomingLevel2,
                 ElementsAre(AllOf(from(withName("caller2")),
                                   iFromRanges(Caller2C.range("A"),
@@ -576,14 +572,12 @@ TEST(CallHierarchy, IncomingMultiFileObjC) {
                             AllOf(from(withName("caller3")),
                                   iFromRanges(Caller3C.range("Caller1")))));
 
-    auto IncomingLevel3 =
-        incomingCalls(IncomingLevel2[0].from, Index.get(), AST);
+    auto IncomingLevel3 = incomingCalls(IncomingLevel2[0].from, Index.get());
     ASSERT_THAT(IncomingLevel3,
                 ElementsAre(AllOf(from(withName("caller3")),
                                   iFromRanges(Caller3C.range("Caller2")))));
 
-    auto IncomingLevel4 =
-        incomingCalls(IncomingLevel3[0].from, Index.get(), AST);
+    auto IncomingLevel4 = incomingCalls(IncomingLevel3[0].from, Index.get());
     EXPECT_THAT(IncomingLevel4, IsEmpty());
   };
 
@@ -628,7 +622,7 @@ TEST(CallHierarchy, CallInLocalVarDecl) {
       prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename));
   ASSERT_THAT(Items, ElementsAre(withName("callee")));
 
-  auto Incoming = incomingCalls(Items[0], Index.get(), AST);
+  auto Incoming = incomingCalls(Items[0], Index.get());
   ASSERT_THAT(Incoming, ElementsAre(AllOf(from(withName("caller1")),
                                           iFromRanges(Source.range("call1"))),
                                     AllOf(from(withName("caller2")),
@@ -655,7 +649,7 @@ TEST(CallHierarchy, HierarchyOnField) {
   std::vector<CallHierarchyItem> Items =
       prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename));
   ASSERT_THAT(Items, ElementsAre(withName("var1")));
-  auto IncomingLevel1 = incomingCalls(Items[0], Index.get(), AST);
+  auto IncomingLevel1 = incomingCalls(Items[0], Index.get());
   ASSERT_THAT(IncomingLevel1,
               ElementsAre(AllOf(from(withName("caller")),
                                 iFromRanges(Source.range("Callee")))));
@@ -676,7 +670,7 @@ TEST(CallHierarchy, HierarchyOnVar) {
   std::vector<CallHierarchyItem> Items =
       prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename));
   ASSERT_THAT(Items, ElementsAre(withName("var")));
-  auto IncomingLevel1 = incomingCalls(Items[0], Index.get(), AST);
+  auto IncomingLevel1 = incomingCalls(Items[0], Index.get());
   ASSERT_THAT(IncomingLevel1,
               ElementsAre(AllOf(from(withName("caller")),
                                 iFromRanges(Source.range("Callee")))));
@@ -698,14 +692,14 @@ TEST(CallHierarchy, HierarchyOnEnumConstant) {
   std::vector<CallHierarchyItem> Items =
       prepareCallHierarchy(AST, Source.point("Heads"), testPath(TU.Filename));
   ASSERT_THAT(Items, ElementsAre(withName("heads")));
-  auto IncomingLevel1 = incomingCalls(Items[0], Index.get(), AST);
+  auto IncomingLevel1 = incomingCalls(Items[0], Index.get());
   ASSERT_THAT(IncomingLevel1,
               ElementsAre(AllOf(from(withName("caller")),
                                 iFromRanges(Source.range("CallerH")))));
   Items =
       prepareCallHierarchy(AST, Source.point("Tails"), testPath(TU.Filename));
   ASSERT_THAT(Items, ElementsAre(withName("tails")));
-  IncomingLevel1 = incomingCalls(Items[0], Index.get(), AST);
+  IncomingLevel1 = incomingCalls(Items[0], Index.get());
   ASSERT_THAT(IncomingLevel1,
               ElementsAre(AllOf(from(withName("caller")),
                                 iFromRanges(Source.range("CallerT")))));
@@ -730,7 +724,7 @@ TEST(CallHierarchy, CallInDifferentFileThanCaller) {
       prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename));
   ASSERT_THAT(Items, ElementsAre(withName("callee")));
 
-  auto Incoming = incomingCalls(Items[0], Index.get(), AST);
+  auto Incoming = incomingCalls(Items[0], Index.get());
 
   // The only call site is in the source file, which is a different file from
   // the declaration of the function containing the call, which is in the
@@ -758,7 +752,7 @@ TEST(CallHierarchy, IncomingCalls) {
       prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename));
   ASSERT_THAT(Items, ElementsAre(withName("callee")));
 
-  auto Incoming = incomingCalls(Items[0], Index.get(), AST);
+  auto Incoming = incomingCalls(Items[0], Index.get());
   EXPECT_THAT(
       Incoming,
       UnorderedElementsAre(AllOf(from(
@@ -784,7 +778,7 @@ TEST(CallHierarchy, OutgoingCalls) {
       prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename));
   ASSERT_THAT(Items, ElementsAre(withName("caller")));
 
-  auto Outgoing = outgoingCalls(Items[0], Index.get(), AST);
+  auto Outgoing = outgoingCalls(Items[0], Index.get());
   EXPECT_THAT(Outgoing, UnorderedElementsAre(AllOf(
                             to(AllOf(withName("callee"),
                                      withSymbolTags(SymbolTag::Declaration,
diff --git a/clang-tools-extra/clangd/unittests/TypeHierarchyTests.cpp b/clang-tools-extra/clangd/unittests/TypeHierarchyTests.cpp
index 21adbb6d0d3a3..754063ede2724 100644
--- a/clang-tools-extra/clangd/unittests/TypeHierarchyTests.cpp
+++ b/clang-tools-extra/clangd/unittests/TypeHierarchyTests.cpp
@@ -762,7 +762,7 @@ struct Child2b : Child1 {};
                            parentsNotResolved(), childrenNotResolved()))));
 
   resolveTypeHierarchy((*Result.front().children)[0], /*ResolveLevels=*/1,
-                       TypeHierarchyDirection::Children, Index.get(), AST);
+                       TypeHierarchyDirection::Children, Index.get());
 
   EXPECT_THAT(
       (*Result.front().children)[0],
@@ -776,10 +776,10 @@ struct Child2b : Child1 {};
 
 TEST(Standard, SubTypes) {
   Annotations Source(R"cpp(
-    struct Pare^nt1 {};
-    struct Parent2 {};
-    struct Child final : Parent1, Parent2 {};
-  )cpp");
+struct Pare^nt1 {};
+struct Parent2 {};
+struct Child final: Parent1, Parent2 {};
+)cpp");
 
   TestTU TU = TestTU::withCode(Source.code());
   auto AST = TU.build();
@@ -789,7 +789,7 @@ TEST(Standard, SubTypes) {
                                  TypeHierarchyDirection::Children, Index.get(),
                                  testPath(TU.Filename));
   ASSERT_THAT(Result, SizeIs(1));
-  auto Children = subTypes(Result.front(), Index.get(), AST);
+  auto Children = subTypes(Result.front(), Index.get());
 
   // Make sure parents are populated when getting children.
   // FIXME: This is partial.
@@ -805,9 +805,9 @@ TEST(Standard, SubTypes) {
 
 TEST(Standard, SuperTypes) {
   Annotations Source(R"cpp(
-    struct Parent {};
-    struct Chil^d : Parent {};
-  )cpp");
+struct Parent {};
+struct Chil^d : Parent {};
+)cpp");
 
   TestTU TU = TestTU::withCode(Source.code());
   auto AST = TU.build();
@@ -817,7 +817,7 @@ TEST(Standard, SuperTypes) {
                                  TypeHierarchyDirection::Children, Index.get(),
                                  testPath(TU.Filename));
   ASSERT_THAT(Result, SizeIs(1));
-  auto Parents = superTypes(Result.front(), Index.get(), AST);
+  auto Parents = superTypes(Result.front(), Index.get());
 
   EXPECT_THAT(Parents,
               Optional(UnorderedElementsAre(AllOf(

>From 66529e55481f7c6e039f3ffa2f36895c50e3559a Mon Sep 17 00:00:00 2001
From: Dimitri Ratz <dimitri.ratz at protonmail.com>
Date: Fri, 20 Mar 2026 13:00:33 +0100
Subject: [PATCH 14/17] Feat: serialize/de-serialize symbol tags.

- Bump index version to 21
- Updated sample index
- Added tests
---
 .../clangd/index/Serialization.cpp            |   4 +-
 .../index-serialization/Inputs/sample.idx     | Bin 470 -> 488 bytes
 .../clangd/unittests/SerializationTests.cpp   | 109 ++++++++++++++++++
 3 files changed, 112 insertions(+), 1 deletion(-)

diff --git a/clang-tools-extra/clangd/index/Serialization.cpp b/clang-tools-extra/clangd/index/Serialization.cpp
index f03839599612c..764ec7903aecb 100644
--- a/clang-tools-extra/clangd/index/Serialization.cpp
+++ b/clang-tools-extra/clangd/index/Serialization.cpp
@@ -328,6 +328,7 @@ void writeSymbol(const Symbol &Sym, const StringTableOut &Strings,
   writeVar(Strings.index(Sym.Documentation), OS);
   writeVar(Strings.index(Sym.ReturnType), OS);
   writeVar(Strings.index(Sym.Type), OS);
+  writeVar(Sym.Tags, OS);
 
   auto WriteInclude = [&](const Symbol::IncludeHeaderWithReferences &Include) {
     writeVar(Strings.index(Include.IncludeHeader), OS);
@@ -357,6 +358,7 @@ Symbol readSymbol(Reader &Data, llvm::ArrayRef<llvm::StringRef> Strings,
   Sym.Documentation = Data.consumeString(Strings);
   Sym.ReturnType = Data.consumeString(Strings);
   Sym.Type = Data.consumeString(Strings);
+  Sym.Tags = Data.consumeVar();
   if (!Data.consumeSize(Sym.IncludeHeaders))
     return Sym;
   for (auto &I : Sym.IncludeHeaders) {
@@ -457,7 +459,7 @@ readCompileCommand(Reader CmdReader, llvm::ArrayRef<llvm::StringRef> Strings) {
 // The current versioning scheme is simple - non-current versions are rejected.
 // If you make a breaking change, bump this version number to invalidate stored
 // data. Later we may want to support some backward compatibility.
-constexpr static uint32_t Version = 20;
+constexpr static uint32_t Version = 21;
 
 llvm::Expected<IndexFileIn> readRIFF(llvm::StringRef Data,
                                      SymbolOrigin Origin) {
diff --git a/clang-tools-extra/clangd/test/index-serialization/Inputs/sample.idx b/clang-tools-extra/clangd/test/index-serialization/Inputs/sample.idx
index 6368e7145b1e4d628708f40d684bc8db1ef7f94d..6a2ce7a87925fd3e5d22a5366bfdb19ee26275e6 100644
GIT binary patch
delta 333
zcmcb{{DQeY$kWa30V4y0bBbq0ZfZ#)3j+g#C=eHy6lG2T(sDq#iaAGn5Aq%|;9)TK
z68HG~elmN|af8l<jK}+uwu>xl?^3LoQ?l!PtMIoEsk076etobZG-$@r-Oo%fHZ<>j
z<yy&e{g4UcfkjgnEV3-E$f}Px?9x!_T<FDZkZ-!JeqFg2UuR(6#n78dZ|{8!H?o-N
zJkh7x<Q41AJr}#V&RhyJH(1k~>9h84`koekKH-dK#g(~9-9U#`RxAGeIVV(KkcpLv
zft`(=jT=k=Wk6s-!zM<C!iCHW4cFMjurmQY!Op_Y!UZNEDi|6X3>chUwj90kwmo?%
zI}<Yl0|zrZGZ&a(lw at FFV*n{(<X|-|lvp&gSMndwCKd(`7Je3CFu}k9waI9*0pl_N
D!NO_d

delta 297
zcmaFCe2uw2$kWa393umRbBbq0ZfZ#)3j+g#2oM*S6lHb;X%3)V#hkspj(p7qJS?9@
z>=^bvzu~MpFHvawlA<>nW*y(YvVP^a?b$Nxu-}eEes#mP-LH;rHS<gqdp~Qt3p3xh
zb={vt<XSbE4y>A*5Ey=PLT3B5udXL~XME?nCamISSJ}7jl-Z?+vh`}slPyI~mh`yo
zRhZ6S71$VgtIST~+A))**ZxIB9~Ng(iNC>6T$!6x2Xs_rwc^j8b3*k6CLR=xV<=q6
zywGrsO$-|o(5>t&>?~Yh0;moQoVOgk^0qyBDH{_r&^yfR%v at lCQIdgyje&uYgVnT9
eV$sZA$$tV&tSk&1Ec`6OV1j`Iq<yjz<1zq~17Di}

diff --git a/clang-tools-extra/clangd/unittests/SerializationTests.cpp b/clang-tools-extra/clangd/unittests/SerializationTests.cpp
index d18ae478c1653..6b95331c8f7c5 100644
--- a/clang-tools-extra/clangd/unittests/SerializationTests.cpp
+++ b/clang-tools-extra/clangd/unittests/SerializationTests.cpp
@@ -49,6 +49,7 @@ Scope:   'clang::'
     Line: 1
     Column: 1
 Flags:    129
+Tags:    258
 Documentation:    'Foo doc'
 ReturnType:    'int'
 IncludeHeaders:
@@ -160,6 +161,10 @@ TEST(SerializationTest, YAMLConversions) {
   EXPECT_EQ(static_cast<uint8_t>(Sym1.Flags), 129);
   EXPECT_TRUE(Sym1.Flags & Symbol::IndexedForCodeCompletion);
   EXPECT_FALSE(Sym1.Flags & Symbol::Deprecated);
+  // Tags: Deprecated (1<<1=2) | Static (1<<8=256) = 258
+  EXPECT_EQ(Sym1.Tags, 258u);
+  EXPECT_TRUE(Sym1.Tags & (1u << static_cast<unsigned>(SymbolTag::Deprecated)));
+  EXPECT_TRUE(Sym1.Tags & (1u << static_cast<unsigned>(SymbolTag::Static)));
   EXPECT_THAT(
       Sym1.IncludeHeaders,
       UnorderedElementsAre(
@@ -176,6 +181,7 @@ TEST(SerializationTest, YAMLConversions) {
             "file:///path/bar.h");
   EXPECT_FALSE(Sym2.Flags & Symbol::IndexedForCodeCompletion);
   EXPECT_TRUE(Sym2.Flags & Symbol::Deprecated);
+  EXPECT_EQ(Sym2.Tags, 0u); // no Tags in YAML → default zero
 
   ASSERT_TRUE(bool(ParsedYAML->Refs));
   EXPECT_THAT(
@@ -292,6 +298,109 @@ TEST(SerializationTest, SrcsTest) {
   }
 }
 
+// Verify that Symbol::Tags survive a full YAML -> RIFF -> read roundtrip.
+// This exercises both writeSymbol (new Tags field) and readSymbol.
+TEST(SerializationTest, TagsRoundTrip) {
+  // YAML fixture with a single symbol carrying non-zero Tags.
+  // Deprecated (SymbolTag=1) -> bit 1 -> 1<<1 = 2
+  // Static     (SymbolTag=8) -> bit 8 -> 1<<8 = 256
+  // Combined: 258
+  const char *TaggedYAML = R"(
+---
+!Symbol
+ID: AABBCCDDEEFF0011
+Name:   'Tagged'
+Scope:   'ns::'
+SymInfo:
+  Kind:            Function
+  Lang:            Cpp
+CanonicalDeclaration:
+  FileURI:        file:///tmp/tagged.h
+  Start:
+    Line: 0
+    Column: 0
+  End:
+    Line: 0
+    Column: 1
+Flags:    1
+Tags:    258
+...
+)";
+  const SymbolID TaggedID = cantFail(SymbolID::fromStr("AABBCCDDEEFF0011"));
+  constexpr SymbolTags ExpectedTags =
+      (1u << static_cast<unsigned>(SymbolTag::Deprecated)) | // bit 1  = 2
+      (1u << static_cast<unsigned>(SymbolTag::Static));      // bit 8  = 256
+  static_assert(ExpectedTags == 258u, "bitmask sanity check");
+
+  // ── Step 1: YAML deserialization ────────────────────────────────────────
+  auto FromYAML = readIndexFile(TaggedYAML);
+  ASSERT_TRUE(bool(FromYAML)) << FromYAML.takeError();
+  ASSERT_TRUE(bool(FromYAML->Symbols));
+  {
+    auto It = FromYAML->Symbols->find(TaggedID);
+    ASSERT_NE(It, FromYAML->Symbols->end())
+        << "symbol not found after YAML parse";
+    EXPECT_EQ(It->Tags, ExpectedTags)
+        << "Tags lost during YAML deserialization";
+  }
+
+  // ── Step 2: RIFF serialization + deserialization ────────────────────────
+  IndexFileOut Out(*FromYAML);
+  Out.Format = IndexFileFormat::RIFF;
+  std::string Serialized = llvm::to_string(Out);
+
+  auto FromRIFF = readIndexFile(Serialized);
+  ASSERT_TRUE(bool(FromRIFF)) << FromRIFF.takeError();
+  ASSERT_TRUE(bool(FromRIFF->Symbols));
+  {
+    auto It = FromRIFF->Symbols->find(TaggedID);
+    ASSERT_NE(It, FromRIFF->Symbols->end())
+        << "symbol not found after RIFF roundtrip";
+    EXPECT_EQ(It->Tags, ExpectedTags) << "Tags lost during RIFF serialization";
+    // Spot-check individual bits
+    EXPECT_TRUE(It->Tags &
+                (1u << static_cast<unsigned>(SymbolTag::Deprecated)));
+    EXPECT_TRUE(It->Tags & (1u << static_cast<unsigned>(SymbolTag::Static)));
+    EXPECT_FALSE(It->Tags & (1u << static_cast<unsigned>(SymbolTag::Abstract)));
+  }
+
+  // ── Step 3: Symbol with Tags=0 must survive too ─────────────────────────
+  const char *UntaggedYAML = R"(
+---
+!Symbol
+ID: 0000000000000001
+Name:   'Untagged'
+Scope:   ''
+SymInfo:
+  Kind:            Variable
+  Lang:            Cpp
+CanonicalDeclaration:
+  FileURI:        file:///tmp/untagged.h
+  Start:
+    Line: 0
+    Column: 0
+  End:
+    Line: 0
+    Column: 1
+Flags:    1
+...
+)";
+  auto FromUntaggedYAML = readIndexFile(UntaggedYAML);
+  ASSERT_TRUE(bool(FromUntaggedYAML)) << FromUntaggedYAML.takeError();
+  ASSERT_TRUE(bool(FromUntaggedYAML->Symbols));
+  IndexFileOut Out2(*FromUntaggedYAML);
+  Out2.Format = IndexFileFormat::RIFF;
+  auto FromUntaggedRIFF = readIndexFile(llvm::to_string(Out2));
+  ASSERT_TRUE(bool(FromUntaggedRIFF)) << FromUntaggedRIFF.takeError();
+  ASSERT_TRUE(bool(FromUntaggedRIFF->Symbols));
+  {
+    const SymbolID UntaggedID = cantFail(SymbolID::fromStr("0000000000000001"));
+    auto It = FromUntaggedRIFF->Symbols->find(UntaggedID);
+    ASSERT_NE(It, FromUntaggedRIFF->Symbols->end());
+    EXPECT_EQ(It->Tags, 0u) << "Tags must be zero for untagged symbol";
+  }
+}
+
 TEST(SerializationTest, CmdlTest) {
   auto In = readIndexFile(YAML);
   EXPECT_TRUE(bool(In)) << In.takeError();

>From fad6461f1c83cac88b195c4642478a670403e55b Mon Sep 17 00:00:00 2001
From: Dimitri Ratz <dimitri.ratz at protonmail.com>
Date: Mon, 23 Mar 2026 14:05:00 +0100
Subject: [PATCH 15/17] Review: code review adjustments.

- avoid returning empty vector
- return either a filled vector or nullpt meaning there are no parents
- removed obsolete function
- texts rewritten correctly
- tightening the check to < 32
---
 clang-tools-extra/clangd/FindSymbols.cpp      | 206 +++++++++---------
 clang-tools-extra/clangd/FindSymbols.h        |   4 +-
 clang-tools-extra/clangd/XRefs.cpp            |  41 +---
 clang-tools-extra/clangd/XRefs.h              |   3 +-
 clang-tools-extra/clangd/index/Symbol.h       |   2 +-
 .../clangd/index/YAMLSerialization.cpp        |   2 +-
 .../clangd/unittests/FindSymbolsTests.cpp     |  74 -------
 .../clangd/unittests/SymbolCollectorTests.cpp |  43 ++++
 8 files changed, 155 insertions(+), 220 deletions(-)

diff --git a/clang-tools-extra/clangd/FindSymbols.cpp b/clang-tools-extra/clangd/FindSymbols.cpp
index 7d87885c5c60c..c909e077a1d67 100644
--- a/clang-tools-extra/clangd/FindSymbols.cpp
+++ b/clang-tools-extra/clangd/FindSymbols.cpp
@@ -120,17 +120,17 @@ bool isAbstract(const Decl *D) {
 }
 
 // Indicates whether declaration D is virtual in cases where D is a method.
+// We want to treat a method as virtual if it is declared virtual, even if it
+// is not implemented in this class, or if it overrides/implements a
+// base-class method. This is because the "virtual" modifier is still relevant
+// to the method's behavior and how it should be highlighted, even if it is
+// not itself a virtual method in the strictest sense. For example, a method
+// that overrides a virtual method from a base class is still considered
+// virtual, even if it is not declared as such in the derived class.
+// Similarly, a method that implements a pure virtual method from a base class
+// is also considered virtual, even if it is not declared as such in the
+// derived class.
 bool isVirtual(const Decl *D) {
-  // We want to treat a method as virtual if it is declared virtual, even if it
-  // is not implemented in this class, or if it overrides/implements a
-  // base-class method. This is because the "virtual" modifier is still relevant
-  // to the method's behavior and how it should be highlighted, even if it is
-  // not itself a virtual method in the strictest sense. For example, a method
-  // that overrides a virtual method from a base class is still considered
-  // virtual, even if it is not declared as such in the derived class.
-  // Similarly, a method that implements a pure virtual method from a base class
-  // is also considered virtual, even if it is not declared as such in the
-  // derived class.
   if (const auto *CMD = llvm::dyn_cast<CXXMethodDecl>(D))
     return CMD->isVirtual();
   return false;
@@ -148,61 +148,34 @@ bool isFinal(const Decl *D) {
   return false;
 }
 
-// Indicates whether declaration D is a unique definition (as opposed to a
-// declaration).
-bool isUniqueDefinition(const NamedDecl *Decl) {
-  if (auto *Func = dyn_cast<FunctionDecl>(Decl))
-    return Func->isThisDeclarationADefinition();
-  if (auto *Klass = dyn_cast<CXXRecordDecl>(Decl))
-    return Klass->isThisDeclarationADefinition();
-  if (auto *Iface = dyn_cast<ObjCInterfaceDecl>(Decl))
-    return Iface->isThisDeclarationADefinition();
-  if (auto *Proto = dyn_cast<ObjCProtocolDecl>(Decl))
-    return Proto->isThisDeclarationADefinition();
-  if (auto *Var = dyn_cast<VarDecl>(Decl))
-    return Var->isThisDeclarationADefinition();
-  return isa<TemplateTypeParmDecl>(Decl) ||
-         isa<NonTypeTemplateParmDecl>(Decl) ||
-         isa<TemplateTemplateParmDecl>(Decl) || isa<ObjCCategoryDecl>(Decl) ||
-         isa<ObjCImplDecl>(Decl);
-}
-} // namespace
-
-SymbolTags toSymbolTagBitmask(const SymbolTag ST) {
-  return (1 << static_cast<unsigned>(ST));
-}
-
+// A method "overrides" if:
+// 1. It overrides at least one method
+// 2. At least one of the overridden methods is virtual (but NOT pure
+// virtual)
 bool isOverrides(const NamedDecl *ND) {
   if (const auto *MD = llvm::dyn_cast<CXXMethodDecl>(ND)) {
-    // A method "overrides" if:
-    // 1. It overrides at least one method
-    // 2. At least one of the overridden methods is virtual (but NOT pure
-    // virtual)
-
     if (MD->size_overridden_methods() == 0)
       return false;
 
-    for (const auto Overridden : MD->overridden_methods()) {
+    for (const auto *Overridden : MD->overridden_methods()) {
       // Check if the overridden method is virtual but not pure virtual
       if (Overridden->isVirtual() && !Overridden->isPureVirtual())
         return true;
     }
-    return false;
   }
   return false;
 }
 
+// A method "implements" pure virtual methods from base classes if:
+// 1. It overrides at least one method
+// 2. It is NOT itself pure virtual (i.e., it has a concrete implementation)
+// 3. ALL overridden methods are pure virtual
 bool isImplements(const NamedDecl *ND) {
   if (const auto *MD = llvm::dyn_cast<CXXMethodDecl>(ND)) {
-    // A method "implements" pure virtual methods from base classes if:
-    // 1. It overrides at least one method
-    // 2. It is NOT itself pure virtual (i.e., it has a concrete implementation)
-    // 3. ALL overridden methods are pure virtual
-
     if (MD->size_overridden_methods() == 0 || MD->isPureVirtual())
       return false;
 
-    for (const auto Overridden : MD->overridden_methods()) {
+    for (const auto *Overridden : MD->overridden_methods()) {
       if (!Overridden->isPureVirtual())
         return false;
     }
@@ -211,63 +184,28 @@ bool isImplements(const NamedDecl *ND) {
   return false;
 }
 
-SymbolTags computeSymbolTags(const NamedDecl &ND) {
-  SymbolTags Result = 0;
-  const auto IsDef = isUniqueDefinition(&ND);
-
-  if (ND.isDeprecated())
-    Result |= toSymbolTagBitmask(SymbolTag::Deprecated);
-
-  if (isConst(&ND))
-    Result |= toSymbolTagBitmask(SymbolTag::ReadOnly);
-
-  if (isStatic(&ND))
-    Result |= toSymbolTagBitmask(SymbolTag::Static);
-
-  if (isVirtual(&ND))
-    Result |= toSymbolTagBitmask(SymbolTag::Virtual);
-
-  if (isAbstract(&ND))
-    Result |= toSymbolTagBitmask(SymbolTag::Abstract);
-
-  if (isFinal(&ND))
-    Result |= toSymbolTagBitmask(SymbolTag::Final);
-
-  if (isOverrides(&ND))
-    Result |= toSymbolTagBitmask(SymbolTag::Overrides);
-
-  if (isImplements(&ND))
-    Result |= toSymbolTagBitmask(SymbolTag::Implements);
-
-  if (not isa<UnresolvedUsingValueDecl>(ND)) {
-    // Do not treat an UnresolvedUsingValueDecl as a declaration.
-    // It's more common to think of it as a reference to the
-    // underlying declaration.
-    Result |= toSymbolTagBitmask(SymbolTag::Declaration);
-
-    if (IsDef)
-      Result |= toSymbolTagBitmask(SymbolTag::Definition);
-  }
-
-  switch (ND.getAccess()) {
-  case AS_public:
-    Result |= toSymbolTagBitmask(SymbolTag::Public);
-    break;
-  case AS_protected:
-    Result |= toSymbolTagBitmask(SymbolTag::Protected);
-    break;
-  case AS_private:
-    Result |= toSymbolTagBitmask(SymbolTag::Private);
-    break;
-  default:
-    break;
-  }
-
-  return Result;
+// Indicates whether declaration D is a unique definition (as opposed to a
+// declaration).
+bool isUniqueDefinition(const NamedDecl *Decl) {
+  if (auto *Func = dyn_cast<FunctionDecl>(Decl))
+    return Func->isThisDeclarationADefinition();
+  if (auto *Klass = dyn_cast<CXXRecordDecl>(Decl))
+    return Klass->isThisDeclarationADefinition();
+  if (auto *Iface = dyn_cast<ObjCInterfaceDecl>(Decl))
+    return Iface->isThisDeclarationADefinition();
+  if (auto *Proto = dyn_cast<ObjCProtocolDecl>(Decl))
+    return Proto->isThisDeclarationADefinition();
+  if (auto *Var = dyn_cast<VarDecl>(Decl))
+    return Var->isThisDeclarationADefinition();
+  return isa<TemplateTypeParmDecl>(Decl) ||
+         isa<NonTypeTemplateParmDecl>(Decl) ||
+         isa<TemplateTemplateParmDecl>(Decl) || isa<ObjCCategoryDecl>(Decl) ||
+         isa<ObjCImplDecl>(Decl);
 }
 
 // Filter symbol tags based on the presence of other tags and the kind of
-// symbol. This is needed to avoid redundant tags.
+// symbol. This is needed to avoid redundant tags, e.g. final implies override,
+// override implies virtual, etc.
 SymbolTags filterSymbolTags(const NamedDecl &ND, const SymbolTags ST) {
   SymbolTags Result = ST;
 
@@ -321,11 +259,71 @@ SymbolTags filterSymbolTags(const NamedDecl &ND, const SymbolTags ST) {
   }
   return Result;
 }
+} // namespace
+
+SymbolTags toSymbolTagBitmask(const SymbolTag ST) {
+  return (1 << static_cast<unsigned>(ST));
+}
+
+SymbolTags computeSymbolTags(const NamedDecl &ND) {
+  SymbolTags Result = 0;
+  const auto IsDef = isUniqueDefinition(&ND);
+
+  if (ND.isDeprecated())
+    Result |= toSymbolTagBitmask(SymbolTag::Deprecated);
 
-std::vector<SymbolTag> expandTagBitmask(const SymbolTags symbolTags) {
+  if (isConst(&ND))
+    Result |= toSymbolTagBitmask(SymbolTag::ReadOnly);
+
+  if (isStatic(&ND))
+    Result |= toSymbolTagBitmask(SymbolTag::Static);
+
+  if (isVirtual(&ND))
+    Result |= toSymbolTagBitmask(SymbolTag::Virtual);
+
+  if (isAbstract(&ND))
+    Result |= toSymbolTagBitmask(SymbolTag::Abstract);
+
+  if (isOverrides(&ND))
+    Result |= toSymbolTagBitmask(SymbolTag::Overrides);
+
+  if (isFinal(&ND))
+    Result |= toSymbolTagBitmask(SymbolTag::Final);
+
+  if (isImplements(&ND))
+    Result |= toSymbolTagBitmask(SymbolTag::Implements);
+
+  if (not isa<UnresolvedUsingValueDecl>(ND)) {
+    // Do not treat an UnresolvedUsingValueDecl as a declaration.
+    // It's more common to think of it as a reference to the
+    // underlying declaration.
+    Result |= toSymbolTagBitmask(SymbolTag::Declaration);
+
+    if (IsDef)
+      Result |= toSymbolTagBitmask(SymbolTag::Definition);
+  }
+
+  switch (ND.getAccess()) {
+  case AS_public:
+    Result |= toSymbolTagBitmask(SymbolTag::Public);
+    break;
+  case AS_protected:
+    Result |= toSymbolTagBitmask(SymbolTag::Protected);
+    break;
+  case AS_private:
+    Result |= toSymbolTagBitmask(SymbolTag::Private);
+    break;
+  default:
+    break;
+  }
+
+  return Result;
+}
+
+std::vector<SymbolTag> expandTagBitmask(const SymbolTags STGS) {
   std::vector<SymbolTag> Tags;
 
-  if (symbolTags == 0)
+  if (STGS == 0)
     return Tags;
 
   // No filtering required since this function is only used for Symbols from the
@@ -339,7 +337,7 @@ std::vector<SymbolTag> expandTagBitmask(const SymbolTags symbolTags) {
   constexpr unsigned MaxTag = static_cast<unsigned>(SymbolTag::LastTag);
   for (unsigned I = MinTag; I <= MaxTag; ++I) {
     auto ST = static_cast<SymbolTag>(I);
-    if (symbolTags & toSymbolTagBitmask(ST))
+    if (STGS & toSymbolTagBitmask(ST))
       Tags.push_back(ST);
   }
   return Tags;
diff --git a/clang-tools-extra/clangd/FindSymbols.h b/clang-tools-extra/clangd/FindSymbols.h
index c13231fb644a0..1e34731ad336f 100644
--- a/clang-tools-extra/clangd/FindSymbols.h
+++ b/clang-tools-extra/clangd/FindSymbols.h
@@ -63,8 +63,8 @@ SymbolTags computeSymbolTags(const NamedDecl &ND);
 /// \p ND The declaration to get tags for.
 std::vector<SymbolTag> getSymbolTags(const NamedDecl &ND);
 
-/// Returns the symbol tags for the given declaration as a bitmask.
-std::vector<SymbolTag> expandTagBitmask(SymbolTags symbolTags);
+/// Expands a SymbolTags bitmask into individual SymbolTag values.
+std::vector<SymbolTag> expandTagBitmask(SymbolTags STGS);
 
 } // namespace clangd
 } // namespace clang
diff --git a/clang-tools-extra/clangd/XRefs.cpp b/clang-tools-extra/clangd/XRefs.cpp
index 134b6a957aacc..72226e420480c 100644
--- a/clang-tools-extra/clangd/XRefs.cpp
+++ b/clang-tools-extra/clangd/XRefs.cpp
@@ -1902,36 +1902,6 @@ symbolToCallHierarchyItem(const Symbol &S, PathRef TUPath) {
   return Result;
 }
 
-// Tries to find a NamedDecl in the AST that matches the given Symbol.
-// Returns nullptr if the symbol is not found in the current AST.
-const NamedDecl *getNamedDeclFromSymbol(const Symbol &Sym,
-                                        const ParsedAST &AST) {
-  // Try to convert the symbol to a location and find the decl at that location
-  auto SymLoc = symbolToLocation(Sym, AST.tuPath());
-  if (!SymLoc)
-    return nullptr;
-
-  // Check if the symbol location is in the main file
-  if (SymLoc->uri.file() != AST.tuPath())
-    return nullptr;
-
-  // Convert LSP position to source location
-  const auto &SM = AST.getSourceManager();
-  auto CurLoc = sourceLocationInMainFile(SM, SymLoc->range.start);
-  if (!CurLoc) {
-    llvm::consumeError(CurLoc.takeError());
-    return nullptr;
-  }
-
-  // Get all decls at this location
-  auto Decls = getDeclAtPosition(const_cast<ParsedAST &>(AST), *CurLoc, {});
-  if (Decls.empty())
-    return nullptr;
-
-  // Return the first decl (usually the most specific one)
-  return Decls[0];
-}
-
 static void fillSubTypes(const SymbolID &ID,
                          std::vector<TypeHierarchyItem> &SubTypes,
                          const SymbolIndex *Index, int Levels, PathRef TUPath) {
@@ -2337,26 +2307,23 @@ getTypeHierarchy(ParsedAST &AST, Position Pos, int ResolveLevels,
 
 std::optional<std::vector<TypeHierarchyItem>>
 superTypes(const TypeHierarchyItem &Item, const SymbolIndex *Index) {
-  std::vector<TypeHierarchyItem> Results;
-  if (!Index)
-    return Results;
-  if (!Item.data.parents)
+  if (!Index || !Item.data.parents)
     return std::nullopt;
-  if (Item.data.parents->empty())
-    return Results;
   LookupRequest Req;
   llvm::DenseMap<SymbolID, const TypeHierarchyItem::ResolveParams *> IDToData;
   for (const auto &Parent : *Item.data.parents) {
     Req.IDs.insert(Parent.symbolID);
     IDToData[Parent.symbolID] = &Parent;
   }
+  std::vector<TypeHierarchyItem> Results;
   Index->lookup(Req, [&Item, &Results, &IDToData](const Symbol &S) {
     if (auto THI = symbolToTypeHierarchyItem(S, Item.uri.file())) {
       THI->data = *IDToData.lookup(S.ID);
       Results.emplace_back(std::move(*THI));
     }
   });
-  return Results;
+  return Results.empty() ? std::nullopt
+                         : std::make_optional(std::move(Results));
 }
 
 std::vector<TypeHierarchyItem> subTypes(const TypeHierarchyItem &Item,
diff --git a/clang-tools-extra/clangd/XRefs.h b/clang-tools-extra/clangd/XRefs.h
index 247e52314c3f9..3cc35e5dfbd2c 100644
--- a/clang-tools-extra/clangd/XRefs.h
+++ b/clang-tools-extra/clangd/XRefs.h
@@ -132,7 +132,8 @@ std::vector<TypeHierarchyItem> getTypeHierarchy(
     const SymbolIndex *Index = nullptr, PathRef TUPath = PathRef{});
 
 /// Returns direct parents of a TypeHierarchyItem using SymbolIDs stored inside
-/// the item.
+/// the item. Returns nullopt if the item does not have parents or if
+/// the index is not provided.
 std::optional<std::vector<TypeHierarchyItem>>
 superTypes(const TypeHierarchyItem &Item, const SymbolIndex *Index);
 /// Returns direct children of a TypeHierarchyItem.
diff --git a/clang-tools-extra/clangd/index/Symbol.h b/clang-tools-extra/clangd/index/Symbol.h
index 45e6bebd04faa..9bce1a1ece7e5 100644
--- a/clang-tools-extra/clangd/index/Symbol.h
+++ b/clang-tools-extra/clangd/index/Symbol.h
@@ -28,7 +28,7 @@ LLVM_ENABLE_BITMASK_ENUMS_IN_NAMESPACE();
 /// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#symbolTag
 using SymbolTags = uint32_t;
 /// Ensure we have enough bits to represent all SymbolTag values.
-static_assert(static_cast<unsigned>(SymbolTag::LastTag) <= 32,
+static_assert(static_cast<unsigned>(SymbolTag::LastTag) < 32,
               "Too many SymbolTags to fit in uint32_t. Change to uint64_t if "
               "we ever have more than 32 tags.");
 
diff --git a/clang-tools-extra/clangd/index/YAMLSerialization.cpp b/clang-tools-extra/clangd/index/YAMLSerialization.cpp
index 6109761d2e8ed..4d6b5822ece72 100644
--- a/clang-tools-extra/clangd/index/YAMLSerialization.cpp
+++ b/clang-tools-extra/clangd/index/YAMLSerialization.cpp
@@ -246,7 +246,7 @@ template <> struct MappingTraits<Symbol> {
     IO.mapOptional("Documentation", Sym.Documentation);
     IO.mapOptional("ReturnType", Sym.ReturnType);
     IO.mapOptional("Type", Sym.Type);
-    IO.mapOptional("Tags", Sym.Tags, clang::clangd::SymbolTags(0));
+    IO.mapOptional("Tags", Sym.Tags);
     IO.mapOptional("IncludeHeaders", NIncludeHeaders->Headers);
   }
 };
diff --git a/clang-tools-extra/clangd/unittests/FindSymbolsTests.cpp b/clang-tools-extra/clangd/unittests/FindSymbolsTests.cpp
index a14c32b25282c..63fb76e330d8f 100644
--- a/clang-tools-extra/clangd/unittests/FindSymbolsTests.cpp
+++ b/clang-tools-extra/clangd/unittests/FindSymbolsTests.cpp
@@ -7,10 +7,8 @@
 //===----------------------------------------------------------------------===//
 #include "Annotations.h"
 #include "FindSymbols.h"
-#include "SyncAPI.h"
 #include "TestFS.h"
 #include "TestTU.h"
-#include "index/FileIndex.h"
 #include "llvm/ADT/StringRef.h"
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
@@ -1333,78 +1331,6 @@ TEST(DocumentSymbolsTest, SymbolTagsCompilation) {
                                             SymbolTag::Implements))))));
 }
 
-TEST(DocumentSymbolsTest, SymbolTagsWithIndexing) {
-  // Test that verifies symbol tags are correctly set when the AST is indexed
-  // through FileIndex, which triggers the full indexing path through
-  // SymbolCollector::addDeclaration where S.Tags = computeSymbolTags(ND)
-  TestTU TU;
-  Annotations Main(R"cpp(
-    class A {
-      public:
-        virtual ~A() = default;
-        virtual void f1() = 0;
-        void f2() const;
-      protected:
-        void f3(){}
-      private:
-        static void f4(){}
-    };
-
-    void A::f2() const {}
-
-    class B final: public A {
-      public:
-        void f1() final {}
-    };
-    )cpp");
-
-  TU.Code = Main.code().str();
-  auto AST = TU.build();
-
-  // This path goes through:
-  //   FileIndex::updateMain() -> indexMainDecls() -> indexSymbols() ->
-  //   SymbolCollector -> addDeclaration() -> S.Tags = computeSymbolTags(ND)
-
-  FileIndex Index{false};
-  Index.updateMain(testPath(TU.Filename), AST);
-  // Verify that the index contains symbols with correct tags
-  // Note: We can't directly inspect Symbol.Tags from the index in this test,
-  // but the fact that updateMain() completes successfully demonstrates that:
-  // 1. SymbolCollector::addDeclaration() was called for each decl
-  // 2. computeSymbolTags() was executed and S.Tags was set
-  // 3. The full indexing pipeline works with our tag implementation
-
-  auto Indexed = runFuzzyFind(Index, "");
-  EXPECT_FALSE(Indexed.empty());
-
-  auto FindByQName = [&](llvm::StringRef QName) -> const Symbol * {
-    for (const auto &S : Indexed) {
-      if ((S.Scope + S.Name).str() == QName.str())
-        return &S;
-    }
-    return nullptr;
-  };
-
-  const Symbol *A = FindByQName("A");
-  ASSERT_TRUE(A);
-  EXPECT_THAT(expandTagBitmask(A->Tags),
-              UnorderedElementsAre(SymbolTag::Abstract, SymbolTag::Declaration,
-                                   SymbolTag::Definition));
-
-  const Symbol *B = FindByQName("B");
-  ASSERT_TRUE(B);
-  EXPECT_THAT(expandTagBitmask(B->Tags),
-              UnorderedElementsAre(SymbolTag::Final, SymbolTag::Declaration,
-                                   SymbolTag::Definition));
-  const Symbol *Bf1 = FindByQName("B::f1");
-  ASSERT_TRUE(Bf1);
-  EXPECT_THAT(expandTagBitmask(Bf1->Tags),
-              UnorderedElementsAre(SymbolTag::Public, SymbolTag::Final,
-                                   SymbolTag::Virtual, SymbolTag::Declaration,
-                                   SymbolTag::Definition,
-                                   SymbolTag::Implements));
-}
-
 } // namespace
 } // namespace clangd
 } // namespace clang
diff --git a/clang-tools-extra/clangd/unittests/SymbolCollectorTests.cpp b/clang-tools-extra/clangd/unittests/SymbolCollectorTests.cpp
index 94116fca3cbb2..10059247aa7d7 100644
--- a/clang-tools-extra/clangd/unittests/SymbolCollectorTests.cpp
+++ b/clang-tools-extra/clangd/unittests/SymbolCollectorTests.cpp
@@ -7,6 +7,7 @@
 //===----------------------------------------------------------------------===//
 
 #include "Annotations.h"
+#include "FindSymbols.h"
 #include "TestFS.h"
 #include "TestTU.h"
 #include "URI.h"
@@ -1335,6 +1336,48 @@ TEST_F(SymbolCollectorTest, OverrideRelationsMultipleInheritance) {
                           OverriddenBy(CBar, DBar), OverriddenBy(CBaz, DBaz)));
 }
 
+TEST_F(SymbolCollectorTest, SymbolTagsWithIndexing) {
+  // Test that verifies symbol tags are correctly set when the AST is indexed
+  // through FileIndex, which triggers the full indexing path through
+  // SymbolCollector::addDeclaration where S.Tags = computeSymbolTags(ND)
+  std::string Header = R"cpp(
+    class A {
+      public:
+        virtual ~A() = default;
+        virtual void f1() = 0;
+        void f2() const;
+      protected:
+        void f3(){}
+      private:
+        static void f4(){}
+    };
+
+    void A::f2() const {}
+
+    class B final: public A {
+      public:
+        void f1() final {}
+    };
+    )cpp";
+
+  runSymbolCollector(Header, /*Main=*/"");
+  const Symbol &A = findSymbol(Symbols, "A");
+  EXPECT_THAT(expandTagBitmask(A.Tags),
+              UnorderedElementsAre(SymbolTag::Abstract, SymbolTag::Declaration,
+                                   SymbolTag::Definition));
+
+  const Symbol &B = findSymbol(Symbols, "B");
+  EXPECT_THAT(expandTagBitmask(B.Tags),
+              UnorderedElementsAre(SymbolTag::Final, SymbolTag::Declaration,
+                                   SymbolTag::Definition));
+  const Symbol &Bf1 = findSymbol(Symbols, "B::f1");
+  EXPECT_THAT(expandTagBitmask(Bf1.Tags),
+              UnorderedElementsAre(SymbolTag::Public, SymbolTag::Final,
+                                   SymbolTag::Virtual, SymbolTag::Declaration,
+                                   SymbolTag::Definition,
+                                   SymbolTag::Implements));
+}
+
 TEST_F(SymbolCollectorTest, ObjCOverrideRelationsSimpleInheritance) {
   std::string Header = R"cpp(
     @interface A

>From f99012b507cf99aa5daa71bc0ecbd1be96eb687e Mon Sep 17 00:00:00 2001
From: Dimitri Ratz <dimitri.ratz at protonmail.com>
Date: Tue, 24 Mar 2026 14:58:05 +0100
Subject: [PATCH 16/17] Change: relocated Tags to decrease padding.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

64-bit

The problematic areas are:

SymInfo (6 bytes) → followed by a 2-byte gap
Origin (2 bytes) → followed by a 2-byte gap before StringRef
Tags (4 bytes) → followed by a 4-byte gap before SmallVector
Flags at the end → 7 bytes end padding

32-bit

SymInfo → 2-byte gap
Origin → 2-byte gap
Flags at the end → 3 bytes end padding

This patch reduces paddings:
64-bit: 240 -> 232 (-8 Bytes, ~3.3%)
32-bit: 144 -> 140 (-4 Bytes, ~2.8%)
---
 clang-tools-extra/clangd/index/Symbol.h | 13 ++++++-------
 1 file changed, 6 insertions(+), 7 deletions(-)

diff --git a/clang-tools-extra/clangd/index/Symbol.h b/clang-tools-extra/clangd/index/Symbol.h
index 9bce1a1ece7e5..5f83953639a55 100644
--- a/clang-tools-extra/clangd/index/Symbol.h
+++ b/clang-tools-extra/clangd/index/Symbol.h
@@ -51,6 +51,8 @@ struct Symbol {
   SymbolID ID;
   /// The symbol information, like symbol kind.
   index::SymbolInfo SymInfo = index::SymbolInfo();
+  /// Where this symbol came from. Usually an index provides a constant value.
+  SymbolOrigin Origin = SymbolOrigin::Unknown;
   /// The unqualified name of the symbol, e.g. "bar" (for ns::bar).
   llvm::StringRef Name;
   /// The containing namespace. e.g. "" (global), "ns::" (top-level namespace).
@@ -70,8 +72,10 @@ struct Symbol {
   /// The number of translation units that reference this symbol from their main
   /// file. This number is only meaningful if aggregated in an index.
   unsigned References = 0;
-  /// Where this symbol came from. Usually an index provides a constant value.
-  SymbolOrigin Origin = SymbolOrigin::Unknown;
+  /// Symbol tags for LSP protocol (Deprecated, Static, Virtual, Abstract,
+  /// Final, ReadOnly, Public, Protected, Private, Declaration, Definition).
+  /// This is a bitmask where each bit represents a SymbolTag.
+  SymbolTags Tags = 0;
   /// A brief description of the symbol that can be appended in the completion
   /// candidate list. For example, "(X x, Y y) const" is a function signature.
   /// Only set when the symbol is indexed for completion.
@@ -97,11 +101,6 @@ struct Symbol {
   /// Only set when the symbol is indexed for completion.
   llvm::StringRef Type;
 
-  /// Symbol tags for LSP protocol (Deprecated, Static, Virtual, Abstract,
-  /// Final, ReadOnly, Public, Protected, Private, Declaration, Definition).
-  /// This is a bitmask where each bit represents a SymbolTag.
-  SymbolTags Tags = 0;
-
   enum IncludeDirective : uint8_t {
     Invalid = 0,
     /// `#include "header.h"`

>From dd4d9fcaaa7a68a0f90d553275bdc4ad96ed1f9d Mon Sep 17 00:00:00 2001
From: Dimitri Ratz <dimitri.ratz at protonmail.com>
Date: Mon, 13 Apr 2026 15:30:34 +0200
Subject: [PATCH 17/17] Review: code review adjustments.

[clangd] Refactor SymbolTag computation and filtering

- Move filterSymbolTags call into computeSymbolTags so that filtering
  is applied at the single point of tag computation

- Add enumIncrement<E> helper for type-safe enum iteration in
  getSymbolTags, replacing raw unsigned arithmetic.

- Align variable naming in getSymbolTags to LLVM CamelCase convention.

- In SymbolCollector::addDeclaration, move the S.Tags assignment to
  after the completion-snippet computation so that all completion data
  is available when tags are derived.

- Revise isVirtual so that it no longer treats overriding or
  implementing methods as virtual; only methods that are themselves
  declared virtual (without overriding/implementing a base) qualify.
  Update SemanticHighlighting and SymbolCollector tests accordingly.
---
 clang-tools-extra/clangd/FindSymbols.cpp      | 32 ++++++++-----------
 .../clangd/index/SymbolCollector.cpp          |  4 +--
 .../unittests/SemanticHighlightingTests.cpp   | 12 +++----
 .../clangd/unittests/SymbolCollectorTests.cpp |  2 --
 4 files changed, 20 insertions(+), 30 deletions(-)

diff --git a/clang-tools-extra/clangd/FindSymbols.cpp b/clang-tools-extra/clangd/FindSymbols.cpp
index c909e077a1d67..961f239002b2d 100644
--- a/clang-tools-extra/clangd/FindSymbols.cpp
+++ b/clang-tools-extra/clangd/FindSymbols.cpp
@@ -120,16 +120,6 @@ bool isAbstract(const Decl *D) {
 }
 
 // Indicates whether declaration D is virtual in cases where D is a method.
-// We want to treat a method as virtual if it is declared virtual, even if it
-// is not implemented in this class, or if it overrides/implements a
-// base-class method. This is because the "virtual" modifier is still relevant
-// to the method's behavior and how it should be highlighted, even if it is
-// not itself a virtual method in the strictest sense. For example, a method
-// that overrides a virtual method from a base class is still considered
-// virtual, even if it is not declared as such in the derived class.
-// Similarly, a method that implements a pure virtual method from a base class
-// is also considered virtual, even if it is not declared as such in the
-// derived class.
 bool isVirtual(const Decl *D) {
   if (const auto *CMD = llvm::dyn_cast<CXXMethodDecl>(D))
     return CMD->isVirtual();
@@ -317,6 +307,8 @@ SymbolTags computeSymbolTags(const NamedDecl &ND) {
     break;
   }
 
+  Result = filterSymbolTags(ND, Result);
+
   return Result;
 }
 
@@ -343,25 +335,27 @@ std::vector<SymbolTag> expandTagBitmask(const SymbolTags STGS) {
   return Tags;
 }
 
+template <typename E> constexpr E enumIncrement(E Value) {
+  return static_cast<E>(static_cast<std::underlying_type_t<E>>(Value) + 1);
+}
+
 std::vector<SymbolTag> getSymbolTags(const NamedDecl &ND) {
-  const auto symbolTags = computeSymbolTags(ND);
+  const auto SymbolTags = computeSymbolTags(ND);
   std::vector<SymbolTag> Tags;
 
-  if (symbolTags == 0)
+  if (SymbolTags == 0)
     return Tags;
 
   // Apply specific filter to the symbol tags.
-  const auto filteredTags = filterSymbolTags(ND, symbolTags);
+  const auto FilteredTags = filterSymbolTags(ND, SymbolTags);
 
   // Iterate through SymbolTag enum values and collect any that are present in
   // the bitmask. SymbolTag values are in the numeric range
   // [FirstTag .. LastTag].
-  constexpr unsigned MinTag = static_cast<unsigned>(SymbolTag::FirstTag);
-  constexpr unsigned MaxTag = static_cast<unsigned>(SymbolTag::LastTag);
-  for (unsigned I = MinTag; I <= MaxTag; ++I) {
-    auto ST = static_cast<SymbolTag>(I);
-    if (filteredTags & toSymbolTagBitmask(ST))
-      Tags.push_back(ST);
+  for (SymbolTag Tag = SymbolTag::FirstTag; Tag <= SymbolTag::LastTag;
+       Tag = enumIncrement(Tag)) {
+    if (FilteredTags & toSymbolTagBitmask(Tag))
+      Tags.push_back(Tag);
   }
   return Tags;
 }
diff --git a/clang-tools-extra/clangd/index/SymbolCollector.cpp b/clang-tools-extra/clangd/index/SymbolCollector.cpp
index 87561f3268f0a..ba0b7f12d7aa0 100644
--- a/clang-tools-extra/clangd/index/SymbolCollector.cpp
+++ b/clang-tools-extra/clangd/index/SymbolCollector.cpp
@@ -1127,9 +1127,6 @@ const Symbol *SymbolCollector::addDeclaration(const NamedDecl &ND, SymbolID ID,
       S.Documentation = Documentation;
     }
   };
-
-  S.Tags = computeSymbolTags(ND);
-
   if (!(S.Flags & Symbol::IndexedForCodeCompletion)) {
     if (Opts.StoreAllDocumentation)
       UpdateDoc();
@@ -1142,6 +1139,7 @@ const Symbol *SymbolCollector::addDeclaration(const NamedDecl &ND, SymbolID ID,
   getSignature(*CCS, &Signature, &SnippetSuffix, SymbolCompletion.Kind,
                SymbolCompletion.CursorKind);
   S.Signature = Signature;
+  S.Tags = computeSymbolTags(ND);
   S.CompletionSnippetSuffix = SnippetSuffix;
   std::string ReturnType = getReturnType(*CCS);
   S.ReturnType = ReturnType;
diff --git a/clang-tools-extra/clangd/unittests/SemanticHighlightingTests.cpp b/clang-tools-extra/clangd/unittests/SemanticHighlightingTests.cpp
index 94cecce1f038c..b48107ef6c334 100644
--- a/clang-tools-extra/clangd/unittests/SemanticHighlightingTests.cpp
+++ b/clang-tools-extra/clangd/unittests/SemanticHighlightingTests.cpp
@@ -670,11 +670,11 @@ sizeof...($TemplateParameter[[Elements]]);
       R"cpp(
       class $Class_def_abstract[[Abstract]] {
       public:
-        virtual void $Method_decl_abstract_virtual[[pure]]() = 0;
-        virtual void $Method_decl_virtual[[impl]]();
+        virtual void $Method_abstract[[pure]]() = 0;
+        virtual void $Method_virtual[[impl]]();
       };
       void $Function_def[[foo]]($Class_abstract[[Abstract]]* $Parameter_def[[A]]) {
-          $Parameter[[A]]->$Method_abstract_virtual[[pure]]();
+          $Parameter[[A]]->$Method_abstract[[pure]]();
           $Parameter[[A]]->$Method_virtual[[impl]]();
       }
       )cpp",
@@ -881,9 +881,9 @@ sizeof...($TemplateParameter[[Elements]]);
       )cpp",
       // override and final
       R"cpp(
-        class $Class_def_abstract[[Base]] { virtual void $Method_decl_abstract_virtual[[m]]() = 0; };
-        class $Class_def[[override]] : public $Class_abstract[[Base]] { void $Method_decl_virtual[[m]]() $Modifier[[override]]; };
-        class $Class_def[[final]] : public $Class[[override]] { void $Method_decl_virtual[[m]]() $Modifier[[override]] $Modifier[[final]]; };
+        class $Class_def_abstract[[Base]] { virtual void $Method_abstract[[m]]() = 0; };
+        class $Class_def[[override]] : public $Class_abstract[[Base]] { void $Method[[m]]() $Modifier[[override]]; };
+        class $Class_def[[final]] : public $Class[[override]] { void $Method[[m]]() $Modifier[[override]] $Modifier[[final]]; };
       )cpp",
       // Issue 1222: readonly modifier for generic parameter
       R"cpp(
diff --git a/clang-tools-extra/clangd/unittests/SymbolCollectorTests.cpp b/clang-tools-extra/clangd/unittests/SymbolCollectorTests.cpp
index 10059247aa7d7..c4b58c84c2957 100644
--- a/clang-tools-extra/clangd/unittests/SymbolCollectorTests.cpp
+++ b/clang-tools-extra/clangd/unittests/SymbolCollectorTests.cpp
@@ -1373,8 +1373,6 @@ TEST_F(SymbolCollectorTest, SymbolTagsWithIndexing) {
   const Symbol &Bf1 = findSymbol(Symbols, "B::f1");
   EXPECT_THAT(expandTagBitmask(Bf1.Tags),
               UnorderedElementsAre(SymbolTag::Public, SymbolTag::Final,
-                                   SymbolTag::Virtual, SymbolTag::Declaration,
-                                   SymbolTag::Definition,
                                    SymbolTag::Implements));
 }
 



More information about the cfe-commits mailing list