Skip to content

Commit b772097

Browse files
committed
Fix crash in call hierarchy provider on anonymous function calls
expand attributes and variables
1 parent 933c2a3 commit b772097

3 files changed

Lines changed: 92 additions & 15 deletions

File tree

apps/language_server/lib/language_server/providers/call_hierarchy/locator.ex

Lines changed: 90 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,10 @@ defmodule ElixirLS.LanguageServer.Providers.CallHierarchy.Locator do
225225
}
226226
end
227227

228+
defp wrap_atom({nil, other}), do: {nil, other}
229+
defp wrap_atom({atom, other}) when is_atom(atom), do: {{:atom, atom}, other}
230+
defp wrap_atom(other), do: other
231+
228232
defp expand({nil, func}, _env, module, _aliases) when module != nil,
229233
do: {nil, func}
230234

@@ -294,13 +298,50 @@ defmodule ElixirLS.LanguageServer.Providers.CallHierarchy.Locator do
294298
else
295299
all_calls = metadata.calls |> Map.values() |> List.flatten()
296300

301+
mods_funs = metadata.mods_funs_to_positions
302+
metadata_types = metadata.types
303+
297304
filtered_calls =
298305
all_calls
306+
# Filter out calls with nil function (anonymous function calls)
307+
|> Enum.filter(fn call -> call.func != nil end)
299308
|> Enum.filter(fn call ->
300-
# Check for the specific function, module and arity
301-
call.func == function and
302-
call.mod == module and
303-
(arity == :any or call.arity == arity)
309+
# Get environment at call position to expand variables and attributes
310+
{call_line, call_column} = call.position
311+
env = Metadata.get_cursor_env(metadata, {call_line, call_column || 1})
312+
313+
binding_env = Binding.from_env(env, metadata, call.position)
314+
315+
# Determine the module for local calls
316+
mod_or_nil =
317+
case call.kind do
318+
:local_function -> nil
319+
:local_macro -> nil
320+
_ -> call.mod
321+
end
322+
323+
# Expand variables and attributes to get actual module and function
324+
found =
325+
{mod_or_nil, call.func}
326+
|> wrap_atom
327+
|> expand(binding_env, env.module, env.aliases)
328+
|> Introspection.actual_mod_fun(
329+
env,
330+
mods_funs,
331+
metadata_types,
332+
call.position,
333+
false
334+
)
335+
336+
# Check if this call matches our target
337+
case found do
338+
{^module, ^function, true, :mod_fun} ->
339+
# Check arity
340+
arity == :any or call.arity == arity
341+
342+
_ ->
343+
false
344+
end
304345
end)
305346

306347
group_calls_by_caller(filtered_calls, metadata)
@@ -500,25 +541,61 @@ defmodule ElixirLS.LanguageServer.Providers.CallHierarchy.Locator do
500541
(end_line == nil or call_line <= end_line)
501542
end)
502543

503-
# Group by callee
544+
# Expand calls to resolve variables and attributes, then group by callee
504545
calls
505-
|> Enum.group_by(fn call ->
506-
# For local calls (mod == nil), use the module from the current context
507-
callee_mod = call.mod || module
508-
{callee_mod, call.func, call.arity}
546+
# Filter out calls where func is not an atom (e.g., anonymous functions)
547+
# ElixirSense can produce calls where func is a tuple (AST) for anonymous function calls
548+
|> Enum.filter(fn call -> is_atom(call.func) end)
549+
|> Enum.flat_map(fn call ->
550+
# Get environment at call position to expand variables and attributes
551+
{call_line, call_column} = call.position
552+
env = Metadata.get_cursor_env(metadata, {call_line, call_column || 1})
553+
554+
binding_env = Binding.from_env(env, metadata, call.position)
555+
556+
# Expand variables and attributes to get actual module
557+
{expanded_mod, expanded_fun} =
558+
{call.mod, call.func}
559+
|> wrap_atom
560+
|> expand(binding_env, env.module, env.aliases)
561+
562+
# For outgoing calls, include the call if we could expand the module/function
563+
# or if it's already a concrete module (even if not in metadata)
564+
case {expanded_mod, expanded_fun} do
565+
{nil, nil} ->
566+
# Couldn't expand - skip this call
567+
[]
568+
569+
{actual_mod, actual_fun} when is_atom(actual_mod) and is_atom(actual_fun) ->
570+
# Successfully resolved to a module and function
571+
[{actual_mod, actual_fun, call.arity, call}]
572+
573+
{nil, actual_fun} when is_atom(actual_fun) ->
574+
# Local call without module - use the module from the function parameter
575+
[{module, actual_fun, call.arity, call}]
576+
577+
_ ->
578+
[]
579+
end
509580
end)
510-
|> Enum.map(fn {{mod, fun, call_arity}, calls} ->
581+
|> Enum.group_by(fn {mod, fun, call_arity, _call} ->
582+
{mod, fun, call_arity}
583+
end)
584+
|> Enum.map(fn {{mod, fun, call_arity}, expanded_calls} ->
585+
# Extract original calls from the expanded tuples
586+
original_calls = Enum.map(expanded_calls, fn {_, _, _, call} -> call end)
587+
511588
%{
512589
to: %{
513590
name: "#{inspect(mod)}.#{fun}/#{call_arity}",
514591
kind: GenLSP.Enumerations.SymbolKind.function(),
515592
uri: nil,
516-
range: build_range_from_call(List.first(calls)),
517-
selection_range: build_range_from_call(List.first(calls)),
593+
range: build_range_from_call(List.first(original_calls)),
594+
selection_range: build_range_from_call(List.first(original_calls)),
518595
tags: [],
519596
detail: nil
520597
},
521-
from_ranges: Enum.map(calls, &build_range_from_call/1)
598+
from_ranges: Enum.map(original_calls, &build_range_from_call/1)
522599
}
523600
end)
524601
else

dep_versions.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[
2-
elixir_sense: "0e6278a48703f065a7bc4c5f9c0575f7836ebe5b",
2+
elixir_sense: "bd6c527fee5725250e68d676c1c19a15bf4906c4",
33
dialyxir_vendored: "f8f64cfb6797c518294687e7c03ae817bacbc6ee",
44
jason_v: "f1c10fa9c445cb9f300266122ef18671054b2330",
55
erl2ex_vendored: "073ac6b9a44282e718b6050c7b27cedf9217a12a",

mix.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"benchee": {:hex, :benchee, "1.1.0", "f3a43817209a92a1fade36ef36b86e1052627fd8934a8b937ac9ab3a76c43062", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}], "hexpm", "7da57d545003165a012b587077f6ba90b89210fd88074ce3c60ce239eb5e6d93"},
33
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"},
44
"dialyxir_vendored": {:git, "https://github.com/elixir-lsp/dialyxir.git", "f8f64cfb6797c518294687e7c03ae817bacbc6ee", [ref: "f8f64cfb6797c518294687e7c03ae817bacbc6ee"]},
5-
"elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "0e6278a48703f065a7bc4c5f9c0575f7836ebe5b", [ref: "0e6278a48703f065a7bc4c5f9c0575f7836ebe5b"]},
5+
"elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "bd6c527fee5725250e68d676c1c19a15bf4906c4", [ref: "bd6c527fee5725250e68d676c1c19a15bf4906c4"]},
66
"erl2ex_vendored": {:git, "https://github.com/elixir-lsp/erl2ex.git", "073ac6b9a44282e718b6050c7b27cedf9217a12a", [ref: "073ac6b9a44282e718b6050c7b27cedf9217a12a"]},
77
"erlex_vendored": {:git, "https://github.com/elixir-lsp/erlex.git", "c0e448db27bcbb3f369861d13e3b0607ed37048d", [ref: "c0e448db27bcbb3f369861d13e3b0607ed37048d"]},
88
"jason_v": {:git, "https://github.com/elixir-lsp/jason.git", "f1c10fa9c445cb9f300266122ef18671054b2330", [ref: "f1c10fa9c445cb9f300266122ef18671054b2330"]},

0 commit comments

Comments
 (0)