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 | |