Skip to content

Commit 4e76a5e

Browse files
committed
Add call hierarchy helpers and tests for outgoing calls in local functions, operators, and properties
1 parent bfb3510 commit 4e76a5e

File tree

5 files changed

+240
-36
lines changed

5 files changed

+240
-36
lines changed

src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs

Lines changed: 53 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,60 @@ open System.Threading.Tasks
4444
open FsAutoComplete.FCSPatches
4545
open Helpers
4646
open System.Runtime.ExceptionServices
47+
open FSharp.Compiler.CodeAnalysis
4748

4849
module ArrayHelpers =
4950
let (|EmptyArray|NonEmptyArray|) (a: 'a array) = if a.Length = 0 then EmptyArray else NonEmptyArray a
5051

5152
open ArrayHelpers
5253

54+
module CallHierarchyHelpers =
55+
/// Determines if a symbol represents a callable (function, method, constructor, or applicable entity)
56+
let isCallableSymbol (symbol: FSharpSymbol) =
57+
match symbol with
58+
| :? FSharpMemberOrFunctionOrValue as mfv ->
59+
mfv.IsFunction
60+
|| mfv.IsMethod
61+
|| mfv.IsConstructor
62+
|| (mfv.IsProperty
63+
&& not (mfv.LogicalName.Contains("get_") || mfv.LogicalName.Contains("set_")))
64+
| :? FSharpEntity as ent -> ent.IsClass || ent.IsFSharpRecord || ent.IsFSharpUnion
65+
| _ -> false
66+
67+
/// Filters symbol uses to those within a binding range that represent outgoing calls
68+
let getOutgoingCallsInBinding
69+
(bindingRange: Range)
70+
(pos: FSharp.Compiler.Text.Position)
71+
(allSymbolUses: FSharpSymbolUse seq)
72+
=
73+
allSymbolUses
74+
|> Seq.filter (fun su ->
75+
Range.rangeContainsRange bindingRange su.Range
76+
&& not su.IsFromDefinition
77+
&& su.Range.Start <> pos
78+
&& isCallableSymbol su.Symbol)
79+
|> Seq.toArray
80+
81+
/// Gets the appropriate SymbolKind for a given FSharpSymbol
82+
let getSymbolKind (symbol: FSharpSymbol) =
83+
match symbol with
84+
| :? FSharpMemberOrFunctionOrValue as mfv ->
85+
if mfv.IsConstructor then SymbolKind.Constructor
86+
elif mfv.IsProperty then SymbolKind.Property
87+
elif mfv.IsMethod then SymbolKind.Method
88+
elif mfv.IsEvent then SymbolKind.Event
89+
else SymbolKind.Function
90+
| :? FSharpEntity as ent ->
91+
if ent.IsClass then SymbolKind.Class
92+
elif ent.IsInterface then SymbolKind.Interface
93+
elif ent.IsFSharpModule then SymbolKind.Module
94+
elif ent.IsEnum then SymbolKind.Enum
95+
elif ent.IsValueType then SymbolKind.Struct
96+
else SymbolKind.Object
97+
| _ -> SymbolKind.Function
98+
99+
open CallHierarchyHelpers
100+
53101
type AdaptiveFSharpLspServer
54102
(
55103
workspaceLoader: IWorkspaceLoader,
@@ -2218,31 +2266,12 @@ type AdaptiveFSharpLspServer
22182266
let allSymbolUses = tyRes.GetCheckResults.GetAllUsesOfAllSymbolsInFile()
22192267

22202268
// Filter to symbol uses within the function body, focusing only on calls
2221-
let bodySymbolUses =
2222-
allSymbolUses
2223-
|> Seq.filter (fun su ->
2224-
Range.rangeContainsRange bindingRange su.Range
2225-
&& not su.IsFromDefinition
2226-
&& su.Range.Start <> pos
2227-
// Filter to only include actual function/method calls, not parameter references or type annotations
2228-
&& match su.Symbol with
2229-
| :? FSharpMemberOrFunctionOrValue as mfv ->
2230-
// Include functions, methods, constructors but be careful with parameters vs calls
2231-
mfv.IsFunction
2232-
|| mfv.IsMethod
2233-
|| mfv.IsConstructor
2234-
|| (mfv.IsProperty
2235-
&& not (mfv.LogicalName.Contains("get_") || mfv.LogicalName.Contains("set_")))
2236-
| :? FSharpEntity as ent ->
2237-
// Include entities only if used as constructors (when they appear in expressions)
2238-
ent.IsClass || ent.IsFSharpRecord || ent.IsFSharpUnion
2239-
| _ -> false)
2240-
|> Seq.toArray
2269+
let bodySymbolUses = getOutgoingCallsInBinding bindingRange pos allSymbolUses
22412270

22422271
// Group symbol uses by the called symbol
22432272
let groupedBySymbol = bodySymbolUses |> Array.groupBy (fun su -> su.Symbol.FullName)
22442273

2245-
let createOutgoingCallItem (_symbolName: string, uses: FSharp.Compiler.CodeAnalysis.FSharpSymbolUse[]) =
2274+
let createOutgoingCallItem (_symbolName: string, uses: FSharpSymbolUse[]) =
22462275
asyncOption {
22472276
if uses.Length = 0 then
22482277
do! None
@@ -2260,20 +2289,8 @@ type AdaptiveFSharpLspServer
22602289
let targetFile = declLoc.FileName |> Utils.normalizePath
22612290
let targetUri = Path.LocalPathToUri targetFile
22622291

2263-
// Get symbol kind
2264-
let symbolKind =
2265-
match symbol with
2266-
| :? FSharpMemberOrFunctionOrValue as mfv ->
2267-
if mfv.IsConstructor then SymbolKind.Constructor
2268-
elif mfv.IsProperty then SymbolKind.Property
2269-
elif mfv.IsMethod then SymbolKind.Method
2270-
else SymbolKind.Function
2271-
| :? FSharpEntity as ent ->
2272-
if ent.IsClass then SymbolKind.Class
2273-
elif ent.IsInterface then SymbolKind.Interface
2274-
elif ent.IsFSharpModule then SymbolKind.Module
2275-
else SymbolKind.Object
2276-
| _ -> SymbolKind.Function
2292+
// Get symbol kind using helper
2293+
let symbolKind = getSymbolKind symbol
22772294

22782295
let displayName = symbol.DisplayName
22792296
let detail = $"In {System.IO.Path.GetFileName(UMX.untag targetFile)}"
@@ -2291,7 +2308,7 @@ type AdaptiveFSharpLspServer
22912308
// Symbol without declaration location (e.g., built-in functions)
22922309
Some
22932310
{ CallHierarchyItem.Name = symbol.DisplayName
2294-
Kind = SymbolKind.Function
2311+
Kind = getSymbolKind symbol
22952312
Tags = None
22962313
Detail = Some "Built-in"
22972314
Uri = p.Item.Uri // Use current file as fallback

test/FsAutoComplete.Tests.Lsp/CallHierarchyTests.fs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,108 @@ let outgoingTests createServer =
201201
"Should find higher-order function calls"
202202
}
203203

204+
testCaseAsync "OperatorExample - Custom operator calls"
205+
<| async {
206+
let! (aDoc, _) = Server.openDocument "OperatorExample.fsx" server
207+
use aDoc = aDoc
208+
let! server = server
209+
210+
// Test outgoing calls from main function (line 10, character 4 - on the function name)
211+
let prepareParams = CallHierarchyPrepareParams.create aDoc.Uri 10u 4u
212+
213+
let! prepareResult =
214+
server.Server.TextDocumentPrepareCallHierarchy prepareParams
215+
|> Async.map resultOptionGet
216+
217+
Expect.equal prepareResult.Length 1 "Should find one symbol"
218+
Expect.equal prepareResult[0].Name "main" "Should find main function"
219+
220+
let outgoingParams: CallHierarchyOutgoingCallsParams =
221+
{ Item = prepareResult[0]
222+
PartialResultToken = None
223+
WorkDoneToken = None }
224+
225+
let! outgoingResult =
226+
server.Server.CallHierarchyOutgoingCalls outgoingParams
227+
|> Async.map resultOptionGet
228+
229+
Expect.isGreaterThan outgoingResult.Length 0 "Should find outgoing calls"
230+
231+
// Should find calls to custom operators ++ and |>>
232+
let callNames = outgoingResult |> Array.map (fun call -> call.To.Name)
233+
Expect.isTrue
234+
(callNames |> Array.exists (fun name -> name.Contains("++") || name = "op_PlusPlus"))
235+
"Should find call to custom ++ operator"
236+
}
237+
238+
testCaseAsync "LocalFunctionExample - Local function calls"
239+
<| async {
240+
let! (aDoc, _) = Server.openDocument "LocalFunctionExample.fsx" server
241+
use aDoc = aDoc
242+
let! server = server
243+
244+
// Test outgoing calls from outerFunction (line 2, character 4 - on the function name)
245+
let prepareParams = CallHierarchyPrepareParams.create aDoc.Uri 2u 4u
246+
247+
let! prepareResult =
248+
server.Server.TextDocumentPrepareCallHierarchy prepareParams
249+
|> Async.map resultOptionGet
250+
251+
Expect.equal prepareResult.Length 1 "Should find one symbol"
252+
Expect.equal prepareResult[0].Name "outerFunction" "Should find outerFunction"
253+
254+
let outgoingParams: CallHierarchyOutgoingCallsParams =
255+
{ Item = prepareResult[0]
256+
PartialResultToken = None
257+
WorkDoneToken = None }
258+
259+
let! outgoingResult =
260+
server.Server.CallHierarchyOutgoingCalls outgoingParams
261+
|> Async.map resultOptionGet
262+
263+
Expect.isGreaterThan outgoingResult.Length 0 "Should find outgoing calls"
264+
265+
// Should find calls to local functions
266+
let callNames = outgoingResult |> Array.map (fun call -> call.To.Name)
267+
Expect.contains callNames "localHelper" "Should find call to localHelper"
268+
Expect.contains callNames "localProcessor" "Should find call to localProcessor"
269+
Expect.contains callNames "nestedOuter" "Should find call to nestedOuter"
270+
}
271+
272+
testCaseAsync "PropertyExample - Property and method calls"
273+
<| async {
274+
let! (aDoc, _) = Server.openDocument "PropertyExample.fsx" server
275+
use aDoc = aDoc
276+
let! server = server
277+
278+
// Test outgoing calls from main function (line 18 1-indexed = line 17 0-indexed, character 4 - on the function name)
279+
let prepareParams = CallHierarchyPrepareParams.create aDoc.Uri 17u 4u
280+
281+
let! prepareResult =
282+
server.Server.TextDocumentPrepareCallHierarchy prepareParams
283+
|> Async.map resultOptionGet
284+
285+
Expect.equal prepareResult.Length 1 "Should find one symbol"
286+
Expect.equal prepareResult[0].Name "main" "Should find main function"
287+
288+
let outgoingParams: CallHierarchyOutgoingCallsParams =
289+
{ Item = prepareResult[0]
290+
PartialResultToken = None
291+
WorkDoneToken = None }
292+
293+
let! outgoingResult =
294+
server.Server.CallHierarchyOutgoingCalls outgoingParams
295+
|> Async.map resultOptionGet
296+
297+
Expect.isGreaterThan outgoingResult.Length 0 "Should find outgoing calls"
298+
299+
// Should find calls to methods
300+
let callNames = outgoingResult |> Array.map (fun call -> call.To.Name)
301+
Expect.contains callNames "Increment" "Should find call to Increment method"
302+
Expect.contains callNames "GetDouble" "Should find call to GetDouble method"
303+
Expect.contains callNames "createPerson" "Should find call to createPerson function"
304+
}
305+
204306
testCaseAsync "RecursiveExample1 - Simple recursion and mutual recursion"
205307
<| async {
206308
let! (aDoc, _) = Server.openDocument "RecursiveExample1.fsx" server
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
module LocalFunctionExample
2+
3+
let outerFunction x =
4+
// Local function definition
5+
let localHelper y = y * 2
6+
7+
// Another local function that calls the first
8+
let localProcessor z =
9+
let doubled = localHelper z
10+
doubled + 1
11+
12+
// Nested local function
13+
let nestedOuter a =
14+
let nestedInner b = b + 10
15+
nestedInner a + localHelper a
16+
17+
// Using local functions - these should be detected as outgoing calls
18+
let result1 = localHelper x
19+
let result2 = localProcessor x
20+
let result3 = nestedOuter x
21+
22+
result1 + result2 + result3
23+
24+
ignore outerFunction
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
module OperatorExample
2+
3+
// Custom operator definition
4+
let (++) x y = x + y + 1
5+
6+
// Another custom operator
7+
let (|>>) x f = f x |> f
8+
9+
let double x = x * 2
10+
11+
let main () =
12+
// Using custom operators - these should be detected as outgoing calls
13+
let a = 1 ++ 2
14+
let b = 3 ++ 4
15+
let c = a ++ b
16+
17+
// Piping with custom operator
18+
let d = 5 |>> double
19+
20+
c + d
21+
22+
ignore main
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
module PropertyExample
2+
3+
type Counter() =
4+
let mutable count = 0
5+
6+
member _.Value
7+
with get () = count
8+
and set v = count <- v
9+
10+
member _.Increment() = count <- count + 1
11+
12+
member _.GetDouble() = count * 2
13+
14+
type Person = { mutable Name: string; mutable Age: int }
15+
16+
let createPerson name age = { Name = name; Age = age }
17+
18+
let main () =
19+
// Constructor call
20+
let counter = Counter()
21+
22+
// Property getter
23+
let initialValue = counter.Value
24+
25+
// Property setter
26+
counter.Value <- 10
27+
28+
// Method calls
29+
counter.Increment()
30+
let doubled = counter.GetDouble()
31+
32+
// Record creation and property access
33+
let person = createPerson "John" 30
34+
let personName = person.Name
35+
person.Age <- 31
36+
37+
initialValue + doubled + person.Age
38+
39+
ignore main

0 commit comments

Comments
 (0)