[clang-tools-extra] [clangd] Add AddUsingReplaceAll tweak to replace all qualified refere… (PR #204151)
Dimitri Ratz via cfe-commits
cfe-commits at lists.llvm.org
Thu Jun 18 00:58:07 PDT 2026
https://github.com/ratzdi updated https://github.com/llvm/llvm-project/pull/204151
>From 6724b163ac711f6c0ad641b0668b9f6395613929 Mon Sep 17 00:00:00 2001
From: Dimitri Ratz <dimitri.ratz at protonmail.com>
Date: Tue, 16 Jun 2026 13:48:16 +0200
Subject: [PATCH 1/2] [clangd] Add AddUsingReplaceAll tweak to replace all
qualified references Adds a new refactoring code action that is triggered on
a fully qualified name and removes its namespace qualifier from every
occurrence in the file, inserting a using-declaration at an appropriate
location. This complements the existing AddUsing tweak, which only rewrites
the single reference under the cursor. Implementation notes: -
collectEnclosingUsings() replaces a RecursiveASTVisitor-based traversal by
walking the DeclContext chain directly, reducing traversal cost from O(AST
nodes) to O(scope depth * decls per scope). - Qualifier extraction is guarded
via getNamespaceFromQualifier() to reject non-namespace qualifiers (e.g.
record-type qualifiers) defensively. - apply() validates the prepared state
before building replacements and only rewrites references whose canonical
target matches the selection. - References inside macro arguments are
rewritten; macro-body expansions are left untouched. Tests cover: basic
multi-reference rewrites, macro arguments, global (::) qualifiers with
nested-namespace syntax, multiple nested macro layers, and unavailability for
non-namespace qualifiers such as record-type qualifiers.
---
.../refactor/tweaks/AddUsingReplaceAll.cpp | 445 ++++++++++++++++++
.../clangd/refactor/tweaks/CMakeLists.txt | 1 +
.../clangd/unittests/CMakeLists.txt | 1 +
.../tweaks/AddUsingReplaceAllTests.cpp | 174 +++++++
4 files changed, 621 insertions(+)
create mode 100644 clang-tools-extra/clangd/refactor/tweaks/AddUsingReplaceAll.cpp
create mode 100644 clang-tools-extra/clangd/unittests/tweaks/AddUsingReplaceAllTests.cpp
diff --git a/clang-tools-extra/clangd/refactor/tweaks/AddUsingReplaceAll.cpp b/clang-tools-extra/clangd/refactor/tweaks/AddUsingReplaceAll.cpp
new file mode 100644
index 0000000000000..d2dc4f793cf12
--- /dev/null
+++ b/clang-tools-extra/clangd/refactor/tweaks/AddUsingReplaceAll.cpp
@@ -0,0 +1,445 @@
+//===--- AddUsingReplaceAll.cpp ---------------------------------*- C++-*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "AST.h"
+#include "Config.h"
+#include "FindTarget.h"
+#include "SourceCode.h"
+#include "refactor/Tweak.h"
+#include "support/Logger.h"
+
+#include "clang/AST/Decl.h"
+#include "clang/AST/DeclBase.h"
+#include "clang/AST/Expr.h"
+#include "clang/AST/NestedNameSpecifier.h"
+#include "clang/AST/Type.h"
+#include "clang/AST/TypeLoc.h"
+#include "clang/Basic/SourceLocation.h"
+#include "clang/Tooling/Core/Replacement.h"
+#include "clang/Tooling/Syntax/Tokens.h"
+#include "llvm/ADT/DenseSet.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/FormatVariadic.h"
+#include "llvm/Support/raw_ostream.h"
+#include <string>
+#include <tuple>
+#include <utility>
+
+namespace clang {
+namespace clangd {
+namespace {
+
+/// Tweak for removing full namespace qualifier under cursor on DeclRefExpr and
+/// types and adding "using" statement instead. This tweak replaces all
+/// occurrences of the qualified symbol in the file, not just the one under the
+/// cursor, but it also requires more preparation and is more expensive to
+/// compute, so it is hidden behind a separate action from AddUsing, which only
+/// removes the qualifier under the cursor.
+class AddUsingReplaceAll : public Tweak {
+public:
+ const char *id() const override;
+
+ bool prepare(const Selection &Inputs) override;
+ Expected<Effect> apply(const Selection &Inputs) override;
+ std::string title() const override;
+ llvm::StringLiteral kind() const override {
+ return CodeAction::REFACTOR_KIND;
+ }
+
+private:
+ NestedNameSpecifierLoc QualifierToRemove;
+ std::string QualifierToSpell;
+ llvm::StringRef SpelledQualifier;
+ llvm::StringRef SpelledName;
+ SourceLocation MustInsertAfterLoc;
+ llvm::DenseSet<const NamedDecl *> RewriteTargets;
+};
+REGISTER_TWEAK(AddUsingReplaceAll)
+
+std::string AddUsingReplaceAll::title() const {
+ return std::string(llvm::formatv(
+ "Add using-declaration for {0} and replace qualified references",
+ SpelledName));
+}
+
+/// Collects all UsingDecls visible at the selection by walking the enclosing
+/// DeclContext chain directly, sorted by source location. This avoids a full
+/// AST traversal and only visits O(depth * declarations-per-scope) nodes.
+///
+/// A using-declaration declared in a scope S is visible at SelectionCtx iff
+/// S is an ancestor of SelectionCtx, i.e. exactly the contexts we walk when
+/// ascending via getLexicalParent().
+std::vector<const UsingDecl *>
+collectEnclosingUsings(const DeclContext *SelectionCtx,
+ const SourceManager &SM) {
+ std::vector<const UsingDecl *> Result;
+ for (const DeclContext *Ctx = SelectionCtx; Ctx;
+ Ctx = Ctx->getLexicalParent()) {
+ for (const Decl *D : Ctx->decls()) {
+ const auto *UD = dyn_cast<UsingDecl>(D);
+ if (!UD)
+ continue;
+ if (SM.getFileID(UD->getUsingLoc()) != SM.getMainFileID())
+ continue;
+ Result.push_back(UD);
+ }
+ }
+ // Merge into a single sorted list so findInsertionPoint can break early.
+ llvm::sort(Result, [&](const UsingDecl *A, const UsingDecl *B) {
+ return SM.isBeforeInTranslationUnit(A->getUsingLoc(), B->getUsingLoc());
+ });
+ return Result;
+}
+
+struct InsertionPointData {
+ SourceLocation Loc;
+ std::string Suffix;
+ bool AlwaysFullyQualify = false;
+};
+
+const NamespaceDecl *getNamespaceFromQualifier(NestedNameSpecifier Qualifier) {
+ if (!Qualifier || Qualifier.getKind() != NestedNameSpecifier::Kind::Namespace)
+ return nullptr;
+ return dyn_cast_or_null<NamespaceDecl>(
+ Qualifier.getAsNamespaceAndPrefix().Namespace);
+}
+
+llvm::Expected<InsertionPointData>
+findInsertionPoint(const Tweak::Selection &Inputs,
+ const NestedNameSpecifierLoc &QualifierToRemove,
+ const llvm::StringRef Name,
+ const SourceLocation MustInsertAfterLoc) {
+ auto &SM = Inputs.AST->getSourceManager();
+
+ const auto *TargetNamespace =
+ getNamespaceFromQualifier(QualifierToRemove.getNestedNameSpecifier());
+ if (!TargetNamespace)
+ return error("Qualifier is not a valid namespace qualifier");
+
+ SourceLocation LastUsingLoc;
+ const std::vector<const UsingDecl *> Usings = collectEnclosingUsings(
+ &Inputs.ASTSelection.commonAncestor()->getDeclContext(), SM);
+
+ auto IsValidPoint = [&](const SourceLocation Loc) {
+ return MustInsertAfterLoc.isInvalid() ||
+ SM.isBeforeInTranslationUnit(MustInsertAfterLoc, Loc);
+ };
+
+ bool AlwaysFullyQualify = true;
+ for (const auto *U : Usings) {
+ if (!U->getQualifier().isFullyQualified())
+ AlwaysFullyQualify = false;
+
+ if (const auto *Namespace = getNamespaceFromQualifier(U->getQualifier())) {
+ if (Namespace->getCanonicalDecl() ==
+ TargetNamespace->getCanonicalDecl() &&
+ U->getName() == Name)
+ return InsertionPointData();
+ }
+
+ LastUsingLoc = U->getUsingLoc();
+ }
+ if (LastUsingLoc.isValid() && IsValidPoint(LastUsingLoc)) {
+ InsertionPointData Out;
+ Out.Loc = LastUsingLoc;
+ Out.AlwaysFullyQualify = AlwaysFullyQualify;
+ return Out;
+ }
+
+ const DeclContext *ParentDeclCtx =
+ &Inputs.ASTSelection.commonAncestor()->getDeclContext();
+ while (ParentDeclCtx && !ParentDeclCtx->isFileContext())
+ ParentDeclCtx = ParentDeclCtx->getLexicalParent();
+
+ if (auto *ND = llvm::dyn_cast_or_null<NamespaceDecl>(ParentDeclCtx)) {
+ auto Toks = Inputs.AST->getTokens().expandedTokens(ND->getSourceRange());
+ const auto *Tok = llvm::find_if(Toks, [](const syntax::Token &Tok) {
+ return Tok.kind() == tok::l_brace;
+ });
+ if (Tok == Toks.end() || Tok->endLocation().isInvalid())
+ return error("Namespace with no {{");
+ if (!Tok->endLocation().isMacroID() && IsValidPoint(Tok->endLocation())) {
+ InsertionPointData Out;
+ Out.Loc = Tok->endLocation();
+ Out.Suffix = "\n";
+ return Out;
+ }
+ }
+
+ auto TLDs = Inputs.AST->getLocalTopLevelDecls();
+ for (const auto &TLD : TLDs) {
+ if (!IsValidPoint(TLD->getBeginLoc()))
+ continue;
+ InsertionPointData Out;
+ Out.Loc = SM.getExpansionLoc(TLD->getBeginLoc());
+ Out.Suffix = "\n\n";
+ return Out;
+ }
+ return error("Cannot find place to insert \"using\"");
+}
+
+bool isNamespaceForbidden(const Tweak::Selection &Inputs,
+ NestedNameSpecifier Namespace) {
+ const auto *NS =
+ dyn_cast<NamespaceDecl>(Namespace.getAsNamespaceAndPrefix().Namespace);
+ if (!NS)
+ return true;
+ std::string NamespaceStr = printNamespaceScope(*NS);
+
+ for (StringRef Banned : Config::current().Style.FullyQualifiedNamespaces) {
+ StringRef PrefixMatch = NamespaceStr;
+ if (PrefixMatch.consume_front(Banned) && PrefixMatch.consume_front("::"))
+ return true;
+ }
+
+ return false;
+}
+
+std::string getNNSLAsString(NestedNameSpecifierLoc NNSL,
+ const PrintingPolicy &Policy) {
+ std::string Out;
+ llvm::raw_string_ostream OutStream(Out);
+ NNSL.getNestedNameSpecifier().print(OutStream, Policy);
+ return OutStream.str();
+}
+
+const NamedDecl *canonicalDecl(const NamedDecl *D) {
+ return D ? llvm::dyn_cast<NamedDecl>(D->getCanonicalDecl()) : nullptr;
+}
+
+bool AddUsingReplaceAll::prepare(const Selection &Inputs) {
+ auto &SM = Inputs.AST->getSourceManager();
+ const auto &TB = Inputs.AST->getTokens();
+
+ QualifierToRemove = NestedNameSpecifierLoc();
+ QualifierToSpell.clear();
+ SpelledQualifier = llvm::StringRef();
+ SpelledName = llvm::StringRef();
+ MustInsertAfterLoc = SourceLocation();
+ RewriteTargets.clear();
+
+ if (isHeaderFile(SM.getFileEntryRefForID(SM.getMainFileID())->getName(),
+ Inputs.AST->getLangOpts()))
+ return false;
+
+ auto *Node = Inputs.ASTSelection.commonAncestor();
+ if (!Node)
+ return false;
+
+ for (; Node->Parent; Node = Node->Parent) {
+ if (Node->ASTNode.get<NestedNameSpecifierLoc>())
+ continue;
+ if (auto *T = Node->ASTNode.get<TypeLoc>()) {
+ if (Node->Parent->ASTNode.get<NestedNameSpecifierLoc>())
+ continue;
+ if (isa<TagType, TemplateSpecializationType, TypedefType, UsingType,
+ UnresolvedUsingType>(T->getTypePtr()))
+ break;
+ if (Node->Parent->ASTNode.get<TypeLoc>())
+ continue;
+ }
+ break;
+ }
+ if (!Node)
+ return false;
+
+ SourceRange SpelledNameRange;
+ if (auto *D = Node->ASTNode.get<DeclRefExpr>()) {
+ if (D->getDecl()->getIdentifier()) {
+ QualifierToRemove = D->getQualifierLoc();
+ SpelledNameRange = D->getSourceRange();
+ if (auto AngleLoc = D->getLAngleLoc(); AngleLoc.isValid())
+ SpelledNameRange.setEnd(AngleLoc.getLocWithOffset(-1));
+ MustInsertAfterLoc = D->getDecl()->getBeginLoc();
+ if (const auto *Canonical = canonicalDecl(D->getDecl()))
+ RewriteTargets.insert(Canonical);
+ }
+ } else if (auto *T = Node->ASTNode.get<TypeLoc>()) {
+ switch (T->getTypeLocClass()) {
+ case TypeLoc::TemplateSpecialization: {
+ auto TL = T->castAs<TemplateSpecializationTypeLoc>();
+ QualifierToRemove = TL.getQualifierLoc();
+ if (!QualifierToRemove)
+ break;
+ SpelledNameRange = TL.getTemplateNameLoc();
+ if (auto *TD = TL.getTypePtr()->getTemplateName().getAsTemplateDecl(
+ /*IgnoreDeduced=*/true)) {
+ MustInsertAfterLoc = TD->getBeginLoc();
+ if (const auto *Canonical = canonicalDecl(TD))
+ RewriteTargets.insert(Canonical);
+ }
+ break;
+ }
+ case TypeLoc::Enum:
+ case TypeLoc::Record:
+ case TypeLoc::InjectedClassName: {
+ auto TL = T->castAs<TagTypeLoc>();
+ QualifierToRemove = TL.getQualifierLoc();
+ if (!QualifierToRemove)
+ break;
+ SpelledNameRange = TL.getNameLoc();
+ MustInsertAfterLoc = TL.getDecl()->getBeginLoc();
+ if (const auto *Canonical = canonicalDecl(TL.getDecl()))
+ RewriteTargets.insert(Canonical);
+ break;
+ }
+ case TypeLoc::Typedef: {
+ auto TL = T->castAs<TypedefTypeLoc>();
+ QualifierToRemove = TL.getQualifierLoc();
+ if (!QualifierToRemove)
+ break;
+ SpelledNameRange = TL.getNameLoc();
+ MustInsertAfterLoc = TL.getDecl()->getBeginLoc();
+ if (const auto *Canonical = canonicalDecl(TL.getDecl()))
+ RewriteTargets.insert(Canonical);
+ break;
+ }
+ case TypeLoc::UnresolvedUsing: {
+ auto TL = T->castAs<UnresolvedUsingTypeLoc>();
+ QualifierToRemove = TL.getQualifierLoc();
+ if (!QualifierToRemove)
+ break;
+ SpelledNameRange = TL.getNameLoc();
+ MustInsertAfterLoc = TL.getDecl()->getBeginLoc();
+ if (const auto *Canonical = canonicalDecl(TL.getDecl()))
+ RewriteTargets.insert(Canonical);
+ break;
+ }
+ case TypeLoc::Using: {
+ auto TL = T->castAs<UsingTypeLoc>();
+ QualifierToRemove = TL.getQualifierLoc();
+ if (!QualifierToRemove)
+ break;
+ SpelledNameRange = TL.getNameLoc();
+ MustInsertAfterLoc = TL.getDecl()->getBeginLoc();
+ if (const auto *Canonical = canonicalDecl(TL.getDecl()))
+ RewriteTargets.insert(Canonical);
+ break;
+ }
+ default:
+ break;
+ }
+ if (QualifierToRemove)
+ SpelledNameRange.setBegin(QualifierToRemove.getBeginLoc());
+ }
+
+ if (!QualifierToRemove || RewriteTargets.empty() ||
+ QualifierToRemove.getNestedNameSpecifier().getKind() !=
+ NestedNameSpecifier::Kind::Namespace ||
+ isNamespaceForbidden(Inputs, QualifierToRemove.getNestedNameSpecifier()))
+ return false;
+
+ if (SM.isMacroBodyExpansion(QualifierToRemove.getBeginLoc()) ||
+ !SM.isWrittenInSameFile(QualifierToRemove.getBeginLoc(),
+ QualifierToRemove.getEndLoc()))
+ return false;
+
+ auto SpelledTokens =
+ TB.spelledForExpanded(TB.expandedTokens(SpelledNameRange));
+ if (!SpelledTokens)
+ return false;
+ auto SpelledRange =
+ syntax::Token::range(SM, SpelledTokens->front(), SpelledTokens->back());
+ std::tie(SpelledQualifier, SpelledName) =
+ splitQualifiedName(SpelledRange.text(SM));
+ QualifierToSpell = getNNSLAsString(
+ QualifierToRemove, Inputs.AST->getASTContext().getPrintingPolicy());
+ if (!llvm::StringRef(QualifierToSpell).ends_with(SpelledQualifier) ||
+ SpelledName.empty())
+ return false;
+ return true;
+}
+
+Expected<Tweak::Effect> AddUsingReplaceAll::apply(const Selection &Inputs) {
+ auto &SM = Inputs.AST->getSourceManager();
+
+ const auto *TargetNamespace =
+ getNamespaceFromQualifier(QualifierToRemove.getNestedNameSpecifier());
+ if (!TargetNamespace)
+ return error("Invalid namespace qualifier in prepared state");
+ if (SpelledName.empty() || QualifierToSpell.empty() || RewriteTargets.empty())
+ return error("Incomplete prepared state for AddUsingReplaceAll");
+
+ tooling::Replacements Repls;
+
+ auto InsertionPoint = findInsertionPoint(Inputs, QualifierToRemove,
+ SpelledName, MustInsertAfterLoc);
+ if (!InsertionPoint)
+ return InsertionPoint.takeError();
+
+ if (InsertionPoint->Loc.isValid()) {
+ std::string UsingText;
+ llvm::raw_string_ostream UsingTextStream(UsingText);
+ UsingTextStream << "using ";
+ if (InsertionPoint->AlwaysFullyQualify &&
+ !QualifierToRemove.getNestedNameSpecifier().isFullyQualified())
+ UsingTextStream << "::";
+ UsingTextStream << QualifierToSpell << SpelledName << ";"
+ << InsertionPoint->Suffix;
+
+ assert(SM.getFileID(InsertionPoint->Loc) == SM.getMainFileID());
+ if (auto Err = Repls.add(tooling::Replacement(SM, InsertionPoint->Loc, 0,
+ UsingTextStream.str())))
+ return std::move(Err);
+ }
+
+ for (const auto &D : Inputs.AST->getLocalTopLevelDecls()) {
+ findExplicitReferences(
+ D,
+ [&](ReferenceLoc Ref) {
+ if (!Ref.Qualifier || Ref.Targets.empty() || Ref.IsDecl)
+ return;
+ if (!getNamespaceFromQualifier(
+ Ref.Qualifier.getNestedNameSpecifier()))
+ return;
+
+ SourceLocation QualifierLoc = Ref.Qualifier.getBeginLoc();
+ SourceLocation NameLoc = Ref.NameLoc;
+ if (QualifierLoc.isMacroID()) {
+ if (!SM.isMacroArgExpansion(QualifierLoc))
+ return;
+ QualifierLoc = SM.getFileLoc(QualifierLoc);
+ }
+ if (NameLoc.isMacroID()) {
+ if (!SM.isMacroArgExpansion(NameLoc))
+ return;
+ NameLoc = SM.getFileLoc(NameLoc);
+ }
+ if (SM.getFileID(QualifierLoc) != SM.getMainFileID() ||
+ SM.getFileID(NameLoc) != SM.getMainFileID())
+ return;
+
+ bool MatchesTarget = false;
+ for (const auto *Target : Ref.Targets) {
+ if (const auto *Canonical = canonicalDecl(Target);
+ Canonical && RewriteTargets.contains(Canonical)) {
+ MatchesTarget = true;
+ break;
+ }
+ }
+ if (!MatchesTarget)
+ return;
+
+ unsigned BeginOffset = SM.getFileOffset(QualifierLoc);
+ unsigned EndOffset = SM.getFileOffset(NameLoc);
+ if (BeginOffset >= EndOffset)
+ return;
+
+ if (Repls.add(tooling::Replacement(SM, QualifierLoc,
+ EndOffset - BeginOffset, "")))
+ return;
+ },
+ Inputs.AST->getHeuristicResolver());
+ }
+
+ return Effect::mainFileEdit(SM, std::move(Repls));
+}
+
+} // namespace
+} // namespace clangd
+} // namespace clang
diff --git a/clang-tools-extra/clangd/refactor/tweaks/CMakeLists.txt b/clang-tools-extra/clangd/refactor/tweaks/CMakeLists.txt
index 1d6e38088ad67..8ed40760fef97 100644
--- a/clang-tools-extra/clangd/refactor/tweaks/CMakeLists.txt
+++ b/clang-tools-extra/clangd/refactor/tweaks/CMakeLists.txt
@@ -13,6 +13,7 @@ set(LLVM_LINK_COMPONENTS
# clangd/tool/CMakeLists.txt for an example.
add_clang_library(clangDaemonTweaks OBJECT
AddUsing.cpp
+ AddUsingReplaceAll.cpp
AnnotateHighlightings.cpp
DefineInline.cpp
DefineOutline.cpp
diff --git a/clang-tools-extra/clangd/unittests/CMakeLists.txt b/clang-tools-extra/clangd/unittests/CMakeLists.txt
index d596ba77efd4a..6b27865fbfd46 100644
--- a/clang-tools-extra/clangd/unittests/CMakeLists.txt
+++ b/clang-tools-extra/clangd/unittests/CMakeLists.txt
@@ -120,6 +120,7 @@ add_unittest(ClangdUnitTests ClangdTests
support/TraceTests.cpp
tweaks/AddUsingTests.cpp
+ tweaks/AddUsingReplaceAllTests.cpp
tweaks/AnnotateHighlightingsTests.cpp
tweaks/DefineInlineTests.cpp
tweaks/DefineOutlineTests.cpp
diff --git a/clang-tools-extra/clangd/unittests/tweaks/AddUsingReplaceAllTests.cpp b/clang-tools-extra/clangd/unittests/tweaks/AddUsingReplaceAllTests.cpp
new file mode 100644
index 0000000000000..741f94c910244
--- /dev/null
+++ b/clang-tools-extra/clangd/unittests/tweaks/AddUsingReplaceAllTests.cpp
@@ -0,0 +1,174 @@
+//===-- AddUsingReplaceAllTests.cpp -----------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "TweakTesting.h"
+#include "gtest/gtest.h"
+
+namespace clang {
+namespace clangd {
+namespace {
+
+TWEAK_TEST(AddUsingReplaceAll);
+
+TEST_F(AddUsingReplaceAllTest, Prepare) {
+ const char *Header = R"cpp(namespace ns { struct SomeStruct {}; })cpp";
+
+ EXPECT_AVAILABLE(std::string(Header) + R"cpp(
+ns::SomeStruct bar() {
+ n^s::SomeStruct x;
+ return x;
+})cpp");
+ EXPECT_UNAVAILABLE(std::string(Header) + R"cpp(
+ns::SomeStruct bar() {
+ ns::SomeStruct ^x;
+ return x;
+})cpp");
+
+ EXPECT_UNAVAILABLE(R"cpp(
+struct S {
+ static void f();
+};
+void fun() {
+ S::f^();
+}
+)cpp");
+}
+
+TEST_F(AddUsingReplaceAllTest, Apply) {
+ ExtraFiles["test.hpp"] = R"cpp(
+namespace one {
+namespace two {
+void ff();
+}
+})cpp";
+
+ EXPECT_EQ(apply(R"cpp(
+#include "test.hpp"
+
+void fun() {
+ one::two::f^f();
+ one::two::ff();
+}
+
+void other() {
+ one::two::ff();
+}
+)cpp"),
+ R"cpp(
+#include "test.hpp"
+
+using one::two::ff;
+
+void fun() {
+ ff();
+ ff();
+}
+
+void other() {
+ ff();
+}
+)cpp");
+}
+
+TEST_F(AddUsingReplaceAllTest, ApplyInsideMacroArgument) {
+ ExtraFiles["test.hpp"] = R"cpp(
+namespace one {
+namespace two {
+void ff();
+}
+})cpp";
+
+ EXPECT_EQ(apply(R"cpp(
+#include "test.hpp"
+#define CALL(name) name()
+
+void fun() {
+ CALL(one::two::f^f);
+ one::two::ff();
+}
+)cpp"),
+ R"cpp(
+#include "test.hpp"
+#define CALL(name) name()
+
+using one::two::ff;
+
+void fun() {
+ CALL(ff);
+ ff();
+}
+)cpp");
+}
+
+TEST_F(AddUsingReplaceAllTest, ApplyWithGlobalQualifierAndNestedNamespaceDecl) {
+ ExtraFiles["test.hpp"] = R"cpp(
+namespace one::two {
+void ff();
+}
+)cpp";
+
+ EXPECT_EQ(apply(R"cpp(
+#include "test.hpp"
+
+void fun() {
+ ::one::two::f^f();
+ one::two::ff();
+}
+)cpp"),
+ R"cpp(
+#include "test.hpp"
+
+using ::one::two::ff;
+
+void fun() {
+ ff();
+ ff();
+}
+)cpp");
+}
+
+TEST_F(AddUsingReplaceAllTest, ApplyWithMultipleMacros) {
+ ExtraFiles["test.hpp"] = R"cpp(
+namespace one {
+namespace two {
+void ff();
+}
+}
+)cpp";
+
+ EXPECT_EQ(apply(R"cpp(
+#include "test.hpp"
+#define ID(X) X
+#define CALL(name) name()
+#define CALL_FF one::two::ff()
+
+void fun() {
+ CALL(ID(one::two::f^f));
+ CALL_FF;
+ one::two::ff();
+}
+)cpp"),
+ R"cpp(
+#include "test.hpp"
+#define ID(X) X
+#define CALL(name) name()
+#define CALL_FF one::two::ff()
+
+using one::two::ff;
+
+void fun() {
+ CALL(ID(ff));
+ CALL_FF;
+ ff();
+}
+)cpp");
+}
+
+} // namespace
+} // namespace clangd
+} // namespace clang
>From f700f3602450a12909fb0ce08d4de03cb2dbfcbf Mon Sep 17 00:00:00 2001
From: Dimitri Ratz <dimitri.ratz at protonmail.com>
Date: Thu, 18 Jun 2026 09:25:20 +0200
Subject: [PATCH 2/2] [clangd] Refactor AddUsingReplaceAll tweak to use static
functions and improve clarity
---
.../refactor/tweaks/AddUsingReplaceAll.cpp | 43 +++++++++----------
1 file changed, 20 insertions(+), 23 deletions(-)
diff --git a/clang-tools-extra/clangd/refactor/tweaks/AddUsingReplaceAll.cpp b/clang-tools-extra/clangd/refactor/tweaks/AddUsingReplaceAll.cpp
index d2dc4f793cf12..b0141bb4da6b1 100644
--- a/clang-tools-extra/clangd/refactor/tweaks/AddUsingReplaceAll.cpp
+++ b/clang-tools-extra/clangd/refactor/tweaks/AddUsingReplaceAll.cpp
@@ -16,8 +16,6 @@
#include "clang/AST/Decl.h"
#include "clang/AST/DeclBase.h"
#include "clang/AST/Expr.h"
-#include "clang/AST/NestedNameSpecifier.h"
-#include "clang/AST/Type.h"
#include "clang/AST/TypeLoc.h"
#include "clang/Basic/SourceLocation.h"
#include "clang/Tooling/Core/Replacement.h"
@@ -32,14 +30,13 @@
namespace clang {
namespace clangd {
-namespace {
-
-/// Tweak for removing full namespace qualifier under cursor on DeclRefExpr and
-/// types and adding "using" statement instead. This tweak replaces all
-/// occurrences of the qualified symbol in the file, not just the one under the
-/// cursor, but it also requires more preparation and is more expensive to
-/// compute, so it is hidden behind a separate action from AddUsing, which only
-/// removes the qualifier under the cursor.
+
+/// Tweak for removing the full namespace qualifier under the cursor on
+/// DeclRefExpr and types and adding "using" statement instead. This tweak
+/// replaces all occurrences of the qualified symbol in the file, not just the
+/// one under the cursor, but it also requires more preparation and is more
+/// expensive to compute, so it is hidden behind a separate action from
+/// AddUsing, which only removes the qualifier under the cursor.
class AddUsingReplaceAll : public Tweak {
public:
const char *id() const override;
@@ -62,9 +59,9 @@ class AddUsingReplaceAll : public Tweak {
REGISTER_TWEAK(AddUsingReplaceAll)
std::string AddUsingReplaceAll::title() const {
- return std::string(llvm::formatv(
- "Add using-declaration for {0} and replace qualified references",
- SpelledName));
+ return std::string(llvm::formatv("Add using-declaration for {0} and replace "
+ "all qualified references in this file",
+ SpelledName));
}
/// Collects all UsingDecls visible at the selection by walking the enclosing
@@ -72,9 +69,9 @@ std::string AddUsingReplaceAll::title() const {
/// AST traversal and only visits O(depth * declarations-per-scope) nodes.
///
/// A using-declaration declared in a scope S is visible at SelectionCtx iff
-/// S is an ancestor of SelectionCtx, i.e. exactly the contexts we walk when
+/// S is an ancestor of SelectionCtx, i.e., exactly the contexts we walk when
/// ascending via getLexicalParent().
-std::vector<const UsingDecl *>
+static std::vector<const UsingDecl *>
collectEnclosingUsings(const DeclContext *SelectionCtx,
const SourceManager &SM) {
std::vector<const UsingDecl *> Result;
@@ -102,14 +99,15 @@ struct InsertionPointData {
bool AlwaysFullyQualify = false;
};
-const NamespaceDecl *getNamespaceFromQualifier(NestedNameSpecifier Qualifier) {
+static const NamespaceDecl *
+getNamespaceFromQualifier(NestedNameSpecifier Qualifier) {
if (!Qualifier || Qualifier.getKind() != NestedNameSpecifier::Kind::Namespace)
return nullptr;
return dyn_cast_or_null<NamespaceDecl>(
Qualifier.getAsNamespaceAndPrefix().Namespace);
}
-llvm::Expected<InsertionPointData>
+static llvm::Expected<InsertionPointData>
findInsertionPoint(const Tweak::Selection &Inputs,
const NestedNameSpecifierLoc &QualifierToRemove,
const llvm::StringRef Name,
@@ -183,8 +181,8 @@ findInsertionPoint(const Tweak::Selection &Inputs,
return error("Cannot find place to insert \"using\"");
}
-bool isNamespaceForbidden(const Tweak::Selection &Inputs,
- NestedNameSpecifier Namespace) {
+static bool isNamespaceForbidden(const Tweak::Selection &Inputs,
+ NestedNameSpecifier Namespace) {
const auto *NS =
dyn_cast<NamespaceDecl>(Namespace.getAsNamespaceAndPrefix().Namespace);
if (!NS)
@@ -200,15 +198,15 @@ bool isNamespaceForbidden(const Tweak::Selection &Inputs,
return false;
}
-std::string getNNSLAsString(NestedNameSpecifierLoc NNSL,
- const PrintingPolicy &Policy) {
+static std::string getNNSLAsString(NestedNameSpecifierLoc NNSL,
+ const PrintingPolicy &Policy) {
std::string Out;
llvm::raw_string_ostream OutStream(Out);
NNSL.getNestedNameSpecifier().print(OutStream, Policy);
return OutStream.str();
}
-const NamedDecl *canonicalDecl(const NamedDecl *D) {
+static const NamedDecl *canonicalDecl(const NamedDecl *D) {
return D ? llvm::dyn_cast<NamedDecl>(D->getCanonicalDecl()) : nullptr;
}
@@ -440,6 +438,5 @@ Expected<Tweak::Effect> AddUsingReplaceAll::apply(const Selection &Inputs) {
return Effect::mainFileEdit(SM, std::move(Repls));
}
-} // namespace
} // namespace clangd
} // namespace clang
More information about the cfe-commits
mailing list