1 | //===------ IndexActionTests.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 "Headers.h" |
10 | #include "TestFS.h" |
11 | #include "URI.h" |
12 | #include "index/IndexAction.h" |
13 | #include "index/Serialization.h" |
14 | #include "clang/Basic/SourceLocation.h" |
15 | #include "clang/Basic/SourceManager.h" |
16 | #include "clang/Tooling/Tooling.h" |
17 | #include "gmock/gmock.h" |
18 | #include "gtest/gtest.h" |
19 | #include <string> |
20 | |
21 | namespace clang { |
22 | namespace clangd { |
23 | namespace { |
24 | |
25 | using ::testing::AllOf; |
26 | using ::testing::ElementsAre; |
27 | using ::testing::EndsWith; |
28 | using ::testing::Not; |
29 | using ::testing::Pair; |
30 | using ::testing::UnorderedElementsAre; |
31 | using ::testing::UnorderedPointwise; |
32 | |
33 | std::string toUri(llvm::StringRef Path) { return URI::create(AbsolutePath: Path).toString(); } |
34 | |
35 | MATCHER(isTU, "" ) { return arg.Flags & IncludeGraphNode::SourceFlag::IsTU; } |
36 | |
37 | MATCHER_P(hasDigest, Digest, "" ) { return arg.Digest == Digest; } |
38 | |
39 | MATCHER_P(hasName, Name, "" ) { return arg.Name == Name; } |
40 | |
41 | MATCHER(hasSameURI, "" ) { |
42 | llvm::StringRef URI = ::testing::get<0>(arg); |
43 | const std::string &Path = ::testing::get<1>(arg); |
44 | return toUri(Path) == URI; |
45 | } |
46 | |
47 | MATCHER_P(, , "" ) { |
48 | return (arg.IncludeHeaders.size() == 1) && |
49 | (arg.IncludeHeaders.begin()->IncludeHeader == P); |
50 | } |
51 | |
52 | ::testing::Matcher<const IncludeGraphNode &> |
53 | includesAre(const std::vector<std::string> &Includes) { |
54 | return ::testing::Field(field: &IncludeGraphNode::DirectIncludes, |
55 | matcher: UnorderedPointwise(tuple2_matcher: hasSameURI(), rhs_container: Includes)); |
56 | } |
57 | |
58 | void checkNodesAreInitialized(const IndexFileIn &IndexFile, |
59 | const std::vector<std::string> &Paths) { |
60 | ASSERT_TRUE(IndexFile.Sources); |
61 | EXPECT_THAT(Paths.size(), IndexFile.Sources->size()); |
62 | for (llvm::StringRef Path : Paths) { |
63 | auto URI = toUri(Path); |
64 | const auto &Node = IndexFile.Sources->lookup(Key: URI); |
65 | // Uninitialized nodes will have an empty URI. |
66 | EXPECT_EQ(Node.URI.data(), IndexFile.Sources->find(URI)->getKeyData()); |
67 | } |
68 | } |
69 | |
70 | std::map<std::string, const IncludeGraphNode &> toMap(const IncludeGraph &IG) { |
71 | std::map<std::string, const IncludeGraphNode &> Nodes; |
72 | for (auto &I : IG) |
73 | Nodes.emplace(args: std::string(I.getKey()), args: I.getValue()); |
74 | return Nodes; |
75 | } |
76 | |
77 | class IndexActionTest : public ::testing::Test { |
78 | public: |
79 | IndexActionTest() : InMemoryFileSystem(new llvm::vfs::InMemoryFileSystem) {} |
80 | |
81 | IndexFileIn |
82 | runIndexingAction(llvm::StringRef MainFilePath, |
83 | const std::vector<std::string> & = {}) { |
84 | IndexFileIn IndexFile; |
85 | llvm::IntrusiveRefCntPtr<FileManager> Files( |
86 | new FileManager(FileSystemOptions(), InMemoryFileSystem)); |
87 | |
88 | auto Action = createStaticIndexingAction( |
89 | Opts, SymbolsCallback: [&](SymbolSlab S) { IndexFile.Symbols = std::move(S); }, |
90 | RefsCallback: [&](RefSlab R) { IndexFile.Refs = std::move(R); }, |
91 | RelationsCallback: [&](RelationSlab R) { IndexFile.Relations = std::move(R); }, |
92 | IncludeGraphCallback: [&](IncludeGraph IG) { IndexFile.Sources = std::move(IG); }); |
93 | |
94 | std::vector<std::string> Args = {"index_action" , "-fsyntax-only" , |
95 | "-xc++" , "-std=c++11" , |
96 | "-iquote" , testRoot()}; |
97 | Args.insert(position: Args.end(), first: ExtraArgs.begin(), last: ExtraArgs.end()); |
98 | Args.push_back(x: std::string(MainFilePath)); |
99 | |
100 | tooling::ToolInvocation Invocation( |
101 | Args, std::move(Action), Files.get(), |
102 | std::make_shared<PCHContainerOperations>()); |
103 | |
104 | Invocation.run(); |
105 | |
106 | checkNodesAreInitialized(IndexFile, Paths: FilePaths); |
107 | return IndexFile; |
108 | } |
109 | |
110 | void addFile(llvm::StringRef Path, llvm::StringRef Content) { |
111 | InMemoryFileSystem->addFile(Path, ModificationTime: 0, |
112 | Buffer: llvm::MemoryBuffer::getMemBufferCopy(InputData: Content)); |
113 | FilePaths.push_back(x: std::string(Path)); |
114 | } |
115 | |
116 | protected: |
117 | SymbolCollector::Options Opts; |
118 | std::vector<std::string> FilePaths; |
119 | llvm::IntrusiveRefCntPtr<llvm::vfs::InMemoryFileSystem> InMemoryFileSystem; |
120 | }; |
121 | |
122 | TEST_F(IndexActionTest, CollectIncludeGraph) { |
123 | std::string MainFilePath = testPath(File: "main.cpp" ); |
124 | std::string MainCode = "#include \"level1.h\"" ; |
125 | std::string = testPath(File: "level1.h" ); |
126 | std::string = "#include \"level2.h\"" ; |
127 | std::string = testPath(File: "level2.h" ); |
128 | std::string = "" ; |
129 | |
130 | addFile(Path: MainFilePath, Content: MainCode); |
131 | addFile(Path: Level1HeaderPath, Content: Level1HeaderCode); |
132 | addFile(Path: Level2HeaderPath, Content: Level2HeaderCode); |
133 | |
134 | IndexFileIn IndexFile = runIndexingAction(MainFilePath); |
135 | auto Nodes = toMap(IG: *IndexFile.Sources); |
136 | |
137 | EXPECT_THAT(Nodes, |
138 | UnorderedElementsAre( |
139 | Pair(toUri(MainFilePath), |
140 | AllOf(isTU(), includesAre({Level1HeaderPath}), |
141 | hasDigest(digest(MainCode)))), |
142 | Pair(toUri(Level1HeaderPath), |
143 | AllOf(Not(isTU()), includesAre({Level2HeaderPath}), |
144 | hasDigest(digest(Level1HeaderCode)))), |
145 | Pair(toUri(Level2HeaderPath), |
146 | AllOf(Not(isTU()), includesAre({}), |
147 | hasDigest(digest(Level2HeaderCode)))))); |
148 | } |
149 | |
150 | TEST_F(IndexActionTest, IncludeGraphSelfInclude) { |
151 | std::string MainFilePath = testPath(File: "main.cpp" ); |
152 | std::string MainCode = "#include \"header.h\"" ; |
153 | std::string = testPath(File: "header.h" ); |
154 | std::string = R"cpp( |
155 | #ifndef _GUARD_ |
156 | #define _GUARD_ |
157 | #include "header.h" |
158 | #endif)cpp" ; |
159 | |
160 | addFile(Path: MainFilePath, Content: MainCode); |
161 | addFile(Path: HeaderPath, Content: HeaderCode); |
162 | |
163 | IndexFileIn IndexFile = runIndexingAction(MainFilePath); |
164 | auto Nodes = toMap(IG: *IndexFile.Sources); |
165 | |
166 | EXPECT_THAT( |
167 | Nodes, |
168 | UnorderedElementsAre( |
169 | Pair(toUri(MainFilePath), AllOf(isTU(), includesAre({HeaderPath}), |
170 | hasDigest(digest(MainCode)))), |
171 | Pair(toUri(HeaderPath), AllOf(Not(isTU()), includesAre({HeaderPath}), |
172 | hasDigest(digest(HeaderCode)))))); |
173 | } |
174 | |
175 | TEST_F(IndexActionTest, IncludeGraphSkippedFile) { |
176 | std::string MainFilePath = testPath(File: "main.cpp" ); |
177 | std::string MainCode = R"cpp( |
178 | #include "common.h" |
179 | #include "header.h" |
180 | )cpp" ; |
181 | |
182 | std::string = testPath(File: "common.h" ); |
183 | std::string = R"cpp( |
184 | #ifndef _GUARD_ |
185 | #define _GUARD_ |
186 | void f(); |
187 | #endif)cpp" ; |
188 | |
189 | std::string = testPath(File: "header.h" ); |
190 | std::string = R"cpp( |
191 | #include "common.h" |
192 | void g();)cpp" ; |
193 | |
194 | addFile(Path: MainFilePath, Content: MainCode); |
195 | addFile(Path: HeaderPath, Content: HeaderCode); |
196 | addFile(Path: CommonHeaderPath, Content: CommonHeaderCode); |
197 | |
198 | IndexFileIn IndexFile = runIndexingAction(MainFilePath); |
199 | auto Nodes = toMap(IG: *IndexFile.Sources); |
200 | |
201 | EXPECT_THAT( |
202 | Nodes, UnorderedElementsAre( |
203 | Pair(toUri(MainFilePath), |
204 | AllOf(isTU(), includesAre({HeaderPath, CommonHeaderPath}), |
205 | hasDigest(digest(MainCode)))), |
206 | Pair(toUri(HeaderPath), |
207 | AllOf(Not(isTU()), includesAre({CommonHeaderPath}), |
208 | hasDigest(digest(HeaderCode)))), |
209 | Pair(toUri(CommonHeaderPath), |
210 | AllOf(Not(isTU()), includesAre({}), |
211 | hasDigest(digest(CommonHeaderCode)))))); |
212 | } |
213 | |
214 | TEST_F(IndexActionTest, IncludeGraphDynamicInclude) { |
215 | std::string MainFilePath = testPath(File: "main.cpp" ); |
216 | std::string MainCode = R"cpp( |
217 | #ifndef FOO |
218 | #define FOO "main.cpp" |
219 | #else |
220 | #define FOO "header.h" |
221 | #endif |
222 | |
223 | #include FOO)cpp" ; |
224 | std::string = testPath(File: "header.h" ); |
225 | std::string = "" ; |
226 | |
227 | addFile(Path: MainFilePath, Content: MainCode); |
228 | addFile(Path: HeaderPath, Content: HeaderCode); |
229 | |
230 | IndexFileIn IndexFile = runIndexingAction(MainFilePath); |
231 | auto Nodes = toMap(IG: *IndexFile.Sources); |
232 | |
233 | EXPECT_THAT( |
234 | Nodes, |
235 | UnorderedElementsAre( |
236 | Pair(toUri(MainFilePath), |
237 | AllOf(isTU(), includesAre({MainFilePath, HeaderPath}), |
238 | hasDigest(digest(MainCode)))), |
239 | Pair(toUri(HeaderPath), AllOf(Not(isTU()), includesAre({}), |
240 | hasDigest(digest(HeaderCode)))))); |
241 | } |
242 | |
243 | TEST_F(IndexActionTest, NoWarnings) { |
244 | std::string MainFilePath = testPath(File: "main.cpp" ); |
245 | std::string MainCode = R"cpp( |
246 | void foo(int x) { |
247 | if (x = 1) // -Wparentheses |
248 | return; |
249 | if (x = 1) // -Wparentheses |
250 | return; |
251 | } |
252 | void bar() {} |
253 | )cpp" ; |
254 | addFile(Path: MainFilePath, Content: MainCode); |
255 | // We set -ferror-limit so the warning-promoted-to-error would be fatal. |
256 | // This would cause indexing to stop (if warnings weren't disabled). |
257 | IndexFileIn IndexFile = runIndexingAction( |
258 | MainFilePath, ExtraArgs: {"-ferror-limit=1" , "-Wparentheses" , "-Werror" }); |
259 | ASSERT_TRUE(IndexFile.Sources); |
260 | ASSERT_NE(0u, IndexFile.Sources->size()); |
261 | EXPECT_THAT(*IndexFile.Symbols, ElementsAre(hasName("foo" ), hasName("bar" ))); |
262 | } |
263 | |
264 | TEST_F(IndexActionTest, SkipFiles) { |
265 | std::string MainFilePath = testPath(File: "main.cpp" ); |
266 | addFile(Path: MainFilePath, Content: R"cpp( |
267 | // clang-format off |
268 | #include "good.h" |
269 | #include "bad.h" |
270 | // clang-format on |
271 | )cpp" ); |
272 | addFile(Path: testPath(File: "good.h" ), Content: R"cpp( |
273 | struct S { int s; }; |
274 | void f1() { S f; } |
275 | auto unskippable1() { return S(); } |
276 | )cpp" ); |
277 | addFile(Path: testPath(File: "bad.h" ), Content: R"cpp( |
278 | struct T { S t; }; |
279 | void f2() { S f; } |
280 | auto unskippable2() { return S(); } |
281 | )cpp" ); |
282 | Opts.FileFilter = [](const SourceManager &SM, FileID F) { |
283 | return !SM.getFileEntryRefForID(FID: F)->getName().ends_with(Suffix: "bad.h" ); |
284 | }; |
285 | IndexFileIn IndexFile = runIndexingAction(MainFilePath, ExtraArgs: {"-std=c++14" }); |
286 | EXPECT_THAT(*IndexFile.Symbols, |
287 | UnorderedElementsAre(hasName("S" ), hasName("s" ), hasName("f1" ), |
288 | hasName("unskippable1" ))); |
289 | for (const auto &Pair : *IndexFile.Refs) |
290 | for (const auto &Ref : Pair.second) |
291 | EXPECT_THAT(Ref.Location.FileURI, EndsWith("good.h" )); |
292 | } |
293 | |
294 | TEST_F(IndexActionTest, SkipNestedSymbols) { |
295 | std::string MainFilePath = testPath(File: "main.cpp" ); |
296 | addFile(Path: MainFilePath, Content: R"cpp( |
297 | namespace ns1 { |
298 | namespace ns2 { |
299 | namespace ns3 { |
300 | namespace ns4 { |
301 | namespace ns5 { |
302 | namespace ns6 { |
303 | namespace ns7 { |
304 | namespace ns8 { |
305 | namespace ns9 { |
306 | class Bar {}; |
307 | void foo() { |
308 | class Baz {}; |
309 | } |
310 | } |
311 | } |
312 | } |
313 | } |
314 | } |
315 | } |
316 | } |
317 | } |
318 | })cpp" ); |
319 | IndexFileIn IndexFile = runIndexingAction(MainFilePath, ExtraArgs: {"-std=c++14" }); |
320 | EXPECT_THAT(*IndexFile.Symbols, testing::Contains(hasName("foo" ))); |
321 | EXPECT_THAT(*IndexFile.Symbols, testing::Contains(hasName("Bar" ))); |
322 | EXPECT_THAT(*IndexFile.Symbols, Not(testing::Contains(hasName("Baz" )))); |
323 | } |
324 | |
325 | TEST_F(IndexActionTest, SymbolFromCC) { |
326 | std::string MainFilePath = testPath(File: "main.cpp" ); |
327 | addFile(Path: MainFilePath, Content: R"cpp( |
328 | #include "main.h" |
329 | void foo() {} |
330 | )cpp" ); |
331 | addFile(Path: testPath(File: "main.h" ), Content: R"cpp( |
332 | #pragma once |
333 | void foo(); |
334 | )cpp" ); |
335 | Opts.FileFilter = [](const SourceManager &SM, FileID F) { |
336 | return !SM.getFileEntryRefForID(FID: F)->getName().ends_with(Suffix: "main.h" ); |
337 | }; |
338 | IndexFileIn IndexFile = runIndexingAction(MainFilePath, ExtraArgs: {"-std=c++14" }); |
339 | EXPECT_THAT(*IndexFile.Symbols, |
340 | UnorderedElementsAre(AllOf( |
341 | hasName("foo" ), |
342 | includeHeader(URI::create(testPath("main.h" )).toString())))); |
343 | } |
344 | |
345 | TEST_F(IndexActionTest, IncludeHeaderForwardDecls) { |
346 | std::string MainFilePath = testPath(File: "main.cpp" ); |
347 | addFile(Path: MainFilePath, Content: R"cpp( |
348 | #include "fwd.h" |
349 | #include "full.h" |
350 | )cpp" ); |
351 | addFile(Path: testPath(File: "fwd.h" ), Content: R"cpp( |
352 | #ifndef _FWD_H_ |
353 | #define _FWD_H_ |
354 | struct Foo; |
355 | #endif |
356 | )cpp" ); |
357 | addFile(Path: testPath(File: "full.h" ), Content: R"cpp( |
358 | #ifndef _FULL_H_ |
359 | #define _FULL_H_ |
360 | struct Foo {}; |
361 | |
362 | // This decl is important, as otherwise we detect control macro for the file, |
363 | // before handling definition of Foo. |
364 | void other(); |
365 | #endif |
366 | )cpp" ); |
367 | IndexFileIn IndexFile = runIndexingAction(MainFilePath); |
368 | EXPECT_THAT(*IndexFile.Symbols, |
369 | testing::Contains(AllOf( |
370 | hasName("Foo" ), |
371 | includeHeader(URI::create(testPath("full.h" )).toString())))) |
372 | << *IndexFile.Symbols->begin(); |
373 | } |
374 | } // namespace |
375 | } // namespace clangd |
376 | } // namespace clang |
377 | |