1 | //===-- CallHierarchyTests.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 | #include "Annotations.h" |
9 | #include "ParsedAST.h" |
10 | #include "TestFS.h" |
11 | #include "TestTU.h" |
12 | #include "TestWorkspace.h" |
13 | #include "XRefs.h" |
14 | #include "llvm/Support/Path.h" |
15 | #include "gmock/gmock.h" |
16 | #include "gtest/gtest.h" |
17 | |
18 | namespace clang { |
19 | namespace clangd { |
20 | |
21 | llvm::raw_ostream &operator<<(llvm::raw_ostream &Stream, |
22 | const CallHierarchyItem &Item) { |
23 | return Stream << Item.name << "@" << Item.selectionRange; |
24 | } |
25 | |
26 | llvm::raw_ostream &operator<<(llvm::raw_ostream &Stream, |
27 | const CallHierarchyIncomingCall &Call) { |
28 | Stream << "{ from: " << Call.from << ", ranges: [" ; |
29 | for (const auto &R : Call.fromRanges) { |
30 | Stream << R; |
31 | Stream << ", " ; |
32 | } |
33 | return Stream << "] }" ; |
34 | } |
35 | |
36 | namespace { |
37 | |
38 | using ::testing::AllOf; |
39 | using ::testing::ElementsAre; |
40 | using ::testing::Field; |
41 | using ::testing::IsEmpty; |
42 | using ::testing::Matcher; |
43 | using ::testing::UnorderedElementsAre; |
44 | |
45 | // Helpers for matching call hierarchy data structures. |
46 | MATCHER_P(withName, N, "" ) { return arg.name == N; } |
47 | MATCHER_P(withDetail, N, "" ) { return arg.detail == N; } |
48 | MATCHER_P(withFile, N, "" ) { return arg.uri.file() == N; } |
49 | MATCHER_P(withSelectionRange, R, "" ) { return arg.selectionRange == R; } |
50 | |
51 | template <class ItemMatcher> |
52 | ::testing::Matcher<CallHierarchyIncomingCall> from(ItemMatcher M) { |
53 | return Field(&CallHierarchyIncomingCall::from, M); |
54 | } |
55 | template <class ItemMatcher> |
56 | ::testing::Matcher<CallHierarchyOutgoingCall> to(ItemMatcher M) { |
57 | return Field(&CallHierarchyOutgoingCall::to, M); |
58 | } |
59 | template <class... RangeMatchers> |
60 | ::testing::Matcher<CallHierarchyIncomingCall> iFromRanges(RangeMatchers... M) { |
61 | return Field(&CallHierarchyIncomingCall::fromRanges, |
62 | UnorderedElementsAre(M...)); |
63 | } |
64 | template <class... RangeMatchers> |
65 | ::testing::Matcher<CallHierarchyOutgoingCall> oFromRanges(RangeMatchers... M) { |
66 | return Field(&CallHierarchyOutgoingCall::fromRanges, |
67 | UnorderedElementsAre(M...)); |
68 | } |
69 | |
70 | TEST(CallHierarchy, IncomingOneFileCpp) { |
71 | Annotations Source(R"cpp( |
72 | void call^ee(int); |
73 | void caller1() { |
74 | $Callee[[callee]](42); |
75 | } |
76 | void caller2() { |
77 | $Caller1A[[caller1]](); |
78 | $Caller1B[[caller1]](); |
79 | } |
80 | void caller3() { |
81 | $Caller1C[[caller1]](); |
82 | $Caller2[[caller2]](); |
83 | } |
84 | )cpp" ); |
85 | TestTU TU = TestTU::withCode(Code: Source.code()); |
86 | auto AST = TU.build(); |
87 | auto Index = TU.index(); |
88 | |
89 | std::vector<CallHierarchyItem> Items = |
90 | prepareCallHierarchy(AST, Pos: Source.point(), TUPath: testPath(File: TU.Filename)); |
91 | ASSERT_THAT(Items, ElementsAre(withName("callee" ))); |
92 | auto IncomingLevel1 = incomingCalls(Item: Items[0], Index: Index.get()); |
93 | ASSERT_THAT( |
94 | IncomingLevel1, |
95 | ElementsAre(AllOf(from(AllOf(withName("caller1" ), withDetail("caller1" ))), |
96 | iFromRanges(Source.range("Callee" ))))); |
97 | auto IncomingLevel2 = incomingCalls(Item: IncomingLevel1[0].from, Index: Index.get()); |
98 | ASSERT_THAT( |
99 | IncomingLevel2, |
100 | ElementsAre(AllOf(from(AllOf(withName("caller2" ), withDetail("caller2" ))), |
101 | iFromRanges(Source.range("Caller1A" ), |
102 | Source.range("Caller1B" ))), |
103 | AllOf(from(AllOf(withName("caller3" ), withDetail("caller3" ))), |
104 | iFromRanges(Source.range("Caller1C" ))))); |
105 | |
106 | auto IncomingLevel3 = incomingCalls(Item: IncomingLevel2[0].from, Index: Index.get()); |
107 | ASSERT_THAT( |
108 | IncomingLevel3, |
109 | ElementsAre(AllOf(from(AllOf(withName("caller3" ), withDetail("caller3" ))), |
110 | iFromRanges(Source.range("Caller2" ))))); |
111 | |
112 | auto IncomingLevel4 = incomingCalls(Item: IncomingLevel3[0].from, Index: Index.get()); |
113 | EXPECT_THAT(IncomingLevel4, IsEmpty()); |
114 | } |
115 | |
116 | TEST(CallHierarchy, IncomingOneFileObjC) { |
117 | Annotations Source(R"objc( |
118 | @implementation MyClass {} |
119 | +(void)call^ee {} |
120 | +(void) caller1 { |
121 | [MyClass $Callee[[callee]]]; |
122 | } |
123 | +(void) caller2 { |
124 | [MyClass $Caller1A[[caller1]]]; |
125 | [MyClass $Caller1B[[caller1]]]; |
126 | } |
127 | +(void) caller3 { |
128 | [MyClass $Caller1C[[caller1]]]; |
129 | [MyClass $Caller2[[caller2]]]; |
130 | } |
131 | @end |
132 | )objc" ); |
133 | TestTU TU = TestTU::withCode(Code: Source.code()); |
134 | TU.Filename = "TestTU.m" ; |
135 | auto AST = TU.build(); |
136 | auto Index = TU.index(); |
137 | std::vector<CallHierarchyItem> Items = |
138 | prepareCallHierarchy(AST, Pos: Source.point(), TUPath: testPath(File: TU.Filename)); |
139 | ASSERT_THAT(Items, ElementsAre(withName("callee" ))); |
140 | auto IncomingLevel1 = incomingCalls(Item: Items[0], Index: Index.get()); |
141 | ASSERT_THAT(IncomingLevel1, |
142 | ElementsAre(AllOf(from(AllOf(withName("caller1" ), |
143 | withDetail("MyClass::caller1" ))), |
144 | iFromRanges(Source.range("Callee" ))))); |
145 | auto IncomingLevel2 = incomingCalls(Item: IncomingLevel1[0].from, Index: Index.get()); |
146 | ASSERT_THAT(IncomingLevel2, |
147 | ElementsAre(AllOf(from(AllOf(withName("caller2" ), |
148 | withDetail("MyClass::caller2" ))), |
149 | iFromRanges(Source.range("Caller1A" ), |
150 | Source.range("Caller1B" ))), |
151 | AllOf(from(AllOf(withName("caller3" ), |
152 | withDetail("MyClass::caller3" ))), |
153 | iFromRanges(Source.range("Caller1C" ))))); |
154 | |
155 | auto IncomingLevel3 = incomingCalls(Item: IncomingLevel2[0].from, Index: Index.get()); |
156 | ASSERT_THAT(IncomingLevel3, |
157 | ElementsAre(AllOf(from(AllOf(withName("caller3" ), |
158 | withDetail("MyClass::caller3" ))), |
159 | iFromRanges(Source.range("Caller2" ))))); |
160 | |
161 | auto IncomingLevel4 = incomingCalls(Item: IncomingLevel3[0].from, Index: Index.get()); |
162 | EXPECT_THAT(IncomingLevel4, IsEmpty()); |
163 | } |
164 | |
165 | TEST(CallHierarchy, MainFileOnlyRef) { |
166 | // In addition to testing that we store refs to main-file only symbols, |
167 | // this tests that anonymous namespaces do not interfere with the |
168 | // symbol re-identification process in callHierarchyItemToSymbo(). |
169 | Annotations Source(R"cpp( |
170 | void call^ee(int); |
171 | namespace { |
172 | void caller1() { |
173 | $Callee[[callee]](42); |
174 | } |
175 | } |
176 | void caller2() { |
177 | $Caller1[[caller1]](); |
178 | } |
179 | )cpp" ); |
180 | TestTU TU = TestTU::withCode(Code: Source.code()); |
181 | auto AST = TU.build(); |
182 | auto Index = TU.index(); |
183 | |
184 | std::vector<CallHierarchyItem> Items = |
185 | prepareCallHierarchy(AST, Pos: Source.point(), TUPath: testPath(File: TU.Filename)); |
186 | ASSERT_THAT(Items, ElementsAre(withName("callee" ))); |
187 | auto IncomingLevel1 = incomingCalls(Item: Items[0], Index: Index.get()); |
188 | ASSERT_THAT( |
189 | IncomingLevel1, |
190 | ElementsAre(AllOf(from(AllOf(withName("caller1" ), withDetail("caller1" ))), |
191 | iFromRanges(Source.range("Callee" ))))); |
192 | |
193 | auto IncomingLevel2 = incomingCalls(Item: IncomingLevel1[0].from, Index: Index.get()); |
194 | EXPECT_THAT( |
195 | IncomingLevel2, |
196 | ElementsAre(AllOf(from(AllOf(withName("caller2" ), withDetail("caller2" ))), |
197 | iFromRanges(Source.range("Caller1" ))))); |
198 | } |
199 | |
200 | TEST(CallHierarchy, IncomingQualified) { |
201 | Annotations Source(R"cpp( |
202 | namespace ns { |
203 | struct Waldo { |
204 | void find(); |
205 | }; |
206 | void Waldo::find() {} |
207 | void caller1(Waldo &W) { |
208 | W.$Caller1[[f^ind]](); |
209 | } |
210 | void caller2(Waldo &W) { |
211 | W.$Caller2[[find]](); |
212 | } |
213 | } |
214 | )cpp" ); |
215 | TestTU TU = TestTU::withCode(Code: Source.code()); |
216 | auto AST = TU.build(); |
217 | auto Index = TU.index(); |
218 | |
219 | std::vector<CallHierarchyItem> Items = |
220 | prepareCallHierarchy(AST, Pos: Source.point(), TUPath: testPath(File: TU.Filename)); |
221 | ASSERT_THAT(Items, ElementsAre(withName("Waldo::find" ))); |
222 | auto Incoming = incomingCalls(Item: Items[0], Index: Index.get()); |
223 | EXPECT_THAT( |
224 | Incoming, |
225 | ElementsAre( |
226 | AllOf(from(AllOf(withName("caller1" ), withDetail("ns::caller1" ))), |
227 | iFromRanges(Source.range("Caller1" ))), |
228 | AllOf(from(AllOf(withName("caller2" ), withDetail("ns::caller2" ))), |
229 | iFromRanges(Source.range("Caller2" ))))); |
230 | } |
231 | |
232 | TEST(CallHierarchy, OutgoingOneFile) { |
233 | // Test outgoing call on the main file, with namespaces and methods |
234 | Annotations Source(R"cpp( |
235 | void callee(int); |
236 | namespace ns { |
237 | struct Foo { |
238 | void caller1(); |
239 | }; |
240 | void Foo::caller1() { |
241 | $Callee[[callee]](42); |
242 | } |
243 | } |
244 | namespace { |
245 | void caller2(ns::Foo& F) { |
246 | F.$Caller1A[[caller1]](); |
247 | F.$Caller1B[[caller1]](); |
248 | } |
249 | } |
250 | void call^er3(ns::Foo& F) { |
251 | F.$Caller1C[[caller1]](); |
252 | $Caller2[[caller2]](F); |
253 | } |
254 | )cpp" ); |
255 | TestTU TU = TestTU::withCode(Code: Source.code()); |
256 | auto AST = TU.build(); |
257 | auto Index = TU.index(); |
258 | |
259 | std::vector<CallHierarchyItem> Items = |
260 | prepareCallHierarchy(AST, Pos: Source.point(), TUPath: testPath(File: TU.Filename)); |
261 | ASSERT_THAT(Items, ElementsAre(withName("caller3" ))); |
262 | auto OugoingLevel1 = outgoingCalls(Item: Items[0], Index: Index.get()); |
263 | ASSERT_THAT( |
264 | OugoingLevel1, |
265 | ElementsAre( |
266 | AllOf(to(AllOf(withName("caller1" ), withDetail("ns::Foo::caller1" ))), |
267 | oFromRanges(Source.range("Caller1C" ))), |
268 | AllOf(to(AllOf(withName("caller2" ), withDetail("caller2" ))), |
269 | oFromRanges(Source.range("Caller2" ))))); |
270 | |
271 | auto OutgoingLevel2 = outgoingCalls(Item: OugoingLevel1[1].to, Index: Index.get()); |
272 | ASSERT_THAT( |
273 | OutgoingLevel2, |
274 | ElementsAre(AllOf( |
275 | to(AllOf(withName("caller1" ), withDetail("ns::Foo::caller1" ))), |
276 | oFromRanges(Source.range("Caller1A" ), Source.range("Caller1B" ))))); |
277 | |
278 | auto OutgoingLevel3 = outgoingCalls(Item: OutgoingLevel2[0].to, Index: Index.get()); |
279 | ASSERT_THAT( |
280 | OutgoingLevel3, |
281 | ElementsAre(AllOf(to(AllOf(withName("callee" ), withDetail("callee" ))), |
282 | oFromRanges(Source.range("Callee" ))))); |
283 | |
284 | auto OutgoingLevel4 = outgoingCalls(Item: OutgoingLevel3[0].to, Index: Index.get()); |
285 | EXPECT_THAT(OutgoingLevel4, IsEmpty()); |
286 | } |
287 | |
288 | TEST(CallHierarchy, MultiFileCpp) { |
289 | // The test uses a .hh suffix for header files to get clang |
290 | // to parse them in C++ mode. .h files are parsed in C mode |
291 | // by default, which causes problems because e.g. symbol |
292 | // USRs are different in C mode (do not include function signatures). |
293 | |
294 | Annotations CalleeH(R"cpp( |
295 | void calle^e(int); |
296 | )cpp" ); |
297 | Annotations CalleeC(R"cpp( |
298 | #include "callee.hh" |
299 | void calle^e(int) {} |
300 | )cpp" ); |
301 | Annotations Caller1H(R"cpp( |
302 | namespace nsa { |
303 | void caller1(); |
304 | } |
305 | )cpp" ); |
306 | Annotations Caller1C(R"cpp( |
307 | #include "callee.hh" |
308 | #include "caller1.hh" |
309 | namespace nsa { |
310 | void caller1() { |
311 | [[calle^e]](42); |
312 | } |
313 | } |
314 | )cpp" ); |
315 | Annotations Caller2H(R"cpp( |
316 | namespace nsb { |
317 | void caller2(); |
318 | } |
319 | )cpp" ); |
320 | Annotations Caller2C(R"cpp( |
321 | #include "caller1.hh" |
322 | #include "caller2.hh" |
323 | namespace nsb { |
324 | void caller2() { |
325 | nsa::$A[[caller1]](); |
326 | nsa::$B[[caller1]](); |
327 | } |
328 | } |
329 | )cpp" ); |
330 | Annotations Caller3H(R"cpp( |
331 | namespace nsa { |
332 | void call^er3(); |
333 | } |
334 | )cpp" ); |
335 | Annotations Caller3C(R"cpp( |
336 | #include "caller1.hh" |
337 | #include "caller2.hh" |
338 | namespace nsa { |
339 | void call^er3() { |
340 | $Caller1[[caller1]](); |
341 | nsb::$Caller2[[caller2]](); |
342 | } |
343 | } |
344 | )cpp" ); |
345 | |
346 | TestWorkspace Workspace; |
347 | Workspace.addSource(Filename: "callee.hh" , Code: CalleeH.code()); |
348 | Workspace.addSource(Filename: "caller1.hh" , Code: Caller1H.code()); |
349 | Workspace.addSource(Filename: "caller2.hh" , Code: Caller2H.code()); |
350 | Workspace.addSource(Filename: "caller3.hh" , Code: Caller3H.code()); |
351 | Workspace.addMainFile(Filename: "callee.cc" , Code: CalleeC.code()); |
352 | Workspace.addMainFile(Filename: "caller1.cc" , Code: Caller1C.code()); |
353 | Workspace.addMainFile(Filename: "caller2.cc" , Code: Caller2C.code()); |
354 | Workspace.addMainFile(Filename: "caller3.cc" , Code: Caller3C.code()); |
355 | |
356 | auto Index = Workspace.index(); |
357 | |
358 | auto CheckIncomingCalls = [&](ParsedAST &AST, Position Pos, PathRef TUPath) { |
359 | std::vector<CallHierarchyItem> Items = |
360 | prepareCallHierarchy(AST, Pos, TUPath); |
361 | ASSERT_THAT(Items, ElementsAre(withName("callee" ))); |
362 | auto IncomingLevel1 = incomingCalls(Item: Items[0], Index: Index.get()); |
363 | ASSERT_THAT(IncomingLevel1, |
364 | ElementsAre(AllOf(from(AllOf(withName("caller1" ), |
365 | withDetail("nsa::caller1" ))), |
366 | iFromRanges(Caller1C.range())))); |
367 | |
368 | auto IncomingLevel2 = incomingCalls(Item: IncomingLevel1[0].from, Index: Index.get()); |
369 | ASSERT_THAT( |
370 | IncomingLevel2, |
371 | ElementsAre( |
372 | AllOf(from(AllOf(withName("caller2" ), withDetail("nsb::caller2" ))), |
373 | iFromRanges(Caller2C.range("A" ), Caller2C.range("B" ))), |
374 | AllOf(from(AllOf(withName("caller3" ), withDetail("nsa::caller3" ))), |
375 | iFromRanges(Caller3C.range("Caller1" ))))); |
376 | |
377 | auto IncomingLevel3 = incomingCalls(Item: IncomingLevel2[0].from, Index: Index.get()); |
378 | ASSERT_THAT(IncomingLevel3, |
379 | ElementsAre(AllOf(from(AllOf(withName("caller3" ), |
380 | withDetail("nsa::caller3" ))), |
381 | iFromRanges(Caller3C.range("Caller2" ))))); |
382 | |
383 | auto IncomingLevel4 = incomingCalls(Item: IncomingLevel3[0].from, Index: Index.get()); |
384 | EXPECT_THAT(IncomingLevel4, IsEmpty()); |
385 | }; |
386 | |
387 | auto CheckOutgoingCalls = [&](ParsedAST &AST, Position Pos, PathRef TUPath, |
388 | bool IsDeclaration) { |
389 | std::vector<CallHierarchyItem> Items = |
390 | prepareCallHierarchy(AST, Pos, TUPath); |
391 | ASSERT_THAT( |
392 | Items, |
393 | ElementsAre(AllOf( |
394 | withName("caller3" ), |
395 | withFile(testPath(IsDeclaration ? "caller3.hh" : "caller3.cc" ))))); |
396 | auto OutgoingLevel1 = outgoingCalls(Item: Items[0], Index: Index.get()); |
397 | ASSERT_THAT( |
398 | OutgoingLevel1, |
399 | // fromRanges are interpreted in the context of Items[0]'s file. |
400 | // If that's the header, we can't get ranges from the implementation |
401 | // file! |
402 | ElementsAre( |
403 | AllOf(to(AllOf(withName("caller1" ), withDetail("nsa::caller1" ))), |
404 | IsDeclaration ? oFromRanges() |
405 | : oFromRanges(Caller3C.range("Caller1" ))), |
406 | AllOf(to(AllOf(withName("caller2" ), withDetail("nsb::caller2" ))), |
407 | IsDeclaration ? oFromRanges() |
408 | : oFromRanges(Caller3C.range("Caller2" ))))); |
409 | |
410 | auto OutgoingLevel2 = outgoingCalls(Item: OutgoingLevel1[1].to, Index: Index.get()); |
411 | ASSERT_THAT(OutgoingLevel2, |
412 | ElementsAre(AllOf( |
413 | to(AllOf(withName("caller1" ), withDetail("nsa::caller1" ))), |
414 | oFromRanges(Caller2C.range("A" ), Caller2C.range("B" ))))); |
415 | |
416 | auto OutgoingLevel3 = outgoingCalls(Item: OutgoingLevel2[0].to, Index: Index.get()); |
417 | ASSERT_THAT( |
418 | OutgoingLevel3, |
419 | ElementsAre(AllOf(to(AllOf(withName("callee" ), withDetail("callee" ))), |
420 | oFromRanges(Caller1C.range())))); |
421 | |
422 | auto OutgoingLevel4 = outgoingCalls(Item: OutgoingLevel3[0].to, Index: Index.get()); |
423 | EXPECT_THAT(OutgoingLevel4, IsEmpty()); |
424 | }; |
425 | |
426 | // Check that invoking from a call site works. |
427 | auto AST = Workspace.openFile(Filename: "caller1.cc" ); |
428 | ASSERT_TRUE(bool(AST)); |
429 | CheckIncomingCalls(*AST, Caller1C.point(), testPath(File: "caller1.cc" )); |
430 | |
431 | // Check that invoking from the declaration site works. |
432 | AST = Workspace.openFile(Filename: "callee.hh" ); |
433 | ASSERT_TRUE(bool(AST)); |
434 | CheckIncomingCalls(*AST, CalleeH.point(), testPath(File: "callee.hh" )); |
435 | AST = Workspace.openFile(Filename: "caller3.hh" ); |
436 | ASSERT_TRUE(bool(AST)); |
437 | CheckOutgoingCalls(*AST, Caller3H.point(), testPath(File: "caller3.hh" ), true); |
438 | |
439 | // Check that invoking from the definition site works. |
440 | AST = Workspace.openFile(Filename: "callee.cc" ); |
441 | ASSERT_TRUE(bool(AST)); |
442 | CheckIncomingCalls(*AST, CalleeC.point(), testPath(File: "callee.cc" )); |
443 | AST = Workspace.openFile(Filename: "caller3.cc" ); |
444 | ASSERT_TRUE(bool(AST)); |
445 | CheckOutgoingCalls(*AST, Caller3C.point(), testPath(File: "caller3.cc" ), false); |
446 | } |
447 | |
448 | TEST(CallHierarchy, IncomingMultiFileObjC) { |
449 | // The test uses a .mi suffix for header files to get clang |
450 | // to parse them in ObjC mode. .h files are parsed in C mode |
451 | // by default, which causes problems because e.g. symbol |
452 | // USRs are different in C mode (do not include function signatures). |
453 | |
454 | Annotations CalleeH(R"objc( |
455 | @interface CalleeClass |
456 | +(void)call^ee; |
457 | @end |
458 | )objc" ); |
459 | Annotations CalleeC(R"objc( |
460 | #import "callee.mi" |
461 | @implementation CalleeClass {} |
462 | +(void)call^ee {} |
463 | @end |
464 | )objc" ); |
465 | Annotations Caller1H(R"objc( |
466 | @interface Caller1Class |
467 | +(void)caller1; |
468 | @end |
469 | )objc" ); |
470 | Annotations Caller1C(R"objc( |
471 | #import "callee.mi" |
472 | #import "caller1.mi" |
473 | @implementation Caller1Class {} |
474 | +(void)caller1 { |
475 | [CalleeClass [[calle^e]]]; |
476 | } |
477 | @end |
478 | )objc" ); |
479 | Annotations Caller2H(R"objc( |
480 | @interface Caller2Class |
481 | +(void)caller2; |
482 | @end |
483 | )objc" ); |
484 | Annotations Caller2C(R"objc( |
485 | #import "caller1.mi" |
486 | #import "caller2.mi" |
487 | @implementation Caller2Class {} |
488 | +(void)caller2 { |
489 | [Caller1Class $A[[caller1]]]; |
490 | [Caller1Class $B[[caller1]]]; |
491 | } |
492 | @end |
493 | )objc" ); |
494 | Annotations Caller3C(R"objc( |
495 | #import "caller1.mi" |
496 | #import "caller2.mi" |
497 | @implementation Caller3Class {} |
498 | +(void)caller3 { |
499 | [Caller1Class $Caller1[[caller1]]]; |
500 | [Caller2Class $Caller2[[caller2]]]; |
501 | } |
502 | @end |
503 | )objc" ); |
504 | |
505 | TestWorkspace Workspace; |
506 | Workspace.addSource(Filename: "callee.mi" , Code: CalleeH.code()); |
507 | Workspace.addSource(Filename: "caller1.mi" , Code: Caller1H.code()); |
508 | Workspace.addSource(Filename: "caller2.mi" , Code: Caller2H.code()); |
509 | Workspace.addMainFile(Filename: "callee.m" , Code: CalleeC.code()); |
510 | Workspace.addMainFile(Filename: "caller1.m" , Code: Caller1C.code()); |
511 | Workspace.addMainFile(Filename: "caller2.m" , Code: Caller2C.code()); |
512 | Workspace.addMainFile(Filename: "caller3.m" , Code: Caller3C.code()); |
513 | auto Index = Workspace.index(); |
514 | |
515 | auto CheckCallHierarchy = [&](ParsedAST &AST, Position Pos, PathRef TUPath) { |
516 | std::vector<CallHierarchyItem> Items = |
517 | prepareCallHierarchy(AST, Pos, TUPath); |
518 | ASSERT_THAT(Items, ElementsAre(withName("callee" ))); |
519 | auto IncomingLevel1 = incomingCalls(Item: Items[0], Index: Index.get()); |
520 | ASSERT_THAT(IncomingLevel1, |
521 | ElementsAre(AllOf(from(withName("caller1" )), |
522 | iFromRanges(Caller1C.range())))); |
523 | |
524 | auto IncomingLevel2 = incomingCalls(Item: IncomingLevel1[0].from, Index: Index.get()); |
525 | ASSERT_THAT(IncomingLevel2, |
526 | ElementsAre(AllOf(from(withName("caller2" )), |
527 | iFromRanges(Caller2C.range("A" ), |
528 | Caller2C.range("B" ))), |
529 | AllOf(from(withName("caller3" )), |
530 | iFromRanges(Caller3C.range("Caller1" ))))); |
531 | |
532 | auto IncomingLevel3 = incomingCalls(Item: IncomingLevel2[0].from, Index: Index.get()); |
533 | ASSERT_THAT(IncomingLevel3, |
534 | ElementsAre(AllOf(from(withName("caller3" )), |
535 | iFromRanges(Caller3C.range("Caller2" ))))); |
536 | |
537 | auto IncomingLevel4 = incomingCalls(Item: IncomingLevel3[0].from, Index: Index.get()); |
538 | EXPECT_THAT(IncomingLevel4, IsEmpty()); |
539 | }; |
540 | |
541 | // Check that invoking from a call site works. |
542 | auto AST = Workspace.openFile(Filename: "caller1.m" ); |
543 | ASSERT_TRUE(bool(AST)); |
544 | CheckCallHierarchy(*AST, Caller1C.point(), testPath(File: "caller1.m" )); |
545 | |
546 | // Check that invoking from the declaration site works. |
547 | AST = Workspace.openFile(Filename: "callee.mi" ); |
548 | ASSERT_TRUE(bool(AST)); |
549 | CheckCallHierarchy(*AST, CalleeH.point(), testPath(File: "callee.mi" )); |
550 | |
551 | // Check that invoking from the definition site works. |
552 | AST = Workspace.openFile(Filename: "callee.m" ); |
553 | ASSERT_TRUE(bool(AST)); |
554 | CheckCallHierarchy(*AST, CalleeC.point(), testPath(File: "callee.m" )); |
555 | } |
556 | |
557 | TEST(CallHierarchy, CallInLocalVarDecl) { |
558 | // Tests that local variable declarations are not treated as callers |
559 | // (they're not indexed, so they can't be represented as call hierarchy |
560 | // items); instead, the caller should be the containing function. |
561 | // However, namespace-scope variable declarations should be treated as |
562 | // callers because those are indexed and there is no enclosing entity |
563 | // that would be a useful caller. |
564 | Annotations Source(R"cpp( |
565 | int call^ee(); |
566 | void caller1() { |
567 | $call1[[callee]](); |
568 | } |
569 | void caller2() { |
570 | int localVar = $call2[[callee]](); |
571 | } |
572 | int caller3 = $call3[[callee]](); |
573 | )cpp" ); |
574 | TestTU TU = TestTU::withCode(Code: Source.code()); |
575 | auto AST = TU.build(); |
576 | auto Index = TU.index(); |
577 | |
578 | std::vector<CallHierarchyItem> Items = |
579 | prepareCallHierarchy(AST, Pos: Source.point(), TUPath: testPath(File: TU.Filename)); |
580 | ASSERT_THAT(Items, ElementsAre(withName("callee" ))); |
581 | |
582 | auto Incoming = incomingCalls(Item: Items[0], Index: Index.get()); |
583 | ASSERT_THAT(Incoming, ElementsAre(AllOf(from(withName("caller1" )), |
584 | iFromRanges(Source.range("call1" ))), |
585 | AllOf(from(withName("caller2" )), |
586 | iFromRanges(Source.range("call2" ))), |
587 | AllOf(from(withName("caller3" )), |
588 | iFromRanges(Source.range("call3" ))))); |
589 | } |
590 | |
591 | TEST(CallHierarchy, HierarchyOnField) { |
592 | // Tests that the call hierarchy works on fields. |
593 | Annotations Source(R"cpp( |
594 | struct Vars { |
595 | int v^ar1 = 1; |
596 | }; |
597 | void caller() { |
598 | Vars values; |
599 | values.$Callee[[var1]]; |
600 | } |
601 | )cpp" ); |
602 | TestTU TU = TestTU::withCode(Code: Source.code()); |
603 | auto AST = TU.build(); |
604 | auto Index = TU.index(); |
605 | |
606 | std::vector<CallHierarchyItem> Items = |
607 | prepareCallHierarchy(AST, Pos: Source.point(), TUPath: testPath(File: TU.Filename)); |
608 | ASSERT_THAT(Items, ElementsAre(withName("var1" ))); |
609 | auto IncomingLevel1 = incomingCalls(Item: Items[0], Index: Index.get()); |
610 | ASSERT_THAT(IncomingLevel1, |
611 | ElementsAre(AllOf(from(withName("caller" )), |
612 | iFromRanges(Source.range("Callee" ))))); |
613 | } |
614 | |
615 | TEST(CallHierarchy, HierarchyOnVar) { |
616 | // Tests that the call hierarchy works on non-local variables. |
617 | Annotations Source(R"cpp( |
618 | int v^ar = 1; |
619 | void caller() { |
620 | $Callee[[var]]; |
621 | } |
622 | )cpp" ); |
623 | TestTU TU = TestTU::withCode(Code: Source.code()); |
624 | auto AST = TU.build(); |
625 | auto Index = TU.index(); |
626 | |
627 | std::vector<CallHierarchyItem> Items = |
628 | prepareCallHierarchy(AST, Pos: Source.point(), TUPath: testPath(File: TU.Filename)); |
629 | ASSERT_THAT(Items, ElementsAre(withName("var" ))); |
630 | auto IncomingLevel1 = incomingCalls(Item: Items[0], Index: Index.get()); |
631 | ASSERT_THAT(IncomingLevel1, |
632 | ElementsAre(AllOf(from(withName("caller" )), |
633 | iFromRanges(Source.range("Callee" ))))); |
634 | } |
635 | |
636 | TEST(CallHierarchy, CallInDifferentFileThanCaller) { |
637 | Annotations (R"cpp( |
638 | #define WALDO void caller() { |
639 | )cpp" ); |
640 | Annotations Source(R"cpp( |
641 | void call^ee(); |
642 | WALDO |
643 | callee(); |
644 | } |
645 | )cpp" ); |
646 | auto TU = TestTU::withCode(Code: Source.code()); |
647 | TU.HeaderCode = Header.code(); |
648 | auto AST = TU.build(); |
649 | auto Index = TU.index(); |
650 | |
651 | std::vector<CallHierarchyItem> Items = |
652 | prepareCallHierarchy(AST, Pos: Source.point(), TUPath: testPath(File: TU.Filename)); |
653 | ASSERT_THAT(Items, ElementsAre(withName("callee" ))); |
654 | |
655 | auto Incoming = incomingCalls(Item: Items[0], Index: Index.get()); |
656 | |
657 | // The only call site is in the source file, which is a different file from |
658 | // the declaration of the function containing the call, which is in the |
659 | // header. The protocol does not allow us to represent such calls, so we drop |
660 | // them. (The call hierarchy item itself is kept.) |
661 | EXPECT_THAT(Incoming, |
662 | ElementsAre(AllOf(from(withName("caller" )), iFromRanges()))); |
663 | } |
664 | |
665 | } // namespace |
666 | } // namespace clangd |
667 | } // namespace clang |
668 | |