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(withSelectionRange, R, "" ) { return arg.selectionRange == R; } |
48 | |
49 | template <class ItemMatcher> |
50 | ::testing::Matcher<CallHierarchyIncomingCall> from(ItemMatcher M) { |
51 | return Field(&CallHierarchyIncomingCall::from, M); |
52 | } |
53 | template <class... RangeMatchers> |
54 | ::testing::Matcher<CallHierarchyIncomingCall> fromRanges(RangeMatchers... M) { |
55 | return Field(&CallHierarchyIncomingCall::fromRanges, |
56 | UnorderedElementsAre(M...)); |
57 | } |
58 | |
59 | TEST(CallHierarchy, IncomingOneFileCpp) { |
60 | Annotations Source(R"cpp( |
61 | void call^ee(int); |
62 | void caller1() { |
63 | $Callee[[callee]](42); |
64 | } |
65 | void caller2() { |
66 | $Caller1A[[caller1]](); |
67 | $Caller1B[[caller1]](); |
68 | } |
69 | void caller3() { |
70 | $Caller1C[[caller1]](); |
71 | $Caller2[[caller2]](); |
72 | } |
73 | )cpp" ); |
74 | TestTU TU = TestTU::withCode(Code: Source.code()); |
75 | auto AST = TU.build(); |
76 | auto Index = TU.index(); |
77 | |
78 | std::vector<CallHierarchyItem> Items = |
79 | prepareCallHierarchy(AST, Pos: Source.point(), TUPath: testPath(File: TU.Filename)); |
80 | ASSERT_THAT(Items, ElementsAre(withName("callee" ))); |
81 | auto IncomingLevel1 = incomingCalls(Item: Items[0], Index: Index.get()); |
82 | ASSERT_THAT(IncomingLevel1, |
83 | ElementsAre(AllOf(from(withName("caller1" )), |
84 | fromRanges(Source.range("Callee" ))))); |
85 | auto IncomingLevel2 = incomingCalls(Item: IncomingLevel1[0].from, Index: Index.get()); |
86 | ASSERT_THAT(IncomingLevel2, |
87 | ElementsAre(AllOf(from(withName("caller2" )), |
88 | fromRanges(Source.range("Caller1A" ), |
89 | Source.range("Caller1B" ))), |
90 | AllOf(from(withName("caller3" )), |
91 | fromRanges(Source.range("Caller1C" ))))); |
92 | |
93 | auto IncomingLevel3 = incomingCalls(Item: IncomingLevel2[0].from, Index: Index.get()); |
94 | ASSERT_THAT(IncomingLevel3, |
95 | ElementsAre(AllOf(from(withName("caller3" )), |
96 | fromRanges(Source.range("Caller2" ))))); |
97 | |
98 | auto IncomingLevel4 = incomingCalls(Item: IncomingLevel3[0].from, Index: Index.get()); |
99 | EXPECT_THAT(IncomingLevel4, IsEmpty()); |
100 | } |
101 | |
102 | TEST(CallHierarchy, IncomingOneFileObjC) { |
103 | Annotations Source(R"objc( |
104 | @implementation MyClass {} |
105 | +(void)call^ee {} |
106 | +(void) caller1 { |
107 | [MyClass $Callee[[callee]]]; |
108 | } |
109 | +(void) caller2 { |
110 | [MyClass $Caller1A[[caller1]]]; |
111 | [MyClass $Caller1B[[caller1]]]; |
112 | } |
113 | +(void) caller3 { |
114 | [MyClass $Caller1C[[caller1]]]; |
115 | [MyClass $Caller2[[caller2]]]; |
116 | } |
117 | @end |
118 | )objc" ); |
119 | TestTU TU = TestTU::withCode(Code: Source.code()); |
120 | TU.Filename = "TestTU.m" ; |
121 | auto AST = TU.build(); |
122 | auto Index = TU.index(); |
123 | std::vector<CallHierarchyItem> Items = |
124 | prepareCallHierarchy(AST, Pos: Source.point(), TUPath: testPath(File: TU.Filename)); |
125 | ASSERT_THAT(Items, ElementsAre(withName("callee" ))); |
126 | auto IncomingLevel1 = incomingCalls(Item: Items[0], Index: Index.get()); |
127 | ASSERT_THAT(IncomingLevel1, |
128 | ElementsAre(AllOf(from(withName("caller1" )), |
129 | fromRanges(Source.range("Callee" ))))); |
130 | auto IncomingLevel2 = incomingCalls(Item: IncomingLevel1[0].from, Index: Index.get()); |
131 | ASSERT_THAT(IncomingLevel2, |
132 | ElementsAre(AllOf(from(withName("caller2" )), |
133 | fromRanges(Source.range("Caller1A" ), |
134 | Source.range("Caller1B" ))), |
135 | AllOf(from(withName("caller3" )), |
136 | fromRanges(Source.range("Caller1C" ))))); |
137 | |
138 | auto IncomingLevel3 = incomingCalls(Item: IncomingLevel2[0].from, Index: Index.get()); |
139 | ASSERT_THAT(IncomingLevel3, |
140 | ElementsAre(AllOf(from(withName("caller3" )), |
141 | fromRanges(Source.range("Caller2" ))))); |
142 | |
143 | auto IncomingLevel4 = incomingCalls(Item: IncomingLevel3[0].from, Index: Index.get()); |
144 | EXPECT_THAT(IncomingLevel4, IsEmpty()); |
145 | } |
146 | |
147 | TEST(CallHierarchy, MainFileOnlyRef) { |
148 | // In addition to testing that we store refs to main-file only symbols, |
149 | // this tests that anonymous namespaces do not interfere with the |
150 | // symbol re-identification process in callHierarchyItemToSymbo(). |
151 | Annotations Source(R"cpp( |
152 | void call^ee(int); |
153 | namespace { |
154 | void caller1() { |
155 | $Callee[[callee]](42); |
156 | } |
157 | } |
158 | void caller2() { |
159 | $Caller1[[caller1]](); |
160 | } |
161 | )cpp" ); |
162 | TestTU TU = TestTU::withCode(Code: Source.code()); |
163 | auto AST = TU.build(); |
164 | auto Index = TU.index(); |
165 | |
166 | std::vector<CallHierarchyItem> Items = |
167 | prepareCallHierarchy(AST, Pos: Source.point(), TUPath: testPath(File: TU.Filename)); |
168 | ASSERT_THAT(Items, ElementsAre(withName("callee" ))); |
169 | auto IncomingLevel1 = incomingCalls(Item: Items[0], Index: Index.get()); |
170 | ASSERT_THAT(IncomingLevel1, |
171 | ElementsAre(AllOf(from(withName("caller1" )), |
172 | fromRanges(Source.range("Callee" ))))); |
173 | |
174 | auto IncomingLevel2 = incomingCalls(Item: IncomingLevel1[0].from, Index: Index.get()); |
175 | EXPECT_THAT(IncomingLevel2, |
176 | ElementsAre(AllOf(from(withName("caller2" )), |
177 | fromRanges(Source.range("Caller1" ))))); |
178 | } |
179 | |
180 | TEST(CallHierarchy, IncomingQualified) { |
181 | Annotations Source(R"cpp( |
182 | namespace ns { |
183 | struct Waldo { |
184 | void find(); |
185 | }; |
186 | void Waldo::find() {} |
187 | void caller1(Waldo &W) { |
188 | W.$Caller1[[f^ind]](); |
189 | } |
190 | void caller2(Waldo &W) { |
191 | W.$Caller2[[find]](); |
192 | } |
193 | } |
194 | )cpp" ); |
195 | TestTU TU = TestTU::withCode(Code: Source.code()); |
196 | auto AST = TU.build(); |
197 | auto Index = TU.index(); |
198 | |
199 | std::vector<CallHierarchyItem> Items = |
200 | prepareCallHierarchy(AST, Pos: Source.point(), TUPath: testPath(File: TU.Filename)); |
201 | ASSERT_THAT(Items, ElementsAre(withName("Waldo::find" ))); |
202 | auto Incoming = incomingCalls(Item: Items[0], Index: Index.get()); |
203 | EXPECT_THAT(Incoming, |
204 | ElementsAre(AllOf(from(withName("caller1" )), |
205 | fromRanges(Source.range("Caller1" ))), |
206 | AllOf(from(withName("caller2" )), |
207 | fromRanges(Source.range("Caller2" ))))); |
208 | } |
209 | |
210 | TEST(CallHierarchy, IncomingMultiFileCpp) { |
211 | // The test uses a .hh suffix for header files to get clang |
212 | // to parse them in C++ mode. .h files are parsed in C mode |
213 | // by default, which causes problems because e.g. symbol |
214 | // USRs are different in C mode (do not include function signatures). |
215 | |
216 | Annotations CalleeH(R"cpp( |
217 | void calle^e(int); |
218 | )cpp" ); |
219 | Annotations CalleeC(R"cpp( |
220 | #include "callee.hh" |
221 | void calle^e(int) {} |
222 | )cpp" ); |
223 | Annotations Caller1H(R"cpp( |
224 | void caller1(); |
225 | )cpp" ); |
226 | Annotations Caller1C(R"cpp( |
227 | #include "callee.hh" |
228 | #include "caller1.hh" |
229 | void caller1() { |
230 | [[calle^e]](42); |
231 | } |
232 | )cpp" ); |
233 | Annotations Caller2H(R"cpp( |
234 | void caller2(); |
235 | )cpp" ); |
236 | Annotations Caller2C(R"cpp( |
237 | #include "caller1.hh" |
238 | #include "caller2.hh" |
239 | void caller2() { |
240 | $A[[caller1]](); |
241 | $B[[caller1]](); |
242 | } |
243 | )cpp" ); |
244 | Annotations Caller3C(R"cpp( |
245 | #include "caller1.hh" |
246 | #include "caller2.hh" |
247 | void caller3() { |
248 | $Caller1[[caller1]](); |
249 | $Caller2[[caller2]](); |
250 | } |
251 | )cpp" ); |
252 | |
253 | TestWorkspace Workspace; |
254 | Workspace.addSource(Filename: "callee.hh" , Code: CalleeH.code()); |
255 | Workspace.addSource(Filename: "caller1.hh" , Code: Caller1H.code()); |
256 | Workspace.addSource(Filename: "caller2.hh" , Code: Caller2H.code()); |
257 | Workspace.addMainFile(Filename: "callee.cc" , Code: CalleeC.code()); |
258 | Workspace.addMainFile(Filename: "caller1.cc" , Code: Caller1C.code()); |
259 | Workspace.addMainFile(Filename: "caller2.cc" , Code: Caller2C.code()); |
260 | Workspace.addMainFile(Filename: "caller3.cc" , Code: Caller3C.code()); |
261 | |
262 | auto Index = Workspace.index(); |
263 | |
264 | auto CheckCallHierarchy = [&](ParsedAST &AST, Position Pos, PathRef TUPath) { |
265 | std::vector<CallHierarchyItem> Items = |
266 | prepareCallHierarchy(AST, Pos, TUPath); |
267 | ASSERT_THAT(Items, ElementsAre(withName("callee" ))); |
268 | auto IncomingLevel1 = incomingCalls(Item: Items[0], Index: Index.get()); |
269 | ASSERT_THAT(IncomingLevel1, |
270 | ElementsAre(AllOf(from(withName("caller1" )), |
271 | fromRanges(Caller1C.range())))); |
272 | |
273 | auto IncomingLevel2 = incomingCalls(Item: IncomingLevel1[0].from, Index: Index.get()); |
274 | ASSERT_THAT( |
275 | IncomingLevel2, |
276 | ElementsAre(AllOf(from(withName("caller2" )), |
277 | fromRanges(Caller2C.range("A" ), Caller2C.range("B" ))), |
278 | AllOf(from(withName("caller3" )), |
279 | fromRanges(Caller3C.range("Caller1" ))))); |
280 | |
281 | auto IncomingLevel3 = incomingCalls(Item: IncomingLevel2[0].from, Index: Index.get()); |
282 | ASSERT_THAT(IncomingLevel3, |
283 | ElementsAre(AllOf(from(withName("caller3" )), |
284 | fromRanges(Caller3C.range("Caller2" ))))); |
285 | |
286 | auto IncomingLevel4 = incomingCalls(Item: IncomingLevel3[0].from, Index: Index.get()); |
287 | EXPECT_THAT(IncomingLevel4, IsEmpty()); |
288 | }; |
289 | |
290 | // Check that invoking from a call site works. |
291 | auto AST = Workspace.openFile(Filename: "caller1.cc" ); |
292 | ASSERT_TRUE(bool(AST)); |
293 | CheckCallHierarchy(*AST, Caller1C.point(), testPath(File: "caller1.cc" )); |
294 | |
295 | // Check that invoking from the declaration site works. |
296 | AST = Workspace.openFile(Filename: "callee.hh" ); |
297 | ASSERT_TRUE(bool(AST)); |
298 | CheckCallHierarchy(*AST, CalleeH.point(), testPath(File: "callee.hh" )); |
299 | |
300 | // Check that invoking from the definition site works. |
301 | AST = Workspace.openFile(Filename: "callee.cc" ); |
302 | ASSERT_TRUE(bool(AST)); |
303 | CheckCallHierarchy(*AST, CalleeC.point(), testPath(File: "callee.cc" )); |
304 | } |
305 | |
306 | TEST(CallHierarchy, IncomingMultiFileObjC) { |
307 | // The test uses a .mi suffix for header files to get clang |
308 | // to parse them in ObjC mode. .h files are parsed in C mode |
309 | // by default, which causes problems because e.g. symbol |
310 | // USRs are different in C mode (do not include function signatures). |
311 | |
312 | Annotations CalleeH(R"objc( |
313 | @interface CalleeClass |
314 | +(void)call^ee; |
315 | @end |
316 | )objc" ); |
317 | Annotations CalleeC(R"objc( |
318 | #import "callee.mi" |
319 | @implementation CalleeClass {} |
320 | +(void)call^ee {} |
321 | @end |
322 | )objc" ); |
323 | Annotations Caller1H(R"objc( |
324 | @interface Caller1Class |
325 | +(void)caller1; |
326 | @end |
327 | )objc" ); |
328 | Annotations Caller1C(R"objc( |
329 | #import "callee.mi" |
330 | #import "caller1.mi" |
331 | @implementation Caller1Class {} |
332 | +(void)caller1 { |
333 | [CalleeClass [[calle^e]]]; |
334 | } |
335 | @end |
336 | )objc" ); |
337 | Annotations Caller2H(R"objc( |
338 | @interface Caller2Class |
339 | +(void)caller2; |
340 | @end |
341 | )objc" ); |
342 | Annotations Caller2C(R"objc( |
343 | #import "caller1.mi" |
344 | #import "caller2.mi" |
345 | @implementation Caller2Class {} |
346 | +(void)caller2 { |
347 | [Caller1Class $A[[caller1]]]; |
348 | [Caller1Class $B[[caller1]]]; |
349 | } |
350 | @end |
351 | )objc" ); |
352 | Annotations Caller3C(R"objc( |
353 | #import "caller1.mi" |
354 | #import "caller2.mi" |
355 | @implementation Caller3Class {} |
356 | +(void)caller3 { |
357 | [Caller1Class $Caller1[[caller1]]]; |
358 | [Caller2Class $Caller2[[caller2]]]; |
359 | } |
360 | @end |
361 | )objc" ); |
362 | |
363 | TestWorkspace Workspace; |
364 | Workspace.addSource(Filename: "callee.mi" , Code: CalleeH.code()); |
365 | Workspace.addSource(Filename: "caller1.mi" , Code: Caller1H.code()); |
366 | Workspace.addSource(Filename: "caller2.mi" , Code: Caller2H.code()); |
367 | Workspace.addMainFile(Filename: "callee.m" , Code: CalleeC.code()); |
368 | Workspace.addMainFile(Filename: "caller1.m" , Code: Caller1C.code()); |
369 | Workspace.addMainFile(Filename: "caller2.m" , Code: Caller2C.code()); |
370 | Workspace.addMainFile(Filename: "caller3.m" , Code: Caller3C.code()); |
371 | auto Index = Workspace.index(); |
372 | |
373 | auto CheckCallHierarchy = [&](ParsedAST &AST, Position Pos, PathRef TUPath) { |
374 | std::vector<CallHierarchyItem> Items = |
375 | prepareCallHierarchy(AST, Pos, TUPath); |
376 | ASSERT_THAT(Items, ElementsAre(withName("callee" ))); |
377 | auto IncomingLevel1 = incomingCalls(Item: Items[0], Index: Index.get()); |
378 | ASSERT_THAT(IncomingLevel1, |
379 | ElementsAre(AllOf(from(withName("caller1" )), |
380 | fromRanges(Caller1C.range())))); |
381 | |
382 | auto IncomingLevel2 = incomingCalls(Item: IncomingLevel1[0].from, Index: Index.get()); |
383 | ASSERT_THAT( |
384 | IncomingLevel2, |
385 | ElementsAre(AllOf(from(withName("caller2" )), |
386 | fromRanges(Caller2C.range("A" ), Caller2C.range("B" ))), |
387 | AllOf(from(withName("caller3" )), |
388 | fromRanges(Caller3C.range("Caller1" ))))); |
389 | |
390 | auto IncomingLevel3 = incomingCalls(Item: IncomingLevel2[0].from, Index: Index.get()); |
391 | ASSERT_THAT(IncomingLevel3, |
392 | ElementsAre(AllOf(from(withName("caller3" )), |
393 | fromRanges(Caller3C.range("Caller2" ))))); |
394 | |
395 | auto IncomingLevel4 = incomingCalls(Item: IncomingLevel3[0].from, Index: Index.get()); |
396 | EXPECT_THAT(IncomingLevel4, IsEmpty()); |
397 | }; |
398 | |
399 | // Check that invoking from a call site works. |
400 | auto AST = Workspace.openFile(Filename: "caller1.m" ); |
401 | ASSERT_TRUE(bool(AST)); |
402 | CheckCallHierarchy(*AST, Caller1C.point(), testPath(File: "caller1.m" )); |
403 | |
404 | // Check that invoking from the declaration site works. |
405 | AST = Workspace.openFile(Filename: "callee.mi" ); |
406 | ASSERT_TRUE(bool(AST)); |
407 | CheckCallHierarchy(*AST, CalleeH.point(), testPath(File: "callee.mi" )); |
408 | |
409 | // Check that invoking from the definition site works. |
410 | AST = Workspace.openFile(Filename: "callee.m" ); |
411 | ASSERT_TRUE(bool(AST)); |
412 | CheckCallHierarchy(*AST, CalleeC.point(), testPath(File: "callee.m" )); |
413 | } |
414 | |
415 | TEST(CallHierarchy, CallInLocalVarDecl) { |
416 | // Tests that local variable declarations are not treated as callers |
417 | // (they're not indexed, so they can't be represented as call hierarchy |
418 | // items); instead, the caller should be the containing function. |
419 | // However, namespace-scope variable declarations should be treated as |
420 | // callers because those are indexed and there is no enclosing entity |
421 | // that would be a useful caller. |
422 | Annotations Source(R"cpp( |
423 | int call^ee(); |
424 | void caller1() { |
425 | $call1[[callee]](); |
426 | } |
427 | void caller2() { |
428 | int localVar = $call2[[callee]](); |
429 | } |
430 | int caller3 = $call3[[callee]](); |
431 | )cpp" ); |
432 | TestTU TU = TestTU::withCode(Code: Source.code()); |
433 | auto AST = TU.build(); |
434 | auto Index = TU.index(); |
435 | |
436 | std::vector<CallHierarchyItem> Items = |
437 | prepareCallHierarchy(AST, Pos: Source.point(), TUPath: testPath(File: TU.Filename)); |
438 | ASSERT_THAT(Items, ElementsAre(withName("callee" ))); |
439 | |
440 | auto Incoming = incomingCalls(Item: Items[0], Index: Index.get()); |
441 | ASSERT_THAT( |
442 | Incoming, |
443 | ElementsAre( |
444 | AllOf(from(withName("caller1" )), fromRanges(Source.range("call1" ))), |
445 | AllOf(from(withName("caller2" )), fromRanges(Source.range("call2" ))), |
446 | AllOf(from(withName("caller3" )), fromRanges(Source.range("call3" ))))); |
447 | } |
448 | |
449 | } // namespace |
450 | } // namespace clangd |
451 | } // namespace clang |
452 | |