From d828397b1ece13b9617b54e5bd75f00c2c69fd64 Mon Sep 17 00:00:00 2001 From: JVApen Date: Sat, 13 Sep 2025 10:22:28 +0200 Subject: [PATCH 1/7] Fixes clangd/clangd#1797 Do not restrict HeaderSourceSwitch to switching between .h and .cpp. For templates, the relevant 'source' is the header containing the implementation of the templated functions. Instead of considering every symbol in the current file as relevant, we restrict it to those that either have the declaration or definition in that file. --- .../clangd/HeaderSourceSwitch.cpp | 42 +++-- .../unittests/HeaderSourceSwitchTests.cpp | 158 +++++++++++++++++- 2 files changed, 183 insertions(+), 17 deletions(-) diff --git a/clang-tools-extra/clangd/HeaderSourceSwitch.cpp b/clang-tools-extra/clangd/HeaderSourceSwitch.cpp index ee4bea1401490..494760d6ffdd3 100644 --- a/clang-tools-extra/clangd/HeaderSourceSwitch.cpp +++ b/clang-tools-extra/clangd/HeaderSourceSwitch.cpp @@ -82,26 +82,36 @@ std::optional getCorrespondingHeaderOrSource(PathRef OriginalFile, Request.IDs.insert(ID); } llvm::StringMap Candidates; // Target path => score. - auto AwardTarget = [&](const char *TargetURI) { - if (auto TargetPath = URI::resolve(TargetURI, OriginalFile)) { - if (!pathEqual(*TargetPath, OriginalFile)) // exclude the original file. - ++Candidates[*TargetPath]; - } else { - elog("Failed to resolve URI {0}: {1}", TargetURI, TargetPath.takeError()); - } - }; - // If we switch from a header, we are looking for the implementation - // file, so we use the definition loc; otherwise we look for the header file, - // we use the decl loc; + // When in the implementation file, we always search for the header file, + // using the decl loc. When we are in a header, this usually implies searching + // for implementation, for which we use the definition loc. For templates, we + // can have separate implementation headers, which behave as an implementation + // file. As such, we always have to add the decl loc and conditionally + // definition loc. // // For each symbol in the original file, we get its target location (decl or // def) from the index, then award that target file. - const bool IsHeader = isHeaderFile(OriginalFile, AST.getLangOpts()); +#ifdef CLANGD_PATH_CASE_INSENSITIVE + auto pathEqual = [](const llvm::StringRef &l, const llvm::StringRef &r) { + return l.equals_insensitive(r); + }; +#else + auto pathEqual = [](const llvm::StringRef &l, const llvm::StringRef &r) { + return l.equals(r); + }; +#endif Index->lookup(Request, [&](const Symbol &Sym) { - if (IsHeader) - AwardTarget(Sym.Definition.FileURI); - else - AwardTarget(Sym.CanonicalDeclaration.FileURI); + auto TargetPathDefinition = + URI::resolve(Sym.Definition.FileURI, OriginalFile); + auto TargetPathDeclaration = + URI::resolve(Sym.CanonicalDeclaration.FileURI, OriginalFile); + if (!TargetPathDefinition || !TargetPathDeclaration) + return; + if (pathEqual(*TargetPathDefinition, OriginalFile)) { + if (!pathEqual(*TargetPathDeclaration, OriginalFile)) + ++Candidates[*TargetPathDeclaration]; + } else if (pathEqual(*TargetPathDeclaration, OriginalFile)) + ++Candidates[*TargetPathDefinition]; }); // FIXME: our index doesn't have any interesting information (this could be // that the background-index is not finished), we should use the decl/def diff --git a/clang-tools-extra/clangd/unittests/HeaderSourceSwitchTests.cpp b/clang-tools-extra/clangd/unittests/HeaderSourceSwitchTests.cpp index e600207de458a..8dd480968fdc6 100644 --- a/clang-tools-extra/clangd/unittests/HeaderSourceSwitchTests.cpp +++ b/clang-tools-extra/clangd/unittests/HeaderSourceSwitchTests.cpp @@ -223,6 +223,162 @@ TEST(HeaderSourceSwitchTest, FromHeaderToSource) { } } +TEST(HeaderSourceSwitchTest, FromHeaderToImplHeader) { + // build a proper index, which contains symbols: + // A_Sym1, declared in TestTU.h, defined in a_ext.h + // B_Sym[1-2], declared in TestTU.h, defined in b_ext.h + SymbolSlab::Builder AllSymbols; + auto addHeader = [&](auto implName, auto declarationContent, + auto implContent) { + TestTU Testing; + Testing.ExtraArgs.push_back( + "-xc++-header"); // inform clang this is a header. + + Testing.Filename = implName; + Testing.Code = implContent; + Testing.HeaderFilename = "TestTU.h"; + Testing.HeaderCode = declarationContent; + + for (auto &Sym : Testing.headerSymbols()) + AllSymbols.insert(Sym); + }; + + addHeader("a_ext.h", "template void A_Sym1();", + "template void A_Sym1() {};"); + addHeader("b_ext.h", R"cpp( + template void B_Sym1(); + template void B_Sym2(); + template void B_Sym3_NoDef(); + )cpp", + R"cpp( + template void B_Sym1() {} + template void B_Sym2() {} + )cpp"); + auto Index = MemIndex::build(std::move(AllSymbols).build(), {}, {}); + + // Test for switch from .h header to .cc source + struct { + llvm::StringRef HeaderCode; + std::optional ExpectedSource; + } TestCases[] = { + {"// empty, no header found", std::nullopt}, + {R"cpp( + // no definition found in the index. + template void NonDefinition(); + )cpp", + std::nullopt}, + {R"cpp( + template void A_Sym1(); + )cpp", + testPath("a_ext.h")}, + {R"cpp( + // b_ext.h wins. + template void A_Sym1(); + template void B_Sym1(); + template void B_Sym2(); + )cpp", + testPath("b_ext.h")}, + {R"cpp( + // a_ext.h and b_ext.h have same scope, but a_ext.h because "a_ext.h" < "b_ext.h". + template void A_Sym1(); + template void B_Sym1(); + )cpp", + testPath("a_ext.h")}, + + {R"cpp( + // We don't have definition in the index, so stay in the header. + template void B_Sym3_NoDef(); + )cpp", + std::nullopt}, + }; + for (const auto &Case : TestCases) { + TestTU TU = TestTU::withCode(Case.HeaderCode); + TU.Filename = "TestTU.h"; + TU.ExtraArgs.push_back("-xc++-header"); // inform clang this is a header. + auto HeaderAST = TU.build(); + EXPECT_EQ(Case.ExpectedSource, + getCorrespondingHeaderOrSource(testPath(TU.Filename), HeaderAST, + Index.get())); + } +} + +TEST(HeaderSourceSwitchTest, FromImplHeaderToHeader) { + // build a proper index, which contains symbols: + // A_Sym1, declared in TestTU.h, defined in a_ext.h + // B_Sym[1-2], declared in TestTU.h, defined in b_ext.h + SymbolSlab::Builder AllSymbols; + auto addHeader = [&](auto declName, auto declContent, auto implContent) { + TestTU Testing; + Testing.ExtraArgs.push_back( + "-xc++-header"); // inform clang this is a header. + + Testing.Filename = "TestTU.h"; + Testing.Code = implContent; + Testing.HeaderFilename = declName; + Testing.HeaderCode = declContent; + + for (auto &Sym : Testing.headerSymbols()) + AllSymbols.insert(Sym); + }; + + addHeader("a.h", "template void A_Sym1();", + "template void A_Sym1() {};"); + addHeader("b.h", R"cpp( + template void B_Sym1(); + template void B_Sym2(); + template void B_Sym3_NoDef(); + )cpp", + R"cpp( + template void B_Sym1() {} + template void B_Sym2() {} + )cpp"); + auto Index = MemIndex::build(std::move(AllSymbols).build(), {}, {}); + + // Test for switch from .h header to .cc source + struct { + llvm::StringRef HeaderCode; + std::optional ExpectedSource; + } TestCases[] = { + {"// empty, no header found", std::nullopt}, + {R"cpp( + // no definition found in the index. + template void NonDefinition(){} + )cpp", + std::nullopt}, + {R"cpp( + template void A_Sym1(){} + )cpp", + testPath("a.h")}, + {R"cpp( + // b.h wins. + template void A_Sym1(){} + template void B_Sym1(){} + template void B_Sym2(){} + )cpp", + testPath("b.h")}, + {R"cpp( + // a.h and b.h have same scope, but a.h because "a.h" < "b.h". + template void A_Sym1(){} + template void B_Sym1(){} + )cpp", + testPath("a.h")}, + + {R"cpp( + // We don't have definition in the index, so stay in the header. + template void B_Sym3_NoDef(); + )cpp", + std::nullopt}, + }; + for (const auto &Case : TestCases) { + TestTU TU = TestTU::withCode(Case.HeaderCode); + TU.Filename = "TestTU.h"; + TU.ExtraArgs.push_back("-xc++-header"); // inform clang this is a header. + auto HeaderAST = TU.build(); + EXPECT_EQ(Case.ExpectedSource, + getCorrespondingHeaderOrSource(testPath(TU.Filename), HeaderAST, + Index.get())); + } +} TEST(HeaderSourceSwitchTest, FromSourceToHeader) { // build a proper index, which contains symbols: // A_Sym1, declared in a.h, defined in TestTU.cpp @@ -281,7 +437,7 @@ TEST(HeaderSourceSwitchTest, FromSourceToHeader) { }; for (const auto &Case : TestCases) { TestTU TU = TestTU::withCode(Case.SourceCode); - TU.Filename = "Test.cpp"; + TU.Filename = "TestTU.cpp"; auto AST = TU.build(); EXPECT_EQ(Case.ExpectedResult, getCorrespondingHeaderOrSource(testPath(TU.Filename), AST, From 3ea26896e248ee68798d7df3dc26fab7a4f51d88 Mon Sep 17 00:00:00 2001 From: JVApen Date: Sun, 14 Sep 2025 08:57:09 +0200 Subject: [PATCH 2/7] Fix linux build --- clang-tools-extra/clangd/HeaderSourceSwitch.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clang-tools-extra/clangd/HeaderSourceSwitch.cpp b/clang-tools-extra/clangd/HeaderSourceSwitch.cpp index 494760d6ffdd3..e8a44528d6297 100644 --- a/clang-tools-extra/clangd/HeaderSourceSwitch.cpp +++ b/clang-tools-extra/clangd/HeaderSourceSwitch.cpp @@ -97,7 +97,7 @@ std::optional getCorrespondingHeaderOrSource(PathRef OriginalFile, }; #else auto pathEqual = [](const llvm::StringRef &l, const llvm::StringRef &r) { - return l.equals(r); + return l == r; }; #endif Index->lookup(Request, [&](const Symbol &Sym) { From cce4abeb315e7407d068dff990330133740d57ad Mon Sep 17 00:00:00 2001 From: JVApen Date: Sun, 14 Sep 2025 08:58:46 +0200 Subject: [PATCH 3/7] StringRef by value --- clang-tools-extra/clangd/HeaderSourceSwitch.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/clang-tools-extra/clangd/HeaderSourceSwitch.cpp b/clang-tools-extra/clangd/HeaderSourceSwitch.cpp index e8a44528d6297..9a110b986c6ed 100644 --- a/clang-tools-extra/clangd/HeaderSourceSwitch.cpp +++ b/clang-tools-extra/clangd/HeaderSourceSwitch.cpp @@ -92,13 +92,11 @@ std::optional getCorrespondingHeaderOrSource(PathRef OriginalFile, // For each symbol in the original file, we get its target location (decl or // def) from the index, then award that target file. #ifdef CLANGD_PATH_CASE_INSENSITIVE - auto pathEqual = [](const llvm::StringRef &l, const llvm::StringRef &r) { + auto pathEqual = [](llvm::StringRef l, llvm::StringRef r) { return l.equals_insensitive(r); }; #else - auto pathEqual = [](const llvm::StringRef &l, const llvm::StringRef &r) { - return l == r; - }; + auto pathEqual = [](llvm::StringRef l, llvm::StringRef r) { return l == r; }; #endif Index->lookup(Request, [&](const Symbol &Sym) { auto TargetPathDefinition = From 4b88198baa91824b56925f578f176b9611a03866 Mon Sep 17 00:00:00 2001 From: JVApen Date: Sun, 14 Sep 2025 09:51:42 +0200 Subject: [PATCH 4/7] Ensure class templates are handled correctly --- .../clangd/HeaderSourceSwitch.cpp | 5 +++ .../unittests/HeaderSourceSwitchTests.cpp | 42 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/clang-tools-extra/clangd/HeaderSourceSwitch.cpp b/clang-tools-extra/clangd/HeaderSourceSwitch.cpp index 9a110b986c6ed..f8bc425fac7e5 100644 --- a/clang-tools-extra/clangd/HeaderSourceSwitch.cpp +++ b/clang-tools-extra/clangd/HeaderSourceSwitch.cpp @@ -147,6 +147,11 @@ std::vector getIndexableLocalDecls(ParsedAST &AST) { for (auto *D : Scope->decls()) TraverseDecl(D); } + // ClassTemplateDecl does not inherit from DeclContext + if (auto *Scope = llvm::dyn_cast(ND)) { + for (auto *D : Scope->getTemplatedDecl()->decls()) + TraverseDecl(D); + } } if (llvm::isa(D)) return; // namespace is indexable, but we're not interested. diff --git a/clang-tools-extra/clangd/unittests/HeaderSourceSwitchTests.cpp b/clang-tools-extra/clangd/unittests/HeaderSourceSwitchTests.cpp index 8dd480968fdc6..237a174c4f9c6 100644 --- a/clang-tools-extra/clangd/unittests/HeaderSourceSwitchTests.cpp +++ b/clang-tools-extra/clangd/unittests/HeaderSourceSwitchTests.cpp @@ -379,6 +379,48 @@ TEST(HeaderSourceSwitchTest, FromImplHeaderToHeader) { Index.get())); } } + +TEST(HeaderSourceSwitchTest, FromHeaderToImplHeaderClassTemplate) { + // build a proper index, which contains symbols: + // A_Sym1, declared in TestTU.h, defined in a_ext.h + // B_Sym[1-2], declared in TestTU.h, defined in b_ext.h + SymbolSlab::Builder AllSymbols; + TestTU Testing; + Testing.Filename = "a.cpp"; + Testing.Code = R"cpp(void f() {} + void g(){})cpp"; + Testing.HeaderFilename = "TestTU.h"; + Testing.HeaderCode = R"cpp(void f(); + void g(); + template struct S + { + void a(); + void b(); + void c(); + };)cpp"; + for (auto &Sym : Testing.headerSymbols()) + AllSymbols.insert(Sym); + + Testing.Filename = "b_ext.h"; + Testing.Code = + R"cpp(template void S::a(){} + template void S::b(){} + template void S::c(){})cpp"; + Testing.ExtraArgs.push_back("-xc++-header"); // inform clang this is a header. + + for (auto &Sym : Testing.headerSymbols()) + AllSymbols.insert(Sym); + auto Index = MemIndex::build(std::move(AllSymbols).build(), {}, {}); + + TestTU TU = TestTU::withCode(Testing.HeaderCode); + TU.Filename = "TestTU.h"; + TU.ExtraArgs.push_back("-xc++-header"); // inform clang this is a header. + auto HeaderAST = TU.build(); + EXPECT_EQ(testPath("b_ext.h"), + getCorrespondingHeaderOrSource(testPath(TU.Filename), HeaderAST, + Index.get())); +} + TEST(HeaderSourceSwitchTest, FromSourceToHeader) { // build a proper index, which contains symbols: // A_Sym1, declared in a.h, defined in TestTU.cpp From db2f4000183fc565cccd869037885714f7967db0 Mon Sep 17 00:00:00 2001 From: JVApen Date: Sun, 14 Sep 2025 09:53:50 +0200 Subject: [PATCH 5/7] Ensure expected is always handled --- clang-tools-extra/clangd/HeaderSourceSwitch.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/clang-tools-extra/clangd/HeaderSourceSwitch.cpp b/clang-tools-extra/clangd/HeaderSourceSwitch.cpp index f8bc425fac7e5..37a41c48e836e 100644 --- a/clang-tools-extra/clangd/HeaderSourceSwitch.cpp +++ b/clang-tools-extra/clangd/HeaderSourceSwitch.cpp @@ -101,9 +101,11 @@ std::optional getCorrespondingHeaderOrSource(PathRef OriginalFile, Index->lookup(Request, [&](const Symbol &Sym) { auto TargetPathDefinition = URI::resolve(Sym.Definition.FileURI, OriginalFile); + if (!TargetPathDefinition) + return; auto TargetPathDeclaration = URI::resolve(Sym.CanonicalDeclaration.FileURI, OriginalFile); - if (!TargetPathDefinition || !TargetPathDeclaration) + if (!TargetPathDeclaration) return; if (pathEqual(*TargetPathDefinition, OriginalFile)) { if (!pathEqual(*TargetPathDeclaration, OriginalFile)) From 4ca5cf6e7219c19ce216a1b60c964c6c9e6aaebc Mon Sep 17 00:00:00 2001 From: JVApen Date: Sun, 14 Sep 2025 16:27:31 +0200 Subject: [PATCH 6/7] Resolve issue unhandled Expected --- clang-tools-extra/clangd/HeaderSourceSwitch.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/clang-tools-extra/clangd/HeaderSourceSwitch.cpp b/clang-tools-extra/clangd/HeaderSourceSwitch.cpp index 37a41c48e836e..f3477047c2508 100644 --- a/clang-tools-extra/clangd/HeaderSourceSwitch.cpp +++ b/clang-tools-extra/clangd/HeaderSourceSwitch.cpp @@ -99,6 +99,9 @@ std::optional getCorrespondingHeaderOrSource(PathRef OriginalFile, auto pathEqual = [](llvm::StringRef l, llvm::StringRef r) { return l == r; }; #endif Index->lookup(Request, [&](const Symbol &Sym) { + if (llvm::StringRef{Sym.Definition.FileURI}.empty() || + llvm::StringRef{Sym.CanonicalDeclaration.FileURI}.empty()) + return; auto TargetPathDefinition = URI::resolve(Sym.Definition.FileURI, OriginalFile); if (!TargetPathDefinition) From 1469aba89062a39ed8f35ec811ffe194055ae414 Mon Sep 17 00:00:00 2001 From: JVApen Date: Sun, 14 Sep 2025 16:50:02 +0200 Subject: [PATCH 7/7] Change test as new algorithm results in nullopt --- .../clangd/unittests/HeaderSourceSwitchTests.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/clang-tools-extra/clangd/unittests/HeaderSourceSwitchTests.cpp b/clang-tools-extra/clangd/unittests/HeaderSourceSwitchTests.cpp index 237a174c4f9c6..181dfddb34177 100644 --- a/clang-tools-extra/clangd/unittests/HeaderSourceSwitchTests.cpp +++ b/clang-tools-extra/clangd/unittests/HeaderSourceSwitchTests.cpp @@ -535,14 +535,14 @@ TEST(HeaderSourceSwitchTest, CaseSensitivity) { // We expect the heuristics to pick: // - header on case sensitive file systems, because the HeaderAbsPath doesn't // match what we've seen through index. - // - source on case insensitive file systems, as the HeaderAbsPath would match - // the filename in index. + // - nullopt on case insensitive file systems, as the HeaderAbsPath doesn't + // match the filename in index. #ifdef CLANGD_PATH_CASE_INSENSITIVE EXPECT_THAT(getCorrespondingHeaderOrSource(HeaderAbsPath, AST, Index.get()), llvm::ValueIs(testing::StrCaseEq(testPath(TU.Filename)))); #else - EXPECT_THAT(getCorrespondingHeaderOrSource(HeaderAbsPath, AST, Index.get()), - llvm::ValueIs(testing::StrCaseEq(testPath(TU.HeaderFilename)))); + EXPECT_EQ(std::optional{}, + getCorrespondingHeaderOrSource(HeaderAbsPath, AST, Index.get())); #endif }