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
21namespace clang {
22namespace clangd {
23namespace {
24
25TEST(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
79TEST(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
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 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
153TEST(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 HeaderCode;
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 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.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
292TEST(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 HeaderPath = 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
314TEST(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 HeaderAbsPath = 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

Provided by KDAB

Privacy Policy
Improve your Profiling and Debugging skills
Find out more

source code of clang-tools-extra/clangd/unittests/HeaderSourceSwitchTests.cpp