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 | 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 | |
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 | |
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(S: 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(S: Sym); |
140 | auto Index = MemIndex::build(Symbols: std::move(AllSymbols).build(), Refs: {}, Relations: {}); |
141 | |
142 | // Test for switch from .h header to .cc source |
143 | struct { |
144 | llvm::StringRef ; |
145 | std::optional<std::string> ExpectedSource; |
146 | } TestCases[] = { |
147 | {.HeaderCode: "// empty, no header found" , .ExpectedSource: std::nullopt}, |
148 | {.HeaderCode: R"cpp( |
149 | // no definition found in the index. |
150 | void NonDefinition(); |
151 | )cpp" , |
152 | .ExpectedSource: std::nullopt}, |
153 | {.HeaderCode: R"cpp( |
154 | void A_Sym1(); |
155 | )cpp" , |
156 | .ExpectedSource: testPath(File: "a.cpp" )}, |
157 | {.HeaderCode: R"cpp( |
158 | // b.cpp wins. |
159 | void A_Sym1(); |
160 | void B_Sym1(); |
161 | void B_Sym2(); |
162 | )cpp" , |
163 | .ExpectedSource: testPath(File: "b.cpp" )}, |
164 | {.HeaderCode: 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 | .ExpectedSource: testPath(File: "a.cpp" )}, |
170 | |
171 | {.HeaderCode: R"cpp( |
172 | // We don't have definition in the index, so stay in the header. |
173 | void B_Sym3_NoDef(); |
174 | )cpp" , |
175 | .ExpectedSource: std::nullopt}, |
176 | }; |
177 | for (const auto &Case : TestCases) { |
178 | TestTU TU = TestTU::withCode(Code: Case.HeaderCode); |
179 | TU.Filename = "TestTU.h" ; |
180 | TU.ExtraArgs.push_back(x: "-xc++-header" ); // inform clang this is a header. |
181 | auto = TU.build(); |
182 | EXPECT_EQ(Case.ExpectedSource, |
183 | getCorrespondingHeaderOrSource(testPath(TU.Filename), HeaderAST, |
184 | Index.get())); |
185 | } |
186 | } |
187 | |
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(Code: 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 | {.SourceCode: "// empty, no header found" , .ExpectedResult: std::nullopt}, |
217 | {.SourceCode: R"cpp( |
218 | // symbol not in index, no header found |
219 | void Local() {} |
220 | )cpp" , |
221 | .ExpectedResult: std::nullopt}, |
222 | |
223 | {.SourceCode: R"cpp( |
224 | // a.h wins. |
225 | void A_Sym1() {} |
226 | )cpp" , |
227 | .ExpectedResult: testPath(File: "a.h" )}, |
228 | |
229 | {.SourceCode: R"cpp( |
230 | // b.h wins. |
231 | void A_Sym1() {} |
232 | void B_Sym1() {} |
233 | void B_Sym2() {} |
234 | )cpp" , |
235 | .ExpectedResult: testPath(File: "b.h" )}, |
236 | |
237 | {.SourceCode: 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 | .ExpectedResult: testPath(File: "a.h" )}, |
243 | }; |
244 | for (const auto &Case : TestCases) { |
245 | TestTU TU = TestTU::withCode(Code: 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 | |
254 | TEST(HeaderSourceSwitchTest, ClangdServerIntegration) { |
255 | MockCompilationDatabase CDB; |
256 | CDB.ExtraClangFlags = {"-I" + |
257 | testPath(File: "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(File: "src/lib/test.cpp" ); |
261 | std::string = testPath(File: "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, File: CppPath, Contents: FileContent); |
272 | EXPECT_EQ(HeaderPath, |
273 | *llvm::cantFail(runSwitchHeaderSource(Server, CppPath))); |
274 | } |
275 | |
276 | TEST(HeaderSourceSwitchTest, CaseSensitivity) { |
277 | TestTU TU = TestTU::withCode(Code: "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 = testPath(File: "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 | |