xref: /llvm-project/clang-tools-extra/clangd/unittests/HeaderSourceSwitchTests.cpp (revision f71ffd3b735b4d6ae3c12be1806cdd6205b3b378)
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 
TEST(HeaderSourceSwitchTest,FileHeuristic)25 TEST(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, source
44   // file with different extension
45   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 letters
55   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 file
64   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 empty
74   // string.
75   PathResult = getCorrespondingHeaderOrSource(Invalid, FS.view(std::nullopt));
76   EXPECT_FALSE(PathResult.has_value());
77 }
78 
79 MATCHER_P(declNamed, Name, "") {
80   if (const NamedDecl *ND = dyn_cast<NamedDecl>(arg))
81     if (ND->getQualifiedNameAsString() == Name)
82       return true;
83   return false;
84 }
85 
TEST(HeaderSourceSwitchTest,GetLocalDecls)86 TEST(HeaderSourceSwitchTest, GetLocalDecls) {
87   TestTU TU;
88   TU.HeaderCode = R"cpp(
89   void HeaderOnly();
90   )cpp";
91   TU.Code = R"cpp(
92   void MainF1();
93   class Foo {};
94   namespace ns {
95   class Foo {
96     void method();
97     int field;
98   };
99   } // namespace ns
100 
101   // Non-indexable symbols
102   namespace {
103   void Ignore1() {}
104   }
105 
106   )cpp";
107 
108   auto AST = TU.build();
109   EXPECT_THAT(getIndexableLocalDecls(AST),
110               testing::UnorderedElementsAre(
111                   declNamed("MainF1"), declNamed("Foo"), declNamed("ns::Foo"),
112                   declNamed("ns::Foo::method"), declNamed("ns::Foo::field")));
113 }
114 
TEST(HeaderSourceSwitchTest,FromHeaderToSource)115 TEST(HeaderSourceSwitchTest, FromHeaderToSource) {
116   // build a proper index, which contains symbols:
117   //   A_Sym1, declared in TestTU.h, defined in a.cpp
118   //   B_Sym[1-2], declared in TestTU.h, defined in b.cpp
119   SymbolSlab::Builder AllSymbols;
120   TestTU Testing;
121   Testing.HeaderFilename = "TestTU.h";
122   Testing.HeaderCode = "void A_Sym1();";
123   Testing.Filename = "a.cpp";
124   Testing.Code = "void A_Sym1() {};";
125   for (auto &Sym : Testing.headerSymbols())
126     AllSymbols.insert(Sym);
127 
128   Testing.HeaderCode = R"cpp(
129   void B_Sym1();
130   void B_Sym2();
131   void B_Sym3_NoDef();
132   )cpp";
133   Testing.Filename = "b.cpp";
134   Testing.Code = R"cpp(
135   void B_Sym1() {}
136   void B_Sym2() {}
137   )cpp";
138   for (auto &Sym : Testing.headerSymbols())
139     AllSymbols.insert(Sym);
140   auto Index = MemIndex::build(std::move(AllSymbols).build(), {}, {});
141 
142   // Test for switch from .h header to .cc source
143   struct {
144     llvm::StringRef HeaderCode;
145     std::optional<std::string> ExpectedSource;
146   } TestCases[] = {
147       {"// empty, no header found", std::nullopt},
148       {R"cpp(
149          // no definition found in the index.
150          void NonDefinition();
151        )cpp",
152        std::nullopt},
153       {R"cpp(
154          void A_Sym1();
155        )cpp",
156        testPath("a.cpp")},
157       {R"cpp(
158          // b.cpp wins.
159          void A_Sym1();
160          void B_Sym1();
161          void B_Sym2();
162        )cpp",
163        testPath("b.cpp")},
164       {R"cpp(
165          // a.cpp and b.cpp have same scope, but a.cpp because "a.cpp" < "b.cpp".
166          void A_Sym1();
167          void B_Sym1();
168        )cpp",
169        testPath("a.cpp")},
170 
171       {R"cpp(
172           // We don't have definition in the index, so stay in the header.
173           void B_Sym3_NoDef();
174        )cpp",
175        std::nullopt},
176   };
177   for (const auto &Case : TestCases) {
178     TestTU TU = TestTU::withCode(Case.HeaderCode);
179     TU.Filename = "TestTU.h";
180     TU.ExtraArgs.push_back("-xc++-header"); // inform clang this is a header.
181     auto HeaderAST = TU.build();
182     EXPECT_EQ(Case.ExpectedSource,
183               getCorrespondingHeaderOrSource(testPath(TU.Filename), HeaderAST,
184                                              Index.get()));
185   }
186 }
187 
TEST(HeaderSourceSwitchTest,FromSourceToHeader)188 TEST(HeaderSourceSwitchTest, FromSourceToHeader) {
189   // build a proper index, which contains symbols:
190   //   A_Sym1, declared in a.h, defined in TestTU.cpp
191   //   B_Sym[1-2], declared in b.h, defined in TestTU.cpp
192   TestTU TUForIndex = TestTU::withCode(R"cpp(
193   #include "a.h"
194   #include "b.h"
195 
196   void A_Sym1() {}
197 
198   void B_Sym1() {}
199   void B_Sym2() {}
200   )cpp");
201   TUForIndex.AdditionalFiles["a.h"] = R"cpp(
202   void A_Sym1();
203   )cpp";
204   TUForIndex.AdditionalFiles["b.h"] = R"cpp(
205   void B_Sym1();
206   void B_Sym2();
207   )cpp";
208   TUForIndex.Filename = "TestTU.cpp";
209   auto Index = TUForIndex.index();
210 
211   // Test for switching from .cc source file to .h header.
212   struct {
213     llvm::StringRef SourceCode;
214     std::optional<std::string> ExpectedResult;
215   } TestCases[] = {
216       {"// empty, no header found", std::nullopt},
217       {R"cpp(
218          // symbol not in index, no header found
219          void Local() {}
220        )cpp",
221        std::nullopt},
222 
223       {R"cpp(
224          // a.h wins.
225          void A_Sym1() {}
226        )cpp",
227        testPath("a.h")},
228 
229       {R"cpp(
230          // b.h wins.
231          void A_Sym1() {}
232          void B_Sym1() {}
233          void B_Sym2() {}
234        )cpp",
235        testPath("b.h")},
236 
237       {R"cpp(
238          // a.h and b.h have same scope, but a.h wins because "a.h" < "b.h".
239          void A_Sym1() {}
240          void B_Sym1() {}
241        )cpp",
242        testPath("a.h")},
243   };
244   for (const auto &Case : TestCases) {
245     TestTU TU = TestTU::withCode(Case.SourceCode);
246     TU.Filename = "Test.cpp";
247     auto AST = TU.build();
248     EXPECT_EQ(Case.ExpectedResult,
249               getCorrespondingHeaderOrSource(testPath(TU.Filename), AST,
250                                              Index.get()));
251   }
252 }
253 
TEST(HeaderSourceSwitchTest,ClangdServerIntegration)254 TEST(HeaderSourceSwitchTest, ClangdServerIntegration) {
255   MockCompilationDatabase CDB;
256   CDB.ExtraClangFlags = {"-I" +
257                          testPath("src/include")}; // add search directory.
258   MockFS FS;
259   // File heuristic fails here, we rely on the index to find the .h file.
260   std::string CppPath = testPath("src/lib/test.cpp");
261   std::string HeaderPath = testPath("src/include/test.h");
262   FS.Files[HeaderPath] = "void foo();";
263   const std::string FileContent = R"cpp(
264     #include "test.h"
265     void foo() {};
266   )cpp";
267   FS.Files[CppPath] = FileContent;
268   auto Options = ClangdServer::optsForTest();
269   Options.BuildDynamicSymbolIndex = true;
270   ClangdServer Server(CDB, FS, Options);
271   runAddDocument(Server, CppPath, FileContent);
272   EXPECT_EQ(HeaderPath,
273             *llvm::cantFail(runSwitchHeaderSource(Server, CppPath)));
274 }
275 
TEST(HeaderSourceSwitchTest,CaseSensitivity)276 TEST(HeaderSourceSwitchTest, CaseSensitivity) {
277   TestTU TU = TestTU::withCode("void foo() {}");
278   // Define more symbols in the header than the source file to trick heuristics
279   // into picking the header as source file, if the matching for header file
280   // path fails.
281   TU.HeaderCode = R"cpp(
282   inline void bar1() {}
283   inline void bar2() {}
284   void foo();)cpp";
285   // Give main file and header different base names to make sure file system
286   // heuristics don't work.
287   TU.Filename = "Source.cpp";
288   TU.HeaderFilename = "Header.h";
289 
290   auto Index = TU.index();
291   TU.Code = std::move(TU.HeaderCode);
292   TU.HeaderCode.clear();
293   auto AST = TU.build();
294 
295   // Provide a different-cased filename in the query than what we have in the
296   // index, check if we can still find the source file, which defines less
297   // symbols than the header.
298   auto HeaderAbsPath = testPath("HEADER.H");
299   // We expect the heuristics to pick:
300   // - header on case sensitive file systems, because the HeaderAbsPath doesn't
301   //   match what we've seen through index.
302   // - source on case insensitive file systems, as the HeaderAbsPath would match
303   //   the filename in index.
304 #ifdef CLANGD_PATH_CASE_INSENSITIVE
305   EXPECT_THAT(getCorrespondingHeaderOrSource(HeaderAbsPath, AST, Index.get()),
306               llvm::ValueIs(testing::StrCaseEq(testPath(TU.Filename))));
307 #else
308   EXPECT_THAT(getCorrespondingHeaderOrSource(HeaderAbsPath, AST, Index.get()),
309               llvm::ValueIs(testing::StrCaseEq(testPath(TU.HeaderFilename))));
310 #endif
311 }
312 
313 } // namespace
314 } // namespace clangd
315 } // namespace clang
316