354 lines · cpp
1//===--- HeaderSourceSwitchTests.cpp - ---------------------------*- C++-*-===//2//3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.4// See https://llvm.org/LICENSE.txt for license information.5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception6//7//===----------------------------------------------------------------------===//8 9#include "HeaderSourceSwitch.h"10 11#include "SyncAPI.h"12#include "TestFS.h"13#include "TestTU.h"14#include "index/MemIndex.h"15#include "support/Path.h"16#include "llvm/Testing/Support/SupportHelpers.h"17#include "gmock/gmock.h"18#include "gtest/gtest.h"19#include <optional>20 21namespace clang {22namespace clangd {23namespace {24 25TEST(HeaderSourceSwitchTest, FileHeuristic) {26 MockFS FS;27 auto FooCpp = testPath("foo.cpp");28 auto FooH = testPath("foo.h");29 auto Invalid = testPath("main.cpp");30 31 FS.Files[FooCpp];32 FS.Files[FooH];33 FS.Files[Invalid];34 std::optional<Path> PathResult =35 getCorrespondingHeaderOrSource(FooCpp, FS.view(std::nullopt));36 EXPECT_TRUE(PathResult.has_value());37 ASSERT_EQ(*PathResult, FooH);38 39 PathResult = getCorrespondingHeaderOrSource(FooH, FS.view(std::nullopt));40 EXPECT_TRUE(PathResult.has_value());41 ASSERT_EQ(*PathResult, FooCpp);42 43 // Test with header file in capital letters and different extension, source44 // file with different extension45 auto FooC = testPath("bar.c");46 auto FooHH = testPath("bar.HH");47 48 FS.Files[FooC];49 FS.Files[FooHH];50 PathResult = getCorrespondingHeaderOrSource(FooC, FS.view(std::nullopt));51 EXPECT_TRUE(PathResult.has_value());52 ASSERT_EQ(*PathResult, FooHH);53 54 // Test with both capital letters55 auto Foo2C = testPath("foo2.C");56 auto Foo2HH = testPath("foo2.HH");57 FS.Files[Foo2C];58 FS.Files[Foo2HH];59 PathResult = getCorrespondingHeaderOrSource(Foo2C, FS.view(std::nullopt));60 EXPECT_TRUE(PathResult.has_value());61 ASSERT_EQ(*PathResult, Foo2HH);62 63 // Test with source file as capital letter and .hxx header file64 auto Foo3C = testPath("foo3.C");65 auto Foo3HXX = testPath("foo3.hxx");66 67 FS.Files[Foo3C];68 FS.Files[Foo3HXX];69 PathResult = getCorrespondingHeaderOrSource(Foo3C, FS.view(std::nullopt));70 EXPECT_TRUE(PathResult.has_value());71 ASSERT_EQ(*PathResult, Foo3HXX);72 73 // Test if asking for a corresponding file that doesn't exist returns an empty74 // string.75 PathResult = getCorrespondingHeaderOrSource(Invalid, FS.view(std::nullopt));76 EXPECT_FALSE(PathResult.has_value());77}78 79TEST(HeaderSourceSwitchTest, ModuleInterfaces) {80 MockFS FS;81 82 auto FooCC = testPath("foo.cc");83 auto FooCPPM = testPath("foo.cppm");84 FS.Files[FooCC];85 FS.Files[FooCPPM];86 std::optional<Path> PathResult =87 getCorrespondingHeaderOrSource(FooCC, FS.view(std::nullopt));88 EXPECT_TRUE(PathResult.has_value());89 ASSERT_EQ(*PathResult, FooCPPM);90 91 auto Foo2CPP = testPath("foo2.cpp");92 auto Foo2CCM = testPath("foo2.ccm");93 FS.Files[Foo2CPP];94 FS.Files[Foo2CCM];95 PathResult = getCorrespondingHeaderOrSource(Foo2CPP, FS.view(std::nullopt));96 EXPECT_TRUE(PathResult.has_value());97 ASSERT_EQ(*PathResult, Foo2CCM);98 99 auto Foo3CXX = testPath("foo3.cxx");100 auto Foo3CXXM = testPath("foo3.cxxm");101 FS.Files[Foo3CXX];102 FS.Files[Foo3CXXM];103 PathResult = getCorrespondingHeaderOrSource(Foo3CXX, FS.view(std::nullopt));104 EXPECT_TRUE(PathResult.has_value());105 ASSERT_EQ(*PathResult, Foo3CXXM);106 107 auto Foo4CPLUSPLUS = testPath("foo4.c++");108 auto Foo4CPLUSPLUSM = testPath("foo4.c++m");109 FS.Files[Foo4CPLUSPLUS];110 FS.Files[Foo4CPLUSPLUSM];111 PathResult =112 getCorrespondingHeaderOrSource(Foo4CPLUSPLUS, FS.view(std::nullopt));113 EXPECT_TRUE(PathResult.has_value());114 ASSERT_EQ(*PathResult, Foo4CPLUSPLUSM);115}116 117MATCHER_P(declNamed, Name, "") {118 if (const NamedDecl *ND = dyn_cast<NamedDecl>(arg))119 if (ND->getQualifiedNameAsString() == Name)120 return true;121 return false;122}123 124TEST(HeaderSourceSwitchTest, GetLocalDecls) {125 TestTU TU;126 TU.HeaderCode = R"cpp(127 void HeaderOnly();128 )cpp";129 TU.Code = R"cpp(130 void MainF1();131 class Foo {};132 namespace ns {133 class Foo {134 void method();135 int field;136 };137 } // namespace ns138 139 // Non-indexable symbols140 namespace {141 void Ignore1() {}142 }143 144 )cpp";145 146 auto AST = TU.build();147 EXPECT_THAT(getIndexableLocalDecls(AST),148 testing::UnorderedElementsAre(149 declNamed("MainF1"), declNamed("Foo"), declNamed("ns::Foo"),150 declNamed("ns::Foo::method"), declNamed("ns::Foo::field")));151}152 153TEST(HeaderSourceSwitchTest, FromHeaderToSource) {154 // build a proper index, which contains symbols:155 // A_Sym1, declared in TestTU.h, defined in a.cpp156 // B_Sym[1-2], declared in TestTU.h, defined in b.cpp157 SymbolSlab::Builder AllSymbols;158 TestTU Testing;159 Testing.HeaderFilename = "TestTU.h";160 Testing.HeaderCode = "void A_Sym1();";161 Testing.Filename = "a.cpp";162 Testing.Code = "void A_Sym1() {};";163 for (auto &Sym : Testing.headerSymbols())164 AllSymbols.insert(Sym);165 166 Testing.HeaderCode = R"cpp(167 void B_Sym1();168 void B_Sym2();169 void B_Sym3_NoDef();170 )cpp";171 Testing.Filename = "b.cpp";172 Testing.Code = R"cpp(173 void B_Sym1() {}174 void B_Sym2() {}175 )cpp";176 for (auto &Sym : Testing.headerSymbols())177 AllSymbols.insert(Sym);178 auto Index = MemIndex::build(std::move(AllSymbols).build(), {}, {});179 180 // Test for switch from .h header to .cc source181 struct {182 llvm::StringRef HeaderCode;183 std::optional<std::string> ExpectedSource;184 } TestCases[] = {185 {"// empty, no header found", std::nullopt},186 {R"cpp(187 // no definition found in the index.188 void NonDefinition();189 )cpp",190 std::nullopt},191 {R"cpp(192 void A_Sym1();193 )cpp",194 testPath("a.cpp")},195 {R"cpp(196 // b.cpp wins.197 void A_Sym1();198 void B_Sym1();199 void B_Sym2();200 )cpp",201 testPath("b.cpp")},202 {R"cpp(203 // a.cpp and b.cpp have same scope, but a.cpp because "a.cpp" < "b.cpp".204 void A_Sym1();205 void B_Sym1();206 )cpp",207 testPath("a.cpp")},208 209 {R"cpp(210 // We don't have definition in the index, so stay in the header.211 void B_Sym3_NoDef();212 )cpp",213 std::nullopt},214 };215 for (const auto &Case : TestCases) {216 TestTU TU = TestTU::withCode(Case.HeaderCode);217 TU.Filename = "TestTU.h";218 TU.ExtraArgs.push_back("-xc++-header"); // inform clang this is a header.219 auto HeaderAST = TU.build();220 EXPECT_EQ(Case.ExpectedSource,221 getCorrespondingHeaderOrSource(testPath(TU.Filename), HeaderAST,222 Index.get()));223 }224}225 226TEST(HeaderSourceSwitchTest, FromSourceToHeader) {227 // build a proper index, which contains symbols:228 // A_Sym1, declared in a.h, defined in TestTU.cpp229 // B_Sym[1-2], declared in b.h, defined in TestTU.cpp230 TestTU TUForIndex = TestTU::withCode(R"cpp(231 #include "a.h"232 #include "b.h"233 234 void A_Sym1() {}235 236 void B_Sym1() {}237 void B_Sym2() {}238 )cpp");239 TUForIndex.AdditionalFiles["a.h"] = R"cpp(240 void A_Sym1();241 )cpp";242 TUForIndex.AdditionalFiles["b.h"] = R"cpp(243 void B_Sym1();244 void B_Sym2();245 )cpp";246 TUForIndex.Filename = "TestTU.cpp";247 auto Index = TUForIndex.index();248 249 // Test for switching from .cc source file to .h header.250 struct {251 llvm::StringRef SourceCode;252 std::optional<std::string> ExpectedResult;253 } TestCases[] = {254 {"// empty, no header found", std::nullopt},255 {R"cpp(256 // symbol not in index, no header found257 void Local() {}258 )cpp",259 std::nullopt},260 261 {R"cpp(262 // a.h wins.263 void A_Sym1() {}264 )cpp",265 testPath("a.h")},266 267 {R"cpp(268 // b.h wins.269 void A_Sym1() {}270 void B_Sym1() {}271 void B_Sym2() {}272 )cpp",273 testPath("b.h")},274 275 {R"cpp(276 // a.h and b.h have same scope, but a.h wins because "a.h" < "b.h".277 void A_Sym1() {}278 void B_Sym1() {}279 )cpp",280 testPath("a.h")},281 };282 for (const auto &Case : TestCases) {283 TestTU TU = TestTU::withCode(Case.SourceCode);284 TU.Filename = "Test.cpp";285 auto AST = TU.build();286 EXPECT_EQ(Case.ExpectedResult,287 getCorrespondingHeaderOrSource(testPath(TU.Filename), AST,288 Index.get()));289 }290}291 292TEST(HeaderSourceSwitchTest, ClangdServerIntegration) {293 MockCompilationDatabase CDB;294 CDB.ExtraClangFlags = {"-I" +295 testPath("src/include")}; // add search directory.296 MockFS FS;297 // File heuristic fails here, we rely on the index to find the .h file.298 std::string CppPath = testPath("src/lib/test.cpp");299 std::string HeaderPath = testPath("src/include/test.h");300 FS.Files[HeaderPath] = "void foo();";301 const std::string FileContent = R"cpp(302 #include "test.h"303 void foo() {};304 )cpp";305 FS.Files[CppPath] = FileContent;306 auto Options = ClangdServer::optsForTest();307 Options.BuildDynamicSymbolIndex = true;308 ClangdServer Server(CDB, FS, Options);309 runAddDocument(Server, CppPath, FileContent);310 EXPECT_EQ(HeaderPath,311 *llvm::cantFail(runSwitchHeaderSource(Server, CppPath)));312}313 314TEST(HeaderSourceSwitchTest, CaseSensitivity) {315 TestTU TU = TestTU::withCode("void foo() {}");316 // Define more symbols in the header than the source file to trick heuristics317 // into picking the header as source file, if the matching for header file318 // path fails.319 TU.HeaderCode = R"cpp(320 inline void bar1() {}321 inline void bar2() {}322 void foo();)cpp";323 // Give main file and header different base names to make sure file system324 // heuristics don't work.325 TU.Filename = "Source.cpp";326 TU.HeaderFilename = "Header.h";327 328 auto Index = TU.index();329 TU.Code = std::move(TU.HeaderCode);330 TU.HeaderCode.clear();331 auto AST = TU.build();332 333 // Provide a different-cased filename in the query than what we have in the334 // index, check if we can still find the source file, which defines less335 // symbols than the header.336 auto HeaderAbsPath = testPath("HEADER.H");337 // We expect the heuristics to pick:338 // - header on case sensitive file systems, because the HeaderAbsPath doesn't339 // match what we've seen through index.340 // - source on case insensitive file systems, as the HeaderAbsPath would match341 // the filename in index.342#ifdef CLANGD_PATH_CASE_INSENSITIVE343 EXPECT_THAT(getCorrespondingHeaderOrSource(HeaderAbsPath, AST, Index.get()),344 llvm::ValueIs(testing::StrCaseEq(testPath(TU.Filename))));345#else346 EXPECT_THAT(getCorrespondingHeaderOrSource(HeaderAbsPath, AST, Index.get()),347 llvm::ValueIs(testing::StrCaseEq(testPath(TU.HeaderFilename))));348#endif349}350 351} // namespace352} // namespace clangd353} // namespace clang354