| 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-exception |
| 6 | // |
| 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 | |
| 21 | namespace clang { |
| 22 | namespace clangd { |
| 23 | namespace { |
| 24 | |
| 25 | TEST(HeaderSourceSwitchTest, FileHeuristic) { |
| 26 | MockFS FS; |
| 27 | auto FooCpp = testPath(File: "foo.cpp" ); |
| 28 | auto FooH = testPath(File: "foo.h" ); |
| 29 | auto Invalid = testPath(File: "main.cpp" ); |
| 30 | |
| 31 | FS.Files[FooCpp]; |
| 32 | FS.Files[FooH]; |
| 33 | FS.Files[Invalid]; |
| 34 | std::optional<Path> PathResult = |
| 35 | getCorrespondingHeaderOrSource(OriginalFile: FooCpp, VFS: FS.view(CWD: std::nullopt)); |
| 36 | EXPECT_TRUE(PathResult.has_value()); |
| 37 | ASSERT_EQ(*PathResult, FooH); |
| 38 | |
| 39 | PathResult = getCorrespondingHeaderOrSource(OriginalFile: FooH, VFS: FS.view(CWD: 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, source |
| 44 | // file with different extension |
| 45 | auto FooC = testPath(File: "bar.c" ); |
| 46 | auto FooHH = testPath(File: "bar.HH" ); |
| 47 | |
| 48 | FS.Files[FooC]; |
| 49 | FS.Files[FooHH]; |
| 50 | PathResult = getCorrespondingHeaderOrSource(OriginalFile: FooC, VFS: FS.view(CWD: std::nullopt)); |
| 51 | EXPECT_TRUE(PathResult.has_value()); |
| 52 | ASSERT_EQ(*PathResult, FooHH); |
| 53 | |
| 54 | // Test with both capital letters |
| 55 | auto Foo2C = testPath(File: "foo2.C" ); |
| 56 | auto Foo2HH = testPath(File: "foo2.HH" ); |
| 57 | FS.Files[Foo2C]; |
| 58 | FS.Files[Foo2HH]; |
| 59 | PathResult = getCorrespondingHeaderOrSource(OriginalFile: Foo2C, VFS: FS.view(CWD: 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 file |
| 64 | auto Foo3C = testPath(File: "foo3.C" ); |
| 65 | auto Foo3HXX = testPath(File: "foo3.hxx" ); |
| 66 | |
| 67 | FS.Files[Foo3C]; |
| 68 | FS.Files[Foo3HXX]; |
| 69 | PathResult = getCorrespondingHeaderOrSource(OriginalFile: Foo3C, VFS: FS.view(CWD: 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 empty |
| 74 | // string. |
| 75 | PathResult = getCorrespondingHeaderOrSource(OriginalFile: Invalid, VFS: FS.view(CWD: std::nullopt)); |
| 76 | EXPECT_FALSE(PathResult.has_value()); |
| 77 | } |
| 78 | |
| 79 | TEST(HeaderSourceSwitchTest, ModuleInterfaces) { |
| 80 | MockFS FS; |
| 81 | |
| 82 | auto FooCC = testPath(File: "foo.cc" ); |
| 83 | auto FooCPPM = testPath(File: "foo.cppm" ); |
| 84 | FS.Files[FooCC]; |
| 85 | FS.Files[FooCPPM]; |
| 86 | std::optional<Path> PathResult = |
| 87 | getCorrespondingHeaderOrSource(OriginalFile: FooCC, VFS: FS.view(CWD: std::nullopt)); |
| 88 | EXPECT_TRUE(PathResult.has_value()); |
| 89 | ASSERT_EQ(*PathResult, FooCPPM); |
| 90 | |
| 91 | auto Foo2CPP = testPath(File: "foo2.cpp" ); |
| 92 | auto Foo2CCM = testPath(File: "foo2.ccm" ); |
| 93 | FS.Files[Foo2CPP]; |
| 94 | FS.Files[Foo2CCM]; |
| 95 | PathResult = getCorrespondingHeaderOrSource(OriginalFile: Foo2CPP, VFS: FS.view(CWD: std::nullopt)); |
| 96 | EXPECT_TRUE(PathResult.has_value()); |
| 97 | ASSERT_EQ(*PathResult, Foo2CCM); |
| 98 | |
| 99 | auto Foo3CXX = testPath(File: "foo3.cxx" ); |
| 100 | auto Foo3CXXM = testPath(File: "foo3.cxxm" ); |
| 101 | FS.Files[Foo3CXX]; |
| 102 | FS.Files[Foo3CXXM]; |
| 103 | PathResult = getCorrespondingHeaderOrSource(OriginalFile: Foo3CXX, VFS: FS.view(CWD: std::nullopt)); |
| 104 | EXPECT_TRUE(PathResult.has_value()); |
| 105 | ASSERT_EQ(*PathResult, Foo3CXXM); |
| 106 | |
| 107 | auto Foo4CPLUSPLUS = testPath(File: "foo4.c++" ); |
| 108 | auto Foo4CPLUSPLUSM = testPath(File: "foo4.c++m" ); |
| 109 | FS.Files[Foo4CPLUSPLUS]; |
| 110 | FS.Files[Foo4CPLUSPLUSM]; |
| 111 | PathResult = |
| 112 | getCorrespondingHeaderOrSource(OriginalFile: Foo4CPLUSPLUS, VFS: FS.view(CWD: std::nullopt)); |
| 113 | EXPECT_TRUE(PathResult.has_value()); |
| 114 | ASSERT_EQ(*PathResult, Foo4CPLUSPLUSM); |
| 115 | } |
| 116 | |
| 117 | MATCHER_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 | |
| 124 | TEST(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 ns |
| 138 | |
| 139 | // Non-indexable symbols |
| 140 | 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 | |
| 153 | TEST(HeaderSourceSwitchTest, FromHeaderToSource) { |
| 154 | // build a proper index, which contains symbols: |
| 155 | // A_Sym1, declared in TestTU.h, defined in a.cpp |
| 156 | // B_Sym[1-2], declared in TestTU.h, defined in b.cpp |
| 157 | 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(S: 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(S: Sym); |
| 178 | auto Index = MemIndex::build(Symbols: std::move(AllSymbols).build(), Refs: {}, Relations: {}); |
| 179 | |
| 180 | // Test for switch from .h header to .cc source |
| 181 | struct { |
| 182 | llvm::StringRef ; |
| 183 | std::optional<std::string> ExpectedSource; |
| 184 | } TestCases[] = { |
| 185 | {.HeaderCode: "// empty, no header found" , .ExpectedSource: std::nullopt}, |
| 186 | {.HeaderCode: R"cpp( |
| 187 | // no definition found in the index. |
| 188 | void NonDefinition(); |
| 189 | )cpp" , |
| 190 | .ExpectedSource: std::nullopt}, |
| 191 | {.HeaderCode: R"cpp( |
| 192 | void A_Sym1(); |
| 193 | )cpp" , |
| 194 | .ExpectedSource: testPath(File: "a.cpp" )}, |
| 195 | {.HeaderCode: R"cpp( |
| 196 | // b.cpp wins. |
| 197 | void A_Sym1(); |
| 198 | void B_Sym1(); |
| 199 | void B_Sym2(); |
| 200 | )cpp" , |
| 201 | .ExpectedSource: testPath(File: "b.cpp" )}, |
| 202 | {.HeaderCode: 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 | .ExpectedSource: testPath(File: "a.cpp" )}, |
| 208 | |
| 209 | {.HeaderCode: R"cpp( |
| 210 | // We don't have definition in the index, so stay in the header. |
| 211 | void B_Sym3_NoDef(); |
| 212 | )cpp" , |
| 213 | .ExpectedSource: std::nullopt}, |
| 214 | }; |
| 215 | for (const auto &Case : TestCases) { |
| 216 | TestTU TU = TestTU::withCode(Code: Case.HeaderCode); |
| 217 | TU.Filename = "TestTU.h" ; |
| 218 | TU.ExtraArgs.push_back(x: "-xc++-header" ); // inform clang this is a header. |
| 219 | auto = TU.build(); |
| 220 | EXPECT_EQ(Case.ExpectedSource, |
| 221 | getCorrespondingHeaderOrSource(testPath(TU.Filename), HeaderAST, |
| 222 | Index.get())); |
| 223 | } |
| 224 | } |
| 225 | |
| 226 | TEST(HeaderSourceSwitchTest, FromSourceToHeader) { |
| 227 | // build a proper index, which contains symbols: |
| 228 | // A_Sym1, declared in a.h, defined in TestTU.cpp |
| 229 | // B_Sym[1-2], declared in b.h, defined in TestTU.cpp |
| 230 | TestTU TUForIndex = TestTU::withCode(Code: 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 | {.SourceCode: "// empty, no header found" , .ExpectedResult: std::nullopt}, |
| 255 | {.SourceCode: R"cpp( |
| 256 | // symbol not in index, no header found |
| 257 | void Local() {} |
| 258 | )cpp" , |
| 259 | .ExpectedResult: std::nullopt}, |
| 260 | |
| 261 | {.SourceCode: R"cpp( |
| 262 | // a.h wins. |
| 263 | void A_Sym1() {} |
| 264 | )cpp" , |
| 265 | .ExpectedResult: testPath(File: "a.h" )}, |
| 266 | |
| 267 | {.SourceCode: R"cpp( |
| 268 | // b.h wins. |
| 269 | void A_Sym1() {} |
| 270 | void B_Sym1() {} |
| 271 | void B_Sym2() {} |
| 272 | )cpp" , |
| 273 | .ExpectedResult: testPath(File: "b.h" )}, |
| 274 | |
| 275 | {.SourceCode: 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 | .ExpectedResult: testPath(File: "a.h" )}, |
| 281 | }; |
| 282 | for (const auto &Case : TestCases) { |
| 283 | TestTU TU = TestTU::withCode(Code: 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 | |
| 292 | TEST(HeaderSourceSwitchTest, ClangdServerIntegration) { |
| 293 | MockCompilationDatabase CDB; |
| 294 | CDB.ExtraClangFlags = {"-I" + |
| 295 | testPath(File: "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(File: "src/lib/test.cpp" ); |
| 299 | std::string = testPath(File: "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, File: CppPath, Contents: FileContent); |
| 310 | EXPECT_EQ(HeaderPath, |
| 311 | *llvm::cantFail(runSwitchHeaderSource(Server, CppPath))); |
| 312 | } |
| 313 | |
| 314 | TEST(HeaderSourceSwitchTest, CaseSensitivity) { |
| 315 | TestTU TU = TestTU::withCode(Code: "void foo() {}" ); |
| 316 | // Define more symbols in the header than the source file to trick heuristics |
| 317 | // into picking the header as source file, if the matching for header file |
| 318 | // 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 system |
| 324 | // 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 the |
| 334 | // index, check if we can still find the source file, which defines less |
| 335 | // symbols than the header. |
| 336 | auto = testPath(File: "HEADER.H" ); |
| 337 | // We expect the heuristics to pick: |
| 338 | // - header on case sensitive file systems, because the HeaderAbsPath doesn't |
| 339 | // match what we've seen through index. |
| 340 | // - source on case insensitive file systems, as the HeaderAbsPath would match |
| 341 | // the filename in index. |
| 342 | #ifdef CLANGD_PATH_CASE_INSENSITIVE |
| 343 | EXPECT_THAT(getCorrespondingHeaderOrSource(HeaderAbsPath, AST, Index.get()), |
| 344 | llvm::ValueIs(testing::StrCaseEq(testPath(TU.Filename)))); |
| 345 | #else |
| 346 | EXPECT_THAT(getCorrespondingHeaderOrSource(HeaderAbsPath, AST, Index.get()), |
| 347 | llvm::ValueIs(testing::StrCaseEq(testPath(TU.HeaderFilename)))); |
| 348 | #endif |
| 349 | } |
| 350 | |
| 351 | } // namespace |
| 352 | } // namespace clangd |
| 353 | } // namespace clang |
| 354 | |