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
79MATCHER_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
86TEST(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
115TEST(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 HeaderCode;
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 HeaderAST = TU.build();
182 EXPECT_EQ(Case.ExpectedSource,
183 getCorrespondingHeaderOrSource(testPath(TU.Filename), HeaderAST,
184 Index.get()));
185 }
186}
187
188TEST(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
254TEST(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 HeaderPath = 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
276TEST(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 HeaderAbsPath = 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

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