[clang-tools-extra] Determine the access method to a referenced symbol. (PR #172462)
Dimitri Ratz via cfe-commits
cfe-commits at lists.llvm.org
Sun Feb 15 13:27:06 PST 2026
https://github.com/ratzdi updated https://github.com/llvm/llvm-project/pull/172462
>From f4f37212f33182f0f65a0f9ae0f4db58f94dedf4 Mon Sep 17 00:00:00 2001
From: Dimitri Ratz <dimitri.ratz at thinkdigital.cc>
Date: Wed, 10 Dec 2025 16:20:56 +0100
Subject: [PATCH 1/7] Determine the access method to a referenced symbol.
The access method helps to know in a quick way, what is happening with the referenced symbol somewhere in the code, i.e.
is the referenced symbol used as a rvalue object to read a value from or as lvalue object to write some value into.
The basic access methods to a referenced symbol can be write and/or read.
---
clang-tools-extra/clangd/Protocol.cpp | 9 +-
clang-tools-extra/clangd/Protocol.h | 13 +++
clang-tools-extra/clangd/XRefs.cpp | 147 ++++++++++++++++++++++++--
3 files changed, 158 insertions(+), 11 deletions(-)
diff --git a/clang-tools-extra/clangd/Protocol.cpp b/clang-tools-extra/clangd/Protocol.cpp
index 560b8e00ed377..6027e60609fe2 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;
@@ -1478,6 +1478,10 @@ llvm::json::Value toJSON(SymbolTag Tag) {
return llvm::json::Value(static_cast<int>(Tag));
}
+llvm::json::Value toJSON(ReferenceTag Tag) {
+ return llvm::json::Value(static_cast<int>(Tag));
+}
+
llvm::json::Value toJSON(const CallHierarchyItem &I) {
llvm::json::Object Result{{"name", I.name},
{"kind", static_cast<int>(I.kind)},
@@ -1490,6 +1494,9 @@ llvm::json::Value toJSON(const CallHierarchyItem &I) {
Result["detail"] = I.detail;
if (!I.data.empty())
Result["data"] = I.data;
+ if (!I.referenceTags.empty())
+ Result["referenceTags"] = I.referenceTags;
+
return std::move(Result);
}
diff --git a/clang-tools-extra/clangd/Protocol.h b/clang-tools-extra/clangd/Protocol.h
index 2248572060431..edb28ef33d6a2 100644
--- a/clang-tools-extra/clangd/Protocol.h
+++ b/clang-tools-extra/clangd/Protocol.h
@@ -405,6 +405,13 @@ enum class SymbolKind {
Operator = 25,
TypeParameter = 26
};
+
+/// Tags describing the reference kind.
+enum class ReferenceTag {
+ Read = 1,
+ Write = 2,
+};
+
bool fromJSON(const llvm::json::Value &, SymbolKind &, llvm::json::Path);
constexpr auto SymbolKindMin = static_cast<size_t>(SymbolKind::File);
constexpr auto SymbolKindMax = static_cast<size_t>(SymbolKind::TypeParameter);
@@ -1513,6 +1520,9 @@ struct TypeHierarchyItem {
/// The kind of this item.
SymbolKind kind;
+ /// The symbol tags for this item.
+ std::vector<ReferenceTag> referenceTags;
+
/// More detail for this item, e.g. the signature of a function.
std::optional<std::string> detail;
@@ -1590,6 +1600,9 @@ struct CallHierarchyItem {
/// Tags for this item.
std::vector<SymbolTag> tags;
+ /// The tags describing reference kinds of this item.
+ std::vector<ReferenceTag> referenceTags;
+
/// More detaill for this item, e.g. the signature of a function.
std::string detail;
diff --git a/clang-tools-extra/clangd/XRefs.cpp b/clang-tools-extra/clangd/XRefs.cpp
index ef45acf501612..fd4dfe95213a5 100644
--- a/clang-tools-extra/clangd/XRefs.cpp
+++ b/clang-tools-extra/clangd/XRefs.cpp
@@ -68,6 +68,7 @@
#include "llvm/Support/ErrorHandling.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/raw_ostream.h"
+#include "support/Logger.h"
#include <algorithm>
#include <optional>
#include <string>
@@ -1749,6 +1750,130 @@ llvm::raw_ostream &operator<<(llvm::raw_ostream &OS,
return OS;
}
+// This would require a visitor pattern:
+class ParamUsageVisitor : public RecursiveASTVisitor<ParamUsageVisitor> {
+public:
+ bool HasWrite = false;
+ bool HasRead = false;
+ const ParmVarDecl *TargetParam;
+
+ ParamUsageVisitor(const ParmVarDecl *P) : TargetParam(P) {}
+
+ bool VisitUnaryOperator(UnaryOperator *UO) {
+ // Check for increment/decrement on parameter
+ if (UO->isIncrementDecrementOp()) {
+ if (auto *DRE = dyn_cast<DeclRefExpr>(UO->getSubExpr())) {
+ if (DRE->getDecl() == TargetParam) {
+ HasWrite = true;
+ }
+ }
+ }
+ return true;
+ }
+
+ bool VisitBinaryOperator(BinaryOperator *BO) {
+ // Check for assignment to parameter
+ if (BO->isAssignmentOp()) {
+ if (auto *DRE = dyn_cast<DeclRefExpr>(BO->getLHS())) {
+ if (DRE->getDecl() == TargetParam) {
+ HasWrite = true;
+ }
+ }
+ }
+ // Any other use is at least a read
+ if (auto *DRE = dyn_cast<DeclRefExpr>(BO->getRHS())) {
+ if (DRE->getDecl() == TargetParam) {
+ HasRead = true;
+ }
+ }
+ return true;
+ }
+};
+
+static std::vector<ReferenceTag> analyseParameterUsage(const FunctionDecl *FD) {
+ std::vector<ReferenceTag> Result;
+ // This requires more sophisticated analysis - checking if param is modified
+ const Stmt *Body = FD->getBody();
+ if (!Body)
+ return Result; // No definition available
+
+ for (unsigned I = 0; I < FD->getNumParams(); ++I) {
+ const ParmVarDecl *Param = FD->getParamDecl(I);
+
+ // Check const qualifier
+ // QualType ParamType = Param->getType();
+ // bool IsReadOnly = ParamType.isConstQualified();
+
+ // For deeper analysis, you'd need to:
+ // 1. Walk the AST of the function body
+ // 2. Find all references to the parameter
+ // 3. Check if they appear on the left side of assignments (write)
+ // or only on the right side (read)
+
+ ParamUsageVisitor Visitor(Param);
+ Visitor.TraverseStmt(const_cast<Stmt *>(Body));
+ if (Visitor.HasWrite)
+ Result.push_back(ReferenceTag::Write);
+ if (Visitor.HasRead)
+ Result.push_back(ReferenceTag::Read);
+ }
+ return Result;
+}
+
+template <typename HierarchyItem>
+static void determineParameterUsage(const NamedDecl &ND, HierarchyItem &HI) {
+ // Get parent context and check if it's a function parameter
+ const DeclContext *DC = ND.getDeclContext();
+ elog("determineParameterUsage: called for ND={0}", ND.getNameAsString());
+ if (const auto *TD = llvm::dyn_cast<TagDecl>(DC)) {
+ elog("determineParameterUsage: ND is inside a TagDecl: {0}", TD->getNameAsString());
+ // No parameter analysis for TagDecl parent contexts.
+ } else if (const auto *FD = llvm::dyn_cast<FunctionDecl>(DC)) {
+ elog("determineParameterUsage: ND is inside a FunctionDecl");
+ for (unsigned I = 0; I < FD->getNumParams(); ++I) {
+ if (FD->getParamDecl(I) == &ND) {
+ elog("determineParameterUsage: ND is the {0}-th parameter of function {1}", I, FD->getNameAsString());
+
+ const ParmVarDecl *Param = FD->getParamDecl(I);
+ QualType ParamType = Param->getType();
+
+ bool IsConst = false;
+ bool IsConstRef = false;
+ bool IsConstPtr = false;
+
+ // Check if const (read-only)
+ IsConst = ParamType.isConstQualified();
+ elog("determineParameterUsage: ParamType.isConstQualified() = {0}", IsConst);
+
+ // Check if it's a const reference
+ if (const auto *RT = ParamType->getAs<ReferenceType>()) {
+ IsConstRef = RT->getPointeeType().isConstQualified();
+ elog("determineParameterUsage: ParamType is ReferenceType, isConstQualified = {0}", IsConstRef);
+ }
+
+ // Check if it's a const pointer
+ if (const auto *PT = ParamType->getAs<PointerType>()) {
+ IsConstPtr = PT->getPointeeType().isConstQualified();
+ elog("determineParameterUsage: ParamType is PointerType, isConstQualified = {0}", IsConstPtr);
+ }
+
+ if (IsConst && IsConstRef && IsConstPtr) {
+ elog("determineParameterUsage: All const checks passed, marking as Read");
+ HI.referenceTags.push_back(ReferenceTag::Read);
+ } else {
+ elog("determineParameterUsage: Performing analyseParameterUsage");
+ HI.referenceTags = analyseParameterUsage(FD);
+ }
+
+ break;
+ }
+ elog("determineParameterUsage: ND is not parameter {0} of function {1}", I, FD->getNameAsString());
+ }
+ } else {
+ elog("determineParameterUsage: ND is not inside a FunctionDecl or TagDecl");
+ }
+}
+
template <typename HierarchyItem>
static std::optional<HierarchyItem>
declToHierarchyItem(const NamedDecl &ND, llvm::StringRef TUPath) {
@@ -1790,6 +1915,8 @@ declToHierarchyItem(const NamedDecl &ND, llvm::StringRef TUPath) {
HI.range = HI.selectionRange;
}
+ determineParameterUsage(ND, HI);
+
HI.uri = URIForFile::canonicalize(*FilePath, TUPath);
return HI;
@@ -2097,15 +2224,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.
@@ -2139,8 +2266,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;
@@ -2162,9 +2289,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()))
>From 2c2a42c3c95cedcde8bd06ff23c68efc753ea5ab Mon Sep 17 00:00:00 2001
From: Dimitri Ratz <dimitri.ratz at thinkdigital.cc>
Date: Fri, 2 Jan 2026 14:02:50 +0100
Subject: [PATCH 2/7] Added Visitor to traverse AST to detemine access type of
expressions.
---
clang-tools-extra/clangd/ClangdLSPServer.cpp | 2 +-
clang-tools-extra/clangd/ClangdServer.cpp | 13 +-
clang-tools-extra/clangd/ClangdServer.h | 2 +-
clang-tools-extra/clangd/Protocol.cpp | 2 +-
clang-tools-extra/clangd/XRefs.cpp | 174 +++++++++---------
clang-tools-extra/clangd/XRefs.h | 2 +-
.../clangd/unittests/CallHierarchyTests.cpp | 137 +++++++++++---
7 files changed, 212 insertions(+), 120 deletions(-)
diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp
index 1518f177b06a0..22aa6eeb581bd 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 ac1e9aa5f0ff1..4d8f3f4bd780d 100644
--- a/clang-tools-extra/clangd/ClangdServer.cpp
+++ b/clang-tools-extra/clangd/ClangdServer.cpp
@@ -908,12 +908,15 @@ void ClangdServer::prepareCallHierarchy(
}
void ClangdServer::incomingCalls(
- const CallHierarchyItem &Item,
+ 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 d45d13fa034b5..20ca6bcc221d0 100644
--- a/clang-tools-extra/clangd/ClangdServer.h
+++ b/clang-tools-extra/clangd/ClangdServer.h
@@ -299,7 +299,7 @@ class ClangdServer {
Callback<std::vector<CallHierarchyItem>> CB);
/// Resolve incoming calls for a given call hierarchy item.
- void incomingCalls(const CallHierarchyItem &Item,
+ void incomingCalls(PathRef File, const CallHierarchyItem &Item,
Callback<std::vector<CallHierarchyIncomingCall>>);
/// Resolve outgoing calls for a given call hierarchy item.
diff --git a/clang-tools-extra/clangd/Protocol.cpp b/clang-tools-extra/clangd/Protocol.cpp
index 6027e60609fe2..7786f62b4347d 100644
--- a/clang-tools-extra/clangd/Protocol.cpp
+++ b/clang-tools-extra/clangd/Protocol.cpp
@@ -1496,7 +1496,7 @@ llvm::json::Value toJSON(const CallHierarchyItem &I) {
Result["data"] = I.data;
if (!I.referenceTags.empty())
Result["referenceTags"] = I.referenceTags;
-
+
return std::move(Result);
}
diff --git a/clang-tools-extra/clangd/XRefs.cpp b/clang-tools-extra/clangd/XRefs.cpp
index fd4dfe95213a5..d3dacee945ff7 100644
--- a/clang-tools-extra/clangd/XRefs.cpp
+++ b/clang-tools-extra/clangd/XRefs.cpp
@@ -1755,10 +1755,18 @@ class ParamUsageVisitor : public RecursiveASTVisitor<ParamUsageVisitor> {
public:
bool HasWrite = false;
bool HasRead = false;
- const ParmVarDecl *TargetParam;
+ const ValueDecl *TargetParam;
- ParamUsageVisitor(const ParmVarDecl *P) : TargetParam(P) {}
+ ParamUsageVisitor(const ValueDecl *P) : TargetParam(P) {}
+ // Any reference to the target is at least a read, unless we later
+ // identify it specifically as a write (e.g. assignment/inc/dec on LHS).
+ bool VisitDeclRefExpr(DeclRefExpr *DRE) {
+ if (DRE->getDecl() == TargetParam)
+ HasRead = true;
+ return true;
+ }
+
bool VisitUnaryOperator(UnaryOperator *UO) {
// Check for increment/decrement on parameter
if (UO->isIncrementDecrementOp()) {
@@ -1774,14 +1782,17 @@ class ParamUsageVisitor : public RecursiveASTVisitor<ParamUsageVisitor> {
bool VisitBinaryOperator(BinaryOperator *BO) {
// Check for assignment to parameter
if (BO->isAssignmentOp()) {
- if (auto *DRE = dyn_cast<DeclRefExpr>(BO->getLHS())) {
+ if (isa<DeclRefExpr>(BO->getLHS())) {
+ auto *DRE = dyn_cast<DeclRefExpr>(BO->getLHS());
if (DRE->getDecl() == TargetParam) {
HasWrite = true;
}
}
}
+
// Any other use is at least a read
- if (auto *DRE = dyn_cast<DeclRefExpr>(BO->getRHS())) {
+ if (isa<DeclRefExpr>(BO->getRHS())) {
+ auto *DRE = dyn_cast<DeclRefExpr>(BO->getRHS());
if (DRE->getDecl() == TargetParam) {
HasRead = true;
}
@@ -1790,90 +1801,25 @@ class ParamUsageVisitor : public RecursiveASTVisitor<ParamUsageVisitor> {
}
};
-static std::vector<ReferenceTag> analyseParameterUsage(const FunctionDecl *FD) {
+static std::vector<ReferenceTag> analyseParameterUsage(const FunctionDecl *FD,
+ const ValueDecl *PVD) {
std::vector<ReferenceTag> Result;
- // This requires more sophisticated analysis - checking if param is modified
const Stmt *Body = FD->getBody();
if (!Body)
return Result; // No definition available
- for (unsigned I = 0; I < FD->getNumParams(); ++I) {
- const ParmVarDecl *Param = FD->getParamDecl(I);
+ // Walk the body and determine read/write usage of the referenced variable
+ // within this function.
+ ParamUsageVisitor Visitor(PVD);
+ Visitor.TraverseStmt(const_cast<Stmt *>(Body));
+ if (Visitor.HasWrite)
+ Result.push_back(ReferenceTag::Write);
+ if (Visitor.HasRead)
+ Result.push_back(ReferenceTag::Read);
- // Check const qualifier
- // QualType ParamType = Param->getType();
- // bool IsReadOnly = ParamType.isConstQualified();
-
- // For deeper analysis, you'd need to:
- // 1. Walk the AST of the function body
- // 2. Find all references to the parameter
- // 3. Check if they appear on the left side of assignments (write)
- // or only on the right side (read)
-
- ParamUsageVisitor Visitor(Param);
- Visitor.TraverseStmt(const_cast<Stmt *>(Body));
- if (Visitor.HasWrite)
- Result.push_back(ReferenceTag::Write);
- if (Visitor.HasRead)
- Result.push_back(ReferenceTag::Read);
- }
return Result;
}
-template <typename HierarchyItem>
-static void determineParameterUsage(const NamedDecl &ND, HierarchyItem &HI) {
- // Get parent context and check if it's a function parameter
- const DeclContext *DC = ND.getDeclContext();
- elog("determineParameterUsage: called for ND={0}", ND.getNameAsString());
- if (const auto *TD = llvm::dyn_cast<TagDecl>(DC)) {
- elog("determineParameterUsage: ND is inside a TagDecl: {0}", TD->getNameAsString());
- // No parameter analysis for TagDecl parent contexts.
- } else if (const auto *FD = llvm::dyn_cast<FunctionDecl>(DC)) {
- elog("determineParameterUsage: ND is inside a FunctionDecl");
- for (unsigned I = 0; I < FD->getNumParams(); ++I) {
- if (FD->getParamDecl(I) == &ND) {
- elog("determineParameterUsage: ND is the {0}-th parameter of function {1}", I, FD->getNameAsString());
-
- const ParmVarDecl *Param = FD->getParamDecl(I);
- QualType ParamType = Param->getType();
-
- bool IsConst = false;
- bool IsConstRef = false;
- bool IsConstPtr = false;
-
- // Check if const (read-only)
- IsConst = ParamType.isConstQualified();
- elog("determineParameterUsage: ParamType.isConstQualified() = {0}", IsConst);
-
- // Check if it's a const reference
- if (const auto *RT = ParamType->getAs<ReferenceType>()) {
- IsConstRef = RT->getPointeeType().isConstQualified();
- elog("determineParameterUsage: ParamType is ReferenceType, isConstQualified = {0}", IsConstRef);
- }
-
- // Check if it's a const pointer
- if (const auto *PT = ParamType->getAs<PointerType>()) {
- IsConstPtr = PT->getPointeeType().isConstQualified();
- elog("determineParameterUsage: ParamType is PointerType, isConstQualified = {0}", IsConstPtr);
- }
-
- if (IsConst && IsConstRef && IsConstPtr) {
- elog("determineParameterUsage: All const checks passed, marking as Read");
- HI.referenceTags.push_back(ReferenceTag::Read);
- } else {
- elog("determineParameterUsage: Performing analyseParameterUsage");
- HI.referenceTags = analyseParameterUsage(FD);
- }
-
- break;
- }
- elog("determineParameterUsage: ND is not parameter {0} of function {1}", I, FD->getNameAsString());
- }
- } else {
- elog("determineParameterUsage: ND is not inside a FunctionDecl or TagDecl");
- }
-}
-
template <typename HierarchyItem>
static std::optional<HierarchyItem>
declToHierarchyItem(const NamedDecl &ND, llvm::StringRef TUPath) {
@@ -1904,7 +1850,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())};
@@ -1915,7 +1861,7 @@ declToHierarchyItem(const NamedDecl &ND, llvm::StringRef TUPath) {
HI.range = HI.selectionRange;
}
- determineParameterUsage(ND, HI);
+ //determineParameterUsage(ND, HI);
HI.uri = URIForFile::canonicalize(*FilePath, TUPath);
@@ -2464,8 +2410,38 @@ prepareCallHierarchy(ParsedAST &AST, Position Pos, 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];
+}
+
std::vector<CallHierarchyIncomingCall>
-incomingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index) {
+incomingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index, ParsedAST &AST) {
std::vector<CallHierarchyIncomingCall> Results;
if (!Index || Item.data.empty())
return Results;
@@ -2474,6 +2450,24 @@ incomingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index) {
elog("incomingCalls failed to find symbol: {0}", ID.takeError());
return Results;
}
+
+
+ LookupRequest LR;
+ LR.IDs.insert(*ID);
+
+ std::optional<const NamedDecl*> PVD;
+ Index->lookup(LR, [&ID, &AST, &PVD](const Symbol &Sym) {
+ // This callback is called once per found symbol; here we expect exactly one
+ if (Sym.ID == *ID) {
+ PVD = getNamedDeclFromSymbol(Sym, AST);
+ }
+ });
+
+ if (PVD == nullptr || !PVD.has_value()) {
+ // Not found in index
+ return Results;
+ }
+
// In this function, we find incoming calls based on the index only.
// In principle, the AST could have more up-to-date information about
// occurrences within the current file. However, going from a SymbolID
@@ -2510,7 +2504,21 @@ 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())) {
+
+ std::optional<CallHierarchyItem> CHI;
+ if (auto *ND = getNamedDeclFromSymbol(Caller, AST)) {
+ CHI = declToCallHierarchyItem(*ND, AST.tuPath());
+ if (const auto *FD = llvm::dyn_cast<clang::FunctionDecl>(ND)) {
+ if (isa<ValueDecl>(PVD.value())) {
+ const auto *VD = llvm::dyn_cast<clang::ValueDecl>(PVD.value());
+ CHI->referenceTags = analyseParameterUsage(FD, VD); // FD is the caller of var
+ }
+ }
+ } else {
+ CHI = symbolToCallHierarchyItem(Caller, Item.uri.file());
+ }
+
+ if (CHI) {
std::vector<Range> FromRanges;
for (const Location &L : It->second) {
if (L.uri != CHI->uri) {
diff --git a/clang-tools-extra/clangd/XRefs.h b/clang-tools-extra/clangd/XRefs.h
index 247e52314c3f9..911cf72d72f03 100644
--- a/clang-tools-extra/clangd/XRefs.h
+++ b/clang-tools-extra/clangd/XRefs.h
@@ -148,7 +148,7 @@ 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, 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..ad5c7d06a0739 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,12 +664,93 @@ 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")))));
}
+TEST(CallHierarchy, HierarchyOnVarWithReadReference) {
+ // Tests that the call hierarchy works on non-local variables and read/write tags are set.
+ Annotations Source(R"cpp(
+ int v^ar = 1;
+ void caller() {
+ int x = 0;
+ x = var;
+ }
+ )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("var")));
+ auto IncomingLevel1 = incomingCalls(Items[0], Index.get(), AST);
+ EXPECT_TRUE(IncomingLevel1.front().from.referenceTags.at(0) == ReferenceTag::Read);
+}
+
+TEST(CallHierarchy, HierarchyOnVarWithWriteReference) {
+ // Tests that the call hierarchy works on non-local variables and read/write tags are set.
+ Annotations Source(R"cpp(
+ int v^ar = 1;
+ void caller() {
+ var = 2;
+ }
+ )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("var")));
+ auto IncomingLevel1 = incomingCalls(Items[0], Index.get(), AST);
+ EXPECT_TRUE(IncomingLevel1.front().from.referenceTags.at(0) == ReferenceTag::Write);
+}
+
+TEST(CallHierarchy, HierarchyOnVarWithUnaryWriteReference) {
+ // Tests that the call hierarchy works on non-local variables and read/write tags are set.
+ Annotations Source(R"cpp(
+ int v^ar = 1;
+ void caller() {
+ var++;
+ }
+ )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("var")));
+ auto IncomingLevel1 = incomingCalls(Items[0], Index.get(), AST);
+ EXPECT_TRUE(IncomingLevel1.front().from.referenceTags.at(0) == ReferenceTag::Write);
+}
+
+// TEST(CallHierarchy, HierarchyOnFieldWithReadWriteReference) {
+// // Tests that the call hierarchy works on non-local variables and read/write tags are set.
+// Annotations Source(R"cpp(
+// struct Vars {
+// int v^ar1 = 1;
+// };
+// void caller() {
+// Vars values;
+// values.var1 = 2;
+// }
+// )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("var1")));
+// auto IncomingLevel1 = incomingCalls(Items[0], Index.get(), AST);
+// EXPECT_TRUE(IncomingLevel1.front().from.referenceTags.at(0) == ReferenceTag::Read);
+// //EXPECT_TRUE(IncomingLevel1.front().from.referenceTags.at(1) == ReferenceTag::Read);
+// }
+
TEST(CallHierarchy, HierarchyOnEnumConstant) {
// Tests that the call hierarchy works on enum constants.
Annotations Source(R"cpp(
@@ -686,14 +767,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 +799,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 98e1167c36c1b4a3b62b779c3647d9ea6e99606a Mon Sep 17 00:00:00 2001
From: Dimitri Ratz <dimitri.ratz at thinkdigital.cc>
Date: Thu, 29 Jan 2026 19:09:15 +0100
Subject: [PATCH 3/7] Fix Decl check.
- Fix lit tests
- Fix unit tests.
---
clang-tools-extra/clangd/XRefs.cpp | 34 +++++----
clang-tools-extra/clangd/XRefs.h | 3 +-
.../clangd/test/call-hierarchy.test | 1 +
.../clangd/test/type-hierarchy-ext.test | 3 +
.../clangd/test/type-hierarchy.test | 1 +
.../clangd/unittests/CallHierarchyTests.cpp | 74 ++++++++++---------
6 files changed, 63 insertions(+), 53 deletions(-)
diff --git a/clang-tools-extra/clangd/XRefs.cpp b/clang-tools-extra/clangd/XRefs.cpp
index d3dacee945ff7..69947c77238f6 100644
--- a/clang-tools-extra/clangd/XRefs.cpp
+++ b/clang-tools-extra/clangd/XRefs.cpp
@@ -68,7 +68,6 @@
#include "llvm/Support/ErrorHandling.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/raw_ostream.h"
-#include "support/Logger.h"
#include <algorithm>
#include <optional>
#include <string>
@@ -1756,9 +1755,9 @@ class ParamUsageVisitor : public RecursiveASTVisitor<ParamUsageVisitor> {
bool HasWrite = false;
bool HasRead = false;
const ValueDecl *TargetParam;
-
+
ParamUsageVisitor(const ValueDecl *P) : TargetParam(P) {}
-
+
// Any reference to the target is at least a read, unless we later
// identify it specifically as a write (e.g. assignment/inc/dec on LHS).
bool VisitDeclRefExpr(DeclRefExpr *DRE) {
@@ -1778,7 +1777,6 @@ class ParamUsageVisitor : public RecursiveASTVisitor<ParamUsageVisitor> {
}
return true;
}
-
bool VisitBinaryOperator(BinaryOperator *BO) {
// Check for assignment to parameter
if (BO->isAssignmentOp()) {
@@ -1861,8 +1859,6 @@ declToHierarchyItem(const NamedDecl &ND, llvm::StringRef TUPath) {
HI.range = HI.selectionRange;
}
- //determineParameterUsage(ND, HI);
-
HI.uri = URIForFile::canonicalize(*FilePath, TUPath);
return HI;
@@ -2441,7 +2437,8 @@ const NamedDecl *getNamedDeclFromSymbol(const Symbol &Sym,
}
std::vector<CallHierarchyIncomingCall>
-incomingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index, ParsedAST &AST) {
+incomingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index,
+ ParsedAST &AST) {
std::vector<CallHierarchyIncomingCall> Results;
if (!Index || Item.data.empty())
return Results;
@@ -2451,20 +2448,22 @@ incomingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index, ParsedAST
return Results;
}
-
LookupRequest LR;
LR.IDs.insert(*ID);
- std::optional<const NamedDecl*> PVD;
- Index->lookup(LR, [&ID, &AST, &PVD](const Symbol &Sym) {
+ std::optional<const NamedDecl *> Decl;
+ Index->lookup(LR, [&ID, &AST, &Decl](const Symbol &Sym) {
// This callback is called once per found symbol; here we expect exactly one
if (Sym.ID == *ID) {
- PVD = getNamedDeclFromSymbol(Sym, AST);
+ Decl = getNamedDeclFromSymbol(Sym, AST);
}
});
- if (PVD == nullptr || !PVD.has_value()) {
- // Not found in index
+ // Note: Decl may be nullptr if the symbol is in a header file (not the main
+ // file). In that case, we still want to continue and use index-based
+ // resolution.
+ if (!Decl.has_value()) {
+ // Symbol not found in index
return Results;
}
@@ -2509,9 +2508,12 @@ incomingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index, ParsedAST
if (auto *ND = getNamedDeclFromSymbol(Caller, AST)) {
CHI = declToCallHierarchyItem(*ND, AST.tuPath());
if (const auto *FD = llvm::dyn_cast<clang::FunctionDecl>(ND)) {
- if (isa<ValueDecl>(PVD.value())) {
- const auto *VD = llvm::dyn_cast<clang::ValueDecl>(PVD.value());
- CHI->referenceTags = analyseParameterUsage(FD, VD); // FD is the caller of var
+ if (Decl.has_value() && Decl.value() != nullptr) {
+ if (const auto *VD =
+ llvm::dyn_cast<clang::ValueDecl>(Decl.value())) {
+ CHI->referenceTags =
+ analyseParameterUsage(FD, VD); // FD is the caller of var
+ }
}
}
} else {
diff --git a/clang-tools-extra/clangd/XRefs.h b/clang-tools-extra/clangd/XRefs.h
index 911cf72d72f03..31e7b0b14440f 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, ParsedAST &AST);
+incomingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index,
+ ParsedAST &AST);
std::vector<CallHierarchyOutgoingCall>
outgoingCalls(const CallHierarchyItem &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 6548ea0068a8d..44c89d22d9c85 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 a5f13ab13d0b3..cf52f7af1ec61 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": {
diff --git a/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp b/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp
index ad5c7d06a0739..15387b10aad50 100644
--- a/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp
+++ b/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp
@@ -67,6 +67,12 @@ ::testing::Matcher<CallHierarchyOutgoingCall> oFromRanges(RangeMatchers... M) {
UnorderedElementsAre(M...));
}
+template <typename... References>
+::testing::Matcher<CallHierarchyItem> withReferenceTags(References... refs) {
+ return Field(&CallHierarchyItem::referenceTags,
+ UnorderedElementsAre(refs...));
+}
+
TEST(CallHierarchy, IncomingOneFileCpp) {
Annotations Source(R"cpp(
void call^ee(int);
@@ -402,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(
@@ -411,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());
};
@@ -558,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"),
@@ -566,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());
};
@@ -671,7 +683,8 @@ TEST(CallHierarchy, HierarchyOnVar) {
}
TEST(CallHierarchy, HierarchyOnVarWithReadReference) {
- // Tests that the call hierarchy works on non-local variables and read/write tags are set.
+ // Tests that the call hierarchy works on non-local variables and a read is
+ // set.
Annotations Source(R"cpp(
int v^ar = 1;
void caller() {
@@ -687,11 +700,14 @@ TEST(CallHierarchy, HierarchyOnVarWithReadReference) {
prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename));
ASSERT_THAT(Items, ElementsAre(withName("var")));
auto IncomingLevel1 = incomingCalls(Items[0], Index.get(), AST);
- EXPECT_TRUE(IncomingLevel1.front().from.referenceTags.at(0) == ReferenceTag::Read);
+ ASSERT_FALSE(IncomingLevel1.empty());
+ EXPECT_THAT(
+ IncomingLevel1,
+ ElementsAre(AllOf(from(
+ AllOf(withName("caller"), withReferenceTags(ReferenceTag::Read))))));
}
TEST(CallHierarchy, HierarchyOnVarWithWriteReference) {
- // Tests that the call hierarchy works on non-local variables and read/write tags are set.
Annotations Source(R"cpp(
int v^ar = 1;
void caller() {
@@ -706,11 +722,15 @@ TEST(CallHierarchy, HierarchyOnVarWithWriteReference) {
prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename));
ASSERT_THAT(Items, ElementsAre(withName("var")));
auto IncomingLevel1 = incomingCalls(Items[0], Index.get(), AST);
- EXPECT_TRUE(IncomingLevel1.front().from.referenceTags.at(0) == ReferenceTag::Write);
+ ASSERT_FALSE(IncomingLevel1.empty());
+ EXPECT_THAT(
+ IncomingLevel1,
+ ElementsAre(AllOf(from(
+ AllOf(withName("caller"),
+ withReferenceTags(ReferenceTag::Write, ReferenceTag::Read))))));
}
-TEST(CallHierarchy, HierarchyOnVarWithUnaryWriteReference) {
- // Tests that the call hierarchy works on non-local variables and read/write tags are set.
+TEST(CallHierarchy, HierarchyOnVarWithUnaryReadWriteReference) {
Annotations Source(R"cpp(
int v^ar = 1;
void caller() {
@@ -725,32 +745,14 @@ TEST(CallHierarchy, HierarchyOnVarWithUnaryWriteReference) {
prepareCallHierarchy(AST, Source.point(), testPath(TU.Filename));
ASSERT_THAT(Items, ElementsAre(withName("var")));
auto IncomingLevel1 = incomingCalls(Items[0], Index.get(), AST);
- EXPECT_TRUE(IncomingLevel1.front().from.referenceTags.at(0) == ReferenceTag::Write);
+ ASSERT_FALSE(IncomingLevel1.empty());
+ EXPECT_THAT(
+ IncomingLevel1,
+ UnorderedElementsAre(AllOf(from(
+ AllOf(withName("caller"),
+ withReferenceTags(ReferenceTag::Write, ReferenceTag::Read))))));
}
-// TEST(CallHierarchy, HierarchyOnFieldWithReadWriteReference) {
-// // Tests that the call hierarchy works on non-local variables and read/write tags are set.
-// Annotations Source(R"cpp(
-// struct Vars {
-// int v^ar1 = 1;
-// };
-// void caller() {
-// Vars values;
-// values.var1 = 2;
-// }
-// )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("var1")));
-// auto IncomingLevel1 = incomingCalls(Items[0], Index.get(), AST);
-// EXPECT_TRUE(IncomingLevel1.front().from.referenceTags.at(0) == ReferenceTag::Read);
-// //EXPECT_TRUE(IncomingLevel1.front().from.referenceTags.at(1) == ReferenceTag::Read);
-// }
-
TEST(CallHierarchy, HierarchyOnEnumConstant) {
// Tests that the call hierarchy works on enum constants.
Annotations Source(R"cpp(
>From 37759683319008b27bc89626f8224aa3d640554f Mon Sep 17 00:00:00 2001
From: Dimitri Ratz <dimitri.ratz at thinkdigital.cc>
Date: Mon, 2 Feb 2026 23:22:07 +0100
Subject: [PATCH 4/7] Fix: expected result in write-reference test.
---
clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp b/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp
index 15387b10aad50..0ec58a7602b11 100644
--- a/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp
+++ b/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp
@@ -726,8 +726,8 @@ TEST(CallHierarchy, HierarchyOnVarWithWriteReference) {
EXPECT_THAT(
IncomingLevel1,
ElementsAre(AllOf(from(
- AllOf(withName("caller"),
- withReferenceTags(ReferenceTag::Write, ReferenceTag::Read))))));
+ AllOf(withName("caller"), withReferenceTags(ReferenceTag::Write))))));
+}
}
TEST(CallHierarchy, HierarchyOnVarWithUnaryReadWriteReference) {
>From e30941813a45fe04f0550dde1139184791ad8208 Mon Sep 17 00:00:00 2001
From: Dimitri Ratz <dimitri.ratz at thinkdigital.cc>
Date: Mon, 2 Feb 2026 23:24:48 +0100
Subject: [PATCH 5/7] New test: caller within a class.
---
.../clangd/unittests/CallHierarchyTests.cpp | 24 +++++++++++++++++++
1 file changed, 24 insertions(+)
diff --git a/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp b/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp
index 0ec58a7602b11..5582997148b33 100644
--- a/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp
+++ b/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp
@@ -728,6 +728,30 @@ TEST(CallHierarchy, HierarchyOnVarWithWriteReference) {
ElementsAre(AllOf(from(
AllOf(withName("caller"), withReferenceTags(ReferenceTag::Write))))));
}
+
+TEST(CallHierarchy, HierarchyOnClassMemberWithWriteReference) {
+ Annotations Source(R"cpp(
+ int v^ar = 1;
+ class MyClass {
+ public:
+ void caller() {
+ var = 2;
+ }
+ };
+ )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("var")));
+ auto IncomingLevel1 = incomingCalls(Items[0], Index.get(), AST);
+ ASSERT_FALSE(IncomingLevel1.empty());
+ EXPECT_THAT(
+ IncomingLevel1,
+ ElementsAre(AllOf(from(
+ AllOf(withName("caller"), withReferenceTags(ReferenceTag::Write))))));
}
TEST(CallHierarchy, HierarchyOnVarWithUnaryReadWriteReference) {
>From b41a24ecc1aebd0d2f63c97a7d022c8bfd775c2f Mon Sep 17 00:00:00 2001
From: Dimitri Ratz <dimitri.ratz at thinkdigital.cc>
Date: Mon, 2 Feb 2026 23:27:46 +0100
Subject: [PATCH 6/7] Replaced Visitor methods for unary and binary operations
with Traverse. Try to get the function definition before calling
analyseParameterUsage. Visit member expressions.
---
clang-tools-extra/clangd/XRefs.cpp | 74 ++++++++++++++++++++----------
1 file changed, 49 insertions(+), 25 deletions(-)
diff --git a/clang-tools-extra/clangd/XRefs.cpp b/clang-tools-extra/clangd/XRefs.cpp
index 69947c77238f6..668eade7eb084 100644
--- a/clang-tools-extra/clangd/XRefs.cpp
+++ b/clang-tools-extra/clangd/XRefs.cpp
@@ -1755,45 +1755,64 @@ class ParamUsageVisitor : public RecursiveASTVisitor<ParamUsageVisitor> {
bool HasWrite = false;
bool HasRead = false;
const ValueDecl *TargetParam;
+ llvm::SmallSet<const Expr *, 4> WriteOnlyExprs;
ParamUsageVisitor(const ValueDecl *P) : TargetParam(P) {}
- // Any reference to the target is at least a read, unless we later
- // identify it specifically as a write (e.g. assignment/inc/dec on LHS).
- bool VisitDeclRefExpr(DeclRefExpr *DRE) {
- if (DRE->getDecl() == TargetParam)
- HasRead = true;
- return true;
- }
-
- bool VisitUnaryOperator(UnaryOperator *UO) {
- // Check for increment/decrement on parameter
- if (UO->isIncrementDecrementOp()) {
- if (auto *DRE = dyn_cast<DeclRefExpr>(UO->getSubExpr())) {
+ // Identify write-only contexts before visiting children
+ bool TraverseBinaryOperator(BinaryOperator *BO) {
+ if (BO->isAssignmentOp()) {
+ auto *LHS = BO->getLHS()->IgnoreParenImpCasts();
+ if (auto *DRE = dyn_cast<DeclRefExpr>(LHS)) {
if (DRE->getDecl() == TargetParam) {
HasWrite = true;
+ // For pure assignment (=), LHS is write-only
+ if (BO->getOpcode() == BO_Assign)
+ WriteOnlyExprs.insert(DRE);
+ }
+ } else if (auto *ME = dyn_cast<MemberExpr>(LHS)) {
+ if (ME->getMemberDecl() == TargetParam) {
+ HasWrite = true;
+ if (BO->getOpcode() == BO_Assign)
+ WriteOnlyExprs.insert(ME);
}
}
}
- return true;
+ // Continue with default traversal
+ return RecursiveASTVisitor::TraverseBinaryOperator(BO);
}
- bool VisitBinaryOperator(BinaryOperator *BO) {
- // Check for assignment to parameter
- if (BO->isAssignmentOp()) {
- if (isa<DeclRefExpr>(BO->getLHS())) {
- auto *DRE = dyn_cast<DeclRefExpr>(BO->getLHS());
+
+ bool TraverseUnaryOperator(UnaryOperator *UO) {
+ if (UO->isIncrementDecrementOp()) {
+ auto *SubExpr = UO->getSubExpr()->IgnoreParenImpCasts();
+ if (auto *DRE = dyn_cast<DeclRefExpr>(SubExpr)) {
if (DRE->getDecl() == TargetParam) {
HasWrite = true;
+ // Inc/Dec is both read and write, don't add to WriteOnlyExprs
+ }
+ } else if (auto *ME = dyn_cast<MemberExpr>(SubExpr)) {
+ if (ME->getMemberDecl() == TargetParam) {
+ HasWrite = true;
}
}
}
+ return RecursiveASTVisitor::TraverseUnaryOperator(UO);
+ }
- // Any other use is at least a read
- if (isa<DeclRefExpr>(BO->getRHS())) {
- auto *DRE = dyn_cast<DeclRefExpr>(BO->getRHS());
- if (DRE->getDecl() == TargetParam) {
+ // Check for reads, excluding write-only contexts
+ bool VisitDeclRefExpr(DeclRefExpr *DRE) {
+ if (DRE->getDecl() == TargetParam) {
+ if (!WriteOnlyExprs.count(DRE))
+ HasRead = true;
+ }
+ return true;
+ }
+
+ // Handle member expressions
+ bool VisitMemberExpr(MemberExpr *ME) {
+ if (ME->getMemberDecl() == TargetParam) {
+ if (!WriteOnlyExprs.count(ME))
HasRead = true;
- }
}
return true;
}
@@ -2511,8 +2530,13 @@ incomingCalls(const CallHierarchyItem &Item, const SymbolIndex *Index,
if (Decl.has_value() && Decl.value() != nullptr) {
if (const auto *VD =
llvm::dyn_cast<clang::ValueDecl>(Decl.value())) {
- CHI->referenceTags =
- analyseParameterUsage(FD, VD); // FD is the caller of var
+ // Use the function definition if available, not just a
+ // declaration
+ const FunctionDecl *FuncDef = FD->getDefinition();
+ if (!FuncDef)
+ FuncDef = FD;
+ CHI->referenceTags = analyseParameterUsage(
+ FuncDef, VD); // FuncDef is the caller of value decl VD
}
}
}
>From a6e99c5e0ae8ad2f731ee063dea6d2c070db181c Mon Sep 17 00:00:00 2001
From: Dimitri Ratz <dimitri.ratz at protonmail.com>
Date: Sun, 15 Feb 2026 22:26:47 +0100
Subject: [PATCH 7/7] New: traverse ctor declaration and corresponding unit
test.
---
clang-tools-extra/clangd/XRefs.cpp | 27 +++++++++----
.../clangd/unittests/CallHierarchyTests.cpp | 39 +++++++++++++++----
2 files changed, 51 insertions(+), 15 deletions(-)
diff --git a/clang-tools-extra/clangd/XRefs.cpp b/clang-tools-extra/clangd/XRefs.cpp
index 668eade7eb084..5f02eb9324e1e 100644
--- a/clang-tools-extra/clangd/XRefs.cpp
+++ b/clang-tools-extra/clangd/XRefs.cpp
@@ -1782,6 +1782,16 @@ class ParamUsageVisitor : public RecursiveASTVisitor<ParamUsageVisitor> {
return RecursiveASTVisitor::TraverseBinaryOperator(BO);
}
+ bool TraverseConstructorInitializer(CXXCtorInitializer *Init) {
+ if (Init) {
+ if (const FieldDecl *FD = Init->getMember()) {
+ if (FD == TargetParam)
+ HasWrite = true;
+ }
+ }
+ return RecursiveASTVisitor::TraverseConstructorInitializer(Init);
+ }
+
bool TraverseUnaryOperator(UnaryOperator *UO) {
if (UO->isIncrementDecrementOp()) {
auto *SubExpr = UO->getSubExpr()->IgnoreParenImpCasts();
@@ -1821,14 +1831,17 @@ class ParamUsageVisitor : public RecursiveASTVisitor<ParamUsageVisitor> {
static std::vector<ReferenceTag> analyseParameterUsage(const FunctionDecl *FD,
const ValueDecl *PVD) {
std::vector<ReferenceTag> Result;
- const Stmt *Body = FD->getBody();
- if (!Body)
- return Result; // No definition available
-
- // Walk the body and determine read/write usage of the referenced variable
- // within this function.
ParamUsageVisitor Visitor(PVD);
- Visitor.TraverseStmt(const_cast<Stmt *>(Body));
+
+ if (const auto *Ctor = dyn_cast<CXXConstructorDecl>(FD)) {
+ Visitor.TraverseDecl(const_cast<CXXConstructorDecl *>(Ctor));
+ } else if (const Stmt *Body = FD->getBody()) {
+ // Walk the body and determine read/write usage of the referenced variable
+ // within this function.
+ Visitor.TraverseStmt(const_cast<Stmt *>(Body));
+ } else
+ return Result;
+
if (Visitor.HasWrite)
Result.push_back(ReferenceTag::Write);
if (Visitor.HasRead)
diff --git a/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp b/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp
index 5582997148b33..72fb35c5ca027 100644
--- a/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp
+++ b/clang-tools-extra/clangd/unittests/CallHierarchyTests.cpp
@@ -731,14 +731,14 @@ TEST(CallHierarchy, HierarchyOnVarWithWriteReference) {
TEST(CallHierarchy, HierarchyOnClassMemberWithWriteReference) {
Annotations Source(R"cpp(
- int v^ar = 1;
- class MyClass {
- public:
- void caller() {
- var = 2;
- }
- };
- )cpp");
+ class MyClass {
+ public:
+ void caller() {
+ var = 2;
+ }
+ int v^ar = 1;
+ };
+ )cpp");
TestTU TU = TestTU::withCode(Source.code());
auto AST = TU.build();
auto Index = TU.index();
@@ -754,6 +754,29 @@ TEST(CallHierarchy, HierarchyOnClassMemberWithWriteReference) {
AllOf(withName("caller"), withReferenceTags(ReferenceTag::Write))))));
}
+TEST(CallHierarchy, HierarchyOnClassMemberWithWriteReferenceInCtorInitList) {
+ Annotations Source(R"cpp(
+ class MyClass {
+ int v^ar;
+ public:
+ MyClass() : var(1) {}
+ };
+ )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("var")));
+ auto IncomingLevel1 = incomingCalls(Items[0], Index.get(), AST);
+ ASSERT_FALSE(IncomingLevel1.empty());
+ EXPECT_THAT(
+ IncomingLevel1,
+ ElementsAre(AllOf(from(AllOf(withName("MyClass"),
+ withReferenceTags(ReferenceTag::Write))))));
+}
+
TEST(CallHierarchy, HierarchyOnVarWithUnaryReadWriteReference) {
Annotations Source(R"cpp(
int v^ar = 1;
More information about the cfe-commits
mailing list