Skip to content

Commit

Permalink
Add inference when using dependency injection via module attribute (#133
Browse files Browse the repository at this point in the history
)

* Add new test case to MetadataBuilder

Add a test case of parsing a module attribute which calls to
`Application.get_env/3` to ensure future implementations that depends on it
does not break

* Add dependency injection inference
  • Loading branch information
gugahoa authored May 22, 2021
1 parent b0994e4 commit 1fb9b6b
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 0 deletions.
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 @@ -893,6 +893,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

0 comments on commit 1fb9b6b

Please sign in to comment.