diff --git a/lib/elixir_sense/core/binding.ex b/lib/elixir_sense/core/binding.ex index 8f731dc8..28d84a2f 100644 --- a/lib/elixir_sense/core/binding.ex +++ b/lib/elixir_sense/core/binding.ex @@ -154,6 +154,45 @@ defmodule ElixirSense.Core.Binding do end end + # dependency injection + def do_expand(env, {:call, {:atom, Application}, fun, args}, stack) + when fun in ~w(compile_env!)a do + # `Application.compile_env!/2` underneath works like `fetch_env!/2` + do_expand(env, {:call, {:atom, Application}, :fetch_env!, args}, stack) + end + + def do_expand(env, {:call, {:atom, Application}, fun, args}, stack) + when fun in ~w(compile_env)a do + # `Application.compile_env/3` underneath works like `get_env/3` + do_expand(env, {:call, {:atom, Application}, :get_env, args}, stack) + end + + def do_expand(env, {:call, {:atom, Application}, fun, args}, stack) + when fun in ~w(get_env fetch_env!)a do + try do + expanded_args = + args + |> Enum.map(&expand(env, &1, stack)) + |> Enum.map(&elem(&1, 1)) + + mod = apply(Application, fun, expanded_args) + + case mod do + :error -> + :none + + mod when is_atom(mod) -> + {:atom, mod} + + _ -> + nil + end + rescue + _ -> + :none + end + end + # remote call def do_expand(env, {:call, target, function, arguments}, stack) do if :none in arguments do diff --git a/test/elixir_sense/core/binding_test.exs b/test/elixir_sense/core/binding_test.exs index e3ebc427..ec09852a 100644 --- a/test/elixir_sense/core/binding_test.exs +++ b/test/elixir_sense/core/binding_test.exs @@ -11,6 +11,70 @@ defmodule ElixirSense.Core.BindingTest do @env %Binding{} describe "expand" do + def build_dependency_injection_binding(fetcher \\ :get_env, default_value \\ nil) + when is_atom(default_value) do + arguments = [ + atom: :elixir_sense, + atom: :some_attribute + ] + + arguments = if(default_value, do: arguments ++ [{:atom, default_value}], else: arguments) + + attribute_info = %AttributeInfo{ + name: :some_module, + type: {:call, {:atom, Application}, fetcher, arguments} + } + + Map.put(@env, :attributes, [ + attribute_info + ]) + end + + test "misconfigured dependency injection" do + Application.delete_env(:elixir_sense, :some_attribute) + + unsafe_fetchers = [:fetch_env!, :compile_env!] + safe_fetchers = [:get_env, :compile_env] + + Enum.each(unsafe_fetchers, fn fetcher -> + assert :none == + Binding.expand( + build_dependency_injection_binding(fetcher), + {:attribute, :some_module} + ) + end) + + Enum.each(safe_fetchers, fn fetcher -> + assert {:atom, nil} == + Binding.expand( + build_dependency_injection_binding(fetcher), + {:attribute, :some_module} + ) + end) + end + + test "dependency injection without default value" do + Application.put_env(:elixir_sense, :some_attribute, ElixirSenseExample.SameModule) + + fetchers = [:get_env, :fetch_env!, :compile_env, :compile_env!] + + Enum.each(fetchers, fn fetcher -> + assert {:atom, ElixirSenseExample.SameModule} == + Binding.expand( + build_dependency_injection_binding(fetcher), + {:attribute, :some_module} + ) + end) + end + + test "dependency injection with default value" do + assert {:atom, ElixirSenseExample.SameModule} == + Binding.expand( + build_dependency_injection_binding(:get_env, ElixirSenseExample.SameModule), + {:attribute, :some_module} + ) + end + test "map" do assert {:map, [abc: nil, cde: {:variable, :a}], nil} == Binding.expand(@env, {:map, [abc: nil, cde: {:variable, :a}], nil}) diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 7efbdd3a..5edf6356 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -38,10 +38,26 @@ defmodule ElixirSense.Core.MetadataBuilderTest do IO.puts @inner_attr end IO.puts "" + @otherattribute Application.get_env(:elixir_sense, :some_attribute, InnerModule) end """ |> string_to_state + assert get_line_attributes(state, 10) == [ + %ElixirSense.Core.State.AttributeInfo{ + name: :myattribute, + positions: [{2, 3}, {3, 11}], + type: {:atom, String} + }, + %AttributeInfo{ + name: :otherattribute, + positions: [{10, 3}], + type: + {:call, {:atom, Application}, :get_env, + [atom: :elixir_sense, atom: :some_attribute, atom: MyModule.InnerModule]} + } + ] + assert get_line_attributes(state, 3) == [ %AttributeInfo{ name: :myattribute, diff --git a/test/elixir_sense/providers/suggestion/complete_test.exs b/test/elixir_sense/providers/suggestion/complete_test.exs index 3a9a3cc1..301ac539 100644 --- a/test/elixir_sense/providers/suggestion/complete_test.exs +++ b/test/elixir_sense/providers/suggestion/complete_test.exs @@ -838,6 +838,71 @@ defmodule ElixirSense.Providers.Suggestion.CompleteTest do ] = expand('S.my_f', env) end + test "complete remote funs from injected module" do + env = %Env{ + scope_module: MyModule, + mods_and_funs: %{ + {Some.OtherModule, nil, nil} => %ModFunInfo{type: :defmodule}, + {Some.OtherModule, :my_fun_other_pub, nil} => %ModFunInfo{type: :def}, + {Some.OtherModule, :my_fun_other_pub, 1} => %ModFunInfo{ + type: :def, + params: [[{:some, [], nil}]] + }, + {Some.OtherModule, :my_fun_other_priv, nil} => %ModFunInfo{type: :defp}, + {Some.OtherModule, :my_fun_other_priv, 1} => %ModFunInfo{ + type: :defp, + params: [[{:some, [], nil}]] + } + }, + attributes: [ + %AttributeInfo{ + name: :get_module, + type: + {:call, {:atom, Application}, :get_env, + [atom: :elixir_sense, atom: :an_attribute, atom: Some.OtherModule]} + }, + %AttributeInfo{ + name: :compile_module, + type: + {:call, {:atom, Application}, :compile_env, + [atom: :elixir_sense, atom: :an_attribute, atom: Some.OtherModule]} + }, + %AttributeInfo{ + name: :fetch_module, + type: + {:call, {:atom, Application}, :fetch_env!, + [atom: :elixir_sense, atom: :other_attribute]} + }, + %AttributeInfo{ + name: :compile_bang_module, + type: + {:call, {:atom, Application}, :compile_env!, + [atom: :elixir_sense, atom: :other_attribute]} + } + ] + } + + assert [ + %{name: "my_fun_other_pub", origin: "Some.OtherModule"} + ] = expand('@get_module.my_f', env) + + assert [ + %{name: "my_fun_other_pub", origin: "Some.OtherModule"} + ] = expand('@compile_module.my_f', env) + + Application.put_env(:elixir_sense, :other_attribute, Some.OtherModule) + + assert [ + %{name: "my_fun_other_pub", origin: "Some.OtherModule"} + ] = expand('@fetch_module.my_f', env) + + assert [ + %{name: "my_fun_other_pub", origin: "Some.OtherModule"} + ] = expand('@compile_bang_module.my_f', env) + after + Application.delete_env(:elixir_sense, :other_attribute) + end + test "complete modules" do env = %Env{ scope_module: MyModule,