Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add inference when using dependency injection via module attribute #133

Merged
merged 2 commits into from
May 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions lib/elixir_sense/core/binding.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
64 changes: 64 additions & 0 deletions test/elixir_sense/core/binding_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down
16 changes: 16 additions & 0 deletions test/elixir_sense/core/metadata_builder_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
65 changes: 65 additions & 0 deletions test/elixir_sense/providers/suggestion/complete_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down