diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a88b907..687278ce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,15 +14,6 @@ jobs: fail-fast: false matrix: include: - - elixir: 1.12.x - otp: 22.x - tests_may_fail: false - - elixir: 1.12.x - otp: 23.x - tests_may_fail: false - - elixir: 1.12.x - otp: 24.x - tests_may_fail: false - elixir: 1.13.x otp: 22.x tests_may_fail: false @@ -65,6 +56,15 @@ jobs: - elixir: 1.16.x otp: 26.x tests_may_fail: false + - elixir: 1.17.x + otp: 25.x + tests_may_fail: false + - elixir: 1.17.x + otp: 26.x + tests_may_fail: false + - elixir: 1.17.x + otp: 27.x + tests_may_fail: false env: MIX_ENV: test steps: @@ -87,15 +87,6 @@ jobs: fail-fast: false matrix: include: - - elixir: 1.12.x - otp: 22.x - tests_may_fail: false - - elixir: 1.12.x - otp: 23.x - tests_may_fail: false - - elixir: 1.12.x - otp: 24.x - tests_may_fail: false - elixir: 1.13.x otp: 22.x tests_may_fail: false @@ -138,6 +129,15 @@ jobs: - elixir: 1.16.x otp: 26.x tests_may_fail: false + - elixir: 1.17.x + otp: 25.x + tests_may_fail: false + - elixir: 1.17.x + otp: 26.x + tests_may_fail: false + - elixir: 1.17.x + otp: 27.x + tests_may_fail: false env: MIX_ENV: test steps: @@ -165,8 +165,8 @@ jobs: strategy: matrix: include: - - elixir: 1.16.x - otp: 26.x + - elixir: 1.17.x + otp: 27.x steps: - uses: actions/checkout@v4 - uses: erlef/setup-beam@v1 diff --git a/lib/elixir_sense/core/ast.ex b/lib/elixir_sense/core/ast.ex deleted file mode 100644 index b8556dbd..00000000 --- a/lib/elixir_sense/core/ast.ex +++ /dev/null @@ -1,198 +0,0 @@ -defmodule ElixirSense.Core.Ast do - @moduledoc """ - Abstract Syntax Tree support - """ - - # TODO the code in this module is broken and probably violates GPL license - - @partials [ - :def, - :defp, - :defmodule, - :defprotocol, - :defimpl, - :defstruct, - :defexception, - :@, - :defmacro, - :defmacrop, - :defguard, - :defguardp, - :defdelegate, - :defoverridable, - :fn, - :__ENV__, - :__CALLER__, - :raise, - :throw, - :reraise, - :send, - :if, - :unless, - :with, - :case, - :cond, - :try, - :for, - :receive, - :in - ] - - @max_expand_count 30_000 - - def expand_partial(ast, env) do - {expanded_ast, _} = Macro.prewalk(ast, {env, 1}, &do_expand_partial/2) - expanded_ast - rescue - _e -> ast - catch - e -> e - end - - def expand_all(ast, env) do - try do - {expanded_ast, _} = Macro.prewalk(ast, {env, 1}, &do_expand_all/2) - expanded_ast - rescue - _e -> ast - catch - e -> e - end - end - - def set_module_for_env(env, module) do - Map.put(env, :module, module) - end - - def add_requires_to_env(env, modules) do - add_directive_modules_to_env(env, :require, modules) - end - - def add_imports_to_env(env, modules) do - add_directive_modules_to_env(env, :import, modules) - end - - defp add_directive_modules_to_env(env, directive, modules) do - directive_string = - modules - |> Enum.map(&format_module(directive, &1)) - |> Enum.filter(&(&1 != nil)) - |> Enum.join("; ") - - {new_env, _} = Code.eval_string("#{directive_string}; __ENV__", [], env) - new_env - end - - defp format_module(_directive, Elixir), do: nil - - defp format_module(directive, module) when is_atom(module) do - if match?({:module, _}, Code.ensure_compiled(module)) do - "#{directive} #{inspect(module)}" - end - end - - defp format_module(directive, {module, options}) when is_atom(module) do - if match?({:module, _}, Code.ensure_compiled(module)) do - formatted_options = - if options != [] do - ", " <> Macro.to_string(options) - else - "" - end - - "#{directive} #{inspect(module)}#{formatted_options}" - end - end - - defp do_expand_all(ast, acc) do - do_expand(ast, acc) - end - - defp do_expand_partial({name, _, _} = ast, acc) when name in @partials do - {ast, acc} - end - - defp do_expand_partial(ast, acc) do - do_expand(ast, acc) - end - - # TODO should we add imports here as well? - - defp do_expand({:require, _, _} = ast, {env, count}) do - # TODO is it ok to loose alias_tuples here? - {modules, _alias_tuples} = extract_directive_modules(:require, ast) - new_env = add_requires_to_env(env, modules) - {ast, {new_env, count}} - end - - defp do_expand(ast, acc) do - do_expand_with_fixes(ast, acc) - end - - # Fix inexpansible `use ExUnit.Case` - defp do_expand_with_fixes({:use, _, [{:__aliases__, _, [:ExUnit, :Case]} | _]}, acc) do - ast = - quote do - import ExUnit.Callbacks - import ExUnit.Assertions - import ExUnit.Case - import ExUnit.DocTest - end - - {ast, acc} - end - - defp do_expand_with_fixes(ast, {env, count}) do - if count > @max_expand_count do - throw({:expand_error, "Cannot expand recursive macro"}) - end - - try do - expanded_ast = Macro.expand(ast, env) - {expanded_ast, {env, count + 1}} - rescue - _e -> - {ast, {env, count + 1}} - end - end - - defp extract_directive_modules(directive, ast) do - case ast do - # multi notation - {^directive, _, [{{:., _, [{:__aliases__, _, prefix_atoms}, :{}]}, _, aliases}]} -> - list = - aliases - |> Enum.map(fn {:__aliases__, _, mods} -> - Module.concat(prefix_atoms ++ mods) - end) - - {list, []} - - # with options - {^directive, _, [module, opts]} when is_atom(module) -> - alias_tuples = - case opts |> Keyword.get(:as) do - nil -> [] - alias -> [{alias, module}] - end - - {[module], alias_tuples} - - # with options - {^directive, _, [{:__aliases__, _, module_parts}, _opts]} -> - {[module_parts |> Module.concat()], []} - - # without options - {^directive, _, [{:__aliases__, _, module_parts}]} -> - {[module_parts |> Module.concat()], []} - - # without options - {^directive, _, [module]} when is_atom(module) -> - {[module], []} - - {^directive, _, [{{:., _, [prefix, :{}]}, _, suffixes} | _]} when is_list(suffixes) -> - list = for suffix <- suffixes, do: Module.concat(prefix, suffix) - {list, []} - end - end -end diff --git a/lib/elixir_sense/core/binding.ex b/lib/elixir_sense/core/binding.ex index 83b823fa..8dfd7ba1 100644 --- a/lib/elixir_sense/core/binding.ex +++ b/lib/elixir_sense/core/binding.ex @@ -8,6 +8,7 @@ defmodule ElixirSense.Core.Binding do alias ElixirSense.Core.Struct alias ElixirSense.Core.TypeInfo + # TODO refactor to use env defstruct structs: %{}, variables: [], attributes: [], @@ -83,20 +84,21 @@ defmodule ElixirSense.Core.Binding do expand(env, combined, stack) end - def do_expand(%Binding{variables: variables} = env, {:variable, variable}, stack) do + def do_expand(%Binding{variables: variables} = env, {:variable, variable, version}, stack) do + sorted_variables = Enum.sort_by(variables, &{&1.name, -&1.version}) + type = - case Enum.find(variables, fn %{name: name} -> name == variable end) do + case Enum.find(sorted_variables, fn %State.VarInfo{} = var -> + var.name == variable and (var.version == version or version == :any) + end) do nil -> # no variable found - treat a local call - expand(env, {:local_call, variable, []}, stack) + # this cannot happen if no parens call is missclassed as variable e.g. by + # Code.Fragment APIs + {:local_call, variable, []} - %State.VarInfo{name: name, type: type} -> - # filter underscored variables - if name |> Atom.to_string() |> String.starts_with?("_") do - :none - else - type - end + %State.VarInfo{type: type} -> + type end expand(env, type, stack) @@ -339,11 +341,28 @@ defmodule ElixirSense.Core.Binding do def do_expand(_env, {:integer, integer}, _stack), do: {:integer, integer} - def do_expand(_env, {:union, [first | rest]} = u, _stack) do - if Enum.all?(rest, &(&1 == first)) do - first - else - u + def do_expand(_env, {:union, all}, _stack) do + # TODO implement union for maps and lists? + all = Enum.filter(all, &(&1 != :none)) + + cond do + all == [] -> + :none + + Enum.any?(all, &(&1 == nil)) -> + nil + + match?([_], all) -> + hd(all) + + true -> + first = hd(all) + + if Enum.all?(tl(all), &(&1 == first)) do + first + else + {:union, all} + end end end @@ -378,6 +397,27 @@ defmodule ElixirSense.Core.Binding do end end + defp expand_call( + env, + {:atom, module}, + name, + [list_candidate | _], + _include_private, + stack + ) + when name in [:++, :--] and module in [Kernel, :erlang] do + case expand(env, list_candidate, stack) do + {:list, type} -> + {:list, type} + + nil -> + nil + + _ -> + :none + end + end + defp expand_call( env, {:atom, Kernel}, @@ -398,14 +438,260 @@ defmodule ElixirSense.Core.Binding do end end + # rewritten elem + defp expand_call( + env, + {:atom, :erlang}, + :element, + [n_candidate, tuple_candidate], + _include_private, + stack + ) do + case expand(env, n_candidate, stack) do + {:integer, n} -> + expand(env, {:tuple_nth, tuple_candidate, n - 1}, stack) + + nil -> + nil + + _ -> + :none + end + end + defp expand_call( env, {:atom, Kernel}, + :put_elem, + [tuple_candidate, n_candidate, value], + _include_private, + stack + ) do + with {:tuple, elems_count, elems} <- expand(env, tuple_candidate, stack), + {:integer, n} when n >= 0 and n < elems_count <- expand(env, n_candidate, stack), + expanded_value when expanded_value != :none <- expand(env, value, stack) do + {:tuple, elems_count, elems |> List.replace_at(n, expanded_value)} + else + nil -> + nil + + _ -> + :none + end + end + + # rewritten put_elem + defp expand_call( + env, + {:atom, :erlang}, + :setelement, + [n_candidate, tuple_candidate, value], + _include_private, + stack + ) do + with {:tuple, elems_count, elems} <- expand(env, tuple_candidate, stack), + {:integer, n} when n >= 1 and n <= elems_count <- expand(env, n_candidate, stack), + expanded_value when expanded_value != :none <- expand(env, value, stack) do + {:tuple, elems_count, elems |> List.replace_at(n - 1, expanded_value)} + else + nil -> + nil + + _ -> + :none + end + end + + defp expand_call( + env, + {:atom, module}, + fun, + [tuple_candidate, value], + _include_private, + stack + ) + when (module == Tuple and fun == :append) or (module == :erlang and fun == :append_element) do + with {:tuple, elems_count, elems} <- expand(env, tuple_candidate, stack), + expanded_value when expanded_value != :none <- expand(env, value, stack) do + {:tuple, elems_count + 1, elems ++ [expanded_value]} + else + nil -> + nil + + _ -> + :none + end + end + + defp expand_call( + env, + {:atom, Tuple}, + :delete_at, + [tuple_candidate, n_candidate], + _include_private, + stack + ) do + with {:tuple, elems_count, elems} <- expand(env, tuple_candidate, stack), + {:integer, n} when n >= 0 and n < elems_count <- expand(env, n_candidate, stack) do + {:tuple, elems_count - 1, elems |> List.delete_at(n)} + else + nil -> + nil + + _ -> + :none + end + end + + # rewritten Tuple.delete_at + defp expand_call( + env, + {:atom, :erlang}, + :delete_element, + [n_candidate, tuple_candidate], + _include_private, + stack + ) do + with {:tuple, elems_count, elems} <- expand(env, tuple_candidate, stack), + {:integer, n} when n > 0 and n <= elems_count <- expand(env, n_candidate, stack) do + {:tuple, elems_count - 1, elems |> List.delete_at(n - 1)} + else + nil -> + nil + + _ -> + :none + end + end + + defp expand_call( + env, + {:atom, Tuple}, + :insert_at, + [tuple_candidate, n_candidate, value], + _include_private, + stack + ) do + with {:tuple, elems_count, elems} <- expand(env, tuple_candidate, stack), + {:integer, n} when n >= 0 and n <= elems_count <- expand(env, n_candidate, stack), + expanded_value when expanded_value != :none <- expand(env, value, stack) do + {:tuple, elems_count + 1, elems |> List.insert_at(n, expanded_value)} + else + nil -> + nil + + _ -> + :none + end + end + + # rewritten Tuple.insert_at + defp expand_call( + env, + {:atom, :erlang}, + :insert_element, + [n_candidate, tuple_candidate, value], + _include_private, + stack + ) do + with {:tuple, elems_count, elems} <- expand(env, tuple_candidate, stack), + {:integer, n} when n > 0 and n <= elems_count + 1 <- expand(env, n_candidate, stack), + expanded_value when expanded_value != :none <- expand(env, value, stack) do + {:tuple, elems_count + 1, elems |> List.insert_at(n - 1, expanded_value)} + else + nil -> + nil + + _ -> + :none + end + end + + defp expand_call( + env, + {:atom, module}, + fun, + [tuple_candidate], + _include_private, + stack + ) + when (module == Tuple and fun == :to_list) or (module == :erlang and fun == :tuple_to_list) do + with {:tuple, _elems_count, elems} <- expand(env, tuple_candidate, stack) do + case elems do + [] -> {:list, :empty} + [first | _] -> {:list, first} + end + else + nil -> + nil + + _ -> + :none + end + end + + defp expand_call( + env, + {:atom, module}, + :tuple_size, + [tuple_candidate], + _include_private, + stack + ) + when module in [Kernel, :erlang] do + with {:tuple, elems_count, _elems} <- expand(env, tuple_candidate, stack) do + {:integer, elems_count} + else + nil -> + nil + + _ -> + :none + end + end + + defp expand_call( + env, + {:atom, module}, + fun, + [value, n_candidate], + _include_private, + stack + ) + when (module == Tuple and fun == :duplicate) or (module == :erlang and fun == :make_tuple) do + {value, n_candidate} = + if module == :erlang do + {n_candidate, value} + else + {value, n_candidate} + end + + # limit to 5 + with {:integer, n} when n >= 0 <- expand(env, n_candidate, stack), + expanded_value when expanded_value != :none <- expand(env, value, stack) do + {:tuple, n, expanded_value |> List.duplicate(n)} + else + nil -> + nil + + {:integer, _n} -> + nil + + _ -> + :none + end + end + + # hd is inlined + defp expand_call( + env, + {:atom, module}, :hd, [list_candidate], _include_private, stack - ) do + ) + when module in [Kernel, :erlang] do case expand(env, list_candidate, stack) do {:list, type} -> type @@ -418,14 +704,16 @@ defmodule ElixirSense.Core.Binding do end end + # tl is inlined defp expand_call( env, - {:atom, Kernel}, + {:atom, module}, :tl, [list_candidate], _include_private, stack - ) do + ) + when module in [Kernel, :erlang] do case expand(env, list_candidate, stack) do {:list, type} -> {:list, type} @@ -722,8 +1010,17 @@ defmodule ElixirSense.Core.Binding do end end - defp expand_call(env, {:atom, Map}, fun, [map, key], _include_private, stack) - when fun in [:fetch, :fetch!, :get] do + defp expand_call(env, {:atom, module}, fun, [map, key], _include_private, stack) + when (module == Map and fun in [:fetch, :fetch!, :get]) or + (module == :maps and fun in [:find, :get]) do + {map, key} = + if module == :maps do + # rewritten versions have different arg order + {key, map} + else + {map, key} + end + fields = expand_map_fields(env, map, stack) if :none in fields do @@ -733,7 +1030,7 @@ defmodule ElixirSense.Core.Binding do {:atom, atom} -> value = fields |> Keyword.get(atom) - if fun == :fetch and value != nil do + if fun in [:fetch, :find] and value != nil do {:tuple, 2, [{:atom, :ok}, value]} else value @@ -769,8 +1066,17 @@ defmodule ElixirSense.Core.Binding do end end - defp expand_call(env, {:atom, Map}, fun, [map, key, value], _include_private, stack) - when fun in [:put, :replace!] do + defp expand_call(env, {:atom, module}, fun, [map, key, value], _include_private, stack) + when (fun == :put and module in [Map, :maps]) or (fun == :update and module == :maps) or + (fun == :replace! and module == Map) do + {map, key, value} = + if module == :maps do + # rewritten versions have different parameter order + {value, map, key} + else + {map, key, value} + end + fields = expand_map_fields(env, map, stack) if :none in fields do @@ -810,7 +1116,16 @@ defmodule ElixirSense.Core.Binding do end end - defp expand_call(env, {:atom, Map}, :delete, [map, key], _include_private, stack) do + defp expand_call(env, {:atom, module}, fun, [map, key], _include_private, stack) + when (module == Map and fun == :delete) or (module == :maps and fun == :remove) do + {map, key} = + if module == :maps do + # rewritten versions have different arg order + {key, map} + else + {map, key} + end + fields = expand_map_fields(env, map, stack) if :none in fields do @@ -829,7 +1144,9 @@ defmodule ElixirSense.Core.Binding do end end - defp expand_call(env, {:atom, Map}, :merge, [map, other_map], _include_private, stack) do + # Map.merge/2 is inlined + defp expand_call(env, {:atom, module}, :merge, [map, other_map], _include_private, stack) + when module in [Map, :maps] do fields = expand_map_fields(env, map, stack) other_fields = @@ -1074,7 +1391,7 @@ defmodule ElixirSense.Core.Binding do {:ok, {:@, _, [{_kind, _, [ast]}]}} -> case extract_type(ast) do {:ok, type} -> - parsed_type = parse_type(env, type, mod, include_private) + parsed_type = parse_type(env, type, mod, include_private, []) expand(env, parsed_type, stack) :error -> @@ -1114,7 +1431,7 @@ defmodule ElixirSense.Core.Binding do defp get_return_from_spec(env, {{fun, _arity}, [ast]}, mod, include_private) do case Typespec.spec_to_quoted(fun, ast) |> extract_type do {:ok, type} -> - parse_type(env, type, mod, include_private) + parse_type(env, type, mod, include_private, []) :error -> nil @@ -1130,8 +1447,8 @@ defmodule ElixirSense.Core.Binding do end # union type - defp parse_type(env, {:|, _, variants}, mod, include_private) do - {:union, variants |> Enum.map(&parse_type(env, &1, mod, include_private))} + defp parse_type(env, {:|, _, variants}, mod, include_private, stack) do + {:union, variants |> Enum.map(&parse_type(env, &1, mod, include_private, stack))} end # struct @@ -1143,12 +1460,13 @@ defmodule ElixirSense.Core.Binding do {:%{}, _, fields} ]}, mod, - include_private + include_private, + stack ) do fields = for {field, type} <- fields, is_atom(field), - do: {field, parse_type(env, type, mod, include_private)} + do: {field, parse_type(env, type, mod, include_private, stack)} module = case struct_mod do @@ -1163,57 +1481,58 @@ defmodule ElixirSense.Core.Binding do end # map - defp parse_type(env, {:%{}, _, fields}, mod, include_private) do + defp parse_type(env, {:%{}, _, fields}, mod, include_private, stack) do fields = for {field, type} <- fields, field = drop_optional(field), is_atom(field), - do: {field, parse_type(env, type, mod, include_private)} + do: {field, parse_type(env, type, mod, include_private, stack)} {:map, fields, nil} end - defp parse_type(_env, {:map, _, []}, _mod, _include_private) do + defp parse_type(_env, {:map, _, []}, _mod, _include_private, _stack) do {:map, [], nil} end - defp parse_type(env, {:{}, _, fields}, mod, include_private) do - {:tuple, length(fields), fields |> Enum.map(&parse_type(env, &1, mod, include_private))} + defp parse_type(env, {:{}, _, fields}, mod, include_private, stack) do + {:tuple, length(fields), + fields |> Enum.map(&parse_type(env, &1, mod, include_private, stack))} end - defp parse_type(_env, [], _mod, _include_private) do + defp parse_type(_env, [], _mod, _include_private, _stack) do {:list, :empty} end - defp parse_type(env, [type | _], mod, include_private) do - {:list, parse_type(env, type, mod, include_private)} + defp parse_type(env, [type | _], mod, include_private, stack) do + {:list, parse_type(env, type, mod, include_private, stack)} end # for simplicity we skip terminator type - defp parse_type(env, {kind, _, [type, _]}, mod, include_private) + defp parse_type(env, {kind, _, [type, _]}, mod, include_private, stack) when kind in [:maybe_improper_list, :nonempty_improper_list, :nonempty_maybe_improper_list] do - {:list, parse_type(env, type, mod, include_private)} + {:list, parse_type(env, type, mod, include_private, stack)} end - defp parse_type(_env, {:list, _, []}, _mod, _include_private) do + defp parse_type(_env, {:list, _, []}, _mod, _include_private, _stack) do {:list, nil} end - defp parse_type(_env, {:keyword, _, []}, _mod, _include_private) do - # TODO no support for atom type for now + defp parse_type(_env, {:keyword, _, []}, _mod, _include_private, _stack) do + # no support for atom type for now {:list, {:tuple, 2, [nil, nil]}} end - defp parse_type(env, {:keyword, _, [type]}, mod, include_private) do - # TODO no support for atom type for now - {:list, {:tuple, 2, [nil, parse_type(env, type, mod, include_private)]}} + defp parse_type(env, {:keyword, _, [type]}, mod, include_private, stack) do + # no support for atom type for now + {:list, {:tuple, 2, [nil, parse_type(env, type, mod, include_private, stack)]}} end # remote user type - defp parse_type(env, {{:., _, [mod, atom]}, _, args}, _mod, _include_private) + defp parse_type(env, {{:., _, [mod, atom]}, _, args}, _mod, _include_private, stack) when is_atom(mod) and is_atom(atom) do # do not propagate include_private when expanding remote types - expand_type(env, mod, atom, args, false) + expand_type(env, mod, atom, args, false, stack) end # remote user type @@ -1221,41 +1540,55 @@ defmodule ElixirSense.Core.Binding do env, {{:., _, [{:__aliases__, _, aliases}, atom]}, _, args}, _mod, - _include_private + _include_private, + stack ) when is_atom(atom) do # do not propagate include_private when expanding remote types - expand_type(env, Module.concat(aliases), atom, args, false) + expand_type(env, Module.concat(aliases), atom, args, false, stack) end # no_return - defp parse_type(_env, {:no_return, _, _}, _, _include_private), do: :none + defp parse_type(_env, {:no_return, _, _}, _, _include_private, _stack), do: :none + + # term, any, dynamic + defp parse_type(_env, {kind, _, _}, _, _include_private, _stack) + when kind in [:term, :any, :dynamic], + do: nil # local user type - defp parse_type(env, {atom, _, args}, mod, include_private) when is_atom(atom) do + defp parse_type(env, {atom, _, args}, mod, include_private, stack) when is_atom(atom) do # propagate include_private when expanding local types - expand_type(env, mod, atom, args, include_private) + expand_type(env, mod, atom, args, include_private, stack) end # atom - defp parse_type(_env, atom, _, _include_private) when is_atom(atom), do: {:atom, atom} + defp parse_type(_env, atom, _, _include_private, _stack) when is_atom(atom), do: {:atom, atom} - defp parse_type(_env, integer, _, _include_private) when is_integer(integer) do + defp parse_type(_env, integer, _, _include_private, _stack) when is_integer(integer) do {:integer, integer} end # other - # defp parse_type(_env, t, _, _include_private) do - # IO.inspect t - # nil - # end - defp parse_type(_env, _, _, _include_private), do: nil + defp parse_type(_env, _type, _, _include_private, _stack), do: nil + + defp expand_type(env, mod, type_name, args, include_private, stack) do + arity = length(args || []) + type = {mod, type_name, arity} - defp expand_type(env, mod, type_name, args, include_private) do + if type in stack do + # self referential type + nil + else + do_expand_type(env, mod, type_name, args, include_private, [type | stack]) + end + end + + defp do_expand_type(env, mod, type_name, args, include_private, stack) do arity = length(args || []) - case expand_type_from_metadata(env, mod, type_name, arity, include_private) do - nil -> expand_type_from_introspection(env, mod, type_name, arity, include_private) + case expand_type_from_metadata(env, mod, type_name, arity, include_private, stack) do + nil -> expand_type_from_introspection(env, mod, type_name, arity, include_private, stack) res -> res end |> drop_no_spec @@ -1268,7 +1601,8 @@ defmodule ElixirSense.Core.Binding do mod, type_name, arity, - include_private + include_private, + stack ) do case types[{mod, type_name, arity}] do %State.TypeInfo{specs: [type_spec], kind: kind} @@ -1277,7 +1611,7 @@ defmodule ElixirSense.Core.Binding do {:ok, {:@, _, [{_kind, _, [ast]}]}} -> case extract_type(ast) do {:ok, type} -> - parse_type(env, type, mod, include_private) || :no_spec + parse_type(env, type, mod, include_private, stack) || :no_spec :error -> nil @@ -1295,12 +1629,12 @@ defmodule ElixirSense.Core.Binding do end end - defp expand_type_from_introspection(env, mod, type_name, arity, include_private) do + defp expand_type_from_introspection(env, mod, type_name, arity, include_private, stack) do case TypeInfo.get_type_spec(mod, type_name, arity) do {kind, spec} when type_is_public(kind, include_private) -> - {:"::", _, [_, type]} = Typespec.type_to_quoted(spec) + {:"::", _, [{_expanded_name, _, _}, type]} = Typespec.type_to_quoted(spec) - parse_type(env, type, mod, include_private) + parse_type(env, type, mod, include_private, stack) _ -> nil @@ -1316,6 +1650,8 @@ defmodule ElixirSense.Core.Binding do defp combine_intersection(type, nil), do: type defp combine_intersection(type, type), do: type + # NOTE intersection is not strict and does an union on map keys + defp combine_intersection({:struct, fields_1, nil, nil}, {:struct, fields_2, nil, nil}) do keys = (safe_keys(fields_1) ++ safe_keys(fields_2)) |> Enum.uniq() fields = for k <- keys, do: {k, combine_intersection(fields_1[k], fields_2[k])} diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex new file mode 100644 index 00000000..a826a950 --- /dev/null +++ b/lib/elixir_sense/core/compiler.ex @@ -0,0 +1,2611 @@ +defmodule ElixirSense.Core.Compiler do + alias ElixirSense.Core.Compiler.State + require Logger + alias ElixirSense.Core.Introspection + alias ElixirSense.Core.TypeInfo + alias ElixirSense.Core.TypeInference + alias ElixirSense.Core.TypeInference.Guard + alias ElixirSense.Core.Normalized.Macro.Env, as: NormalizedMacroEnv + alias ElixirSense.Core.State.ModFunInfo + + @env :elixir_env.new() + def env, do: @env + + def expand(ast, state, env) do + try do + state = + case ast do + {_, meta, _} when is_list(meta) -> + state + |> State.add_current_env_to_line(meta, env) + |> State.update_closest_env(meta, env) + + # state + _ -> + state + end + + do_expand(ast, state, env) + catch + kind, payload -> + Logger.warning( + "Unable to expand ast node #{inspect(ast)}: #{Exception.format(kind, payload, __STACKTRACE__)}" + ) + + {ast, state, env} + end + end + + # =/2 + + defp do_expand({:=, meta, [left, right]}, s, e) do + # elixir validates we are not in guard context + {e_right, sr, er} = expand(right, s, e) + {e_left, sl, el} = __MODULE__.Clauses.match(&expand/3, left, sr, s, er) + + e_expr = {:=, meta, [e_left, e_right]} + + vars_with_inferred_types = TypeInference.find_typed_vars(e_expr, nil, el.context) + + sl = State.merge_inferred_types(sl, vars_with_inferred_types) + + {e_expr, sl, el} + end + + # Literal operators + + defp do_expand({:{}, meta, args}, state, env) do + {args, state, env} = expand_args(args, state, env) + {{:{}, meta, args}, state, env} + end + + defp do_expand({:%{}, meta, args}, state, env) do + __MODULE__.Map.expand_map(meta, args, state, env) + end + + defp do_expand({:%, meta, [left, right]}, state, env) do + __MODULE__.Map.expand_struct(meta, left, right, state, env) + end + + defp do_expand({:<<>>, meta, args}, state, env) do + __MODULE__.Bitstring.expand(meta, args, state, env, false) + end + + defp do_expand({:->, meta, [left, right]}, s, e) do + # elixir raises here unhandled_arrow_op + expand({:"__->__", meta, [left, right]}, s, e) + end + + defp do_expand({:"::", meta, [left, right]}, s, e) do + # elixir raises here unhandled_type_op + expand({:"__::__", meta, [left, right]}, s, e) + end + + defp do_expand({:|, meta, [left, right]}, s, e) do + # elixir raises here unhandled_cons_op + expand({:"__|__", meta, [left, right]}, s, e) + end + + defp do_expand({:"\\\\", meta, [left, right]}, s, e) do + # elixir doesn't match on naked default args operator + expand({:"__\\\\__", meta, [left, right]}, s, e) + end + + # __block__ + + defp do_expand({:__block__, _meta, []}, s, e), do: {nil, s, e} + + defp do_expand({:__block__, _meta, [arg]}, s, e) do + expand(arg, s, e) + end + + defp do_expand({:__block__, meta, args}, s, e) when is_list(args) do + {e_args, sa, ea} = expand_block(args, [], meta, s, e) + {{:__block__, meta, e_args}, sa, ea} + end + + # __aliases__ + + defp do_expand({:__aliases__, meta, [head | tail] = list}, state, env) do + case NormalizedMacroEnv.expand_alias(env, meta, list, trace: false) do + {:alias, alias} -> + # TODO track alias + {alias, state, env} + + :error -> + {head, state, env} = expand(head, state, env) + + if is_atom(head) do + # TODO track alias + {Module.concat([head | tail]), state, env} + else + # elixir raises here invalid_alias + {{:__aliases__, meta, [head | tail]}, state, env} + end + end + end + + # require, alias, import + + defp do_expand({form, meta, [{{:., _, [base, :{}]}, _, refs} | rest]}, state, env) + when form in [:require, :alias, :import] do + case rest do + [] -> + expand_multi_alias_call(form, meta, base, refs, [], state, env) + + [opts] -> + # elixir raises if there is :as in opts, we omit it + opts = Keyword.delete(opts, :as) + + expand_multi_alias_call(form, meta, base, refs, opts, state, env) + end + end + + defp do_expand({form, meta, [arg]}, state, env) when form in [:require, :alias, :import] do + expand({form, meta, [arg, []]}, state, env) + end + + defp do_expand({:alias, meta, [arg, opts]}, state, env) do + state = + state + |> State.add_first_alias_positions(env, meta) + |> State.add_current_env_to_line(meta, env) + + # no need to call expand_without_aliases_report - we never report + {arg, state, env} = expand(arg, state, env) + {opts, state, env} = expand_opts([:as, :warn], no_alias_opts(opts), state, env) + + if is_atom(arg) do + case NormalizedMacroEnv.define_alias(env, meta, arg, [trace: false] ++ opts) do + {:ok, env} -> + {arg, state, env} + + {:error, _} -> + # elixir_aliases + {arg, state, env} + end + else + # expected_compile_time_module + {arg, state, env} + end + end + + defp do_expand({:require, meta, [arg, opts]}, state, env) do + state = + state + |> State.add_current_env_to_line(meta, env) + + # no need to call expand_without_aliases_report - we never report + {arg, state, env} = expand(arg, state, env) + + {opts, state, env} = + expand_opts([:as, :warn], no_alias_opts(opts), state, env) + + # elixir handles special meta key :defined in the require call. + # It is only set by defmodule and we handle it there + + if is_atom(arg) do + # elixir calls here :elixir_aliases.ensure_loaded(meta, e_ref, et) + # and optionally waits until required module is compiled + case NormalizedMacroEnv.define_require(env, meta, arg, [trace: false] ++ opts) do + {:ok, env} -> + {arg, state, env} + + {:error, _} -> + # elixir_aliases + {arg, state, env} + end + else + # expected_compile_time_module + {arg, state, env} + end + end + + defp do_expand({:import, meta, [arg, opts]}, state, env) do + state = + state + |> State.add_current_env_to_line(meta, env) + + # no need to call expand_without_aliases_report - we never report + {arg, state, env} = expand(arg, state, env) + {opts, state, env} = expand_opts([:only, :except, :warn], opts, state, env) + + if is_atom(arg) do + opts = + opts + |> Keyword.merge( + trace: false, + emit_warnings: false, + info_callback: import_info_callback(arg, state) + ) + + case NormalizedMacroEnv.define_import(env, meta, arg, opts) do + {:ok, env} -> + {arg, state, env} + + _ -> + {arg, state, env} + end + else + # expected_compile_time_module + {arg, state, env} + end + end + + # Compilation environment macros + + defp do_expand({:__MODULE__, meta, ctx}, state, env) when is_atom(ctx) do + state = State.add_current_env_to_line(state, meta, env) + + {env.module, state, env} + end + + defp do_expand({:__DIR__, meta, ctx}, state, env) when is_atom(ctx) do + state = State.add_current_env_to_line(state, meta, env) + + {Path.dirname(env.file), state, env} + end + + defp do_expand({:__CALLER__, meta, ctx} = caller, state, env) when is_atom(ctx) do + # elixir checks if context is not match and if caller is allowed + state = State.add_current_env_to_line(state, meta, env) + + {caller, state, env} + end + + defp do_expand({:__STACKTRACE__, meta, ctx} = stacktrace, state, env) when is_atom(ctx) do + # elixir checks if context is not match and if stacktrace is allowed + state = State.add_current_env_to_line(state, meta, env) + + {stacktrace, state, env} + end + + defp do_expand({:__ENV__, meta, ctx}, state, env) when is_atom(ctx) do + # elixir checks if context is not match + state = State.add_current_env_to_line(state, meta, env) + + {escape_map(escape_env_entries(meta, state, env)), state, env} + end + + defp do_expand({{:., dot_meta, [{:__ENV__, meta, atom}, field]}, call_meta, []}, s, e) + when is_atom(atom) and is_atom(field) do + # elixir checks if context is not match + s = State.add_current_env_to_line(s, call_meta, e) + + env = escape_env_entries(meta, s, e) + + case Map.fetch(env, field) do + {:ok, value} -> {value, s, e} + :error -> {{{:., dot_meta, [escape_map(env), field]}, call_meta, []}, s, e} + end + end + + # Quote + + defp do_expand({unquote_call, meta, [arg]}, s, e) + when unquote_call in [:unquote, :unquote_splicing] do + # elixir raises here unquote_outside_quote + # we may have cursor there + {arg, s, e} = expand(arg, s, e) + s = s |> State.add_current_env_to_line(meta, e) + {{unquote_call, meta, [arg]}, s, e} + end + + defp do_expand({:quote, meta, [opts]}, s, e) when is_list(opts) do + case Keyword.pop(opts, :do) do + {nil, _} -> + # elixir raises here missing_option + # generate a fake do block + expand({:quote, meta, [opts, [{:do, {:__block__, [], []}}]]}, s, e) + + {do_block, new_opts} -> + expand({:quote, meta, [new_opts, [{:do, do_block}]]}, s, e) + end + end + + defp do_expand({:quote, meta, [arg]}, s, e) do + # elixir raises here invalid_args + # we may have cursor there + {arg, s, e} = expand(arg, s, e) + s = s |> State.add_current_env_to_line(meta, e) + {{:quote, meta, [arg]}, s, e} + end + + defp do_expand({:quote, meta, [opts, do_block]}, s, e) when is_list(do_block) do + exprs = + case Keyword.fetch(do_block, :do) do + {:ok, expr} -> + expr + + :error -> + # elixir raises here missing_option + # try to recover from error by generating a fake do block + {:__block__, [], [do_block]} + end + + valid_opts = [:context, :location, :line, :file, :unquote, :bind_quoted, :generated] + {e_opts, st, et} = expand_opts(valid_opts, opts, s, e) + + context = Keyword.get(e_opts, :context, e.module || :"Elixir") + + {file, line} = + case Keyword.fetch(e_opts, :location) do + {:ok, :keep} -> {e.file, true} + :error -> {Keyword.get(e_opts, :file, nil), Keyword.get(e_opts, :line, false)} + end + + {binding, default_unquote} = + case Keyword.fetch(e_opts, :bind_quoted) do + {:ok, bq} -> + if is_list(bq) do + # safe to drop, opts already expanded + bq = Enum.filter(bq, &match?({key, _} when is_atom(key), &1)) + {bq, false} + else + {[], false} + end + + :error -> + {[], true} + end + + unquote_opt = Keyword.get(e_opts, :unquote, default_unquote) + generated = Keyword.get(e_opts, :generated, false) + + # alternative implementation + # res = expand_quote(exprs, st, et) + # res |> elem(0) |> IO.inspect + # res + {q, q_context, q_prelude} = + __MODULE__.Quote.build(meta, line, file, context, unquote_opt, generated, et) + + {e_prelude, sp, ep} = expand(q_prelude, st, et) + {e_context, sc, ec} = expand(q_context, sp, ep) + quoted = __MODULE__.Quote.quote(exprs, q) + {e_quoted, es, eq} = expand(quoted, sc, ec) + + es = es |> State.add_current_env_to_line(meta, eq) + + e_binding = + for {k, v} <- binding do + {:{}, [], [:=, [], [{:{}, [], [k, meta, e_context]}, v]]} + end + + e_binding_quoted = + case e_binding do + [] -> e_quoted + _ -> {:{}, [], [:__block__, [], e_binding ++ [e_quoted]]} + end + + case e_prelude do + [] -> {e_binding_quoted, es, eq} + _ -> {{:__block__, [], e_prelude ++ [e_binding_quoted]}, es, eq} + end + end + + defp do_expand({:quote, meta, [arg1, arg2]}, s, e) do + # elixir raises here invalid_args + # try to recover from error by wrapping arg in a do block + expand({:quote, meta, [arg1, [{:do, {:__block__, [], [arg2]}}]]}, s, e) + end + + # Functions + + defp do_expand({:&, meta, [{:super, super_meta, args} = expr]}, s, e) when is_list(args) do + case resolve_super(meta, length(args), s, e) do + {kind, name, _} when kind in [:def, :defp] -> + expand_fn_capture(meta, {name, super_meta, args}, s, e) + + _ -> + expand_fn_capture(meta, expr, s, e) + end + end + + defp do_expand( + {:&, meta, [{:/, arity_meta, [{:super, super_meta, context}, arity]} = expr]}, + s, + e + ) + when is_atom(context) and is_integer(arity) do + case resolve_super(meta, arity, s, e) do + {kind, name, _} when kind in [:def, :defp] -> + s = + s + |> State.add_call_to_line({nil, name, arity}, super_meta) + |> State.add_current_env_to_line(super_meta, e) + + {{:&, meta, [{:/, arity_meta, [{name, super_meta, context}, arity]}]}, s, e} + + _ -> + expand_fn_capture(meta, expr, s, e) + end + end + + defp do_expand({:&, meta, [arg]}, s, e) do + expand_fn_capture(meta, arg, s, e) + end + + defp do_expand({:fn, meta, pairs}, s, e) do + __MODULE__.Fn.expand(meta, pairs, s, e) + end + + # case/cond/try/receive + + defp do_expand({:cond, meta, [opts]}, s, e) do + # elixir raises underscore_in_cond if the last clause is _ + {e_clauses, sc, ec} = __MODULE__.Clauses.cond(opts, s, e) + {{:cond, meta, [e_clauses]}, sc, ec} + end + + defp do_expand({:case, meta, [expr, options]}, s, e) do + expand_case(meta, expr, options, s, e) + end + + defp do_expand({:receive, meta, [opts]}, s, e) do + {e_clauses, sc, ec} = __MODULE__.Clauses.receive(opts, s, e) + {{:receive, meta, [e_clauses]}, sc, ec} + end + + defp do_expand({:try, meta, [opts]}, s, e) do + {e_clauses, sc, ec} = __MODULE__.Clauses.try(opts, s, e) + {{:try, meta, [e_clauses]}, sc, ec} + end + + defp do_expand({:for, _, [_ | _]} = expr, s, e), do: expand_for(expr, s, e, true) + + defp do_expand({:with, meta, [_ | _] = args}, s, e) do + __MODULE__.Clauses.with(meta, args, s, e) + end + + # Cursor + + defp do_expand({:__cursor__, meta, args}, s, e) when is_list(args) do + s = + unless s.cursor_env do + s + |> State.add_cursor_env(meta, e) + else + s + end + + case args do + [h | _] -> + expand(h, s, e) + + [] -> + {nil, s, e} + end + end + + # Super + + defp do_expand({:super, meta, args}, s, e) when is_list(args) do + arity = length(args) + + case resolve_super(meta, arity, s, e) do + {kind, name, _} -> + {e_args, sa, ea} = expand_args(args, s, e) + + sa = + sa + |> State.add_call_to_line({nil, name, arity}, meta) + |> State.add_current_env_to_line(meta, ea) + + {{:super, [{:super, {kind, name}} | meta], e_args}, sa, ea} + + _ -> + # elixir does not allow this branch + expand_local(meta, :super, args, s, e) + end + end + + # Vars + + # Pin operator + # It only appears inside match and it disables the match behaviour. + + defp do_expand({:^, meta, [arg]}, %{prematch: {prematch, _, _}, vars: {_, write}} = s, e) do + no_match_s = %{s | prematch: :pin, vars: {prematch, write}} + + case expand(arg, no_match_s, %{e | context: nil}) do + {{name, _var_meta, kind} = var, %{unused: unused}, _} + when is_atom(name) and is_atom(kind) -> + s = State.add_var_read(s, var) + {{:^, meta, [var]}, %{s | unused: unused}, e} + + {arg, s, _e} -> + # elixir raises here invalid_arg_for_pin + # we may have cursor in arg + {{:^, meta, [arg]}, s, e} + end + end + + defp do_expand({:^, _meta, [arg]}, s, e) do + # elixir raises here pin_outside_of_match + # try to recover from error by dropping the pin and expanding arg + expand(arg, s, e) + end + + defp do_expand({:_, _meta, kind} = var, s, e) when is_atom(kind) do + # elixir raises unbound_underscore if context is not match + {var, s, e} + end + + defp do_expand({name, meta, kind}, s, %{context: :match} = e) + when is_atom(name) and is_atom(kind) do + %{ + prematch: {_, prematch_version, _}, + unused: version, + vars: {read, write} + } = s + + pair = {name, var_context(meta, kind)} + + case read do + # Variable was already overridden + %{^pair => var_version} when var_version >= prematch_version -> + var = {name, [{:version, var_version} | meta], kind} + # it's a write but for simplicity treat it as read + s = State.add_var_read(s, var) + {var, %{s | unused: version}, e} + + # Variable is being overridden now + %{^pair => _} -> + new_read = Map.put(read, pair, version) + new_write = if write != false, do: Map.put(write, pair, version), else: write + var = {name, [{:version, version} | meta], kind} + s = State.add_var_write(s, var) + {var, %{s | vars: {new_read, new_write}, unused: version + 1}, e} + + # Variable defined for the first time + _ -> + new_read = Map.put(read, pair, version) + new_write = if write != false, do: Map.put(write, pair, version), else: write + var = {name, [{:version, version} | meta], kind} + s = State.add_var_write(s, var) + {var, %{s | vars: {new_read, new_write}, unused: version + 1}, e} + end + end + + defp do_expand({name, meta, kind}, s, e) when is_atom(name) and is_atom(kind) do + %{vars: {read, _write}, prematch: prematch} = s + pair = {name, var_context(meta, kind)} + + result = + case read do + %{^pair => current_version} -> + case prematch do + {pre, _counter, {_bitsize, original}} -> + cond do + Map.get(pre, pair) != current_version -> + {:ok, current_version} + + Map.has_key?(pre, pair) -> + # elixir plans to remove this case on 2.0 + {:ok, current_version} + + not Map.has_key?(original, pair) -> + {:ok, current_version} + + true -> + :raise + end + + _ -> + {:ok, current_version} + end + + _ -> + prematch + end + + case result do + {:ok, pair_version} -> + var = {name, [{:version, pair_version} | meta], kind} + s = State.add_var_read(s, var) + {var, s, e} + + error -> + case Keyword.fetch(meta, :if_undefined) do + {:ok, :apply} -> + # convert to local call + expand({name, meta, []}, s, e) + + # elixir plans to remove this clause on v2.0 + {:ok, :raise} -> + # elixir raises here undefined_var + {{name, meta, kind}, s, e} + + # elixir plans to remove this clause on v2.0 + _ when error == :warn -> + # convert to local call and add if_undefined meta + expand({name, [{:if_undefined, :warn} | meta], []}, s, e) + + _ when error == :pin -> + # elixir raises here undefined_var_pin + {{name, meta, kind}, s, e} + + _ -> + # elixir raises here undefined_var and attaches span meta + {{name, meta, kind}, s, e} + end + end + end + + # Local calls + + defp do_expand({fun, meta, args}, state, env) + when is_atom(fun) and is_list(meta) and is_list(args) do + # elixir checks here id fall is not ambiguous + arity = length(args) + + # If we are inside a function, we support reading from locals. + allow_locals = match?({n, a} when fun != n or arity != a, env.function) + + case NormalizedMacroEnv.expand_import(env, meta, fun, arity, + trace: false, + allow_locals: allow_locals, + check_deprecations: false + ) do + {:macro, module, callback} -> + # NOTE there is a subtle difference - callback will call expander with state derived from env via + # :elixir_env.env_to_ex(env) possibly losing some details. Jose Valim is convinced this is not a problem + state = + state + |> State.add_call_to_line({module, fun, length(args)}, meta) + |> State.add_current_env_to_line(meta, env) + + expand_macro(meta, module, fun, args, callback, state, env) + + {:function, module, fun} -> + {ar, af} = + case __MODULE__.Rewrite.inline(module, fun, arity) do + {ar, an} -> + {ar, an} + + false -> + {module, fun} + end + + expand_remote(ar, meta, af, meta, args, state, State.prepare_write(state), env) + + {:error, :not_found} -> + expand_local(meta, fun, args, state, env) + + {:error, {:conflict, _module}} -> + # elixir raises here, expand args to look for cursor + {_, state, _e} = expand_args(args, state, env) + {{fun, meta, args}, state, env} + + {:error, {:ambiguous, _module}} -> + # elixir raises here, expand args to look for cursor + {_, state, _e} = expand_args(args, state, env) + {{fun, meta, args}, state, env} + end + end + + # Remote call + + defp do_expand({{:., dot_meta, [module, fun]}, meta, args}, state, env) + when (is_tuple(module) or is_atom(module)) and is_atom(fun) and is_list(meta) and + is_list(args) do + # dbg({module, fun, args}) + {module, state_l, env} = expand(module, State.prepare_write(state), env) + arity = length(args) + + if is_atom(module) do + case __MODULE__.Rewrite.inline(module, fun, arity) do + {ar, an} -> + expand_remote(ar, dot_meta, an, meta, args, state, state_l, env) + + false -> + case NormalizedMacroEnv.expand_require(env, meta, module, fun, arity, + trace: false, + check_deprecations: false + ) do + {:macro, module, callback} -> + # NOTE there is a subtle difference - callback will call expander with state derived from env via + # :elixir_env.env_to_ex(env) possibly losing some details. Jose Valim is convinced this is not a problem + state = + state + |> State.add_call_to_line({module, fun, length(args)}, meta) + |> State.add_current_env_to_line(meta, env) + + expand_macro(meta, module, fun, args, callback, state, env) + + :error -> + expand_remote(module, dot_meta, fun, meta, args, state, state_l, env) + end + end + else + expand_remote(module, dot_meta, fun, meta, args, state, state_l, env) + end + end + + # Anonymous calls + + defp do_expand({{:., dot_meta, [expr]}, meta, args}, s, e) when is_list(args) do + {[e_expr | e_args], sa, ea} = expand_args([expr | args], s, e) + + # elixir validates if e_expr is not atom and raises invalid_function_call + + # for remote calls we emit position of right side of . + # to make it consistent we shift dot position here + dot_meta = dot_meta |> Keyword.put(:column_correction, 1) + + sa = + sa + |> State.add_call_to_line({nil, e_expr, length(e_args)}, dot_meta) + |> State.add_current_env_to_line(meta, e) + + {{{:., dot_meta, [e_expr]}, meta, e_args}, sa, ea} + end + + # Invalid calls + + defp do_expand({other, meta, args}, s, e) when is_list(meta) and is_list(args) do + # elixir raises invalid_call, we may have cursor in other + {other_exp, s, e} = expand(other, s, e) + + if other_exp != other do + expand(other_exp, s, e) + else + {args, s, e} = expand_args(args, s, e) + {{other, meta, args}, s, e} + end + end + + # Literals + + defp do_expand({left, right}, state, env) do + {[e_left, e_right], state, env} = expand_args([left, right], state, env) + {{e_left, e_right}, state, env} + end + + defp do_expand(list, s, %{context: :match} = e) when is_list(list) do + expand_list(list, &expand/3, s, e, []) + end + + defp do_expand(list, s, e) when is_list(list) do + {e_args, {se, _}, ee} = + expand_list(list, &expand_arg/3, {State.prepare_write(s), s}, e, []) + + {e_args, State.close_write(se, s), ee} + end + + defp do_expand(function, s, e) when is_function(function) do + type_info = :erlang.fun_info(function, :type) + env_info = :erlang.fun_info(function, :env) + + case {type_info, env_info} do + {{:type, :external}, {:env, []}} -> + {__MODULE__.Quote.fun_to_quoted(function), s, e} + + _other -> + # elixir raises here invalid_quoted_expr + {nil, s, e} + end + end + + defp do_expand(pid, s, e) when is_pid(pid) do + case e.function do + nil -> + {pid, s, e} + + _function -> + # elixir plans to error here invalid_pid_in_function on 2.0 + {pid, s, e} + end + end + + defp do_expand(other, s, e) when is_number(other) or is_atom(other) or is_binary(other) do + {other, s, e} + end + + defp do_expand(_other, s, e) do + # elixir raises here invalid_quoted_expr + {nil, s, e} + end + + # Macro handling + + defp expand_macro( + meta, + Kernel, + :defdelegate, + [funs, opts], + _callback, + state, + env = %{module: module} + ) + when module != nil do + {opts, state, env} = expand(opts, state, env) + # elixir does validation here + target = Keyword.get(opts, :to, :__unknown__) + + # TODO Remove List.wrap when multiple funs are no longer supported by elixir + state = + funs + |> List.wrap() + |> Enum.reduce(state, fn fun, state -> + state_orig = state + + {fun, state, has_unquotes} = + if __MODULE__.Quote.has_unquotes(fun) do + state = State.new_vars_scope(state) + # dynamic defdelegate - replace unquote expression with fake call + case fun do + {{:unquote, _, unquote_args}, meta, args} -> + {_, state, _} = expand(unquote_args, state, env) + {{:__unknown__, meta, args}, state, true} + + _ -> + {fun, state, true} + end + else + state = State.new_func_vars_scope(state) + {fun, state, false} + end + + {name, args, as, as_args} = __MODULE__.Utils.defdelegate_each(fun, opts) + arity = length(args) + + # no need to reset versioned_vars - we never update it + env_for_expand = %{env | function: {name, arity}} + + # expand defaults and pass args without defaults to expand_args + {args_no_defaults, args, state} = + expand_defaults(args, state, %{env_for_expand | context: nil}, [], []) + + # based on :elixir_clauses.def + {e_args_no_defaults, state, _env_for_expand} = + expand_args(args_no_defaults, %{state | prematch: {%{}, 0, :none}}, %{ + env_for_expand + | context: :match + }) + + args = + Enum.zip(args, e_args_no_defaults) + |> Enum.map(fn + {{:"\\\\", meta, [_, expanded_default]}, expanded_arg} -> + {:"\\\\", meta, [expanded_arg, expanded_default]} + + {_, expanded_arg} -> + expanded_arg + end) + + state = + unless has_unquotes do + # restore module vars + State.remove_func_vars_scope(state, state_orig) + else + # remove scope + State.remove_vars_scope(state, state_orig) + end + + state + |> State.add_current_env_to_line(meta, %{env | context: nil, function: {name, arity}}) + |> State.add_func_to_index( + env, + name, + args, + State.extract_range(meta), + :defdelegate, + target: {target, as, length(as_args)} + ) + end) + + {[], state, env} + end + + defp expand_macro( + meta, + Kernel, + :@, + [{:__cursor__, _meta, list} = arg], + _callback, + state, + env + ) + when is_list(list) do + {arg, state, _env} = expand(arg, state, env) + {{:@, meta, [arg]}, state, env} + end + + defp expand_macro( + meta, + Kernel, + :@, + [{:behaviour, _meta, [arg]}], + _callback, + state, + env = %{module: module} + ) + when module != nil do + state = + state + |> State.add_current_env_to_line(meta, env) + + {arg, state, env} = expand(arg, state, env) + State.add_behaviour(arg, state, env) + end + + defp expand_macro( + meta, + Kernel, + :@, + [{:moduledoc, doc_meta, [arg]}], + _callback, + state, + env = %{module: module} + ) + when module != nil do + state = + state + |> State.add_current_env_to_line(meta, env) + + {arg, state, env} = expand(arg, state, env) + + state = + state + |> State.add_moduledoc_positions( + env, + meta + ) + |> State.register_doc(env, :moduledoc, arg) + + {{:@, meta, [{:moduledoc, doc_meta, [arg]}]}, state, env} + end + + defp expand_macro( + meta, + Kernel, + :@, + [{doc, doc_meta, [arg]}], + _callback, + state, + env = %{module: module} + ) + when doc in [:doc, :typedoc] and module != nil do + state = + state + |> State.add_current_env_to_line(meta, env) + + {arg, state, env} = expand(arg, state, env) + + state = + state + |> State.register_doc(env, doc, arg) + + {{:@, meta, [{doc, doc_meta, [arg]}]}, state, env} + end + + defp expand_macro( + meta, + Kernel, + :@, + [{:impl, doc_meta, [arg]}], + _callback, + state, + env = %{module: module} + ) + when module != nil do + state = + state + |> State.add_current_env_to_line(meta, env) + + {arg, state, env} = expand(arg, state, env) + + # impl adds sets :hidden by default + state = + state + |> State.register_doc(env, :doc, :impl) + + {{:@, meta, [{:impl, doc_meta, [arg]}]}, state, env} + end + + defp expand_macro( + meta, + Kernel, + :@, + [{:optional_callbacks, doc_meta, [arg]}], + _callback, + state, + env = %{module: module} + ) + when module != nil do + state = + state + |> State.add_current_env_to_line(meta, env) + + {arg, state, env} = expand(arg, state, env) + + state = + state + |> State.register_optional_callbacks(arg) + + {{:@, meta, [{:optional_callbacks, doc_meta, [arg]}]}, state, env} + end + + defp expand_macro( + meta, + Kernel, + :@, + [{:deprecated, doc_meta, [arg]}], + _callback, + state, + env = %{module: module} + ) + when module != nil do + state = + state + |> State.add_current_env_to_line(meta, env) + + {arg, state, env} = expand(arg, state, env) + + state = + state + |> State.register_doc(env, :doc, deprecated: arg) + + {{:@, meta, [{:deprecated, doc_meta, [arg]}]}, state, env} + end + + defp expand_macro( + meta, + Kernel, + :@, + [{:derive, doc_meta, [derived_protos]}], + _callback, + state, + env = %{module: module} + ) + when module != nil do + state = + List.wrap(derived_protos) + |> Enum.map(fn + {proto, _opts} -> proto + proto -> proto + end) + |> Enum.reduce(state, fn proto, acc -> + case expand(proto, acc, env) do + {proto_module, acc, _env} when is_atom(proto_module) -> + # protocol implementation module for Any + mod_any = Module.concat(proto_module, Any) + + # protocol implementation module built by @derive + mod = Module.concat(proto_module, module) + + case acc.mods_funs_to_positions[{mod_any, nil, nil}] do + nil -> + # implementation for: Any not detected (is in other file etc.) + acc + |> State.add_module_to_index(mod, State.extract_range(meta), generated: true) + + _any_mods_funs -> + # copy implementation for: Any + copied_mods_funs_to_positions = + for {{module, fun, arity}, val} <- acc.mods_funs_to_positions, + module == mod_any, + into: %{}, + do: {{mod, fun, arity}, val} + + %{ + acc + | mods_funs_to_positions: + acc.mods_funs_to_positions |> Map.merge(copied_mods_funs_to_positions) + } + end + + _other -> + acc + end + end) + + {{:@, meta, [{:derive, doc_meta, [derived_protos]}]}, state, env} + end + + defp expand_macro( + attr_meta, + Kernel, + :@, + [{kind, kind_meta, [expr | _]}], + _callback, + state, + env = %{module: module} + ) + when kind in [:type, :typep, :opaque] and module != nil do + cursor_before? = state.cursor_env != nil + {expr, state, env} = __MODULE__.Typespec.expand_type(expr, state, env) + + {name, type_args} = __MODULE__.Typespec.type_to_signature(expr) + type_args = type_args || [] + + name = + cond do + name in [:required, :optional] -> + # elixir raises here type #{name}/#{1} is a reserved type and it cannot be defined + :"__#{name}__" + + __MODULE__.Typespec.built_in_type?(name, length(type_args)) -> + # elixir raises here type #{name}/#{length(type_args)} is a built-in type and it cannot be redefined + :"__#{name}__" + + true -> + name + end + + cursor_after? = state.cursor_env != nil + + spec = TypeInfo.typespec_to_string(kind, expr) + + state = + state + |> State.add_type(env, name, type_args, spec, kind, State.extract_range(attr_meta)) + |> State.with_typespec({name, length(type_args)}) + |> State.add_current_env_to_line(attr_meta, env) + |> State.with_typespec(nil) + + state = + if not cursor_before? and cursor_after? do + {meta, env} = state.cursor_env + env = %{env | typespec: {name, length(type_args)}} + %{state | cursor_env: {meta, env}} + else + state + end + + {{:@, attr_meta, [{kind, kind_meta, [expr]}]}, state, env} + end + + defp expand_macro( + attr_meta, + Kernel, + :@, + [{kind, kind_meta, [expr | _]}], + _callback, + state, + env = %{module: module} + ) + when kind in [:callback, :macrocallback, :spec] and module != nil do + cursor_before? = state.cursor_env != nil + {expr, state, env} = __MODULE__.Typespec.expand_spec(expr, state, env) + + {name, type_args} = __MODULE__.Typespec.spec_to_signature(expr) + cursor_after? = state.cursor_env != nil + spec = TypeInfo.typespec_to_string(kind, expr) + + range = State.extract_range(attr_meta) + + state = + if kind in [:callback, :macrocallback] do + state + |> State.add_func_to_index( + env, + :behaviour_info, + [{:atom, attr_meta, nil}], + range, + :def, + generated: true + ) + else + state + end + + type_args = type_args || [] + + state = + state + |> State.add_spec(env, name, type_args, spec, kind, range) + |> State.with_typespec({name, length(type_args)}) + |> State.add_current_env_to_line(attr_meta, env) + |> State.with_typespec(nil) + + state = + if not cursor_before? and cursor_after? do + {meta, env} = state.cursor_env + env = %{env | typespec: {name, length(type_args)}} + %{state | cursor_env: {meta, env}} + else + state + end + + {{:@, attr_meta, [{kind, kind_meta, [expr]}]}, state, env} + end + + defp expand_macro( + meta, + Kernel, + :@, + [{name, name_meta, args}], + _callback, + state, + env = %{module: module} + ) + when is_atom(name) and module != nil do + {is_definition, {e_args, state, env}} = + case args do + arg when is_atom(arg) -> + # @attribute + {false, {nil, state, env}} + + [] -> + # deprecated @attribute() + {false, {nil, state, env}} + + [_] -> + # @attribute(arg) + # elixir validates env.function is nil + # elixir forbids behavior name + {true, expand_args(args, state, env)} + + args -> + # elixir raises "invalid @ call #{inspect(args)}" + {e_args, state, env} = expand_args(args, state, env) + {true, {[hd(e_args)], state, env}} + end + + inferred_type = + case e_args do + nil -> nil + [arg] -> TypeInference.type_of(arg, env.context) + end + + state = + state + |> State.add_attribute(env, name, meta, e_args, inferred_type, is_definition) + |> State.add_current_env_to_line(meta, env) + + {{:@, meta, [{name, name_meta, e_args}]}, state, env} + end + + defp expand_macro( + meta, + Kernel, + :defoverridable, + [arg], + _callback, + state, + env = %{module: module} + ) + when module != nil do + {arg, state, env} = expand(arg, state, env) + + case arg do + keyword when is_list(keyword) -> + {nil, State.make_overridable(state, env, keyword, meta[:context]), env} + + behaviour_module when is_atom(behaviour_module) -> + if Code.ensure_loaded?(behaviour_module) and + function_exported?(behaviour_module, :behaviour_info, 1) do + keyword = + behaviour_module.behaviour_info(:callbacks) + |> Enum.map(&Introspection.drop_macro_prefix/1) + + {nil, State.make_overridable(state, env, keyword, meta[:context]), env} + else + {nil, state, env} + end + + _ -> + {nil, state, env} + end + end + + defp expand_macro( + meta, + Kernel, + type, + [fields], + _callback, + state, + env = %{module: module} + ) + when type in [:defstruct, :defexception] and module != nil do + if Map.has_key?(state.structs, module) do + raise ArgumentError, + "defstruct has already been called for " <> + "#{inspect(module)}, defstruct can only be called once per module" + end + + {fields, state, env} = expand(fields, state, env) + + fields = + case fields do + fs when is_list(fs) -> + fs + + _other -> + # elixir raises ArgumentError here + [] + end + + fields = + fields + |> Enum.filter(fn + field when is_atom(field) -> true + {field, _} when is_atom(field) -> true + _ -> false + end) + |> Enum.map(fn + field when is_atom(field) -> {field, nil} + {field, value} when is_atom(field) -> {field, value} + end) + + state = + state + |> State.add_struct_or_exception(env, type, fields, State.extract_range(meta)) + + {{type, meta, [fields]}, state, env} + end + + defp expand_macro( + meta, + Record, + call, + [_name, _fields] = args, + _callback, + state, + env = %{module: module} + ) + when call in [:defrecord, :defrecordp] and module != nil do + range = State.extract_range(meta) + {[name, _fields] = args, state, env} = expand(args, state, env) + + type = + case call do + :defrecord -> :defmacro + :defrecordp -> :defmacrop + end + + options = [generated: true] + + state = + state + |> State.add_func_to_index( + env, + name, + [{:\\, [], [{:args, [], nil}, []]}], + range, + type, + options + ) + |> State.add_func_to_index( + env, + name, + [{:record, [], nil}, {:args, [], nil}], + range, + type, + options + ) + |> State.add_current_env_to_line(meta, env) + + {{{:., meta, [Record, call]}, meta, args}, state, env} + end + + defp expand_macro( + meta, + Kernel, + :defprotocol, + [_alias, [do: _block]] = args, + callback, + state, + env + ) do + original_env = env + # expand the macro normally + {ast, state, env} = + expand_macro_callback!(meta, Kernel, :defprotocol, args, callback, state, env) + + [module] = env.context_modules -- original_env.context_modules + # add behaviour_info builtin + # generate callbacks as macro expansion currently fails + state = + state + |> State.add_func_to_index( + %{env | module: module}, + :behaviour_info, + [:atom], + State.extract_range(meta), + :def, + generated: true + ) + |> State.generate_protocol_callbacks(%{env | module: module}) + + {ast, state, env} + end + + defp expand_macro( + meta, + Kernel, + :defimpl, + [name, do_block], + callback, + state, + env + ) do + expand_macro( + meta, + Kernel, + :defimpl, + [name, [], do_block], + callback, + state, + env + ) + end + + defp expand_macro( + meta, + Kernel, + :defimpl, + [name, opts, do_block], + callback, + state, + env + ) do + opts = Keyword.merge(opts, do_block) + + {for, opts} = + Keyword.pop_lazy(opts, :for, fn -> + env.module || + raise ArgumentError, "defimpl/3 expects a :for option when declared outside a module" + end) + + for = + __MODULE__.Macro.expand_literals(for, %{ + env + | module: env.module || Elixir, + function: {:__impl__, 1} + }) + + {for, state} = + if is_atom(for) or (is_list(for) and Enum.all?(for, &is_atom/1)) do + {for, state} + else + {_, state, _} = expand(for, state, env) + {:"Elixir.__Unknown__", state} + end + + {protocol, state, _env} = expand(name, state, env) + + impl = fn protocol, for, block, state, env -> + name = Module.concat(protocol, for) + + expand_macro( + meta, + Kernel, + :defmodule, + [name, [do: block]], + callback, + state, + env + ) + end + + block = + case opts do + [] -> + # elixir raises here + nil + + [do: block] -> + block + + _ -> + raise ArgumentError, "unknown options given to defimpl, got: #{Macro.to_string(opts)}" + end + + for_wrapped = + for + |> List.wrap() + + {ast, state, env} = + for_wrapped + |> Enum.reduce({[], state, env}, fn for, {acc, state, env} -> + {ast, state, env} = + impl.(protocol, for, block, %{state | protocol: {protocol, for_wrapped}}, env) + + {[ast | acc], state, env} + end) + + {Enum.reverse(ast), %{state | protocol: nil}, env} + end + + defp expand_macro( + meta, + Kernel, + :defmodule, + [alias, [do: block]] = _args, + _callback, + state, + env + ) do + state_orig = state + original_env = env + + {expanded, _state, _env} = expand(alias, state, env) + + {full, env} = + if is_atom(expanded) do + alias_defmodule(alias, expanded, env) + else + # elixir raises here + {:"Elixir.__Unknown__", env} + end + + # elixir emits a special require directive with :defined key set in meta + # require expand does alias, updates context_modules and runtime_modules + # we do it here instead + + env = %{env | context_modules: [full | env.context_modules]} + + state = + case original_env do + %{function: nil} -> + state + + _ -> + %{state | runtime_modules: [full | state.runtime_modules]} + end + + range = State.extract_range(meta) + + module_functions = + case state.protocol do + nil -> [] + _ -> [{:__impl__, [:atom], :def}] + end + + state = + state + |> State.add_module_to_index(full, range, []) + |> State.add_module() + |> State.add_current_env_to_line(meta, %{env | module: full}) + |> State.add_module_functions(%{env | module: full}, module_functions, range) + |> State.new_vars_scope() + |> State.new_attributes_scope() + + {state, _env} = State.maybe_add_protocol_behaviour(state, %{env | module: full}) + + {_result, state, e_env} = expand(block, state, %{env | module: full}) + + # here we handle module callbacks. Only before_compile macro callbacks are expanded as they + # affect module body. Func before_compile callbacks are not executed. after_compile and after_verify + # are not executed as we do not preform a real compilation + {state, _e_env} = + for args <- Map.get(state.attribute_store, {full, :before_compile}, []) do + case args do + {module, fun} -> [module, fun] + module -> [module, :__before_compile__] + end + end + |> Enum.reduce({state, e_env}, fn target, {state, env} -> + # module vars are not accessible in module callbacks + env = %{env | versioned_vars: %{}, line: meta[:line]} + state_orig = state + state = State.new_func_vars_scope(state) + + # elixir dispatches callbacks by raw dispatch and eval_forms + # instead we expand a bock with require and possibly expand macros + # we do not attempt to exec function callbacks + ast = + {:__block__, [], + [ + {:require, [], [hd(target)]}, + {{:., [], target}, [], [env]} + ]} + + {_result, state, env} = expand(ast, state, env) + {State.remove_func_vars_scope(state, state_orig), env} + end) + + # restore vars from outer scope + # restore version counter + state = + state + |> State.apply_optional_callbacks(%{env | module: full}) + |> State.remove_vars_scope(state_orig, true) + |> State.remove_attributes_scope() + |> State.remove_module() + + # in elixir the result of defmodule expansion is + # require (a module atom) and :elixir_module.compile dot call in block + # we don't need that + + {{:__block__, [], []}, state, env} + end + + defp expand_macro( + meta, + Protocol, + :def, + [{name, _, _args = [_ | _]} = call], + callback, + state, + env + ) + when is_atom(name) do + # transform protocol def to def with empty body + {ast, state, env} = + expand_macro(meta, Kernel, :def, [call, nil], callback, state, env) + + {ast, state, env} + end + + defp expand_macro(meta, Kernel, def_kind, [call], callback, state, env) + when def_kind in [:def, :defp, :defmacro, :defmacrop, :defguard, :defguardp] do + # transform guard and function head to def with empty body + expand_macro(meta, Kernel, def_kind, [call, nil], callback, state, env) + end + + defp expand_macro( + meta, + Kernel, + def_kind, + [call, expr], + _callback, + state, + env = %{module: module} + ) + when module != nil and + def_kind in [:def, :defp, :defmacro, :defmacrop, :defguard, :defguardp] do + state = + case call do + {:__cursor__, _, list} when is_list(list) -> + {_, state, _} = expand(call, state, %{env | function: {:__unknown__, 0}}) + state + + _ -> + state + end + + state_orig = state + + unquoted_call = __MODULE__.Quote.has_unquotes(call) + unquoted_expr = __MODULE__.Quote.has_unquotes(expr) + has_unquotes = unquoted_call or unquoted_expr + + # if there are unquote fragments in either call or body elixir escapes both and evaluates + # if unquoted_expr or unquoted_call, do: __MODULE__.Quote.escape({call, expr}, :none, true) + # instead we try to expand the call and body ignoring the unquotes + # + + {name_and_args, guards} = __MODULE__.Utils.extract_guards(call) + + # elixir raises here if def is invalid, we try to continue with unknown + # especially, we return unknown for calls with unquote fragments + {{name, _meta_1, args}, state} = + case name_and_args do + {n, m, a} when is_atom(n) and is_atom(a) -> + {{n, m, []}, state} + + {n, m, a} when is_atom(n) and is_list(a) -> + {{n, m, a}, state} + + {{:unquote, _, unquote_args}, m, a} when is_atom(a) -> + {_, state, _} = expand(unquote_args, state, env) + {{:__unknown__, m, []}, state} + + {{:unquote, _, unquote_args}, m, a} when is_list(a) -> + {_, state, _} = expand(unquote_args, state, env) + {{:__unknown__, m, a}, state} + + {_n, m, a} when is_atom(a) -> + {{:__unknown__, m, []}, state} + + {_n, m, a} when is_list(a) -> + {{:__unknown__, m, a}, state} + + _ -> + {{:__unknown__, [], []}, state} + end + + arity = length(args) + + # based on :elixir_def.env_for_expansion + state = + unless has_unquotes do + # module vars are not accessible in def body + %{ + state + | caller: def_kind in [:defmacro, :defmacrop, :defguard, :defguardp] + } + |> State.new_func_vars_scope() + else + # make module variables accessible if there are unquote fragments in def body + %{ + state + | caller: def_kind in [:defmacro, :defmacrop, :defguard, :defguardp] + } + |> State.new_vars_scope() + end + + # no need to reset versioned_vars - we never update it + env_for_expand = %{env | function: {name, arity}} + + # expand defaults and pass args without defaults to expand_args + {args_no_defaults, args, state} = + expand_defaults(args, state, %{env_for_expand | context: nil}, [], []) + + # based on :elixir_clauses.def + {e_args_no_defaults, state, env_for_expand} = + expand_args(args_no_defaults, %{state | prematch: {%{}, 0, :none}}, %{ + env_for_expand + | context: :match + }) + + args = + Enum.zip(args, e_args_no_defaults) + |> Enum.map(fn + {{:"\\\\", meta, [_, expanded_default]}, expanded_arg} -> + {:"\\\\", meta, [expanded_arg, expanded_default]} + + {_, expanded_arg} -> + expanded_arg + end) + + prematch = + if Version.match?(System.version(), ">= 1.15.0-dev") do + Code.get_compiler_option(:on_undefined_variable) + else + :warn + end + + {e_guard, state, env_for_expand} = + __MODULE__.Clauses.guard( + guards, + %{state | prematch: prematch}, + %{env_for_expand | context: :guard} + ) + + type_info = Guard.type_information_from_guards(e_guard) + + state = State.merge_inferred_types(state, type_info) + + env_for_expand = %{env_for_expand | context: nil} + + state = + state + |> State.add_current_env_to_line(meta, env_for_expand) + |> State.add_func_to_index( + env, + name, + args, + State.extract_range(meta), + def_kind + ) + + expr = + case expr do + nil -> + # function head + nil + + [do: do_block] -> + # do block only + do_block + + _ -> + if is_list(expr) and Keyword.has_key?(expr, :do) do + # do block with receive/catch/else/after + # wrap in try + # NOTE origin kind may be not correct here but origin is not used and + # elixir uses it only for error messages in elixir_clauses module + {:try, [{:origin, def_kind} | meta], [expr]} + else + # elixir raises here + expr + end + end + + {_e_body, state, _env_for_expand} = + expand(expr, state, env_for_expand) + + # restore vars from outer scope + state = + %{state | caller: false} + + state = + unless has_unquotes do + # restore module vars + State.remove_func_vars_scope(state, state_orig) + else + # remove scope + State.remove_vars_scope(state, state_orig) + end + + # result of def expansion is fa tuple + {{name, arity}, state, env} + end + + defp expand_macro( + meta, + ExUnit.Case, + :test, + [name | rest], + callback, + state, + env = %{module: module} + ) + when module != nil and is_binary(name) do + {args, do_block} = + case rest do + [] -> {[{:_, [], nil}], [do: {:__block__, [], []}]} + [do_block] -> {[{:_, [], nil}], do_block} + [context, do_block | _] -> {[context], do_block} + end + + call = {ex_unit_test_name(state, name), meta, args} + expand_macro(meta, Kernel, :def, [call, do_block], callback, state, env) + end + + defp expand_macro( + meta, + ExUnit.Callbacks, + setup, + rest, + callback, + state, + env = %{module: module} + ) + when module != nil and setup in [:setup, :setup_all] do + {args, do_block} = + case rest do + [] -> {[{:_, [], nil}], [do: {:__block__, [], []}]} + [do_block] -> {[{:_, [], nil}], do_block} + [context, do_block | _] -> {[context], do_block} + end + + line = __MODULE__.Utils.get_line(meta) + + # NOTE this name is not 100% correct - ex_unit uses counters instead of line but it's too complicated + call = {:"__ex_unit_#{setup}_#{line}", meta, args} + expand_macro(meta, Kernel, :def, [call, do_block], callback, state, env) + end + + defp expand_macro( + _meta, + ExUnit.Case, + :describe, + [name, [{:do, block}]], + _callback, + state, + env = %{module: module} + ) + when module != nil and is_binary(name) do + state = %{state | ex_unit_describe: name} + {ast, state, _env} = expand(block, state, env) + state = %{state | ex_unit_describe: nil} + {{:__block__, [], [ast]}, state, env} + end + + defp expand_macro(meta, module, fun, args, callback, state, env) do + expand_macro_callback(meta, module, fun, args, callback, state, env) + end + + defp expand_macro_callback(meta, module, fun, args, callback, state, env) do + # dbg({module, fun, args}) + try do + callback.(meta, args) + catch + # If expanding the macro fails, we just give up. + kind, payload -> + Logger.warning( + "Unable to expand macro #{inspect(module)}.#{fun}/#{length(args)}: #{Exception.format(kind, payload, __STACKTRACE__)}" + ) + + # look for cursor in args + {_ast, state, _env} = expand(args, state, env) + + {{{:., meta, [module, fun]}, meta, args}, state, env} + else + ast -> + state = + if __MODULE__.Utils.has_cursor?(args) and not __MODULE__.Utils.has_cursor?(ast) do + # in case there was cursor in the original args but it's not present in macro result + # expand a fake node + {_ast, state, _env} = expand({:__cursor__, [], []}, state, env) + state + else + state + end + + {ast, state, env} = expand(ast, state, env) + {ast, state, env} + end + end + + defp expand_macro_callback!(meta, _module, _fun, args, callback, state, env) do + ast = callback.(meta, args) + {ast, state, env} = expand(ast, state, env) + {ast, state, env} + end + + defp ex_unit_test_name(state, name) do + case state.ex_unit_describe do + nil -> "test #{name}" + describe -> "test #{describe} #{name}" + end + |> String.to_atom() + end + + defp expand_defaults([{:"\\\\", meta, [expr, default]} | args], s, e, acc_no_defaults, acc) do + {expanded_default, se, _} = expand(default, s, e) + + expand_defaults(args, se, e, [expr | acc_no_defaults], [ + {:"\\\\", meta, [expr, expanded_default]} | acc + ]) + end + + defp expand_defaults([arg | args], s, e, acc_no_defaults, acc), + do: expand_defaults(args, s, e, [arg | acc_no_defaults], [arg | acc]) + + defp expand_defaults([], s, _e, acc_no_defaults, acc), + do: {Enum.reverse(acc_no_defaults), Enum.reverse(acc), s} + + # defmodule helpers + # defmodule automatically defines aliases, we need to mirror this feature here. + + # defmodule Elixir.Alias + if Version.match?(System.version(), "< 1.16.0-dev") do + # see https://github.com/elixir-lang/elixir/pull/12451#issuecomment-1461393633 + defp alias_defmodule({:__aliases__, meta, [:"Elixir", t] = x}, module, env) do + alias = String.to_atom("Elixir." <> Atom.to_string(t)) + {:ok, env} = NormalizedMacroEnv.define_alias(env, meta, alias, as: alias, trace: false) + {module, env} + end + end + + defp alias_defmodule({:__aliases__, _, [:"Elixir", _ | _]}, module, env), do: {module, env} + + # defmodule Alias in root + defp alias_defmodule({:__aliases__, _, _}, module, %{module: nil} = env), + do: {module, env} + + # defmodule Alias nested + defp alias_defmodule({:__aliases__, meta, [h | t]}, _module, env) when is_atom(h) do + module = Module.concat([env.module, h]) + alias = String.to_atom("Elixir." <> Atom.to_string(h)) + {:ok, env} = NormalizedMacroEnv.define_alias(env, meta, module, as: alias, trace: false) + + case t do + [] -> {module, env} + _ -> {String.to_atom(Enum.join([module | t], ".")), env} + end + end + + # defmodule _ + defp alias_defmodule(_raw, module, env) do + {module, env} + end + + # Helpers + + defp expand_remote(receiver, dot_meta, right, meta, args, s, sl, %{context: context} = e) + when is_atom(receiver) or is_tuple(receiver) do + if context == :guard and is_tuple(receiver) do + # elixir raises parens_map_lookup unless no_parens is set in meta + # look for cursor in discarded args + {_ast, sl, _env} = expand(args, sl, e) + + sl = + sl + |> State.add_call_to_line({receiver, right, length(args)}, meta) + |> State.add_current_env_to_line(meta, e) + + {{{:., dot_meta, [receiver, right]}, meta, []}, sl, e} + else + attached_meta = attach_runtime_module(receiver, meta, s, e) + {e_args, {sa, _}, ea} = map_fold(&expand_arg/3, {sl, s}, e, args) + + case __MODULE__.Rewrite.rewrite( + context, + receiver, + dot_meta, + right, + attached_meta, + e_args, + s + ) do + {:ok, rewritten} -> + s = + State.close_write(sa, s) + |> State.add_call_to_line({receiver, right, length(e_args)}, meta) + |> State.add_current_env_to_line(meta, e) + + {rewritten, s, ea} + + {:error, _error} -> + # elixir raises here elixir_rewrite + s = + State.close_write(sa, s) + |> State.add_call_to_line({receiver, right, length(e_args)}, meta) + |> State.add_current_env_to_line(meta, e) + + {{{:., dot_meta, [receiver, right]}, attached_meta, e_args}, s, ea} + end + end + end + + defp expand_remote(receiver, dot_meta, right, meta, args, s, sl, e) do + # elixir raises here invalid_call + {e_args, {sa, _}, ea} = map_fold(&expand_arg/3, {sl, s}, e, args) + + s = + State.close_write(sa, s) + |> State.add_call_to_line({receiver, right, length(e_args)}, meta) + |> State.add_current_env_to_line(meta, e) + + {{{:., dot_meta, [receiver, right]}, meta, e_args}, s, ea} + end + + defp attach_runtime_module(receiver, meta, s, _e) do + if receiver in s.runtime_modules do + [{:runtime_module, true} | meta] + else + meta + end + end + + defp expand_local(meta, :when, [_, _] = args, state, env = %{context: nil}) do + # naked when, try to transform into a case + ast = + {:case, meta, + [ + {:_, meta, nil}, + [ + do: [ + {:->, meta, + [ + [ + {:when, meta, args} + ], + :ok + ]} + ] + ] + ]} + + expand(ast, state, env) + end + + defp expand_local(meta, fun, args, state, env) do + # elixir check if there are no clauses + # elixir raises here invalid_local_invocation if context is match or guard + # elixir compiler raises here undefined_function if env.function is nil + + state = + state + |> State.add_call_to_line({nil, fun, length(args)}, meta) + |> State.add_current_env_to_line(meta, env) + + {args, state, env} = expand_args(args, state, env) + {{fun, meta, args}, state, env} + end + + defp expand_opts(allowed, opts, s, e) do + {e_opts, se, ee} = expand(opts, s, e) + # safe to drop after expand + e_opts = sanitize_opts(allowed, e_opts) + {e_opts, se, ee} + end + + defp no_alias_opts(opts) when is_list(opts) do + case Keyword.fetch(opts, :as) do + {:ok, as} -> Keyword.put(opts, :as, no_alias_expansion(as)) + :error -> opts + end + end + + defp no_alias_opts(opts), do: opts + + defp no_alias_expansion({:__aliases__, _, [h | t]} = _aliases) when is_atom(h) do + Module.concat([h | t]) + end + + defp no_alias_expansion(other), do: other + + defp expand_list([{:|, meta, args} = _head], fun, s, e, list) do + {e_args, s_acc, e_acc} = map_fold(fun, s, e, args) + expand_list([], fun, s_acc, e_acc, [{:|, meta, e_args} | list]) + end + + defp expand_list([h | t], fun, s, e, list) do + {e_arg, s_acc, e_acc} = fun.(h, s, e) + expand_list(t, fun, s_acc, e_acc, [e_arg | list]) + end + + defp expand_list([], _fun, s, e, list) do + {Enum.reverse(list), s, e} + end + + defp expand_block([], acc, _meta, s, e), do: {Enum.reverse(acc), s, e} + + defp expand_block([h], acc, meta, s, e) do + # s = s |> State.add_current_env_to_line(meta, e) + {eh, se, ee} = expand(h, s, e) + expand_block([], [eh | acc], meta, se, ee) + end + + defp expand_block([{:for, _, [_ | _]} = h | t], acc, meta, s, e) do + {eh, se, ee} = expand_for(h, s, e, false) + {eh, se, ee} = expand_block(t, [eh | acc], meta, se, ee) + {eh, se, ee} + end + + defp expand_block([{:=, _, [{:_, _, ctx}, {:for, _, [_ | _]} = h]} | t], acc, meta, s, e) + when is_atom(ctx) do + {eh, se, ee} = expand_for(h, s, e, false) + expand_block(t, [eh | acc], meta, se, ee) + end + + defp expand_block([h | t], acc, meta, s, e) do + # s = s |> State.add_current_env_to_line(meta, e) + {eh, se, ee} = expand(h, s, e) + expand_block(t, [eh | acc], meta, se, ee) + end + + # defp expand_quote(ast, state, env) do + # {_, {state, env}} = + # Macro.prewalk(ast, {state, env}, fn + # # We need to traverse inside unquotes + # {unquote, _, [expr]}, {state, env} when unquote in [:unquote, :unquote_splicing] -> + # {_expr, state, env} = expand(expr, state, env) + # {:ok, {state, env}} + + # # If we find a quote inside a quote, we stop traversing it + # {:quote, _, [_]}, acc -> + # {:ok, acc} + + # {:quote, _, [_, _]}, acc -> + # {:ok, acc} + + # # Otherwise we go on + # node, acc -> + # {node, acc} + # end) + + # {ast, state, env} + # end + + defp expand_multi_alias_call(kind, meta, base, refs, opts, state, env) do + {base_ref, state, env} = expand(base, state, env) + + fun = fn + {:__aliases__, _, ref}, state, env -> + expand({kind, meta, [Module.concat([base_ref | ref]), opts]}, state, env) + + ref, state, env when is_atom(ref) -> + expand({kind, meta, [Module.concat([base_ref, ref]), opts]}, state, env) + + other, s, e -> + # elixir raises here + # expected_compile_time_module + # we search for cursor + {_, s, _} = expand(other, s, e) + {other, s, e} + end + + map_fold(fun, state, env, refs) + end + + defp overridable_name(name, count) when is_integer(count), do: :"#{name} (overridable #{count})" + + defp resolve_super(_meta, _arity, _state, %{module: module, function: function}) + when module == nil or function == nil do + # elixir asserts scope is function + nil + end + + defp resolve_super(_meta, arity, state, %{module: module, function: function}) do + case function do + {name, ^arity} -> + state.mods_funs_to_positions + + case state.mods_funs_to_positions[{module, name, arity}] do + %ModFunInfo{overridable: {true, _}} = info -> + kind = + case info.type do + :defdelegate -> :def + :defguard -> :defmacro + :defguardp -> :defmacrop + other -> other + end + + hidden = Map.get(info.meta, :hidden, false) + # def meta is not used anyway so let's pass empty + meta = [] + # we hardcode count to 1 + count = 1 + + case hidden do + false -> + {kind, name, meta} + + true when kind in [:defmacro, :defmacrop] -> + {:defmacrop, overridable_name(name, count), meta} + + true -> + {:defp, overridable_name(name, count), meta} + end + + _ -> + # elixir raises here no_super + nil + end + + _ -> + # elixir raises here wrong_number_of_args_for_super + nil + end + end + + defp expand_fn_capture(meta, arg, s, e) do + case __MODULE__.Fn.capture(meta, arg, s, e) do + {{:remote, remote, fun, arity}, require_meta, dot_meta, se, ee} -> + attached_meta = attach_runtime_module(remote, require_meta, s, e) + + se = + se + |> State.add_call_to_line({remote, fun, arity}, attached_meta) + |> State.add_current_env_to_line(attached_meta, ee) + + {{:&, meta, [{:/, [], [{{:., dot_meta, [remote, fun]}, attached_meta, []}, arity]}]}, se, + ee} + + {{:local, fun, arity}, local_meta, _, se, ee} -> + # elixir raises undefined_local_capture if ee.function is nil + + se = + se + |> State.add_call_to_line({nil, fun, arity}, local_meta) + |> State.add_current_env_to_line(local_meta, ee) + + {{:&, meta, [{:/, [], [{fun, local_meta, nil}, arity]}]}, se, ee} + + {:expand, expr, se, ee} -> + expand(expr, se, ee) + end + end + + defp expand_for({:for, meta, [_ | _] = args}, s, e, return) do + {cases, block} = __MODULE__.Utils.split_opts(args) + + {expr, opts} = + case Keyword.pop(block, :do) do + {nil, do_opts} -> + # elixir raises missing_option here + {[], do_opts} + + {do_expr, do_opts} -> + {do_expr, do_opts} + end + + {e_opts, so, eo} = expand(opts, State.new_vars_scope(s), e) + {e_cases, sc, ec} = map_fold(&expand_for_generator/3, so, eo, cases) + # elixir raises here for_generator_start on invalid start generator + + # safe to drop after expand + e_opts = sanitize_opts([:into, :uniq, :reduce], e_opts) + + {maybe_reduce, normalized_opts} = + sanitize_for_options(e_opts, false, false, false, return, meta, e, []) + + {e_expr, se, _ee} = expand_for_do_block(expr, sc, ec, maybe_reduce) + + {{:for, meta, e_cases ++ [[{:do, e_expr} | normalized_opts]]}, State.remove_vars_scope(se, s), + e} + end + + defp expand_for_do_block([{:->, _, _} | _] = clauses, s, e, false) do + # elixir raises here for_without_reduce_bad_block + # try to recover from error by emitting fake reduce + expand_for_do_block(clauses, s, e, {:reduce, []}) + end + + defp expand_for_do_block(expr, s, e, false), do: expand(expr, s, e) + + defp expand_for_do_block([{:->, _, _} | _] = clauses, s, e, {:reduce, _}) do + transformer = fn + {:->, clause_meta, [args, right]}, sa -> + # elixir checks here that clause has exactly 1 arg by matching against {_, _, [[_], _]} + # we drop excessive or generate a fake arg + + {args, discarded_args} = + case args do + [] -> + {[{:_, [], e.module}], []} + + [{:when, meta, [head | rest]}] -> + [last | rest_reversed] = Enum.reverse(rest) + {[{:when, meta, [head, last]}], Enum.reverse(rest_reversed)} + + [head | rest] -> + {[head], rest} + end + + # check if there is cursor in dropped arg + {_ast, sa, _e} = expand(discarded_args, sa, e) + + clause = {:->, clause_meta, [args, right]} + s_reset = State.new_vars_scope(sa) + + # no point in doing type inference here, we are only certain of the initial value of the accumulator + {e_clause, s_acc, _e_acc} = + __MODULE__.Clauses.clause(&__MODULE__.Clauses.head/3, clause, s_reset, e) + + {e_clause, State.remove_vars_scope(s_acc, sa)} + end + + {do_expr, sa} = Enum.map_reduce(clauses, s, transformer) + {do_expr, sa, e} + end + + defp expand_for_do_block(expr, s, e, {:reduce, _} = reduce) do + # elixir raises here for_with_reduce_bad_block + case expr do + [] -> + # try to recover from error by emitting a fake clause + expand_for_do_block([{:->, [], [[{:_, [], e.module}], :ok]}], s, e, reduce) + + _ -> + # try to recover from error by wrapping the expression in clause + expand_for_do_block([{:->, [], [[expr], :ok]}], s, e, reduce) + end + end + + defp expand_for_generator({:<-, meta, [left, right]}, s, e) do + {e_right, sr, er} = expand(right, s, e) + sm = State.reset_read(sr, s) + {[e_left], sl, el} = __MODULE__.Clauses.head([left], sm, er) + + match_context_r = TypeInference.type_of(e_right, e.context) + + vars_l_with_inferred_types = + TypeInference.find_typed_vars(e_left, {:for_expression, match_context_r}, :match) + + sl = State.merge_inferred_types(sl, vars_l_with_inferred_types) + + {{:<-, meta, [e_left, e_right]}, sl, el} + end + + defp expand_for_generator({:<<>>, meta, args} = x, s, e) when is_list(args) do + case __MODULE__.Utils.split_last(args) do + {left_start, {:<-, op_meta, [left_end, right]}} -> + {e_right, sr, er} = expand(right, s, e) + sm = State.reset_read(sr, s) + + {e_left, sl, el} = + __MODULE__.Clauses.match( + fn barg, bs, be -> + __MODULE__.Bitstring.expand(meta, barg, bs, be, true) + end, + left_start ++ [left_end], + sm, + sm, + er + ) + + # no point in doing type inference here, we're only going to find integers and binaries + + {{:<<>>, meta, [{:<-, op_meta, [e_left, e_right]}]}, sl, el} + + _ -> + expand(x, s, e) + end + end + + defp expand_for_generator(x, s, e) do + {x, s, e} = expand(x, s, e) + {x, s, e} + end + + defp sanitize_for_options([{:into, _} = pair | opts], _into, uniq, reduce, return, meta, e, acc) do + sanitize_for_options(opts, pair, uniq, reduce, return, meta, e, [pair | acc]) + end + + defp sanitize_for_options( + [{:uniq, _} = pair | opts], + into, + _uniq, + reduce, + return, + meta, + e, + acc + ) do + # elixir checks if uniq value is boolean + # we do not care - there may be cursor in it + sanitize_for_options(opts, into, pair, reduce, return, meta, e, [pair | acc]) + end + + defp sanitize_for_options( + [{:reduce, _} = pair | opts], + into, + uniq, + _reduce, + return, + meta, + e, + acc + ) do + # elixir raises for_conflicting_reduce_into_uniq when reduce, uniq and true is enabled + sanitize_for_options(opts, into, uniq, pair, return, meta, e, [pair | acc]) + end + + defp sanitize_for_options([], false, uniq, false, true, meta, e, acc) do + pair = {:into, []} + sanitize_for_options([pair], pair, uniq, false, true, meta, e, acc) + end + + defp sanitize_for_options([], false, {:uniq, true}, false, false, meta, e, acc) do + # safe to drop here even if there's a cursor options are already expanded + acc_without_uniq = Keyword.delete(acc, :uniq) + sanitize_for_options([], false, false, false, false, meta, e, acc_without_uniq) + end + + defp sanitize_for_options([], _into, _uniq, reduce, _return, _meta, _e, acc) do + {reduce, Enum.reverse(acc)} + end + + defp sanitize_opts(allowed, opts) when is_list(opts) do + for {key, value} <- opts, Enum.member?(allowed, key), do: {key, value} + end + + defp sanitize_opts(_allowed, _opts), do: [] + + defp escape_env_entries(meta, %{vars: {read, _}}, env) do + env = + case env.function do + nil -> env + _ -> %{env | lexical_tracker: nil, tracers: []} + end + + %{env | versioned_vars: escape_map(read), line: __MODULE__.Utils.get_line(meta)} + end + + defp escape_map(map) do + {:%{}, [], Enum.sort(Map.to_list(map))} + end + + defp map_fold(fun, s, e, list), do: map_fold(fun, s, e, list, []) + + defp map_fold(fun, s, e, [h | t], acc) do + {rh, rs, re} = fun.(h, s, e) + map_fold(fun, rs, re, t, [rh | acc]) + end + + defp map_fold(_fun, s, e, [], acc), do: {Enum.reverse(acc), s, e} + + defp var_context(meta, kind) do + case Keyword.fetch(meta, :counter) do + {:ok, counter} -> counter + :error -> kind + end + end + + defp expand_case(meta, expr, opts, s, e) do + {e_expr, se, ee} = expand(expr, s, e) + + r_opts = + if Keyword.get(meta, :optimize_boolean, false) do + if :elixir_utils.returns_boolean(e_expr) do + rewrite_case_clauses(opts) + else + generated_case_clauses(opts) + end + else + opts + end + + {e_opts, so, eo} = __MODULE__.Clauses.case(e_expr, r_opts, se, ee) + {{:case, meta, [e_expr, e_opts]}, so, eo} + end + + def rewrite_case_clauses( + do: [ + {:->, false_meta, + [ + [{:when, _, [var, {{:., _, [Kernel, :in]}, _, [var, [false, nil]]}]}], + false_expr + ]}, + {:->, true_meta, + [ + [{:_, _, _}], + true_expr + ]} + ] + ) do + rewrite_case_clauses(false_meta, false_expr, true_meta, true_expr) + end + + def rewrite_case_clauses( + do: [ + {:->, false_meta, [[false], false_expr]}, + {:->, true_meta, [[true], true_expr]} | _ + ] + ) do + rewrite_case_clauses(false_meta, false_expr, true_meta, true_expr) + end + + def rewrite_case_clauses(other) do + generated_case_clauses(other) + end + + defp rewrite_case_clauses(false_meta, false_expr, true_meta, true_expr) do + [ + do: [ + {:->, __MODULE__.Utils.generated(false_meta), [[false], false_expr]}, + {:->, __MODULE__.Utils.generated(true_meta), [[true], true_expr]} + ] + ] + end + + defp generated_case_clauses(do: clauses) do + r_clauses = + for {:->, meta, args} <- clauses, do: {:->, __MODULE__.Utils.generated(meta), args} + + [do: r_clauses] + end + + def expand_arg(arg, acc, e) + when is_number(arg) or is_atom(arg) or is_binary(arg) or is_pid(arg) do + {arg, acc, e} + end + + def expand_arg(arg, {acc, s}, e) do + {e_arg, s_acc, e_acc} = expand(arg, State.reset_read(acc, s), e) + {e_arg, {s_acc, s}, e_acc} + end + + def expand_args([arg], s, e) do + {e_arg, se, ee} = expand(arg, s, e) + {[e_arg], se, ee} + end + + def expand_args(args, s, %{context: :match} = e) do + map_fold(&expand/3, s, e, args) + end + + def expand_args(args, s, e) do + {e_args, {sa, _}, ea} = map_fold(&expand_arg/3, {State.prepare_write(s), s}, e, args) + {e_args, State.close_write(sa, s), ea} + end + + if Version.match?(System.version(), ">= 1.15.0-dev") do + @internals [{:behaviour_info, 1}, {:module_info, 1}, {:module_info, 0}] + else + @internals [{:module_info, 1}, {:module_info, 0}] + end + + defp import_info_callback(module, state) do + fn kind -> + if Map.has_key?(state.mods_funs_to_positions, {module, nil, nil}) do + category = if kind == :functions, do: :function, else: :macro + + for {{^module, fun, arity}, info} when fun != nil <- state.mods_funs_to_positions, + {fun, arity} not in @internals, + ModFunInfo.get_category(info) == category, + not ModFunInfo.private?(info) do + {fun, arity} + end + else + # this branch is based on implementation in :elixir_import + if Code.ensure_loaded?(module) do + try do + module.__info__(kind) + rescue + UndefinedFunctionError -> + if kind == :functions do + module.module_info(:exports) -- @internals + else + [] + end + end + else + [] + end + end + end + end +end diff --git a/lib/elixir_sense/core/compiler/bitstring.ex b/lib/elixir_sense/core/compiler/bitstring.ex new file mode 100644 index 00000000..e63ac6f0 --- /dev/null +++ b/lib/elixir_sense/core/compiler/bitstring.ex @@ -0,0 +1,427 @@ +defmodule ElixirSense.Core.Compiler.Bitstring do + alias ElixirSense.Core.Compiler + alias ElixirSense.Core.Compiler.Utils + alias ElixirSense.Core.Compiler.State + + defp expand_match(expr, {s, original_s}, e) do + {e_expr, se, ee} = Compiler.expand(expr, s, e) + {e_expr, {se, original_s}, ee} + end + + def expand(meta, args, s, e, require_size) do + case Map.get(e, :context) do + :match -> + {e_args, alignment, {sa, _}, ea} = + expand(meta, &expand_match/3, args, [], {s, s}, e, 0, require_size) + + # elixir validates if there is no nested match + + {{:<<>>, [{:alignment, alignment} | meta], e_args}, sa, ea} + + _ -> + pair_s = {State.prepare_write(s), s} + + {e_args, alignment, {sa, _}, ea} = + expand(meta, &Compiler.expand_arg/3, args, [], pair_s, e, 0, require_size) + + {{:<<>>, [{:alignment, alignment} | meta], e_args}, State.close_write(sa, s), ea} + end + end + + def expand(_bitstr_meta, _fun, [], acc, s, e, alignment, _require_size) do + {Enum.reverse(acc), alignment, s, e} + end + + def expand( + bitstr_meta, + fun, + [{:"::", meta, [left, right]} | t], + acc, + s, + e, + alignment, + require_size + ) do + {e_left, {sl, original_s}, el} = expand_expr(meta, left, fun, s, e) + + match_or_require_size = require_size or is_match_size(t, el) + e_type = expr_type(e_left) + + expect_size = + case e_left do + _ when not match_or_require_size -> :optional + {:^, _, [{_, _, _}]} -> {:infer, e_left} + _ -> :required + end + + {e_right, e_alignment, ss, es} = + expand_specs(e_type, meta, right, sl, original_s, el, expect_size) + + e_acc = concat_or_prepend_bitstring(meta, e_left, e_right, acc) + + expand( + bitstr_meta, + fun, + t, + e_acc, + {ss, original_s}, + es, + alignment(alignment, e_alignment), + require_size + ) + end + + def expand(bitstr_meta, fun, [h | t], acc, s, e, alignment, require_size) do + meta = extract_meta(h, bitstr_meta) + {e_left, {ss, original_s}, es} = expand_expr(meta, h, fun, s, e) + + e_type = expr_type(e_left) + e_right = infer_spec(e_type, meta) + + inferred_meta = [{:inferred_bitstring_spec, true} | meta] + + e_acc = + concat_or_prepend_bitstring( + inferred_meta, + e_left, + e_right, + acc + ) + + expand(meta, fun, t, e_acc, {ss, original_s}, es, alignment, require_size) + end + + defp expand_expr( + _meta, + {{:., _, [mod, :to_string]}, _, [arg]} = ast, + fun, + s, + %{context: context} = e + ) + when context != nil and (mod == Kernel or mod == String.Chars) do + case fun.(arg, s, e) do + {ebin, se, ee} when is_binary(ebin) -> {ebin, se, ee} + _ -> fun.(ast, s, e) + end + end + + defp expand_expr(_meta, component, fun, s, e) do + case fun.(component, s, e) do + {e_component, s, e} when is_list(e_component) or is_atom(e_component) -> + # elixir raises here invalid_literal + # try to recover from error by replacing it with "" + {"", s, e} + + expanded -> + expanded + end + end + + defp expand_specs(expr_type, meta, info, s, original_s, e, expect_size) do + default = + %{size: :default, unit: :default, sign: :default, type: :default, endianness: :default} + + {specs, ss, es} = + expand_each_spec(meta, unpack_specs(info, []), default, s, original_s, e) + + merged_type = type(expr_type, specs.type) + + # elixir validates if unsized binary is not on the end + + size_and_unit = size_and_unit(expr_type, specs.size, specs.unit) + alignment = compute_alignment(merged_type, specs.size, specs.unit) + + maybe_inferred_size = + case {expect_size, merged_type, size_and_unit} do + {{:infer, pinned_var}, :binary, []} -> + [{:size, meta, [{{:., meta, [:erlang, :byte_size]}, meta, [pinned_var]}]}] + + {{:infer, pinned_var}, :bitstring, []} -> + [{:size, meta, [{{:., meta, [:erlang, :bit_size]}, meta, [pinned_var]}]}] + + _ -> + size_and_unit + end + + [h | t] = + build_spec( + specs.size, + specs.unit, + merged_type, + specs.endianness, + specs.sign, + maybe_inferred_size + ) + + {Enum.reduce(t, h, fn i, acc -> {:-, meta, [acc, i]} end), alignment, ss, es} + end + + defp type(:default, :default), do: :integer + defp type(expr_type, :default), do: expr_type + + defp type(:binary, type) when type in [:binary, :bitstring, :utf8, :utf16, :utf32], + do: type + + defp type(:bitstring, type) when type in [:binary, :bitstring], do: type + + defp type(:integer, type) when type in [:integer, :float, :utf8, :utf16, :utf32], + do: type + + defp type(:float, :float), do: :float + defp type(:default, type), do: type + + defp type(_other, _type) do + # elixir raises here bittype_mismatch + type(:default, :default) + end + + defp expand_each_spec(meta, [{:__cursor__, _, args} = h | t], map, s, original_s, e) + when is_list(args) do + {h, s, e} = Compiler.expand(h, s, e) + + args = + case h do + nil -> t + h -> [h | t] + end + + expand_each_spec(meta, args, map, s, original_s, e) + end + + defp expand_each_spec(meta, [{expr, meta_e, args} = h | t], map, s, original_s, e) + when is_atom(expr) do + case validate_spec(expr, args) do + {key, arg} -> + {value, se, ee} = expand_spec_arg(arg, s, original_s, e) + # elixir validates spec arg here + # elixir raises bittype_mismatch in some cases + expand_each_spec(meta, t, Map.put(map, key, value), se, original_s, ee) + + :none -> + ha = + if args == nil do + {expr, meta_e, []} + else + h + end + + # TODO how to check for cursor here? + case Compiler.Macro.expand(ha, Map.put(e, :line, Utils.get_line(meta))) do + ^ha -> + # elixir raises here undefined_bittype + # we omit the spec + expand_each_spec(meta, t, map, s, original_s, e) + + new_types -> + expand_each_spec(meta, unpack_specs(new_types, []) ++ t, map, s, original_s, e) + end + end + end + + defp expand_each_spec(meta, [_expr | tail], map, s, original_s, e) do + # elixir raises undefined_bittype + # we skip it + expand_each_spec(meta, tail, map, s, original_s, e) + end + + defp expand_each_spec(_meta, [], map, s, _original_s, e), do: {map, s, e} + + defp compute_alignment(_, size, unit) when is_integer(size) and is_integer(unit), + do: rem(size * unit, 8) + + defp compute_alignment(:default, size, unit), do: compute_alignment(:integer, size, unit) + defp compute_alignment(:integer, :default, unit), do: compute_alignment(:integer, 8, unit) + defp compute_alignment(:integer, size, :default), do: compute_alignment(:integer, size, 1) + defp compute_alignment(:bitstring, size, :default), do: compute_alignment(:bitstring, size, 1) + defp compute_alignment(:binary, size, :default), do: compute_alignment(:binary, size, 8) + defp compute_alignment(:binary, _, _), do: 0 + defp compute_alignment(:float, _, _), do: 0 + defp compute_alignment(:utf32, _, _), do: 0 + defp compute_alignment(:utf16, _, _), do: 0 + defp compute_alignment(:utf8, _, _), do: 0 + defp compute_alignment(_, _, _), do: :unknown + + defp alignment(left, right) when is_integer(left) and is_integer(right) do + rem(left + right, 8) + end + + defp alignment(_, _), do: :unknown + + defp extract_meta({_, meta, _}, _), do: meta + defp extract_meta(_, meta), do: meta + + defp infer_spec(:bitstring, meta), do: {:bitstring, meta, nil} + defp infer_spec(:binary, meta), do: {:binary, meta, nil} + defp infer_spec(:float, meta), do: {:float, meta, nil} + defp infer_spec(:integer, meta), do: {:integer, meta, nil} + defp infer_spec(:default, meta), do: {:integer, meta, nil} + + defp expr_type(integer) when is_integer(integer), do: :integer + defp expr_type(float) when is_float(float), do: :float + defp expr_type(binary) when is_binary(binary), do: :binary + defp expr_type({:<<>>, _, _}), do: :bitstring + defp expr_type(_), do: :default + + defp concat_or_prepend_bitstring(_meta, {:<<>>, _, []}, _e_right, acc), + do: acc + + defp concat_or_prepend_bitstring( + meta, + {:<<>>, parts_meta, parts} = e_left, + e_right, + acc + ) do + # elixir raises unsized_binary in some cases + + case e_right do + {:binary, _, nil} -> + alignment = Keyword.fetch!(parts_meta, :alignment) + + if is_integer(alignment) do + # elixir raises unaligned_binary if alignment != 0 + Enum.reverse(parts, acc) + else + [{:"::", meta, [e_left, e_right]} | acc] + end + + {:bitstring, _, nil} -> + Enum.reverse(parts, acc) + end + end + + defp concat_or_prepend_bitstring(meta, e_left, e_right, acc) do + [{:"::", meta, [e_left, e_right]} | acc] + end + + defp unpack_specs({:-, _, [h, t]}, acc), do: unpack_specs(h, unpack_specs(t, acc)) + + defp unpack_specs({:*, _, [{:_, _, atom}, unit]}, acc) when is_atom(atom), + do: [{:unit, [], [unit]} | acc] + + defp unpack_specs({:*, _, [size, unit]}, acc), + do: [{:size, [], [size]}, {:unit, [], [unit]} | acc] + + defp unpack_specs(size, acc) when is_integer(size), do: [{:size, [], [size]} | acc] + + defp unpack_specs({expr, meta, args}, acc) when is_atom(expr) do + list_args = + cond do + is_atom(args) -> nil + is_list(args) -> args + true -> args + end + + [{expr, meta, list_args} | acc] + end + + defp unpack_specs(other, acc), do: [other | acc] + + defp validate_spec(spec, []), do: validate_spec(spec, nil) + defp validate_spec(:big, nil), do: {:endianness, :big} + defp validate_spec(:little, nil), do: {:endianness, :little} + defp validate_spec(:native, nil), do: {:endianness, :native} + defp validate_spec(:size, [size]), do: {:size, size} + defp validate_spec(:unit, [unit]), do: {:unit, unit} + defp validate_spec(:integer, nil), do: {:type, :integer} + defp validate_spec(:float, nil), do: {:type, :float} + defp validate_spec(:binary, nil), do: {:type, :binary} + defp validate_spec(:bytes, nil), do: {:type, :binary} + defp validate_spec(:bitstring, nil), do: {:type, :bitstring} + defp validate_spec(:bits, nil), do: {:type, :bitstring} + defp validate_spec(:utf8, nil), do: {:type, :utf8} + defp validate_spec(:utf16, nil), do: {:type, :utf16} + defp validate_spec(:utf32, nil), do: {:type, :utf32} + defp validate_spec(:signed, nil), do: {:sign, :signed} + defp validate_spec(:unsigned, nil), do: {:sign, :unsigned} + defp validate_spec(_, _), do: :none + + defp expand_spec_arg(expr, s, _original_s, e) when is_atom(expr) or is_integer(expr) do + {expr, s, e} + end + + defp expand_spec_arg(expr, s, original_s, %{context: :match} = e) do + %{prematch: {pre_read, pre_counter, _} = old_pre} = s + %{vars: {original_read, _}} = original_s + new_pre = {pre_read, pre_counter, {:bitsize, original_read}} + + {e_expr, se, ee} = + Compiler.expand(expr, %{s | prematch: new_pre}, %{e | context: :guard}) + + {e_expr, %{se | prematch: old_pre}, %{ee | context: :match}} + end + + defp expand_spec_arg(expr, s, original_s, e) do + Compiler.expand(expr, State.reset_read(s, original_s), e) + end + + defp size_and_unit(type, size, unit) + when type in [:bitstring, :binary] and (size != :default or unit != :default) do + # elixir raises here bittype_literal_bitstring or bittype_literal_string + # we don't care + size_and_unit(type, :default, :default) + end + + defp size_and_unit(_expr_type, size, unit) do + add_arg(:unit, unit, add_arg(:size, size, [])) + end + + defp build_spec(_size, _unit, type, endianness, _sign, spec) + when type in [:utf8, :utf16, :utf32] do + # elixir raises bittype_signed if signed + # elixir raises bittype_utf if size specified + # we don't care + + add_spec(type, add_spec(endianness, spec)) + end + + defp build_spec(_size, _unit, type, _endianness, _sign, spec) + when type in [:binary, :bitstring] do + # elixir raises bittype_signed if signed + # elixir raises bittype_mismatch if bitstring unit != 1 or default + # we don't care + + add_spec(type, spec) + end + + defp build_spec(size, unit, type, endianness, sign, spec) + when type in [:integer, :float] do + number_size = number_size(size, unit) + + cond do + type == :float and is_integer(number_size) -> + if valid_float_size(number_size) do + add_spec(type, add_spec(endianness, add_spec(sign, spec))) + else + # elixir raises here bittype_float_size + # we fall back to 64 + build_spec(64, :default, type, endianness, sign, spec) + end + + size == :default and unit != :default -> + # elixir raises here bittype_unit + # we fall back to default + build_spec(size, :default, type, endianness, sign, spec) + + true -> + add_spec(type, add_spec(endianness, add_spec(sign, spec))) + end + end + + defp add_spec(:default, spec), do: spec + defp add_spec(key, spec), do: [{key, [], nil} | spec] + + defp number_size(size, :default) when is_integer(size), do: size + defp number_size(size, unit) when is_integer(size), do: size * unit + defp number_size(size, _), do: size + + defp valid_float_size(16), do: true + defp valid_float_size(32), do: true + defp valid_float_size(64), do: true + defp valid_float_size(_), do: false + + defp add_arg(_key, :default, spec), do: spec + defp add_arg(key, arg, spec), do: [{key, [], [arg]} | spec] + + defp is_match_size([_ | _], %{context: :match}), do: true + defp is_match_size(_, _), do: false +end diff --git a/lib/elixir_sense/core/compiler/clauses.ex b/lib/elixir_sense/core/compiler/clauses.ex new file mode 100644 index 00000000..1ef8eee4 --- /dev/null +++ b/lib/elixir_sense/core/compiler/clauses.ex @@ -0,0 +1,549 @@ +defmodule ElixirSense.Core.Compiler.Clauses do + alias ElixirSense.Core.Compiler + alias ElixirSense.Core.Compiler.Utils + alias ElixirSense.Core.Compiler.State + alias ElixirSense.Core.TypeInference + + def match(fun, expr, after_s, _before_s, %{context: :match} = e) do + fun.(expr, after_s, e) + end + + def match(fun, expr, after_s, before_s, e) do + %{vars: current, unused: unused} = after_s + %{vars: {read, _write}, prematch: prematch} = before_s + + call_s = %{ + before_s + | prematch: {read, unused, :none}, + unused: unused, + vars: current, + calls: after_s.calls, + lines_to_env: after_s.lines_to_env, + vars_info: after_s.vars_info, + cursor_env: after_s.cursor_env, + closest_env: after_s.closest_env + } + + call_e = Map.put(e, :context, :match) + {e_expr, %{vars: new_current, unused: new_unused} = s_expr, ee} = fun.(expr, call_s, call_e) + + end_s = %{ + after_s + | prematch: prematch, + unused: new_unused, + vars: new_current, + calls: s_expr.calls, + lines_to_env: s_expr.lines_to_env, + vars_info: s_expr.vars_info, + cursor_env: s_expr.cursor_env, + closest_env: s_expr.closest_env + } + + end_e = Map.put(ee, :context, Map.get(e, :context)) + {e_expr, end_s, end_e} + end + + def clause(fun, {:->, clause_meta, [_, _]} = clause, s, e) + when is_function(fun, 4) do + clause(fn x, sa, ea -> fun.(clause_meta, x, sa, ea) end, clause, s, e) + end + + def clause(fun, {:->, meta, [left, right]}, s, e) do + {e_left, sl, el} = fun.(left, s, e) + {e_right, sr, er} = Compiler.expand(right, sl, el) + {{:->, meta, [e_left, e_right]}, sr, er} + end + + def clause(fun, expr, s, e) do + # try to recover from error by wrapping the expression in clause + # elixir raises here bad_or_missing_clauses + clause(fun, {:->, [], [[expr], :ok]}, s, e) + end + + def head([{:when, meta, [_ | _] = all}], s, e) do + {args, guard} = Utils.split_last(all) + prematch = s.prematch + + {{e_args, e_guard}, sg, eg} = + match( + fn _ok, sm, em -> + {e_args, sa, ea} = Compiler.expand_args(args, sm, em) + + {e_guard, sg, eg} = + guard(guard, %{sa | prematch: prematch}, %{ea | context: :guard}) + + type_info = TypeInference.Guard.type_information_from_guards(e_guard) + + sg = State.merge_inferred_types(sg, type_info) + + {{e_args, e_guard}, sg, eg} + end, + :ok, + s, + s, + e + ) + + {[{:when, meta, e_args ++ [e_guard]}], sg, eg} + end + + def head(args, s, e) do + match(&Compiler.expand_args/3, args, s, s, e) + end + + def guard({:when, meta, [left, right]}, s, e) do + {e_left, sl, el} = guard(left, s, e) + {e_right, sr, er} = guard(right, sl, el) + {{:when, meta, [e_left, e_right]}, sr, er} + end + + def guard(guard, s, e) do + {e_guard, sg, eg} = Compiler.expand(guard, s, e) + {e_guard, sg, eg} + end + + # case + + @valid_case_opts [:do] + + def case(e_expr, [], s, e) do + # elixir raises here missing_option + # emit a fake do block + case(e_expr, [do: []], s, e) + end + + def case(_e_expr, opts, s, e) when not is_list(opts) do + # elixir raises here invalid_args + # there may be cursor + Compiler.expand(opts, s, e) + end + + def case(e_expr, opts, s, e) do + # expand invalid opts in case there's cursor + {_ast, s, _e} = Compiler.expand(opts |> Keyword.drop(@valid_case_opts), s, e) + + opts = sanitize_opts(opts, @valid_case_opts) + + match_context = TypeInference.type_of(e_expr, e.context) + + {case_clauses, sa} = + Enum.map_reduce(opts, s, fn x, sa -> + expand_case(x, match_context, sa, e) + end) + + {case_clauses, sa, e} + end + + defp expand_case({:do, _} = do_clause, match_context, s, e) do + expand_clauses( + fn c, s, e -> + case head(c, s, e) do + {[h | _] = c, s, e} -> + clause_vars_with_inferred_types = + TypeInference.find_typed_vars(h, match_context, :match) + + s = State.merge_inferred_types(s, clause_vars_with_inferred_types) + + {c, s, e} + + other -> + other + end + end, + do_clause, + s, + e + ) + end + + # cond + + @valid_cond_opts [:do] + + def cond([], s, e) do + # elixir raises here missing_option + # emit a fake do block + cond([do: []], s, e) + end + + def cond(opts, s, e) when not is_list(opts) do + # elixir raises here invalid_args + # there may be cursor + Compiler.expand(opts, s, e) + end + + def cond(opts, s, e) do + # expand invalid opts in case there's cursor + {_ast, s, _e} = Compiler.expand(opts |> Keyword.drop(@valid_cond_opts), s, e) + + opts = sanitize_opts(opts, @valid_cond_opts) + + {cond_clauses, sa} = + Enum.map_reduce(opts, s, fn x, sa -> + expand_cond(x, sa, e) + end) + + {cond_clauses, sa, e} + end + + defp expand_cond({:do, _} = do_clause, s, e) do + expand_clauses(&Compiler.expand_args/3, do_clause, s, e) + end + + # receive + + @valid_receive_opts [:do, :after] + + def receive([], s, e) do + # elixir raises here missing_option + # emit a fake do block + receive([do: []], s, e) + end + + def receive(opts, s, e) when not is_list(opts) do + # elixir raises here invalid_args + # there may be cursor + Compiler.expand(opts, s, e) + end + + def receive(opts, s, e) do + # expand invalid opts in case there's cursor + {_ast, s, _e} = Compiler.expand(opts |> Keyword.drop(@valid_receive_opts), s, e) + + opts = sanitize_opts(opts, @valid_receive_opts) + + {receive_clauses, sa} = + Enum.map_reduce(opts, s, fn x, sa -> + expand_receive(x, sa, e) + end) + + {receive_clauses, sa, e} + end + + defp expand_receive({:do, {:__block__, _, []}} = do_block, s, _e) do + {do_block, s} + end + + defp expand_receive({:do, _} = do_clause, s, e) do + # no point in doing type inference here, we have no idea what message we may get + expand_clauses(&head/3, do_clause, s, e) + end + + defp expand_receive({:after, [_ | _]} = after_clause, s, e) do + expand_clauses(&Compiler.expand_args/3, after_clause, s, e) + end + + defp expand_receive({:after, expr}, s, e) when not is_list(expr) do + # elixir raises here multiple_after_clauses_in_receive + case expr do + expr when not is_list(expr) -> + # try to recover from error by wrapping the expression in list + expand_receive({:after, [expr]}, s, e) + + [first | discarded] -> + # try to recover from error by taking first clause only + # expand other in case there's cursor + {_ast, s, _e} = Compiler.expand(discarded, s, e) + expand_receive({:after, [first]}, s, e) + + [] -> + # try to recover from error by inserting a fake clause + expand_receive({:after, [{:->, [], [[0], :ok]}]}, s, e) + end + end + + # with + + @valid_with_opts [:do, :else] + + def with(meta, args, s, e) do + {exprs, opts0} = Utils.split_opts(args) + + # expand invalid opts in case there's cursor + {_ast, s, _e} = Compiler.expand(opts0 |> Keyword.drop(@valid_with_opts), s, e) + + opts0 = sanitize_opts(opts0, @valid_with_opts) + s0 = State.new_vars_scope(s) + {e_exprs, {s1, e1}} = Enum.map_reduce(exprs, {s0, e}, &expand_with/2) + {e_do, opts1, s2} = expand_with_do(meta, opts0, s, s1, e1) + {e_opts, _opts2, s3} = expand_with_else(opts1, s2, e) + + {{:with, meta, e_exprs ++ [[{:do, e_do} | e_opts]]}, s3, e} + end + + defp expand_with({:<-, meta, [left, right]}, {s, e}) do + {e_right, sr, er} = Compiler.expand(right, s, e) + sm = State.reset_read(sr, s) + {[e_left], sl, el} = head([left], sm, er) + + match_context_r = TypeInference.type_of(e_right, e.context) + vars_l_with_inferred_types = TypeInference.find_typed_vars(e_left, match_context_r, :match) + + sl = State.merge_inferred_types(sl, vars_l_with_inferred_types) + + {{:<-, meta, [e_left, e_right]}, {sl, el}} + end + + defp expand_with(expr, {s, e}) do + {e_expr, se, ee} = Compiler.expand(expr, s, e) + {e_expr, {se, ee}} + end + + defp expand_with_do(_meta, opts, s, acc, e) do + {expr, rest_opts} = Keyword.pop(opts, :do) + # elixir raises here missing_option + # we return empty expression + expr = expr || [] + + {e_expr, s_acc, _e_acc} = Compiler.expand(expr, acc, e) + + {e_expr, rest_opts, State.remove_vars_scope(s_acc, s)} + end + + defp expand_with_else(opts, s, e) do + case Keyword.pop(opts, :else) do + {nil, _} -> + {[], opts, s} + + {expr, rest_opts} -> + pair = {:else, expr} + + # no point in doing type inference here, we have no idea what data we are matching against + {e_pair, se} = expand_clauses(&head/3, pair, s, e) + {[e_pair], rest_opts, se} + end + end + + # try + + @valid_try_opts [:do, :rescue, :catch, :else, :after] + + def try([], s, e) do + # elixir raises here missing_option + # emit a fake do block + try([do: []], s, e) + end + + def try(opts, s, e) when not is_list(opts) do + # elixir raises here invalid_args + # there may be cursor + Compiler.expand(opts, s, e) + end + + def try(opts, s, e) do + # expand invalid opts in case there's cursor + {_ast, s, _e} = Compiler.expand(opts |> Keyword.drop(@valid_try_opts), s, e) + + opts = sanitize_opts(opts, @valid_try_opts) + + {try_clauses, sa} = + Enum.map_reduce(opts, s, fn x, sa -> + expand_try(x, sa, e) + end) + + {try_clauses, sa, e} + end + + defp expand_try({:do, expr}, s, e) do + {e_expr, se, _ee} = Compiler.expand(expr, State.new_vars_scope(s), e) + {{:do, e_expr}, State.remove_vars_scope(se, s)} + end + + defp expand_try({:after, expr}, s, e) do + {e_expr, se, _ee} = Compiler.expand(expr, State.new_vars_scope(s), e) + {{:after, e_expr}, State.remove_vars_scope(se, s)} + end + + defp expand_try({:else, _} = else_clause, s, e) do + # TODO we could try to infer type from last try block expression + expand_clauses(&head/3, else_clause, s, e) + end + + defp expand_try({:catch, _} = catch_clause, s, e) do + expand_clauses_with_stacktrace(&expand_catch/4, catch_clause, s, e) + end + + defp expand_try({:rescue, _} = rescue_clause, s, e) do + expand_clauses_with_stacktrace(&expand_rescue/4, rescue_clause, s, e) + end + + defp expand_clauses_with_stacktrace(fun, clauses, s, e) do + old_stacktrace = s.stacktrace + ss = %{s | stacktrace: true} + {ret, se} = expand_clauses(fun, clauses, ss, e) + {ret, %{se | stacktrace: old_stacktrace}} + end + + defp expand_catch(meta, [{:when, when_meta, [a1, a2, a3, dh | dt]}], s, e) do + # elixir raises here wrong_number_of_args_for_clause + {_, s, _} = Compiler.expand([dh | dt], s, e) + expand_catch(meta, [{:when, when_meta, [a1, a2, a3]}], s, e) + end + + defp expand_catch(_meta, args = [_], s, e) do + # no point in doing type inference here, we have no idea what throw we caught + head(args, s, e) + end + + defp expand_catch(_meta, args = [_, _], s, e) do + # TODO is it worth to infer type of the first arg? :error | :exit | :throw | {:EXIT, pid()} + head(args, s, e) + end + + defp expand_catch(meta, [a1, a2 | d], s, e) do + # attempt to recover from error by taking 2 first args + # elixir raises here wrong_number_of_args_for_clause + {_, s, _} = Compiler.expand(d, s, e) + expand_catch(meta, [a1, a2], s, e) + end + + defp expand_rescue(_meta, [arg], s, e) do + # elixir is strict here and raises invalid_rescue_clause on invalid args + {e_arg, sa, ea} = expand_rescue(arg, s, e) + {[e_arg], sa, ea} + end + + defp expand_rescue(meta, [a1 | d], s, e) do + # try to recover from error by taking first argument only + # elixir raises here wrong_number_of_args_for_clause + {_, s, _} = Compiler.expand(d, s, e) + expand_rescue(meta, [a1], s, e) + end + + # rescue var + defp expand_rescue({name, _, atom} = var, s, e) when is_atom(name) and is_atom(atom) do + {e_left, sl, el} = match(&Compiler.expand/3, var, s, s, e) + + match_context = {:struct, [], {:atom, Exception}, nil} + + vars_with_inferred_types = TypeInference.find_typed_vars(e_left, match_context, :match) + sl = State.merge_inferred_types(sl, vars_with_inferred_types) + + {e_left, sl, el} + end + + # rescue Alias => _ in [Alias] + defp expand_rescue({:__aliases__, _, [_ | _]} = alias, s, e) do + expand_rescue({:in, [], [{:_, [], e.module}, alias]}, s, e) + end + + # rescue var in _ + defp expand_rescue( + {:in, _, [{name, _, var_context} = var, {:_, _, underscore_context}]}, + s, + e + ) + when is_atom(name) and is_atom(var_context) and is_atom(underscore_context) do + {e_left, sl, el} = match(&Compiler.expand/3, var, s, s, e) + + match_context = {:struct, [], {:atom, Exception}, nil} + + vars_with_inferred_types = TypeInference.find_typed_vars(e_left, match_context, :match) + sl = State.merge_inferred_types(sl, vars_with_inferred_types) + + {e_left, sl, el} + end + + # rescue var in (list() or atom()) + defp expand_rescue({:in, meta, [left, right]}, s, e) do + {e_left, sl, el} = match(&Compiler.expand/3, left, s, s, e) + {e_right, sr, er} = Compiler.expand(right, sl, el) + + case e_left do + {name, _, atom} when is_atom(name) and is_atom(atom) -> + normalized = normalize_rescue(e_right, e) + + match_context = + for exception <- normalized, reduce: nil do + nil -> {:struct, [], {:atom, exception}, nil} + other -> {:union, [other, {:struct, [], {:atom, exception}, nil}]} + end + + match_context = + if match_context == nil do + {:struct, [], {:atom, Exception}, nil} + else + match_context + end + + vars_with_inferred_types = TypeInference.find_typed_vars(e_left, match_context, :match) + sr = State.merge_inferred_types(sr, vars_with_inferred_types) + + {{:in, meta, [e_left, normalized]}, sr, er} + + _ -> + # elixir rejects this case, we normalize to underscore + {{:in, meta, [{:_, [], e.module}, normalize_rescue(e_right, e)]}, sr, er} + end + end + + # rescue expr() => rescue expanded_expr() + defp expand_rescue({_, meta, _} = arg, s, e) do + # TODO how to check for cursor here? + case Compiler.Macro.expand_once(arg, %{e | line: Utils.get_line(meta)}) do + ^arg -> + # elixir rejects this case + # try to recover from error by generating fake expression + expand_rescue({:in, meta, [arg, {:_, [], e.module}]}, s, e) + + new_arg -> + expand_rescue(new_arg, s, e) + end + end + + # rescue list() or atom() => _ in (list() or atom()) + defp expand_rescue(arg, s, e) do + expand_rescue({:in, [], [{:_, [], e.module}, arg]}, s, e) + end + + defp normalize_rescue(atom, _e) when is_atom(atom) do + [atom] + end + + defp normalize_rescue(other, e) do + # elixir is strict here, we reject invalid nodes + res = + if is_list(other) do + Enum.filter(other, &is_atom/1) + else + [] + end + + if res == [] do + [{:_, [], e.module}] + else + res + end + end + + defp expand_clauses(fun, {key, [_ | _] = clauses}, s, e) do + transformer = fn clause, sa -> + {e_clause, s_acc, _e_acc} = + clause(fun, clause, State.new_vars_scope(sa), e) + + {e_clause, State.remove_vars_scope(s_acc, sa)} + end + + {values, se} = Enum.map_reduce(clauses, s, transformer) + {{key, values}, se} + end + + defp expand_clauses(fun, {key, expr}, s, e) do + # try to recover from error by wrapping the expression in a clauses list + # elixir raises here bad_or_missing_clauses + expand_clauses(fun, {key, [expr]}, s, e) + end + + # helpers + + defp sanitize_opt(opts, opt) do + case Keyword.fetch(opts, opt) do + :error -> [] + {:ok, value} -> [{opt, value}] + end + end + + defp sanitize_opts(opts, allowed) do + Enum.flat_map(allowed, fn opt -> sanitize_opt(opts, opt) end) + end +end diff --git a/lib/elixir_sense/core/compiler/dispatch.ex b/lib/elixir_sense/core/compiler/dispatch.ex new file mode 100644 index 00000000..0a32a2b4 --- /dev/null +++ b/lib/elixir_sense/core/compiler/dispatch.ex @@ -0,0 +1,179 @@ +defmodule ElixirSense.Core.Compiler.Dispatch do + alias ElixirSense.Core.Compiler.Rewrite + alias ElixirSense.Core.State.ModFunInfo + import :ordsets, only: [is_element: 2] + + def find_import(meta, name, arity, e) do + tuple = {name, arity} + + case find_import_by_name_arity(meta, tuple, [], e) do + {:function, receiver} -> + # TODO trace call? + # TODO address when https://github.com/elixir-lang/elixir/issues/13878 is resolved + # ElixirEnv.trace({:imported_function, meta, receiver, name, arity}, e) + receiver + + {:macro, receiver} -> + # TODO trace call? + # ElixirEnv.trace({:imported_macro, meta, receiver, name, arity}, e) + receiver + + {:ambiguous, [head | _]} -> + # elixir raises here, we choose first one + # TODO trace call? + head + + _ -> + false + end + end + + def find_imports(meta, name, e) do + funs = e.functions + macs = e.macros + + acc0 = %{} + acc1 = find_imports_by_name(funs, acc0, name, meta, e) + acc2 = find_imports_by_name(macs, acc1, name, meta, e) + + imports = acc2 |> Map.to_list() |> Enum.sort() + # trace_import_quoted(imports, meta, name, e) + imports + end + + def import_function(meta, name, arity, s, e) do + tuple = {name, arity} + + case find_import_by_name_arity(meta, tuple, [], e) do + {:function, receiver} -> + remote_function(meta, receiver, name, arity, e) + + {:macro, _receiver} -> + false + + {:import, receiver} -> + require_function(meta, receiver, name, arity, s, e) + + {:ambiguous, [first | _]} -> + # elixir raises here, we return first matching + require_function(meta, first, name, arity, s, e) + + false -> + if Macro.special_form?(name, arity) do + false + else + function = e.function + + mfa = {e.module, name, arity} + + if function != nil and function != tuple and + Enum.any?(s.mods_funs_to_positions, fn {key, info} -> + key == mfa and ModFunInfo.get_category(info) == :macro + end) do + false + else + {:local, name, arity} + end + end + end + end + + def require_function(meta, receiver, name, arity, s, e) do + required = receiver in e.requires + + if is_macro(name, arity, receiver, required, s) do + false + else + remote_function(meta, receiver, name, arity, e) + end + end + + defp remote_function(_meta, receiver, name, arity, _e) do + case Rewrite.inline(receiver, name, arity) do + {ar, an} -> {:remote, ar, an, arity} + false -> {:remote, receiver, name, arity} + end + end + + def find_imports_by_name([{mod, imports} | mod_imports], acc, name, meta, e) do + new_acc = find_imports_by_name(name, imports, acc, mod, meta, e) + find_imports_by_name(mod_imports, new_acc, name, meta, e) + end + + def find_imports_by_name([], acc, _name, _meta, _e), do: acc + + def find_imports_by_name(name, [{name, arity} | imports], acc, mod, meta, e) do + case Map.get(acc, arity) do + nil -> + find_imports_by_name(name, imports, Map.put(acc, arity, mod), mod, meta, e) + + _other_mod -> + # elixir raises here ambiguous_call + find_imports_by_name(name, imports, acc, mod, meta, e) + end + end + + def find_imports_by_name(name, [{import_name, _} | imports], acc, mod, meta, e) + when name > import_name do + find_imports_by_name(name, imports, acc, mod, meta, e) + end + + def find_imports_by_name(_name, _imports, acc, _mod, _meta, _e), do: acc + + defp find_import_by_name_arity(meta, {_name, arity} = tuple, extra, e) do + case is_import(meta, arity) do + {:import, _} = import_res -> + import_res + + false -> + funs = e.functions + macs = extra ++ e.macros + fun_match = find_import_by_name_arity(tuple, funs) + mac_match = find_import_by_name_arity(tuple, macs) + + case {fun_match, mac_match} do + {[], [receiver]} -> + {:macro, receiver} + + {[receiver], []} -> + {:function, receiver} + + {[], []} -> + false + + _ -> + {:ambiguous, fun_match ++ mac_match} + end + end + end + + defp find_import_by_name_arity(tuple, list) do + for {receiver, set} <- list, is_element(tuple, set), do: receiver + end + + defp is_import(meta, arity) do + with {:ok, imports = [_ | _]} <- Keyword.fetch(meta, :imports), + {:ok, _} <- Keyword.fetch(meta, :context), + {_arity, receiver} <- :lists.keyfind(arity, 1, imports) do + {:import, receiver} + else + _ -> false + end + end + + defp is_macro(_name, _arity, _module, false, _s), do: false + + defp is_macro(name, arity, receiver, true, s) do + mfa = {receiver, name, arity} + + Enum.any?(s.mods_funs_to_positions, fn {key, info} -> + key == mfa and ModFunInfo.get_category(info) == :macro + end) || + try do + macros = receiver.__info__(:macros) + {name, arity} in macros + rescue + _error -> false + end + end +end diff --git a/lib/elixir_sense/core/compiler/fn.ex b/lib/elixir_sense/core/compiler/fn.ex new file mode 100644 index 00000000..82268789 --- /dev/null +++ b/lib/elixir_sense/core/compiler/fn.ex @@ -0,0 +1,248 @@ +defmodule ElixirSense.Core.Compiler.Fn do + alias ElixirSense.Core.Compiler + alias ElixirSense.Core.Compiler.Clauses + alias ElixirSense.Core.Compiler.Dispatch + alias ElixirSense.Core.Compiler.Utils + alias ElixirSense.Core.Compiler.State + + def expand(meta, clauses, s, e) when is_list(clauses) do + transformer = fn + {:->, _, [_left, _right]} = clause, sa -> + # elixir raises defaults_in_args + s_reset = State.new_vars_scope(sa) + + # no point in doing type inference here, we have no idea what the fn will be called with + {e_clause, s_acc, _e_acc} = + Clauses.clause(&Clauses.head/3, clause, s_reset, e) + + {e_clause, State.remove_vars_scope(s_acc, sa)} + end + + {e_clauses, se} = Enum.map_reduce(clauses, s, transformer) + + {{:fn, meta, e_clauses}, se, e} + end + + # Capture + + def capture(meta, {:/, _, [{{:., _, [_m, f]} = dot, require_meta, []}, a]}, s, e) + when is_atom(f) and is_integer(a) do + args = args_from_arity(meta, a) + + capture_require({dot, require_meta, args}, s, e, true) + end + + def capture(meta, {:/, _, [{f, import_meta, c}, a]}, s, e) + when is_atom(f) and is_integer(a) and is_atom(c) do + args = args_from_arity(meta, a) + capture_import({f, import_meta, args}, s, e, true) + end + + def capture(_meta, {{:., _, [_, fun]}, _, args} = expr, s, e) + when is_atom(fun) and is_list(args) do + capture_require(expr, s, e, is_sequential_and_not_empty(args)) + end + + def capture(meta, {{:., _, [_]}, _, args} = expr, s, e) when is_list(args) do + capture_expr(meta, expr, s, e, false) + end + + def capture(meta, {:__block__, _, [expr]}, s, e) do + capture(meta, expr, s, e) + end + + def capture(meta, {:__block__, _, expr}, s, e) do + # elixir raises block_expr_in_capture + # try to recover from error + expr = + case expr do + [] -> + {:"&1", meta, e.module} + + list -> + Utils.select_with_cursor(list) || hd(list) + end + + capture(meta, expr, s, e) + end + + def capture(_meta, {atom, _, args} = expr, s, e) when is_atom(atom) and is_list(args) do + capture_import(expr, s, e, is_sequential_and_not_empty(args)) + end + + def capture(meta, {left, right}, s, e) do + capture(meta, {:{}, meta, [left, right]}, s, e) + end + + def capture(meta, list, s, e) when is_list(list) do + capture_expr(meta, list, s, e, is_sequential_and_not_empty(list)) + end + + def capture(meta, integer, s, e) when is_integer(integer) do + # elixir raises here capture_arg_outside_of_capture + # emit fake capture + capture(meta, [{:&, meta, [1]}], s, e) + end + + def capture(meta, arg, s, e) do + # elixir raises invalid_args_for_capture + # we try to transform the capture to local fun capture + case arg do + {var, _, context} when is_atom(var) and is_atom(context) -> + capture(meta, {:/, meta, [arg, 0]}, s, e) + + _ -> + # try to wrap it in list + capture(meta, [arg], s, e) + end + end + + defp capture_import({atom, import_meta, args} = expr, s, e, sequential) do + res = + if sequential do + Dispatch.import_function(import_meta, atom, length(args), s, e) + else + false + end + + handle_capture(res, import_meta, import_meta, expr, s, e, sequential) + end + + defp capture_require({{:., dot_meta, [left, right]}, require_meta, args}, s, e, sequential) do + case escape(left, []) do + {esc_left, []} -> + {e_left, se, ee} = Compiler.expand(esc_left, s, e) + + res = + if sequential do + case e_left do + {name, _, context} when is_atom(name) and is_atom(context) -> + {:remote, e_left, right, length(args)} + + _ when is_atom(e_left) -> + Dispatch.require_function( + require_meta, + e_left, + right, + length(args), + s, + ee + ) + + _ -> + false + end + else + false + end + + dot = {{:., dot_meta, [e_left, right]}, require_meta, args} + handle_capture(res, require_meta, dot_meta, dot, se, ee, sequential) + + {esc_left, escaped} -> + dot = {{:., dot_meta, [esc_left, right]}, require_meta, args} + capture_expr(require_meta, dot, s, e, escaped, sequential) + end + end + + defp handle_capture(false, meta, _dot_meta, expr, s, e, sequential) do + capture_expr(meta, expr, s, e, sequential) + end + + defp handle_capture(local_or_remote, meta, dot_meta, _expr, s, e, _sequential) do + {local_or_remote, meta, dot_meta, s, e} + end + + defp capture_expr(meta, expr, s, e, sequential) do + capture_expr(meta, expr, s, e, [], sequential) + end + + defp capture_expr(meta, expr, s, e, escaped, sequential) do + case escape(expr, escaped) do + {e_expr, []} when not sequential -> + # elixir raises here invalid_args_for_capture + # we emit fn without args + fn_expr = {:fn, meta, [{:->, meta, [[], e_expr]}]} + {:expand, fn_expr, s, e} + + {e_expr, e_dict} -> + # elixir raises capture_arg_without_predecessor here + # if argument vars are not consecutive + e_vars = Enum.map(e_dict, &elem(&1, 1)) + fn_expr = {:fn, meta, [{:->, meta, [e_vars, e_expr]}]} + {:expand, fn_expr, s, e} + end + end + + defp escape({:&, meta, [pos]}, dict) when is_integer(pos) and pos > 0 do + # This might pollute user space but is unlikely because variables + # named :"&1" are not valid syntax. + var = {:"&#{pos}", meta, nil} + {var, :orddict.store(pos, var, dict)} + + case :orddict.find(pos, dict) do + {:ok, var} -> + {var, dict} + + :error -> + # elixir uses here elixir_module:next_counter(?key(E, module)) + # but we are not compiling and do not need to keep count in module scope + # elixir 1.17 also renames the var to `capture` + next = System.unique_integer() + var = {:"&#{pos}", [{:counter, next} | meta], nil} + {var, :orddict.store(pos, var, dict)} + end + end + + defp escape({:&, meta, [pos]}, dict) when is_integer(pos) do + # elixir raises here invalid_arity_for_capture + # we substitute arg number + escape({:&, meta, [1]}, dict) + end + + defp escape({:&, _meta, args}, dict) do + # elixir raises here nested_capture + # try to recover from error by dropping & + escape(args, dict) + end + + defp escape({left, meta, right}, dict0) do + {t_left, dict1} = escape(left, dict0) + {t_right, dict2} = escape(right, dict1) + {{t_left, meta, t_right}, dict2} + end + + defp escape({left, right}, dict0) do + {t_left, dict1} = escape(left, dict0) + {t_right, dict2} = escape(right, dict1) + {{t_left, t_right}, dict2} + end + + defp escape(list, dict) when is_list(list) do + Enum.map_reduce(list, dict, fn x, acc -> escape(x, acc) end) + end + + defp escape(other, dict) do + {other, dict} + end + + defp args_from_arity(_meta, 0), do: [] + + defp args_from_arity(meta, a) when is_integer(a) and a >= 1 and a <= 255 do + for x <- 1..a do + {:&, meta, [x]} + end + end + + defp args_from_arity(_meta, _a) do + # elixir raises invalid_arity_for_capture + [] + end + + defp is_sequential_and_not_empty([]), do: false + defp is_sequential_and_not_empty(list), do: is_sequential(list, 1) + + defp is_sequential([{:&, _, [int]} | t], int), do: is_sequential(t, int + 1) + defp is_sequential([], _int), do: true + defp is_sequential(_, _int), do: false +end diff --git a/lib/elixir_sense/core/compiler/macro.ex b/lib/elixir_sense/core/compiler/macro.ex new file mode 100644 index 00000000..d46f5a17 --- /dev/null +++ b/lib/elixir_sense/core/compiler/macro.ex @@ -0,0 +1,200 @@ +defmodule ElixirSense.Core.Compiler.Macro do + alias ElixirSense.Core.Normalized.Macro.Env, as: NormalizedMacroEnv + + @spec expand_literals(Macro.input(), Macro.Env.t()) :: Macro.output() + def expand_literals(ast, env) do + {ast, :ok} = expand_literals(ast, :ok, fn node, :ok -> {expand(node, env), :ok} end) + ast + end + + @spec expand_literals(Macro.t(), acc, (Macro.t(), acc -> {Macro.t(), acc})) :: Macro.t() + when acc: term() + def expand_literals(ast, acc, fun) + + def expand_literals({:__aliases__, meta, args}, acc, fun) do + {args, acc} = expand_literals(args, acc, fun) + + if :lists.all(&is_atom/1, args) do + fun.({:__aliases__, meta, args}, acc) + else + {{:__aliases__, meta, args}, acc} + end + end + + def expand_literals({:__MODULE__, _meta, ctx} = node, acc, fun) when is_atom(ctx) do + fun.(node, acc) + end + + def expand_literals({:%, meta, [left, right]}, acc, fun) do + {left, acc} = expand_literals(left, acc, fun) + {right, acc} = expand_literals(right, acc, fun) + {{:%, meta, [left, right]}, acc} + end + + def expand_literals({:%{}, meta, args}, acc, fun) do + {args, acc} = expand_literals(args, acc, fun) + {{:%{}, meta, args}, acc} + end + + def expand_literals({:{}, meta, args}, acc, fun) do + {args, acc} = expand_literals(args, acc, fun) + {{:{}, meta, args}, acc} + end + + def expand_literals({left, right}, acc, fun) do + {left, acc} = expand_literals(left, acc, fun) + {right, acc} = expand_literals(right, acc, fun) + {{left, right}, acc} + end + + def expand_literals(list, acc, fun) when is_list(list) do + :lists.mapfoldl(&expand_literals(&1, &2, fun), acc, list) + end + + def expand_literals( + {{:., _, [{:__aliases__, _, [:Application]}, :compile_env]} = node, meta, + [app, key, default]}, + acc, + fun + ) do + # TODO track call? + {default, acc} = expand_literals(default, acc, fun) + {{node, meta, [app, key, default]}, acc} + end + + def expand_literals(term, acc, _fun), do: {term, acc} + + @spec expand(Macro.input(), Macro.Env.t()) :: Macro.output() + def expand(ast, env) do + expand_until({ast, true}, env) + end + + defp expand_until({ast, true}, env) do + expand_until(do_expand_once(ast, env), env) + end + + defp expand_until({ast, false}, _env) do + ast + end + + @spec expand_once(Macro.input(), Macro.Env.t()) :: Macro.output() + def expand_once(ast, env) do + elem(do_expand_once(ast, env), 0) + end + + defp do_expand_once({:__aliases__, meta, [head | tail] = list} = alias, env) do + case NormalizedMacroEnv.expand_alias(env, meta, list, trace: false) do + {:alias, alias} -> + # TODO track alias + {alias, true} + + :error -> + {head, _} = do_expand_once(head, env) + + if is_atom(head) do + receiver = Module.concat([head | tail]) + # TODO track alias + {receiver, true} + else + {alias, false} + end + end + end + + # Expand compilation environment macros + defp do_expand_once({:__MODULE__, _, atom}, env) when is_atom(atom), do: {env.module, true} + + defp do_expand_once({:__DIR__, _, atom}, env) when is_atom(atom), + do: {:filename.dirname(env.file), true} + + defp do_expand_once({:__ENV__, _, atom}, env) when is_atom(atom) and env.context != :match do + env = update_in(env.versioned_vars, &maybe_escape_map/1) + {maybe_escape_map(env), true} + end + + defp do_expand_once({{:., _, [{:__ENV__, _, atom}, field]}, _, []} = original, env) + when is_atom(atom) and is_atom(field) and env.context != :match do + if Map.has_key?(env, field) do + {maybe_escape_map(Map.get(env, field)), true} + else + {original, false} + end + end + + defp do_expand_once({name, meta, context} = original, _env) + when is_atom(name) and is_list(meta) and is_atom(context) do + {original, false} + end + + defp do_expand_once({name, meta, args} = original, env) + when is_atom(name) and is_list(args) and is_list(meta) do + arity = length(args) + + case NormalizedMacroEnv.expand_import(env, meta, name, arity, + trace: false, + check_deprecations: false + ) do + {:macro, _receiver, expander} -> + # TODO register call + # We don't want the line to propagate yet, but generated might! + {expander.(Keyword.take(meta, [:generated]), args), true} + + {:function, Kernel, op} when op in [:+, :-] and arity == 1 -> + case expand_once(hd(args), env) do + integer when is_integer(integer) -> + # TODO register call + {apply(Kernel, op, [integer]), true} + + _ -> + {original, false} + end + + {:function, _receiver, _name} -> + {original, false} + + {:error, :not_found} -> + {original, false} + + {:error, _other} -> + # elixir raises elixir_dispatch here + {original, false} + end + end + + # Expand possible macro require invocation + defp do_expand_once({{:., _, [left, name]}, meta, args} = original, env) when is_atom(name) do + {receiver, _} = do_expand_once(left, env) + + case is_atom(receiver) do + false -> + {original, false} + + true -> + case NormalizedMacroEnv.expand_require(env, meta, receiver, name, length(args), + trace: false, + check_deprecations: false + ) do + {:macro, _receiver, expander} -> + # TODO register call + # We don't want the line to propagate yet, but generated might! + {expander.(Keyword.take(meta, [:generated]), args), true} + + :error -> + {original, false} + end + end + end + + # Anything else is just returned + defp do_expand_once(other, _env), do: {other, false} + + defp maybe_escape_map(map) when is_map(map), do: {:%{}, [], Map.to_list(map)} + defp maybe_escape_map(other), do: other + + @spec escape(term, keyword) :: Macro.t() + def escape(expr, opts \\ []) do + unquote = Keyword.get(opts, :unquote, false) + kind = if Keyword.get(opts, :prune_metadata, false), do: :prune_metadata, else: :none + ElixirSense.Core.Compiler.Quote.escape(expr, kind, unquote) + end +end diff --git a/lib/elixir_sense/core/compiler/map.ex b/lib/elixir_sense/core/compiler/map.ex new file mode 100644 index 00000000..eb717ef5 --- /dev/null +++ b/lib/elixir_sense/core/compiler/map.ex @@ -0,0 +1,184 @@ +defmodule ElixirSense.Core.Compiler.Map do + alias ElixirSense.Core.Compiler + + def expand_struct(meta, left, {:%{}, map_meta, map_args}, s, %{context: context} = e) do + clean_map_args = clean_struct_key_from_map_args(map_args) + + {[e_left, e_right], se, ee} = + Compiler.expand_args([left, {:%{}, map_meta, clean_map_args}], s, e) + + case validate_struct(e_left, context) do + true when is_atom(e_left) -> + # TODO register alias/struct + case extract_struct_assocs(e_right) do + {:expand, map_meta, assocs} when context != :match -> + assoc_keys = Enum.map(assocs, fn {k, _} -> k end) + struct = load_struct(e_left, [assocs], se, ee) + keys = [:__struct__ | assoc_keys] + without_keys = Elixir.Map.drop(struct, keys) + + struct_assocs = + Compiler.Macro.escape(Enum.sort(Elixir.Map.to_list(without_keys))) + + {{:%, meta, [e_left, {:%{}, map_meta, struct_assocs ++ assocs}]}, se, ee} + + {_, _, _assocs} -> + # elixir validates assocs against struct keys + # we don't need to validate keys + {{:%, meta, [e_left, e_right]}, se, ee} + end + + _ -> + # elixir raises invalid_struct_name if validate_struct returns false + {{:%, meta, [e_left, e_right]}, se, ee} + end + end + + def expand_struct(meta, left, right, s, e) do + # elixir raises here non_map_after_struct + # try to recover from error by wrapping the expression in map + expand_struct(meta, left, wrap_in_fake_map(right), s, e) + end + + defp wrap_in_fake_map(right) do + map_args = + case right do + list when is_list(list) -> + if Keyword.keyword?(list) do + list + else + [__fake_key__: list] + end + + _ -> + [__fake_key__: right] + end + + {:%{}, [], map_args} + end + + def expand_map(meta, [{:|, update_meta, [left, right]}], s, e) do + # elixir raises update_syntax_in_wrong_context if e.context is not nil + {[e_left | e_right], se, ee} = Compiler.expand_args([left | right], s, e) + e_right = sanitize_kv(e_right, e) + {{:%{}, meta, [{:|, update_meta, [e_left, e_right]}]}, se, ee} + end + + def expand_map(meta, args, s, e) do + {e_args, se, ee} = Compiler.expand_args(args, s, e) + e_args = sanitize_kv(e_args, e) + {{:%{}, meta, e_args}, se, ee} + end + + defp clean_struct_key_from_map_args([{:|, pipe_meta, [left, map_assocs]}]) do + [{:|, pipe_meta, [left, delete_struct_key(map_assocs)]}] + end + + defp clean_struct_key_from_map_args(map_assocs) do + delete_struct_key(map_assocs) + end + + defp sanitize_kv(kv, %{context: context}) do + Enum.filter(kv, fn + {k, _v} -> + if context == :match do + validate_match_key(k) + else + true + end + + _ -> + false + end) + end + + defp validate_match_key({name, _, context}) + when is_atom(name) and is_atom(context) do + # elixir raises here invalid_variable_in_map_key_match + false + end + + defp validate_match_key({:"::", _, [left, _]}) do + validate_match_key(left) + end + + defp validate_match_key({:^, _, [{name, _, context}]}) + when is_atom(name) and is_atom(context), + do: true + + defp validate_match_key({:%{}, _, [_ | _]}), do: true + + defp validate_match_key({left, _, right}) do + validate_match_key(left) and validate_match_key(right) + end + + defp validate_match_key({left, right}) do + validate_match_key(left) and validate_match_key(right) + end + + defp validate_match_key(list) when is_list(list) do + Enum.all?(list, &validate_match_key/1) + end + + defp validate_match_key(_), do: true + + defp validate_struct({:^, _, [{var, _, ctx}]}, :match) when is_atom(var) and is_atom(ctx), + do: true + + defp validate_struct({var, _meta, ctx}, :match) when is_atom(var) and is_atom(ctx), do: true + defp validate_struct(atom, _) when is_atom(atom), do: true + defp validate_struct(_, _), do: false + + defp sanitize_assocs(list) do + Enum.filter(list, &match?({k, _} when is_atom(k), &1)) + end + + defp extract_struct_assocs({:%{}, meta, [{:|, _, [_, assocs]}]}) do + {:update, meta, delete_struct_key(sanitize_assocs(assocs))} + end + + defp extract_struct_assocs({:%{}, meta, assocs}) do + {:expand, meta, delete_struct_key(sanitize_assocs(assocs))} + end + + defp extract_struct_assocs(right) do + # elixir raises here non_map_after_struct + # try to recover from error by wrapping the expression in map + extract_struct_assocs(wrap_in_fake_map(right)) + end + + defp delete_struct_key(assocs) do + Keyword.delete(assocs, :__struct__) + end + + def load_struct(name, assocs, s, _e) do + case s.structs[name] do + nil -> + try do + apply(name, :__struct__, assocs) + else + %{:__struct__ => ^name} = struct -> + struct + + _ -> + # recover from invalid return value + [__struct__: name] |> merge_assocs(assocs) + rescue + _ -> + # recover from error by building the fake struct + [__struct__: name] |> merge_assocs(assocs) + end + + info -> + info.fields |> merge_assocs(assocs) + end + end + + defp merge_assocs(fields, []) do + fields |> Elixir.Map.new() + end + + defp merge_assocs(fields, [assocs]) do + fields |> Keyword.merge(assocs) |> Elixir.Map.new() + end +end diff --git a/lib/elixir_sense/core/compiler/quote.ex b/lib/elixir_sense/core/compiler/quote.ex new file mode 100644 index 00000000..ca2dda74 --- /dev/null +++ b/lib/elixir_sense/core/compiler/quote.ex @@ -0,0 +1,546 @@ +defmodule ElixirSense.Core.Compiler.Quote do + alias ElixirSense.Core.Compiler.Dispatch + alias ElixirSense.Core.Normalized.Macro.Env, as: NormalizedMacroEnv + + defstruct line: false, + file: nil, + context: nil, + op: :none, + aliases_hygiene: nil, + imports_hygiene: nil, + unquote: true, + generated: false + + def fun_to_quoted(function) do + {:module, module} = :erlang.fun_info(function, :module) + {:name, name} = :erlang.fun_info(function, :name) + {:arity, arity} = :erlang.fun_info(function, :arity) + + {:&, [], [{:/, [], [{{:., [], [module, name]}, [{:no_parens, true}], []}, arity]}]} + end + + def has_unquotes(ast), do: has_unquotes(ast, 0) + + def has_unquotes({:quote, _, [child]}, quote_level) do + has_unquotes(child, quote_level + 1) + end + + def has_unquotes({:quote, _, [quote_opts, child]}, quote_level) do + case disables_unquote(quote_opts) do + true -> false + _ -> has_unquotes(child, quote_level + 1) + end + end + + def has_unquotes({unquote, _, [child]}, quote_level) + when unquote in [:unquote, :unquote_splicing] do + case quote_level do + 0 -> true + _ -> has_unquotes(child, quote_level - 1) + end + end + + def has_unquotes({{:., _, [_, :unquote]}, _, [_]}, _), do: true + def has_unquotes({var, _, ctx}, _) when is_atom(var) and is_atom(ctx), do: false + + def has_unquotes({name, _, args}, quote_level) when is_list(args) do + has_unquotes(name) or Enum.any?(args, fn child -> has_unquotes(child, quote_level) end) + end + + def has_unquotes({left, right}, quote_level) do + has_unquotes(left, quote_level) or has_unquotes(right, quote_level) + end + + def has_unquotes(list, quote_level) when is_list(list) do + Enum.any?(list, fn child -> has_unquotes(child, quote_level) end) + end + + def has_unquotes(_other, _), do: false + + defp disables_unquote([{:unquote, false} | _]), do: true + defp disables_unquote([{:bind_quoted, _} | _]), do: true + defp disables_unquote([_h | t]), do: disables_unquote(t) + defp disables_unquote(_), do: false + + def build(meta, line, file, context, unquote, generated, e) do + acc0 = [] + + {v_line, acc1} = validate_compile(meta, :line, line, acc0) + {v_file, acc2} = validate_compile(meta, :file, file, acc1) + {v_context, acc3} = validate_compile(meta, :context, context, acc2) + + unquote = validate_runtime(:unquote, unquote) + generated = validate_runtime(:generated, generated) + + q = %__MODULE__{ + op: :add_context, + aliases_hygiene: e, + imports_hygiene: e, + line: v_line, + file: v_file, + unquote: unquote, + context: v_context, + generated: generated + } + + {q, v_context, acc3} + end + + defp validate_compile(_meta, :line, value, acc) when is_boolean(value) do + {value, acc} + end + + defp validate_compile(_meta, :file, nil, acc) do + {nil, acc} + end + + defp validate_compile(meta, key, value, acc) do + case is_valid(key, value) do + true -> + {value, acc} + + false -> + var = {key, meta, __MODULE__} + call = {{:., meta, [__MODULE__, :validate_runtime]}, meta, [key, value]} + {var, [{:=, meta, [var, call]} | acc]} + end + end + + defp validate_runtime(key, value) do + case is_valid(key, value) do + true -> + value + + false -> + # elixir raises here invalid runtime value for option + default(key) + end + end + + defp is_valid(:line, line), do: is_integer(line) + defp is_valid(:file, file), do: is_binary(file) + defp is_valid(:context, context), do: is_atom(context) and context != nil + defp is_valid(:generated, generated), do: is_boolean(generated) + defp is_valid(:unquote, unquote), do: is_boolean(unquote) + defp default(:unquote), do: true + defp default(:generated), do: false + + def escape(expr, op, unquote) do + do_quote( + expr, + %__MODULE__{ + line: true, + file: nil, + op: op, + unquote: unquote + } + ) + end + + def quote({:unquote_splicing, _, [_]} = expr, %__MODULE__{unquote: true} = q) do + # elixir raises here unquote_splicing only works inside arguments and block contexts + # try to recover from error by wrapping it in block + __MODULE__.quote({:__block__, [], [expr]}, q) + end + + def quote(expr, q) do + do_quote(expr, q) + end + + # quote/unquote + + defp do_quote({:quote, meta, [arg]}, q) when is_list(meta) do + t_arg = do_quote(arg, %__MODULE__{q | unquote: false}) + + new_meta = + case q do + %__MODULE__{op: :add_context, context: context} -> + keystore(:context, meta, context) + + _ -> + meta + end + + {:{}, [], [:quote, meta(new_meta, q), [t_arg]]} + end + + defp do_quote({:quote, meta, [opts, arg]}, q) when is_list(meta) do + t_opts = do_quote(opts, q) + t_arg = do_quote(arg, %__MODULE__{q | unquote: false}) + + new_meta = + case q do + %__MODULE__{op: :add_context, context: context} -> + keystore(:context, meta, context) + + _ -> + meta + end + + {:{}, [], [:quote, meta(new_meta, q), [t_opts, t_arg]]} + end + + defp do_quote({:unquote, meta, [expr]}, %__MODULE__{unquote: true}) when is_list(meta), + do: expr + + # Aliases + + defp do_quote({:__aliases__, meta, [h | t] = list}, %__MODULE__{aliases_hygiene: e = %{}} = q) + when is_atom(h) and h != Elixir and is_list(meta) do + annotation = + case NormalizedMacroEnv.expand_alias(e, meta, list, trace: false) do + {:alias, atom} -> atom + :error -> false + end + + alias_meta = keystore(:alias, Keyword.delete(meta, :counter), annotation) + do_quote_tuple(:__aliases__, alias_meta, [h | t], q) + end + + # Vars + + defp do_quote({name, meta, nil}, %__MODULE__{op: :add_context} = q) + when is_atom(name) and is_list(meta) do + import_meta = + case q.imports_hygiene do + nil -> meta + e -> import_meta(meta, name, 0, q, e) + end + + {:{}, [], [name, meta(import_meta, q), q.context]} + end + + # cursor + + defp do_quote( + {:__cursor__, meta, args}, + %__MODULE__{unquote: _} + ) + when is_list(args) do + # emit cursor as is regardless of unquote + {:__cursor__, meta, args} + end + + # Unquote + + defp do_quote( + {{{:., meta, [left, :unquote]}, _, [expr]}, _, args}, + %__MODULE__{unquote: true} = q + ) + when is_list(meta) do + do_quote_call(left, meta, expr, args, q) + end + + defp do_quote({{:., meta, [left, :unquote]}, _, [expr]}, %__MODULE__{unquote: true} = q) + when is_list(meta) do + do_quote_call(left, meta, expr, nil, q) + end + + # Imports + + defp do_quote( + {:&, meta, [{:/, _, [{f, _, c}, a]}] = args}, + %__MODULE__{imports_hygiene: e = %{}} = q + ) + when is_atom(f) and is_integer(a) and is_atom(c) and is_list(meta) do + new_meta = + case Dispatch.find_import(meta, f, a, e) do + false -> + meta + + receiver -> + keystore(:context, keystore(:imports, meta, [{a, receiver}]), q.context) + end + + do_quote_tuple(:&, new_meta, args, q) + end + + defp do_quote({name, meta, args_or_context}, %__MODULE__{imports_hygiene: e = %{}} = q) + when is_atom(name) and is_list(meta) and + (is_list(args_or_context) or is_atom(args_or_context)) do + arity = + case args_or_context do + args when is_list(args) -> length(args) + context when is_atom(context) -> 0 + end + + import_meta = import_meta(meta, name, arity, q, e) + annotated = annotate({name, import_meta, args_or_context}, q.context) + do_quote_tuple(annotated, q) + end + + # Two-element tuples + + defp do_quote({left, right}, %__MODULE__{unquote: true} = q) + when is_tuple(left) and elem(left, 0) == :unquote_splicing and + is_tuple(right) and elem(right, 0) == :unquote_splicing do + do_quote({:{}, [], [left, right]}, q) + end + + defp do_quote({left, right}, q) do + t_left = do_quote(left, q) + t_right = do_quote(right, q) + {t_left, t_right} + end + + # Everything else + + defp do_quote(other, q = %{op: op}) when op != :add_context do + do_escape(other, q) + end + + defp do_quote({_, _, _} = tuple, q) do + annotated = annotate(tuple, q.context) + do_quote_tuple(annotated, q) + end + + defp do_quote([], _), do: [] + + defp do_quote([h | t], %__MODULE__{unquote: false} = q) do + head_quoted = do_quote(h, q) + do_quote_simple_list(t, head_quoted, q) + end + + defp do_quote([h | t], q) do + do_quote_tail(:lists.reverse(t, [h]), q) + end + + defp do_quote(other, _), do: other + + defp import_meta(meta, name, arity, q, e) do + case Keyword.get(meta, :imports, false) == false && + Dispatch.find_imports(meta, name, e) do + [_ | _] = imports -> + keystore(:imports, keystore(:context, meta, q.context), imports) + + _ -> + case arity == 1 && Keyword.fetch(meta, :ambiguous_op) do + {:ok, nil} -> + keystore(:ambiguous_op, meta, q.context) + + _ -> + meta + end + end + end + + defp do_quote_call(left, meta, expr, args, q) do + all = [left, {:unquote, meta, [expr]}, args, q.context] + tall = Enum.map(all, fn x -> do_quote(x, q) end) + {{:., meta, [:elixir_quote, :dot]}, meta, [meta(meta, q) | tall]} + end + + defp do_quote_tuple({left, meta, right}, q) do + do_quote_tuple(left, meta, right, q) + end + + defp do_quote_tuple(left, meta, right, q) do + t_left = do_quote(left, q) + t_right = do_quote(right, q) + {:{}, [], [t_left, meta(meta, q), t_right]} + end + + defp do_quote_simple_list([], prev, _), do: [prev] + + defp do_quote_simple_list([h | t], prev, q) do + [prev | do_quote_simple_list(t, do_quote(h, q), q)] + end + + defp do_quote_simple_list(other, prev, q) do + [{:|, [], [prev, do_quote(other, q)]}] + end + + defp do_quote_tail( + [{:|, meta, [{:unquote_splicing, _, [left]}, right]} | t], + %__MODULE__{unquote: true} = q + ) do + tt = do_quote_splice(t, q, [], []) + tr = do_quote(right, q) + do_runtime_list(meta, :tail_list, [left, tr, tt]) + end + + defp do_quote_tail(list, q) do + do_quote_splice(list, q, [], []) + end + + defp do_quote_splice( + [{:unquote_splicing, meta, [expr]} | t], + %__MODULE__{unquote: true} = q, + buffer, + acc + ) do + runtime = do_runtime_list(meta, :list, [expr, do_list_concat(buffer, acc)]) + do_quote_splice(t, q, [], runtime) + end + + defp do_quote_splice([h | t], q, buffer, acc) do + th = do_quote(h, q) + do_quote_splice(t, q, [th | buffer], acc) + end + + defp do_quote_splice([], _q, buffer, acc) do + do_list_concat(buffer, acc) + end + + defp do_list_concat(left, []), do: left + defp do_list_concat([], right), do: right + + defp do_list_concat(left, right) do + {{:., [], [:erlang, :++]}, [], [left, right]} + end + + defp do_runtime_list(meta, fun, args) do + {{:., meta, [:elixir_quote, fun]}, meta, args} + end + + defp meta(meta, q) do + generated(keep(Keyword.delete(meta, :column), q), q) + end + + defp generated(meta, %__MODULE__{generated: true}), do: [{:generated, true} | meta] + defp generated(meta, %__MODULE__{generated: false}), do: meta + + defp keep(meta, %__MODULE__{file: nil, line: line}) do + line(meta, line) + end + + defp keep(meta, %__MODULE__{file: file, line: true}) do + case Keyword.pop(meta, :line) do + {nil, _} -> + [{:keep, {file, 0}} | meta] + + {line, meta_no_line} -> + [{:keep, {file, line}} | meta_no_line] + end + end + + defp keep(meta, %__MODULE__{file: file, line: false}) do + [{:keep, {file, 0}} | Keyword.delete(meta, :line)] + end + + defp keep(meta, %__MODULE__{file: file, line: line}) do + [{:keep, {file, line}} | Keyword.delete(meta, :line)] + end + + defp line(meta, true), do: meta + + defp line(meta, false) do + Keyword.delete(meta, :line) + end + + defp line(meta, line) do + keystore(:line, meta, line) + end + + defguardp defs(kind) when kind in [:def, :defp, :defmacro, :defmacrop, :@] + defguardp lexical(kind) when kind in [:import, :alias, :require] + + defp annotate({def, meta, [h | t]}, context) when defs(def) do + {def, meta, [annotate_def(h, context) | t]} + end + + defp annotate({{:., _, [_, def]} = target, meta, [h | t]}, context) when defs(def) do + {target, meta, [annotate_def(h, context) | t]} + end + + defp annotate({lexical, meta, [_ | _] = args}, context) when lexical(lexical) do + new_meta = keystore(:context, Keyword.delete(meta, :counter), context) + {lexical, new_meta, args} + end + + defp annotate(tree, _context), do: tree + + defp annotate_def({:when, meta, [left, right]}, context) do + {:when, meta, [annotate_def(left, context), right]} + end + + defp annotate_def({fun, meta, args}, context) do + {fun, keystore(:context, meta, context), args} + end + + defp annotate_def(other, _context), do: other + + defp do_escape({left, meta, right}, q = %{op: :prune_metadata}) when is_list(meta) do + tm = for {k, v} <- meta, k == :no_parens or k == :line, do: {k, v} + tl = do_quote(left, q) + tr = do_quote(right, q) + {:{}, [], [tl, tm, tr]} + end + + defp do_escape(tuple, q) when is_tuple(tuple) do + tt = do_quote(Tuple.to_list(tuple), q) + {:{}, [], tt} + end + + defp do_escape(bitstring, _) when is_bitstring(bitstring) do + case Bitwise.band(bit_size(bitstring), 7) do + 0 -> + bitstring + + size -> + <> = bitstring + + {:<<>>, [], + [{:"::", [], [bits, {size, [], [size]}]}, {:"::", [], [bytes, {:binary, [], nil}]}]} + end + end + + defp do_escape(map, q) when is_map(map) do + tt = do_quote(Enum.sort(Map.to_list(map)), q) + {:%{}, [], tt} + end + + defp do_escape([], _), do: [] + + defp do_escape([h | t], %__MODULE__{unquote: false} = q) do + do_quote_simple_list(t, do_quote(h, q), q) + end + + defp do_escape([h | t], q) do + # The improper case is inefficient, but improper lists are rare. + try do + l = Enum.reverse(t, [h]) + do_quote_tail(l, q) + catch + _ -> + {l, r} = reverse_improper(t, [h]) + tl = do_quote_splice(l, q, [], []) + tr = do_quote(r, q) + update_last(tl, fn x -> {:|, [], [x, tr]} end) + end + end + + defp do_escape(other, _) when is_number(other) or is_pid(other) or is_atom(other), + do: other + + defp do_escape(fun, _) when is_function(fun) do + case {Function.info(fun, :env), Function.info(fun, :type)} do + {{:env, []}, {:type, :external}} -> + fun_to_quoted(fun) + + _ -> + # elixir raises here ArgumentError + nil + end + end + + defp do_escape(_other, _) do + # elixir raises here ArgumentError + nil + end + + defp reverse_improper([h | t], acc), do: reverse_improper(t, [h | acc]) + defp reverse_improper([], acc), do: acc + defp reverse_improper(t, acc), do: {acc, t} + defp update_last([], _), do: [] + defp update_last([h], f), do: [f.(h)] + defp update_last([h | t], f), do: [h | update_last(t, f)] + + defp keystore(_key, meta, value) when value == nil do + meta + end + + defp keystore(key, meta, value) do + :lists.keystore(key, 1, meta, {key, value}) + end +end diff --git a/lib/elixir_sense/core/compiler/rewrite.ex b/lib/elixir_sense/core/compiler/rewrite.ex new file mode 100644 index 00000000..d3053b86 --- /dev/null +++ b/lib/elixir_sense/core/compiler/rewrite.ex @@ -0,0 +1,32 @@ +defmodule ElixirSense.Core.Compiler.Rewrite do + def inline(module, fun, arity) do + :elixir_rewrite.inline(module, fun, arity) + end + + def rewrite(context, receiver, dot_meta, right, meta, e_args, s) do + do_rewrite(context, receiver, dot_meta, right, meta, e_args, s) + end + + defp do_rewrite(_, :erlang, _, :+, _, [arg], _s) when is_number(arg), do: {:ok, arg} + + defp do_rewrite(_, :erlang, _, :-, _, [arg], _s) when is_number(arg), do: {:ok, -arg} + + defp do_rewrite(:match, receiver, dot_meta, right, meta, e_args, _s) do + :elixir_rewrite.match_rewrite(receiver, dot_meta, right, meta, e_args) + end + + if Version.match?(System.version(), "< 1.14.0") do + defp do_rewrite(:guard, receiver, dot_meta, right, meta, e_args, s) do + :elixir_rewrite.guard_rewrite(receiver, dot_meta, right, meta, e_args) + end + else + defp do_rewrite(:guard, receiver, dot_meta, right, meta, e_args, _s) do + # elixir uses guard context for error messages + :elixir_rewrite.guard_rewrite(receiver, dot_meta, right, meta, e_args, "guard") + end + end + + defp do_rewrite(_, receiver, dot_meta, right, meta, e_args, _s) do + {:ok, :elixir_rewrite.rewrite(receiver, dot_meta, right, meta, e_args)} + end +end diff --git a/lib/elixir_sense/core/compiler/state.ex b/lib/elixir_sense/core/compiler/state.ex new file mode 100644 index 00000000..bc2ace42 --- /dev/null +++ b/lib/elixir_sense/core/compiler/state.ex @@ -0,0 +1,1376 @@ +defmodule ElixirSense.Core.Compiler.State do + alias ElixirSense.Core.BuiltinFunctions + alias ElixirSense.Core.State.Env + + alias ElixirSense.Core.State.{ + CallInfo, + StructInfo, + ModFunInfo, + SpecInfo, + TypeInfo, + VarInfo, + AttributeInfo + } + + require Logger + + @type fun_arity :: {atom, non_neg_integer} + @type scope :: atom | fun_arity | {:typespec, atom, non_neg_integer} + + @type alias_t :: {module, module} + @type scope_id_t :: non_neg_integer + @type position_t :: {pos_integer, pos_integer} + + @type mods_funs_to_positions_t :: %{ + optional({module, atom, nil | non_neg_integer}) => ElixirSense.Core.State.ModFunInfo.t() + } + @type lines_to_env_t :: %{optional(pos_integer) => ElixirSense.Core.State.Env.t()} + @type calls_t :: %{optional(pos_integer) => list(ElixirSense.Core.State.CallInfo.t())} + + @type types_t :: %{ + optional({module, atom, nil | non_neg_integer}) => ElixirSense.Core.State.TypeInfo.t() + } + @type specs_t :: %{ + optional({module, atom, nil | non_neg_integer}) => ElixirSense.Core.State.SpecInfo.t() + } + @type vars_info_per_scope_id_t :: %{ + optional(scope_id_t) => [ + %{optional({atom(), non_neg_integer()}) => ElixirSense.Core.State.VarInfo.t()} + ] + } + @type structs_t :: %{optional(module) => ElixirSense.Core.State.StructInfo.t()} + @type protocol_t :: {module, nonempty_list(module)} + @type var_type :: nil | {:atom, atom} | {:map, keyword} | {:struct, keyword, module} + + @type t :: %__MODULE__{ + attributes: list(list(ElixirSense.Core.State.AttributeInfo.t())), + scope_attributes: list(list(atom)), + behaviours: %{optional(module) => [module]}, + specs: specs_t, + types: types_t, + mods_funs_to_positions: mods_funs_to_positions_t, + structs: structs_t, + calls: calls_t, + vars_info: + list(%{optional({atom, non_neg_integer}) => ElixirSense.Core.State.VarInfo.t()}), + vars_info_per_scope_id: vars_info_per_scope_id_t, + scope_id_count: non_neg_integer, + scope_ids: list(scope_id_t), + typespec: nil | {atom, arity}, + protocol: nil | {atom, [atom]}, + + # elixir_ex + vars: {map, false | map()}, + unused: non_neg_integer(), + prematch: atom | tuple, + stacktrace: boolean(), + caller: boolean(), + runtime_modules: list(module), + first_alias_positions: map(), + moduledoc_positions: map(), + doc_context: list(), + typedoc_context: list(), + optional_callbacks_context: list(), + lines_to_env: lines_to_env_t, + cursor_env: nil | {keyword, ElixirSense.Core.State.Env.t()}, + closest_env: + nil + | {{pos_integer, pos_integer}, {non_neg_integer, non_neg_integer}, + ElixirSense.Core.State.Env.t()}, + ex_unit_describe: nil | atom, + attribute_store: %{optional({module, atom}) => term}, + cursor_position: nil | {pos_integer, pos_integer} + } + + defstruct attributes: [[]], + scope_attributes: [[]], + behaviours: %{}, + specs: %{}, + types: %{}, + mods_funs_to_positions: %{}, + structs: %{}, + calls: %{}, + vars_info: [%{}], + vars_info_per_scope_id: %{}, + scope_id_count: 0, + scope_ids: [0], + typespec: nil, + protocol: nil, + + # elixir_ex + vars: {%{}, false}, + unused: 0, + prematch: :raise, + stacktrace: false, + caller: false, + runtime_modules: [], + first_alias_positions: %{}, + moduledoc_positions: %{}, + doc_context: [[]], + typedoc_context: [[]], + optional_callbacks_context: [[]], + lines_to_env: %{}, + cursor_env: nil, + closest_env: nil, + ex_unit_describe: nil, + attribute_store: %{}, + cursor_position: nil + + def get_current_env(%__MODULE__{} = state, macro_env) do + current_attributes = state |> get_current_attributes() + current_behaviours = state.behaviours |> Map.get(macro_env.module, []) + + current_scope_id = hd(state.scope_ids) + + # Macro.Env versioned_vars is not updated + # elixir keeps current vars in state + # write vars are not really interesting (nor are write vars from upper write) + # only read vars are accessible + # NOTE definition/hover/references providers get all vars mapped to scope_id + # this means write vars are already closed + # completions provider do not need write vars at all + {read, _write} = state.vars + versioned_vars = read + + [current_vars_info | _] = state.vars_info + + # here we filter vars to only return the ones with nil context to maintain macro hygiene + # &n capture args are an exception as they have non nil context everywhere (since elixir 1.17) + # we return them all but the risk of breaking hygiene is small + vars = + for {{name, context}, version} <- versioned_vars, + context == nil or String.starts_with?(to_string(name), "&") do + Map.fetch!(current_vars_info, {name, version}) + end + + current_protocol = + case state.protocol do + nil -> + nil + + {protocol, for_list} -> + # check wether we are in implementation or implementation child module + if Enum.any?(for_list, fn for -> macro_env.module == Module.concat(protocol, for) end) do + {protocol, for_list} + end + end + + %Env{ + functions: macro_env.functions, + macros: macro_env.macros, + requires: macro_env.requires, + aliases: macro_env.aliases, + macro_aliases: macro_env.macro_aliases, + module: macro_env.module, + function: macro_env.function, + context_modules: macro_env.context_modules, + context: macro_env.context, + vars: vars, + versioned_vars: versioned_vars, + attributes: current_attributes, + behaviours: current_behaviours, + typespec: state.typespec, + scope_id: current_scope_id, + protocol: current_protocol + } + end + + def add_cursor_env(%__MODULE__{} = state, meta, macro_env) do + env = get_current_env(state, macro_env) + %__MODULE__{state | cursor_env: {meta, env}} + end + + def update_closest_env(%__MODULE__{cursor_position: cursor_position} = state, meta, macro_env) + when is_list(meta) and cursor_position != nil do + case Keyword.get(meta, :line, 0) do + line when is_integer(line) and line > 0 -> + column = Keyword.get(meta, :column, 0) + + {cursor_line, cursor_column} = cursor_position + + line_distance = abs(cursor_line - line) + column_distance = abs(cursor_column - column) + + store = + case state.closest_env do + nil -> + true + + {_, {old_line_distance, old_column_distance}, _} -> + line_distance < old_line_distance or + (line_distance == old_line_distance and column_distance < old_column_distance) + end + + if store do + env = get_current_env(state, macro_env) + + %__MODULE__{ + state + | closest_env: {{line, column}, {line_distance, column_distance}, env} + } + else + state + end + + _ -> + state + end + end + + def update_closest_env(%__MODULE__{} = state, _meta, _macro_env) do + state + end + + def add_current_env_to_line(%__MODULE__{} = state, meta, macro_env) when is_list(meta) do + do_add_current_env_to_line(state, Keyword.get(meta, :line, 0), macro_env) + end + + defp do_add_current_env_to_line(%__MODULE__{} = state, line, macro_env) + when is_integer(line) and line > 0 do + env = get_current_env(state, macro_env) + %__MODULE__{state | lines_to_env: Map.put(state.lines_to_env, line, env)} + end + + defp do_add_current_env_to_line(%__MODULE__{} = state, _line, _macro_env), do: state + + def add_moduledoc_positions( + %__MODULE__{} = state, + env, + meta + ) do + module = env.module + + case Keyword.get(meta, :end_of_expression) do + nil -> + state + + end_of_expression -> + line_to_insert_alias = Keyword.fetch!(end_of_expression, :line) + 1 + column = Keyword.get(meta, :column, 1) + + %__MODULE__{ + state + | moduledoc_positions: + Map.put(state.moduledoc_positions, module, {line_to_insert_alias, column}) + } + end + end + + def add_first_alias_positions( + %__MODULE__{} = state, + %{module: module, function: nil}, + meta + ) do + # TODO shouldn't that look for end_of_expression + line = Keyword.get(meta, :line, 0) + + if line > 0 do + column = Keyword.get(meta, :column, 1) + + %__MODULE__{ + state + | first_alias_positions: Map.put_new(state.first_alias_positions, module, {line, column}) + } + else + state + end + end + + def add_first_alias_positions(%__MODULE__{} = state, _env, _meta), do: state + + def add_call_to_line( + %__MODULE__{} = state, + {{:@, _meta, [{name, _name_meta, nil}]}, func, arity}, + meta + ) + when is_atom(name) do + do_add_call_to_line(state, {{:attribute, name}, func, arity}, meta) + end + + def add_call_to_line( + %__MODULE__{} = state, + {{name, var_meta, args}, func, arity}, + meta + ) + when is_atom(name) and is_atom(args) and + name not in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__, :_] do + do_add_call_to_line( + state, + {{:variable, name, Keyword.get(var_meta, :version, :any)}, func, arity}, + meta + ) + end + + def add_call_to_line( + %__MODULE__{} = state, + {nil, {:@, _meta, [{name, _name_meta, _args}]}, arity}, + meta + ) + when is_atom(name) do + do_add_call_to_line(state, {nil, {:attribute, name}, arity}, meta) + end + + def add_call_to_line( + %__MODULE__{} = state, + {nil, {name, var_meta, args}, arity}, + meta + ) + when is_atom(name) and is_atom(args) and + name not in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__, :_] do + do_add_call_to_line( + state, + {nil, {:variable, name, Keyword.get(var_meta, :version, :any)}, arity}, + meta + ) + end + + def add_call_to_line(state, call, meta) do + do_add_call_to_line(state, call, meta) + end + + defp do_add_call_to_line(%__MODULE__{} = state, {mod, func, arity}, meta) do + # extract_position is not suitable here, we need to handle invalid lines + line = Keyword.get(meta, :line, 0) + column = Keyword.get(meta, :column, nil) + + column = + if column do + column + Keyword.get(meta, :column_correction, 0) + end + + call = %CallInfo{mod: mod, func: func, arity: arity, position: {line, column}} + + calls = + Map.update(state.calls, line, [call], fn line_calls -> + [call | line_calls] + end) + + %__MODULE__{state | calls: calls} + end + + defp add_struct(%__MODULE__{} = state, env, type, fields) do + structs = + state.structs + |> Map.put(env.module, %StructInfo{type: type, fields: fields ++ [__struct__: env.module]}) + + %__MODULE__{state | structs: structs} + end + + defp get_current_attributes(%__MODULE__{} = state) do + state.scope_attributes |> :lists.reverse() |> List.flatten() + end + + defp add_mod_fun_to_position( + %__MODULE__{} = state, + {module, fun, arity}, + position, + end_position, + params, + type, + doc, + meta, + options + ) do + current_info = Map.get(state.mods_funs_to_positions, {module, fun, arity}, %ModFunInfo{}) + current_params = current_info |> Map.get(:params, []) + current_positions = current_info |> Map.get(:positions, []) + current_end_positions = current_info |> Map.get(:end_positions, []) + new_params = [params | current_params] + new_positions = [position | current_positions] + new_end_positions = [end_position | current_end_positions] + + overridable = current_info |> Map.get(:overridable, false) + + meta = + if overridable do + Map.put(meta, :overridable, true) + else + meta + end + + info = %ModFunInfo{ + positions: new_positions, + end_positions: new_end_positions, + params: new_params, + type: type, + doc: doc, + meta: meta, + generated: [Keyword.get(options, :generated, false) | current_info.generated], + overridable: overridable + } + + info = + Enum.reduce(options, info, fn option, acc -> process_option(state, acc, type, option) end) + + mods_funs_to_positions = Map.put(state.mods_funs_to_positions, {module, fun, arity}, info) + + %__MODULE__{state | mods_funs_to_positions: mods_funs_to_positions} + end + + defp process_option( + _state, + info, + :defdelegate, + {:target, {target, as, _as_arity}} + ) do + # TODO remove this and rely on meta + + %ModFunInfo{ + info + | target: {target, as} + } + end + + defp process_option(_state, info, _, {:overridable, {true, module}}) do + %ModFunInfo{ + info + | overridable: {true, module}, + meta: Map.put(info.meta, :overridable, true) + } + end + + defp process_option(_state, info, _type, _option), do: info + + def add_module(%__MODULE__{} = state) do + %__MODULE__{ + state + | doc_context: [[] | state.doc_context], + typedoc_context: [[] | state.typedoc_context], + optional_callbacks_context: [[] | state.optional_callbacks_context] + } + end + + def remove_module(%__MODULE__{} = state) do + %{ + state + | doc_context: tl(state.doc_context), + typedoc_context: tl(state.typedoc_context), + optional_callbacks_context: tl(state.optional_callbacks_context) + } + end + + def register_optional_callbacks(%__MODULE__{} = state, list) do + [_ | rest] = state.optional_callbacks_context + %{state | optional_callbacks_context: [list | rest]} + end + + def apply_optional_callbacks(%__MODULE__{} = state, env) do + [list | _rest] = state.optional_callbacks_context + module = env.module + + updated_specs = + list + |> Enum.reduce(state.specs, fn {fun, arity}, acc -> + acc + |> Map.update!({module, fun, arity}, fn spec_info = %SpecInfo{} -> + %{spec_info | meta: spec_info.meta |> Map.put(:optional, true)} + end) + end) + + %{state | specs: updated_specs} + end + + def add_module_to_index(%__MODULE__{} = state, module, {position, end_position}, options) + when (is_tuple(position) and is_tuple(end_position)) or is_nil(end_position) do + # TODO :defprotocol, :defimpl? + add_mod_fun_to_position( + state, + {module, nil, nil}, + position, + end_position, + nil, + :defmodule, + "", + %{}, + options + ) + end + + def add_func_to_index( + %__MODULE__{} = state, + env, + func, + params, + {position, end_position}, + type, + options \\ [] + ) + when (is_tuple(position) and is_tuple(end_position)) or is_nil(end_position) do + current_module = env.module + arity = length(params) + + {state, {doc, meta}} = + if Keyword.get(options, :generated, false) do + # do not consume docs on generated functions + {state, {"", %{generated: true}}} + else + consume_doc_context(state) + end + + hidden = Map.get(meta, :hidden) + + # underscored and @impl defs are hidden by default unless they have @doc + meta = + if (String.starts_with?(to_string(func), "_") or hidden == :impl) and doc == "" do + Map.put(meta, :hidden, true) + else + if hidden != true do + Map.delete(meta, :hidden) + else + meta + end + end + + meta = + if type == :defdelegate do + {target, as, as_arity} = options[:target] + + Map.put( + meta, + :delegate_to, + {target, as, as_arity} + ) + else + meta + end + + meta = + if type in [:defguard, :defguardp] do + Map.put(meta, :guard, true) + else + meta + end + + doc = + if type in [:defp, :defmacrop, :defguardp] do + # documentation is discarded on private + "" + else + doc + end + + add_mod_fun_to_position( + state, + {current_module, func, arity}, + position, + end_position, + params, + type, + doc, + meta, + options + ) + end + + def make_overridable( + %__MODULE__{} = state, + env, + fa_list, + overridable_module + ) do + module = env.module + + mods_funs_to_positions = + fa_list + |> Enum.reduce(state.mods_funs_to_positions, fn {f, a}, mods_funs_to_positions_acc -> + if Map.has_key?(mods_funs_to_positions_acc, {module, f, a}) do + mods_funs_to_positions_acc + |> make_def_overridable({module, f, a}, overridable_module) + else + # Some behaviour callbacks can be not implemented by __using__ macro + mods_funs_to_positions_acc + end + end) + + %__MODULE__{state | mods_funs_to_positions: mods_funs_to_positions} + end + + defp make_def_overridable(mods_funs_to_positions, mfa, overridable_module) do + update_in(mods_funs_to_positions[mfa], fn mod_fun_info = %ModFunInfo{} -> + %ModFunInfo{ + mod_fun_info + | overridable: {true, overridable_module}, + meta: Map.put(mod_fun_info.meta, :overridable, true) + } + end) + end + + def new_vars_scope(%__MODULE__{} = state) do + scope_id = state.scope_id_count + 1 + + %__MODULE__{ + state + | scope_ids: [scope_id | state.scope_ids], + scope_id_count: scope_id, + vars_info: [hd(state.vars_info) | state.vars_info] + } + end + + def new_func_vars_scope(%__MODULE__{} = state) do + scope_id = state.scope_id_count + 1 + + %__MODULE__{ + state + | scope_ids: [scope_id | state.scope_ids], + scope_id_count: scope_id, + vars_info: [%{} | state.vars_info], + # elixir_ex entries + # each def starts versioning from 0 + unused: 0, + vars: {%{}, false} + } + end + + def new_attributes_scope(%__MODULE__{} = state) do + %__MODULE__{state | attributes: [[] | state.attributes], scope_attributes: [[]]} + end + + def remove_vars_scope( + %__MODULE__{} = state, + %{vars: vars, unused: unused}, + restore_version_counter \\ false + ) do + state = maybe_move_vars_to_outer_scope(state) + + state = %__MODULE__{ + state + | scope_ids: tl(state.scope_ids), + vars_info: tl(state.vars_info), + vars_info_per_scope_id: update_vars_info_per_scope_id(state), + # restore elixir_ex fields + vars: vars + } + + if restore_version_counter do + # this is used by defmodule as module body does not affect outside versioning + %__MODULE__{ + state + | unused: unused + } + else + state + end + end + + def remove_func_vars_scope(%__MODULE__{} = state, %{vars: vars, unused: unused}) do + %__MODULE__{ + state + | scope_ids: tl(state.scope_ids), + vars_info: tl(state.vars_info), + vars_info_per_scope_id: update_vars_info_per_scope_id(state), + # restore elixir_ex fields + vars: vars, + # restore versioning + unused: unused + } + end + + # TODO check if we can get rid of this function + defp update_vars_info_per_scope_id(state) do + [scope_id | _other_scope_ids] = state.scope_ids + [current_scope_vars | _other_scope_vars] = state.vars_info + + for {scope_id, vars} <- state.vars_info_per_scope_id, into: %{} do + updated_vars = + for {key, var} <- vars, into: %{} do + updated_var = + case Map.get(current_scope_vars, key) do + nil -> + var + + scope_var -> + if hd(scope_var.positions) == hd(var.positions) do + scope_var + else + var + end + end + + {key, updated_var} + end + + {scope_id, updated_vars} + end + |> Map.put(scope_id, current_scope_vars) + end + + def reset_read(%{vars: {_, write}} = s, %{vars: {read, _}}) do + %{s | vars: {read, write}} + end + + def prepare_write(%{vars: {read, _}} = s) do + %{s | vars: {read, read}} + end + + def close_write(%{vars: {_read, write}} = s, %{vars: {_, false}}) do + %{s | vars: {write, false}} + end + + def close_write(%{vars: {_read, write}} = s, %{vars: {_, upper_write}}) do + %{s | vars: {write, merge_vars(upper_write, write)}} + end + + defp merge_vars(v, v), do: v + + defp merge_vars(v1, v2) do + :maps.fold( + fn k, m2, acc -> + case Map.fetch(acc, k) do + {:ok, m1} when m1 >= m2 -> acc + _ -> Map.put(acc, k, m2) + end + end, + v1, + v2 + ) + end + + def remove_attributes_scope(%__MODULE__{} = state) do + attributes = tl(state.attributes) + %__MODULE__{state | attributes: attributes, scope_attributes: attributes} + end + + def add_type( + %__MODULE__{} = state, + env, + type_name, + type_args, + spec, + kind, + {pos, end_pos}, + options \\ [] + ) do + arg_names = + type_args + |> Enum.map(&Macro.to_string/1) + + {state, {doc, meta}} = consume_typedoc_context(state) + + # underscored types are hidden by default unless they have @typedoc + meta = + if String.starts_with?(to_string(type_name), "_") and doc == "" do + Map.put(meta, :hidden, true) + else + meta + end + + meta = + if kind == :opaque do + Map.put(meta, :opaque, true) + else + meta + end + + doc = + if kind == :typep do + # documentation is discarded on private + "" + else + doc + end + + type_info = %TypeInfo{ + name: type_name, + args: [arg_names], + kind: kind, + specs: [spec], + generated: [Keyword.get(options, :generated, false)], + positions: [pos], + end_positions: [end_pos], + doc: doc, + meta: meta + } + + current_module = env.module + + types = + state.types + |> Map.put({current_module, type_name, length(arg_names)}, type_info) + + %__MODULE__{state | types: types} + end + + defp combine_specs(nil, new), do: new + + defp combine_specs(%SpecInfo{} = existing, %SpecInfo{} = new) do + %SpecInfo{ + existing + | positions: [hd(new.positions) | existing.positions], + end_positions: [hd(new.end_positions) | existing.end_positions], + generated: [hd(new.generated) | existing.generated], + args: [hd(new.args) | existing.args], + specs: [hd(new.specs) | existing.specs] + } + end + + def add_spec( + %__MODULE__{} = state, + env, + type_name, + type_args, + spec, + kind, + {pos, end_pos}, + options \\ [] + ) do + arg_names = + type_args + |> Enum.map(&Macro.to_string/1) + + {state, {doc, meta}} = + if kind in [:callback, :macrocallback] do + consume_doc_context(state) + else + # do not consume doc context for specs + {state, {"", %{}}} + end + + # underscored callbacks are hidden by default unless they have @doc + meta = + if String.starts_with?(to_string(type_name), "_") and doc == "" do + Map.put(meta, :hidden, true) + else + meta + end + + type_info = %SpecInfo{ + name: type_name, + args: [arg_names], + specs: [spec], + kind: kind, + generated: [Keyword.get(options, :generated, false)], + positions: [pos], + end_positions: [end_pos], + doc: doc, + meta: meta + } + + current_module = env.module + + arity_info = + combine_specs(state.specs[{current_module, type_name, length(arg_names)}], type_info) + + specs = + state.specs + |> Map.put({current_module, type_name, length(arg_names)}, arity_info) + + %__MODULE__{state | specs: specs} + end + + def add_var_write(%__MODULE__{} = state, {name, meta, _}) when name != :_ do + version = meta[:version] + scope_id = hd(state.scope_ids) + + info = %VarInfo{ + name: name, + version: version, + positions: [extract_position(meta)], + scope_id: scope_id + } + + [vars_from_scope | other_vars] = state.vars_info + vars_from_scope = Map.put(vars_from_scope, {name, version}, info) + + %__MODULE__{ + state + | vars_info: [vars_from_scope | other_vars] + } + end + + def add_var_write(%__MODULE__{} = state, _), do: state + + def add_var_read(%__MODULE__{} = state, {name, meta, _}) when name != :_ do + version = meta[:version] + + [vars_from_scope | other_vars] = state.vars_info + + case Map.get(vars_from_scope, {name, version}) do + nil -> + state + + info -> + info = %VarInfo{ + info + | positions: (info.positions ++ [extract_position(meta)]) |> Enum.uniq() + } + + vars_from_scope = Map.put(vars_from_scope, {name, version}, info) + + %__MODULE__{ + state + | vars_info: [vars_from_scope | other_vars] + } + end + end + + def add_var_read(%__MODULE__{} = state, _), do: state + + def add_attribute(%__MODULE__{} = state, env, attribute, meta, args, type, is_definition) do + position = extract_position(meta) + [attributes_from_scope | other_attributes] = state.attributes + + existing_attribute_index = + attributes_from_scope + |> Enum.find_index(&(&1.name == attribute)) + + attributes_from_scope = + case existing_attribute_index do + nil -> + if is_definition do + [ + %AttributeInfo{ + name: attribute, + type: type, + positions: [position] + } + | attributes_from_scope + ] + else + attributes_from_scope + end + + index -> + attributes_from_scope + |> List.update_at(index, fn existing -> + type = if is_definition, do: type, else: existing.type + + %AttributeInfo{ + existing + | # TODO this is wrong for accumulating attributes + type: type, + positions: (existing.positions ++ [position]) |> Enum.uniq() |> Enum.sort() + } + end) + end + + attributes = [attributes_from_scope | other_attributes] + scope_attributes = [attributes_from_scope | tl(state.scope_attributes)] + + # TODO handle other + # {moduledoc, nil, nil, []}, + # {after_compile, [], accumulate, []}, + # {after_verify, [], accumulate, []}, + # {before_compile, [], accumulate, []}, + # {behaviour, [], accumulate, []}, + # {compile, [], accumulate, []}, + # {derive, [], accumulate, []}, + # {dialyzer, [], accumulate, []}, + # {external_resource, [], accumulate, []}, + # {on_definition, [], accumulate, []}, + # {type, [], accumulate, []}, + # {opaque, [], accumulate, []}, + # {typep, [], accumulate, []}, + # {spec, [], accumulate, []}, + # {callback, [], accumulate, []}, + # {macrocallback, [], accumulate, []}, + # {optional_callbacks, [], accumulate, []}, + accumulating? = + attribute in [:before_compile, :after_compile, :after_verify, :on_definition, :on_load] + + attribute_store = + if is_definition do + [arg] = args + + if accumulating? do + state.attribute_store |> Map.update({env.module, attribute}, [arg], &(&1 ++ [arg])) + else + state.attribute_store |> Map.put({env.module, attribute}, arg) + end + else + state.attribute_store + end + + %__MODULE__{ + state + | attributes: attributes, + scope_attributes: scope_attributes, + attribute_store: attribute_store + } + end + + def add_behaviour(module, %__MODULE__{} = state, env) when is_atom(module) do + state = + update_in(state.behaviours[env.module], &Enum.uniq([module | &1 || []])) + + {module, state, env} + end + + def add_behaviour(_module, %__MODULE__{} = state, env), do: {nil, state, env} + + def register_doc(%__MODULE__{} = state, env, :moduledoc, doc_arg) do + current_module = env.module + doc_arg_formatted = format_doc_arg(doc_arg) + + mods_funs_to_positions = + state.mods_funs_to_positions + |> Map.update!({current_module, nil, nil}, fn info = %ModFunInfo{} -> + case doc_arg_formatted do + {:meta, meta} -> + %{info | meta: Map.merge(info.meta, meta)} + + text_or_hidden -> + %{info | doc: text_or_hidden} + end + end) + + %{state | mods_funs_to_positions: mods_funs_to_positions} + end + + def register_doc(%__MODULE__{} = state, _env, :doc, doc_arg) do + [doc_context | doc_context_rest] = state.doc_context + + %{state | doc_context: [[doc_arg | doc_context] | doc_context_rest]} + end + + def register_doc(%__MODULE__{} = state, _env, :typedoc, doc_arg) do + [doc_context | doc_context_rest] = state.typedoc_context + + %{state | typedoc_context: [[doc_arg | doc_context] | doc_context_rest]} + end + + defp consume_doc_context(%__MODULE__{} = state) do + [doc_context | doc_context_rest] = state.doc_context + state = %{state | doc_context: [[] | doc_context_rest]} + + {state, reduce_doc_context(doc_context)} + end + + defp consume_typedoc_context(%__MODULE__{} = state) do + [doc_context | doc_context_rest] = state.typedoc_context + state = %{state | typedoc_context: [[] | doc_context_rest]} + + {state, reduce_doc_context(doc_context)} + end + + defp reduce_doc_context(doc_context) do + Enum.reduce(doc_context, {"", %{}}, fn doc_arg, {doc_acc, meta_acc} -> + case format_doc_arg(doc_arg) do + {:meta, meta} -> {doc_acc, Map.merge(meta_acc, meta)} + doc -> {doc, meta_acc} + end + end) + end + + defp format_doc_arg(binary) when is_binary(binary), do: binary + + defp format_doc_arg(list) when is_list(list) do + # TODO pass env and expand metadata + if Keyword.keyword?(list) do + {:meta, Map.new(list)} + else + to_string(list) + end + end + + defp format_doc_arg(false), do: {:meta, %{hidden: true}} + defp format_doc_arg(:impl), do: {:meta, %{hidden: :impl}} + + defp format_doc_arg(quoted) do + try do + # TODO pass env to eval + case Code.eval_quoted(quoted) do + {binary, _} when is_binary(binary) -> + binary + + {list, _} when is_list(list) -> + if Keyword.keyword?(list) do + {:meta, Map.new(list)} + else + to_string(list) + end + + other -> + Logger.warning(""" + Unable to format docstring expression: + + #{inspect(quoted, pretty: true)} + + Eval resulted in: + + #{inspect(other)} + """) + + "" + end + rescue + e -> + Logger.warning(""" + Unable to format docstring expression: + + #{inspect(quoted, pretty: true)} + + #{Exception.format(:error, e, __STACKTRACE__)} + """) + + "" + end + end + + defp maybe_move_vars_to_outer_scope( + %__MODULE__{vars_info: [current_scope_vars, outer_scope_vars | other_scopes_vars]} = + state + ) do + outer_scope_vars = + for {key, _} <- outer_scope_vars, + into: %{} do + # take type from outer scope as type narrowing in inner scope is not guaranteed to + # affect outer scope + type = outer_scope_vars[key].type + {key, %{current_scope_vars[key] | type: type}} + end + + vars_info = [current_scope_vars, outer_scope_vars | other_scopes_vars] + + %__MODULE__{state | vars_info: vars_info} + end + + defp maybe_move_vars_to_outer_scope(state), do: state + + @module_functions [ + {:__info__, [:atom], :def}, + {:module_info, [], :def}, + {:module_info, [:atom], :def} + ] + + def add_module_functions(state, env, functions, range) do + {line, column} = + case range do + {{line, column}, _} -> {line, column} + _ -> {0, nil} + end + + meta = [line: line] ++ if(column > 0, do: [column: column], else: []) + + (functions ++ @module_functions) + |> Enum.reduce(state, fn {name, args, kind}, acc -> + mapped_args = for arg <- args, do: {arg, meta, nil} + + acc + |> add_func_to_index( + env, + name, + mapped_args, + range, + kind, + generated: true + ) + end) + end + + def with_typespec(%__MODULE__{} = state, typespec) do + %{state | typespec: typespec} + end + + def add_struct_or_exception(state, env, type, fields, range) do + {line, column} = + case range do + {{line, column}, _} -> {line, column} + _ -> {0, nil} + end + + meta = [line: line || 0] ++ if(column > 0, do: [column: column], else: []) + + fields = + fields ++ + if type == :defexception do + [__exception__: true] + else + [] + end + + options = [generated: true] + + state = + if type == :defexception do + {_, state, env} = add_behaviour(Exception, state, env) + + if Keyword.has_key?(fields, :message) do + state + |> add_func_to_index( + env, + :exception, + [{:msg, meta, nil}], + range, + :def, + options + ) + |> add_func_to_index( + env, + :message, + [{:exception, meta, nil}], + range, + :def, + options + ) + else + state + end + |> add_func_to_index( + env, + :exception, + [{:args, meta, nil}], + range, + :def, + options + ) + else + state + end + |> add_func_to_index(env, :__struct__, [], range, :def, options) + |> add_func_to_index( + env, + :__struct__, + [{:kv, meta, nil}], + range, + :def, + options + ) + + state + |> add_struct(env, type, fields) + end + + def generate_protocol_callbacks(state, env) do + # turn specs into callbacks or create dummy callbacks + builtins = BuiltinFunctions.all() |> Keyword.keys() + + current_module = env.module + + keys = + state.mods_funs_to_positions + |> Enum.filter(fn + {{^current_module, name, _arity}, info} when not is_nil(name) -> + name not in builtins and info.type == :def + + _ -> + false + end) + + new_specs = + for {key = {_mod, name, _arity}, mod_fun_info} <- keys, + into: %{}, + do: + ( + new_spec = + case state.specs[key] do + nil -> + %ModFunInfo{positions: positions, params: params} = mod_fun_info + + args = + for param_variant <- params do + case tl(param_variant) do + [] -> ["t()"] + other -> ["t()" | Enum.map(other, fn _ -> "term()" end)] + end + end + + specs = + for arg <- args do + joined = Enum.join(arg, ", ") + "@callback #{name}(#{joined}) :: term()" + end + + %SpecInfo{ + name: name, + args: args, + specs: specs, + kind: :callback, + positions: positions, + end_positions: Enum.map(positions, fn _ -> nil end), + generated: Enum.map(positions, fn _ -> true end) + } + + spec = %SpecInfo{specs: specs} -> + %SpecInfo{ + spec + | # TODO :spec will get replaced here, refactor into array + kind: :callback, + specs: + specs + |> Enum.map(fn s -> + String.replace_prefix(s, "@spec", "@callback") + end) + |> Kernel.++(specs) + } + end + + {key, new_spec} + ) + + specs = Map.merge(state.specs, new_specs) + + %{state | specs: specs} + end + + def maybe_add_protocol_behaviour(%{protocol: {protocol, _}} = state, env) do + {_, state, env} = add_behaviour(protocol, state, env) + {state, env} + end + + def maybe_add_protocol_behaviour(state, env), do: {state, env} + + def merge_inferred_types(state, []), do: state + + def merge_inferred_types(state, inferred_types) do + [h | t] = state.vars_info + + h = + for {key, type} <- inferred_types, reduce: h do + acc -> + Map.update!(acc, key, fn %VarInfo{type: old} = v -> + %{v | type: ElixirSense.Core.TypeInference.intersect(old, type)} + end) + end + + %{state | vars_info: [h | t]} + end + + def extract_position(meta) do + line = Keyword.get(meta, :line, 0) + + if line <= 0 do + {1, 1} + else + { + line, + Keyword.get(meta, :column, 1) + } + end + end + + def extract_range(meta) do + line = Keyword.get(meta, :line, 0) + + if line <= 0 do + {{1, 1}, nil} + else + position = { + line, + Keyword.get(meta, :column, 1) + } + + end_position = + case meta[:end] do + nil -> + case meta[:end_of_expression] do + nil -> + nil + + end_of_expression_meta -> + { + Keyword.fetch!(end_of_expression_meta, :line), + Keyword.fetch!(end_of_expression_meta, :column) + } + end + + end_meta -> + { + Keyword.fetch!(end_meta, :line), + Keyword.fetch!(end_meta, :column) + 3 + } + end + + {position, end_position} + end + end +end diff --git a/lib/elixir_sense/core/compiler/typespec.ex b/lib/elixir_sense/core/compiler/typespec.ex new file mode 100644 index 00000000..bf7de478 --- /dev/null +++ b/lib/elixir_sense/core/compiler/typespec.ex @@ -0,0 +1,629 @@ +defmodule ElixirSense.Core.Compiler.Typespec do + alias ElixirSense.Core.Compiler + alias ElixirSense.Core.Compiler.Utils + alias ElixirSense.Core.Compiler.State + def spec_to_signature({:when, _, [spec, _]}), do: type_to_signature(spec) + def spec_to_signature(other), do: type_to_signature(other) + + def type_to_signature({:"::", _, [{name, _, context}, _]}) + when is_atom(name) and name != :"::" and is_atom(context), + do: {name, []} + + def type_to_signature({:"::", _, [{:__cursor__, _, args}, _]}) + when is_list(args) do + # type name replaced by cursor + {:__unknown__, []} + end + + def type_to_signature({:"::", _, [{name, _, args}, _]}) + when is_atom(name) and name != :"::", + do: {name, args} + + def type_to_signature({:__cursor__, _, args}) when is_list(args) do + # type name replaced by cursor + {:__unknown__, []} + end + + def type_to_signature({name, _, args}) when is_atom(name) and name != :"::" do + # elixir returns :error here, we handle incomplete signatures + {name, args} + end + + def type_to_signature(_), do: {:__unknown__, []} + + def expand_spec(ast, state, env) do + # unless there are unquotes module vars are not accessible + state_orig = state + + unless Compiler.Quote.has_unquotes(ast) do + {ast, state, env} = do_expand_spec(ast, State.new_func_vars_scope(state), env) + + {ast, State.remove_func_vars_scope(state, state_orig), env} + else + {ast, state, env} = do_expand_spec(ast, State.new_vars_scope(state), env) + + {ast, State.remove_vars_scope(state, state_orig), env} + end + end + + defp do_expand_spec({:when, meta, [spec, guard]}, state, env) do + {spec, guard, state, env} = do_expand_spec(spec, guard, meta, state, env) + {{:when, meta, [spec, guard]}, state, env} + end + + defp do_expand_spec(spec, state, env) do + {spec, _guard, state, env} = do_expand_spec(spec, [], [], state, env) + {spec, state, env} + end + + defp do_expand_spec( + {:"::", meta, [{name, name_meta, args}, return]}, + guard, + guard_meta, + state, + env + ) + when is_atom(name) and name != :"::" do + args = + if is_atom(args) do + [] + else + args + end + |> sanitize_args() + + {_, state} = expand_typespec({name, name_meta, args}, :disabled, state, env) + + {guard, state, env} = + if is_list(guard) do + {guard, state, env} + else + # invalid guard may still have cursor + {_, state} = expand_typespec(guard, :disabled, state, env) + {[], state, env} + end + + {state, var_names} = + Enum.reduce(guard, {state, []}, fn + {name, _val}, {state, var_names} when is_atom(name) -> + # guard is a keyword list so we don't have exact meta on keys + {State.add_var_write(state, {name, guard_meta, nil}), [name | var_names]} + + _, acc -> + # invalid entry + acc + end) + + {args_reverse, state} = + Enum.reduce(args, {[], state}, fn + arg, {acc, state} -> + {arg, state} = expand_typespec(arg, var_names, state, env) + {[arg | acc], state} + end) + + args = Enum.reverse(args_reverse) + + {return, state} = expand_typespec(return, var_names, state, env) + + {guard_reverse, state} = + Enum.reduce(guard, {[], state}, fn + {name, {:var, _, context}} = pair, {acc, state} when is_atom(name) and is_atom(context) -> + # special type var + {[pair | acc], state} + + {name, type}, {acc, state} when is_atom(name) -> + {type, state} = expand_typespec(type, var_names, state, env) + {[{name, type} | acc], state} + + other, {acc, state} -> + # there may be cursor in invalid entries + {_type, state} = expand_typespec(other, var_names, state, env) + {acc, state} + end) + + guard = Enum.reverse(guard_reverse) + + {{:"::", meta, [{name, name_meta, args}, return]}, guard, state, env} + end + + defp do_expand_spec(other, guard, guard_meta, state, env) do + case other do + {:"::", meta, [{{:unquote, _, unquote_args}, meta1, call_args}, definition]} -> + # replace unquote fragment and try to expand args to find variables + {_, state, env} = Compiler.expand(unquote_args, state, env) + + do_expand_spec( + {:"::", meta, [{:__unknown__, meta1, call_args}, definition]}, + guard, + guard_meta, + state, + env + ) + + {name, meta, args} when is_atom(name) and name != :"::" -> + # invalid or incomplete spec + # try to wrap in :: expression + do_expand_spec({:"::", meta, [{name, meta, args}, nil]}, guard, guard_meta, state, env) + + _ -> + # there may be cursor in invalid entries + {_type, state} = expand_typespec(guard, [], state, env) + {_type, state} = expand_typespec(other, [], state, env) + {other, guard, state, env} + end + end + + defp sanitize_args(args) do + Enum.map(args, fn + {:"::", meta, [left, right]} -> + {:"::", meta, [remove_default(left), remove_default(right)]} + + other -> + remove_default(other) + end) + end + + defp remove_default({:\\, _, [left, _]}), do: left + defp remove_default(other), do: other + + def expand_type(ast, state, env) do + # unless there are unquotes module vars are not accessible + state_orig = state + + unless Compiler.Quote.has_unquotes(ast) do + {ast, state, env} = do_expand_type(ast, State.new_func_vars_scope(state), env) + + {ast, State.remove_func_vars_scope(state, state_orig), env} + else + {ast, state, env} = do_expand_type(ast, State.new_vars_scope(state), env) + + {ast, State.remove_vars_scope(state, state_orig), env} + end + end + + defp do_expand_type({:"::", meta, [{name, name_meta, args}, definition]}, state, env) + when is_atom(name) and name != :"::" do + args = + if is_atom(args) do + [] + else + args + end + + {_, state} = expand_typespec({name, name_meta, args}, :disabled, state, env) + + {state, var_names} = + Enum.reduce(args, {state, []}, fn + {name, meta, context}, {state, var_names} + when is_atom(name) and is_atom(context) and name != :_ -> + {State.add_var_write(state, {name, meta, context}), [name | var_names]} + + _other, acc -> + # silently skip invalid typespec params + acc + end) + + {definition, state} = expand_typespec(definition, var_names, state, env) + {{:"::", meta, [{name, name_meta, args}, definition]}, state, env} + end + + defp do_expand_type(other, state, env) do + case other do + {:"::", meta, [{{:unquote, _, unquote_args}, meta1, call_args}, definition]} -> + # replace unquote fragment and try to expand args to find variables + {_, state, env} = Compiler.expand(unquote_args, state, env) + do_expand_type({:"::", meta, [{:__unknown__, meta1, call_args}, definition]}, state, env) + + {name, meta, args} when is_atom(name) and name != :"::" -> + # invalid or incomplete type + # try to wrap in :: expression + do_expand_type({:"::", meta, [{name, meta, args}, nil]}, state, env) + + _ -> + # there may be cursor in invalid entries + {_type, state} = expand_typespec(other, [], state, env) + {other, state, env} + end + end + + def expand_typespec(ast, var_names \\ [], state, env) do + typespec(ast, var_names, env, state) + end + + # TODO Remove char_list type by v2.0 + def built_in_type?(:char_list, 0), do: true + def built_in_type?(:charlist, 0), do: true + def built_in_type?(:as_boolean, 1), do: true + def built_in_type?(:struct, 0), do: true + def built_in_type?(:nonempty_charlist, 0), do: true + def built_in_type?(:keyword, 0), do: true + def built_in_type?(:keyword, 1), do: true + def built_in_type?(:var, 0), do: true + def built_in_type?(name, arity), do: :erl_internal.is_type(name, arity) + + defp typespec({:__cursor__, meta, args}, vars, caller, state) when is_list(args) do + state = + unless state.cursor_env do + state + |> State.add_cursor_env(meta, caller) + else + state + end + + node = + case args do + [h | _] -> h + [] -> nil + end + + typespec(node, vars, caller, state) + end + + # Handle unions + defp typespec({:|, meta, [left, right]}, vars, caller, state) do + {left, state} = typespec(left, vars, caller, state) + {right, state} = typespec(right, vars, caller, state) + + {{:|, meta, [left, right]}, state} + end + + # Handle binaries + defp typespec({:<<>>, meta, args}, vars, caller, state) do + {args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args) + # elixir does complex binary spec validation + {{:<<>>, meta, args}, state} + end + + ## Handle maps and structs + defp typespec({:%{}, meta, fields}, vars, caller, state) do + fun = fn + {{:required, _meta2, [k]}, v}, state -> + {arg1, state} = typespec(k, vars, caller, state) + {arg2, state} = typespec(v, vars, caller, state) + {{arg1, arg2}, state} + + {{:optional, meta2, [k]}, v}, state -> + {arg1, state} = typespec(k, vars, caller, state) + {arg2, state} = typespec(v, vars, caller, state) + {{{:optional, meta2, [arg1]}, arg2}, state} + + {k, v}, state -> + {arg1, state} = typespec(k, vars, caller, state) + {arg2, state} = typespec(v, vars, caller, state) + {{arg1, arg2}, state} + + invalid, state -> + # elixir raises here invalid map specification + {_, state} = typespec(invalid, vars, caller, state) + {nil, state} + end + + {fields, state} = :lists.mapfoldl(fun, state, fields) + {{:%{}, meta, fields |> Enum.filter(&(&1 != nil))}, state} + end + + defp typespec({:%, struct_meta, [name, {:%{}, meta, fields}]}, vars, caller, state) do + case Compiler.Macro.expand(name, %{caller | function: {:__info__, 1}}) do + module when is_atom(module) -> + # TODO register alias/struct + struct = + Compiler.Map.load_struct(module, [], state, caller) + |> Map.delete(:__struct__) + |> Map.to_list() + + {fields, state} = + fields + |> Enum.reverse() + |> Enum.reduce({[], state}, fn + {k, v}, {fields, state} when is_atom(k) -> + {[{k, v} | fields], state} + + other, {fields, state} -> + # elixir raises expected key-value pairs in struct + {_, state} = typespec(other, vars, caller, state) + {fields, state} + end) + + types = + :lists.map( + fn + {:__exception__ = field, true} -> {field, Keyword.get(fields, field, true)} + {field, _} -> {field, Keyword.get(fields, field, quote(do: term()))} + end, + :lists.sort(struct) + ) + + # look for cursor in invalid fields + # elixir raises if there are any + state = + fields + |> Enum.filter(fn {field, _} -> not Keyword.has_key?(struct, field) end) + |> Enum.reduce(state, fn {_, type}, acc -> + {_, acc} = typespec(type, vars, caller, acc) + acc + end) + + {map, state} = typespec({:%{}, meta, types}, vars, caller, state) + {{:%, struct_meta, [module, map]}, state} + + other -> + # elixir raises here unexpected expression in typespec + {name, state} = typespec(other, vars, caller, state) + {map, state} = typespec({:%{}, meta, fields}, vars, caller, state) + {{:%, struct_meta, [name, map]}, state} + end + end + + # Handle records + defp typespec({:record, meta, [atom]}, vars, caller, state) do + typespec({:record, meta, [atom, []]}, vars, caller, state) + end + + defp typespec({:record, meta, [tag, field_specs]}, vars, caller, state) + when is_atom(tag) and is_list(field_specs) do + # We cannot set a function name to avoid tracking + # as a compile time dependency because for records it actually is one. + case Compiler.Macro.expand({tag, [], [{:{}, [], []}]}, caller) do + {_, _, [name, fields | _]} when is_list(fields) -> + types = + :lists.map( + fn {field, _} -> + {:"::", [], + [ + {field, [], nil}, + Keyword.get(field_specs, field, quote(do: term())) + ]} + end, + fields + ) + + # look for cursor in invalid fields + # elixir raises if there are any + state = + field_specs + |> Enum.filter(fn {field, _} -> not Keyword.has_key?(fields, field) end) + |> Enum.reduce(state, fn {_, type}, acc -> + {_, acc} = typespec(type, vars, caller, acc) + acc + end) + + typespec({:{}, meta, [name | types]}, vars, caller, state) + + _ -> + # elixir raises here + {field_specs, state} = + :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, field_specs) + + {{:record, meta, [tag, field_specs]}, state} + end + end + + # Handle ranges + defp typespec({:.., meta, [left, right]}, vars, caller, state) do + {left, state} = typespec(left, vars, caller, state) + {right, state} = typespec(right, vars, caller, state) + # elixir validates range here + + {{:.., meta, [left, right]}, state} + end + + # Handle special forms + defp typespec({:__MODULE__, _, atom}, vars, caller, state) when is_atom(atom) do + typespec(caller.module, vars, caller, state) + end + + defp typespec({:__aliases__, _, _} = alias, vars, caller, state) do + typespec(expand_remote(alias, caller), vars, caller, state) + end + + # Handle funs + defp typespec([{:->, meta, [args, return]}], vars, caller, state) + when is_list(args) do + {args, state} = fn_args(args, vars, caller, state) + {spec, state} = typespec(return, vars, caller, state) + + {[{:->, meta, [args, spec]}], state} + end + + # Handle type operator + defp typespec( + {:"::", meta, [{var_name, var_meta, context}, expr]}, + vars, + caller, + state + ) + when is_atom(var_name) and is_atom(context) do + # elixir warns if :: is nested + {right, state} = typespec(expr, vars, caller, state) + {{:"::", meta, [{var_name, var_meta, context}, right]}, state} + end + + defp typespec({:"::", meta, [left, right]}, vars, caller, state) do + # elixir warns here + # invalid type annotation. The left side of :: must be a variable + + {left, state} = typespec(left, vars, caller, state) + {right, state} = typespec(right, vars, caller, state) + {{:"::", meta, [left, right]}, state} + end + + # Handle remote calls in the form of @module_attribute.type. + # These are not handled by the general remote type clause as calling + # Macro.expand/2 on the remote does not expand module attributes (but expands + # things like __MODULE__). + defp typespec( + {{:., dot_meta, [{:@, attr_meta, [{attr, _, _}]}, name]}, meta, args}, + vars, + caller, + state + ) do + # TODO Module.get_attribute(caller.module, attr) + state = + state + |> State.add_attribute(caller, attr, attr_meta, nil, nil, false) + |> State.add_call_to_line({Kernel, :@, 0}, attr_meta) + + case Map.get(state.attribute_store, {caller.module, attr}) do + remote when is_atom(remote) and remote != nil -> + {remote_spec, state} = typespec(remote, vars, caller, state) + {name_spec, state} = typespec(name, vars, caller, state) + remote_type({{:., dot_meta, [remote_spec, name_spec]}, meta, args}, vars, caller, state) + + _ -> + # elixir raises here invalid remote in typespec + {name_spec, state} = typespec(name, vars, caller, state) + remote_type({{:., dot_meta, [nil, name_spec]}, meta, args}, vars, caller, state) + end + end + + # Handle remote calls + defp typespec({{:., dot_meta, [remote, name]}, meta, args}, vars, caller, state) do + remote = expand_remote(remote, caller) + + if remote == caller.module do + typespec({name, dot_meta, args}, vars, caller, state) + else + # elixir raises if remote is not atom + {remote_spec, state} = typespec(remote, vars, caller, state) + {name_spec, state} = typespec(name, vars, caller, state) + remote_type({{:., dot_meta, [remote_spec, name_spec]}, meta, args}, vars, caller, state) + end + end + + # Handle tuples + defp typespec({left, right}, vars, caller, state) do + {left, state} = typespec(left, vars, caller, state) + {right, state} = typespec(right, vars, caller, state) + {{left, right}, state} + end + + defp typespec({:{}, meta, args}, vars, caller, state) do + {args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args) + + {{:{}, meta, args}, state} + end + + # Handle blocks + defp typespec({:__block__, _meta, [arg]}, vars, caller, state) do + typespec(arg, vars, caller, state) + end + + # Handle variables or local calls + defp typespec({name, meta, atom}, :disabled, _caller, state) when is_atom(atom) do + {{name, meta, atom}, state} + end + + defp typespec({:_, meta, arg}, _vars, _caller, state) when not is_list(arg) do + {{:_, meta, arg}, state} + end + + defp typespec({name, meta, atom} = node, vars, caller, state) when is_atom(atom) do + if :lists.member(name, vars) do + state = State.add_var_read(state, node) + {{name, meta, atom}, state} + else + typespec({name, meta, []}, vars, caller, state) + end + end + + # handle unquote fragment + defp typespec({key, _, args}, _vars, caller, state) + when is_list(args) and key in [:unquote, :unquote_splicing] do + {_, state, _env} = Compiler.expand(args, state, caller) + {:__unknown__, state} + end + + # Handle local calls + defp typespec({name, meta, args}, :disabled, caller, state) when is_atom(name) do + {args, state} = :lists.mapfoldl(&typespec(&1, :disabled, caller, &2), state, args) + {{name, meta, args}, state} + end + + defp typespec({name, meta, args}, vars, caller, state) when is_atom(name) do + {args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args) + # elixir raises if type is not defined + + state = State.add_call_to_line(state, {nil, name, length(args)}, meta) + + {{name, meta, args}, state} + end + + # Handle literals + defp typespec(atom, _, _, state) when is_atom(atom) do + {atom, state} + end + + defp typespec(integer, _, _, state) when is_integer(integer) do + {integer, state} + end + + defp typespec([], _vars, _caller, state) do + {[], state} + end + + defp typespec([{:..., meta, _}], vars, caller, state) do + typespec({:nonempty_list, [], [{:any, meta, []}]}, vars, caller, state) + end + + defp typespec([spec, {:..., _, _}], vars, caller, state) do + typespec({:nonempty_list, [], [spec]}, vars, caller, state) + end + + defp typespec([spec], vars, caller, state) do + typespec({:list, [], [spec]}, vars, caller, state) + end + + defp typespec(list, vars, caller, state) when is_list(list) do + {list_reversed, state} = + Enum.reduce(list, {[], state}, fn + {k, v}, {acc, state} when is_atom(k) -> + {[{k, v} | acc], state} + + other, {acc, state} -> + # elixir raises on invalid list entries + {_, state} = typespec(other, vars, caller, state) + {acc, state} + end) + + case list_reversed do + [head | tail] -> + union = + :lists.foldl( + fn elem, acc -> {:|, [], [elem, acc]} end, + head, + tail + ) + + typespec({:list, [], [union]}, vars, caller, state) + + [] -> + {[], state} + end + end + + defp typespec(other, vars, caller, state) do + # elixir raises here unexpected expression in typespec + {_, state} = + if Utils.has_cursor?(other) do + typespec({:__cursor__, [], []}, vars, caller, state) + else + {nil, state} + end + + {nil, state} + end + + # TODO trace alias? + # TODO trace calls in expand + defdelegate expand_remote(other, env), to: ElixirSense.Core.Compiler.Macro, as: :expand + + defp remote_type({{:., dot_meta, [remote_spec, name_spec]}, meta, args}, vars, caller, state) do + {args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args) + state = State.add_call_to_line(state, {remote_spec, name_spec, length(args)}, meta) + {{{:., dot_meta, [remote_spec, name_spec]}, meta, args}, state} + end + + defp fn_args(args, vars, caller, state) do + :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args) + end +end diff --git a/lib/elixir_sense/core/compiler/utils.ex b/lib/elixir_sense/core/compiler/utils.ex new file mode 100644 index 00000000..6cc07090 --- /dev/null +++ b/lib/elixir_sense/core/compiler/utils.ex @@ -0,0 +1,98 @@ +defmodule ElixirSense.Core.Compiler.Utils do + def generated([{:generated, true} | _] = meta), do: meta + def generated(meta), do: [{:generated, true} | meta] + + def split_last([]), do: {[], []} + + def split_last(list), do: split_last(list, []) + + defp split_last([h], acc), do: {Enum.reverse(acc), h} + + defp split_last([h | t], acc), do: split_last(t, [h | acc]) + + def split_opts(args) do + case split_last(args) do + {outer_cases, outer_opts} when is_list(outer_opts) -> + case split_last(outer_cases) do + {inner_cases, inner_opts} when is_list(inner_opts) -> + {inner_cases, inner_opts ++ outer_opts} + + _ -> + {outer_cases, outer_opts} + end + + _ -> + {args, []} + end + end + + def get_line(opts) when is_list(opts) do + case Keyword.fetch(opts, :line) do + {:ok, line} when is_integer(line) -> line + _ -> 0 + end + end + + def extract_guards({:when, _, [left, right]}), do: {left, extract_or_guards(right)} + def extract_guards(term), do: {term, []} + + def extract_or_guards({:when, _, [left, right]}), do: [left | extract_or_guards(right)] + def extract_or_guards(term), do: [term] + + def select_with_cursor(ast_list) do + Enum.find(ast_list, &has_cursor?/1) + end + + def has_cursor?(ast) do + # TODO rewrite to lazy prewalker + {_, result} = + Macro.prewalk(ast, false, fn + _node, true -> + {nil, true} + + {:__cursor__, _, list}, _state when is_list(list) -> + {nil, true} + + node, false -> + {node, false} + end) + + result + end + + def defdelegate_each(fun, opts) when is_list(opts) do + # TODO Remove on elixir v2.0 + append_first? = Keyword.get(opts, :append_first, false) + + {name, args} = + case fun do + {:when, _, [_left, right]} -> + raise ArgumentError, + "guards are not allowed in defdelegate/2, got: when #{Macro.to_string(right)}" + + _ -> + case Macro.decompose_call(fun) do + {_, _} = pair -> pair + _ -> raise ArgumentError, "invalid syntax in defdelegate #{Macro.to_string(fun)}" + end + end + + as = Keyword.get(opts, :as, name) + as_args = build_as_args(args, append_first?) + + {name, args, as, as_args} + end + + defp build_as_args(args, append_first?) do + as_args = :lists.map(&build_as_arg/1, args) + + case append_first? do + true -> tl(as_args) ++ [hd(as_args)] + false -> as_args + end + end + + # elixir validates arg + defp build_as_arg({:\\, _, [arg, _default_arg]}), do: arg + defp build_as_arg(arg), do: arg +end diff --git a/lib/elixir_sense/core/guard.ex b/lib/elixir_sense/core/guard.ex deleted file mode 100644 index 77897dae..00000000 --- a/lib/elixir_sense/core/guard.ex +++ /dev/null @@ -1,139 +0,0 @@ -defmodule ElixirSense.Core.Guard do - @moduledoc """ - This module is responsible for infer type information from guard expressions - """ - - import ElixirSense.Core.State - - alias ElixirSense.Core.MetadataBuilder - - # A guard expression can be in either these form: - # :and :or - # / \ or / \ or guard_expr - # guard_expr guard_expr guard_expr guard_expr - # - # type information from :and subtrees are mergeable - # type information from :or subtrees are discarded - def type_information_from_guards({:and, _, [guard_l, guard_r]}, state) do - left = type_information_from_guards(guard_l, state) - right = type_information_from_guards(guard_r, state) - - Keyword.merge(left, right, fn _k, v1, v2 -> {:intersection, [v1, v2]} end) - end - - def type_information_from_guards({:or, _, [guard_l, guard_r]}, state) do - left = type_information_from_guards(guard_l, state) - right = type_information_from_guards(guard_r, state) - - Keyword.merge(left, right, fn _k, v1, v2 -> - case {v1, v2} do - {{:union, types_1}, {:union, types_2}} -> {:union, types_1 ++ types_2} - {{:union, types}, _} -> {:union, types ++ [v2]} - {_, {:union, types}} -> {:union, [v1 | types]} - _ -> {:union, [v1, v2]} - end - end) - end - - def type_information_from_guards(guard_ast, state) do - {_, acc} = - Macro.prewalk(guard_ast, [], fn - # Standalone variable: func my_func(x) when x - {var, _, nil} = node, acc -> - {node, [{var, :boolean} | acc]} - - {guard_predicate, _, params} = node, acc -> - case guard_predicate_type(guard_predicate, params, state) do - {type, binding} -> - {var, _, nil} = binding - # If we found the predicate type, we can prematurely exit traversing the subtree - {[], [{var, type} | acc]} - - nil -> - {node, acc} - end - - node, acc -> - {node, acc} - end) - - acc - end - - defp guard_predicate_type(p, params, _) - when p in [:is_number, :is_float, :is_integer, :round, :trunc, :div, :rem, :abs], - do: {:number, hd(params)} - - defp guard_predicate_type(p, params, _) when p in [:is_binary, :binary_part], - do: {:binary, hd(params)} - - defp guard_predicate_type(p, params, _) when p in [:is_bitstring, :bit_size, :byte_size], - do: {:bitstring, hd(params)} - - defp guard_predicate_type(p, params, _) when p in [:is_list, :length], do: {:list, hd(params)} - - defp guard_predicate_type(p, params, _) when p in [:hd, :tl], - do: {{:list, :boolean}, hd(params)} - - # when hd(x) == 1 - # when tl(x) <= 2 - defp guard_predicate_type(p, [{guard, _, guard_params}, rhs], _) - when p in [:==, :===, :>=, :>, :<=, :<] and guard in [:hd, :tl] do - rhs_type = - cond do - is_number(rhs) -> :number - is_binary(rhs) -> :binary - is_bitstring(rhs) -> :bitstring - is_atom(rhs) -> :atom - is_boolean(rhs) -> :boolean - true -> nil - end - - rhs_type = if rhs_type, do: {:list, rhs_type}, else: :list - - {rhs_type, hd(guard_params)} - end - - defp guard_predicate_type(p, params, _) when p in [:is_tuple, :elem], - do: {:tuple, hd(params)} - - # when tuple_size(x) == 1 - # when tuple_size(x) == 2 - defp guard_predicate_type(p, [{:tuple_size, _, guard_params}, size], _) - when p in [:==, :===] do - type = - if is_integer(size) do - {:tuple, size, if(size > 0, do: Enum.map(1..size, fn _ -> nil end), else: [])} - else - :tuple - end - - {type, hd(guard_params)} - end - - defp guard_predicate_type(:is_map, params, _), do: {{:map, [], nil}, hd(params)} - defp guard_predicate_type(:map_size, params, _), do: {{:map, [], nil}, hd(params)} - - defp guard_predicate_type(:is_map_key, [var, key], state) do - type = - case MetadataBuilder.get_binding_type(state, key) do - {:atom, key} -> {:map, [{key, nil}], nil} - nil when is_binary(key) -> {:map, [{key, nil}], nil} - _ -> {:map, [], nil} - end - - {type, var} - end - - defp guard_predicate_type(:is_atom, params, _), do: {:atom, hd(params)} - defp guard_predicate_type(:is_boolean, params, _), do: {:boolean, hd(params)} - - defp guard_predicate_type(:is_struct, [var, {:__aliases__, _, _list} = module], state) do - {module, _state, _env} = expand(module, state) - type = {:struct, [], {:atom, module}, nil} - {type, var} - end - - defp guard_predicate_type(:is_struct, params, _), do: {{:struct, [], nil, nil}, hd(params)} - defp guard_predicate_type(_, _, _), do: nil -end diff --git a/lib/elixir_sense/core/introspection.ex b/lib/elixir_sense/core/introspection.ex index e1bd77a4..05091c3e 100644 --- a/lib/elixir_sense/core/introspection.ex +++ b/lib/elixir_sense/core/introspection.ex @@ -568,6 +568,10 @@ defmodule ElixirSense.Core.Introspection do next_snippet(ast, index) end + defp term_to_snippet({name, _, []} = ast, index) when is_atom(name) do + next_snippet(ast, index) + end + defp term_to_snippet(ast, index) do {ast, index} end @@ -1137,148 +1141,6 @@ defmodule ElixirSense.Core.Introspection do def is_function_type(type), do: type in [:def, :defp, :defdelegate] def is_macro_type(type), do: type in [:defmacro, :defmacrop, :defguard, :defguardp] - def expand_import({functions, macros}, module, opts, mods_funs) do - opts = - if Keyword.keyword?(opts) do - opts - else - [] - end - - {all_exported_functions, all_exported_macros} = - if (functions[module] != nil or macros[module] != nil) and Keyword.keyword?(opts[:except]) do - {functions[module], macros[module]} - else - if Map.has_key?(mods_funs, {module, nil, nil}) do - funs_macros = - mods_funs - |> Enum.filter(fn {{m, _f, a}, info} -> - m == module and a != nil and is_pub(info.type) - end) - |> Enum.split_with(fn {_, info} -> is_macro_type(info.type) end) - - functions = - funs_macros - |> elem(1) - |> Enum.flat_map(fn {{_m, f, _a}, info} -> - for {arity, default_args} <- State.ModFunInfo.get_arities(info), - args <- (arity - default_args)..arity do - {f, args} - end - end) - - macros = - funs_macros - |> elem(0) - |> Enum.flat_map(fn {{_m, f, _a}, info} -> - for {arity, default_args} <- State.ModFunInfo.get_arities(info), - args <- (arity - default_args)..arity do - {f, args} - end - end) - - {functions, macros} - else - {macros, functions} = - get_exports(module) - |> Enum.split_with(fn {_, {_, kind}} -> kind == :macro end) - - {functions |> Enum.map(fn {f, {a, _}} -> {f, a} end), - macros |> Enum.map(fn {f, {a, _}} -> {f, a} end)} - end - end - - imported_functions = - all_exported_functions - |> Enum.reject(fn - {:__info__, 1} -> - true - - {:module_info, arity} when arity in [0, 1] -> - true - - {:behaviour_info, 1} -> - if Version.match?(System.version(), ">= 1.15.0-dev") do - true - else - # elixir < 1.15 imports behaviour_info from erlang behaviours - # https://github.com/elixir-lang/elixir/commit/4b26edd8c164b46823e1dc1ec34b639cc3563246 - elixir_module?(module) - end - - {:orelse, 2} -> - module == :erlang - - {:andalso, 2} -> - module == :erlang - - {name, arity} -> - reject_import(name, arity, :function, opts) - end) - - imported_macros = - all_exported_macros - |> Enum.reject(fn - {name, arity} -> - reject_import(name, arity, :macro, opts) - end) - - functions = - case imported_functions do - [] -> - Keyword.delete(functions, module) - - _ -> - Keyword.put(functions, module, imported_functions) - end - - macros = - case imported_macros do - [] -> - Keyword.delete(macros, module) - - _ -> - Keyword.put(macros, module, imported_macros) - end - - {functions, macros} - end - - defp reject_import(name, arity, kind, opts) do - name_string = name |> Atom.to_string() - - rejected_after_only? = - cond do - opts[:only] == :sigils and not String.starts_with?(name_string, "sigil_") -> - true - - opts[:only] == :macros and kind != :macro -> - true - - opts[:only] == :functions and kind != :function -> - true - - Keyword.keyword?(opts[:only]) -> - {name, arity} not in opts[:only] - - String.starts_with?(name_string, "_") -> - true - - true -> - false - end - - if rejected_after_only? do - true - else - if Keyword.keyword?(opts[:except]) do - {name, arity} in opts[:except] - else - false - end - end - end - def combine_imports({functions, macros}) do Enum.reduce(functions, macros, fn {module, imports}, acc -> case acc[module] do diff --git a/lib/elixir_sense/core/macro_expander.ex b/lib/elixir_sense/core/macro_expander.ex deleted file mode 100644 index 930a67f5..00000000 --- a/lib/elixir_sense/core/macro_expander.ex +++ /dev/null @@ -1,88 +0,0 @@ -defmodule ElixirSense.Core.MacroExpander do - @moduledoc false - - def add_default_meta(expr) do - Macro.update_meta(expr, fn keyword -> - Keyword.merge(keyword, context: Elixir, import: Kernel) - end) - end - - def expand_use(ast, module, current_aliases, meta) do - env = %Macro.Env{ - module: module, - function: nil, - aliases: current_aliases, - macros: __ENV__.macros - } - - {use_expanded, _env} = Macro.prewalk(ast, env, &require_and_expand/2) - {use_expanded_with_meta, _meta} = Macro.prewalk(use_expanded, meta, &append_meta/2) - use_expanded_with_meta - end - - defp require_and_expand({:require, _, _} = ast, env) do - {env_after_require, _binding} = Code.eval_string("#{Macro.to_string(ast)}; __ENV__", [], env) - {ast, env_after_require} - end - - defp require_and_expand({:use, meta, arg}, env) do - use_directive_expanded = Macro.expand_once({:use, meta, arg}, env) - {use_directive_expanded, env} - end - - defp require_and_expand({{:., meta1, [module, :__using__]}, meta2, params}, env) - when is_atom(module) do - splitted = - Module.split(module) - |> Enum.map(&String.to_atom/1) - - module_expanded = Macro.expand_once({:__aliases__, [], splitted}, env) - ast_with_module_expanded = {{:., meta1, [module_expanded, :__using__]}, meta2, params} - ast_expanded = Macro.expand_once(ast_with_module_expanded, env) - - if ast_with_module_expanded != ast_expanded do - {{:__block__, [], [ast_expanded]}, env} - else - {[], env} - end - end - - defp require_and_expand(ast, env) do - {ast, env} - end - - defp append_meta({:defoverridable, ast_meta, args}, meta) when is_list(ast_meta) do - {{:defoverridable, Keyword.merge(ast_meta, meta), args}, meta} - end - - defp append_meta({:__aliases__, ast_meta, args}, meta) when is_list(ast_meta) do - new_args = - case ast_meta[:alias] do - false -> - args - - nil -> - args - - alias when is_atom(alias) -> - Module.split(alias) - |> Enum.map(&String.to_atom/1) - end - - {{:__aliases__, meta, new_args}, meta} - end - - defp append_meta({atom, ast_meta, args}, meta) when is_atom(atom) and is_list(ast_meta) do - new_args = - case args do - atom when is_atom(atom) -> nil - other -> other - end - - {{atom, meta, new_args}, meta} - end - - defp append_meta(other, meta) do - {other, meta} - end -end diff --git a/lib/elixir_sense/core/metadata.ex b/lib/elixir_sense/core/metadata.ex index 79bf62b8..be55e721 100644 --- a/lib/elixir_sense/core/metadata.ex +++ b/lib/elixir_sense/core/metadata.ex @@ -8,10 +8,16 @@ defmodule ElixirSense.Core.Metadata do alias ElixirSense.Core.Normalized.Code, as: NormalizedCode alias ElixirSense.Core.State alias ElixirSense.Core.BuiltinFunctions + alias ElixirSense.Core.MetadataBuilder @type t :: %ElixirSense.Core.Metadata{ source: String.t(), mods_funs_to_positions: State.mods_funs_to_positions_t(), + cursor_env: nil | {keyword(), ElixirSense.Core.State.Env.t()}, + closest_env: + nil + | {{pos_integer, pos_integer}, {non_neg_integer, non_neg_integer}, + ElixirSense.Core.State.Env.t()}, lines_to_env: State.lines_to_env_t(), calls: State.calls_t(), vars_info_per_scope_id: State.vars_info_per_scope_id_t(), @@ -25,6 +31,8 @@ defmodule ElixirSense.Core.Metadata do defstruct source: "", mods_funs_to_positions: %{}, + cursor_env: nil, + closest_env: nil, lines_to_env: %{}, calls: %{}, vars_info_per_scope_id: %{}, @@ -59,6 +67,88 @@ defmodule ElixirSense.Core.Metadata do } end + def get_cursor_env( + metadata, + position, + surround \\ nil + ) + + if Version.match?(System.version(), "< 1.15.0") do + # return early if cursor env already found by parser replacing line + # this helps on < 1.15 and braks tests on later versions + def get_cursor_env(%__MODULE__{cursor_env: {_, env}}, _position, _surround) do + env + end + end + + def get_cursor_env( + %__MODULE__{} = metadata, + {line, column}, + surround + ) do + {prefix, source_with_cursor} = + case surround do + {{begin_line, begin_column}, {end_line, end_column}} -> + [prefix, needle, suffix] = + ElixirSense.Core.Source.split_at(metadata.source, [ + {begin_line, begin_column}, + {end_line, end_column} + ]) + + source_with_cursor = prefix <> "__cursor__(#{needle})" <> suffix + + {prefix, source_with_cursor} + + nil -> + [prefix, suffix] = + ElixirSense.Core.Source.split_at(metadata.source, [ + {line, column} + ]) + + source_with_cursor = prefix <> "__cursor__()" <> suffix + + {prefix, source_with_cursor} + end + + {meta, cursor_env} = + case Code.string_to_quoted(source_with_cursor, columns: true, token_metadata: true) do + {:ok, ast} -> + MetadataBuilder.build(ast).cursor_env || {[], nil} + + _ -> + {[], nil} + end + + {_meta, cursor_env} = + if cursor_env != nil do + {meta, cursor_env} + else + # IO.puts(prefix <> "|") + case NormalizedCode.Fragment.container_cursor_to_quoted(prefix, + columns: true, + token_metadata: true + ) do + {:ok, ast} -> + MetadataBuilder.build(ast).cursor_env || {[], nil} + + _ -> + {[], nil} + end + end + + if cursor_env != nil do + cursor_env + else + case metadata.closest_env do + {_pos, _dist, env} -> + env + + nil -> + get_env(metadata, {line, column}) + end + end + end + @spec get_env(__MODULE__.t(), {pos_integer, pos_integer}) :: State.Env.t() def get_env(%__MODULE__{} = metadata, {line, column}) do all_scopes = @@ -144,7 +234,7 @@ defmodule ElixirSense.Core.Metadata do end, &>=/2, fn -> - {line, State.default_env()} + {line, MetadataBuilder.default_env({line, column})} end ) |> elem(1) @@ -183,23 +273,21 @@ defmodule ElixirSense.Core.Metadata do |> Enum.min(fn -> nil end) end - def add_scope_vars( - %State.Env{} = env, + def find_var( %__MODULE__{vars_info_per_scope_id: vars_info_per_scope_id}, - {line, column}, - predicate \\ fn _ -> true end + variable, + version, + position ) do - scope_vars = vars_info_per_scope_id[env.scope_id] || [] - env_vars_names = env.vars |> Enum.map(& &1.name) - - scope_vars_missing_in_env = - scope_vars - |> Enum.filter(fn var -> - var.name not in env_vars_names and Enum.min(var.positions) <= {line, column} and - predicate.(var) + vars_info_per_scope_id + |> Enum.find_value(fn {_scope_id, vars} -> + vars + |> Enum.find_value(fn {{n, v}, info} -> + if n == variable and (v == version or version == :any) and position in info.positions do + info + end end) - - %{env | vars: env.vars ++ scope_vars_missing_in_env} + end) end @spec at_module_body?(State.Env.t()) :: boolean() @@ -244,7 +332,12 @@ defmodule ElixirSense.Core.Metadata do def get_call_arity(%__MODULE__{}, _module, nil, _line, _column), do: nil def get_call_arity( - %__MODULE__{calls: calls, error: error, mods_funs_to_positions: mods_funs_to_positions}, + %__MODULE__{ + calls: calls, + error: error, + mods_funs_to_positions: mods_funs_to_positions, + types: types + }, module, fun, line, @@ -272,10 +365,26 @@ defmodule ElixirSense.Core.Metadata do end) end + result = + if result == nil do + mods_funs_to_positions + |> Enum.find_value(fn + {{^module, ^fun, arity}, %{positions: positions}} when not is_nil(arity) -> + if Enum.any?(positions, &match?({^line, _}, &1)) do + arity + end + + _ -> + nil + end) + else + result + end + if result == nil do - mods_funs_to_positions + types |> Enum.find_value(fn - {{^module, ^fun, arity}, %{positions: positions}} when not is_nil(arity) -> + {{^module, ^fun, arity}, %{positions: positions}} -> if Enum.any?(positions, &match?({^line, _}, &1)) do arity end @@ -344,7 +453,7 @@ defmodule ElixirSense.Core.Metadata do when not is_nil(module) and not is_nil(type) do metadata.types |> Enum.filter(fn - {{^module, ^type, arity}, _type_info} when not is_nil(arity) -> true + {{^module, ^type, _arity}, _type_info} -> true _ -> false end) |> Enum.map(fn {_, %State.TypeInfo{} = type_info} -> diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index 32112d3b..101434e2 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -3,1956 +3,73 @@ defmodule ElixirSense.Core.MetadataBuilder do This module is responsible for building/retrieving environment information from an AST. """ - import ElixirSense.Core.State - import ElixirSense.Log - - alias ElixirSense.Core.BuiltinFunctions - alias ElixirSense.Core.Source - alias ElixirSense.Core.State - alias ElixirSense.Core.State.VarInfo - alias ElixirSense.Core.TypeInfo - alias ElixirSense.Core.Guard - - @scope_keywords [:for, :fn, :with] - @block_keywords [:do, :else, :rescue, :catch, :after] - @defs [:def, :defp, :defmacro, :defmacrop, :defdelegate, :defguard, :defguardp] - @protocol_types [{:t, [], :type, "@type t :: term"}] - @protocol_functions [ - {:__protocol__, [:atom], :def}, - {:impl_for, [:data], :def}, - {:impl_for!, [:data], :def}, - {:behaviour_info, [:atom], :def} - ] - - defguardp is_call(call, params) - when is_atom(call) and is_list(params) and - call not in [:., :__aliases__, :"::", :{}, :|>, :%, :%{}] + alias ElixirSense.Core.Compiler.State + alias ElixirSense.Core.Compiler @doc """ Traverses the AST building/retrieving the environment information. It returns a `ElixirSense.Core.State` struct containing the information. """ - @spec build(Macro.t()) :: State.t() - def build(ast) do - # dbg(ast) - {_ast, [state]} = - Macro.traverse(ast, [%State{}], &safe_call_pre/2, &safe_call_post/2) - - try do - state - |> remove_attributes_scope - |> remove_lexical_scope - |> remove_vars_scope - |> remove_module - |> remove_protocol_implementation - rescue - exception -> - warn( - Exception.format( - :error, - "#{inspect(exception.__struct__)} during metadata build scope closing:\n" <> - "#{Exception.message(exception)}\n" <> - "ast node: #{inspect(ast, limit: :infinity)}", - __STACKTRACE__ - ) - ) - - vars_info_per_scope_id = - try do - update_vars_info_per_scope_id(state) - rescue - _ -> - state.vars_info_per_scope_id - end - - %{ - state - | attributes: [], - scope_attributes: [], - aliases: [], - requires: [], - functions: [], - macros: [], - scope_ids: [], - vars: [], - scope_vars: [], - vars_info_per_scope_id: vars_info_per_scope_id, - module: [], - scopes: [], - protocols: [] - } - end - end - - defp safe_call_pre(ast, [state = %State{} | _] = states) do - try do - # if operation == :pre do - # dbg(ast) - # end - {ast_after_pre, state_after_pre} = pre(ast, state) - {ast_after_pre, [state_after_pre | states]} - rescue - exception -> - # reraise(exception, __STACKTRACE__) - warn( - Exception.format( - :error, - "#{inspect(exception.__struct__)} during metadata build pre:\n" <> - "#{Exception.message(exception)}\n" <> - "ast node: #{inspect(ast, limit: :infinity)}", - __STACKTRACE__ - ) - ) - - {nil, [:error | states]} - end - end - - defp safe_call_post(ast, [:error | states]) do - {ast, states} - end - - defp safe_call_post(ast_after_pre, [state_after_pre = %State{} | states]) do - try do - # if operation == :pre do - # dbg(ast_after_pre) - # end - {ast_after_post, state_after_post} = post(ast_after_pre, state_after_pre) - {ast_after_post, [state_after_post | tl(states)]} - rescue - exception -> - warn( - Exception.format( - :error, - "#{inspect(exception.__struct__)} during metadata build post:\n" <> - "#{Exception.message(exception)}\n" <> - "ast node: #{inspect(ast_after_pre, limit: :infinity)}", - __STACKTRACE__ - ) - ) - - {nil, states} - end - end - - defp pre_module(ast, state, meta, alias, types \\ [], functions \\ [], options \\ []) do - {position, end_position} = extract_range(meta) - - {full, module, state} = - case Keyword.get(options, :for) do - nil -> - {module, state, env} = expand(alias, state) - {full, state, _env} = alias_defmodule(alias, module, state, env) - - {full, module, state} - - implementations -> - {implementation_alias(alias, implementations), {alias, implementations}, state} - end - - state = - state - |> maybe_add_protocol_implementation(module) - |> add_module(full) - |> add_current_module_to_index(position, end_position, generated: state.generated) - |> new_lexical_scope - |> new_attributes_scope - |> new_vars_scope - - env = get_current_env(state) - {state, env} = maybe_add_protocol_behaviour(module, state, env) - - state = - types - |> Enum.reduce(state, fn {type_name, type_args, spec, kind}, acc -> - acc - |> add_type(env, type_name, type_args, kind, spec, position, end_position, - generated: true - ) - end) - - state = add_module_functions(state, env, functions, position, end_position) - - state - |> result(ast) - end - - defp post_module(ast, state) do - env = get_current_env(state) - - state - |> apply_optional_callbacks(env) - |> remove_attributes_scope - |> remove_lexical_scope - |> maybe_move_vars_to_outer_scope - |> remove_vars_scope - |> remove_module - |> remove_protocol_implementation - |> result(ast) - end - - def pre_protocol(ast, state, meta, module) do - # protocol defines a type `@type t :: term` - # and functions __protocol__/1, impl_for/1, impl_for!/1 - - pre_module(ast, state, meta, module, @protocol_types, @protocol_functions) - end - - def post_protocol(ast, state) do - # turn specs into callbacks or create dummy callbacks - builtins = BuiltinFunctions.all() |> Keyword.keys() - - current_module = get_current_module(state) - - keys = - state.mods_funs_to_positions - |> Map.keys() - |> Enum.filter(fn - {^current_module, name, _arity} when not is_nil(name) -> - name not in builtins - - _ -> - false - end) - - new_specs = - for key = {_mod, name, _arity} <- keys, - into: %{}, - do: - ( - new_spec = - case state.specs[key] do - nil -> - %State.ModFunInfo{positions: positions, params: params} = - state.mods_funs_to_positions[key] - - args = - for param_variant <- params do - param_variant - |> Enum.map(&Macro.to_string/1) - end - - specs = - for arg <- args do - joined = Enum.join(arg, ", ") - "@callback #{name}(#{joined}) :: term" - end - - %State.SpecInfo{ - name: name, - args: args, - specs: specs, - kind: :callback, - positions: positions, - end_positions: Enum.map(positions, fn _ -> nil end), - generated: Enum.map(positions, fn _ -> true end) - } - - spec = %State.SpecInfo{specs: specs} -> - %State.SpecInfo{ - spec - | # TODO :spec will get replaced here, refactor into array - kind: :callback, - specs: - specs - |> Enum.map(fn s -> - String.replace_prefix(s, "@spec", "@callback") - end) - |> Kernel.++(specs) - } - end - - {key, new_spec} - ) - - specs = Map.merge(state.specs, new_specs) - - state = %{state | specs: specs} - post_module(ast, state) - end - - defp pre_func({type, meta, ast_args}, state, meta, name, params, options \\ []) - when is_atom(name) do - vars = - state - |> find_vars(params) - |> merge_same_name_vars() - - vars = - if options[:guards], - do: infer_type_from_guards(options[:guards], vars, state), - else: vars - - {position, end_position} = extract_range(meta) - - options = Keyword.put(options, :generated, state.generated) - - ast = {type, Keyword.put(meta, :func, true), ast_args} - - env = get_current_env(state) - - state - |> new_named_func(name, length(params || [])) - |> add_func_to_index(env, name, params || [], position, end_position, type, options) - |> new_lexical_scope - |> new_func_vars_scope - |> add_vars(vars, true) - |> add_current_env_to_line(Keyword.fetch!(meta, :line)) - |> result(ast) - end - - defp extract_range(meta) do - position = { - Keyword.fetch!(meta, :line), - Keyword.fetch!(meta, :column) - } - - end_position = - case meta[:end] do - nil -> - case meta[:end_of_expression] do - nil -> - nil - - end_of_expression_meta -> - { - Keyword.fetch!(end_of_expression_meta, :line), - Keyword.fetch!(end_of_expression_meta, :column) - } - end + @spec build(Macro.t(), nil | {pos_integer, pos_integer}) :: State.t() + def build(ast, cursor_position \\ nil) do + state_initial = initial_state(cursor_position) - end_meta -> - { - Keyword.fetch!(end_meta, :line), - Keyword.fetch!(end_meta, :column) + 3 - } - end + {_ast, state, _env} = Compiler.expand(ast, state_initial, Compiler.env()) - {position, end_position} - end - - defp post_func(ast, state) do - # dbg(ast) - state - |> remove_lexical_scope - |> remove_func_vars_scope - |> remove_last_scope_from_scopes - |> result(ast) - end - - defp pre_scope_keyword(ast, state, line) do - state = - case ast do - {:for, _, _} -> - state |> push_binding_context(:for) - - _ -> - state - end - - state - |> add_current_env_to_line(line) - |> new_vars_scope - |> result(ast) - end - - defp post_scope_keyword(ast, state) do - state = - case ast do - {:for, _, _} -> - state |> pop_binding_context - - _ -> - state - end - - state - |> maybe_move_vars_to_outer_scope - |> remove_vars_scope - |> result(ast) - end - - defp pre_block_keyword(ast, state) do - state = - case ast do - {:rescue, _} -> - state |> push_binding_context(:rescue) - - _ -> - state - end - - state - |> new_lexical_scope - |> new_vars_scope - |> result(ast) - end - - defp post_block_keyword(ast, state) do - state = - case ast do - {:rescue, _} -> - state |> pop_binding_context - - _ -> - state - end - - state - |> remove_lexical_scope - |> maybe_move_vars_to_outer_scope - |> remove_vars_scope - |> result(ast) - end - - defp pre_clause({_clause, meta, _} = ast, state, lhs) do - line = meta |> Keyword.fetch!(:line) - - vars = - state - |> find_vars(lhs, Enum.at(state.binding_context, 0)) - |> merge_same_name_vars() - - state - |> new_lexical_scope - |> new_vars_scope - |> add_vars(vars, true) - |> add_current_env_to_line(line) - |> result(ast) - end - - defp post_clause(ast, state) do state - |> remove_lexical_scope - |> maybe_move_vars_to_outer_scope - |> remove_vars_scope - |> result(ast) + |> State.remove_attributes_scope() + |> State.remove_vars_scope(state_initial) + |> State.remove_module() end - defp pre_spec(ast, state, meta, type_name, type_args, spec, kind) do - spec = TypeInfo.typespec_to_string(kind, spec) - - {position = {line, _column}, end_position} = extract_range(meta) - env = get_current_env(state) - - state = - if kind in [:callback, :macrocallback] do - state - |> add_func_to_index( - env, - :behaviour_info, - [{:atom, meta, nil}], - position, - end_position, - :def, - generated: true - ) - else - state - end - - state - |> add_spec(env, type_name, type_args, spec, kind, position, end_position, - generated: state.generated - ) - |> add_typespec_namespace(type_name, length(type_args)) - |> add_current_env_to_line(line) - |> result(ast) - end - - defp post_string_literal(ast, state, line, str) do - str - |> Source.split_lines() - |> Enum.with_index() - |> Enum.reduce(state, fn {_s, i}, acc -> add_current_env_to_line(acc, line + i) end) - |> result(ast) - end - - defp pre( - {:defmodule, meta, [module, _]} = ast, - state - ) do - pre_module(ast, state, meta, module) - end - - defp pre( - {:defprotocol, meta, [protocol, _]} = ast, - state - ) do - pre_protocol(ast, state, meta, protocol) - end - - defp pre( - {:defimpl, meta, [protocol, impl_args | _]} = ast, - state - ) do - pre_protocol_implementation(ast, state, meta, protocol, impl_args) - end - - defp pre( - {:defdelegate, meta, [{name, meta2, params}, body]}, - state - ) - when is_atom(name) and is_list(body) do - ast_without_params = {:defdelegate, meta, [{name, add_no_call(meta2), []}, body]} - target_module = body |> Keyword.get(:to) - - target_function = - case body |> Keyword.get(:as) do - nil -> {:ok, name} - as when is_atom(as) -> {:ok, as} - _ -> :error - end - - options = - with mod = target_module, - {:ok, target_function} <- target_function do - [target: {mod, target_function}] - else - _ -> [] - end - - pre_func(ast_without_params, state, meta, name, params, options) - end - - # quote do - # quote options do - defp pre({:quote, _meta, list}, state) when is_list(list) do - # replace with an empty AST node - {[], state} - end - - # ex_unit describe - defp pre( - {:describe, meta, [name, _body]} = ast, - state = %{scopes: [atom | _]} - ) - when is_binary(name) and is_atom(atom) and atom != nil do - line = Keyword.fetch!(meta, :line) - column = Keyword.fetch!(meta, :column) - - state = - state - |> add_call_to_line({nil, :describe, 2}, {line, column}) - - %{state | context: Map.put(state.context, :ex_unit_describe, name)} - |> add_current_env_to_line(line) - |> result(ast) - end - - # ex_unit not implemented test - defp pre( - {:test, meta, [name]}, - state = %{scopes: [atom | _]} - ) - when is_binary(name) and is_atom(atom) and atom != nil do - def_name = ex_unit_test_name(state, name) - line = Keyword.fetch!(meta, :line) - column = Keyword.fetch!(meta, :column) - - ast_without_params = {:def, meta, [{def_name, add_no_call([]), []}, [], []]} - - state = - state - |> add_call_to_line({nil, :test, 0}, {line, column}) - - pre_func(ast_without_params, state, meta, def_name, [{:_, [line: line, column: column], nil}]) - end - - # ex_unit test without context - defp pre( - {:test, meta, [name, body]}, - state = %{scopes: [atom | _]} - ) - when is_binary(name) and is_atom(atom) and atom != nil do - def_name = ex_unit_test_name(state, name) - line = Keyword.fetch!(meta, :line) - column = Keyword.fetch!(meta, :column) - - ast_without_params = {:def, meta, [{def_name, add_no_call([]), []}, [], body]} - - state = - state - |> add_call_to_line({nil, :test, 2}, {line, column}) - - pre_func(ast_without_params, state, meta, def_name, [{:_, [line: line, column: column], nil}]) - end - - # ex_unit test with context - defp pre( - {:test, meta, [name, param, body]}, - state = %{scopes: [atom | _]} - ) - when is_binary(name) and is_atom(atom) and atom != nil do - def_name = ex_unit_test_name(state, name) - line = Keyword.fetch!(meta, :line) - column = Keyword.fetch!(meta, :column) - - ast_without_params = {:def, meta, [{def_name, add_no_call([]), []}, [], body]} - - state = - state - |> add_call_to_line({nil, :test, 3}, {line, column}) - - pre_func(ast_without_params, state, meta, def_name, [param]) - end - - # ex_unit setup with context - defp pre( - {setup, meta, [param, body]}, - state = %{scopes: [atom | _]} - ) - when setup in [:setup, :setup_all] and is_atom(atom) and atom != nil do - line = Keyword.fetch!(meta, :line) - column = Keyword.fetch!(meta, :column) - - # NOTE this name is not 100% correct - ex_unit uses counters instead of line but it's too complicated - def_name = :"__ex_unit_#{setup}_#{line}" - ast_without_params = {:def, meta, [{def_name, add_no_call([]), []}, [], body]} - - state = - state - |> add_call_to_line({nil, setup, 2}, {line, column}) - - pre_func(ast_without_params, state, meta, def_name, [param]) - end - - # ex_unit setup without context - defp pre( - {setup, meta, [body]}, - state = %{scopes: [atom | _]} - ) - when setup in [:setup, :setup_all] and is_atom(atom) and atom != nil do - line = Keyword.fetch!(meta, :line) - column = Keyword.fetch!(meta, :column) - - # NOTE this name is not 100% correct - ex_unit uses counters instead of line but it's too complicated - def_name = :"__ex_unit_#{setup}_#{line}" - ast_without_params = {:def, meta, [{def_name, add_no_call([]), []}, [], body]} - - state = - state - |> add_call_to_line({nil, setup, 2}, {line, column}) - - pre_func(ast_without_params, state, meta, def_name, [{:_, [line: line, column: column], nil}]) - end - - # function head with guards - defp pre( - {def_name, meta, [{:when, _, [{name, meta2, params}, guards]}, body]}, - state - ) - when def_name in @defs and is_atom(name) do - ast_without_params = {def_name, meta, [{name, add_no_call(meta2), []}, guards, body]} - pre_func(ast_without_params, state, meta, name, params, guards: guards) - end - - defp pre( - {def_name, meta, [{name, meta2, params}, body]}, - state - ) - when def_name in @defs and is_atom(name) do - ast_without_params = {def_name, meta, [{name, add_no_call(meta2), []}, body]} - pre_func(ast_without_params, state, meta, name, params) - end - - # defguard and defguardp - defp pre( - {def_name, meta, - [ - {:when, _meta, [{name, meta2, params}, body]} - ]}, - state - ) - when def_name in @defs and is_atom(name) do - ast_without_params = {def_name, meta, [{name, add_no_call(meta2), []}, body]} - pre_func(ast_without_params, state, meta, name, params) - end - - # function head - defp pre({def_name, meta, [{name, meta2, params}]}, state) - when def_name in @defs and is_atom(name) do - ast_without_params = {def_name, meta, [{name, add_no_call(meta2), []}, nil]} - pre_func(ast_without_params, state, meta, name, params) - end - - defp pre( - {:@, meta_attr, [{:moduledoc, meta, [doc_arg]}]}, - state - ) do - new_ast = {:@, meta_attr, [{:moduledoc, add_no_call(meta), [doc_arg]}]} - env = get_current_env(state) - - state - |> add_moduledoc_positions( - env, - meta_attr - ) - |> register_doc(env, :moduledoc, doc_arg) - |> result(new_ast) - end - - defp pre( - {:@, meta_attr, [{doc, meta, [doc_arg]}]}, - state - ) - when doc in [:doc, :typedoc] do - new_ast = {:@, meta_attr, [{doc, add_no_call(meta), [doc_arg]}]} - env = get_current_env(state) - - state - |> register_doc(env, doc, doc_arg) - |> result(new_ast) - end - - defp pre( - {:@, meta_attr, [{:impl, meta, [impl_arg]}]}, - state - ) do - new_ast = {:@, meta_attr, [{:impl, add_no_call(meta), [impl_arg]}]} - env = get_current_env(state) - # impl adds sets :hidden by default - state - |> register_doc(env, :doc, :impl) - |> result(new_ast) - end - - defp pre( - {:@, meta_attr, [{:optional_callbacks, meta, [args]}]}, - state - ) do - new_ast = {:@, meta_attr, [{:optional_callbacks, add_no_call(meta), [args]}]} - - state - |> register_optional_callbacks(args) - |> result(new_ast) - end - - defp pre( - {:@, meta_attr, [{:deprecated, meta, [deprecated_arg]}]}, - state - ) do - new_ast = {:@, meta_attr, [{:deprecated, add_no_call(meta), [deprecated_arg]}]} - env = get_current_env(state) - # treat @deprecated message as @doc deprecated: message - state - |> register_doc(env, :doc, deprecated: deprecated_arg) - |> result(new_ast) - end - - defp pre({:@, _meta, [{:behaviour, _, [_arg]}]} = ast, state) do - {ast, state, _env} = expand(ast, state) - {ast, state} - end - - # protocol derive - defp pre( - {:@, meta, [{:derive, _, [derived_protos]}]} = ast, - state - ) do - line = Keyword.fetch!(meta, :line) - column = Keyword.fetch!(meta, :column) - current_module = state |> get_current_module - - List.wrap(derived_protos) - |> Enum.map(fn - {proto, _opts} -> proto - proto -> proto - end) - |> Enum.reduce(state, fn proto, acc -> - case expand(proto, acc) do - {proto_module, acc, _env} when is_atom(proto_module) -> - # protocol implementation module for Any - mod_any = Module.concat(proto_module, Any) - - # protocol implementation module built by @derive - mod = Module.concat(proto_module, current_module) - - case acc.mods_funs_to_positions[{mod_any, nil, nil}] do - nil -> - # implementation for: Any not detected (is in other file etc.) - acc - |> add_module_to_index(mod, {line, column}, nil, generated: true) - - _any_mods_funs -> - # copy implementation for: Any - copied_mods_funs_to_positions = - for {{module, fun, arity}, val} <- acc.mods_funs_to_positions, - module == mod_any, - into: %{}, - do: {{mod, fun, arity}, val} - - %{ - acc - | mods_funs_to_positions: - acc.mods_funs_to_positions |> Map.merge(copied_mods_funs_to_positions) - } - end - - :error -> - acc - end - end) - |> result(ast) - end - - defp pre( - {:@, meta_attr, - [ - {kind, kind_meta, - [{:"::", _meta, _params = [{name, _, type_args}, _type_def]} = spec] = kind_args} - ]}, - state - ) - when kind in [:type, :typep, :opaque] and is_atom(name) and - (is_nil(type_args) or is_list(type_args)) do - ast = {:@, meta_attr, [{kind, add_no_call(kind_meta), kind_args}]} - spec = expand_aliases_in_ast(state, spec) - type_args = List.wrap(type_args) - - spec = TypeInfo.typespec_to_string(kind, spec) - - {position = {line, _column}, end_position} = extract_range(meta_attr) - env = get_current_env(state) - - state - |> add_type(env, name, type_args, spec, kind, position, end_position, - generated: state.generated - ) - |> add_typespec_namespace(name, length(type_args)) - |> add_current_env_to_line(line) - |> result(ast) - end - - defp pre( - {:@, meta_attr, - [ - {kind, kind_meta, - [{:when, _, [{:"::", _meta, _params = [{name, _, type_args}, _type_def]}, _]} = spec] = - kind_args} - ]}, - state - ) - when kind in [:spec, :callback, :macrocallback] and is_atom(name) and - (is_nil(type_args) or is_list(type_args)) do - pre_spec( - {:@, meta_attr, - [ - {kind, add_no_call(kind_meta), kind_args} - ]}, - state, - meta_attr, - name, - expand_aliases_in_ast(state, List.wrap(type_args)), - expand_aliases_in_ast(state, spec), - kind - ) - end - - defp pre( - {:@, meta_attr, - [ - {kind, meta_kind, - [{:"::", _meta, _params = [{name, _, type_args}, _type_def]} = spec] = kind_args} - ]}, - state - ) - when kind in [:spec, :callback, :macrocallback] and is_atom(name) and - (is_nil(type_args) or is_list(type_args)) do - pre_spec( - {:@, meta_attr, [{kind, add_no_call(meta_kind), kind_args}]}, - state, - meta_attr, - name, - expand_aliases_in_ast(state, List.wrap(type_args)), - expand_aliases_in_ast(state, spec), - kind - ) - end - - # incomplete spec - # @callback my(integer) - defp pre( - {:@, meta_attr, [{kind, meta_kind, [{name, meta_name, type_args}]} = spec]}, - state - ) - when kind in [:spec, :callback, :macrocallback] and is_atom(name) and - (is_nil(type_args) or is_list(type_args)) do - pre_spec( - {:@, meta_attr, [{kind, add_no_call(meta_kind), [{name, meta_name, type_args}]}]}, - state, - meta_attr, - name, - expand_aliases_in_ast(state, List.wrap(type_args)), - expand_aliases_in_ast(state, spec), - kind - ) - end - - defp pre({:@, meta_attr, [{name, meta, params}]}, state) when is_atom(name) do - name_string = Atom.to_string(name) - - if String.match?(name_string, ~r/^[_\p{Ll}\p{Lo}][\p{L}\p{N}_]*[?!]?$/u) and - not String.starts_with?(name_string, "__atom_elixir_marker_") do - line = Keyword.fetch!(meta_attr, :line) - column = Keyword.fetch!(meta_attr, :column) - - binding = - case List.wrap(params) do - [] -> - {nil, false} - - [param] -> - {get_binding_type(state, param), true} - - _ -> - :error - end - - case binding do - {type, is_definition} -> - new_ast = {:@, meta_attr, [{name, add_no_call(meta), params}]} - - state - |> add_attribute(name, type, is_definition, {line, column}) - |> add_current_env_to_line(line) - |> result(new_ast) - - _ -> - {[], state} - end - else - # most likely not an attribute - {[], state} - end - end - - defp pre( - {directive, _meta, _args} = ast, - state - ) - when directive in [:alias, :require, :import, :use] do - {ast, state, _env} = expand(ast, state) - {ast, state} - end - - defp pre({:defoverridable, _meta, [_arg]} = ast, state) do - {ast, state, _env} = expand(ast, state) - {ast, state} - end - - defp pre({atom, meta, [_ | _]} = ast, state) - when atom in @scope_keywords do - line = Keyword.fetch!(meta, :line) - pre_scope_keyword(ast, state, line) - end - - defp pre({atom, _block} = ast, state) when atom in @block_keywords do - pre_block_keyword(ast, state) - end - - defp pre({:->, meta, [[{:when, _, [_var, guards]} = lhs], rhs]}, state) do - pre_clause({:->, meta, [guards, rhs]}, state, lhs) - end - - defp pre({:->, meta, [[lhs], rhs]}, state) do - pre_clause({:->, meta, [:_, rhs]}, state, lhs) - end - - defp pre({:->, meta, [lhs, rhs]}, state) do - pre_clause({:->, meta, [:_, rhs]}, state, lhs) - end - - defp pre({atom, meta, [lhs, rhs]}, state) - when atom in [:=, :<-] do - result(state, {atom, meta, [lhs, rhs]}) - end - - defp pre({var_or_call, meta, nil} = ast, state) - when is_atom(var_or_call) and var_or_call != :__MODULE__ do - line = Keyword.fetch!(meta, :line) - column = Keyword.fetch!(meta, :column) - - if Enum.any?(get_current_vars(state), &(&1.name == var_or_call)) do - vars = - state - |> find_vars(ast) - |> merge_same_name_vars() - - add_vars(state, vars, false) - else - # pre Elixir 1.4 local call syntax - # TODO can we remove when we require elixir 1.15+? - # it's only legal inside typespecs - # credo:disable-for-next-line - if not Keyword.get(meta, :no_call, false) and - (Version.match?(System.version(), "< 1.15.0-dev") or - match?([{:typespec, _, _} | _], state.scopes)) do - add_call_to_line(state, {nil, var_or_call, 0}, {line, column}) - else - state - end - end - |> add_current_env_to_line(line) - |> result(ast) - end - - defp pre({:when, meta, [lhs, rhs]}, state) do - vars = - state - |> find_vars(lhs) - |> merge_same_name_vars() - - state - |> add_vars(vars, true) - |> result({:when, meta, [:_, rhs]}) - end - - defp pre({type, meta, fields} = ast, state) - when type in [:defstruct, :defexception] do - {position, end_position} = extract_range(meta) - - fields = - case fields do - [fields] when is_list(fields) -> - fields - |> Enum.filter(fn - field when is_atom(field) -> true - {field, _} when is_atom(field) -> true - _ -> false - end) - |> Enum.map(fn - field when is_atom(field) -> {field, nil} - {field, value} when is_atom(field) -> {field, value} - end) - - _ -> - [] - end - - env = get_current_env(state) - - state - |> add_struct_or_exception(env, type, fields, position, end_position) - |> result(ast) - end - - # transform `a |> b(c)` calls into `b(a, c)` - defp pre({:|>, _, [params_1, {call, meta, params_rest}]}, state) do - params = [params_1 | params_rest || []] - pre({call, meta, params}, state) - end - - # transform external and local func capture into fake call - defp pre({:&, _, [{:/, _, [fun, arity]}]}, state) when is_integer(arity) do - fake_params = - if arity == 0 do - [] - else - for _ <- 1..arity, do: nil - end - - call = - case fun do - {func, position, nil} -> - {func, position, fake_params} - - {{:., _, [_ | _]} = ast_part, position, []} -> - {ast_part, position, fake_params} - - _ -> - nil - end - - pre(call, state) - end - - defp pre( - {:case, meta, - [ - condition_ast, - [ - do: _clauses - ] - ]} = ast, - state - ) do - line = Keyword.fetch!(meta, :line) - column = Keyword.fetch!(meta, :column) - - state - |> push_binding_context(get_binding_type(state, condition_ast)) - |> add_call_to_line({nil, :case, 2}, {line, column}) - |> add_current_env_to_line(line) - |> result(ast) - end - - defp pre( - {{:., meta1, [{:__aliases__, _, [:Record]} = module_expression, call]}, _meta, - params = [name, _]} = ast, - state - ) - when is_call(call, params) and call in [:defrecord, :defrecordp] and - is_atom(name) do - {position = {line, column}, end_position} = extract_range(meta1) - - # TODO pass env - {module, state, _env} = expand(module_expression, state) - - type = - case call do - :defrecord -> :defmacro - :defrecordp -> :defmacrop - end - - options = [generated: true] - - shift = if state.generated, do: 0, else: 1 - env = get_current_env(state) - - state - |> new_named_func(name, 1) - |> add_func_to_index( - env, - name, - [{:\\, [], [{:args, [], nil}, []]}], - position, - end_position, - type, - options - ) - |> new_named_func(name, 2) - |> add_func_to_index( - env, - name, - [{:record, [], nil}, {:args, [], nil}], - position, - end_position, - type, - options - ) - |> add_call_to_line({module, call, length(params)}, {line, column + shift}) - |> add_current_env_to_line(line) - |> result(ast) - end - - defp pre( - {{:., meta1, [{:__aliases__, _, [_ | _]} = module_expression, call]}, _meta, params} = - ast, - state - ) - when is_call(call, params) do - line = Keyword.fetch!(meta1, :line) - column = Keyword.fetch!(meta1, :column) - - try do - # TODO pass env - {module, state, _env} = expand(module_expression, state) - - shift = if state.generated, do: 0, else: 1 - - state - |> add_call_to_line({module, call, length(params)}, {line, column + shift}) - |> add_current_env_to_line(line) - |> result(ast) - rescue - _ -> - # Module.concat can fail for invalid aliases - result(state, nil) - end - end - - defp pre( - {{:., meta1, [{:__MODULE__, _, nil}, call]}, _meta, params} = ast, - state - ) - when is_call(call, params) do - line = Keyword.fetch!(meta1, :line) - column = Keyword.fetch!(meta1, :column) - module = get_current_module(state) - - shift = if state.generated, do: 0, else: 1 - - state - |> add_call_to_line({module, call, length(params)}, {line, column + shift}) - |> add_current_env_to_line(line) - |> result(ast) - end - - defp pre( - {{:., meta1, [{:@, _, [{attribute, _, nil}]}, call]}, _meta, params} = ast, - state - ) - when is_call(call, params) and is_atom(attribute) do - line = Keyword.fetch!(meta1, :line) - column = Keyword.fetch!(meta1, :column) - - shift = if state.generated, do: 0, else: 1 - - state - |> add_call_to_line({{:attribute, attribute}, call, length(params)}, {line, column + shift}) - |> add_current_env_to_line(line) - |> result(ast) - end - - defp pre( - {{:., meta1, [{:@, _, [{attribute, _, nil}]}]}, _meta, params} = ast, - state - ) - when is_atom(attribute) do - line = Keyword.fetch!(meta1, :line) - column = Keyword.fetch!(meta1, :column) - - shift = if state.generated, do: 0, else: 1 - - state - |> add_call_to_line({nil, {:attribute, attribute}, length(params)}, {line, column + shift}) - |> add_current_env_to_line(line) - |> result(ast) - end - - defp pre( - {{:., meta1, [{variable, _var_meta, nil}]}, _meta, params} = ast, - state - ) - when is_atom(variable) do - line = Keyword.fetch!(meta1, :line) - column = Keyword.fetch!(meta1, :column) - - shift = if state.generated, do: 0, else: 1 - - state - |> add_call_to_line({nil, {:variable, variable}, length(params)}, {line, column + shift}) - |> add_current_env_to_line(line) - |> result(ast) - end - - defp pre( - {{:., meta1, [{variable, _var_meta, nil}, call]}, _meta, params} = ast, - state - ) - when is_call(call, params) and is_atom(variable) do - line = Keyword.fetch!(meta1, :line) - column = Keyword.fetch!(meta1, :column) - - shift = if state.generated, do: 0, else: 1 - - state - |> add_call_to_line({{:variable, variable}, call, length(params)}, {line, column + shift}) - |> add_current_env_to_line(line) - |> result(ast) - end - - defp pre( - {{:., meta1, [module, call]}, _meta, params} = ast, - state - ) - when is_atom(module) and is_call(call, params) do - line = Keyword.fetch!(meta1, :line) - column = Keyword.fetch!(meta1, :column) - - shift = if state.generated, do: 0, else: 1 - - state - |> add_call_to_line({module, call, length(params)}, {line, column + shift}) - |> add_current_env_to_line(line) - |> result(ast) - end - - defp pre({call, meta, params} = ast, state) - when is_call(call, params) do - case Keyword.get(meta, :line) do - nil -> - {ast, state} - - _ -> - line = Keyword.fetch!(meta, :line) - - # credo:disable-for-next-line - if not Keyword.get(meta, :no_call, false) do - column = Keyword.fetch!(meta, :column) - - state = - if String.starts_with?(to_string(call), "__atom_elixir_marker_") do - state - else - add_call_to_line(state, {nil, call, length(params)}, {line, column}) - end - - state - |> add_current_env_to_line(line) - |> result(ast) + def initial_state(cursor_position) do + %State{ + cursor_position: cursor_position, + prematch: + if Version.match?(System.version(), ">= 1.15.0-dev") do + Code.get_compiler_option(:on_undefined_variable) else - state - |> add_current_env_to_line(line) - |> result(ast) + :warn end - end - end - - # Any other tuple with a line - defp pre({_, meta, _} = ast, state) do - case Keyword.get(meta, :line) do - nil -> - {ast, state} - - line -> - state - |> add_current_env_to_line(line) - |> result(ast) - end - end - - defp pre(ast, state) do - {ast, state} - end - - defp post( - {:defmodule, _meta, [_module, _]} = ast, - state - ) do - post_module(ast, state) - end - - defp post( - {:defprotocol, _meta, [_protocol, _]} = ast, - state - ) do - post_protocol(ast, state) - end - - defp post( - {:defimpl, _meta, [_protocol, _impl_args | _]} = ast, - state - ) do - post_module(ast, state) - end - - # ex_unit describe - defp post( - {:describe, _meta, [name, _body]} = ast, - state - ) - when is_binary(name) do - %{state | context: Map.delete(state.context, :ex_unit_describe)} - |> result(ast) - end - - defp post({def_name, meta, [{name, _, _params} | _]} = ast, state) - when def_name in @defs and is_atom(name) do - if Keyword.get(meta, :func, false) do - post_func(ast, state) - else - {ast, state} - end - end - - defp post({def_name, _, _} = ast, state) when def_name in @defs do - {ast, state} - end - - defp post( - {:@, _meta_attr, - [{kind, _, [{:"::", _meta, _params = [{name, _, type_args}, _type_def]} = _spec]}]} = - ast, - state - ) - when kind in [:type, :typep, :opaque] and is_atom(name) and - (is_nil(type_args) or is_list(type_args)) do - state = - state - |> remove_last_scope_from_scopes - - {ast, state} - end - - defp post( - {:@, _meta_attr, - [ - {kind, _, - [ - {:when, _, [{:"::", _meta, _params = [{name, _, type_args}, _type_def]}, _]} = - _spec - ]} - ]} = ast, - state - ) - when kind in [:spec, :callback, :macrocallback] and is_atom(name) and - (is_nil(type_args) or is_list(type_args)) do - state = - state - |> remove_last_scope_from_scopes - - {ast, state} - end - - defp post( - {:@, _meta_attr, - [{kind, _, [{:"::", _meta, _params = [{name, _, type_args}, _type_def]} = _spec]}]} = - ast, - state - ) - when kind in [:spec, :callback, :macrocallback] and is_atom(name) and - (is_nil(type_args) or is_list(type_args)) do - state = - state - |> remove_last_scope_from_scopes - - {ast, state} - end - - defp post( - {:case, _meta, - [ - _condition_ast, - [ - do: _clauses - ] - ]} = ast, - state - ) do - state - |> pop_binding_context - |> result(ast) - end - - defp post({atom, _, [_ | _]} = ast, state) when atom in @scope_keywords do - post_scope_keyword(ast, state) - end - - defp post({atom, _block} = ast, state) when atom in @block_keywords do - post_block_keyword(ast, state) - end - - defp post({:->, _meta, [_lhs, _rhs]} = ast, state) do - post_clause(ast, state) - end - - defp post({:__generated__, _meta, inner}, state) do - {inner, %{state | generated: false}} - end - - defp post({atom, meta, [lhs, rhs]} = ast, state) - when atom in [:=, :<-] do - line = Keyword.fetch!(meta, :line) - match_context_r = get_binding_type(state, rhs) - - match_context_r = - if atom == :<- and match?([:for | _], state.binding_context) do - {:for_expression, match_context_r} - else - match_context_r - end - - vars_l = find_vars(state, lhs, match_context_r) - - vars = - case rhs do - {:=, _, [nested_lhs, _nested_rhs]} -> - match_context_l = get_binding_type(state, lhs) - nested_vars = find_vars(state, nested_lhs, match_context_l) - - vars_l ++ nested_vars - - _ -> - vars_l - end - |> merge_same_name_vars() - - # Variables and calls were added for the left side of the assignment in `pre` call, but without - # the context of an assignment. Thus, they have to be removed here. On their place there will - # be added new variables having types merged with types of corresponding deleted variables. - remove_positions = Enum.flat_map(vars, fn %VarInfo{positions: positions} -> positions end) - - state - |> remove_calls(remove_positions) - |> merge_new_vars(vars, remove_positions) - |> add_current_env_to_line(line) - |> result(ast) - end - - # String literal - defp post({_, [no_call: true, line: line, column: _column], [str]} = ast, state) - when is_binary(str) do - post_string_literal(ast, state, line, str) - end - - # String literal in sigils - defp post({:<<>>, [indentation: _, line: line, column: _column], [str]} = ast, state) - when is_binary(str) do - post_string_literal(ast, state, line, str) - end - - defp post(ast, state) do - {ast, state} - end - - defp result(state, ast) do - {ast, state} - end - - defp find_vars(state, ast, match_context \\ nil) - - defp find_vars(_state, {var, _meta, nil}, _) - when var in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__] do - # TODO local calls? - [] - end - - defp find_vars(_state, {var, meta, nil}, :rescue) when is_atom(var) do - line = Keyword.fetch!(meta, :line) - column = Keyword.fetch!(meta, :column) - match_context = {:struct, [], {:atom, Exception}, nil} - [%VarInfo{name: var, positions: [{line, column}], type: match_context, is_definition: true}] - end - - defp find_vars(state, ast, match_context) do - {_ast, {vars, _match_context}} = - Macro.prewalk(ast, {[], match_context}, &match_var(state, &1, &2)) - - vars - end - - defp match_var( - state, - {:in, _meta, - [ - left, - right - ]}, - {vars, _match_context} - ) do - exception_type = - case right do - [elem] -> - get_binding_type(state, elem) - - list when is_list(list) -> - types = for elem <- list, do: get_binding_type(state, elem) - if Enum.all?(types, &match?({:atom, _}, &1)), do: {:atom, Exception} - - elem -> - get_binding_type(state, elem) - end - - match_context = - case exception_type do - {:atom, atom} -> {:struct, [], {:atom, atom}, nil} - _ -> nil - end - - match_var(state, left, {vars, match_context}) - end - - defp match_var( - state, - {:=, _meta, - [ - left, - right - ]}, - {vars, _match_context} - ) do - {_ast, {vars, _match_context}} = - match_var(state, left, {vars, get_binding_type(state, right)}) - - {_ast, {vars, _match_context}} = - match_var(state, right, {vars, get_binding_type(state, left)}) - - {[], {vars, nil}} - end - - defp match_var( - _state, - {:^, _meta, [{var, meta, nil}]}, - {vars, match_context} = ast - ) - when is_atom(var) do - line = Keyword.fetch!(meta, :line) - column = Keyword.fetch!(meta, :column) - var_info = %VarInfo{name: var, positions: [{line, column}], type: match_context} - {ast, {[var_info | vars], nil}} - end - - defp match_var( - _state, - {var, meta, nil} = ast, - {vars, match_context} - ) - when is_atom(var) and - var not in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__] do - # TODO local calls? - # TODO {:__MODULE__, meta, nil} is not expanded here - line = Keyword.fetch!(meta, :line) - column = Keyword.fetch!(meta, :column) - - var_info = %VarInfo{ - name: var, - positions: [{line, column}], - type: match_context, - is_definition: true } - - {ast, {[var_info | vars], nil}} - end - - # drop right side of guard expression as guards cannot define vars - defp match_var(state, {:when, _, [left, _right]}, {vars, _match_context}) do - match_var(state, left, {vars, nil}) - end - - defp match_var(state, {:%, _, [type_ast, {:%{}, _, ast}]}, {vars, match_context}) - when not is_nil(match_context) do - {_ast, {type_vars, _match_context}} = match_var(state, type_ast, {[], nil}) - - destructured_vars = - ast - |> Enum.flat_map(fn {key, value_ast} -> - key_type = get_binding_type(state, key) - - {_ast, {new_vars, _match_context}} = - match_var(state, value_ast, {[], {:map_key, match_context, key_type}}) - - new_vars - end) - - {ast, {vars ++ destructured_vars ++ type_vars, nil}} - end - - defp match_var(state, {:%{}, _, ast}, {vars, match_context}) when not is_nil(match_context) do - destructured_vars = - ast - |> Enum.flat_map(fn {key, value_ast} -> - key_type = get_binding_type(state, key) - - {_ast, {new_vars, _match_context}} = - match_var(state, value_ast, {[], {:map_key, match_context, key_type}}) - - new_vars - end) - - {ast, {vars ++ destructured_vars, nil}} - end - - # regular tuples use {:{}, [], [field_1, field_2]} ast - # two element use `{field_1, field_2}` ast (probably as an optimization) - # detect and convert to regular - defp match_var(state, ast, {vars, match_context}) - when is_tuple(ast) and tuple_size(ast) == 2 do - match_var(state, {:{}, [], ast |> Tuple.to_list()}, {vars, match_context}) - end - - defp match_var(state, {:{}, _, ast}, {vars, match_context}) when not is_nil(match_context) do - indexed = ast |> Enum.with_index() - total = length(ast) - - destructured_vars = - indexed - |> Enum.flat_map(fn {nth_elem_ast, n} -> - bond = - {:tuple, total, - indexed |> Enum.map(&if(n != elem(&1, 1), do: get_binding_type(state, elem(&1, 0))))} - - match_context = - if match_context != bond do - {:intersection, [match_context, bond]} - else - match_context - end - - {_ast, {new_vars, _match_context}} = - match_var(state, nth_elem_ast, {[], {:tuple_nth, match_context, n}}) - - new_vars - end) - - {ast, {vars ++ destructured_vars, nil}} - end - - # two element tuples on the left of `->` are encoded as list `[field1, field2]` - # detect and convert to regular - defp match_var(state, {:->, meta, [[left], right]}, {vars, match_context}) do - match_var(state, {:->, meta, [left, right]}, {vars, match_context}) - end - - defp match_var(state, list, {vars, match_context}) - when not is_nil(match_context) and is_list(list) do - match_var_list = fn head, tail -> - {_ast, {new_vars_head, _match_context}} = - match_var(state, head, {[], {:list_head, match_context}}) - - {_ast, {new_vars_tail, _match_context}} = - match_var(state, tail, {[], {:list_tail, match_context}}) - - {list, {vars ++ new_vars_head ++ new_vars_tail, nil}} - end - - case list do - [] -> - {list, {vars, nil}} - - [{:|, _, [head, tail]}] -> - match_var_list.(head, tail) - - [head | tail] -> - match_var_list.(head, tail) - end - end - - defp match_var(_state, ast, {vars, match_context}) do - {ast, {vars, match_context}} - end - - def infer_type_from_guards(guard_ast, vars, state) do - type_info = Guard.type_information_from_guards(guard_ast, state) - - Enum.reduce(type_info, vars, fn {var, type}, acc -> - index = Enum.find_index(acc, &(&1.name == var)) - - if index, - do: List.update_at(acc, index, &Map.put(&1, :type, type)), - else: acc - end) - end - - # struct or struct update - def get_binding_type( - state, - {:%, _meta, - [ - struct_ast, - {:%{}, _, _} = ast - ]} - ) do - {fields, updated_struct} = - case get_binding_type(state, ast) do - {:map, fields, updated_map} -> {fields, updated_map} - {:struct, fields, _, updated_struct} -> {fields, updated_struct} - _ -> {[], nil} - end - - # expand struct type - only compile type atoms or attributes are supported - type = - case get_binding_type(state, struct_ast) do - {:atom, atom} -> {:atom, atom} - {:attribute, attribute} -> {:attribute, attribute} - _ -> nil - end - - {:struct, fields, type, updated_struct} - end - - # pipe - def get_binding_type(state, {:|>, _, [params_1, {call, meta, params_rest}]}) do - params = [params_1 | params_rest || []] - get_binding_type(state, {call, meta, params}) - end - - # remote call - def get_binding_type(state, {{:., _, [target, fun]}, _, args}) - when is_atom(fun) and is_list(args) do - target = get_binding_type(state, target) - {:call, target, fun, Enum.map(args, &get_binding_type(state, &1))} end - # current module - def get_binding_type(state, {:__MODULE__, _, nil} = module) do - {module, _state, _env} = expand(module, state) - {:atom, module} - end - - # elixir module - def get_binding_type(state, {:__aliases__, _, list} = module) when is_list(list) do - try do - {module, _state, _env} = expand(module, state) - {:atom, module} - rescue - _ -> nil - end - end - - # variable or local no parens call - def get_binding_type(_state, {var, _, nil}) when is_atom(var) do - {:variable, var} - end - - # attribute - def get_binding_type(_state, {:@, _, [{attribute, _, nil}]}) - when is_atom(attribute) do - {:attribute, attribute} - end - - # erlang module or atom - def get_binding_type(_state, atom) when is_atom(atom) do - {:atom, atom} - end - - # map update - def get_binding_type( - state, - {:%{}, _meta, - [ - {:|, _meta1, - [ - updated_map, - fields - ]} - ]} - ) - when is_list(fields) do - {:map, get_fields_binding_type(state, fields), get_binding_type(state, updated_map)} - end - - # map - def get_binding_type(state, {:%{}, _meta, fields}) when is_list(fields) do - {:map, get_fields_binding_type(state, fields), nil} - end - - # match - def get_binding_type(state, {:=, _, [_, ast]}) do - get_binding_type(state, ast) - end - - # stepped range struct - def get_binding_type(_state, {:"..//", _, [_, _, _]}) do - {:struct, [], {:atom, Range}} - end - - # range struct - def get_binding_type(_state, {:.., _, [_, _]}) do - {:struct, [], {:atom, Range}} - end - - @builtin_sigils %{ - sigil_D: Date, - sigil_T: Time, - sigil_U: DateTime, - sigil_N: NaiveDateTime, - sigil_R: Regex, - sigil_r: Regex - } - - # builtin sigil struct - def get_binding_type(_state, {sigil, _, _}) when is_map_key(@builtin_sigils, sigil) do - # TODO support custom sigils - {:struct, [], {:atom, @builtin_sigils |> Map.fetch!(sigil)}} - end - - # tuple - # regular tuples use {:{}, [], [field_1, field_2]} ast - # two element use {field_1, field_2} ast (probably as an optimization) - # detect and convert to regular - def get_binding_type(state, ast) when is_tuple(ast) and tuple_size(ast) == 2 do - get_binding_type(state, {:{}, [], Tuple.to_list(ast)}) - end - - def get_binding_type(state, {:{}, _, list}) do - {:tuple, length(list), list |> Enum.map(&get_binding_type(state, &1))} - end - - def get_binding_type(state, list) when is_list(list) do - type = - case list do - [] -> :empty - [{:|, _, [head, _tail]}] -> get_binding_type(state, head) - [head | _] -> get_binding_type(state, head) - end - - {:list, type} - end - - def get_binding_type(state, list) when is_list(list) do - {:list, list |> Enum.map(&get_binding_type(state, &1))} - end - - # pinned variable - def get_binding_type(state, {:^, _, [pinned]}), do: get_binding_type(state, pinned) - - # local call - def get_binding_type(state, {var, _, args}) when is_atom(var) and is_list(args) do - {:local_call, var, Enum.map(args, &get_binding_type(state, &1))} - end - - # integer - def get_binding_type(_state, integer) when is_integer(integer) do - {:integer, integer} - end - - # other - def get_binding_type(_state, _), do: nil - - defp get_fields_binding_type(state, fields) do - for {field, value} <- fields, - is_atom(field) do - {field, get_binding_type(state, value)} - end - end - - defp add_no_call(meta) do - [{:no_call, true} | meta] - end - - defp pre_protocol_implementation( - ast, - state, - meta, - protocol, - for_expression - ) do - # TODO pass env - {protocol, state, _env} = expand(protocol, state) - implementations = get_implementations_from_for_expression(state, for_expression) - - pre_module(ast, state, meta, protocol, [], [{:__impl__, [:atom], :def}], for: implementations) - end - - defp get_implementations_from_for_expression(state, for: for_expression) do - # TODO fold state? - for_expression - |> List.wrap() - |> Enum.map(fn alias -> - {module, _state, _env} = expand(alias, state) - module - end) - end - - defp get_implementations_from_for_expression(state, _other) do - [state |> get_current_module] - end - - defp maybe_add_protocol_behaviour({protocol, _}, state, env) do - {_, state, env} = add_behaviour(protocol, state, env) - {state, env} - end - - defp maybe_add_protocol_behaviour(_, state, env), do: {state, env} - - defp expand_aliases_in_ast(state, ast) do - # TODO shouldn't that handle more cases? - Macro.prewalk(ast, fn - {:__aliases__, meta, [Elixir]} -> - {:__aliases__, meta, [Elixir]} - - {:__aliases__, meta, _list} = module -> - {module, _state, _env} = expand(module, state) - list = module |> Module.split() |> Enum.map(&String.to_atom/1) - {:__aliases__, meta, list} - - {:__MODULE__, meta, nil} = module -> - {module, _state, _env} = expand(module, state) - list = module |> Module.split() |> Enum.map(&String.to_atom/1) - {:__aliases__, meta, list} - - other -> - other - end) - end - - defp ex_unit_test_name(state, name) do - case state.context[:ex_unit_describe] do - nil -> "test #{name}" - describe -> "test #{describe} #{name}" - end - |> String.to_atom() - end + def default_env(cursor_position \\ nil) do + macro_env = Compiler.env() + state_initial = initial_state(cursor_position) + State.get_current_env(state_initial, macro_env) + end + + # defp post_string_literal(ast, _state, _line, str) do + # str + # |> Source.split_lines() + # |> Enum.with_index() + # # |> Enum.reduce(state, fn {_s, i}, acc -> add_current_env_to_line(acc, line + i) end) + # # |> result(ast) + # end + + # # Any other tuple with a line + # defp pre({_, meta, _} = ast, state) do + # case Keyword.get(meta, :line) do + # nil -> + # {ast, state} + + # _line -> + # state + # # |> add_current_env_to_line(line) + # # |> result(ast) + # end + # end + + # # String literal + # defp post({_, [no_call: true, line: line, column: _column], [str]} = ast, state) + # when is_binary(str) do + # post_string_literal(ast, state, line, str) + # end + + # # String literal in sigils + # defp post({:<<>>, [indentation: _, line: line, column: _column], [str]} = ast, state) + # when is_binary(str) do + # post_string_literal(ast, state, line, str) + # end end diff --git a/lib/elixir_sense/core/normalized/code/formatter.ex b/lib/elixir_sense/core/normalized/code/formatter.ex index 800114c4..96c0e0d1 100644 --- a/lib/elixir_sense/core/normalized/code/formatter.ex +++ b/lib/elixir_sense/core/normalized/code/formatter.ex @@ -5,7 +5,6 @@ defmodule ElixirSense.Core.Normalized.Code.Formatter do apply(Code.Formatter, :locals_without_parens, []) true -> - # fall back to bundled on < 1.13 # on 1.13 use our version as it has all the fixes from last 1.13 release apply(ElixirSense.Core.Normalized.Code.ElixirSense.Formatter, :locals_without_parens, []) end @@ -17,7 +16,6 @@ defmodule ElixirSense.Core.Normalized.Code.Formatter do apply(Code.Formatter, :local_without_parens?, [fun, arity, locals_without_parens]) true -> - # fall back to bundled on < 1.13 # on 1.13 use our version as it has all the fixes from last 1.13 release apply(ElixirSense.Core.Normalized.Code.ElixirSense.Formatter, :local_without_parens?, [ fun, diff --git a/lib/elixir_sense/core/normalized/code/fragment.ex b/lib/elixir_sense/core/normalized/code/fragment.ex index 3774b06c..3c41d30e 100644 --- a/lib/elixir_sense/core/normalized/code/fragment.ex +++ b/lib/elixir_sense/core/normalized/code/fragment.ex @@ -8,7 +8,6 @@ defmodule ElixirSense.Core.Normalized.Code.Fragment do apply(Code.Fragment, :cursor_context, [string, opts]) true -> - # fall back to bundled on < 1.13 # on 1.13 use our version as it has all the fixes from last 1.13 release apply(ElixirSense.Core.Normalized.Code.ElixirSense.Fragment, :cursor_context, [ string, @@ -34,7 +33,6 @@ defmodule ElixirSense.Core.Normalized.Code.Fragment do apply(Code.Fragment, :surround_context, [fragment, position, options]) true -> - # fall back to bundled on < 1.13 # on 1.13 use our version as it has all the fixes from last 1.13 release apply(ElixirSense.Core.Normalized.Code.ElixirSense.Fragment, :surround_context, [ fragment, @@ -61,7 +59,6 @@ defmodule ElixirSense.Core.Normalized.Code.Fragment do apply(Code.Fragment, :container_cursor_to_quoted, [fragment, opts]) true -> - # fall back to bundled on < 1.13 # on 1.13 use our version as it has all the fixes from last 1.13 release apply( ElixirSense.Core.Normalized.Code.ElixirSense.Fragment, diff --git a/lib/elixir_sense/core/normalized/macro/env.ex b/lib/elixir_sense/core/normalized/macro/env.ex new file mode 100644 index 00000000..211b47c5 --- /dev/null +++ b/lib/elixir_sense/core/normalized/macro/env.ex @@ -0,0 +1,683 @@ +defmodule ElixirSense.Core.Normalized.Macro.Env do + if Version.match?(System.version(), ">= 1.17.0-dev") do + defdelegate expand_import(env, meta, fun, arity, opts), to: Macro.Env + defdelegate expand_require(env, meta, module, fun, arity, opts), to: Macro.Env + defdelegate expand_alias(env, meta, list, opts), to: Macro.Env + defdelegate define_alias(env, meta, arg, opts), to: Macro.Env + defdelegate define_require(env, meta, arg, opts), to: Macro.Env + defdelegate define_import(env, meta, arg, opts), to: Macro.Env + else + def fake_expand_callback(_meta, _args) do + {:__block__, [], []} + end + + def expand_import(env, meta, name, arity, opts \\ []) + when is_list(meta) and is_atom(name) and is_integer(arity) and is_list(opts) do + case Macro.special_form?(name, arity) do + true -> + {:error, :not_found} + + false -> + allow_locals = Keyword.get(opts, :allow_locals, true) + trace = Keyword.get(opts, :trace, true) + module = env.module + + extra = + case allow_locals and function_exported?(module, :__info__, 1) do + true -> [{module, module.__info__(:macros)}] + false -> [] + end + + case __MODULE__.Dispatch.expand_import( + meta, + name, + arity, + env, + extra, + allow_locals, + trace + ) do + {:macro, receiver, expander} -> + {:macro, receiver, wrap_expansion(receiver, expander, meta, name, arity, env, opts)} + + {:function, receiver, name} -> + {:function, receiver, name} + + error -> + {:error, error} + end + end + end + + def expand_require(env, meta, module, name, arity, opts \\ []) + when is_list(meta) and is_atom(module) and is_atom(name) and is_integer(arity) and + is_list(opts) do + trace = Keyword.get(opts, :trace, true) + + case __MODULE__.Dispatch.expand_require(meta, module, name, arity, env, trace) do + {:macro, receiver, expander} -> + {:macro, receiver, wrap_expansion(receiver, expander, meta, name, arity, env, opts)} + + :error -> + :error + end + end + + defp wrap_expansion(receiver, expander, _meta, _name, _arity, env, _opts) do + fn expansion_meta, args -> + quoted = expander.(args, env) + next = :elixir_module.next_counter(env.module) + + if Version.match?(System.version(), ">= 1.14.0-dev") do + :elixir_quote.linify_with_context_counter(expansion_meta, {receiver, next}, quoted) + else + :elixir_quote.linify_with_context_counter( + expansion_meta |> Keyword.get(:line, 0), + {receiver, next}, + quoted + ) + end + end + end + + def expand_alias(env, meta, list, opts \\ []) + when is_list(meta) and is_list(list) and is_list(opts) do + trace = Keyword.get(opts, :trace, true) + + case __MODULE__.Aliases.expand(meta, list, env, trace) do + atom when is_atom(atom) -> {:alias, atom} + [_ | _] -> :error + end + end + + def define_require(env, meta, module, opts \\ []) + when is_list(meta) and is_atom(module) and is_list(opts) do + {trace, opts} = Keyword.pop(opts, :trace, true) + env = __MODULE__.Aliases.require(meta, module, opts, env, trace) + result = __MODULE__.Aliases.alias(meta, module, false, opts, env, trace) + maybe_define_error(result, :elixir_aliases) + end + + def define_alias(env, meta, module, opts \\ []) + when is_list(meta) and is_atom(module) and is_list(opts) do + {trace, opts} = Keyword.pop(opts, :trace, true) + result = __MODULE__.Aliases.alias(meta, module, true, opts, env, trace) + maybe_define_error(result, :elixir_aliases) + end + + def define_import(env, meta, module, opts \\ []) + when is_list(meta) and is_atom(module) and is_list(opts) do + {trace, opts} = Keyword.pop(opts, :trace, true) + {warnings, opts} = Keyword.pop(opts, :emit_warnings, true) + {info_callback, opts} = Keyword.pop(opts, :info_callback, &module.__info__/1) + + result = __MODULE__.Import.import(meta, module, opts, env, warnings, trace, info_callback) + maybe_define_error(result, :elixir_import) + end + + defp maybe_define_error({:ok, env}, _mod), + do: {:ok, env} + + defp maybe_define_error({:error, reason}, _mod), + do: {:error, inspect(reason)} + + defmodule Aliases do + def require(_meta, ref, _opts, e, _trace) do + %{e | requires: :ordsets.add_element(ref, e.requires)} + end + + def alias(meta, ref, include_by_default, opts, e, _trace) do + %{aliases: aliases, macro_aliases: macro_aliases} = e + + case expand_as(:lists.keyfind(:as, 1, opts), include_by_default, ref) do + {:ok, ^ref} -> + {:ok, + %{ + e + | aliases: remove_alias(ref, aliases), + macro_aliases: remove_macro_alias(meta, ref, macro_aliases) + }} + + {:ok, new} -> + {:ok, + %{ + e + | aliases: store_alias(new, ref, aliases), + macro_aliases: store_macro_alias(meta, new, ref, macro_aliases) + }} + + :none -> + {:ok, e} + + {:error, reason} -> + {:error, reason} + end + end + + defp expand_as({:as, atom}, _include_by_default, _ref) + when is_atom(atom) and not is_boolean(atom) do + case Atom.to_charlist(atom) do + ~c"Elixir." ++ ([first_letter | _] = rest) when first_letter in ?A..?Z -> + case :string.tokens(rest, ~c".") do + [_] -> + {:ok, atom} + + _ -> + {:error, {:invalid_alias_for_as, :nested_alias, atom}} + end + + _ -> + {:error, {:invalid_alias_for_as, :not_alias, atom}} + end + end + + defp expand_as({:as, other}, _include_by_default, _ref) do + {:error, {:invalid_alias_for_as, :not_alias, other}} + end + + defp expand_as(false, true, ref) do + case Atom.to_charlist(ref) do + ~c"Elixir." ++ [first_letter | _] = list when first_letter in ?A..?Z -> + last = last(Enum.reverse(list), []) + {:ok, :"Elixir.#{last}"} + + _ -> + {:error, {:invalid_alias_module, ref}} + end + end + + defp expand_as(false, false, _ref) do + :none + end + + defp last([?. | _], acc), do: acc + defp last([h | t], acc), do: last(t, [h | acc]) + defp last([], acc), do: acc + + defp store_alias(new, old, aliases) do + :lists.keystore(new, 1, aliases, {new, old}) + end + + defp store_macro_alias(meta, new, old, aliases) do + case :lists.keyfind(:counter, 1, meta) do + {:counter, counter} -> + :lists.keystore(new, 1, aliases, {new, {counter, old}}) + + false -> + aliases + end + end + + defp remove_alias(atom, aliases) do + :lists.keydelete(atom, 1, aliases) + end + + defp remove_macro_alias(meta, atom, aliases) do + case :lists.keyfind(:counter, 1, meta) do + {:counter, _counter} -> + :lists.keydelete(atom, 1, aliases) + + false -> + aliases + end + end + + def expand(_meta, [Elixir | _] = list, _e, _trace) do + list + end + + def expand(_meta, [h | _] = list, _e, _trace) when not is_atom(h) do + list + end + + def expand(meta, list, %{aliases: aliases, macro_aliases: macro_aliases} = e, trace) do + case :lists.keyfind(:alias, 1, meta) do + {:alias, false} -> + expand(meta, list, macro_aliases, e, trace) + + {:alias, atom} when is_atom(atom) -> + atom + + false -> + expand(meta, list, aliases, e, trace) + end + end + + def expand(meta, [h | t], aliases, _e, _trace) do + lookup = String.to_atom("Elixir." <> Atom.to_string(h)) + + counter = + case :lists.keyfind(:counter, 1, meta) do + {:counter, c} -> c + _ -> nil + end + + case lookup(lookup, aliases, counter) do + ^lookup -> + [h | t] + + atom -> + case t do + [] -> atom + _ -> Module.concat([atom | t]) + end + end + end + + defp lookup(else_val, list, counter) do + case :lists.keyfind(else_val, 1, list) do + {^else_val, {^counter, value}} -> value + {^else_val, value} when is_atom(value) -> value + _ -> else_val + end + end + end + + defmodule Import do + alias ElixirSense.Core.Normalized.Macro.Env.Aliases + + def import(meta, ref, opts, e, warn, trace) do + import(meta, ref, opts, e, warn, trace, &ref.__info__/1) + end + + def import(meta, ref, opts, e, warn, trace, info_callback) do + case import_only_except(meta, ref, opts, e, warn, info_callback) do + {functions, macros, _added} -> + ei = %{e | functions: functions, macros: macros} + {:ok, Aliases.require(meta, ref, opts, ei, trace)} + + {:error, reason} -> + {:error, reason} + end + end + + defp import_only_except(meta, ref, opts, e, warn, info_callback) do + maybe_only = :lists.keyfind(:only, 1, opts) + + case :lists.keyfind(:except, 1, opts) do + false -> + import_only_except(meta, ref, maybe_only, false, e, warn, info_callback) + + {:except, dup_except} when is_list(dup_except) -> + case ensure_keyword_list(dup_except) do + :ok -> + except = ensure_no_duplicates(dup_except, :except, meta, e, warn) + import_only_except(meta, ref, maybe_only, except, e, warn, info_callback) + + :error -> + {:error, {:invalid_option, :except, dup_except}} + end + + {:except, other} -> + {:error, {:invalid_option, :except, other}} + end + end + + def import_only_except(meta, ref, maybe_only, except, e, warn, info_callback) do + case maybe_only do + {:only, :functions} -> + {added1, _used1, funs} = import_functions(meta, ref, except, e, warn, info_callback) + {funs, :lists.keydelete(ref, 1, e.macros), added1} + + {:only, :macros} -> + {added2, _used2, macs} = import_macros(meta, ref, except, e, warn, info_callback) + {:lists.keydelete(ref, 1, e.functions), macs, added2} + + {:only, :sigils} -> + {added1, _used1, funs} = + import_sigil_functions(meta, ref, except, e, warn, info_callback) + + {added2, _used2, macs} = + import_sigil_macros(meta, ref, except, e, warn, info_callback) + + {funs, macs, added1 or added2} + + {:only, dup_only} when is_list(dup_only) -> + case ensure_keyword_list(dup_only) do + :ok when except == false -> + only = ensure_no_duplicates(dup_only, :only, meta, e, warn) + + {added1, _used1, funs} = + import_listed_functions(meta, ref, only, e, warn, info_callback) + + {added2, _used2, macs} = + import_listed_macros(meta, ref, only, e, warn, info_callback) + + # for {name, arity} <- (only -- used1) -- used2, warn, do: elixir_errors.file_warn(meta, e, __MODULE__, {:invalid_import, {ref, name, arity}}) + {funs, macs, added1 or added2} + + :ok -> + {:error, :only_and_except_given} + + :error -> + {:error, {:invalid_option, :only, dup_only}} + end + + {:only, other} -> + {:error, {:invalid_option, :only, other}} + + false -> + {added1, _used1, funs} = import_functions(meta, ref, except, e, warn, info_callback) + {added2, _used2, macs} = import_macros(meta, ref, except, e, warn, info_callback) + {funs, macs, added1 or added2} + end + end + + def import_listed_functions(meta, ref, only, e, warn, info_callback) do + new = intersection(only, get_functions(ref, info_callback)) + calculate_key(meta, ref, Map.get(e, :functions), new, e, warn) + end + + def import_listed_macros(meta, ref, only, e, warn, info_callback) do + new = intersection(only, get_macros(info_callback)) + calculate_key(meta, ref, Map.get(e, :macros), new, e, warn) + end + + def import_functions(meta, ref, except, e, warn, info_callback) do + calculate_except(meta, ref, except, Map.get(e, :functions), e, warn, fn -> + get_functions(ref, info_callback) + end) + end + + def import_macros(meta, ref, except, e, warn, info_callback) do + calculate_except(meta, ref, except, Map.get(e, :macros), e, warn, fn -> + get_macros(info_callback) + end) + end + + def import_sigil_functions(meta, ref, except, e, warn, info_callback) do + calculate_except(meta, ref, except, Map.get(e, :functions), e, warn, fn -> + filter_sigils(info_callback.(:functions)) + end) + end + + def import_sigil_macros(meta, ref, except, e, warn, info_callback) do + calculate_except(meta, ref, except, Map.get(e, :macros), e, warn, fn -> + filter_sigils(info_callback.(:macros)) + end) + end + + defp calculate_except(meta, key, false, old, e, warn, existing) do + new = remove_underscored(existing.()) + calculate_key(meta, key, old, new, e, warn) + end + + defp calculate_except(meta, key, except, old, e, warn, existing) do + new = + case :lists.keyfind(key, 1, old) do + false -> remove_underscored(existing.()) -- except + {^key, old_imports} -> old_imports -- except + end + + calculate_key(meta, key, old, new, e, warn) + end + + defp calculate_key(meta, key, old, new, e, warn) do + case :ordsets.from_list(new) do + [] -> + {false, [], :lists.keydelete(key, 1, old)} + + set -> + final_set = ensure_no_special_form_conflict(set, key, meta, e, warn) + {true, final_set, [{key, final_set} | :lists.keydelete(key, 1, old)]} + end + end + + defp get_functions(module, info_callback) do + try do + info_callback.(:functions) + catch + _, _ -> remove_internals(module.module_info(:exports)) + end + end + + defp get_macros(info_callback) do + try do + info_callback.(:macros) + catch + _, _ -> [] + end + end + + defp filter_sigils(funs) do + Enum.filter(funs, &is_sigil/1) + end + + defp is_sigil({name, 2}) do + case Atom.to_string(name) do + "sigil_" <> letters -> + case letters do + <> when l in ?a..?z -> + true + + "" -> + false + + <> when h in ?A..?Z -> + String.to_charlist(t) + |> Enum.all?(fn l -> l in ?0..?9 or l in ?A..?Z end) + + _ -> + false + end + + _ -> + false + end + end + + defp is_sigil(_) do + false + end + + defp intersection([h | t], all) do + if Enum.member?(all, h) do + [h | intersection(t, all)] + else + intersection(t, all) + end + end + + defp intersection([], _all) do + [] + end + + defp remove_underscored(list) do + Enum.filter(list, fn {name, _} -> + case Atom.to_string(name) do + "_" <> _ -> false + _ -> true + end + end) + end + + if Version.match?(System.version(), ">= 1.15.0-dev") do + defp remove_internals(set) do + set -- [{:behaviour_info, 1}, {:module_info, 1}, {:module_info, 0}] + end + else + defp remove_internals(set) do + set -- [{:module_info, 1}, {:module_info, 0}] + end + end + + defp ensure_keyword_list([]) do + :ok + end + + defp ensure_keyword_list([{key, value} | rest]) when is_atom(key) and is_integer(value) do + ensure_keyword_list(rest) + end + + defp ensure_keyword_list(_other) do + :error + end + + defp ensure_no_special_form_conflict(set, _key, _meta, _e, _warn) do + Enum.filter(set, fn {name, arity} -> + if Macro.special_form?(name, arity) do + false + else + true + end + end) + end + + defp ensure_no_duplicates(option, _kind, _meta, _e, _warn) do + Enum.reduce(option, [], fn {name, arity}, acc -> + if Enum.member?(acc, {name, arity}) do + acc + else + [{name, arity} | acc] + end + end) + end + end + + defmodule Dispatch do + def expand_import(meta, name, arity, e, extra, allow_locals, trace) do + tuple = {name, arity} + module = e.module + dispatch = find_import_by_name_arity(meta, tuple, extra, e) + + case dispatch do + {:ambiguous, ambiguous} -> + {:ambiguous, ambiguous} + + {:import, _} -> + do_expand_import(dispatch, meta, name, arity, module, e, trace) + + _ -> + local = + allow_locals and + if Version.match?(System.version(), ">= 1.14.0-dev") do + :elixir_def.local_for(meta, name, arity, [:defmacro, :defmacrop], e) + else + :elixir_def.local_for(module, name, arity, [:defmacro, :defmacrop]) + end + + case dispatch do + {_, receiver} when local != false and receiver != module -> + {:conflict, receiver} + + _ when local == false -> + do_expand_import(dispatch, meta, name, arity, module, e, trace) + + _ -> + {:macro, module, expander_macro_fun(meta, local, module, name, e)} + end + end + end + + defp do_expand_import(result, meta, name, arity, module, e, trace) do + case result do + {:function, receiver} -> + {:function, receiver, name} + + {:macro, receiver} -> + {:macro, receiver, expander_macro_named(meta, receiver, name, arity, e)} + + {:import, receiver} -> + case expand_require(true, meta, receiver, name, arity, e, trace) do + {:macro, _, _} = response -> response + :error -> {:function, receiver, name} + end + + false when module == Kernel -> + case :elixir_rewrite.inline(module, name, arity) do + {ar, an} -> {:function, ar, an} + false -> :not_found + end + + false -> + :not_found + end + end + + def expand_require(meta, receiver, name, arity, e, trace) do + required = + receiver == e.module or + :lists.keyfind(:required, 1, meta) == {:required, true} or + receiver in e.requires + + expand_require(required, meta, receiver, name, arity, e, trace) + end + + defp expand_require(required, meta, receiver, name, arity, e, _trace) do + if is_macro(name, arity, receiver, required) do + {:macro, receiver, expander_macro_named(meta, receiver, name, arity, e)} + else + :error + end + end + + defp expander_macro_fun(meta, fun, receiver, name, e) do + fn args, caller -> expand_macro_fun(meta, fun, receiver, name, args, caller, e) end + end + + defp expander_macro_named(meta, receiver, name, arity, e) do + proper_name = :"MACRO-#{name}" + proper_arity = arity + 1 + fun = Function.capture(receiver, proper_name, proper_arity) + fn args, caller -> expand_macro_fun(meta, fun, receiver, name, args, caller, e) end + end + + defp expand_macro_fun(_meta, fun, _receiver, _name, args, caller, _e) do + apply(fun, [caller | args]) + end + + defp is_macro(_name, _arity, _module, false), do: false + + defp is_macro(name, arity, receiver, true) do + try do + macros = receiver.__info__(:macros) + :lists.member({name, arity}, macros) + rescue + _ -> false + end + end + + defp find_import_by_name_arity(meta, {_name, arity} = tuple, extra, e) do + case is_import(meta, arity) do + {:import, _} = import_res -> + import_res + + false -> + funs = e.functions + macs = extra ++ e.macros + fun_match = find_import_by_name_arity(tuple, funs) + mac_match = find_import_by_name_arity(tuple, macs) + + case {fun_match, mac_match} do + {[], [receiver]} -> + {:macro, receiver} + + {[receiver], []} -> + {:function, receiver} + + {[], []} -> + false + + _ -> + {:ambiguous, fun_match ++ mac_match} + end + end + end + + defp find_import_by_name_arity(tuple, list) do + import :ordsets, only: [is_element: 2] + for {receiver, set} <- list, is_element(tuple, set), do: receiver + end + + defp is_import(meta, arity) do + with {:ok, imports = [_ | _]} <- Keyword.fetch(meta, :imports), + {:ok, _} <- Keyword.fetch(meta, :context), + {_arity, receiver} <- :lists.keyfind(arity, 1, imports) do + {:import, receiver} + else + _ -> false + end + end + end + end +end diff --git a/lib/elixir_sense/core/normalized/tokenizer.ex b/lib/elixir_sense/core/normalized/tokenizer.ex index 57aca21b..19954b2e 100644 --- a/lib/elixir_sense/core/normalized/tokenizer.ex +++ b/lib/elixir_sense/core/normalized/tokenizer.ex @@ -18,7 +18,6 @@ defmodule ElixirSense.Core.Normalized.Tokenizer do if Version.match?(System.version(), ">= 1.14.0-dev") do :elixir_tokenizer.tokenize(prefix_charlist, 1, []) else - # fall back to bundled on < 1.13 # on 1.13 use our version as it has all the fixes from last 1.13 release :elixir_sense_tokenizer.tokenize(prefix_charlist, 1, []) end diff --git a/lib/elixir_sense/core/normalized/typespec.ex b/lib/elixir_sense/core/normalized/typespec.ex index 2d7d95bb..38d0b2ce 100644 --- a/lib/elixir_sense/core/normalized/typespec.ex +++ b/lib/elixir_sense/core/normalized/typespec.ex @@ -81,7 +81,6 @@ defmodule ElixirSense.Core.Normalized.Typespec do if Version.match?(System.version(), ">= 1.14.0-dev") do Code.Typespec else - # fall back to bundled on < 1.13 (1.12 is broken on OTP 24) # on 1.13 use our version as it has all the fixes from last 1.13 release ElixirSense.Core.Normalized.Code.ElixirSense.Typespec end diff --git a/lib/elixir_sense/core/parser.ex b/lib/elixir_sense/core/parser.ex index 54724bcb..88036c04 100644 --- a/lib/elixir_sense/core/parser.ex +++ b/lib/elixir_sense/core/parser.ex @@ -45,11 +45,15 @@ defmodule ElixirSense.Core.Parser do fallback_to_container_cursor_to_quoted: try_to_fix_parse_error ] - case string_to_ast(source, string_to_ast_options) do + # source_with_cursor = inject_cursor(source, cursor_position) + source_with_cursor = source + + case string_to_ast(source_with_cursor, string_to_ast_options) do {:ok, ast, modified_source, error} -> - acc = MetadataBuilder.build(ast) + acc = MetadataBuilder.build(ast, cursor_position) - if cursor_position == nil or Map.has_key?(acc.lines_to_env, elem(cursor_position, 0)) or + if cursor_position == nil or acc.cursor_env != nil or + Map.has_key?(acc.lines_to_env, elem(cursor_position, 0)) or !try_to_fix_line_not_found do create_metadata(source, {:ok, acc, error}) else @@ -186,6 +190,8 @@ defmodule ElixirSense.Core.Parser do specs: acc.specs, structs: acc.structs, mods_funs_to_positions: acc.mods_funs_to_positions, + cursor_env: acc.cursor_env, + closest_env: acc.closest_env, lines_to_env: acc.lines_to_env, vars_info_per_scope_id: acc.vars_info_per_scope_id, calls: acc.calls, @@ -218,7 +224,7 @@ defmodule ElixirSense.Core.Parser do |> List.update_at(line_number - 1, fn line -> # try to replace token do with do: marker line - |> String.replace("do", "do: " <> marker(line_number), global: false) + |> String.replace("do", "do: " <> marker(), global: false) end) |> Enum.join("\n") end @@ -325,7 +331,7 @@ defmodule ElixirSense.Core.Parser do |> List.update_at(line_number - 1, fn line -> # try to prepend unexpected terminator with marker line - |> String.replace(terminator, marker(line_number) <> " " <> terminator, global: false) + |> String.replace(terminator, marker() <> " " <> terminator, global: false) end) |> Enum.join("\n") end @@ -448,7 +454,7 @@ defmodule ElixirSense.Core.Parser do replaced_line = case terminator do - "end" -> previous <> "; " <> marker(length(rest)) <> "; end" + "end" -> previous <> "; " <> marker() <> "; end" _ -> previous <> " " <> terminator end @@ -515,7 +521,7 @@ defmodule ElixirSense.Core.Parser do |> Source.split_lines() # by replacing a line here we risk introducing a syntax error # instead we append marker to the existing line - |> List.update_at(line_number - 1, &(&1 <> "; " <> marker(line_number))) + |> List.update_at(line_number - 1, &(&1 <> "; " <> marker())) |> Enum.join("\n") end @@ -523,7 +529,7 @@ defmodule ElixirSense.Core.Parser do # IO.puts :stderr, "REPLACING LINE: #{line}" source |> Source.split_lines() - |> List.replace_at(line_number - 1, marker(line_number)) + |> List.replace_at(line_number - 1, marker()) |> Enum.join("\n") end @@ -535,7 +541,7 @@ defmodule ElixirSense.Core.Parser do |> Enum.join("\n") end - defp marker(line_number), do: "(__atom_elixir_marker_#{line_number}__())" + defp marker(), do: "(__cursor__())" defp get_line_from_meta(meta) when is_integer(meta), do: meta defp get_line_from_meta(meta), do: Keyword.fetch!(meta, :line) diff --git a/lib/elixir_sense/core/source.ex b/lib/elixir_sense/core/source.ex index e20628bd..029bc833 100644 --- a/lib/elixir_sense/core/source.ex +++ b/lib/elixir_sense/core/source.ex @@ -118,12 +118,64 @@ defmodule ElixirSense.Core.Source do end end + @spec prefix_suffix(String.t(), pos_integer, pos_integer) :: {String.t(), String.t()} + def prefix_suffix(code, line, col) do + line = code |> split_lines() |> Enum.at(line - 1, "") + + line = + if String.length(line) < col do + line_padding = for _ <- 1..(col - String.length(line)), into: "", do: " " + line <> line_padding + else + line + end + + # Extract the prefix + line_str = line |> String.slice(0, col - 1) + + prefix = + case Regex.run(~r/[\p{L}\p{N}\.\_\!\?\:\@\&\^\~\+\-\<\>\=\*\/\|\\]+$/u, line_str) do + nil -> "" + [prefix] when is_binary(prefix) -> prefix + end + + # Extract the suffix + suffix = + line + |> String.slice((col - 1)..-1//1) + |> case do + nil -> + "" + + str -> + case Regex.run(~r/^[\p{L}\p{N}\.\_\!\?\:\@\&\^\~\+\-\<\>\=\*\/\|\\]+/u, str) do + nil -> "" + [suffix] when is_binary(suffix) -> suffix + end + end + + {prefix, suffix} + end + @spec split_at(String.t(), pos_integer, pos_integer) :: {String.t(), String.t()} def split_at(code, line, col) do - pos = find_position(code, line, col, {0, 1, 1}) + pos = find_position(code, max(line, 1), max(col, 1), {0, 1, 1}) String.split_at(code, pos) end + @spec split_at(String.t(), list({pos_integer, pos_integer})) :: list(String.t()) + def split_at(code, list) do + do_split_at(code, Enum.reverse(list), []) + end + + defp do_split_at(code, [], acc), do: [code | acc] + + defp do_split_at(code, [{line, col} | rest], acc) do + pos = find_position(code, max(line, 1), max(col, 1), {0, 1, 1}) + {text_before, text_after} = String.split_at(code, pos) + do_split_at(text_before, rest, [text_after | acc]) + end + @spec text_before(String.t(), pos_integer, pos_integer) :: String.t() def text_before(code, line, col) do {text, _rest} = split_at(code, line, col) @@ -163,7 +215,7 @@ defmodule ElixirSense.Core.Source do end) end - @type var_or_attr_t :: {:variable, atom} | {:attribute, atom} | nil + @type var_or_attr_t :: {:variable, atom, non_neg_integer | :any} | {:attribute, atom} | nil @spec which_struct(String.t(), nil | module) :: nil @@ -251,8 +303,17 @@ defmodule ElixirSense.Core.Source do end end - defp get_var_or_attr({var, _, nil}) when is_atom(var) and var != :__MODULE__ do - {:variable, var} + defp get_var_or_attr({var, meta, context}) + when is_atom(var) and is_atom(context) and + var not in [ + :__MODULE__, + :__DIR__, + :__ENV__, + :__CALLER__, + :__STACKTRACE__, + :_ + ] do + {:variable, var, Keyword.get(meta, :version, :any)} end defp get_var_or_attr({:@, _, [{attr, _, nil}]}) when is_atom(attr) do @@ -518,8 +579,10 @@ defmodule ElixirSense.Core.Source do end end - def get_mod_fun([{name, _, nil}, fun], binding_env) when is_atom(name) do - case Binding.expand(binding_env, {:variable, name}) do + def get_mod_fun([{name, meta, context}, fun], binding_env) + when is_atom(name) and is_atom(context) and + name not in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__, :_] do + case Binding.expand(binding_env, {:variable, name, Keyword.get(meta, :version, :any)}) do {:atom, atom} -> {{atom, false}, fun} diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex deleted file mode 100644 index 4cac0d73..00000000 --- a/lib/elixir_sense/core/state.ex +++ /dev/null @@ -1,1942 +0,0 @@ -defmodule ElixirSense.Core.State do - @moduledoc """ - Core State - """ - - alias ElixirSense.Core.Introspection - require Logger - - @type fun_arity :: {atom, non_neg_integer} - @type scope :: atom | fun_arity | {:typespec, atom, non_neg_integer} - - @type alias_t :: {module, module} - @type scope_id_t :: non_neg_integer - @type position_t :: {pos_integer, pos_integer} - - @type mods_funs_to_positions_t :: %{ - optional({module, atom, nil | non_neg_integer}) => ElixirSense.Core.State.ModFunInfo.t() - } - @type lines_to_env_t :: %{optional(pos_integer) => ElixirSense.Core.State.Env.t()} - @type calls_t :: %{optional(pos_integer) => list(ElixirSense.Core.State.CallInfo.t())} - - @type types_t :: %{ - optional({module, atom, nil | non_neg_integer}) => ElixirSense.Core.State.TypeInfo.t() - } - @type specs_t :: %{ - optional({module, atom, nil | non_neg_integer}) => ElixirSense.Core.State.SpecInfo.t() - } - @type vars_info_per_scope_id_t :: %{ - optional(scope_id_t) => %{optional(atom) => ElixirSense.Core.State.VarInfo.t()} - } - @type structs_t :: %{optional(module) => ElixirSense.Core.State.StructInfo.t()} - @type protocol_t :: {module, nonempty_list(module)} - @type var_type :: nil | {:atom, atom} | {:map, keyword} | {:struct, keyword, module} - - @type t :: %ElixirSense.Core.State{ - module: [atom], - scopes: [scope], - requires: list(list(module)), - aliases: list(list(alias_t)), - attributes: list(list(ElixirSense.Core.State.AttributeInfo.t())), - protocols: list(protocol_t() | nil), - scope_attributes: list(list(atom)), - behaviours: %{optional(module) => [module]}, - specs: specs_t, - vars_info: list(list(ElixirSense.Core.State.VarInfo.t())), - scope_vars_info: list(list(ElixirSense.Core.State.VarInfo.t())), - scope_id_count: non_neg_integer, - scope_ids: list(scope_id_t), - vars_info_per_scope_id: vars_info_per_scope_id_t, - mods_funs_to_positions: mods_funs_to_positions_t, - lines_to_env: lines_to_env_t, - calls: calls_t, - structs: structs_t, - types: types_t, - generated: boolean, - first_alias_positions: map(), - moduledoc_positions: map(), - context: map(), - doc_context: list(), - typedoc_context: list(), - optional_callbacks_context: list(), - # TODO better type - binding_context: list, - typespec: nil | {atom, arity} - } - - @auto_imported_functions :elixir_env.new().functions - @auto_imported_macros :elixir_env.new().macros - @auto_required [Application, Kernel] ++ - (if Version.match?(System.version(), ">= 1.17.0-dev") do - [] - else - [Kernel.Typespec] - end) - - defstruct module: [nil], - scopes: [nil], - functions: [@auto_imported_functions], - macros: [@auto_imported_macros], - requires: [@auto_required], - aliases: [[]], - attributes: [[]], - protocols: [nil], - scope_attributes: [[]], - behaviours: %{}, - specs: %{}, - vars_info: [[]], - scope_vars_info: [[]], - scope_id_count: 0, - scope_ids: [0], - vars_info_per_scope_id: %{}, - mods_funs_to_positions: %{}, - lines_to_env: %{}, - calls: %{}, - structs: %{}, - types: %{}, - generated: false, - binding_context: [], - context: %{}, - first_alias_positions: %{}, - doc_context: [[]], - typedoc_context: [[]], - optional_callbacks_context: [[]], - moduledoc_positions: %{}, - typespec: nil - - defmodule Env do - @moduledoc """ - Line environment - """ - - @type t :: %Env{ - functions: [{module, [{atom, arity}]}], - macros: [{module, [{atom, arity}]}], - requires: list(module), - aliases: list(ElixirSense.Core.State.alias_t()), - module: nil | module, - function: nil | {atom, arity}, - protocol: nil | ElixirSense.Core.State.protocol_t(), - versioned_vars: %{optional({atom, atom}) => non_neg_integer}, - vars: list(ElixirSense.Core.State.VarInfo.t()), - attributes: list(ElixirSense.Core.State.AttributeInfo.t()), - behaviours: list(module), - typespec: nil | {atom, arity}, - scope_id: nil | ElixirSense.Core.State.scope_id_t() - } - defstruct functions: [], - macros: [], - requires: [], - aliases: [], - # NOTE for protocol implementation this will be the first variant - module: nil, - function: nil, - # NOTE for protocol implementation this will be the first variant - protocol: nil, - versioned_vars: %{}, - vars: [], - attributes: [], - behaviours: [], - typespec: nil, - scope_id: nil - end - - defmodule VarInfo do - @moduledoc """ - Variable info - """ - - @type t :: %VarInfo{ - name: atom, - positions: list(ElixirSense.Core.State.position_t()), - scope_id: nil | ElixirSense.Core.State.scope_id_t(), - is_definition: boolean, - type: ElixirSense.Core.State.var_type() - } - defstruct name: nil, - positions: [], - scope_id: nil, - is_definition: false, - type: nil - end - - defmodule TypeInfo do - @moduledoc """ - Type definition info - """ - @type t :: %TypeInfo{ - name: atom, - args: list(list(String.t())), - specs: [String.t()], - kind: :type | :typep | :opaque, - positions: [ElixirSense.Core.State.position_t()], - end_positions: [ElixirSense.Core.State.position_t() | nil], - doc: String.t(), - meta: map(), - generated: list(boolean) - } - defstruct name: nil, - args: [], - specs: [], - kind: :type, - positions: [], - end_positions: [], - generated: [], - doc: "", - meta: %{} - end - - defmodule SpecInfo do - @moduledoc """ - Type definition info - """ - @type t :: %SpecInfo{ - name: atom, - args: list(list(String.t())), - specs: [String.t()], - kind: :spec | :callback | :macrocallback, - positions: [ElixirSense.Core.State.position_t()], - end_positions: [ElixirSense.Core.State.position_t() | nil], - doc: String.t(), - meta: map(), - generated: list(boolean) - } - defstruct name: nil, - args: [], - specs: [], - kind: :spec, - positions: [], - end_positions: [], - generated: [], - doc: "", - meta: %{} - end - - defmodule StructInfo do - @moduledoc """ - Structure definition info - """ - @type field_t :: {atom, any} - @type t :: %StructInfo{ - type: :defstruct | :defexception, - fields: list(field_t) - } - defstruct type: :defstruct, fields: [] - end - - defmodule AttributeInfo do - @moduledoc """ - Variable info - """ - @type t :: %AttributeInfo{ - name: atom, - positions: list(ElixirSense.Core.State.position_t()), - type: ElixirSense.Core.State.var_type() - } - defstruct name: nil, positions: [], type: nil - end - - defmodule CallInfo do - @moduledoc """ - Function call info - """ - @type t :: %CallInfo{ - arity: non_neg_integer, - position: ElixirSense.Core.State.position_t(), - func: atom, - mod: module | {:attribute, atom} - } - defstruct arity: 0, - position: {1, 1}, - func: nil, - mod: Elixir - end - - defmodule ModFunInfo do - @moduledoc """ - Module or function info - """ - - @type t :: %ModFunInfo{ - params: list(list(term)), - positions: list(ElixirSense.Core.State.position_t()), - end_positions: list(ElixirSense.Core.State.position_t() | nil), - target: nil | {module, atom}, - overridable: false | {true, module}, - generated: list(boolean), - doc: String.t(), - meta: map(), - # TODO defmodule defprotocol defimpl? - type: - :def - | :defp - | :defmacro - | :defmacrop - | :defdelegate - | :defguard - | :defguardp - | :defmodule - } - - defstruct params: [], - positions: [], - end_positions: [], - target: nil, - type: nil, - generated: [], - overridable: false, - doc: "", - meta: %{} - - def get_arities(%ModFunInfo{params: params_variants}) do - params_variants - |> Enum.map(fn params -> - {length(params), Introspection.count_defaults(params)} - end) - end - - def get_category(%ModFunInfo{type: type}) - when type in [:defmacro, :defmacrop, :defguard, :defguardp], - do: :macro - - def get_category(%ModFunInfo{type: type}) when type in [:def, :defp, :defdelegate], - do: :function - - def get_category(%ModFunInfo{}), do: :module - end - - def current_aliases(%__MODULE__{} = state) do - state.aliases |> List.flatten() |> Enum.uniq_by(&elem(&1, 0)) |> Enum.reverse() - end - - def current_requires(%__MODULE__{} = state) do - state.requires |> :lists.reverse() |> List.flatten() |> Enum.uniq() |> Enum.sort() - end - - def get_current_env(%__MODULE__{} = state) do - current_module = get_current_module(state) - current_functions = state.functions |> hd() - current_macros = state.macros |> hd() - current_requires = current_requires(state) - current_aliases = current_aliases(state) - current_vars = state |> get_current_vars() - current_attributes = state |> get_current_attributes() - current_behaviours = state.behaviours |> Map.get(current_module, []) - current_scope = hd(state.scopes) - current_scope_id = hd(state.scope_ids) - current_scope_protocol = hd(state.protocols) - - current_function = - case current_scope do - {_name, _arity} = function -> function - _ -> nil - end - - current_typespec = - case current_scope do - {:typespec, name, arity} -> {name, arity} - _ -> nil - end - - versioned_vars = - current_vars - |> Enum.map(&{&1.name, nil}) - |> Enum.sort() - |> Enum.with_index() - |> Map.new() - - %Env{ - functions: current_functions, - macros: current_macros, - requires: current_requires, - aliases: current_aliases, - module: current_module, - function: current_function, - typespec: current_typespec, - vars: current_vars, - versioned_vars: versioned_vars, - attributes: current_attributes, - behaviours: current_behaviours, - scope_id: current_scope_id, - protocol: current_scope_protocol - } - end - - def get_current_module(%__MODULE__{} = state) do - state.module |> hd - end - - def add_current_env_to_line(%__MODULE__{} = state, line) when is_integer(line) do - previous_env = state.lines_to_env[line] - current_env = get_current_env(state) - - env = merge_env_vars(current_env, previous_env) - %__MODULE__{state | lines_to_env: Map.put(state.lines_to_env, line, env)} - end - - defp merge_env_vars(%Env{vars: current_vars} = current_env, previous_env) do - case previous_env do - nil -> - current_env - - %Env{vars: previous_vars} -> - vars_to_preserve = - Enum.filter(previous_vars, fn previous_var -> - Enum.all?(current_vars, fn current_var -> - current_var_positions = MapSet.new(current_var.positions) - - previous_var.positions - |> MapSet.new() - |> MapSet.disjoint?(current_var_positions) - end) - end) - - %Env{current_env | vars: current_vars ++ vars_to_preserve} - end - end - - def add_moduledoc_positions( - %__MODULE__{} = state, - env, - meta - ) do - module = env.module - - case Keyword.get(meta, :end_of_expression) do - nil -> - state - - end_of_expression -> - line_to_insert_alias = Keyword.fetch!(end_of_expression, :line) + 1 - column = Keyword.get(meta, :column, 1) - - %__MODULE__{ - state - | moduledoc_positions: - Map.put(state.moduledoc_positions, module, {line_to_insert_alias, column}) - } - end - end - - def add_first_alias_positions( - %__MODULE__{} = state, - _env = %{module: module, function: nil}, - meta - ) do - # TODO shouldn't that look for end_of_expression - line = Keyword.get(meta, :line, 0) - - if line > 0 do - column = Keyword.get(meta, :column, 1) - - %__MODULE__{ - state - | first_alias_positions: Map.put_new(state.first_alias_positions, module, {line, column}) - } - else - state - end - end - - def add_first_alias_positions(%__MODULE__{} = state, _env, _meta), do: state - - # TODO remove this - def add_call_to_line(%__MODULE__{} = state, {nil, :__block__, _}, _position), do: state - - def add_call_to_line( - %__MODULE__{} = state, - {{:@, _meta, [{name, _name_meta, _args}]}, func, arity}, - {_line, _column} = position - ) - when is_atom(name) do - add_call_to_line(state, {{:attribute, name}, func, arity}, position) - end - - def add_call_to_line( - %__MODULE__{} = state, - {{name, _name_meta, args}, func, arity}, - {_line, _column} = position - ) - when is_atom(args) do - add_call_to_line(state, {{:variable, name}, func, arity}, position) - end - - def add_call_to_line( - %__MODULE__{} = state, - {nil, {:@, _meta, [{name, _name_meta, _args}]}, arity}, - {_line, _column} = position - ) - when is_atom(name) do - add_call_to_line(state, {nil, {:attribute, name}, arity}, position) - end - - def add_call_to_line( - %__MODULE__{} = state, - {nil, {name, _name_meta, args}, arity}, - {_line, _column} = position - ) - when is_atom(args) do - add_call_to_line(state, {nil, {:variable, name}, arity}, position) - end - - def add_call_to_line(%__MODULE__{} = state, {mod, func, arity}, {line, _column} = position) do - call = %CallInfo{mod: mod, func: func, arity: arity, position: position} - - calls = - Map.update(state.calls, line, [call], fn line_calls -> - [call | line_calls] - end) - - %__MODULE__{state | calls: calls} - end - - def remove_calls(%__MODULE__{} = state, positions) do - Enum.reduce(positions, state, fn {line, _column} = position, state -> - case state.calls[line] do - nil -> - state - - calls -> - updated_calls = Enum.reject(calls, fn call_info -> call_info.position == position end) - - case updated_calls do - [] -> - %__MODULE__{state | calls: Map.delete(state.calls, line)} - - _non_empty_list -> - %__MODULE__{state | calls: Map.put(state.calls, line, updated_calls)} - end - end - end) - end - - def add_struct(%__MODULE__{} = state, env, type, fields) do - structs = - state.structs - |> Map.put(env.module, %StructInfo{type: type, fields: fields ++ [__struct__: env.module]}) - - %__MODULE__{state | structs: structs} - end - - def get_current_vars(%__MODULE__{} = state) do - state.scope_vars_info - |> List.flatten() - |> reduce_vars() - |> Enum.flat_map(fn {_var, scopes} -> scopes end) - end - - def get_current_vars_refs(%__MODULE__{} = state) do - state.scope_vars_info |> List.flatten() - end - - def get_current_attributes(%__MODULE__{} = state) do - state.scope_attributes |> :lists.reverse() |> List.flatten() - end - - def is_variable_defined(%__MODULE__{} = state, var_name) do - state - |> get_current_vars_refs() - |> Enum.any?(fn %VarInfo{name: name, is_definition: is_definition} -> - name == var_name && is_definition - end) - end - - def add_mod_fun_to_position( - %__MODULE__{} = state, - {module, fun, arity}, - position, - end_position, - params, - type, - doc, - meta, - options \\ [] - ) - when is_tuple(position) do - current_info = Map.get(state.mods_funs_to_positions, {module, fun, arity}, %ModFunInfo{}) - current_params = current_info |> Map.get(:params, []) - current_positions = current_info |> Map.get(:positions, []) - current_end_positions = current_info |> Map.get(:end_positions, []) - new_params = [params | current_params] - new_positions = [position | current_positions] - new_end_positions = [end_position | current_end_positions] - - info_type = - if fun != nil and arity == nil and - current_info.type not in [nil, :defp, :defmacrop, :defguardp] and - not match?({true, _}, current_info.overridable) do - # in case there are multiple definitions for nil arity prefer public ones - # unless this is an overridable def - current_info.type - else - type - end - - overridable = current_info |> Map.get(:overridable, false) - - meta = - if overridable do - Map.put(meta, :overridable, true) - else - meta - end - - info = %ModFunInfo{ - positions: new_positions, - end_positions: new_end_positions, - params: new_params, - type: info_type, - doc: doc, - meta: meta, - generated: [Keyword.get(options, :generated, false) | current_info.generated], - overridable: overridable - } - - info = - Enum.reduce(options, info, fn option, acc -> process_option(state, acc, type, option) end) - - mods_funs_to_positions = Map.put(state.mods_funs_to_positions, {module, fun, arity}, info) - - %__MODULE__{state | mods_funs_to_positions: mods_funs_to_positions} - end - - defp process_option( - state, - info, - :defdelegate, - {:target, {target_module_expression, target_function}} - ) do - {module, _state, _env} = expand(target_module_expression, state) - - %ModFunInfo{ - info - | target: {module, target_function} - } - end - - defp process_option(_state, info, _, {:overridable, {true, module}}) do - %ModFunInfo{ - info - | overridable: {true, module}, - meta: Map.put(info.meta, :overridable, true) - } - end - - defp process_option(_state, info, _type, _option), do: info - - def implementation_alias(protocol, [first | _]) do - Module.concat(protocol, first) - end - - def add_module(%__MODULE__{} = state, module) do - # TODO refactor to allow {:implementation, protocol, [implementations]} in scope - %__MODULE__{ - state - | module: [module | state.module], - scopes: [module | state.scopes], - doc_context: [[] | state.doc_context], - typedoc_context: [[] | state.typedoc_context], - optional_callbacks_context: [[] | state.optional_callbacks_context] - } - end - - def remove_module(%__MODULE__{} = state) do - %{ - state - | module: tl(state.module), - scopes: tl(state.scopes), - doc_context: tl(state.doc_context), - typedoc_context: tl(state.typedoc_context), - optional_callbacks_context: tl(state.optional_callbacks_context) - } - end - - def add_typespec_namespace(%__MODULE__{} = state, name, arity) do - %{state | scopes: [{:typespec, name, arity} | state.scopes]} - end - - def register_optional_callbacks(%__MODULE__{} = state, list) do - [_ | rest] = state.optional_callbacks_context - %{state | optional_callbacks_context: [list | rest]} - end - - def apply_optional_callbacks(%__MODULE__{} = state, env) do - [list | _rest] = state.optional_callbacks_context - module = env.module - - updated_specs = - list - |> Enum.reduce(state.specs, fn {fun, arity}, acc -> - acc - |> Map.update!({module, fun, arity}, fn spec_info = %SpecInfo{} -> - %{spec_info | meta: spec_info.meta |> Map.put(:optional, true)} - end) - end) - - %{state | specs: updated_specs} - end - - def new_named_func(%__MODULE__{} = state, name, arity) do - %{state | scopes: [{name, arity} | state.scopes]} - end - - def maybe_add_protocol_implementation( - %__MODULE__{} = state, - protocol = {_protocol, _implementations} - ) do - %__MODULE__{state | protocols: [protocol | state.protocols]} - end - - def maybe_add_protocol_implementation(%__MODULE__{} = state, _) do - %__MODULE__{state | protocols: [nil | state.protocols]} - end - - def remove_protocol_implementation(%__MODULE__{} = state) do - %__MODULE__{state | protocols: tl(state.protocols)} - end - - def remove_last_scope_from_scopes(%__MODULE__{} = state) do - %__MODULE__{state | scopes: tl(state.scopes)} - end - - def add_current_module_to_index(%__MODULE__{} = state, position, end_position, options) - when (is_tuple(position) and is_tuple(end_position)) or is_nil(end_position) do - current_module = get_current_module(state) - - add_module_to_index(state, current_module, position, end_position, options) - end - - def add_module_to_index(%__MODULE__{} = state, module, position, end_position, options) - when (is_tuple(position) and is_tuple(end_position)) or is_nil(end_position) do - # TODO :defprotocol, :defimpl? - add_mod_fun_to_position( - state, - {module, nil, nil}, - position, - end_position, - nil, - :defmodule, - "", - %{}, - options - ) - end - - # TODO require end position - def add_func_to_index( - state, - env, - func, - params, - position, - end_position \\ nil, - type, - options \\ [] - ) - - def add_func_to_index( - %__MODULE__{} = state, - env, - func, - params, - position, - end_position, - type, - options - ) - when (is_tuple(position) and is_tuple(end_position)) or is_nil(end_position) do - current_module = env.module - arity = length(params) - - {state, {doc, meta}} = - if not state.generated and Keyword.get(options, :generated, false) do - # do not consume docs on generated functions - # NOTE state.generated is set when expanding use macro - # we want to consume docs there - {state, {"", %{generated: true}}} - else - consume_doc_context(state) - end - - hidden = Map.get(meta, :hidden) - - # underscored and @impl defs are hidden by default unless they have @doc - meta = - if (String.starts_with?(to_string(func), "_") or hidden == :impl) and doc == "" do - Map.put(meta, :hidden, true) - else - if hidden != true do - Map.delete(meta, :hidden) - else - meta - end - end - - meta = - if type == :defdelegate do - {target_module_expression, target_fun} = options[:target] - {module, _state, _env} = expand(target_module_expression, state) - - Map.put( - meta, - :delegate_to, - {module, target_fun, arity} - ) - else - meta - end - - meta = - if type in [:defguard, :defguardp] do - Map.put(meta, :guard, true) - else - meta - end - - doc = - if type in [:defp, :defmacrop] do - # documentation is discarded on private - "" - else - doc - end - - add_mod_fun_to_position( - state, - {current_module, func, arity}, - position, - end_position, - params, - type, - doc, - meta, - options - ) - end - - def make_overridable( - %__MODULE__{} = state, - env, - fa_list, - overridable_module - ) do - module = env.module - - mods_funs_to_positions = - fa_list - |> Enum.reduce(state.mods_funs_to_positions, fn {f, a}, mods_funs_to_positions_acc -> - if Map.has_key?(mods_funs_to_positions_acc, {module, f, a}) do - mods_funs_to_positions_acc - |> make_def_overridable({module, f, a}, overridable_module) - else - # Some behaviour callbacks can be not implemented by __using__ macro - mods_funs_to_positions_acc - end - end) - - %__MODULE__{state | mods_funs_to_positions: mods_funs_to_positions} - end - - defp make_def_overridable(mods_funs_to_positions, mfa, overridable_module) do - update_in(mods_funs_to_positions[mfa], fn mod_fun_info = %ModFunInfo{} -> - %ModFunInfo{ - mod_fun_info - | overridable: {true, overridable_module}, - meta: Map.put(mod_fun_info.meta, :overridable, true) - } - end) - end - - def new_vars_scope(%__MODULE__{} = state) do - scope_id = state.scope_id_count + 1 - - %__MODULE__{ - state - | scope_ids: [scope_id | state.scope_ids], - scope_id_count: scope_id, - vars_info: [[] | state.vars_info], - scope_vars_info: [[] | state.scope_vars_info] - } - end - - def push_binding_context(%__MODULE__{} = state, binding_context) do - %__MODULE__{ - state - | binding_context: [binding_context | state.binding_context] - } - end - - def pop_binding_context(%__MODULE__{} = state) do - %__MODULE__{ - state - | binding_context: tl(state.binding_context) - } - end - - def new_func_vars_scope(%__MODULE__{} = state) do - scope_id = state.scope_id_count + 1 - - %__MODULE__{ - state - | scope_ids: [scope_id | state.scope_ids], - scope_id_count: scope_id, - vars_info: [[] | state.vars_info], - scope_vars_info: [[]] - } - end - - def new_attributes_scope(%__MODULE__{} = state) do - %__MODULE__{state | attributes: [[] | state.attributes], scope_attributes: [[]]} - end - - def remove_vars_scope(%__MODULE__{} = state) do - %__MODULE__{ - state - | scope_ids: tl(state.scope_ids), - vars_info: tl(state.vars_info), - scope_vars_info: tl(state.scope_vars_info), - vars_info_per_scope_id: update_vars_info_per_scope_id(state) - } - end - - def remove_func_vars_scope(%__MODULE__{} = state) do - %__MODULE__{ - state - | scope_ids: tl(state.scope_ids), - vars_info: tl(state.vars_info), - scope_vars_info: tl(state.vars_info), - vars_info_per_scope_id: update_vars_info_per_scope_id(state) - } - end - - def update_vars_info_per_scope_id(state) do - [scope_id | _other_scope_ids] = state.scope_ids - - [current_scope_vars | other_scope_vars] = state.scope_vars_info - - current_scope_reduced_vars = reduce_vars(current_scope_vars) - - vars_info = - other_scope_vars - |> List.flatten() - |> reduce_vars(current_scope_reduced_vars, false) - |> Enum.flat_map(fn {_var, scopes} -> scopes end) - - Map.put(state.vars_info_per_scope_id, scope_id, vars_info) - end - - defp reduce_vars(vars, initial_acc \\ %{}, keep_all_same_name_vars \\ true) do - Enum.reduce(vars, initial_acc, fn %VarInfo{name: var, positions: positions} = el, acc -> - updated = - case acc[var] do - nil -> - [el] - - [%VarInfo{is_definition: false} = var_info | same_name_vars] -> - type = - if Enum.all?(positions, fn position -> position in var_info.positions end) do - merge_type(el.type, var_info.type) - else - el.type - end - - [ - %VarInfo{ - el - | positions: (var_info.positions ++ positions) |> Enum.uniq() |> Enum.sort(), - type: type - } - | same_name_vars - ] - - [%VarInfo{is_definition: true} = var_info | same_name_vars] -> - cond do - Enum.all?(positions, fn position -> position in var_info.positions end) -> - type = merge_type(el.type, var_info.type) - - [ - %VarInfo{ - var_info - | positions: (var_info.positions ++ positions) |> Enum.uniq() |> Enum.sort(), - type: type - } - | same_name_vars - ] - - keep_all_same_name_vars -> - [el, var_info | same_name_vars] - - true -> - [var_info | same_name_vars] - end - end - - Map.put(acc, var, updated) - end) - end - - def remove_attributes_scope(%__MODULE__{} = state) do - attributes = tl(state.attributes) - %__MODULE__{state | attributes: attributes, scope_attributes: attributes} - end - - def add_alias(%__MODULE__{} = state, {alias, aliased}) when is_list(aliased) do - if Introspection.elixir_module?(alias) do - alias = Module.split(alias) |> Enum.take(-1) |> Module.concat() - [aliases_from_scope | inherited_aliases] = state.aliases - aliases_from_scope = aliases_from_scope |> Enum.reject(&match?({^alias, _}, &1)) - - # TODO pass env - {expanded, state, _env} = expand(aliased, state) - - aliases_from_scope = - if alias != expanded do - [{alias, expanded} | aliases_from_scope] - else - aliases_from_scope - end - - %__MODULE__{ - state - | aliases: [ - aliases_from_scope | inherited_aliases - ] - } - else - state - end - end - - def add_alias(%__MODULE__{} = state, {alias, aliased}) when is_atom(aliased) do - if Introspection.elixir_module?(alias) do - [aliases_from_scope | inherited_aliases] = state.aliases - aliases_from_scope = aliases_from_scope |> Enum.reject(&match?({^alias, _}, &1)) - - aliases_from_scope = - if alias != aliased do - [{alias, aliased} | aliases_from_scope] - else - aliases_from_scope - end - - %__MODULE__{ - state - | aliases: [ - aliases_from_scope | inherited_aliases - ] - } - else - [aliases_from_scope | inherited_aliases] = state.aliases - aliases_from_scope = aliases_from_scope |> Enum.reject(&match?({^alias, _}, &1)) - - %__MODULE__{ - state - | aliases: [ - [{Module.concat([alias]), aliased} | aliases_from_scope] | inherited_aliases - ] - } - end - end - - def add_alias(%__MODULE__{} = state, _), do: state - - def new_lexical_scope(%__MODULE__{} = state) do - %__MODULE__{ - state - | functions: [hd(state.functions) | state.functions], - macros: [hd(state.macros) | state.macros], - requires: [[] | state.requires], - aliases: [[] | state.aliases] - } - end - - def remove_lexical_scope(%__MODULE__{} = state) do - %__MODULE__{ - state - | functions: tl(state.functions), - macros: tl(state.macros), - requires: tl(state.requires), - aliases: tl(state.aliases) - } - end - - def add_import(%__MODULE__{} = state, module, opts) when is_atom(module) do - {functions, macros} = - Introspection.expand_import( - {hd(state.functions), hd(state.macros)}, - module, - opts, - state.mods_funs_to_positions - ) - - %__MODULE__{ - state - | functions: [functions | tl(state.functions)], - macros: [macros | tl(state.macros)] - } - end - - def add_import(%__MODULE__{} = state, _module, _opts), do: state - - def add_require(%__MODULE__{} = state, module) when is_atom(module) do - [requires_from_scope | inherited_requires] = state.requires - - current_requires = state.requires |> :lists.reverse() |> List.flatten() - - requires_from_scope = - if module in current_requires do - requires_from_scope - else - [module | requires_from_scope] - end - - %__MODULE__{state | requires: [requires_from_scope | inherited_requires]} - end - - def add_require(%__MODULE__{} = state, _module), do: state - - def add_type( - %__MODULE__{} = state, - env, - type_name, - type_args, - spec, - kind, - pos, - end_pos, - options \\ [] - ) do - arg_names = - type_args - |> Enum.map(&Macro.to_string/1) - - {state, {doc, meta}} = consume_typedoc_context(state) - - # underscored types are hidden by default unless they have @typedoc - meta = - if String.starts_with?(to_string(type_name), "_") and doc == "" do - Map.put(meta, :hidden, true) - else - meta - end - - meta = - if kind == :opaque do - Map.put(meta, :opaque, true) - else - meta - end - - doc = - if kind == :typep do - # documentation is discarded on private - "" - else - doc - end - - type_info = %TypeInfo{ - name: type_name, - args: [arg_names], - kind: kind, - specs: [spec], - generated: [Keyword.get(options, :generated, false)], - positions: [pos], - end_positions: [end_pos], - doc: doc, - meta: meta - } - - current_module = env.module - - types = - state.types - |> Map.put({current_module, type_name, length(arg_names)}, type_info) - - %__MODULE__{state | types: types} - end - - defp combine_specs(nil, new), do: new - - defp combine_specs(%SpecInfo{} = existing, %SpecInfo{} = new) do - %SpecInfo{ - existing - | positions: [hd(new.positions) | existing.positions], - end_positions: [hd(new.end_positions) | existing.end_positions], - generated: [hd(new.generated) | existing.generated], - args: [hd(new.args) | existing.args], - specs: [hd(new.specs) | existing.specs] - } - end - - def add_spec( - %__MODULE__{} = state, - env, - type_name, - type_args, - spec, - kind, - pos, - end_pos, - options - ) do - arg_names = - type_args - |> Enum.map(&Macro.to_string/1) - - {state, {doc, meta}} = - if kind in [:callback, :macrocallback] do - consume_doc_context(state) - else - # do not consume doc context for specs - {state, {"", %{}}} - end - - # underscored callbacks are hidden by default unless they have @doc - meta = - if String.starts_with?(to_string(type_name), "_") and doc == "" do - Map.put(meta, :hidden, true) - else - meta - end - - type_info = %SpecInfo{ - name: type_name, - args: [arg_names], - specs: [spec], - kind: kind, - generated: [Keyword.get(options, :generated, false)], - positions: [pos], - end_positions: [end_pos], - doc: doc, - meta: meta - } - - current_module = env.module - - arity_info = - combine_specs(state.specs[{current_module, type_name, length(arg_names)}], type_info) - - specs = - state.specs - |> Map.put({current_module, type_name, length(arg_names)}, arity_info) - - %__MODULE__{state | specs: specs} - end - - def add_var( - %__MODULE__{} = state, - %VarInfo{name: var_name} = var_info, - is_definition - ) do - scope = hd(state.scopes) - [vars_from_scope | other_vars] = state.vars_info - is_var_defined = is_variable_defined(state, var_name) - var_name_as_string = Atom.to_string(var_name) - - vars_from_scope = - case {is_definition and var_info.is_definition, is_var_defined, var_name_as_string} do - {_, _, "__MODULE__"} -> - raise "foo" - - {_, _, "_"} -> - vars_from_scope - - {_, _, ^scope} -> - vars_from_scope - - {is_definition, is_var_defined, _} when is_definition or is_var_defined -> - [ - %VarInfo{ - var_info - | scope_id: hd(state.scope_ids), - is_definition: is_definition - } - | vars_from_scope - ] - - _ -> - vars_from_scope - end - - %__MODULE__{ - state - | vars_info: [vars_from_scope | other_vars], - scope_vars_info: [vars_from_scope | tl(state.scope_vars_info)] - } - end - - @builtin_attributes ElixirSense.Core.BuiltinAttributes.all() - - def add_attributes(%__MODULE__{} = state, attributes, position) do - Enum.reduce(attributes, state, fn attribute, state -> - add_attribute(state, attribute, nil, true, position) - end) - end - - def add_attribute(%__MODULE__{} = state, attribute, type, is_definition, position) - when attribute not in @builtin_attributes do - [attributes_from_scope | other_attributes] = state.attributes - - existing_attribute_index = - attributes_from_scope - |> Enum.find_index(&(&1.name == attribute)) - - attributes_from_scope = - case existing_attribute_index do - nil -> - if is_definition do - [ - %AttributeInfo{ - name: attribute, - type: type, - positions: [position] - } - | attributes_from_scope - ] - else - attributes_from_scope - end - - index -> - attributes_from_scope - |> List.update_at(index, fn existing -> - type = if is_definition, do: type, else: existing.type - - %AttributeInfo{ - existing - | # FIXME this is wrong for accumulating attributes - type: type, - positions: (existing.positions ++ [position]) |> Enum.uniq() |> Enum.sort() - } - end) - end - - attributes = [attributes_from_scope | other_attributes] - scope_attributes = [attributes_from_scope | tl(state.scope_attributes)] - %__MODULE__{state | attributes: attributes, scope_attributes: scope_attributes} - end - - def add_attribute(%__MODULE__{} = state, _attribute, _type, _is_definition, _position) do - state - end - - def add_behaviour(module, %__MODULE__{} = state, env) when is_atom(module) do - state = - update_in(state.behaviours[env.module], &Enum.uniq([module | &1 || []])) - - {module, state, env} - end - - def add_behaviour(_module, %__MODULE__{} = state, env), do: {nil, state, env} - - def register_doc(%__MODULE__{} = state, env, :moduledoc, doc_arg) do - current_module = env.module - doc_arg_formatted = format_doc_arg(doc_arg) - - mods_funs_to_positions = - state.mods_funs_to_positions - |> Map.update!({current_module, nil, nil}, fn info = %ModFunInfo{} -> - case doc_arg_formatted do - {:meta, meta} -> - %{info | meta: Map.merge(info.meta, meta)} - - text_or_hidden -> - %{info | doc: text_or_hidden} - end - end) - - %{state | mods_funs_to_positions: mods_funs_to_positions} - end - - def register_doc(%__MODULE__{} = state, _env, :doc, doc_arg) do - [doc_context | doc_context_rest] = state.doc_context - - %{state | doc_context: [[doc_arg | doc_context] | doc_context_rest]} - end - - def register_doc(%__MODULE__{} = state, _env, :typedoc, doc_arg) do - [doc_context | doc_context_rest] = state.typedoc_context - - %{state | typedoc_context: [[doc_arg | doc_context] | doc_context_rest]} - end - - defp consume_doc_context(%__MODULE__{} = state) do - [doc_context | doc_context_rest] = state.doc_context - state = %{state | doc_context: [[] | doc_context_rest]} - - {state, reduce_doc_context(doc_context)} - end - - defp consume_typedoc_context(%__MODULE__{} = state) do - [doc_context | doc_context_rest] = state.typedoc_context - state = %{state | typedoc_context: [[] | doc_context_rest]} - - {state, reduce_doc_context(doc_context)} - end - - defp reduce_doc_context(doc_context) do - Enum.reduce(doc_context, {"", %{}}, fn doc_arg, {doc_acc, meta_acc} -> - case format_doc_arg(doc_arg) do - {:meta, meta} -> {doc_acc, Map.merge(meta_acc, meta)} - doc -> {doc, meta_acc} - end - end) - end - - defp format_doc_arg(binary) when is_binary(binary), do: binary - - defp format_doc_arg(list) when is_list(list) do - # TODO pass env and expand metadata - if Keyword.keyword?(list) do - {:meta, Map.new(list)} - else - to_string(list) - end - end - - defp format_doc_arg(false), do: {:meta, %{hidden: true}} - defp format_doc_arg(:impl), do: {:meta, %{hidden: :impl}} - - defp format_doc_arg(quoted) do - try do - # TODO pass env to eval - case Code.eval_quoted(quoted) do - {binary, _} when is_binary(binary) -> - binary - - {list, _} when is_list(list) -> - if Keyword.keyword?(list) do - {:meta, Map.new(list)} - else - to_string(list) - end - - other -> - Logger.warning(""" - Unable to format docstring expression: - - #{inspect(quoted, pretty: true)} - - Eval resulted in: - - #{inspect(other)} - """) - - "" - end - rescue - e -> - Logger.warning(""" - Unable to format docstring expression: - - #{inspect(quoted, pretty: true)} - - #{Exception.format(:error, e, __STACKTRACE__)} - """) - - "" - end - end - - def add_vars(%__MODULE__{} = state, vars, is_definition) do - vars |> Enum.reduce(state, fn var, state -> add_var(state, var, is_definition) end) - end - - # Simultaneously performs two operations: - # - deletes variables that contain any of `remove_positions` - # - adds `vars` to the state, but with types merged with the corresponding removed variables - def merge_new_vars(%__MODULE__{} = state, vars, remove_positions) do - {state, vars} = - Enum.reduce(remove_positions, {state, vars}, fn position, {state, vars} -> - case pop_var(state, position) do - {nil, state} -> - {state, vars} - - {removed_var, state} -> - vars = - Enum.reduce(vars, [], fn %VarInfo{positions: positions} = var, vars -> - if positions == removed_var.positions do - type = merge_type(var.type, removed_var.type) - - [%VarInfo{var | type: type} | vars] - else - [var | vars] - end - end) - - {state, vars} - end - end) - - add_vars(state, vars, true) - end - - defp pop_var(%__MODULE__{} = state, position) do - [current_scope_vars | other_vars] = state.vars_info - - var = - Enum.find(current_scope_vars, fn %VarInfo{positions: positions} -> position in positions end) - - current_scope_vars = - Enum.reject(current_scope_vars, fn %VarInfo{positions: positions} -> - position in positions - end) - - state = %__MODULE__{ - state - | vars_info: [current_scope_vars | other_vars], - scope_vars_info: [current_scope_vars | tl(state.scope_vars_info)] - } - - {var, state} - end - - def merge_same_name_vars(vars) do - vars - |> Enum.reduce(%{}, fn %VarInfo{name: var, positions: positions} = el, acc -> - updated = - case acc[var] do - nil -> - el - - %VarInfo{} = var_info -> - type = - if Enum.all?(positions, fn position -> position in var_info.positions end) do - merge_type(el.type, var_info.type) - else - el.type - end - - %VarInfo{ - var_info - | positions: (var_info.positions ++ positions) |> Enum.uniq() |> Enum.sort(), - type: type - } - end - - Map.put(acc, var, updated) - end) - |> Enum.map(fn {_name, var_info} -> var_info end) - end - - defp merge_type(nil, new), do: new - defp merge_type(old, nil), do: old - defp merge_type(old, old), do: old - defp merge_type(old, new), do: {:intersection, [old, new]} - - def default_env, do: %ElixirSense.Core.State.Env{} - - def expand(ast, %__MODULE__{} = state) do - expand(ast, state, get_current_env(state)) - end - - def expand({:@, meta, [{:behaviour, _, [arg]}]}, state, env) do - line = Keyword.fetch!(meta, :line) - - state = - state - |> add_current_env_to_line(line) - - {arg, state, env} = expand(arg, state, env) - add_behaviour(arg, state, env) - end - - def expand({:defoverridable, meta, [arg]}, state, env) do - {arg, state, env} = expand(arg, state, env) - - case arg do - keyword when is_list(keyword) -> - {nil, make_overridable(state, env, keyword, meta[:context]), env} - - behaviour_module when is_atom(behaviour_module) -> - if Code.ensure_loaded?(behaviour_module) and - function_exported?(behaviour_module, :behaviour_info, 1) do - keyword = - behaviour_module.behaviour_info(:callbacks) - |> Enum.map(&Introspection.drop_macro_prefix/1) - - {nil, make_overridable(state, env, keyword, meta[:context]), env} - else - {nil, state, env} - end - - _ -> - {nil, state, env} - end - end - - def expand({form, meta, [{{:., _, [base, :{}]}, _, refs} | rest]}, state, env) - when form in [:require, :alias, :import, :use] do - case rest do - [] -> - expand_multi_alias_call(form, meta, base, refs, [], state, env) - - [opts] -> - opts = Keyword.delete(opts, :as) - # if Keyword.has_key?(opts, :as) do - # raise "as_in_multi_alias_call" - # end - - expand_multi_alias_call(form, meta, base, refs, opts, state, env) - end - end - - def expand({form, meta, [arg]}, state, env) when form in [:require, :alias, :import] do - expand({form, meta, [arg, []]}, state, env) - end - - def expand(module, %__MODULE__{} = state, env) when is_atom(module) do - {module, state, env} - end - - def expand({:alias, meta, [arg, opts]}, state, env) do - line = Keyword.fetch!(meta, :line) - - {arg, state, env} = expand(arg, state, env) - # options = expand(no_alias_opts(arg), state, env, env) - - if is_atom(arg) do - state = add_first_alias_positions(state, env, meta) - - alias_tuple = - case Keyword.get(opts, :as) do - nil -> - {Module.concat([List.last(Module.split(arg))]), arg} - - as -> - # alias with `as:` option - {no_alias_expansion(as), arg} - end - - state = - state - |> add_current_env_to_line(line) - |> add_alias(alias_tuple) - - {arg, state, env} - else - {nil, state, env} - end - rescue - ArgumentError -> {nil, state, env} - end - - def expand({:require, meta, [arg, opts]}, state, env) do - line = Keyword.fetch!(meta, :line) - - {arg, state, env} = expand(arg, state, env) - # opts = expand(no_alias_opts(opts), state, env) - - if is_atom(arg) do - state = - state - |> add_current_env_to_line(line) - - state = - case Keyword.get(opts, :as) do - nil -> - state - - as -> - # require with `as:` option - alias_tuple = {no_alias_expansion(as), arg} - add_alias(state, alias_tuple) - end - |> add_require(arg) - - {arg, state, env} - else - {nil, state, env} - end - end - - def expand({:import, meta, [arg, opts]}, state, env) do - line = Keyword.fetch!(meta, :line) - - {arg, state, env} = expand(arg, state, env) - # opts = expand(no_alias_opts(opts), state, env) - - if is_atom(arg) do - state = - state - |> add_current_env_to_line(line) - |> add_require(arg) - |> add_import(arg, opts) - - {arg, state, env} - else - {nil, state, env} - end - end - - def expand({:use, _meta, []} = ast, state, env) do - # defmacro use in Kernel - {ast, state, env} - end - - def expand({:use, meta, [_ | _]} = ast, state, env) do - alias ElixirSense.Core.MacroExpander - line = Keyword.fetch!(meta, :line) - - state = - state - |> add_current_env_to_line(line) - - # TODO pass env - expanded_ast = - ast - |> MacroExpander.add_default_meta() - |> MacroExpander.expand_use( - env.module, - env.aliases, - meta |> Keyword.take([:line, :column]) - ) - - {{:__generated__, [], [expanded_ast]}, %{state | generated: true}, env} - end - - def expand( - {:__aliases__, _, [Elixir | _] = module}, - %__MODULE__{} = state, - env - ) do - {Module.concat(module), state, env} - end - - def expand({:__MODULE__, _, nil}, %__MODULE__{} = state, env) do - {env.module, state, env} - end - - def expand( - {:__aliases__, _, [{:__MODULE__, _, nil} | rest]}, - %__MODULE__{} = state, - env - ) do - {Module.concat([env.module | rest]), state, env} - end - - def expand({:__aliases__, _, module}, %__MODULE__{} = state, env) - when is_list(module) do - {Introspection.expand_alias(Module.concat(module), env.aliases), state, env} - end - - def expand(ast, %__MODULE__{} = state, env) do - {ast, state, env} - end - - def maybe_move_vars_to_outer_scope(%__MODULE__{} = state) do - scope_vars = move_references_to_outer_scope(state.scope_vars_info) - vars = move_references_to_outer_scope(state.vars_info) - - %__MODULE__{state | vars_info: vars, scope_vars_info: scope_vars} - end - - defp move_references_to_outer_scope(vars) do - {current_scope_vars, outer_scope_vars, other_scopes_vars} = - case vars do - [current_scope_vars, outer_scope_vars | other_scopes_vars] -> - {current_scope_vars, outer_scope_vars, other_scopes_vars} - - [current_scope_vars | []] -> - {current_scope_vars, [], []} - end - - vars_to_move = - current_scope_vars - |> Enum.reduce(%{}, fn - %VarInfo{name: var, is_definition: true}, acc -> - Map.delete(acc, var) - - %VarInfo{name: var, positions: positions, is_definition: false} = el, acc -> - updated = - case acc[var] do - nil -> - el - - var_info -> - type = - if Enum.all?(positions, fn position -> position in var_info.positions end) do - merge_type(el.type, var_info.type) - else - el.type - end - - %VarInfo{ - el - | positions: (var_info.positions ++ positions) |> Enum.uniq() |> Enum.sort(), - type: type - } - end - - Map.put(acc, var, updated) - end) - |> Enum.map(fn {_name, var_info} -> var_info end) - |> Enum.reject(fn var_info -> is_nil(var_info) end) - - [current_scope_vars, vars_to_move ++ outer_scope_vars | other_scopes_vars] - end - - def no_alias_expansion({:__aliases__, _, [h | t]} = _aliases) when is_atom(h) do - Module.concat([h | t]) - end - - def no_alias_expansion(other), do: other - - def alias_defmodule({:__aliases__, _, [Elixir, h | t]}, module, state, env) do - if t == [] and Version.match?(System.version(), "< 1.16.0-dev") do - # on elixir < 1.16 unaliasing is happening - # https://github.com/elixir-lang/elixir/issues/12456 - alias = String.to_atom("Elixir." <> Atom.to_string(h)) - state = add_alias(state, {alias, module}) - {module, state, env} - else - {module, state, env} - end - end - - # defmodule Alias in root - def alias_defmodule({:__aliases__, _, _}, module, state, %{module: nil} = env) do - {module, state, env} - end - - # defmodule Alias nested - def alias_defmodule({:__aliases__, _meta, [h | t]}, _module, state, env) when is_atom(h) do - module = Module.concat([env.module, h]) - alias = String.to_atom("Elixir." <> Atom.to_string(h)) - # {:ok, env} = Macro.Env.define_alias(env, meta, module, as: alias, trace: false) - state = add_alias(state, {alias, module}) - - case t do - [] -> {module, state, env} - _ -> {String.to_atom(Enum.join([module | t], ".")), state, env} - end - end - - # defmodule _ - def alias_defmodule(_raw, module, state, env) do - {module, state, env} - end - - defp expand_multi_alias_call(kind, meta, base, refs, opts, state, env) do - {base_ref, state, env} = expand(base, state, env) - - fun = fn - {:__aliases__, _, ref}, state, env -> - expand({kind, meta, [Module.concat([base_ref | ref]), opts]}, state, env) - - ref, state, env when is_atom(ref) -> - expand({kind, meta, [Module.concat([base_ref, ref]), opts]}, state, env) - - _other, s, e -> - {nil, s, e} - # raise "expected_compile_time_module" - end - - map_fold(fun, state, env, refs) - end - - defp map_fold(fun, s, e, list), do: map_fold(fun, s, e, list, []) - - defp map_fold(fun, s, e, [h | t], acc) do - {rh, rs, re} = fun.(h, s, e) - map_fold(fun, rs, re, t, [rh | acc]) - end - - defp map_fold(_fun, s, e, [], acc), do: {Enum.reverse(acc), s, e} - - @module_functions [ - {:__info__, [:atom], :def}, - {:module_info, [], :def}, - {:module_info, [:atom], :def} - ] - - def add_module_functions(state, env, functions, position, end_position) do - {line, column} = position - - (functions ++ @module_functions) - |> Enum.reduce(state, fn {name, args, kind}, acc -> - mapped_args = for arg <- args, do: {arg, [line: line, column: column], nil} - - acc - |> add_func_to_index( - env, - name, - mapped_args, - position, - end_position, - kind, - generated: true - ) - end) - end - - def with_typespec(%__MODULE__{} = state, typespec) do - %{state | typespec: typespec} - end - - def add_struct_or_exception(state, env, type, fields, {line, column} = position, end_position) do - fields = - fields ++ - if type == :defexception do - [__exception__: true] - else - [] - end - - options = [generated: true] - - state = - if type == :defexception do - {_, state, env} = add_behaviour(Exception, state, env) - - if Keyword.has_key?(fields, :message) do - state - |> add_func_to_index( - env, - :exception, - [{:msg, [line: line, column: column], nil}], - position, - end_position, - :def, - options - ) - |> add_func_to_index( - env, - :message, - [{:exception, [line: line, column: column], nil}], - position, - end_position, - :def, - options - ) - else - state - end - |> add_func_to_index( - env, - :exception, - [{:args, [line: line, column: column], nil}], - position, - end_position, - :def, - options - ) - else - state - end - |> add_func_to_index(env, :__struct__, [], position, end_position, :def, options) - |> add_func_to_index( - env, - :__struct__, - [{:kv, [line: line, column: column], nil}], - position, - end_position, - :def, - options - ) - - state - |> add_struct(env, type, fields) - end -end diff --git a/lib/elixir_sense/core/state/attribute_info.ex b/lib/elixir_sense/core/state/attribute_info.ex new file mode 100644 index 00000000..9a12e2bb --- /dev/null +++ b/lib/elixir_sense/core/state/attribute_info.ex @@ -0,0 +1,11 @@ +defmodule ElixirSense.Core.State.AttributeInfo do + @moduledoc """ + Variable info + """ + @type t :: %ElixirSense.Core.State.AttributeInfo{ + name: atom, + positions: list(ElixirSense.Core.State.position_t()), + type: ElixirSense.Core.State.var_type() + } + defstruct name: nil, positions: [], type: nil +end diff --git a/lib/elixir_sense/core/state/call_info.ex b/lib/elixir_sense/core/state/call_info.ex new file mode 100644 index 00000000..c0fdfa0c --- /dev/null +++ b/lib/elixir_sense/core/state/call_info.ex @@ -0,0 +1,15 @@ +defmodule ElixirSense.Core.State.CallInfo do + @moduledoc """ + Function call info + """ + @type t :: %ElixirSense.Core.State.CallInfo{ + arity: non_neg_integer, + position: ElixirSense.Core.State.position_t(), + func: atom, + mod: module | {:attribute, atom} + } + defstruct arity: 0, + position: {1, 1}, + func: nil, + mod: Elixir +end diff --git a/lib/elixir_sense/core/state/env.ex b/lib/elixir_sense/core/state/env.ex new file mode 100644 index 00000000..f66447c7 --- /dev/null +++ b/lib/elixir_sense/core/state/env.ex @@ -0,0 +1,77 @@ +defmodule ElixirSense.Core.State.Env do + @moduledoc """ + Line environment + """ + + @type t :: %ElixirSense.Core.State.Env{ + functions: [{module, [{atom, arity}]}], + macros: [{module, [{atom, arity}]}], + requires: list(module), + aliases: list(ElixirSense.Core.State.alias_t()), + macro_aliases: [{module, {term, module}}], + context: nil | :match | :guard, + module: nil | module, + function: nil | {atom, arity}, + protocol: nil | ElixirSense.Core.State.protocol_t(), + versioned_vars: %{optional({atom, atom}) => non_neg_integer}, + vars: list(ElixirSense.Core.State.VarInfo.t()), + attributes: list(ElixirSense.Core.State.AttributeInfo.t()), + behaviours: list(module), + context_modules: list(module), + typespec: nil | {atom, arity}, + scope_id: nil | ElixirSense.Core.State.scope_id_t() + } + defstruct functions: [], + macros: [], + requires: [], + aliases: [], + macro_aliases: [], + # NOTE for protocol implementation this will be the first variant + module: nil, + function: nil, + # NOTE for protocol implementation this will be the first variant + protocol: nil, + versioned_vars: %{}, + vars: [], + attributes: [], + behaviours: [], + context_modules: [], + context: nil, + typespec: nil, + scope_id: nil + + def to_macro_env(%__MODULE__{} = env, file \\ "nofile", line \\ 1) do + # we omit lexical_tracker and tracers + %Macro.Env{ + line: line, + file: file, + context: env.context, + module: env.module, + function: env.function, + context_modules: env.context_modules, + macros: env.macros, + functions: env.functions, + requires: env.requires, + aliases: env.aliases, + macro_aliases: env.macro_aliases, + versioned_vars: env.versioned_vars + } + end + + def update_from_macro_env(%__MODULE__{} = env, macro_env = %Macro.Env{}) do + # we omit lexical_tracker and tracers + %__MODULE__{ + env + | context: macro_env.context, + module: macro_env.module, + function: macro_env.function, + context_modules: macro_env.context_modules, + macros: macro_env.macros, + functions: macro_env.functions, + requires: macro_env.requires, + aliases: macro_env.aliases, + macro_aliases: macro_env.macro_aliases, + versioned_vars: macro_env.versioned_vars + } + end +end diff --git a/lib/elixir_sense/core/state/mod_fun_info.ex b/lib/elixir_sense/core/state/mod_fun_info.ex new file mode 100644 index 00000000..7e1f8f19 --- /dev/null +++ b/lib/elixir_sense/core/state/mod_fun_info.ex @@ -0,0 +1,56 @@ +defmodule ElixirSense.Core.State.ModFunInfo do + @moduledoc """ + Module or function info + """ + alias ElixirSense.Core.State.ModFunInfo + alias ElixirSense.Core.Introspection + + @type t :: %ElixirSense.Core.State.ModFunInfo{ + params: list(list(term)), + positions: list(ElixirSense.Core.State.position_t()), + end_positions: list(ElixirSense.Core.State.position_t() | nil), + target: nil | {module, atom}, + overridable: false | {true, module}, + generated: list(boolean), + doc: String.t(), + meta: map(), + # TODO defmodule defprotocol defimpl? + type: + :def + | :defp + | :defmacro + | :defmacrop + | :defdelegate + | :defguard + | :defguardp + | :defmodule + } + + defstruct params: [], + positions: [], + end_positions: [], + target: nil, + type: nil, + generated: [], + overridable: false, + doc: "", + meta: %{} + + def get_arities(%ModFunInfo{params: params_variants}) do + params_variants + |> Enum.map(fn params -> + {length(params), Introspection.count_defaults(params)} + end) + end + + def get_category(%ModFunInfo{type: type}) + when type in [:defmacro, :defmacrop, :defguard, :defguardp], + do: :macro + + def get_category(%ModFunInfo{type: type}) when type in [:def, :defp, :defdelegate], + do: :function + + def get_category(%ModFunInfo{}), do: :module + + def private?(%ModFunInfo{type: type}), do: type in [:defp, :defmacrop, :defguardp] +end diff --git a/lib/elixir_sense/core/state/spec_info.ex b/lib/elixir_sense/core/state/spec_info.ex new file mode 100644 index 00000000..a80be660 --- /dev/null +++ b/lib/elixir_sense/core/state/spec_info.ex @@ -0,0 +1,25 @@ +defmodule ElixirSense.Core.State.SpecInfo do + @moduledoc """ + Type definition info + """ + @type t :: %ElixirSense.Core.State.SpecInfo{ + name: atom, + args: list(list(String.t())), + specs: [String.t()], + kind: :spec | :callback | :macrocallback, + positions: [ElixirSense.Core.State.position_t()], + end_positions: [ElixirSense.Core.State.position_t() | nil], + doc: String.t(), + meta: map(), + generated: list(boolean) + } + defstruct name: nil, + args: [], + specs: [], + kind: :spec, + positions: [], + end_positions: [], + generated: [], + doc: "", + meta: %{} +end diff --git a/lib/elixir_sense/core/state/struct_info.ex b/lib/elixir_sense/core/state/struct_info.ex new file mode 100644 index 00000000..f1f46632 --- /dev/null +++ b/lib/elixir_sense/core/state/struct_info.ex @@ -0,0 +1,11 @@ +defmodule ElixirSense.Core.State.StructInfo do + @moduledoc """ + Structure definition info + """ + @type field_t :: {atom, any} + @type t :: %ElixirSense.Core.State.StructInfo{ + type: :defstruct | :defexception, + fields: list(field_t) + } + defstruct type: :defstruct, fields: [] +end diff --git a/lib/elixir_sense/core/state/type_info.ex b/lib/elixir_sense/core/state/type_info.ex new file mode 100644 index 00000000..4052a581 --- /dev/null +++ b/lib/elixir_sense/core/state/type_info.ex @@ -0,0 +1,25 @@ +defmodule ElixirSense.Core.State.TypeInfo do + @moduledoc """ + Type definition info + """ + @type t :: %ElixirSense.Core.State.TypeInfo{ + name: atom, + args: list(list(String.t())), + specs: [String.t()], + kind: :type | :typep | :opaque, + positions: [ElixirSense.Core.State.position_t()], + end_positions: [ElixirSense.Core.State.position_t() | nil], + doc: String.t(), + meta: map(), + generated: list(boolean) + } + defstruct name: nil, + args: [], + specs: [], + kind: :type, + positions: [], + end_positions: [], + generated: [], + doc: "", + meta: %{} +end diff --git a/lib/elixir_sense/core/state/var_info.ex b/lib/elixir_sense/core/state/var_info.ex new file mode 100644 index 00000000..c7eba2be --- /dev/null +++ b/lib/elixir_sense/core/state/var_info.ex @@ -0,0 +1,18 @@ +defmodule ElixirSense.Core.State.VarInfo do + @moduledoc """ + Variable info + """ + + @type t :: %ElixirSense.Core.State.VarInfo{ + name: atom, + positions: list(ElixirSense.Core.State.position_t()), + scope_id: nil | ElixirSense.Core.State.scope_id_t(), + version: non_neg_integer(), + type: ElixirSense.Core.State.var_type() + } + defstruct name: nil, + positions: [], + scope_id: nil, + version: 0, + type: nil +end diff --git a/lib/elixir_sense/core/surround_context.ex b/lib/elixir_sense/core/surround_context.ex index 4898c711..8f1b5893 100644 --- a/lib/elixir_sense/core/surround_context.ex +++ b/lib/elixir_sense/core/surround_context.ex @@ -38,7 +38,7 @@ defmodule ElixirSense.Core.SurroundContext do end defp to_binding_impl({:local_or_var, charlist}, _current_module) do - {:variable, :"#{charlist}"} + {:variable, :"#{charlist}", :any} end defp to_binding_impl({:local_arity, charlist}, _current_module) do @@ -120,7 +120,7 @@ defmodule ElixirSense.Core.SurroundContext do end defp inside_dot_to_binding({:var, inside_charlist}, _current_module) do - {:variable, :"#{inside_charlist}"} + {:variable, :"#{inside_charlist}", :any} end defp inside_dot_to_binding(:expr, _current_module) do diff --git a/lib/elixir_sense/core/type_inference.ex b/lib/elixir_sense/core/type_inference.ex new file mode 100644 index 00000000..e31a1fbc --- /dev/null +++ b/lib/elixir_sense/core/type_inference.ex @@ -0,0 +1,443 @@ +defmodule ElixirSense.Core.TypeInference do + def type_of( + {:%, _struct_meta, + [ + _struct_ast, + {:%{}, _map_meta, [{:|, _, _} | _]} + ]}, + :match + ), + do: :none + + def type_of( + {:%, _meta, + [ + struct_ast, + {:%{}, _, _} = ast + ]}, + context + ) do + {fields, updated_struct} = + case type_of(ast, context) do + {:map, fields, updated_map} -> {fields, updated_map} + {:struct, fields, _, updated_struct} -> {fields, updated_struct} + _ -> {[], nil} + end + + type = type_of(struct_ast, context) |> known_struct_type() + + {:struct, fields, type, updated_struct} + end + + # remote call + def type_of({{:., _, [target, fun]}, _, args}, context) + when is_atom(fun) and is_list(args) do + target = type_of(target, context) + {:call, target, fun, Enum.map(args, &type_of(&1, context))} + end + + # pinned variable + def type_of({:^, _, [pinned]}, :match), do: type_of(pinned, nil) + def type_of({:^, _, [_pinned]}, _context), do: :none + + # variable + def type_of({:_, _meta, var_context}, context) + when is_atom(var_context) and context != :match, + do: :none + + def type_of({var, meta, var_context}, context) + when is_atom(var) and is_atom(var_context) and + var not in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__, :_] and + context != :match do + case Keyword.fetch(meta, :version) do + {:ok, version} -> + {:variable, var, version} + + _ -> + nil + end + end + + # attribute + # expanded attribute reference has nil arg + def type_of({:@, _, [{attribute, _, nil}]}, _context) + when is_atom(attribute) do + {:attribute, attribute} + end + + # module or atom + def type_of(atom, _context) when is_atom(atom) do + {:atom, atom} + end + + # map + def type_of({:%{}, _meta, [{:|, _, _} | _]}, :match), do: :none + + def type_of({:%{}, _meta, ast}, context) do + {updated_map, fields} = + case ast do + [{:|, _, [left, right]}] -> + {type_of(left, context), right} + + list -> + {nil, list} + end + + field_types = get_fields_type(fields, context) + + case field_types |> Keyword.fetch(:__struct__) do + {:ok, type} -> + {:struct, field_types |> Keyword.delete(:__struct__), type |> known_struct_type(), + updated_map} + + _ -> + {:map, field_types, updated_map} + end + end + + # match + def type_of({:=, _, [left, right]}, context) do + intersect(type_of(left, :match), type_of(right, context)) + end + + # stepped range struct + def type_of({:"..//", _, [first, last, step]}, context) do + {:struct, + [ + first: type_of(first, context), + last: type_of(last, context), + step: type_of(step, context) + ], {:atom, Range}, nil} + end + + # range struct + def type_of({:.., _, [first, last]}, context) do + {:struct, + [ + first: type_of(first, context), + last: type_of(last, context), + step: type_of(1, context) + ], {:atom, Range}, nil} + end + + @builtin_sigils %{ + sigil_D: Date, + sigil_T: Time, + sigil_U: DateTime, + sigil_N: NaiveDateTime, + sigil_R: Regex, + sigil_r: Regex + } + + # builtin sigil struct + def type_of({sigil, _, _}, _context) when is_map_key(@builtin_sigils, sigil) do + # TODO support custom sigils? + {:struct, [], {:atom, @builtin_sigils |> Map.fetch!(sigil)}, nil} + end + + # tuple + # regular tuples use {:{}, [], [field_1, field_2]} ast + # two element use {field_1, field_2} ast (probably as an optimization) + # detect and convert to regular + def type_of(ast, context) when is_tuple(ast) and tuple_size(ast) == 2 do + type_of({:{}, [], Tuple.to_list(ast)}, context) + end + + def type_of({:{}, _, list}, context) do + {:tuple, length(list), list |> Enum.map(&type_of(&1, context))} + end + + def type_of(list, context) when is_list(list) do + type = + case list do + [] -> + :empty + + [{:|, _, [head, _tail]}] -> + type_of(head, context) + + [head | _] -> + type_of(head, context) + end + + {:list, type} + end + + def type_of(list, context) when is_list(list) do + {:list, list |> Enum.map(&type_of(&1, context))} + end + + # block expressions + def type_of({:__block__, _meta, exprs}, context) do + case List.last(exprs) do + nil -> nil + last_expr -> type_of(last_expr, context) + end + end + + # anonymous functions + def type_of({:fn, _meta, _clauses}, _context), do: nil + + # special forms + # for case/cond/with/receive/for/try we have no idea what the type is going to be + # we don't support binaries + # TODO guard? + # other are not worth handling + def type_of({form, _meta, _clauses}, _context) + when form in [ + :case, + :cond, + :try, + :receive, + :for, + :with, + :quote, + :unquote, + :unquote_splicing, + :import, + :alias, + :require, + :__aliases__, + :__cursor__, + :__DIR__, + :super, + :<<>>, + :"::" + ], + do: nil + + # __ENV__ is already expanded to map + def type_of({form, _meta, _clauses}, _context) when form in [:__CALLER__] do + {:struct, [], {:atom, Macro.Env}, nil} + end + + def type_of({:__STACKTRACE__, _meta, _clauses}, _context) do + {:list, nil} + end + + # local call + def type_of({var, _, args}, context) when is_atom(var) and is_list(args) do + {:local_call, var, Enum.map(args, &type_of(&1, context))} + end + + # integer + def type_of(integer, _context) when is_integer(integer) do + {:integer, integer} + end + + # other + def type_of(_, _), do: nil + + defp get_fields_type(fields, context) do + for {field, value} <- fields, + is_atom(field) do + {field, type_of(value, context)} + end + end + + # expand struct type - only compile type atoms or attributes are supported + # variables supported in match context + defp known_struct_type(type) do + case type do + {:atom, atom} -> {:atom, atom} + {:attribute, attribute} -> {:attribute, attribute} + {:variable, variable, version} -> {:variable, variable, version} + _ -> nil + end + end + + def find_typed_vars(ast, match_context, context) do + {_ast, {vars, _match_context, _context}} = + Macro.prewalk(ast, {[], match_context, context}, &match_var(&1, &2)) + + Enum.uniq(vars) + end + + defp match_var({:when, _, [left, _right]}, match_context) do + # no variables can be defined in guard context so we skip that subtree + match_var(left, match_context) + end + + defp match_var( + {:=, _meta, + [ + left, + right + ]}, + {vars, match_context, context} + ) do + {_ast, {vars, _match_context, _context}} = + match_var( + left, + {vars, intersect(match_context, type_of(right, context)), :match} + ) + + {_ast, {vars, _match_context, _context}} = + match_var( + right, + {vars, intersect(match_context, type_of(left, :match)), context} + ) + + {nil, {vars, nil, context}} + end + + # pinned variable + defp match_var( + {:^, _meta, [{var, _var_meta, var_context}]}, + {vars, match_context, context} + ) + when is_atom(var) and is_atom(var_context) do + {nil, {vars, match_context, context}} + end + + # variable + defp match_var( + {var, meta, var_context}, + {vars, match_context, :match} + ) + when is_atom(var) and is_atom(var_context) and + var not in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__, :_] do + case Keyword.fetch(meta, :version) do + {:ok, version} -> + {nil, {[{{var, version}, match_context} | vars], nil, :match}} + + _ -> + {nil, {vars, match_context, :match}} + end + end + + defp match_var({:%, _, [type_ast, {:%{}, _, _ast} = map_ast]}, {vars, match_context, context}) do + {_ast, {type_vars, _match_context, _context}} = + match_var( + type_ast, + {[], + propagate_context( + match_context, + &{:map_key, &1, type_of(:__struct__, context)} + ), context} + ) + + {_ast, {map_vars, _match_context, _context}} = + match_var(map_ast, {[], match_context, context}) + + {nil, {vars ++ map_vars ++ type_vars, nil, context}} + end + + defp match_var({:%{}, _, ast}, {vars, match_context, context}) do + {updated_vars, list} = + case ast do + [{:|, _, [left, right]} | _] -> + if context == :match do + # map update is forbidden in match, we're in invalid code + {[], []} + else + {_ast, {updated_vars, _match_context, _context}} = match_var(left, {[], nil, context}) + {updated_vars, right} + end + + list -> + {[], list} + end + + destructured_vars = + list + |> Enum.flat_map(fn + {key, value_ast} -> + key_type = type_of(key, context) + + {_ast, {new_vars, _match_context, _context}} = + match_var( + value_ast, + {[], propagate_context(match_context, &{:map_key, &1, key_type}), context} + ) + + new_vars + end) + + {nil, {vars ++ destructured_vars ++ updated_vars, nil, context}} + end + + # regular tuples use {:{}, [], [field_1, field_2]} ast + # two element use `{field_1, field_2}` ast (probably as an optimization) + # detect and convert to regular + defp match_var(ast, {vars, match_context, context}) + when is_tuple(ast) and tuple_size(ast) == 2 do + match_var({:{}, [], ast |> Tuple.to_list()}, {vars, match_context, context}) + end + + defp match_var({:{}, _, ast}, {vars, match_context, context}) do + destructured_vars = + ast + |> Enum.with_index() + |> Enum.flat_map(fn {nth_elem_ast, n} -> + {_ast, {new_vars, _match_context, _context}} = + match_var( + nth_elem_ast, + {[], propagate_context(match_context, &{:tuple_nth, &1, n}), context} + ) + + new_vars + end) + + {nil, {vars ++ destructured_vars, nil, context}} + end + + defp match_var({{:., _, [:erlang, :++]}, _, [left, right]}, {vars, match_context, context}) + when is_list(left) do + # NOTE this may produce improper lists + match_var(left ++ right, {vars, match_context, context}) + end + + defp match_var(list, {vars, match_context, context}) when is_list(list) do + match_var_list = fn head, tail -> + {_ast, {new_vars_head, _match_context, _context}} = + match_var(head, {[], propagate_context(match_context, &{:list_head, &1}), context}) + + {_ast, {new_vars_tail, _match_context, _context}} = + match_var(tail, {[], propagate_context(match_context, &{:list_tail, &1}), context}) + + {nil, {vars ++ new_vars_head ++ new_vars_tail, nil, context}} + end + + case list do + [] -> + {nil, {vars, nil, context}} + + [{:|, _, [head, tail]}] -> + match_var_list.(head, tail) + + [head | tail] -> + match_var_list.(head, tail) + end + end + + defp match_var(ast, {vars, _match_context, context}) do + # traverse literals, not expanded macro calls and bitstrings with nil match_context + # we cannot assume anything basing on match_context on variables there + {ast, {vars, nil, context}} + end + + defp propagate_context(nil, _), do: nil + defp propagate_context(:none, _), do: :none + defp propagate_context(match_context, fun), do: fun.(match_context) + + def intersect(nil, new), do: new + def intersect(old, nil), do: old + def intersect(:none, _), do: :none + def intersect(_, :none), do: :none + def intersect(old, old), do: old + + def intersect({:intersection, old}, {:intersection, new}) do + {:intersection, Enum.uniq(old ++ new)} + end + + def intersect({:intersection, old}, new) do + {:intersection, Enum.uniq([new | old])} + end + + def intersect(old, {:intersection, new}) do + {:intersection, Enum.uniq([old | new])} + end + + def intersect(old, new), do: {:intersection, [old, new]} +end diff --git a/lib/elixir_sense/core/type_inference/guard.ex b/lib/elixir_sense/core/type_inference/guard.ex new file mode 100644 index 00000000..9867045c --- /dev/null +++ b/lib/elixir_sense/core/type_inference/guard.ex @@ -0,0 +1,298 @@ +defmodule ElixirSense.Core.TypeInference.Guard do + alias ElixirSense.Core.TypeInference + + @moduledoc """ + This module is responsible for infer type information from guard expressions + """ + + # A guard expression can be in either these form: + # :and :or + # / \ or / \ or :not guard_expr or guard_expr or list(guard_expr) + # guard_expr guard_expr guard_expr guard_expr + # + def type_information_from_guards({:when, meta, [left, right]}) do + # treat nested guard as or expression + # this is not valid only in case of raising expressions in guard + # but it doesn't matter in our case we are not evaluating + type_information_from_guards({{:., meta, [:erlang, :orelse]}, meta, [left, right]}) + end + + def type_information_from_guards(list) when is_list(list) do + for expr <- list, reduce: %{} do + acc -> + right = type_information_from_guards(expr) + + Map.merge(acc, right, fn _k, v1, v2 -> + case {v1, v2} do + {{:union, types_1}, {:union, types_2}} -> {:union, types_1 ++ types_2} + {{:union, types}, _} -> {:union, types ++ [v2]} + {_, {:union, types}} -> {:union, [v1 | types]} + _ -> {:union, [v1, v2]} + end + end) + end + end + + def type_information_from_guards({{:., _, [:erlang, :not]}, _, [guard_l]}) do + left = type_information_from_guards(guard_l) + for {k, _v} <- left, into: %{}, do: {k, nil} + end + + def type_information_from_guards({{:., _, [:erlang, :andalso]}, _, [guard_l, guard_r]}) do + left = type_information_from_guards(guard_l) + right = type_information_from_guards(guard_r) + + Map.merge(left, right, fn _k, v1, v2 -> + TypeInference.intersect(v1, v2) + end) + end + + def type_information_from_guards({{:., _, [:erlang, :orelse]}, _, [guard_l, guard_r]}) do + left = type_information_from_guards(guard_l) + right = type_information_from_guards(guard_r) + + merged_keys = (Map.keys(left) ++ Map.keys(right)) |> Enum.uniq() + + Enum.reduce(merged_keys, %{}, fn key, acc -> + v1 = Map.get(left, key) + v2 = Map.get(right, key) + + # we can union types only if both sides constrain the same variable + # otherwise, it's not possible to infer type information from guard expression + # e.g. is_integer(x) or is_atom(x) can be unionized + # is_integer(x) or is_atom(y) cannot + + new_value = + case {v1, v2} do + {nil, nil} -> nil + {nil, _} -> nil + {_, nil} -> nil + {{:union, types_1}, {:union, types_2}} -> {:union, types_1 ++ types_2} + {{:union, types}, other} -> {:union, types ++ [other]} + {other, {:union, types}} -> {:union, [other | types]} + {other1, other2} -> {:union, [other1, other2]} + end + + Map.put(acc, key, new_value) + end) + end + + # {{:., _, [target, key]}, _, []} + def type_information_from_guards({{:., _, [target, key]}, _, []}) when is_atom(key) do + case extract_var_type(target, {:map, [{key, {:atom, true}}], []}) do + nil -> %{} + {var, type} -> %{var => type} + end + end + + # Standalone variable: func my_func(x) when x + def type_information_from_guards({var, meta, context}) when is_atom(var) and is_atom(context) do + case Keyword.fetch(meta, :version) do + {:ok, version} -> + %{{var, version} => {:atom, true}} + + _ -> + %{} + end + end + + def type_information_from_guards(guard_ast) do + {_, acc} = + Macro.prewalk(guard_ast, %{}, fn + {{:., _dot_meta, [:erlang, fun]}, _call_meta, params}, acc + when is_atom(fun) and is_list(params) -> + with {type, binding} <- guard_predicate_type(fun, params), + {{var, version}, type} <- extract_var_type(binding, type) do + # If we found the predicate type, we can prematurely exit traversing the subtree + {nil, Map.put(acc, {var, version}, type)} + else + _ -> + # traverse params + {params, acc} + end + + {{:., _dot_meta, [_remote, _fun]}, _call_meta, params}, acc -> + # not expanded remote or fun - traverse params + {params, acc} + + node, acc -> + {node, acc} + end) + + acc + end + + defp extract_var_type({var, meta, context}, type) when is_atom(var) and is_atom(context) do + case Keyword.fetch(meta, :version) do + {:ok, version} -> + {{var, version}, type} + + _ -> + nil + end + end + + defp extract_var_type({{:., _, [target, key]}, _, []}, type) when is_atom(key) do + extract_var_type(target, {:map, [{key, type}], []}) + end + + defp extract_var_type(_, _), do: nil + + # TODO div and rem only work on first arg + defp guard_predicate_type(p, [first | _]) + when p in [ + :is_number, + :is_float, + :is_integer, + :round, + :trunc, + :div, + :rem, + :abs, + :ceil, + :floor + ], + do: {:number, first} + + defp guard_predicate_type(p, [first | _]) when p in [:is_binary, :binary_part], + do: {:binary, first} + + defp guard_predicate_type(p, [first | _]) when p in [:is_bitstring, :bit_size, :byte_size], + do: {:bitstring, first} + + defp guard_predicate_type(p, [first | _]) when p in [:is_list, :length], do: {:list, first} + + defp guard_predicate_type(p, [first | _]) when p in [:hd, :tl], + do: {{:list, :boolean}, first} + + # when hd(x) == 1 + # when tl(x) == [2] + defp guard_predicate_type(p, [{{:., _, [:erlang, guard]}, _, [first | _]}, rhs]) + when p in [:==, :===, :>=, :>, :<=, :<] and guard in [:hd, :tl] do + rhs_type = type_of(rhs) + + rhs_type = if guard == :hd and rhs_type != nil, do: {:list, rhs_type}, else: :list + + {rhs_type, first} + end + + defp guard_predicate_type(p, [lhs, {{:., _, [:erlang, guard]}, _, _guard_params} = call]) + when p in [:==, :===, :>=, :>, :<=, :<] and guard in [:hd, :tl] do + guard_predicate_type(p, [call, lhs]) + end + + defp guard_predicate_type(p, [first | _]) when p in [:is_tuple], + do: {:tuple, first} + + defp guard_predicate_type(p, [_, second | _]) when p in [:element], + do: {:tuple, second} + + # when tuple_size(x) == 1 + # when tuple_size(x) == 2 + defp guard_predicate_type(p, [{{:., _, [:erlang, :tuple_size]}, _, [first | _]}, size]) + when p in [:==, :===, :>=, :>, :<=, :<] do + type = + if is_integer(size) and p in [:==, :===] do + {:tuple, size, if(size > 0, do: Enum.map(1..size, fn _ -> nil end), else: [])} + else + :tuple + end + + {type, first} + end + + defp guard_predicate_type(p, [size, {{:., _, [:erlang, :tuple_size]}, _, _guard_params} = call]) + when p in [:==, :===, :>=, :>, :<=, :<] do + guard_predicate_type(p, [call, size]) + end + + defp guard_predicate_type(p, [{{:., _, [:erlang, :map_get]}, _, [key, second | _]}, value]) + when p in [:==, :===] do + type = + cond do + key == :__struct__ and is_atom(value) -> + {:struct, [], {:atom, value}, nil} + + key == :__struct__ -> + {:struct, [], nil, nil} + + is_atom(key) or is_binary(key) -> + # TODO other types of keys? + rhs_type = type_of(value) + + {:map, [{key, rhs_type}], nil} + + true -> + {:map, [], nil} + end + + {type, second} + end + + defp guard_predicate_type(p, [value, {{:., _, [:erlang, :map_get]}, _, _guard_params} = call]) + when p in [:==, :===] do + guard_predicate_type(p, [call, value]) + end + + defp guard_predicate_type(p, [{variable_l, _, context_l}, {variable_r, _, context_r}]) + when p in [:==, :===] and is_atom(variable_l) and is_atom(context_l) and + is_atom(variable_r) and is_atom(context_r), + do: nil + + defp guard_predicate_type(p, [{variable, _, context} = lhs, value]) + when p in [:==, :===] and is_atom(variable) and is_atom(context) do + {type_of(value), lhs} + end + + defp guard_predicate_type(p, [{{:., _, _}, _, []} = lhs, value]) when p in [:==, :===] do + {type_of(value), lhs} + end + + defp guard_predicate_type(p, [value, {variable, _, context} = rhs]) + when p in [:==, :===] and is_atom(variable) and is_atom(context) do + guard_predicate_type(p, [rhs, value]) + end + + defp guard_predicate_type(p, [value, {{:., _, _}, _, []} = rhs]) when p in [:==, :===] do + guard_predicate_type(p, [rhs, value]) + end + + defp guard_predicate_type(:is_map, [first | _]), do: {{:map, [], nil}, first} + defp guard_predicate_type(:is_non_struct_map, [first | _]), do: {{:map, [], nil}, first} + defp guard_predicate_type(:map_size, [first | _]), do: {{:map, [], nil}, first} + + defp guard_predicate_type(:is_map_key, [key, var | _]) do + # TODO other types of keys? + type = + case key do + :__struct__ -> {:struct, [], nil, nil} + key when is_atom(key) when is_binary(key) -> {:map, [{key, nil}], nil} + _ -> {:map, [], nil} + end + + {type, var} + end + + defp guard_predicate_type(:map_get, [key, var | _]) do + # TODO other types of keys? + type = + case key do + :__struct__ -> {:struct, [], nil, nil} + key when is_atom(key) when is_binary(key) -> {:map, [{key, nil}], nil} + _ -> {:map, [], nil} + end + + {type, var} + end + + defp guard_predicate_type(:is_atom, [first | _]), do: {:atom, first} + defp guard_predicate_type(:is_boolean, [first | _]), do: {:boolean, first} + + defp guard_predicate_type(_, _), do: nil + + defp type_of(expression) do + TypeInference.type_of(expression, :guard) + end + + # TODO :in :is_function/1-2 :is_pid :is_port :is_reference :node/0-1 :self +end diff --git a/lib/elixir_sense/providers/completion/completion_engine.ex b/lib/elixir_sense/providers/completion/completion_engine.ex index 36ed51bd..88472936 100644 --- a/lib/elixir_sense/providers/completion/completion_engine.ex +++ b/lib/elixir_sense/providers/completion/completion_engine.ex @@ -306,7 +306,7 @@ defmodule ElixirSense.Providers.Completion.CompletionEngine do end defp expand_dot_path({:var, var}, %State.Env{} = env, %Metadata{} = metadata) do - value_from_binding({:variable, List.to_atom(var)}, env, metadata) + value_from_binding({:variable, List.to_atom(var), :any}, env, metadata) end defp expand_dot_path({:module_attribute, attribute}, %State.Env{} = env, %Metadata{} = metadata) do diff --git a/lib/elixir_sense/providers/completion/suggestion.ex b/lib/elixir_sense/providers/completion/suggestion.ex index 4a37d7de..20aeb2d5 100644 --- a/lib/elixir_sense/providers/completion/suggestion.ex +++ b/lib/elixir_sense/providers/completion/suggestion.ex @@ -122,11 +122,6 @@ defmodule ElixirSense.Providers.Completion.Suggestion do env = Metadata.get_env(metadata, {line, column}) - |> Metadata.add_scope_vars( - metadata, - {line, column}, - &(to_string(&1.name) != hint) - ) # if variable is rebound then in env there are many variables with the same name # find the one defined closest to cursor diff --git a/lib/elixir_sense/providers/definition/locator.ex b/lib/elixir_sense/providers/definition/locator.ex index 90a420f2..46613c97 100644 --- a/lib/elixir_sense/providers/definition/locator.ex +++ b/lib/elixir_sense/providers/definition/locator.ex @@ -40,7 +40,6 @@ defmodule ElixirSense.Providers.Definition.Locator do env = Metadata.get_env(metadata, {line, column}) - |> Metadata.add_scope_vars(metadata, {line, column}) find( context, @@ -78,12 +77,13 @@ defmodule ElixirSense.Providers.Definition.Locator do {:keyword, _} -> nil - {:variable, variable} -> + {:variable, variable, version} -> var_info = vars |> Enum.find(fn - %VarInfo{name: name, positions: positions} -> - name == variable and context.begin in positions + %VarInfo{} = info -> + info.name == variable and (info.version == version or version == :any) and + context.begin in info.positions end) if var_info != nil do diff --git a/lib/elixir_sense/providers/hover/docs.ex b/lib/elixir_sense/providers/hover/docs.ex index d2317816..5bcaa736 100644 --- a/lib/elixir_sense/providers/hover/docs.ex +++ b/lib/elixir_sense/providers/hover/docs.ex @@ -127,14 +127,15 @@ defmodule ElixirSense.Providers.Hover.Docs do docs: docs } - {:variable, variable} -> + {:variable, variable, version} -> {line, column} = context.begin var_info = vars |> Enum.find(fn - %VarInfo{name: name, positions: positions} -> - name == variable and {line, column} in positions + %VarInfo{} = info -> + info.name == variable and (info.version == version or version == :any) and + {line, column} in info.positions end) if var_info != nil do @@ -144,7 +145,7 @@ defmodule ElixirSense.Providers.Hover.Docs do } else mod_fun_docs( - type, + {nil, variable}, context, binding_env, env, diff --git a/lib/elixir_sense/providers/implementation/locator.ex b/lib/elixir_sense/providers/implementation/locator.ex index e6851fc9..2e0e76b8 100644 --- a/lib/elixir_sense/providers/implementation/locator.ex +++ b/lib/elixir_sense/providers/implementation/locator.ex @@ -63,6 +63,10 @@ defmodule ElixirSense.Providers.Implementation.Locator do {kind, _} when kind in [:attribute, :keyword] -> [] + {:variable, name, _} -> + # treat variable name as local function call + do_find(nil, name, context, env, metadata, binding_env) + {module_type, function} -> module = case Binding.expand(binding_env, module_type) do @@ -73,32 +77,36 @@ defmodule ElixirSense.Providers.Implementation.Locator do env.module end - {line, column} = context.end - call_arity = Metadata.get_call_arity(metadata, module, function, line, column) || :any - - behaviour_implementations = - find_behaviour_implementations( - module, - function, - call_arity, - module, - env, - metadata, - binding_env - ) + do_find(module, function, context, env, metadata, binding_env) + end + end - if behaviour_implementations == [] do - find_delegatee( - {module, function}, - call_arity, - env, - metadata, - binding_env - ) - |> List.wrap() - else - behaviour_implementations - end + defp do_find(module, function, context, env, metadata, binding_env) do + {line, column} = context.end + call_arity = Metadata.get_call_arity(metadata, module, function, line, column) || :any + + behaviour_implementations = + find_behaviour_implementations( + module, + function, + call_arity, + module, + env, + metadata, + binding_env + ) + + if behaviour_implementations == [] do + find_delegatee( + {module, function}, + call_arity, + env, + metadata, + binding_env + ) + |> List.wrap() + else + behaviour_implementations end end diff --git a/lib/elixir_sense/providers/plugins/phoenix/scope.ex b/lib/elixir_sense/providers/plugins/phoenix/scope.ex index 75c08634..e7730b2c 100644 --- a/lib/elixir_sense/providers/plugins/phoenix/scope.ex +++ b/lib/elixir_sense/providers/plugins/phoenix/scope.ex @@ -98,8 +98,10 @@ defmodule ElixirSense.Providers.Plugins.Phoenix.Scope do get_mod(scope_alias, binding_env) end - defp get_mod({name, _, nil}, binding_env) when is_atom(name) do - case Binding.expand(binding_env, {:variable, name}) do + defp get_mod({name, meta, context}, binding_env) + when is_atom(name) and is_atom(context) and + name not in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__, :_] do + case Binding.expand(binding_env, {:variable, name, Keyword.get(meta, :version, :any)}) do {:atom, atom} -> atom diff --git a/lib/elixir_sense/providers/references/locator.ex b/lib/elixir_sense/providers/references/locator.ex index 1de5b900..87a3f381 100644 --- a/lib/elixir_sense/providers/references/locator.ex +++ b/lib/elixir_sense/providers/references/locator.ex @@ -31,7 +31,6 @@ defmodule ElixirSense.Providers.References.Locator do module: module } = Metadata.get_env(metadata, {line, column}) - |> Metadata.add_scope_vars(metadata, {line, column}) # find last env of current module attributes = get_attributes(metadata, module) @@ -186,12 +185,15 @@ defmodule ElixirSense.Providers.References.Locator do {:keyword, _} -> [] - {:variable, variable} -> + {:variable, variable, version} -> {line, column} = context.begin var_info = - Enum.find(vars, fn %VarInfo{name: name, positions: positions} -> - name == variable and {line, column} in positions + Enum.find_value(vars, fn {{_name, _version}, %VarInfo{} = info} -> + if info.name == variable and (info.version == version or version == :any) and + {line, column} in info.positions do + info + end end) if var_info != nil do diff --git a/mix.exs b/mix.exs index d30afbf1..d452fab5 100644 --- a/mix.exs +++ b/mix.exs @@ -6,7 +6,7 @@ defmodule ElixirSense.MixProject do [ app: :elixir_sense, version: "2.0.0", - elixir: "~> 1.12", + elixir: "~> 1.13", elixirc_paths: elixirc_paths(Mix.env()), build_embedded: Mix.env() == :prod, start_permanent: Mix.env() == :prod, diff --git a/test/elixir_sense/core/ast_test.exs b/test/elixir_sense/core/ast_test.exs deleted file mode 100644 index 5a6eac35..00000000 --- a/test/elixir_sense/core/ast_test.exs +++ /dev/null @@ -1,36 +0,0 @@ -defmodule ElixirSense.Core.AstTest do - use ExUnit.Case, async: true - alias ElixirSense.Core.Ast - - defmodule ExpandRecursive do - defmacro my_macro do - quote do - abc = my_macro() - end - end - end - - test "expand_partial cannot expand recursive macros" do - import ExpandRecursive - - result = - quote do - my_macro() - end - |> Ast.expand_partial(__ENV__) - - assert result == {:expand_error, "Cannot expand recursive macro"} - end - - test "expand_all cannot expand recursive macros" do - import ExpandRecursive - - result = - quote do - my_macro() - end - |> Ast.expand_all(__ENV__) - - assert result == {:expand_error, "Cannot expand recursive macro"} - end -end diff --git a/test/elixir_sense/core/binding_test.exs b/test/elixir_sense/core/binding_test.exs index 28217c28..84cac38a 100644 --- a/test/elixir_sense/core/binding_test.exs +++ b/test/elixir_sense/core/binding_test.exs @@ -79,15 +79,15 @@ defmodule ElixirSense.Core.BindingTest do end test "map" do - assert {:map, [abc: nil, cde: {:variable, :a}], nil} == - Binding.expand(@env, {:map, [abc: nil, cde: {:variable, :a}], nil}) + assert {:map, [abc: nil, cde: {:variable, :a, 1}], nil} == + Binding.expand(@env, {:map, [abc: nil, cde: {:variable, :a, 1}], nil}) end test "map update" do - assert {:map, [{:efg, {:atom, :a}}, {:abc, nil}, {:cde, {:variable, :a}}], nil} == + assert {:map, [{:efg, {:atom, :a}}, {:abc, nil}, {:cde, {:variable, :a, 1}}], nil} == Binding.expand( @env, - {:map, [abc: nil, cde: {:variable, :a}], + {:map, [abc: nil, cde: {:variable, :a, 1}], {:map, [abc: nil, cde: nil, efg: {:atom, :a}], nil}} ) end @@ -106,6 +106,94 @@ defmodule ElixirSense.Core.BindingTest do ) end + test "introspection struct from guard" do + assert {:struct, [__struct__: nil], nil, nil} == + Binding.expand( + @env, + { + :intersection, + [ + {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, + {:struct, [], nil, nil} + ] + } + ) + + assert { + :struct, + [ + {:__struct__, {:atom, URI}} | _ + ], + {:atom, URI}, + nil + } = + Binding.expand( + @env, + { + :intersection, + [ + {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, + {:struct, [], {:atom, URI}, nil} + ] + } + ) + + assert {:struct, [__struct__: nil, __exception__: {:atom, true}], nil, nil} == + Binding.expand( + @env, + { + :intersection, + [ + { + :intersection, + [ + { + :intersection, + [ + {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, + {:struct, [], nil, nil} + ] + }, + {:map, [{:__exception__, nil}], nil} + ] + }, + {:map, [{:__exception__, {:atom, true}}], nil} + ] + } + ) + + assert { + :struct, + [ + {:__struct__, {:atom, ArgumentError}} | _ + ], + {:atom, ArgumentError}, + nil + } = + Binding.expand( + @env, + { + :intersection, + [ + { + :intersection, + [ + { + :intersection, + [ + {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, + {:struct, [], {:atom, ArgumentError}, nil} + ] + }, + {:map, [{:__exception__, nil}], nil} + ] + }, + {:map, [{:__exception__, {:atom, true}}], nil} + ] + } + ) + end + test "introspection module not a struct" do assert :none == Binding.expand(@env, {:struct, [], {:atom, ElixirSenseExample.EmptyModule}, nil}) @@ -169,9 +257,13 @@ defmodule ElixirSense.Core.BindingTest do Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :v, type: {:atom, ElixirSenseExample.ModuleWithTypedStruct}} + %VarInfo{ + version: 1, + name: :v, + type: {:atom, ElixirSenseExample.ModuleWithTypedStruct} + } ]), - {:struct, [], {:variable, :v}, nil} + {:struct, [], {:variable, :v, 1}, nil} ) end @@ -209,29 +301,43 @@ defmodule ElixirSense.Core.BindingTest do test "known variable" do assert {:atom, :abc} == Binding.expand( - @env |> Map.put(:variables, [%VarInfo{name: :v, type: {:atom, :abc}}]), - {:variable, :v} + @env + |> Map.put(:variables, [%VarInfo{version: 1, name: :v, type: {:atom, :abc}}]), + {:variable, :v, 1} ) end - test "known variable self referencing" do - assert nil == + test "known variable any version chooses max" do + assert {:atom, :abc} == Binding.expand( - @env |> Map.put(:variables, [%VarInfo{name: :v, type: {:variable, :v}}]), - {:variable, :v} + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :v, type: {:atom, :foo}}, + %VarInfo{version: 3, name: :v, type: {:atom, :abc}}, + %VarInfo{version: 2, name: :v, type: {:atom, :bar}} + ]), + {:variable, :v, :any} ) end - test "anonymous variable" do - assert :none == + test "known variable self referencing" do + assert nil == Binding.expand( - @env |> Map.put(:variables, [%VarInfo{name: :_, type: {:atom, :abc}}]), - {:variable, :_} + @env + |> Map.put(:variables, [%VarInfo{version: 1, name: :v, type: {:variable, :v, 1}}]), + {:variable, :v, 1} ) end test "unknown variable" do - assert :none == Binding.expand(@env, {:variable, :v}) + assert :none == Binding.expand(@env, {:variable, :v, 1}) + + assert :none == + Binding.expand( + @env + |> Map.put(:variables, [%VarInfo{version: 1, name: :v, type: {:integer, 1}}]), + {:variable, :v, 2} + ) end test "known attribute" do @@ -259,10 +365,14 @@ defmodule ElixirSense.Core.BindingTest do Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :tuple, type: {:tuple, 2, [nil, {:variable, :a}]}}, - %VarInfo{name: :a, type: {:atom, :abc}} + %VarInfo{ + version: 1, + name: :tuple, + type: {:tuple, 2, [nil, {:variable, :a, 1}]} + }, + %VarInfo{version: 1, name: :a, type: {:atom, :abc}} ]), - {:variable, :tuple} + {:variable, :tuple, 1} ) end @@ -271,10 +381,10 @@ defmodule ElixirSense.Core.BindingTest do Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :tuple, type: {:tuple, 2, [nil, {:atom, :a}]}}, - %VarInfo{name: :ref, type: {:tuple_nth, {:variable, :tuple}, 1}} + %VarInfo{version: 1, name: :tuple, type: {:tuple, 2, [nil, {:atom, :a}]}}, + %VarInfo{version: 1, name: :ref, type: {:tuple_nth, {:variable, :tuple, 1}, 1}} ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -283,10 +393,10 @@ defmodule ElixirSense.Core.BindingTest do Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :list, type: {:list, {:variable, :a}}}, - %VarInfo{name: :a, type: {:atom, :abc}} + %VarInfo{version: 1, name: :list, type: {:list, {:variable, :a, 1}}}, + %VarInfo{version: 1, name: :a, type: {:atom, :abc}} ]), - {:variable, :list} + {:variable, :list, 1} ) end @@ -295,10 +405,14 @@ defmodule ElixirSense.Core.BindingTest do Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :map, type: {:map_key, {:variable, :a}, {:atom, :x}}}, - %VarInfo{name: :a, type: {:map, [x: {:atom, :abc}], nil}} + %VarInfo{ + version: 1, + name: :map, + type: {:map_key, {:variable, :a, 1}, {:atom, :x}} + }, + %VarInfo{version: 1, name: :a, type: {:map, [x: {:atom, :abc}], nil}} ]), - {:variable, :map} + {:variable, :map, 1} ) assert {:atom, :abc} == @@ -306,37 +420,43 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :struct, - type: {:map_key, {:variable, :a}, {:atom, :typed_field}} + type: {:map_key, {:variable, :a, 1}, {:atom, :typed_field}} }, %VarInfo{ + version: 1, name: :a, type: {:struct, [typed_field: {:atom, :abc}], {:atom, ElixirSenseExample.ModuleWithTypedStruct}, nil} } ]), - {:variable, :struct} + {:variable, :struct, 1} ) assert nil == Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :map, type: {:map_key, {:variable, :a}, {:atom, :y}}}, - %VarInfo{name: :a, type: {:map, [x: {:atom, :abc}], nil}} + %VarInfo{ + version: 1, + name: :map, + type: {:map_key, {:variable, :a, 1}, {:atom, :y}} + }, + %VarInfo{version: 1, name: :a, type: {:map, [x: {:atom, :abc}], nil}} ]), - {:variable, :map} + {:variable, :map, 1} ) assert nil == Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :map, type: {:map_key, {:variable, :a}, nil}}, - %VarInfo{name: :a, type: {:map, [x: {:atom, :abc}], nil}} + %VarInfo{version: 1, name: :map, type: {:map_key, {:variable, :a, 1}, nil}}, + %VarInfo{version: 1, name: :a, type: {:map, [x: {:atom, :abc}], nil}} ]), - {:variable, :map} + {:variable, :map, 1} ) end @@ -345,30 +465,30 @@ defmodule ElixirSense.Core.BindingTest do Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :list, type: {:for_expression, {:variable, :a}}}, - %VarInfo{name: :a, type: {:list, {:atom, :abc}}} + %VarInfo{version: 1, name: :list, type: {:for_expression, {:variable, :a, 1}}}, + %VarInfo{version: 1, name: :a, type: {:list, {:atom, :abc}}} ]), - {:variable, :list} + {:variable, :list, 1} ) assert {:tuple, 2, [nil, {:atom, :abc}]} == Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :map, type: {:for_expression, {:variable, :a}}}, - %VarInfo{name: :a, type: {:map, [x: {:atom, :abc}], nil}} + %VarInfo{version: 1, name: :map, type: {:for_expression, {:variable, :a, 1}}}, + %VarInfo{version: 1, name: :a, type: {:map, [x: {:atom, :abc}], nil}} ]), - {:variable, :map} + {:variable, :map, 1} ) assert :none == Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :list, type: {:list_head, {:variable, :a}}}, - %VarInfo{name: :a, type: {:list, :empty}} + %VarInfo{version: 1, name: :list, type: {:list_head, {:variable, :a, 1}}}, + %VarInfo{version: 1, name: :a, type: {:list, :empty}} ]), - {:variable, :list} + {:variable, :list, 1} ) end @@ -377,20 +497,20 @@ defmodule ElixirSense.Core.BindingTest do Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :list, type: {:list_head, {:variable, :a}}}, - %VarInfo{name: :a, type: {:list, {:atom, :abc}}} + %VarInfo{version: 1, name: :list, type: {:list_head, {:variable, :a, 1}}}, + %VarInfo{version: 1, name: :a, type: {:list, {:atom, :abc}}} ]), - {:variable, :list} + {:variable, :list, 1} ) assert :none == Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :list, type: {:list_head, {:variable, :a}}}, - %VarInfo{name: :a, type: {:list, :empty}} + %VarInfo{version: 1, name: :list, type: {:list_head, {:variable, :a, 1}}}, + %VarInfo{version: 1, name: :a, type: {:list, :empty}} ]), - {:variable, :list} + {:variable, :list, 1} ) end @@ -399,20 +519,20 @@ defmodule ElixirSense.Core.BindingTest do Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :list, type: {:list_tail, {:variable, :a}}}, - %VarInfo{name: :a, type: {:list, {:atom, :abc}}} + %VarInfo{version: 1, name: :list, type: {:list_tail, {:variable, :a, 1}}}, + %VarInfo{version: 1, name: :a, type: {:list, {:atom, :abc}}} ]), - {:variable, :list} + {:variable, :list, 1} ) assert :none == Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :list, type: {:list_tail, {:variable, :a}}}, - %VarInfo{name: :a, type: {:list, :empty}} + %VarInfo{version: 1, name: :list, type: {:list_tail, {:variable, :a, 1}}}, + %VarInfo{version: 1, name: :a, type: {:list, :empty}} ]), - {:variable, :list} + {:variable, :list, 1} ) end @@ -421,10 +541,14 @@ defmodule ElixirSense.Core.BindingTest do Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :map, type: {:map, [field: {:atom, :a}], nil}}, - %VarInfo{name: :ref, type: {:call, {:variable, :map}, :field, []}} + %VarInfo{version: 1, name: :map, type: {:map, [field: {:atom, :a}], nil}}, + %VarInfo{ + version: 1, + name: :ref, + type: {:call, {:variable, :map, 1}, :field, []} + } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -433,10 +557,14 @@ defmodule ElixirSense.Core.BindingTest do Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :map, type: {:map, [field: {:atom, :a}], nil}}, - %VarInfo{name: :ref, type: {:call, {:variable, :map}, :field, [nil]}} + %VarInfo{version: 1, name: :map, type: {:map, [field: {:atom, :a}], nil}}, + %VarInfo{ + version: 1, + name: :ref, + type: {:call, {:variable, :map, 1}, :field, [nil]} + } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -445,10 +573,14 @@ defmodule ElixirSense.Core.BindingTest do Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :map, type: {:map, [field: {:atom, :a}], nil}}, - %VarInfo{name: :ref, type: {:call, {:variable, :map}, :not_existing, []}} + %VarInfo{version: 1, name: :map, type: {:map, [field: {:atom, :a}], nil}}, + %VarInfo{ + version: 1, + name: :ref, + type: {:call, {:variable, :map, 1}, :not_existing, []} + } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -458,14 +590,19 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :map, type: {:struct, [typed_field: {:atom, :abc}], {:atom, ElixirSenseExample.ModuleWithTypedStruct}, nil} }, - %VarInfo{name: :ref, type: {:call, {:variable, :map}, :typed_field, []}} + %VarInfo{ + version: 1, + name: :ref, + type: {:call, {:variable, :map, 1}, :typed_field, []} + } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -475,14 +612,19 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :map, type: {:struct, [typed_field: {:atom, :abc}], {:atom, ElixirSenseExample.ModuleWithTypedStruct}, nil} }, - %VarInfo{name: :ref, type: {:call, {:variable, :map}, :not_existing, []}} + %VarInfo{ + version: 1, + name: :ref, + type: {:call, {:variable, :map, 1}, :not_existing, []} + } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -492,14 +634,19 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :map, type: {:struct, [typed_field: {:atom, :abc}], {:atom, ElixirSenseExample.ModuleWithTypedStruct}, nil} }, - %VarInfo{name: :ref, type: {:call, {:variable, :map}, :typed_field, [nil]}} + %VarInfo{ + version: 1, + name: :ref, + type: {:call, {:variable, :map, 1}, :typed_field, [nil]} + } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -508,36 +655,44 @@ defmodule ElixirSense.Core.BindingTest do Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :ref, type: {:call, nil, :not_existing, []}} + %VarInfo{version: 1, name: :ref, type: {:call, nil, :not_existing, []}} ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert :none == Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :ref, type: {:call, :none, :not_existing, []}} + %VarInfo{version: 1, name: :ref, type: {:call, :none, :not_existing, []}} ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert :none == Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :ref, type: {:call, {:atom, nil}, :not_existing, []}} + %VarInfo{ + version: 1, + name: :ref, + type: {:call, {:atom, nil}, :not_existing, []} + } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert :none == Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :ref, type: {:call, {:atom, true}, :not_existing, []}} + %VarInfo{ + version: 1, + name: :ref, + type: {:call, {:atom, true}, :not_existing, []} + } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -547,13 +702,14 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :not_existing, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -563,12 +719,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f1, [nil]} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -578,12 +735,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f1x, [:none]} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -593,11 +751,12 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f01, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -607,11 +766,12 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f02, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -621,11 +781,12 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f04, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -635,12 +796,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :list1, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:list, nil} == @@ -648,12 +810,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :list2, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:list, nil} == @@ -661,12 +824,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :list3, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:list, {:atom, :ok}} == @@ -674,12 +838,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :list4, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:list, {:atom, :ok}} == @@ -687,12 +852,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :list5, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:list, {:atom, :ok}} == @@ -700,12 +866,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :list6, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:list, {:atom, :ok}} == @@ -713,12 +880,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :list7, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:list, {:atom, :ok}} == @@ -726,12 +894,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :list8, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:list, {:atom, :ok}} == @@ -739,12 +908,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :list9, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:list, {:atom, :ok}} == @@ -752,12 +922,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :list10, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:list, {:tuple, 2, [nil, nil]}} == @@ -765,12 +936,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :list11, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:list, {:tuple, 2, [nil, {:atom, :ok}]}} == @@ -778,12 +950,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :list12, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:list, {:tuple, 2, [{:atom, :some}, {:atom, :ok}]}} == @@ -791,12 +964,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :list13, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -806,11 +980,12 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f03, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -820,11 +995,12 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f05, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -839,11 +1015,12 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f1, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -856,11 +1033,12 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f3, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -873,11 +1051,12 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f5, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -887,11 +1066,12 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f2, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -901,11 +1081,12 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f4, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -915,11 +1096,12 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f6, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -929,11 +1111,12 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f7, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -943,6 +1126,7 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, @@ -950,7 +1134,7 @@ defmodule ElixirSense.Core.BindingTest do :abc, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -960,12 +1144,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f71, [nil]} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -975,11 +1160,12 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f8, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -989,13 +1175,14 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f_no_return, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1005,12 +1192,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f_any, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert nil == @@ -1018,12 +1206,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f_term, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1033,11 +1222,12 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f91, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1047,7 +1237,7 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.merge(%{ variables: [ - %VarInfo{name: :ref, type: {:local_call, :fun, []}} + %VarInfo{version: 1, name: :ref, type: {:local_call, :fun, []}} ], current_module: MyMod, specs: %{ @@ -1067,7 +1257,7 @@ defmodule ElixirSense.Core.BindingTest do } } }), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1077,7 +1267,7 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.merge(%{ variables: [ - %VarInfo{name: :ref, type: {:local_call, :fun, []}} + %VarInfo{version: 1, name: :ref, type: {:local_call, :fun, []}} ], current_module: MyMod, specs: %{ @@ -1102,7 +1292,7 @@ defmodule ElixirSense.Core.BindingTest do } } }), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1112,7 +1302,7 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.merge(%{ variables: [ - %VarInfo{name: :ref, type: {:local_call, :fun, []}} + %VarInfo{version: 1, name: :ref, type: {:local_call, :fun, []}} ], current_module: MyMod, specs: %{ @@ -1138,7 +1328,7 @@ defmodule ElixirSense.Core.BindingTest do } } }), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1148,7 +1338,7 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.merge(%{ variables: [ - %VarInfo{name: :ref, type: {:call, {:atom, MyMod}, :fun, []}} + %VarInfo{version: 1, name: :ref, type: {:call, {:atom, MyMod}, :fun, []}} ], current_module: SomeMod, specs: %{ @@ -1173,7 +1363,7 @@ defmodule ElixirSense.Core.BindingTest do } } }), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1183,7 +1373,7 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.merge(%{ variables: [ - %VarInfo{name: :ref, type: {:call, {:atom, MyMod}, :fun, []}} + %VarInfo{version: 1, name: :ref, type: {:call, {:atom, MyMod}, :fun, []}} ], current_module: SomeMod, specs: %{ @@ -1209,7 +1399,7 @@ defmodule ElixirSense.Core.BindingTest do } } }), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1219,7 +1409,7 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.merge(%{ variables: [ - %VarInfo{name: :ref, type: {:call, {:atom, MyMod}, :fun, []}} + %VarInfo{version: 1, name: :ref, type: {:call, {:atom, MyMod}, :fun, []}} ], current_module: SomeMod, specs: %{ @@ -1244,7 +1434,7 @@ defmodule ElixirSense.Core.BindingTest do } } }), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1281,45 +1471,49 @@ defmodule ElixirSense.Core.BindingTest do Binding.expand( env |> Map.put(:variables, [ - %VarInfo{name: :ref, type: {:local_call, :fun, []}} + %VarInfo{version: 1, name: :ref, type: {:local_call, :fun, []}} ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:struct, [{:__struct__, {:atom, MyMod}}, {:abc, nil}], {:atom, MyMod}, nil} == Binding.expand( env |> Map.put(:variables, [ - %VarInfo{name: :ref, type: {:local_call, :fun, [nil]}} + %VarInfo{version: 1, name: :ref, type: {:local_call, :fun, [nil]}} ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:struct, [{:__struct__, {:atom, MyMod}}, {:abc, nil}], {:atom, MyMod}, nil} == Binding.expand( env |> Map.put(:variables, [ - %VarInfo{name: :ref, type: {:local_call, :fun, [nil, nil]}} + %VarInfo{version: 1, name: :ref, type: {:local_call, :fun, [nil, nil]}} ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:struct, [{:__struct__, {:atom, MyMod}}, {:abc, nil}], {:atom, MyMod}, nil} == Binding.expand( env |> Map.put(:variables, [ - %VarInfo{name: :ref, type: {:local_call, :fun, [nil, nil, nil]}} + %VarInfo{version: 1, name: :ref, type: {:local_call, :fun, [nil, nil, nil]}} ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert :none == Binding.expand( env |> Map.put(:variables, [ - %VarInfo{name: :ref, type: {:local_call, :fun, [nil, nil, nil, nil]}} + %VarInfo{ + version: 1, + name: :ref, + type: {:local_call, :fun, [nil, nil, nil, nil]} + } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1329,11 +1523,12 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f10, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:atom, String} == @@ -1341,12 +1536,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f10, [nil]} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:atom, String} == @@ -1354,13 +1550,14 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f10, [nil, nil]} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:atom, String} == @@ -1368,13 +1565,14 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f10, [nil, nil, nil]} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert :none == @@ -1382,13 +1580,14 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f10, [nil, nil, nil, nil]} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1398,11 +1597,11 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.merge(%{ variables: [ - %VarInfo{name: :ref, type: {:local_call, :f02, []}} + %VarInfo{version: 1, name: :ref, type: {:local_call, :f02, []}} ], functions: [{ElixirSenseExample.FunctionsWithReturnSpec, [{:f02, 0}]}] }), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1412,10 +1611,10 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.merge(%{ variables: [ - %VarInfo{name: :ref, type: {:local_call, :f02, []}} + %VarInfo{version: 1, name: :ref, type: {:local_call, :f02, []}} ] }), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1425,11 +1624,11 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.merge(%{ variables: [ - %VarInfo{name: :ref, type: {:variable, :f02}} + %VarInfo{version: 1, name: :ref, type: {:variable, :f02, 1}} ], functions: [{ElixirSenseExample.FunctionsWithReturnSpec, [{:f02, 0}]}] }), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1440,6 +1639,7 @@ defmodule ElixirSense.Core.BindingTest do |> Map.merge(%{ variables: [ %VarInfo{ + version: 1, name: :ref, type: {:call, @@ -1448,7 +1648,7 @@ defmodule ElixirSense.Core.BindingTest do } ] }), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1459,6 +1659,7 @@ defmodule ElixirSense.Core.BindingTest do |> Map.merge(%{ variables: [ %VarInfo{ + version: 1, name: :ref, type: {:call, @@ -1467,7 +1668,7 @@ defmodule ElixirSense.Core.BindingTest do } ] }), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1478,6 +1679,7 @@ defmodule ElixirSense.Core.BindingTest do |> Map.merge(%{ variables: [ %VarInfo{ + version: 1, name: :ref, type: {:call, @@ -1486,7 +1688,7 @@ defmodule ElixirSense.Core.BindingTest do } ] }), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1516,18 +1718,122 @@ defmodule ElixirSense.Core.BindingTest do end describe "Kernel functions" do + test "++" do + assert {:list, {:integer, 1}} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :a, type: {:integer, 1}}, + %VarInfo{version: 1, name: :b, type: {:integer, 2}} + ]), + {:local_call, :++, [list: {:variable, :a, 1}, list: {:variable, :b, 1}]} + ) + + assert {:list, {:integer, 1}} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :a, type: {:integer, 1}}, + %VarInfo{version: 1, name: :b, type: {:integer, 2}} + ]), + {:call, {:atom, :erlang}, :++, + [list: {:variable, :a, 1}, list: {:variable, :b, 1}]} + ) + end + test "tuple elem" do assert {:atom, :a} == Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :tuple, type: {:tuple, 2, [nil, {:atom, :a}]}}, + %VarInfo{version: 1, name: :tuple, type: {:tuple, 2, [nil, {:atom, :a}]}}, %VarInfo{ + version: 1, name: :ref, - type: {:local_call, :elem, [{:variable, :tuple}, {:integer, 1}]} + type: {:local_call, :elem, [{:variable, :tuple, 1}, {:integer, 1}]} } ]), - {:variable, :ref} + {:variable, :ref, 1} + ) + + assert {:atom, :a} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :tuple, type: {:tuple, 2, [nil, {:atom, :a}]}}, + %VarInfo{ + version: 1, + name: :ref, + type: + {:call, {:atom, :erlang}, :element, + [{:integer, 2}, {:variable, :tuple, 1}]} + } + ]), + {:variable, :ref, 1} + ) + end + + test "tuple put_elem" do + assert {:tuple, 2, [{:atom, :b}, {:atom, :a}]} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :tuple, type: {:tuple, 2, [nil, {:atom, :a}]}}, + %VarInfo{ + version: 1, + name: :ref, + type: + {:local_call, :put_elem, + [{:variable, :tuple, 1}, {:integer, 0}, {:atom, :b}]} + } + ]), + {:variable, :ref, 1} + ) + + assert {:tuple, 2, [{:atom, :b}, {:atom, :a}]} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :tuple, type: {:tuple, 2, [nil, {:atom, :a}]}}, + %VarInfo{ + version: 1, + name: :ref, + type: + {:call, {:atom, :erlang}, :setelement, + [{:integer, 1}, {:variable, :tuple, 1}, {:atom, :b}]} + } + ]), + {:variable, :ref, 1} + ) + end + + test "tuple_size" do + assert {:integer, 2} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :tuple, type: {:tuple, 2, [nil, {:atom, :a}]}}, + %VarInfo{ + version: 1, + name: :ref, + type: {:local_call, :tuple_size, [{:variable, :tuple, 1}]} + } + ]), + {:variable, :ref, 1} + ) + + assert {:integer, 2} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :tuple, type: {:tuple, 2, [nil, {:atom, :a}]}}, + %VarInfo{ + version: 1, + name: :ref, + type: {:call, {:atom, :erlang}, :tuple_size, [{:variable, :tuple, 1}]} + } + ]), + {:variable, :ref, 1} ) end @@ -1536,13 +1842,28 @@ defmodule ElixirSense.Core.BindingTest do Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :list, type: {:list, {:atom, :a}}}, + %VarInfo{version: 1, name: :list, type: {:list, {:atom, :a}}}, + %VarInfo{ + version: 1, + name: :ref, + type: {:local_call, :hd, [{:variable, :list, 1}]} + } + ]), + {:variable, :ref, 1} + ) + + assert {:atom, :a} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :list, type: {:list, {:atom, :a}}}, %VarInfo{ + version: 1, name: :ref, - type: {:local_call, :hd, [{:variable, :list}]} + type: {:call, {:atom, :erlang}, :hd, [{:variable, :list, 1}]} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1551,13 +1872,208 @@ defmodule ElixirSense.Core.BindingTest do Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :list, type: {:list, {:atom, :a}}}, + %VarInfo{version: 1, name: :list, type: {:list, {:atom, :a}}}, %VarInfo{ + version: 1, name: :ref, - type: {:local_call, :tl, [{:variable, :list}]} + type: {:local_call, :tl, [{:variable, :list, 1}]} } ]), - {:variable, :ref} + {:variable, :ref, 1} + ) + + assert {:list, {:atom, :a}} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :list, type: {:list, {:atom, :a}}}, + %VarInfo{ + version: 1, + name: :ref, + type: {:call, {:atom, :erlang}, :tl, [{:variable, :list, 1}]} + } + ]), + {:variable, :ref, 1} + ) + end + end + + describe "Tuple functions" do + test "append" do + assert {:tuple, 3, [nil, {:atom, :a}, {:atom, :b}]} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :tuple, type: {:tuple, 2, [nil, {:atom, :a}]}}, + %VarInfo{ + version: 1, + name: :ref, + type: {:call, {:atom, Tuple}, :append, [{:variable, :tuple, 1}, {:atom, :b}]} + } + ]), + {:variable, :ref, 1} + ) + + assert {:tuple, 3, [nil, {:atom, :a}, {:atom, :b}]} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :tuple, type: {:tuple, 2, [nil, {:atom, :a}]}}, + %VarInfo{ + version: 1, + name: :ref, + type: + {:call, {:atom, :erlang}, :append_element, + [{:variable, :tuple, 1}, {:atom, :b}]} + } + ]), + {:variable, :ref, 1} + ) + end + + test "delete_at" do + assert {:tuple, 1, [{:atom, :a}]} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :tuple, type: {:tuple, 2, [nil, {:atom, :a}]}}, + %VarInfo{ + version: 1, + name: :ref, + type: + {:call, {:atom, Tuple}, :delete_at, + [{:variable, :tuple, 1}, {:integer, 0}]} + } + ]), + {:variable, :ref, 1} + ) + + assert {:tuple, 1, [{:atom, :a}]} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :tuple, type: {:tuple, 2, [nil, {:atom, :a}]}}, + %VarInfo{ + version: 1, + name: :ref, + type: + {:call, {:atom, :erlang}, :delete_element, + [{:integer, 1}, {:variable, :tuple, 1}]} + } + ]), + {:variable, :ref, 1} + ) + end + + test "insert_at" do + assert {:tuple, 3, [{:atom, :b}, nil, {:atom, :a}]} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :tuple, type: {:tuple, 2, [nil, {:atom, :a}]}}, + %VarInfo{ + version: 1, + name: :ref, + type: + {:call, {:atom, Tuple}, :insert_at, + [{:variable, :tuple, 1}, {:integer, 0}, {:atom, :b}]} + } + ]), + {:variable, :ref, 1} + ) + + assert {:tuple, 3, [{:atom, :b}, nil, {:atom, :a}]} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :tuple, type: {:tuple, 2, [nil, {:atom, :a}]}}, + %VarInfo{ + version: 1, + name: :ref, + type: + {:call, {:atom, :erlang}, :insert_element, + [{:integer, 1}, {:variable, :tuple, 1}, {:atom, :b}]} + } + ]), + {:variable, :ref, 1} + ) + end + + test "to_list" do + assert {:list, {:atom, :a}} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :tuple, type: {:tuple, 1, [{:atom, :a}]}}, + %VarInfo{ + version: 1, + name: :ref, + type: {:call, {:atom, Tuple}, :to_list, [{:variable, :tuple, 1}]} + } + ]), + {:variable, :ref, 1} + ) + + assert {:list, :empty} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :tuple, type: {:tuple, 0, []}}, + %VarInfo{ + version: 1, + name: :ref, + type: {:call, {:atom, Tuple}, :to_list, [{:variable, :tuple, 1}]} + } + ]), + {:variable, :ref, 1} + ) + + assert {:list, {:atom, :a}} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :tuple, type: {:tuple, 1, [{:atom, :a}]}}, + %VarInfo{ + version: 1, + name: :ref, + type: {:call, {:atom, :erlang}, :tuple_to_list, [{:variable, :tuple, 1}]} + } + ]), + {:variable, :ref, 1} + ) + end + + test "duplicate" do + assert {:tuple, 2, [{:atom, :a}, {:atom, :a}]} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :tuple, type: {:atom, :a}}, + %VarInfo{ + version: 1, + name: :ref, + type: + {:call, {:atom, Tuple}, :duplicate, + [{:variable, :tuple, 1}, {:integer, 2}]} + } + ]), + {:variable, :ref, 1} + ) + + assert {:tuple, 2, [{:atom, :a}, {:atom, :a}]} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :tuple, type: {:atom, :a}}, + %VarInfo{ + version: 1, + name: :ref, + type: + {:call, {:atom, :erlang}, :make_tuple, + [{:integer, 2}, {:variable, :tuple, 1}]} + } + ]), + {:variable, :ref, 1} ) end end @@ -1570,6 +2086,13 @@ defmodule ElixirSense.Core.BindingTest do {:call, {:atom, Map}, :put, [{:map, [abc: {:atom, :a}], nil}, {:atom, :cde}, {:atom, :b}]} ) + + assert {:map, [cde: {:atom, :b}, abc: {:atom, :a}], nil} = + Binding.expand( + @env, + {:call, {:atom, :maps}, :put, + [{:atom, :cde}, {:atom, :b}, {:map, [abc: {:atom, :a}], nil}]} + ) end test "put not a map" do @@ -1592,6 +2115,13 @@ defmodule ElixirSense.Core.BindingTest do {:call, {:atom, Map}, :delete, [{:map, [abc: {:atom, :a}, cde: nil], nil}, {:atom, :cde}]} ) + + assert {:map, [abc: {:atom, :a}], nil} = + Binding.expand( + @env, + {:call, {:atom, :maps}, :remove, + [{:atom, :cde}, {:map, [abc: {:atom, :a}, cde: nil], nil}]} + ) end test "merge" do @@ -1601,6 +2131,13 @@ defmodule ElixirSense.Core.BindingTest do {:call, {:atom, Map}, :merge, [{:map, [abc: {:atom, :a}], nil}, {:map, [cde: {:atom, :b}], nil}]} ) + + assert {:map, [abc: {:atom, :a}, cde: {:atom, :b}], nil} = + Binding.expand( + @env, + {:call, {:atom, :maps}, :merge, + [{:map, [abc: {:atom, :a}], nil}, {:map, [cde: {:atom, :b}], nil}]} + ) end test "merge/3" do @@ -1655,6 +2192,13 @@ defmodule ElixirSense.Core.BindingTest do {:call, {:atom, Map}, :replace!, [{:map, [abc: {:atom, :a}], nil}, {:atom, :abc}, {:atom, :b}]} ) + + assert {:map, [abc: {:atom, :b}], nil} = + Binding.expand( + @env, + {:call, {:atom, :maps}, :update, + [{:atom, :abc}, {:atom, :b}, {:map, [abc: {:atom, :a}], nil}]} + ) end test "put_new" do @@ -1681,6 +2225,12 @@ defmodule ElixirSense.Core.BindingTest do @env, {:call, {:atom, Map}, :fetch!, [{:map, [abc: {:atom, :a}], nil}, {:atom, :abc}]} ) + + assert {:atom, :a} = + Binding.expand( + @env, + {:call, {:atom, :maps}, :get, [{:atom, :abc}, {:map, [abc: {:atom, :a}], nil}]} + ) end test "fetch" do @@ -1689,6 +2239,12 @@ defmodule ElixirSense.Core.BindingTest do @env, {:call, {:atom, Map}, :fetch, [{:map, [abc: {:atom, :a}], nil}, {:atom, :abc}]} ) + + assert {:tuple, 2, [atom: :ok, atom: :a]} = + Binding.expand( + @env, + {:call, {:atom, :maps}, :find, [{:atom, :abc}, {:map, [abc: {:atom, :a}], nil}]} + ) end test "get" do @@ -1827,8 +2383,11 @@ defmodule ElixirSense.Core.BindingTest do describe "intersection" do test "intersection" do assert {:struct, - [{:__struct__, {:atom, State}}, {:abc, nil}, {:formatted, {:variable, :formatted}}], - {:atom, State}, + [ + {:__struct__, {:atom, State}}, + {:abc, nil}, + {:formatted, {:variable, :formatted, 1}} + ], {:atom, State}, nil} == Binding.expand( @env @@ -1839,35 +2398,49 @@ defmodule ElixirSense.Core.BindingTest do } }, variables: [ - %VarInfo{ - name: :socket, - type: nil - } + %VarInfo{version: 1, name: :socket, type: nil} ] }), {:intersection, [ - {:call, {:call, {:variable, :socket}, :assigns, []}, :state, []}, - {:struct, [formatted: {:variable, :formatted}], {:atom, State}, nil} + {:call, {:call, {:variable, :socket, 1}, :assigns, []}, :state, []}, + {:struct, [formatted: {:variable, :formatted, 1}], {:atom, State}, nil} ]} ) end - test "none" do + test "none intersection" do assert :none == Binding.expand(@env, {:intersection, [{:atom, A}, :none]}) assert :none == Binding.expand(@env, {:intersection, [:none, {:atom, A}]}) assert :none == Binding.expand(@env, {:intersection, [:none, :none]}) end - test "unknown" do + test "none union" do + assert {:atom, A} == Binding.expand(@env, {:union, [{:atom, A}, :none]}) + assert {:atom, A} == Binding.expand(@env, {:union, [:none, {:atom, A}]}) + assert :none == Binding.expand(@env, {:union, [:none, :none]}) + end + + test "unknown intersection" do assert {:atom, A} == Binding.expand(@env, {:intersection, [{:atom, A}, nil]}) assert {:atom, A} == Binding.expand(@env, {:intersection, [nil, {:atom, A}]}) assert nil == Binding.expand(@env, {:intersection, [nil, nil]}) end + test "unknown union" do + assert nil == Binding.expand(@env, {:union, [{:atom, A}, nil]}) + assert nil == Binding.expand(@env, {:union, [nil, {:atom, A}]}) + assert nil == Binding.expand(@env, {:union, [nil, nil]}) + end + test "equal" do assert {:atom, A} == Binding.expand(@env, {:intersection, [{:atom, A}, {:atom, A}]}) assert :none == Binding.expand(@env, {:intersection, [{:atom, A}, {:atom, B}]}) + + assert {:atom, A} == Binding.expand(@env, {:union, [{:atom, A}, {:atom, A}]}) + + assert {:union, [{:atom, A}, {:atom, B}]} == + Binding.expand(@env, {:union, [{:atom, A}, {:atom, B}]}) end test "tuple" do @@ -1888,6 +2461,7 @@ defmodule ElixirSense.Core.BindingTest do assert {:map, [], nil} == Binding.expand(@env, {:intersection, [{:map, [], nil}, {:map, [], nil}]}) + # NOTE intersection is not strict and does an union on map keys assert {:map, [{:a, nil}, {:b, {:tuple, 2, [atom: Z, atom: X]}}, {:c, {:atom, C}}], nil} == Binding.expand( @env, @@ -1914,7 +2488,7 @@ defmodule ElixirSense.Core.BindingTest do [ {:__struct__, {:atom, State}}, {:abc, {:atom, X}}, - {:formatted, {:variable, :formatted}} + {:formatted, {:variable, :formatted, 1}} ], {:atom, State}, nil} == Binding.expand( @@ -1928,7 +2502,7 @@ defmodule ElixirSense.Core.BindingTest do }), {:intersection, [ - {:struct, [formatted: {:variable, :formatted}], {:atom, State}, nil}, + {:struct, [formatted: {:variable, :formatted, 1}], {:atom, State}, nil}, {:map, [not_existing: nil, abc: {:atom, X}], nil} ]} ) @@ -1937,7 +2511,7 @@ defmodule ElixirSense.Core.BindingTest do [ {:__struct__, {:atom, State}}, {:abc, {:atom, X}}, - {:formatted, {:variable, :formatted}} + {:formatted, {:variable, :formatted, 1}} ], {:atom, State}, nil} == Binding.expand( @@ -1952,7 +2526,7 @@ defmodule ElixirSense.Core.BindingTest do {:intersection, [ {:map, [not_existing: nil, abc: {:atom, X}], nil}, - {:struct, [formatted: {:variable, :formatted}], {:atom, State}, nil} + {:struct, [formatted: {:variable, :formatted, 1}], {:atom, State}, nil} ]} ) end @@ -1961,7 +2535,7 @@ defmodule ElixirSense.Core.BindingTest do assert {:struct, [ {:__struct__, nil}, - {:formatted, {:variable, :formatted}}, + {:formatted, {:variable, :formatted, 1}}, {:not_existing, nil}, {:abc, {:atom, X}} ], nil, @@ -1970,7 +2544,7 @@ defmodule ElixirSense.Core.BindingTest do @env, {:intersection, [ - {:struct, [formatted: {:variable, :formatted}], nil, nil}, + {:struct, [formatted: {:variable, :formatted, 1}], nil, nil}, {:map, [not_existing: nil, abc: {:atom, X}], nil} ]} ) @@ -1980,7 +2554,7 @@ defmodule ElixirSense.Core.BindingTest do assert {:struct, [ {:__struct__, nil}, - {:formatted, {:variable, :formatted}}, + {:formatted, {:variable, :formatted, 1}}, {:not_existing, nil}, {:abc, {:atom, X}} ], nil, @@ -1989,7 +2563,7 @@ defmodule ElixirSense.Core.BindingTest do @env, {:intersection, [ - {:struct, [formatted: {:variable, :formatted}], nil, nil}, + {:struct, [formatted: {:variable, :formatted, 1}], nil, nil}, {:struct, [not_existing: nil, abc: {:atom, X}], nil, nil} ]} ) @@ -2000,7 +2574,7 @@ defmodule ElixirSense.Core.BindingTest do [ {:__struct__, {:atom, State}}, {:abc, {:atom, X}}, - {:formatted, {:variable, :formatted}} + {:formatted, {:variable, :formatted, 1}} ], {:atom, State}, nil} == Binding.expand( @@ -2014,7 +2588,7 @@ defmodule ElixirSense.Core.BindingTest do }), {:intersection, [ - {:struct, [formatted: {:variable, :formatted}], {:atom, State}, nil}, + {:struct, [formatted: {:variable, :formatted, 1}], {:atom, State}, nil}, {:struct, [not_existing: nil, abc: {:atom, X}], nil, nil} ]} ) @@ -2023,7 +2597,7 @@ defmodule ElixirSense.Core.BindingTest do [ {:__struct__, {:atom, State}}, {:abc, {:atom, X}}, - {:formatted, {:variable, :formatted}} + {:formatted, {:variable, :formatted, 1}} ], {:atom, State}, nil} == Binding.expand( @@ -2038,7 +2612,7 @@ defmodule ElixirSense.Core.BindingTest do {:intersection, [ {:struct, [not_existing: nil, abc: {:atom, X}], nil, nil}, - {:struct, [formatted: {:variable, :formatted}], {:atom, State}, nil} + {:struct, [formatted: {:variable, :formatted, 1}], {:atom, State}, nil} ]} ) end @@ -2048,7 +2622,7 @@ defmodule ElixirSense.Core.BindingTest do [ {:__struct__, {:atom, State}}, {:abc, {:atom, X}}, - {:formatted, {:variable, :formatted}} + {:formatted, {:variable, :formatted, 1}} ], {:atom, State}, nil} == Binding.expand( @@ -2062,7 +2636,7 @@ defmodule ElixirSense.Core.BindingTest do }), {:intersection, [ - {:struct, [formatted: {:variable, :formatted}], {:atom, State}, nil}, + {:struct, [formatted: {:variable, :formatted, 1}], {:atom, State}, nil}, {:struct, [not_existing: nil, abc: {:atom, X}], {:atom, State}, nil} ]} ) @@ -2083,7 +2657,7 @@ defmodule ElixirSense.Core.BindingTest do {:intersection, [ {:struct, [not_existing: nil, abc: {:atom, X}], {:atom, Other}, nil}, - {:struct, [formatted: {:variable, :formatted}], {:atom, State}, nil} + {:struct, [formatted: {:variable, :formatted, 1}], {:atom, State}, nil} ]} ) end diff --git a/test/elixir_sense/core/compiler/typespec_test.exs b/test/elixir_sense/core/compiler/typespec_test.exs new file mode 100644 index 00000000..976ad462 --- /dev/null +++ b/test/elixir_sense/core/compiler/typespec_test.exs @@ -0,0 +1,399 @@ +defmodule ElixirSense.Core.Compiler.TypespecTest do + use ExUnit.Case, async: true + alias ElixirSense.Core.Compiler.Typespec + alias ElixirSense.Core.Compiler + alias ElixirSense.Core.Compiler.State + alias ElixirSense.Core.Normalized.Macro.Env, as: NormalizedMacroEnv + + defp default_state, + do: %State{ + prematch: + if Version.match?(System.version(), ">= 1.15.0-dev") do + Code.get_compiler_option(:on_undefined_variable) + else + :warn + end + } + + defp expand_typespec(ast, vars \\ [], state \\ default_state(), env \\ Compiler.env()) do + Typespec.expand_typespec(ast, vars, state, env) + end + + describe "expand_typespec" do + test "literal" do + assert {:foo, _state} = expand_typespec(:foo) + assert {1, _state} = expand_typespec(1) + assert {[], _state} = expand_typespec([]) + assert {{:nonempty_list, [], [{:any, [], []}]}, _state} = expand_typespec([{:..., [], []}]) + assert {{:nonempty_list, [], [:foo]}, _state} = expand_typespec([:foo, {:..., [], []}]) + assert {{:list, [], [:foo]}, _state} = expand_typespec([:foo]) + + assert {{:list, [], [{:|, [], [foo: 1, bar: :baz]}]}, _state} = + expand_typespec(foo: 1, bar: :baz) + + # invalid + assert {{:list, [], [{:|, [], [foo: 1, bar: :baz]}]}, _state} = + expand_typespec([0, {:foo, 1}, {:bar, :baz}]) + end + + test "local type" do + assert {{:local_type, [], []}, _state} = expand_typespec({:local_type, [], []}) + + assert {{:local_type, [], [:foo, 1]}, _state} = + expand_typespec({:local_type, [], [:foo, 1]}) + end + + test "local type no parens" do + assert {{:foo, [], []}, _state} = expand_typespec({:foo, [], nil}) + end + + test "var" do + assert {{:foo, [], nil}, _state} = expand_typespec({:foo, [], nil}, [:foo]) + end + + test "named ..." do + assert {{:..., [], []}, _state} = expand_typespec({:..., [], []}) + end + + test "fun" do + assert {{:fun, [], [:foo, 1]}, _state} = expand_typespec({:fun, [], [:foo, 1]}) + assert {{:fun, [], []}, _state} = expand_typespec({:fun, [], []}) + + assert {[{:->, [], [[:foo], 1]}], _state} = expand_typespec([{:->, [], [[:foo], 1]}]) + + assert {[{:->, [], [[:foo, :bar], 1]}], _state} = + expand_typespec([{:->, [], [[:foo, :bar], 1]}]) + + assert {[{:->, [], [[{:..., [], []}], 1]}], _state} = + expand_typespec([{:->, [], [[{:..., [], []}], 1]}]) + + assert {[{:->, [], [[{:..., [], []}], {:any, [], []}]}], _state} = + expand_typespec([{:->, [], [[{:..., [], []}], {:any, [], []}]}]) + end + + test "charlist" do + assert {{:charlist, [], []}, _state} = + expand_typespec({:charlist, [], []}) + + assert {{:char_list, [], []}, _state} = + expand_typespec({:char_list, [], []}) + + assert {{:nonempty_charlist, [], []}, _state} = + expand_typespec({:nonempty_charlist, [], []}) + end + + test "struct" do + assert {{:struct, [], []}, _state} = expand_typespec({:struct, [], []}) + end + + test "as_boolean" do + assert {{:as_boolean, [], [:foo]}, _state} = + expand_typespec({:as_boolean, [], [:foo]}) + end + + test "keyword" do + assert {{:keyword, [], []}, _state} = + expand_typespec({:keyword, [], []}) + + assert {{:keyword, [], [:foo]}, _state} = + expand_typespec({:keyword, [], [:foo]}) + end + + test "string" do + assert {{:string, [], []}, _state} = expand_typespec({:string, [], []}) + assert {{:nonempty_string, [], []}, _state} = expand_typespec({:nonempty_string, [], []}) + end + + test "__block__" do + assert {:foo, _state} = expand_typespec({:__block__, [], [:foo]}) + end + + test "tuple" do + assert {{:{}, [], [:foo]}, _state} = expand_typespec({:{}, [], [:foo]}) + assert {{:foo, :bar}, _state} = expand_typespec({:foo, :bar}) + assert {{:tuple, [], []}, _state} = expand_typespec({:tuple, [], []}) + end + + test "remote" do + assert {{{:., [], [:some, :remote]}, [], [:foo]}, _state} = + expand_typespec({{:., [], [:some, :remote]}, [], [:foo]}) + + assert {{{:., [], [Foo.Bar, :remote]}, [], [:foo]}, _state} = + expand_typespec( + {{:., [], [{:__aliases__, [], [:Foo, :Bar]}, :remote]}, [], [:foo]} + ) + + env = %{Compiler.env() | module: Foo.Bar} + + assert {{:remote, [], [:foo]}, _state} = + expand_typespec( + {{:., [], [{:__aliases__, [], [:Foo, :Bar]}, :remote]}, [], [:foo]}, + [], + default_state(), + env + ) + + env = %{Compiler.env() | aliases: [{Foo, Foo.Bar}]} + + assert {{{:., [], [Foo.Bar, :remote]}, [], [:foo]}, _state} = + expand_typespec( + {{:., [], [{:__aliases__, [], [:Foo]}, :remote]}, [], [:foo]}, + [], + default_state(), + env + ) + + env = %{Compiler.env() | module: Foo.Bar} + + assert {{:remote, [], [:foo]}, _state} = + expand_typespec( + {{:., [], [{:__MODULE__, [], nil}, :remote]}, [], [:foo]}, + [], + default_state(), + env + ) + + env = %{Compiler.env() | module: Foo.Bar} + state = %{default_state() | attribute_store: %{{Foo.Bar, :baz} => :some}} + + assert {{{:., [], [:some, :remote]}, [], [:foo]}, _state} = + expand_typespec( + {{:., [], [{:@, [], [{:baz, [], nil}]}, :remote]}, [], [:foo]}, + [], + state, + env + ) + + assert {{{:., [], [nil, :remote]}, [], [:foo]}, _state} = + expand_typespec( + {{:., [], [{:@, [], [{:baz, [], nil}]}, :remote]}, [], [:foo]}, + [], + default_state(), + env + ) + + # invalid + assert {{{:., [], [1, :remote]}, [], [:foo]}, _state} = + expand_typespec({{:., [], [1, :remote]}, [], [:foo]}) + end + + test "unary op" do + assert {{:+, [], [1]}, _state} = expand_typespec({:+, [], [1]}) + assert {{:-, [], [1]}, _state} = expand_typespec({:-, [], [1]}) + end + + test "special forms" do + env = %{Compiler.env() | module: Foo.Bar} + assert {Foo.Bar, _state} = expand_typespec({:__MODULE__, [], nil}, [], default_state(), env) + + env = %{Compiler.env() | aliases: [{Foo, Foo.Bar}]} + + assert {Foo.Bar, _state} = + expand_typespec({:__aliases__, [], [:Foo]}, [], default_state(), env) + end + + test "annotated type" do + assert {{:"::", [], [{:some, [], nil}, {:any, [], []}]}, _state} = + expand_typespec({:"::", [], [{:some, [], nil}, {:any, [], nil}]}) + + # invalid + assert {{:"::", [], [1, {:any, [], []}]}, _state} = + expand_typespec({:"::", [], [1, {:any, [], nil}]}) + + # invalid nested + assert {{ + :"::", + [], + [{:some, [], nil}, {:"::", [], [{:other, [], nil}, {:any, [], []}]}] + }, + _state} = + expand_typespec( + {:"::", [], + [{:some, [], nil}, {:"::", [], [{:other, [], nil}, {:any, [], nil}]}]} + ) + end + + test "range" do + assert {{:.., [], [1, 10]}, _state} = expand_typespec({:.., [], [1, 10]}) + end + end + + test "union" do + assert {{:|, [], [{:some, [], []}, {:any, [], []}]}, _state} = + expand_typespec({:|, [], [{:some, [], nil}, {:any, [], nil}]}) + + assert {{ + :|, + [], + [{:some, [], []}, {:|, [], [{:other, [], []}, {:any, [], []}]}] + }, + _state} = + expand_typespec( + {:|, [], [{:some, [], nil}, {:|, [], [{:other, [], nil}, {:any, [], nil}]}]} + ) + end + + test "map" do + assert {{:map, [], []}, _state} = expand_typespec({:map, [], []}) + assert {{:map, [], []}, _state} = expand_typespec({:map, [], nil}) + assert {{:%{}, [], []}, _state} = expand_typespec({:%{}, [], []}) + + assert {{:%{}, [], [foo: :bar]}, _state} = expand_typespec({:%{}, [], [foo: :bar]}) + + assert {{:%{}, [], [{{:optional, [], [:foo]}, :bar}]}, _state} = + expand_typespec({:%{}, [], [{{:optional, [], [:foo]}, :bar}]}) + + assert {{:%{}, [], [foo: :bar]}, _state} = + expand_typespec({:%{}, [], [{{:required, [], [:foo]}, :bar}]}) + + # illegal update + assert {{:%{}, [], []}, _state} = + expand_typespec({:%{}, [], [{:|, [], [{:s, [], nil}, [asd: 324]]}]}) + end + + test "struct" do + assert {{ + :%, + [], + [ + Date, + {:%{}, [], + [ + calendar: {:term, [], []}, + day: {:term, [], []}, + month: {:term, [], []}, + year: {:term, [], []} + ]} + ] + }, _state} = expand_typespec({:%, [], [{:__aliases__, [], [:Date]}, {:%{}, [], []}]}) + + assert {{ + :%, + [], + [ + Date, + {:%{}, [], + [ + calendar: {:term, [], []}, + day: :foo, + month: {:term, [], []}, + year: {:term, [], []} + ]} + ] + }, + _state} = + expand_typespec({:%, [], [{:__aliases__, [], [:Date]}, {:%{}, [], [day: :foo]}]}) + + # non atom key + assert {{ + :%, + [], + [ + Date, + {:%{}, [], + [ + calendar: {:term, [], []}, + day: {:term, [], []}, + month: {:term, [], []}, + year: {:term, [], []} + ]} + ] + }, + _state} = + expand_typespec({:%, [], [{:__aliases__, [], [:Date]}, {:%{}, [], [{"day", :foo}]}]}) + + # invalid key + assert {{ + :%, + [], + [ + Date, + {:%{}, [], + [ + calendar: {:term, [], []}, + day: {:term, [], []}, + month: {:term, [], []}, + year: {:term, [], []} + ]} + ] + }, + _state} = + expand_typespec({:%, [], [{:__aliases__, [], [:Date]}, {:%{}, [], [{:baz, :foo}]}]}) + + # non atom + assert {{:%, [], [1, {:%{}, [], []}]}, _} = expand_typespec({:%, [], [1, {:%{}, [], []}]}) + + # unknown + assert {{:%, [], [UnknownStruct, {:%{}, [], []}]}, _} = + expand_typespec({:%, [], [{:__aliases__, [], [:UnknownStruct]}, {:%{}, [], []}]}) + end + + test "binaries" do + type = {:<<>>, [], []} + assert {^type, _state} = expand_typespec(type) + type = {:<<>>, [], [{:"::", [], [{:_, [], nil}, {:*, [], [{:_, [], nil}, 8]}]}]} + assert {^type, _state} = expand_typespec(type) + type = {:<<>>, [], [{:"::", [], [{:_, [], nil}, 8]}]} + assert {^type, _state} = expand_typespec(type) + + type = { + :<<>>, + [], + [ + {:"::", [], [{:_, [], nil}, 32]}, + {:"::", [], [{:_, [], nil}, {:*, [], [{:_, [], nil}, 8]}]} + ] + } + + assert {^type, _state} = expand_typespec(type) + end + + test "records" do + {:ok, env} = + Compiler.env() + |> NormalizedMacroEnv.define_import([], ElixirSenseExample.ModuleWithRecord, trace: false) + + assert {{ + :{}, + [], + [ + :user, + {:"::", [], [{:name, [], nil}, {:term, [], []}]}, + {:"::", [], [{:age, [], nil}, {:term, [], []}]} + ] + }, _state} = expand_typespec({:record, [], [:user]}, [], default_state(), env) + + assert {{ + :{}, + [], + [ + :user, + {:"::", [], [{:name, [], nil}, {:term, [], []}]}, + {:"::", [], [{:age, [], nil}, :foo]} + ] + }, + _state} = + expand_typespec({:record, [], [:user, [age: :foo]]}, [], default_state(), env) + + # invalid record + assert {{:record, [], [1, []]}, _} = expand_typespec({:record, [], [1]}) + + # invalid field + assert {{ + :{}, + [], + [ + :user, + {:"::", [], [{:name, [], nil}, {:term, [], []}]}, + {:"::", [], [{:age, [], nil}, {:term, [], []}]} + ] + }, + _state} = + expand_typespec({:record, [], [:user, [invalid: :foo]]}, [], default_state(), env) + + # unknown record + assert {{:record, [], [:foo, []]}, _} = expand_typespec({:record, [], [:foo]}) + + # TODO make it work with metadata records + end +end diff --git a/test/elixir_sense/core/compiler_test.exs b/test/elixir_sense/core/compiler_test.exs new file mode 100644 index 00000000..c66a4e0b --- /dev/null +++ b/test/elixir_sense/core/compiler_test.exs @@ -0,0 +1,984 @@ +defmodule ElixirSense.Core.CompilerTest do + use ExUnit.Case, async: true + alias ElixirSense.Core.Compiler + alias ElixirSense.Core.Compiler.State + require Record + + defp to_quoted!(ast, true), do: ast + + defp to_quoted!(string, false), + do: Code.string_to_quoted!(string, columns: true, token_metadata: true) + + if Version.match?(System.version(), ">= 1.17.0-dev") do + Record.defrecordp(:elixir_ex, + caller: false, + prematch: :raise, + stacktrace: false, + unused: {%{}, 0}, + runtime_modules: [], + vars: {%{}, false} + ) + + defp elixir_ex_to_map( + elixir_ex( + caller: caller, + prematch: prematch, + stacktrace: stacktrace, + unused: {_, unused}, + runtime_modules: runtime_modules, + vars: vars + ) + ) do + %{ + caller: caller, + prematch: prematch, + stacktrace: stacktrace, + unused: unused, + runtime_modules: runtime_modules, + vars: vars + } + end + else + Record.defrecordp(:elixir_ex, + caller: false, + prematch: %State{ + prematch: + if Version.match?(System.version(), ">= 1.15.0-dev") do + Code.get_compiler_option(:on_undefined_variable) + else + :warn + end + }, + stacktrace: false, + unused: {%{}, 0}, + vars: {%{}, false} + ) + + defp elixir_ex_to_map( + elixir_ex( + caller: caller, + prematch: prematch, + stacktrace: stacktrace, + unused: {_, unused}, + vars: vars + ) + ) do + %{ + caller: caller, + prematch: prematch, + stacktrace: stacktrace, + unused: unused, + runtime_modules: [], + vars: vars + } + end + end + + defp state_to_map(%State{} = state) do + Map.take(state, [:caller, :prematch, :stacktrace, :unused, :runtime_modules, :vars]) + end + + defp expand(ast) do + Compiler.expand( + ast, + state_with_prematch(), + Compiler.env() + ) + end + + defp elixir_expand(ast) do + env = :elixir_env.new() + :elixir_expand.expand(ast, :elixir_env.env_to_ex(env), env) + end + + defmacrop assert_expansion(code, ast \\ false) do + quote do + ast = to_quoted!(unquote(code), unquote(ast)) + {elixir_expanded, elixir_state, elixir_env} = elixir_expand(ast) + # dbg(elixir_expanded) + {expanded, state, env} = expand(ast) + # dbg(expanded) + + assert clean_capture_arg(expanded) == clean_capture_arg_elixir(elixir_expanded) + assert env == elixir_env + assert state_to_map(state) == elixir_ex_to_map(elixir_state) + end + end + + defmacrop assert_expansion_env(code, ast \\ false) do + quote do + ast = to_quoted!(unquote(code), unquote(ast)) + {elixir_expanded, elixir_state, elixir_env} = elixir_expand(ast) + # dbg(elixir_expanded) + # dbg(elixir_ex_to_map(elixir_state)) + {expanded, state, env} = expand(ast) + # dbg(expanded) + # dbg(state_to_map(state)) + + assert env == elixir_env + assert state_to_map(state) == elixir_ex_to_map(elixir_state) + end + end + + setup do + # Application.put_env(:elixir_sense, :compiler_rewrite, true) + on_exit(fn -> + Application.put_env(:elixir_sense, :compiler_rewrite, false) + end) + + {:ok, %{}} + end + + defp state_with_prematch do + %State{ + prematch: + if Version.match?(System.version(), ">= 1.15.0-dev") do + Code.get_compiler_option(:on_undefined_variable) + else + :warn + end + } + end + + test "initial" do + elixir_env = :elixir_env.new() + assert Compiler.env() == elixir_env + + assert state_to_map(state_with_prematch()) == + elixir_ex_to_map(:elixir_env.env_to_ex(elixir_env)) + end + + describe "special forms" do + test "expands =" do + assert_expansion("1 = 1") + end + + test "expands {}" do + assert_expansion("{}") + assert_expansion("{1, 2, 3}") + assert_expansion("{a, b} = {:ok, 1}") + end + + test "expands %{}" do + assert_expansion("%{1 => 2}") + assert_expansion("%{a: 3}") + assert_expansion("%{a: a} = %{}") + assert_expansion("%{1 => a} = %{}") + assert_expansion("%{%{a: 1} | a: 2}") + assert_expansion("%{%{\"a\" => 1} | \"a\" => 2}") + end + + test "expands %" do + assert_expansion("%Date{year: 2024, month: 2, day: 18}") + assert_expansion("%Date{calendar: Calendar.ISO, year: 2024, month: 2, day: 18}") + assert_expansion("%{year: x} = %Date{year: 2024, month: 2, day: 18}") + assert_expansion("%Date{year: x} = %Date{year: 2024, month: 2, day: 18}") + assert_expansion("%Date{%Date{year: 2024, month: 2, day: 18} | day: 1}") + assert_expansion("%x{} = %Date{year: 2024, month: 2, day: 18}") + end + + if Version.match?(System.version(), ">= 1.15.0") do + test "expands <<>>" do + assert_expansion("<<>>") + assert_expansion("<<1>>") + assert_expansion("<> = \"\"") + end + + test "expands <<>> with modifier" do + assert_expansion("x = 1; y = 1; <>") + assert_expansion("x = 1; y = 1; <> = <<>>") + end + end + + test "expands __block__" do + assert_expansion({:__block__, [], []}, true) + assert_expansion({:__block__, [], [1]}, true) + assert_expansion({:__block__, [], [1, 2]}, true) + end + + test "expands __aliases__" do + assert_expansion({:__aliases__, [], [:Asd, :Foo]}, true) + assert_expansion({:__block__, [], [:Asd]}, true) + assert_expansion({:__block__, [], [Elixir, :Asd]}, true) + end + + test "expands alias" do + assert_expansion("alias Foo") + assert_expansion("alias Foo.Bar") + assert_expansion("alias Foo.Bar, as: Baz") + end + + test "expands require" do + assert_expansion("require Code") + assert_expansion("require Code.Fragment") + assert_expansion("require Code.Fragment, as: Baz") + end + + test "expands import" do + assert_expansion("import Code") + assert_expansion("import Code.Fragment") + assert_expansion("import Code.Fragment, only: :functions") + end + + test "expands multi alias" do + assert_expansion("alias Foo.{Bar, Some.Other}") + end + + test "expands __MODULE__" do + ast = {:__MODULE__, [], nil} + + {expanded, state, env} = + Compiler.expand(ast, state_with_prematch(), %{Compiler.env() | module: Foo}) + + elixir_env = %{:elixir_env.new() | module: Foo} + + {elixir_expanded, elixir_state, elixir_env} = + :elixir_expand.expand(ast, :elixir_env.env_to_ex(elixir_env), elixir_env) + + assert expanded == elixir_expanded + assert env == elixir_env + assert state_to_map(state) == elixir_ex_to_map(elixir_state) + end + + test "expands __DIR__" do + ast = {:__DIR__, [], nil} + + {expanded, state, env} = + Compiler.expand(ast, state_with_prematch(), %{Compiler.env() | file: __ENV__.file}) + + elixir_env = %{:elixir_env.new() | file: __ENV__.file} + + {elixir_expanded, elixir_state, elixir_env} = + :elixir_expand.expand(ast, :elixir_env.env_to_ex(elixir_env), elixir_env) + + assert expanded == elixir_expanded + assert env == elixir_env + assert state_to_map(state) == elixir_ex_to_map(elixir_state) + end + + test "expands __CALLER__" do + ast = {:__CALLER__, [], nil} + + {expanded, state, env} = + Compiler.expand(ast, %State{state_with_prematch() | caller: true}, Compiler.env()) + + elixir_env = :elixir_env.new() + + {elixir_expanded, elixir_state, elixir_env} = + :elixir_expand.expand( + ast, + elixir_ex(:elixir_env.env_to_ex(elixir_env), caller: true), + elixir_env + ) + + assert expanded == elixir_expanded + assert env == elixir_env + assert state_to_map(state) == elixir_ex_to_map(elixir_state) + end + + test "expands __STACKTRACE__" do + ast = {:__STACKTRACE__, [], nil} + + {expanded, state, env} = + Compiler.expand(ast, %State{state_with_prematch() | stacktrace: true}, Compiler.env()) + + elixir_env = :elixir_env.new() + + {elixir_expanded, elixir_state, elixir_env} = + :elixir_expand.expand( + ast, + elixir_ex(:elixir_env.env_to_ex(elixir_env), stacktrace: true), + elixir_env + ) + + assert expanded == elixir_expanded + assert env == elixir_env + assert state_to_map(state) == elixir_ex_to_map(elixir_state) + end + + test "expands __ENV__" do + ast = {:__ENV__, [], nil} + {expanded, state, env} = Compiler.expand(ast, state_with_prematch(), Compiler.env()) + elixir_env = :elixir_env.new() + + {elixir_expanded, elixir_state, elixir_env} = + :elixir_expand.expand(ast, :elixir_env.env_to_ex(elixir_env), elixir_env) + + assert {:%{}, [], expanded_fields} = expanded + assert {:%{}, [], elixir_fields} = elixir_expanded + + assert Enum.sort(expanded_fields) == Enum.sort(elixir_fields) + assert env == elixir_env + assert state_to_map(state) == elixir_ex_to_map(elixir_state) + end + + test "expands __ENV__.property" do + assert_expansion("__ENV__.requires") + + if Version.match?(System.version(), ">= 1.15.0") do + # elixir 1.14 returns fields in different order + # we don't test that as the code is invalid anyway + assert_expansion("__ENV__.foo") + end + end + + if Version.match?(System.version(), ">= 1.16.0") do + test "expands quote literal" do + assert_expansion("quote do: 2") + assert_expansion("quote do: :foo") + assert_expansion("quote do: \"asd\"") + assert_expansion("quote do: []") + assert_expansion("quote do: [12]") + assert_expansion("quote do: [12, 34]") + assert_expansion("quote do: [12 | 34]") + assert_expansion("quote do: [12 | [34]]") + assert_expansion("quote do: {12}") + assert_expansion("quote do: {12, 34}") + assert_expansion("quote do: %{a: 12}") + end + end + + if Version.match?(System.version(), ">= 1.16.0") do + test "expands quote variable" do + assert_expansion("quote do: abc") + end + end + + if Version.match?(System.version(), ">= 1.16.0") do + test "expands quote quote" do + assert_expansion(""" + quote do: (quote do: 1) + """) + end + end + + test "expands quote block" do + assert_expansion(""" + quote do: () + """) + end + + test "expands quote unquote" do + assert_expansion(""" + a = 1 + quote do: unquote(a) + """) + end + + test "expands quote unquote block" do + assert_expansion(""" + a = 1 + quote do: (unquote(a)) + """) + end + + test "expands quote unquote_splicing tuple" do + assert_expansion(""" + quote do: {unquote_splicing([1, 2]), unquote_splicing([2])} + """) + end + + if Version.match?(System.version(), ">= 1.16.0") do + test "expands quote unquote_splicing" do + assert_expansion(""" + a = [1, 2, 3] + quote do: (unquote_splicing(a)) + """) + end + end + + if Version.match?(System.version(), ">= 1.16.0") do + test "expands quote unquote_splicing in list" do + assert_expansion(""" + a = [1, 2, 3] + quote do: [unquote_splicing(a) | [1]] + """) + + assert_expansion(""" + a = [1, 2, 3] + quote do: [1 | unquote_splicing(a)] + """) + end + end + + if Version.match?(System.version(), ">= 1.16.0") do + test "expands quote alias" do + assert_expansion("quote do: Date") + assert_expansion("quote do: Elixir.Date") + assert_expansion("quote do: String.Chars") + assert_expansion("alias String.Chars; quote do: Chars") + assert_expansion("alias String.Chars; quote do: Chars.foo().A") + end + end + + if Version.match?(System.version(), ">= 1.16.0") do + test "expands quote import" do + assert_expansion("quote do: inspect(1)") + assert_expansion("quote do: &inspect/1") + end + end + + if Version.match?(System.version(), ">= 1.17.0") do + test "expands quote with bind_quoted" do + assert_expansion(""" + kv = [a: 1] + quote bind_quoted: [kv: kv] do + Enum.each(kv, fn {k, v} -> + def unquote(k)(), do: unquote(v) + end) + end + """) + end + end + + if Version.match?(System.version(), ">= 1.16.0") do + test "expands quote with unquote false" do + assert_expansion(""" + quote unquote: false do + unquote("hello") + end + """) + end + end + + if Version.match?(System.version(), ">= 1.17.0") do + test "expands quote with file" do + assert_expansion(""" + quote file: "some.ex", do: bar(1, 2, 3) + """) + end + end + + if Version.match?(System.version(), ">= 1.16.0") do + test "expands quote with line" do + assert_expansion(""" + quote line: 123, do: bar(1, 2, 3) + """) + end + end + + if Version.match?(System.version(), ">= 1.16.0") do + test "expands quote with location: keep" do + assert_expansion(""" + quote location: :keep, do: bar(1, 2, 3) + """) + end + end + + if Version.match?(System.version(), ">= 1.16.0") do + test "expands quote with context" do + assert_expansion(""" + quote context: Foo, do: abc = 3 + """) + end + end + + test "expands &super" do + assert_expansion_env(""" + defmodule Abc do + use ElixirSense.Core.CompilerTest.Overridable + + def foo(a) do + &super/1 + end + end + """) + + assert_expansion_env(""" + defmodule Abc do + use ElixirSense.Core.CompilerTest.Overridable + + def foo(a) do + &super(&1) + end + end + """) + end + + if Version.match?(System.version(), ">= 1.17.0") do + test "expands &" do + assert_expansion("& &1") + assert_expansion("&Enum.take(&1, 5)") + assert_expansion("&{&1, &2}") + assert_expansion("&[&1 | &2]") + assert_expansion("&inspect/1") + assert_expansion("&Enum.count/1") + assert_expansion("a = %{}; &a.b(&1)") + assert_expansion("&Enum.count(&1)") + assert_expansion("&inspect(&1)") + assert_expansion("&Enum.map(&2, &1)") + assert_expansion("&inspect([&2, &1])") + end + end + + test "expands fn" do + assert_expansion("fn -> 1 end") + assert_expansion("fn a, b -> {a, b} end") + + assert_expansion(""" + fn + 1 -> 1 + a -> a + end + """) + end + + test "expands cond" do + assert_expansion(""" + cond do + nil -> 0 + true -> 1 + end + """) + end + + test "expands case" do + assert_expansion(""" + case 1 do + 0 -> 0 + 1 -> 1 + end + """) + end + + test "expands try" do + assert_expansion(""" + try do + inspect(1) + rescue + e in ArgumentError -> + e + catch + {k, e} -> + {k, e} + else + _ -> :ok + after + IO.puts("") + end + """) + end + + test "expands receive" do + assert_expansion(""" + receive do + x -> x + after + 100 -> IO.puts("") + end + """) + end + + if Version.match?(System.version(), ">= 1.14.0") do + test "expands for" do + assert_expansion(""" + for i <- [1, 2, 3] do + i + end + """) + + assert_expansion(""" + for i <- [1, 2, 3], j <- [1, 2], true, into: %{}, do: {i, j} + """) + end + end + + if Version.match?(System.version(), ">= 1.15.0") do + test "expands for with bitstring generator" do + assert_expansion(""" + for <> do + :ok + end + """) + end + + test "expands for with reduce" do + assert_expansion(""" + for <>, x in ?a..?z, reduce: %{} do + acc -> acc + end + """) + end + end + + if Version.match?(System.version(), ">= 1.14.0") do + test "expands for in block" do + assert_expansion(""" + for i <- [1, 2, 3] do + i + end + :ok + """) + + assert_expansion(""" + for i <- [1, 2, 3], uniq: true do + i + end + :ok + """) + + assert_expansion(""" + _ = for i <- [1, 2, 3] do + i + end + :ok + """) + end + end + + test "expands with" do + assert_expansion(""" + with i <- :ok do + i + end + """) + + assert_expansion(""" + with :ok <- :ok, j = 5 do + j + else + a -> a + end + """) + end + + defmodule Overridable do + defmacro __using__(_args) do + quote do + def foo(a) do + a + end + + defmacro bar(ast) do + ast + end + + defoverridable foo: 1, bar: 1 + end + end + end + + test "expands super" do + assert_expansion_env(""" + defmodule Abc do + use ElixirSense.Core.CompilerTest.Overridable + + def foo(a) do + super(a + 1) + end + + defmacro bar(a) do + quote do + unquote(super(b)) - 1 + end + end + end + """) + end + + test "expands underscored var write" do + assert_expansion("_ = 5") + end + + test "expands var write" do + assert_expansion("a = 5") + end + + test "expands var read" do + assert_expansion("a = 5; a") + end + + test "expands var overwrite" do + assert_expansion("a = 5; a = 6") + end + + test "expands var overwrite already overwritten" do + assert_expansion("[a, a] = [5, 5]") + end + + test "expands var pin" do + assert_expansion("a = 5; ^a = 6") + end + + test "expands nullary call if_undefined: :apply" do + ast = {:self, [if_undefined: :apply], nil} + {expanded, state, env} = Compiler.expand(ast, state_with_prematch(), Compiler.env()) + elixir_env = :elixir_env.new() + + {elixir_expanded, elixir_state, elixir_env} = + :elixir_expand.expand(ast, :elixir_env.env_to_ex(elixir_env), elixir_env) + + assert expanded == elixir_expanded + assert env == elixir_env + assert state_to_map(state) == elixir_ex_to_map(elixir_state) + end + + if Version.match?(System.version(), ">= 1.15.0") do + test "expands nullary call if_undefined: :warn" do + Code.put_compiler_option(:on_undefined_variable, :warn) + ast = {:self, [], nil} + + {expanded, state, env} = + Compiler.expand( + ast, + %State{ + prematch: Code.get_compiler_option(:on_undefined_variable) || :warn + }, + Compiler.env() + ) + + elixir_env = :elixir_env.new() + + {elixir_expanded, elixir_state, elixir_env} = + :elixir_expand.expand(ast, :elixir_env.env_to_ex(elixir_env), elixir_env) + + assert expanded == elixir_expanded + assert env == elixir_env + assert state_to_map(state) == elixir_ex_to_map(elixir_state) + after + Code.put_compiler_option(:on_undefined_variable, :raise) + end + end + + test "expands local call" do + assert_expansion("get_in(%{}, [:bar])") + assert_expansion("length([])") + end + + test "expands local operator call" do + assert_expansion("a = b = []; a ++ b") + end + + test "expands local call macro" do + assert_expansion("if true, do: :ok") + assert_expansion("1 |> IO.inspect") + end + + test "expands remote call" do + assert_expansion("Kernel.get_in(%{}, [:bar])") + assert_expansion("Kernel.length([])") + assert_expansion("Some.fun().other()") + end + + test "expands remote call macro" do + assert_expansion("Kernel.|>(1, IO.inspect)") + end + + test "expands anonymous call" do + assert_expansion("foo = fn a -> a end; foo.(1)") + end + + test "expands 2-tuple" do + assert_expansion("{1, 2}") + assert_expansion("{a, b} = {1, 2}") + end + + test "expands list" do + assert_expansion("[]") + assert_expansion("[1, 2]") + assert_expansion("[1 | [2]]") + assert_expansion("[a | b] = [1, 2, 3]") + assert_expansion("[a] ++ [b] = [1, 2]") + end + + test "expands function" do + assert_expansion(&inspect/1, true) + end + + test "expands pid" do + assert_expansion(self(), true) + end + + test "expands number" do + assert_expansion(1, true) + assert_expansion(1.5, true) + end + + test "expands atom" do + assert_expansion(true, true) + assert_expansion(:foo, true) + assert_expansion(Kernel, true) + end + + test "expands binary" do + assert_expansion("abc", true) + end + end + + describe "Kernel macros" do + test "@" do + assert_expansion_env(""" + defmodule Abc do + @foo 1 + @foo + end + """) + end + + test "defmodule" do + assert_expansion_env("defmodule Abc, do: :ok") + + assert_expansion_env(""" + defmodule Abc do + foo = 1 + end + """) + + assert_expansion_env(""" + defmodule Abc.Some do + foo = 1 + end + """) + + assert_expansion_env(""" + defmodule Elixir.Abc.Some do + foo = 1 + end + """) + + assert_expansion_env(""" + defmodule Abc.Some do + defmodule Child do + foo = 1 + end + end + """) + + assert_expansion_env(""" + defmodule Abc.Some do + defmodule Elixir.Child do + foo = 1 + end + end + """) + end + + test "context local macro" do + # this does not expand the macro + assert_expansion_env(""" + defmodule Abc do + defmacro foo(x) do + quote do + unquote(x) + 1 + end + end + + def go(z) do + foo(z) + end + end + """) + end + + test "context remote macro" do + # this does not expand the macro + assert_expansion_env(""" + defmodule Abc do + defmacro foo(x) do + quote do + unquote(x) + 1 + end + end + end + + defmodule Cde do + require Abc + def go(z) do + Abc.foo(z) + end + end + """) + end + + test "def" do + ast = + Code.string_to_quoted( + """ + defmodule Abc do + def abc, do: :ok + end + """, + columns: true, + token_metadata: true + ) + + {_expanded, _state, _env} = + Compiler.expand(ast, %State{}, %{Compiler.env() | module: Foo}) + + # elixir_env = %{:elixir_env.new() | module: Foo} + # {elixir_expanded, _elixir_state, elixir_env} = :elixir_expand.expand(ast, :elixir_env.env_to_ex(elixir_env), elixir_env) + + # assert expanded == elixir_expanded + # assert env == elixir_env + end + end + + defmodule Foo do + defguard my(a) when is_integer(a) and a > 1 + + defmacro aaa(a) do + quote do + is_integer(unquote(a)) and unquote(a) > 1 + end + end + end + + test "guard" do + code = """ + require ElixirSense.Core.CompilerTest.Foo, as: Foo + Foo.my(5) + """ + + assert_expansion(code) + end + + test "macro" do + code = """ + require ElixirSense.Core.CompilerTest.Foo, as: Foo + Foo.aaa(5) + """ + + assert_expansion(code) + end + + defp clean_capture_arg(ast) do + {ast, _} = + Macro.prewalk(ast, nil, fn + {{:., dot_meta, target}, call_meta, args}, state -> + dot_meta = Keyword.delete(dot_meta, :column_correction) + {{{:., dot_meta, target}, call_meta, args}, state} + + {atom, meta, nil} = node, state when is_atom(atom) -> + # elixir changes the name to capture and does different counter tracking + node = + with "&" <> int <- to_string(atom), {_, ""} <- Integer.parse(int) do + meta = Keyword.delete(meta, :counter) + {:capture, meta, nil} + else + _ -> node + end + + {node, state} + + node, state -> + {node, state} + end) + + ast + end + + defp clean_capture_arg_elixir(ast) do + {ast, _} = + Macro.prewalk(ast, nil, fn + {:capture, meta, nil} = _node, state -> + # elixir changes the name to capture and does different counter tracking + meta = Keyword.delete(meta, :counter) + {{:capture, meta, nil}, state} + + node, state -> + {node, state} + end) + + ast + end +end diff --git a/test/elixir_sense/core/introspection_test.exs b/test/elixir_sense/core/introspection_test.exs index 46a4c8a3..3fd890ad 100644 --- a/test/elixir_sense/core/introspection_test.exs +++ b/test/elixir_sense/core/introspection_test.exs @@ -55,7 +55,7 @@ defmodule ElixirSense.Core.IntrospectionTest do if System.otp_release() |> String.to_integer() >= 23 do if System.otp_release() |> String.to_integer() >= 27 do - assert "This function is" <> _ = summary + assert "Select the _callback mode_" <> _ = summary else assert "- CallbackMode = " <> _ = summary end @@ -146,25 +146,37 @@ defmodule ElixirSense.Core.IntrospectionTest do test "get_returns_from_callback (erlang specs)" do returns = get_returns_from_callback(:gen_fsm, :handle_event, 3) - assert returns == [ - %{ - description: "{:next_state, nextStateName, newStateData}", - snippet: "{:next_state, \"${1:nextStateName}$\", \"${2:newStateData}$\"}", - spec: "{:next_state, nextStateName :: atom(), newStateData :: term()}" - }, - %{ - description: "{:next_state, nextStateName, newStateData, timeout() | :hibernate}", - snippet: - "{:next_state, \"${1:nextStateName}$\", \"${2:newStateData}$\", \"${3:timeout() | :hibernate}$\"}", - spec: - "{:next_state, nextStateName :: atom(), newStateData :: term(), timeout() | :hibernate}" - }, - %{ - description: "{:stop, reason, newStateData}", - snippet: "{:stop, \"${1:reason}$\", \"${2:newStateData}$\"}", - spec: "{:stop, reason :: term(), newStateData :: term()}" - } - ] + if System.otp_release() |> String.to_integer() >= 27 do + assert returns == [ + %{ + description: "result", + snippet: "\"${1:result}$\"", + spec: + "result\nwhen event: term(),\n stateName: atom(),\n stateData: term(),\n result:\n {:next_state, nextStateName, newStateData}\n | {:next_state, nextStateName, newStateData, timeout}\n | {:next_state, nextStateName, newStateData, :hibernate}\n | {:stop, reason, newStateData},\n nextStateName: atom(),\n newStateData: term(),\n timeout: timeout(),\n reason: term()" + } + ] + else + assert returns == [ + %{ + description: "{:next_state, nextStateName, newStateData}", + snippet: "{:next_state, \"${1:nextStateName}$\", \"${2:newStateData}$\"}", + spec: "{:next_state, nextStateName :: atom(), newStateData :: term()}" + }, + %{ + description: + "{:next_state, nextStateName, newStateData, timeout() | :hibernate}", + snippet: + "{:next_state, \"${1:nextStateName}$\", \"${2:newStateData}$\", \"${3:timeout() | :hibernate}$\"}", + spec: + "{:next_state, nextStateName :: atom(), newStateData :: term(), timeout() | :hibernate}" + }, + %{ + description: "{:stop, reason, newStateData}", + snippet: "{:stop, \"${1:reason}$\", \"${2:newStateData}$\"}", + spec: "{:stop, reason :: term(), newStateData :: term()}" + } + ] + end end test "actual_mod_fun Elixir proxy" do diff --git a/test/elixir_sense/core/macro_expander_test.exs b/test/elixir_sense/core/macro_expander_test.exs deleted file mode 100644 index 69705aca..00000000 --- a/test/elixir_sense/core/macro_expander_test.exs +++ /dev/null @@ -1,74 +0,0 @@ -defmodule ElixirSense.Core.MacroExpanderTest do - use ExUnit.Case, async: true - - alias ElixirSense.Core.MacroExpander - - test "expand use" do - ast = - quote do - use ElixirSenseExample.OverridableFunctions - end - - expanded = - ast - |> MacroExpander.add_default_meta() - |> MacroExpander.expand_use(MyModule, [], line: 2, column: 1) - - if Version.match?(System.version(), ">= 1.13.0") do - assert Macro.to_string(expanded) =~ "defmacro required(var)" - end - end - - test "expand use with alias" do - ast = - quote do - use OverridableFunctions - end - - expanded = - ast - |> MacroExpander.add_default_meta() - |> MacroExpander.expand_use( - MyModule, - [{OverridableFunctions, ElixirSenseExample.OverridableFunctions}], - line: 2, - column: 1 - ) - - if Version.match?(System.version(), ">= 1.13.0") do - assert Macro.to_string(expanded) =~ "defmacro required(var)" - end - end - - test "expand use calling use" do - ast = - quote do - use ElixirSenseExample.Overridable.Using - end - - expanded = - ast - |> MacroExpander.add_default_meta() - |> MacroExpander.expand_use(MyModule, [], line: 2, column: 1) - - if Version.match?(System.version(), ">= 1.13.0") do - assert Macro.to_string(expanded) =~ "defmacro bar(var)" - end - end - - test "expand use when module does not define __using__ macro" do - ast = - quote do - use ElixirSenseExample.OverridableBehaviour - end - - expanded = - ast - |> MacroExpander.add_default_meta() - |> MacroExpander.expand_use(MyModule, [], line: 2, column: 1) - - if Version.match?(System.version(), ">= 1.13.0") do - assert Macro.to_string(expanded) =~ "require ElixirSenseExample.OverridableBehaviour" - end - end -end diff --git a/test/elixir_sense/core/metadata_builder/alias_test.exs b/test/elixir_sense/core/metadata_builder/alias_test.exs index fa813b94..8c5f012f 100644 --- a/test/elixir_sense/core/metadata_builder/alias_test.exs +++ b/test/elixir_sense/core/metadata_builder/alias_test.exs @@ -58,6 +58,7 @@ defmodule ElixirSense.Core.MetadataBuilder.AliasTest do assert metadata_env = state.lines_to_env[env.line] assert metadata_env.aliases == env.aliases + # assert State.macro_env(state, metadata_env, env.line) == env end end diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs new file mode 100644 index 00000000..7081b110 --- /dev/null +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -0,0 +1,2971 @@ +defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do + use ExUnit.Case, async: true + + alias ElixirSense.Core.MetadataBuilder + alias ElixirSense.Core.Normalized.Code, as: NormalizedCode + + defp get_cursor_env(code, use_string_to_quoted \\ false) do + {:ok, ast} = + if use_string_to_quoted do + Code.string_to_quoted(code, + columns: true, + token_metadata: true + ) + else + NormalizedCode.Fragment.container_cursor_to_quoted(code, + columns: true, + token_metadata: true + ) + end + + # dbg(ast) + state = MetadataBuilder.build(ast) + state.cursor_env + end + + describe "incomplete case" do + test "no arg 1" do + code = """ + case [] + \ + """ + + assert {_meta, _env} = get_cursor_env(code) + end + + test "no arg 2" do + code = """ + case [], [] + \ + """ + + assert {_meta, _env} = get_cursor_env(code) + end + + test "cursor in argument 1" do + code = """ + case [], \ + """ + + assert {_meta, _env} = get_cursor_env(code) + end + + test "cursor in argument 2" do + code = """ + x = 5 + case \ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end + + test "cursor in clause left side" do + code = """ + case a do + [x, \ + """ + + assert {_meta, _env} = get_cursor_env(code) + # this test fails + # assert Enum.any?(env.vars, &(&1.name == :x)) + end + + test "cursor in clause guard" do + code = """ + case a do + x when \ + """ + + assert {_meta, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert Enum.any?(env.vars, &(&1.name == :x)) + end + end + + test "cursor in clause guard call" do + code = """ + case a do + x when is_atom(\ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end + + test "cursor in clause right side" do + code = """ + case a do + x -> \ + """ + + assert {_meta, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert Enum.any?(env.vars, &(&1.name == :x)) + end + end + + test "cursor in clause right side after expressions" do + code = """ + case a do + x -> + foo(1) + \ + """ + + assert {_meta, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert Enum.any?(env.vars, &(&1.name == :x)) + end + end + + if Version.match?(System.version(), ">= 1.15.0") do + test "invalid number of args with when" do + code = """ + case nil do 0, z when not is_nil(z) -> \ + """ + + assert get_cursor_env(code) + end + end + + if Version.match?(System.version(), ">= 1.15.0") do + test "invalid number of args" do + code = """ + case nil do 0, z -> \ + """ + + assert get_cursor_env(code) + end + end + end + + describe "incomplete cond" do + test "no arg" do + code = """ + cond [] + \ + """ + + assert {_meta, _env} = get_cursor_env(code) + end + + test "cursor in arg" do + code = """ + x = foo() + cond \ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end + + test "cursor in clause left side" do + code = """ + x = foo() + cond do + \ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end + + test "cursor in clause left side with assignment" do + code = """ + cond do + (x = foo(); \ + """ + + assert {_meta, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert Enum.any?(env.vars, &(&1.name == :x)) + end + end + + test "cursor in clause right side" do + code = """ + cond do + x = foo() -> \ + """ + + assert {_meta, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert Enum.any?(env.vars, &(&1.name == :x)) + end + end + + test "cursor in clause right side after expressions" do + code = """ + cond do + x = foo() -> + foo(1) + \ + """ + + assert {_meta, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert Enum.any?(env.vars, &(&1.name == :x)) + end + end + + if Version.match?(System.version(), ">= 1.15.0") do + test "invalid number of args" do + code = """ + cond do 0, z -> \ + """ + + assert get_cursor_env(code) + end + end + end + + describe "incomplete receive" do + test "no arg" do + code = """ + receive [] + \ + """ + + assert {_meta, _env} = get_cursor_env(code) + end + + test "cursor in arg" do + code = """ + x = foo() + receive \ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end + + test "cursor in clause left side" do + code = """ + x = foo() + receive do + \ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end + + test "cursor in clause left side pin" do + code = """ + x = foo() + receive do + {^\ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end + + test "cursor in clause left side multiple matches" do + code = """ + receive do + {:msg, x, \ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end + + test "cursor in clause left side guard" do + code = """ + receive do + {:msg, x} when \ + """ + + assert {_meta, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert Enum.any?(env.vars, &(&1.name == :x)) + end + end + + test "cursor in clause left side guard call" do + code = """ + receive do + {:msg, x} when is_atom(\ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end + + test "cursor in clause right side" do + code = """ + receive do + {:msg, x} -> \ + """ + + assert {_meta, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert Enum.any?(env.vars, &(&1.name == :x)) + end + end + + test "cursor in after clause left side" do + code = """ + x = foo() + receive do + a -> :ok + after + \ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end + + test "cursor in after clause right side" do + code = """ + x = foo() + receive do + a -> :ok + after + 0 -> \ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end + + if Version.match?(System.version(), ">= 1.15.0") do + test "invalid number of args in after" do + code = """ + receive do + a -> :ok + after + 0, z -> \ + """ + + assert get_cursor_env(code) + end + end + + test "invalid number of clauses in after" do + code = """ + receive do + a -> :ok + after + 0 -> :ok + 1 -> \ + """ + + assert get_cursor_env(code) + end + end + + describe "incomplete try" do + test "no arg" do + code = """ + try [] + \ + """ + + assert {_meta, _env} = get_cursor_env(code) + end + + test "cursor in arg" do + code = """ + x = foo() + try \ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end + + test "cursor in do block" do + code = """ + x = foo() + try do + \ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end + + test "cursor in left side of rescue clause" do + code = """ + x = foo() + try do + bar(x) + rescue + \ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end + + test "cursor in left side of rescue clause match expression - invalid var" do + code = """ + x = foo() + try do + bar(x) + rescue + bar() in [\ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end + + test "cursor in left side of rescue clause match expression" do + code = """ + x = foo() + try do + bar(x) + rescue + e in [\ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end + + test "cursor in right side of rescue clause" do + code = """ + try do + bar() + rescue + x in [Error] -> \ + """ + + assert {_meta, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert Enum.any?(env.vars, &(&1.name == :x)) + end + end + + test "cursor in left side of catch clause" do + code = """ + x = foo() + try do + bar() + catch + \ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end + + test "cursor in left side of catch clause guard" do + code = """ + try do + bar() + catch + x when \ + """ + + assert {_meta, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert Enum.any?(env.vars, &(&1.name == :x)) + end + end + + if Version.match?(System.version(), ">= 1.17.0") do + test "cursor in left side of catch clause after type" do + code = """ + try do + bar() + catch + x, \ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end + end + + if Version.match?(System.version(), ">= 1.17.0") do + test "cursor in left side of catch clause 2 arg guard" do + code = """ + try do + bar() + catch + x, _ when \ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end + end + + test "cursor in right side of catch clause" do + code = """ + try do + bar() + catch + x -> \ + """ + + assert {_meta, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert Enum.any?(env.vars, &(&1.name == :x)) + end + end + + if Version.match?(System.version(), ">= 1.15.0") do + test "cursor in right side of catch clause 2 arg" do + code = """ + try do + bar() + catch + x, _ -> \ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end + end + + test "cursor in left side of else clause" do + code = """ + x = foo() + try do + bar() + else + \ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end + + test "cursor in left side of else clause guard" do + code = """ + try do + bar() + else + x when \ + """ + + assert {_meta, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert Enum.any?(env.vars, &(&1.name == :x)) + end + end + + test "cursor in right side of else clause" do + code = """ + try do + bar() + else + x -> \ + """ + + assert {_meta, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert Enum.any?(env.vars, &(&1.name == :x)) + end + end + + test "cursor in after block" do + code = """ + x = foo() + try do + bar() + after + \ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end + end + + describe "incomplete with" do + test "cursor in arg" do + code = """ + x = foo() + with [], \ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end + + test "cursor in match expressions" do + code = """ + x = foo() + with \ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end + + test "cursor in match expressions guard" do + code = """ + with x when \ + """ + + assert {_meta, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert Enum.any?(env.vars, &(&1.name == :x)) + end + end + + test "cursor in match expressions - right side" do + code = """ + x = foo() + with 1 <- \ + """ + + assert {_meta, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert Enum.any?(env.vars, &(&1.name == :x)) + end + end + + test "cursor in match expressions - right side next expression" do + code = """ + with x <- foo(), \ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end + + test "cursor in do block" do + code = """ + with x <- foo() do + \ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end + + test "cursor in else clause left side" do + code = """ + x = foo() + with 1 <- foo() do + :ok + else + \ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end + + test "cursor in else clause left side guard" do + code = """ + with 1 <- foo() do + :ok + else + x when \ + """ + + assert {_meta, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert Enum.any?(env.vars, &(&1.name == :x)) + end + end + + test "cursor in else clause right side" do + code = """ + with 1 <- foo() do + :ok + else + x -> \ + """ + + assert {_meta, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert Enum.any?(env.vars, &(&1.name == :x)) + end + end + end + + describe "incomplete for" do + test "cursor in arg" do + code = """ + x = foo() + for [], \ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end + + test "cursor in generator match expressions" do + code = """ + x = foo() + for \ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end + + test "cursor in generator match expression guard" do + code = """ + for x when \ + """ + + assert {_meta, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert Enum.any?(env.vars, &(&1.name == :x)) + end + end + + test "cursor in generator match expression right side" do + code = """ + x = foo() + for a <- \ + """ + + assert {_meta, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert Enum.any?(env.vars, &(&1.name == :x)) + end + end + + test "cursor in generator match expressions bitstring" do + code = """ + x = foo() + for <<\ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end + + test "cursor in generator match expression guard bitstring" do + code = """ + for <= 1.15.0") do + assert Enum.any?(env.vars, &(&1.name == :x)) + end + end + + test "cursor in generator match expression right side bitstring" do + code = """ + x = foo() + for <= 1.15.0") do + assert Enum.any?(env.vars, &(&1.name == :y)) + end + end + + if Version.match?(System.version(), ">= 1.17.0") do + test "cursor in do block reduce left side of clause too many args" do + code = """ + for x <- [], reduce: %{} do + y, \ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + # this test fails + # assert Enum.any?(env.vars, &(&1.name == :y)) + end + end + + test "cursor in do block reduce right side of clause" do + code = """ + for x <- [], reduce: %{} do + y -> \ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + + if Version.match?(System.version(), ">= 1.15.0") do + assert Enum.any?(env.vars, &(&1.name == :y)) + end + end + + if Version.match?(System.version(), ">= 1.15.0") do + test "cursor in do block reduce right side of clause too many args" do + code = """ + for x <- [], reduce: %{} do + y, z -> \ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + assert Enum.any?(env.vars, &(&1.name == :y)) + end + end + + test "cursor in do block reduce right side of clause too little args" do + code = """ + for x <- [], reduce: %{} do + -> \ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end + + test "cursor in do block right side of clause without reduce" do + code = """ + for x <- [] do + y -> \ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + + if Version.match?(System.version(), ">= 1.15.0") do + assert Enum.any?(env.vars, &(&1.name == :y)) + end + end + end + + describe "invalid fn" do + # unfortunately container_cursor_to_quoted cannot handle fn + test "different clause arities" do + code = """ + fn + _ -> :ok + x, _ -> __cursor__() + end + """ + + assert {_meta, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert Enum.any?(env.vars, &(&1.name == :x)) + end + end + + test "default args in clause" do + code = """ + fn + x \\\\ nil -> __cursor__() + end + """ + + assert {_meta, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert Enum.any?(env.vars, &(&1.name == :x)) + end + end + + test "incomplete clause left side" do + code = """ + x = foo() + fn \ + """ + + assert {_meta, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert Enum.any?(env.vars, &(&1.name == :x)) + end + end + + if Version.match?(System.version(), ">= 1.17.0") do + test "incomplete clause left side guard" do + code = """ + fn + x when \ + """ + + assert {_meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end + end + + test "incomplete clause right side" do + code = """ + fn + x -> __cursor__() + end + """ + + assert {_meta, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert Enum.any?(env.vars, &(&1.name == :x)) + end + end + end + + describe "capture" do + test "empty" do + code = """ + &\ + """ + + assert get_cursor_env(code) + end + + test "local" do + code = """ + &foo\ + """ + + assert get_cursor_env(code) + end + + test "local slash no arity" do + code = """ + &foo/\ + """ + + assert get_cursor_env(code) + end + + test "local slash arity" do + code = """ + &foo/1\ + """ + + assert get_cursor_env(code) + end + + test "local slash invalid arity" do + code = """ + &foo/1000; \ + """ + + assert get_cursor_env(code) + end + + test "local dot" do + code = """ + &foo.\ + """ + + assert get_cursor_env(code) + end + + test "local dot call" do + code = """ + &foo.(\ + """ + + assert get_cursor_env(code) + end + + test "local dot call closed" do + code = """ + &foo.()\ + """ + + assert get_cursor_env(code) + end + + test "local dot right" do + code = """ + &foo.bar\ + """ + + assert get_cursor_env(code) + end + + test "remote" do + code = """ + &Foo\ + """ + + assert get_cursor_env(code) + end + + test "remote dot" do + code = """ + &Foo.\ + """ + + assert get_cursor_env(code) + end + + test "remote dot right" do + code = """ + &Foo.bar\ + """ + + assert get_cursor_env(code) + end + + test "remote dot right no arity" do + code = """ + &Foo.bar/\ + """ + + assert get_cursor_env(code) + end + + test "remote dot right arity" do + code = """ + &Foo.bar/1\ + """ + + assert get_cursor_env(code) + end + + test "remote dot call" do + code = """ + &Foo.bar(\ + """ + + assert get_cursor_env(code) + end + + test "remote dot call closed" do + code = """ + &Foo.bar()\ + """ + + assert get_cursor_env(code) + end + + test "tuple" do + code = """ + &{\ + """ + + assert get_cursor_env(code) + end + + test "tuple closed" do + code = """ + &{}\ + """ + + assert get_cursor_env(code) + end + + test "list" do + code = """ + &[\ + """ + + assert get_cursor_env(code) + end + + test "list closed" do + code = """ + &[]\ + """ + + assert get_cursor_env(code) + end + + test "bitstring" do + code = """ + &<<\ + """ + + assert get_cursor_env(code) + end + + test "bitstring closed" do + code = """ + &<<>>\ + """ + + assert get_cursor_env(code) + end + + test "map no braces" do + code = """ + &%\ + """ + + assert get_cursor_env(code) + end + + test "map" do + code = """ + &%{\ + """ + + assert get_cursor_env(code) + end + + test "map closed" do + code = """ + &%{}\ + """ + + assert get_cursor_env(code) + end + + test "struct no braces" do + code = """ + &%Foo\ + """ + + assert get_cursor_env(code) + end + + test "struct" do + code = """ + &%Foo{\ + """ + + assert get_cursor_env(code) + end + + test "struct closed" do + code = """ + &%Foo{}\ + """ + + assert get_cursor_env(code) + end + + test "block" do + code = """ + & (\ + """ + + assert get_cursor_env(code) + end + + test "block multiple expressions" do + code = """ + & (:ok; \ + """ + + assert get_cursor_env(code) + end + + test "arg var incomplete" do + code = """ + & &\ + """ + + assert get_cursor_env(code) + end + + test "arg var" do + code = """ + & &2\ + """ + + assert get_cursor_env(code) + end + + test "arg var in list" do + code = """ + &[&1, \ + """ + + assert get_cursor_env(code) + end + + test "arg var in list without predecessor" do + code = """ + &[&2, \ + """ + + assert get_cursor_env(code) + end + + test "no arg" do + code = """ + &{}; \ + """ + + assert get_cursor_env(code) + end + + test "invalid arg number" do + code = """ + & &0; \ + """ + + assert get_cursor_env(code) + end + + test "outside of capture" do + code = """ + &1; \ + """ + + assert get_cursor_env(code) + end + + test "invalid arg local" do + code = """ + &foo; \ + """ + + assert get_cursor_env(code) + end + + test "invalid arg" do + code = """ + &"foo"; \ + """ + + assert get_cursor_env(code) + end + + test "undefined local capture" do + code = """ + defmodule A do + (&asdf/1) +\ + """ + + assert get_cursor_env(code) + end + + test "ambiguous local" do + code = """ + defmodule Kernel.ErrorsTest.FunctionImportConflict do + import :erlang, only: [exit: 1], warn: false + def foo, do: (&exit/1) +\ + """ + + assert get_cursor_env(code) + end + end + + describe "pin" do + test "outside of match" do + code = """ + ^\ + """ + + assert get_cursor_env(code) + end + + test "cursor in match" do + code = """ + ^__cursor__() = x\ + """ + + assert get_cursor_env(code) + end + end + + describe "map" do + test "invalid key in match" do + code = """ + %{foo => x} = x\ + """ + + assert get_cursor_env(code) + end + + test "update in match" do + code = """ + %{a | x: __cursor__()} = x\ + """ + + assert get_cursor_env(code) + end + + test "cursor in place of key value pair" do + code = """ + %{a: "123", \ + """ + + assert get_cursor_env(code) + end + end + + describe "struct" do + test "no map" do + code = """ + %\ + """ + + assert get_cursor_env(code) + end + + test "invalid map name" do + code = """ + %foo{\ + """ + + assert get_cursor_env(code) + end + + test "invalid key" do + code = """ + %Foo{"asd" => [\ + """ + + assert get_cursor_env(code) + end + end + + describe "bitstring" do + test "no size specifier with unit" do + code = """ + <>)::32, \ + """ + + assert get_cursor_env(code) + end + + test "unsized" do + code = """ + <> = \ + """ + + assert get_cursor_env(code) + end + + test "bad argument" do + code = """ + <<"foo"::size(8)-unit(:oops), \ + """ + + assert get_cursor_env(code) + end + + test "undefined" do + code = """ + <<1::unknown(), \ + """ + + assert get_cursor_env(code) + end + + test "unknown" do + code = """ + <<1::refb_spec, \ + """ + + assert get_cursor_env(code) + end + + test "invalid literal" do + code = """ + <<:ok, \ + """ + + assert get_cursor_env(code) + end + + test "nested match" do + code = """ + <> = \ + """ + + assert get_cursor_env(code) + end + + test "incomplete" do + code = """ + <<\ + """ + + assert get_cursor_env(code) + end + + test "incomplete ::" do + code = """ + <<1::\ + """ + + assert get_cursor_env(code) + end + + test "incomplete -" do + code = """ + <<1::binary-\ + """ + + assert get_cursor_env(code) + end + + test "incomplete open parens" do + code = """ + <<1::size(\ + """ + + assert get_cursor_env(code) + end + end + + describe "quote/unquote/unquote_splicing" do + test "invalid bind quoted" do + code = """ + quote [bind_quoted: 123] do\ + """ + + assert get_cursor_env(code) + end + + test "incomplete 1" do + code = """ + quote \ + """ + + assert get_cursor_env(code) + end + + test "incomplete 2" do + code = """ + quote [\ + """ + + assert get_cursor_env(code) + end + + test "incomplete 3" do + code = """ + quote [bind_quoted: \ + """ + + assert get_cursor_env(code) + end + + test "incomplete 4" do + code = """ + quote [bind_quoted: [\ + """ + + assert get_cursor_env(code) + end + + test "incomplete 5" do + code = """ + quote [bind_quoted: [asd: \ + """ + + assert get_cursor_env(code) + end + + test "incomplete 6" do + code = """ + quote [bind_quoted: [asd: 1]], \ + """ + + assert get_cursor_env(code) + end + + test "incomplete 7" do + code = """ + quote [bind_quoted: [asd: 1]], [\ + """ + + assert get_cursor_env(code) + end + + test "incomplete 8" do + code = """ + quote :foo, [\ + """ + + assert get_cursor_env(code) + end + + test "in do block" do + code = """ + quote do + \ + """ + + assert get_cursor_env(code) + end + + test "in do block unquote" do + code = """ + quote do + unquote(\ + """ + + assert get_cursor_env(code) + end + + test "in do block unquote_splicing" do + code = """ + quote do + unquote_splicing(\ + """ + + assert get_cursor_env(code) + end + + test "in do block unquote with bind_quoted" do + code = """ + quote bind_quoted: [a: 1] do + unquote(\ + """ + + assert get_cursor_env(code) + end + + test "unquote without quote" do + code = """ + unquote(\ + """ + + assert get_cursor_env(code) + end + + test "invalid compile option" do + code = """ + quote [file: 1] do\ + """ + + assert get_cursor_env(code) + end + + test "invalid runtime option" do + code = """ + quote [unquote: 1] do\ + """ + + assert get_cursor_env(code) + end + + test "unquote_splicing not in block" do + code = """ + quote do: unquote_splicing(\ + """ + + assert get_cursor_env(code) + end + end + + describe "calls" do + test "invalid anonymous call" do + code = """ + :foo.(a, \ + """ + + assert get_cursor_env(code) + end + + test "anonymous call in match" do + code = """ + a.() = \ + """ + + assert get_cursor_env(code) + end + + test "anonymous call in guard" do + code = """ + case x do + y when a.() -> \ + """ + + assert get_cursor_env(code) + end + + test "parens map lookup guard" do + code = """ + case x do + y when map.field() -> \ + """ + + assert get_cursor_env(code) + end + + test "remote call in match" do + code = """ + Foo.bar() = \ + """ + + assert get_cursor_env(code) + end + + test "invalid remote call" do + code = """ + __ENV__.line.foo \ + """ + + assert get_cursor_env(code) + end + + test "clause in remote call" do + code = """ + Foo.foo do + a -> \ + """ + + assert get_cursor_env(code) + end + + test "invalid local call" do + code = """ + 1.foo \ + """ + + assert get_cursor_env(code) + end + + test "ambiguous local call" do + code = """ + a = 1 + a -1 .. \ + """ + + assert get_cursor_env(code) + end + + test "clause in local call" do + code = """ + foo do + a -> \ + """ + + assert get_cursor_env(code) + end + + test "local call in match" do + code = """ + bar() = \ + """ + + assert get_cursor_env(code) + end + + test "ambiguous call" do + code = """ + defmodule Kernel.ErrorsTest.FunctionImportConflict do + import :erlang, only: [exit: 1], warn: false + def foo, do: exit(\ + """ + + assert get_cursor_env(code) + end + + # this is not crashing because we don't support local macros yet + test "conflicting macro" do + code = """ + defmodule Kernel.ErrorsTest.MacroLocalConflict do + def hello, do: 1 || 2 + defmacro _ || _, do: :ok + + defmacro _ && _, do: :error + def world, do: 1 && \ + """ + + assert get_cursor_env(code) + end + + test "invalid call cursor" do + code = """ + __cursor__(a.b)() + """ + + assert get_cursor_env(code, true) + end + end + + describe "__ENV__, __MODULE__, __CALLER__, __STACKTRACE__, __DIR__" do + test "__ENV__ in match" do + code = """ + __ENV__ = \ + """ + + assert get_cursor_env(code) + end + + test "__CALLER__ not in macro" do + code = """ + inspect(__CALLER__, \ + """ + + assert get_cursor_env(code) + end + + test "__STACKTRACE__ outside of catch/rescue" do + code = """ + inspect(__STACKTRACE__, \ + """ + + assert get_cursor_env(code) + end + + test "__MODULE__ outside of module" do + code = """ + inspect(__MODULE__, \ + """ + + assert get_cursor_env(code) + end + + test "__DIR__ when nofile" do + code = """ + inspect(__DIR__, \ + """ + + assert get_cursor_env(code) + end + end + + describe "alias/import/require" do + test "invalid alias expansion" do + code = """ + foo = :foo + foo.Foo.a(\ + """ + + assert get_cursor_env(code) + end + + test "incomplete" do + code = """ + alias \ + """ + + assert get_cursor_env(code) + + code = """ + require \ + """ + + assert get_cursor_env(code) + + code = """ + import \ + """ + + assert get_cursor_env(code) + end + + test "multi" do + code = """ + alias ElixirSenseExample\ + """ + + assert get_cursor_env(code) + + code = """ + alias ElixirSenseExample.\ + """ + + assert get_cursor_env(code) + + code = """ + alias ElixirSenseExample.{\ + """ + + assert get_cursor_env(code) + + code = """ + alias ElixirSenseExample.{S\ + """ + + assert get_cursor_env(code) + + code = """ + alias ElixirSenseExample.{Some, \ + """ + + assert get_cursor_env(code) + + code = """ + alias ElixirSenseExample.{Some, Mod\ + """ + + assert get_cursor_env(code) + end + + test "invalid" do + code = """ + alias A.a\ + """ + + assert get_cursor_env(code) + + code = """ + require A.a\ + """ + + assert get_cursor_env(code) + + code = """ + import A.a\ + """ + + assert get_cursor_env(code) + end + + test "in options" do + code = """ + alias A.B, \ + """ + + assert get_cursor_env(code) + + code = """ + require A.B, \ + """ + + assert get_cursor_env(code) + + code = """ + import A.B, \ + """ + + assert get_cursor_env(code) + end + + test "in option" do + code = """ + alias A.B, warn: \ + """ + + assert get_cursor_env(code) + + code = """ + require A.B, warn: \ + """ + + assert get_cursor_env(code) + + code = """ + import A.B, warn: \ + """ + + assert get_cursor_env(code) + end + end + + describe "super" do + test "call outside module" do + code = """ + super(\ + """ + + assert get_cursor_env(code) + end + + test "call outside function" do + code = """ + defmodule A do + super(\ + """ + + assert get_cursor_env(code) + end + + test "call in match" do + code = """ + super() = \ + """ + + assert get_cursor_env(code) + end + + test "capture expression outside module" do + code = """ + & super(&1, \ + """ + + assert get_cursor_env(code) + end + + test "capture outside module" do + code = """ + &super\ + """ + + assert get_cursor_env(code) + + code = """ + &super/\ + """ + + assert get_cursor_env(code) + + code = """ + &super/1 \ + """ + + assert get_cursor_env(code) + + code = """ + (&super/1) +\ + """ + + assert get_cursor_env(code) + end + + test "call wrong args" do + code = """ + defmodule A do + def a do + super(\ + """ + + assert get_cursor_env(code) + end + + test "call no super" do + code = """ + defmodule A do + def a(1), do: :ok + def a(x) do + super(x) +\ + """ + + assert get_cursor_env(code) + end + end + + describe "var" do + test "_ in cond" do + code = """ + cond do + x -> x + _ -> \ + """ + + assert get_cursor_env(code) + end + + test "_ outside of match" do + code = """ + {1, _, [\ + """ + + assert get_cursor_env(code) + end + + test "parallel bitstring match" do + code = """ + <> = <> = \ + """ + + assert get_cursor_env(code) + end + + test "match in guard" do + code = """ + cond do + x when x = \ + """ + + assert get_cursor_env(code) + end + + test "stacktrace in match" do + code = """ + __STACKTRACE__ = \ + """ + + assert get_cursor_env(code) + end + end + + describe "typespec" do + test "in type name" do + code = """ + defmodule Abc do + @type foo\ + """ + + assert {_, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert env.typespec == {:__unknown__, 0} + end + end + + test "in spec name" do + code = """ + defmodule Abc do + @spec foo\ + """ + + assert {_, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert env.typespec == {:__unknown__, 0} + end + end + + test "in type after ::" do + code = """ + defmodule Abc do + @type foo :: \ + """ + + assert {_, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert env.typespec == {:foo, 0} + end + end + + test "in spec after ::" do + code = """ + defmodule Abc do + @spec foo :: \ + """ + + assert {_, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert env.typespec == {:foo, 0} + end + end + + test "in type after :: type" do + code = """ + defmodule Abc do + @type foo :: bar\ + """ + + assert {_, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert env.typespec == {:foo, 0} + end + end + + test "in type after :: type with | empty" do + code = """ + defmodule Abc do + @type foo :: bar | \ + """ + + assert {_, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert env.typespec == {:foo, 0} + end + end + + test "in type after :: type with |" do + code = """ + defmodule Abc do + @type foo :: bar | baz\ + """ + + assert {_, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert env.typespec == {:foo, 0} + end + end + + test "in type after :: type with fun" do + code = """ + defmodule Abc do + @type foo :: (...\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type after :: type with fun ->" do + code = """ + defmodule Abc do + @type foo :: (... -> \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type after :: type with fun -> no arg" do + code = """ + defmodule Abc do + @type foo :: (-> \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type after :: type with fun (" do + code = """ + defmodule Abc do + @type foo :: (\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + if Version.match?(System.version(), ">= 1.17.0") do + test "in type after :: type with fun ( nex arg" do + code = """ + defmodule Abc do + @type foo :: (bar, \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + end + + test "in type after :: type with map empty" do + code = """ + defmodule Abc do + @type foo :: %{\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type after :: type with map key" do + code = """ + defmodule Abc do + @type foo :: %{bar\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type after :: type with map after key" do + code = """ + defmodule Abc do + @type foo :: %{bar: \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type after :: type with map after =>" do + code = """ + defmodule Abc do + @type foo :: %{:bar => \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type after :: type with map optional" do + code = """ + defmodule Abc do + @type foo :: %{optional(\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type after :: type named empty" do + code = """ + defmodule Abc do + @type foo :: {bar :: \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type after :: type named" do + code = """ + defmodule Abc do + @type foo :: {bar :: baz\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in spec after :: type" do + code = """ + defmodule Abc do + @spec foo :: bar\ + """ + + assert {_, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert env.typespec == {:foo, 0} + end + end + + test "in type after :: remote type" do + code = """ + defmodule Abc do + @type foo :: Remote.bar\ + """ + + assert {_, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert env.typespec == {:foo, 0} + end + end + + test "in type after :: type args" do + code = """ + defmodule Abc do + @type foo :: Remote.bar(\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in spec after :: type args" do + code = """ + defmodule Abc do + @spec foo :: Remote.bar(\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type arg empty" do + code = """ + defmodule Abc do + @type foo(\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 1} + end + + test "in spec arg empty" do + code = """ + defmodule Abc do + @spec foo(\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 1} + end + + test "in type arg" do + code = """ + defmodule Abc do + @type foo(bar\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 1} + end + + test "in spec arg" do + code = """ + defmodule Abc do + @spec foo(bar\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 1} + end + + test "in spec arg named empty" do + code = """ + defmodule Abc do + @spec foo(bar :: \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 1} + end + + test "in spec arg named" do + code = """ + defmodule Abc do + @spec foo(bar :: baz\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 1} + end + + test "in type arg next" do + code = """ + defmodule Abc do + @type foo(asd, \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 2} + end + + test "in spec when" do + code = """ + defmodule Abc do + @spec foo(a) :: integer when \ + """ + + assert {_, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert env.typespec == {:foo, 1} + end + end + + test "in spec when after :" do + code = """ + defmodule Abc do + @spec foo(a) :: integer when x: \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 1} + end + + test "in spec when after : type" do + code = """ + defmodule Abc do + @spec foo(a) :: integer when x: bar\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 1} + end + + test "in spec when after : type arg" do + code = """ + defmodule Abc do + @spec foo(a) :: integer when x: bar(\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 1} + end + + test "in spec when after : next" do + code = """ + defmodule Abc do + @spec foo(a) :: integer when x: bar(), \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 1} + end + + test "in type invalid expression" do + code = """ + defmodule Abc do + @type [{\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:__unknown__, 0} + end + + test "in spec invalid expression" do + code = """ + defmodule Abc do + @spec [{\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:__unknown__, 0} + end + + test "redefining built in" do + code = """ + defmodule Abc do + @type required(a) :: \ + """ + + assert {_, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert env.typespec == {:__required__, 1} + end + end + + test "in type list" do + code = """ + defmodule Abc do + @type foo :: [\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type list next" do + code = """ + defmodule Abc do + @type foo :: [:foo, \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type list keyword" do + code = """ + defmodule Abc do + @type foo :: [foo: \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type tuple" do + code = """ + defmodule Abc do + @type foo :: {\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type tuple next" do + code = """ + defmodule Abc do + @type foo :: {:foo, \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type union" do + code = """ + defmodule Abc do + @type foo :: :foo | \ + """ + + assert {_, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert env.typespec == {:foo, 0} + end + end + + test "in type bitstring" do + code = """ + defmodule Abc do + @type foo :: <<\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type bitstring after ::" do + code = """ + defmodule Abc do + @type foo :: <<_::\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + # test "in type bitstring next" do + # code = """ + # defmodule Abc do + # @type foo :: <<_::, \ + # """ + + # assert {_, env} = get_cursor_env(code) + # assert env.typespec == {:foo, 0} + # end + + test "in type bitstring next after" do + code = """ + defmodule Abc do + @type foo :: <<_::size, _::_*\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type struct" do + code = """ + defmodule Abc do + @type foo :: %\ + """ + + assert {_, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert env.typespec == {:foo, 0} + end + end + + test "in type struct {}" do + code = """ + defmodule Abc do + @type foo :: %Date{\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type struct key" do + code = """ + defmodule Abc do + @type foo :: %Date{key: \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type range" do + code = """ + defmodule Abc do + @type foo :: 1..\ + """ + + assert {_, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert env.typespec == {:foo, 0} + end + end + + test "type with underscored arg" do + code = """ + defmodule Abc do + @type foo(_) :: 1..\ + """ + + assert {_, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert env.typespec == {:foo, 1} + end + end + end + + describe "def" do + test "in def" do + code = """ + defmodule Abc do + def\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.module == Abc + end + + test "in def name" do + code = """ + defmodule Abc do + def foo\ + """ + + assert {_, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert env.function == {:__unknown__, 0} + end + end + + test "in def args" do + code = """ + defmodule Abc do + def foo(\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.function == {:foo, 1} + end + + test "in def arg" do + code = """ + defmodule Abc do + def foo(some\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.function == {:foo, 1} + end + + test "in def arg next" do + code = """ + defmodule Abc do + def foo(some, \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.function == {:foo, 2} + end + + test "in def after args" do + code = """ + defmodule Abc do + def foo(some) \ + """ + + assert {_, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert env.function == {:__unknown__, 0} + end + end + + test "in def after," do + code = """ + defmodule Abc do + def foo(some), \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.function == {:foo, 1} + end + + test "in def after do:" do + code = """ + defmodule Abc do + def foo(some), do: \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.function == {:foo, 1} + end + + test "in def after do" do + code = """ + defmodule Abc do + def foo(some) do + \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.function == {:foo, 1} + end + + test "in def guard" do + code = """ + defmodule Abc do + def foo(some) when \ + """ + + assert {_, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert env.function == {:foo, 1} + assert env.context == :guard + end + end + + test "in def guard variable" do + code = """ + defmodule Abc do + def foo(some) when some\ + """ + + assert {_, env} = get_cursor_env(code) + + if Version.match?(System.version(), ">= 1.15.0") do + assert env.function == {:foo, 1} + assert env.context == :guard + end + end + + test "in def after block" do + code = """ + defmodule Abc do + def foo(some) do + :ok + after + \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.function == {:foo, 1} + end + end + + describe "defmodule" do + test "in defmodule" do + code = """ + defmodule\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.module == nil + end + + test "in defmodule alias" do + code = """ + defmodule A\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.module == nil + end + + test "in defmodule after do:" do + code = """ + defmodule Abc, do: \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.module == Abc + end + + test "in defmodule after do" do + code = """ + defmodule Abc do\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.module == Abc + end + + test "in defmodule invalid alias" do + code = """ + defmodule 123, do: \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.module == :"Elixir.__Unknown__" + end + end + + describe "attribute" do + test "after @" do + code = """ + defmodule Abc do + @\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.module == Abc + end + + test "in name" do + code = """ + defmodule Abc do + @foo\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.module == Abc + end + + test "after name" do + code = """ + defmodule Abc do + @foo \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.module == Abc + end + + test "outside module" do + code = """ + @foo [\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.module == nil + end + + test "setting inside def" do + code = """ + defmodule Abc do + def go do + @foo \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.module == Abc + end + + test "invalid args" do + code = """ + defmodule Abc do + @ + def init(id) do + {:ok, + %Some.Mod{ + id: id, + events: [], + version: __cursor__() + }} + end + \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.module == Abc + end + end + + test "defimpl for" do + code = """ + defimpl Enumerable, for: \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.module == nil + end +end diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 5e6f465f..cff0e92c 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -6,15 +6,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do alias ElixirSense.Core.State alias ElixirSense.Core.State.{VarInfo, CallInfo, StructInfo, ModFunInfo, AttributeInfo} - @attribute_binding_support Version.match?(System.version(), "< 1.17.0-dev") - @expand_eval false - @binding_support Version.match?(System.version(), "< 1.17.0-dev") - @protocol_support Version.match?(System.version(), "< 1.17.0-dev") - @macro_calls_support Version.match?(System.version(), "< 1.17.0-dev") - @typespec_calls_support Version.match?(System.version(), "< 1.17.0-dev") - @record_support Version.match?(System.version(), "< 1.17.0-dev") - @compiler Code.ensure_loaded?(ElixirSense.Core.Compiler) - describe "versioned_vars" do test "in block" do state = @@ -31,6 +22,21 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] = state |> get_line_vars(2) end + test "call does not create a scope" do + state = + """ + inspect(abc = 5) + record_env() + """ + |> string_to_state + + assert Map.has_key?(state.lines_to_env[2].versioned_vars, {:abc, nil}) + + assert [ + %VarInfo{name: :abc, positions: [{1, 9}]} + ] = state |> get_line_vars(2) + end + test "nested binding" do state = """ @@ -61,7 +67,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert [ %VarInfo{name: :abc, positions: [{1, 1}]}, - %VarInfo{name: :abc, positions: [{1, 13}]}, + # %VarInfo{name: :abc, positions: [{1, 13}]}, %VarInfo{name: :cde, positions: [{1, 7}]} ] = state |> get_line_vars(2) end @@ -129,6 +135,19 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] = state |> get_line_vars(3) end + test "pin undefined" do + state = + """ + ^abc = foo() + record_env() + """ + |> string_to_state + + refute Map.has_key?(state.lines_to_env[2].versioned_vars, {:abc, nil}) + + assert [] = state |> get_line_vars(3) + end + test "rebinding" do state = """ @@ -141,9 +160,55 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert Map.has_key?(state.lines_to_env[3].versioned_vars, {:abc, nil}) assert [ - %VarInfo{name: :abc, positions: [{1, 1}]}, - %VarInfo{name: :abc, positions: [{2, 1}]} + %VarInfo{name: :abc, version: 1, positions: [{2, 1}]} ] = state |> get_line_vars(3) + + assert [ + %VarInfo{name: :abc, version: 0, positions: [{1, 1}]} + ] = state |> get_line_vars(2) + + assert state.vars_info_per_scope_id[0] == %{ + {:abc, 0} => %VarInfo{ + name: :abc, + positions: [{1, 1}], + scope_id: 0, + version: 0, + type: {:integer, 5} + }, + {:abc, 1} => %VarInfo{ + name: :abc, + positions: [{2, 1}], + scope_id: 0, + version: 1, + type: {:local_call, :foo, []} + } + } + end + + test "rebinding in defs" do + state = + """ + defmodule MyModule do + def go(asd = 3, asd, x) do + :ok + end + + def go(asd = 3, [2, asd], y) do + :ok + end + end + """ + |> string_to_state + + assert %{ + {:x, 1} => %VarInfo{positions: [{2, 24}]}, + {:asd, 0} => %VarInfo{positions: [{2, 10}, {2, 19}]} + } = state.vars_info_per_scope_id[2] + + assert %{ + {:y, 1} => %VarInfo{positions: [{6, 29}]}, + {:asd, 0} => %VarInfo{positions: [{6, 10}, {6, 23}]} + } = state.vars_info_per_scope_id[3] end test "binding in function call" do @@ -226,17 +291,9 @@ defmodule ElixirSense.Core.MetadataBuilderTest do {:y, nil} ] - if Version.match?(System.version(), ">= 1.17.0-dev") and @compiler do - assert [ - %VarInfo{name: :y, positions: [{1, 1}, {2, 11}, {3, 11}]} - ] = state |> get_line_vars(4) - else - # TODO this is wrong - assert [ - %VarInfo{name: :y, positions: [{1, 1}, {2, 11}]}, - %VarInfo{name: :y, positions: [{3, 11}]} - ] = state |> get_line_vars(4) - end + assert [ + %VarInfo{name: :y, positions: [{1, 1}, {2, 11}, {3, 11}]} + ] = state |> get_line_vars(4) end test "undefined usage" do @@ -355,12 +412,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert Map.has_key?(state.lines_to_env[4].versioned_vars, {:cde, nil}) - # TODO is it OK - assert ([ - %VarInfo{name: :cde, positions: [{1, 1}], scope_id: scope_id_1}, - %VarInfo{name: :cde, positions: [{3, 3}], scope_id: scope_id_2} - ] - when scope_id_1 != scope_id_2) = state |> get_line_vars(4) + assert [ + # %VarInfo{name: :cde, positions: [{1, 1}], scope_id: scope_id_1}, + %VarInfo{name: :cde, positions: [{3, 3}]} + ] = state |> get_line_vars(4) assert Map.has_key?(state.lines_to_env[6].versioned_vars, {:cde, nil}) @@ -502,96 +557,44 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - if Version.match?(System.version(), ">= 1.17.0-dev") and @compiler do - assert Map.keys(state.lines_to_env[1].versioned_vars) == [] - assert [] = state |> get_line_vars(1) - - assert Map.keys(state.lines_to_env[2].versioned_vars) == [{:abc, nil}] - - assert [ - %VarInfo{name: :abc, positions: [{1, 6}]} - ] = state |> get_line_vars(2) - - assert Map.keys(state.lines_to_env[3].versioned_vars) == [{:abc, nil}, {:cde, nil}] - - assert [ - %VarInfo{name: :abc, positions: [{1, 6}]}, - %VarInfo{name: :cde, positions: [{2, 3}]} - ] = state |> get_line_vars(3) - - assert Map.keys(state.lines_to_env[5].versioned_vars) == [ - {:abc, nil}, - {:cde, nil}, - {:z, nil} - ] - - assert [ - %VarInfo{name: :abc, positions: [{1, 6}, {4, 7}]}, - %VarInfo{name: :cde, positions: [{2, 3}, {4, 13}]}, - %VarInfo{name: :z, positions: [{4, 3}]} - ] = state |> get_line_vars(5) + assert Map.keys(state.lines_to_env[1].versioned_vars) == [] + assert [] = state |> get_line_vars(1) - assert Map.keys(state.lines_to_env[9].versioned_vars) == [{:c, nil}, {:other, nil}] + assert Map.keys(state.lines_to_env[2].versioned_vars) == [{:abc, nil}] - assert [ - %VarInfo{name: :c, positions: [{8, 5}]}, - %VarInfo{name: :other, positions: [{7, 3}]} - ] = state |> get_line_vars(9) - - assert Map.keys(state.lines_to_env[11].versioned_vars) == [] - - assert [] = state |> get_line_vars(11) - else - assert Map.keys(state.lines_to_env[1].versioned_vars) == [{:abc, nil}] - - assert [ - %VarInfo{name: :abc, positions: [{1, 6}]} - ] = state |> get_line_vars(1) + assert [ + %VarInfo{name: :abc, positions: [{1, 6}]} + ] = state |> get_line_vars(2) - assert Map.keys(state.lines_to_env[2].versioned_vars) == [{:abc, nil}, {:cde, nil}] + assert Map.keys(state.lines_to_env[3].versioned_vars) == [{:abc, nil}, {:cde, nil}] - assert [ - %VarInfo{name: :abc, positions: [{1, 6}]}, - %VarInfo{name: :cde, positions: [{2, 3}]} - ] = state |> get_line_vars(2) + assert [ + %VarInfo{name: :abc, positions: [{1, 6}]}, + %VarInfo{name: :cde, positions: [{2, 3}]} + ] = state |> get_line_vars(3) - assert Map.keys(state.lines_to_env[3].versioned_vars) == [{:abc, nil}, {:cde, nil}] + assert Map.keys(state.lines_to_env[5].versioned_vars) == [ + {:abc, nil}, + {:cde, nil}, + {:z, nil} + ] - assert [ - %VarInfo{name: :abc, positions: [{1, 6}]}, - %VarInfo{name: :cde, positions: [{2, 3}]} - ] = state |> get_line_vars(3) + assert [ + %VarInfo{name: :abc, positions: [{1, 6}, {4, 7}]}, + %VarInfo{name: :cde, positions: [{2, 3}, {4, 13}]}, + %VarInfo{name: :z, positions: [{4, 3}]} + ] = state |> get_line_vars(5) - assert Map.keys(state.lines_to_env[5].versioned_vars) == [ - {:abc, nil}, - {:cde, nil}, - {:z, nil} - ] + assert Map.keys(state.lines_to_env[9].versioned_vars) == [{:c, nil}, {:other, nil}] - assert [ - %VarInfo{name: :abc, positions: [{1, 6}, {4, 7}]}, - %VarInfo{name: :cde, positions: [{2, 3}, {4, 13}]}, - %VarInfo{name: :z, positions: [{4, 3}]} - ] = state |> get_line_vars(5) - - # TODO this is quite wrong - assert Map.keys(state.lines_to_env[9].versioned_vars) == [ - {:abc, nil}, - {:c, nil}, - {:cde, nil}, - {:other, nil} - ] + assert [ + %VarInfo{name: :c, positions: [{8, 5}]}, + %VarInfo{name: :other, positions: [{7, 3}]} + ] = state |> get_line_vars(9) - assert [ - %VarInfo{name: :abc, positions: [{1, 6}, {4, 7}]}, - %VarInfo{name: :c, positions: [{8, 5}]}, - %VarInfo{name: :cde, positions: [{2, 3}, {4, 13}]}, - %VarInfo{name: :other, positions: [{7, 3}]} - ] = state |> get_line_vars(9) + assert Map.keys(state.lines_to_env[11].versioned_vars) == [] - assert Map.keys(state.lines_to_env[11].versioned_vars) == [] - assert [] = state |> get_line_vars(11) - end + assert [] = state |> get_line_vars(11) end test "for" do @@ -606,80 +609,48 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - if Version.match?(System.version(), ">= 1.17.0-dev") and @compiler do - assert Map.keys(state.lines_to_env[1].versioned_vars) == [] - assert [] = state |> get_line_vars(3) - - assert Map.keys(state.lines_to_env[2].versioned_vars) == [{:abc, nil}] - - assert [ - %VarInfo{name: :abc, positions: [{1, 5}]} - ] = state |> get_line_vars(2) - - assert Map.keys(state.lines_to_env[4].versioned_vars) == [ - {:abc, nil}, - {:cde, nil}, - {:z, nil} - ] - - assert [ - %VarInfo{name: :abc, positions: [{1, 5}]}, - %VarInfo{name: :cde, positions: [{2, 3}]}, - %VarInfo{name: :z, positions: [{3, 3}]} - ] = state |> get_line_vars(4) + assert Map.keys(state.lines_to_env[1].versioned_vars) == [] + assert [] = state |> get_line_vars(1) - assert Map.keys(state.lines_to_env[6].versioned_vars) == [] - assert [] = state |> get_line_vars(3) - else - assert Map.keys(state.lines_to_env[1].versioned_vars) == [{:abc, nil}] - - assert [ - %VarInfo{name: :abc, positions: [{1, 5}]} - ] = state |> get_line_vars(1) + assert Map.keys(state.lines_to_env[2].versioned_vars) == [{:abc, nil}] - assert Map.keys(state.lines_to_env[2].versioned_vars) == [{:abc, nil}, {:cde, nil}] - - assert [ - %VarInfo{name: :abc, positions: [{1, 5}]}, - %VarInfo{name: :cde, positions: [{2, 3}]} - ] = state |> get_line_vars(2) + assert [ + %VarInfo{name: :abc, positions: [{1, 5}]} + ] = state |> get_line_vars(2) - assert Map.keys(state.lines_to_env[4].versioned_vars) == [ - {:abc, nil}, - {:cde, nil}, - {:z, nil} - ] + assert Map.keys(state.lines_to_env[4].versioned_vars) == [ + {:abc, nil}, + {:cde, nil}, + {:z, nil} + ] - assert [ - %VarInfo{name: :abc, positions: [{1, 5}]}, - %VarInfo{name: :cde, positions: [{2, 3}]}, - %VarInfo{name: :z, positions: [{3, 3}]} - ] = state |> get_line_vars(4) + assert [ + %VarInfo{name: :abc, positions: [{1, 5}]}, + %VarInfo{name: :cde, positions: [{2, 3}]}, + %VarInfo{name: :z, positions: [{3, 3}]} + ] = state |> get_line_vars(4) - assert Map.keys(state.lines_to_env[6].versioned_vars) == [] - assert [] = state |> get_line_vars(6) - end + assert Map.keys(state.lines_to_env[6].versioned_vars) == [] + assert [] = state |> get_line_vars(6) end - if Version.match?(System.version(), ">= 1.17.0-dev") and @compiler do - test "for bitstring" do - state = - """ - for <> do - record_env() - end + test "for bitstring" do + state = + """ + for <> do record_env() - """ - |> string_to_state + end + record_env() + """ + |> string_to_state - assert Map.keys(state.lines_to_env[2].versioned_vars) == [{:b, nil}, {:g, nil}, {:r, nil}] + assert Map.keys(state.lines_to_env[2].versioned_vars) == [{:b, nil}, {:g, nil}, {:r, nil}] - assert [ - %VarInfo{name: :b, positions: [{1, 19}]}, - %VarInfo{name: :g, positions: [{1, 13}]}, - %VarInfo{name: :r, positions: [{1, 7}]} - ] = state |> get_line_vars(2) - end + assert [ + %VarInfo{name: :b, positions: [{1, 19}]}, + %VarInfo{name: :g, positions: [{1, 13}]}, + %VarInfo{name: :r, positions: [{1, 7}]} + ] = state |> get_line_vars(2) end test "for assignment" do @@ -801,11 +772,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert Map.keys(state.lines_to_env[5].versioned_vars) == [{:y, nil}, {:z, nil}] - # TODO sort? assert [ %VarInfo{name: :y, positions: [{4, 3}]}, %VarInfo{name: :z, positions: [{4, 6}, {4, 24}]} - ] = state |> get_line_vars(5) |> Enum.sort_by(& &1.name) + ] = state |> get_line_vars(5) assert Map.keys(state.lines_to_env[7].versioned_vars) == [{:a, nil}] @@ -877,11 +847,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert Map.keys(state.lines_to_env[4].versioned_vars) == [{:abc, nil}] - assert ([ - %VarInfo{name: :abc, positions: [{1, 1}], scope_id: scope_id_1}, - %VarInfo{name: :abc, positions: [{3, 3}], scope_id: scope_id_2} - ] - when scope_id_1 != scope_id_2) = state |> get_line_vars(4) + assert [ + # %VarInfo{name: :abc, positions: [{1, 1}], scope_id: scope_id_1}, + %VarInfo{name: :abc, positions: [{3, 3}]} + ] = state |> get_line_vars(4) assert Map.keys(state.lines_to_env[6].versioned_vars) == [{:abc, nil}] @@ -975,11 +944,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert Map.has_key?(state.lines_to_env[5].versioned_vars, {:abc, nil}) - if Version.match?(System.version(), ">= 1.17.0-dev") and @compiler do - assert [%VarInfo{name: :abc, positions: [{1, 1}, {3, 11}]}] = state |> get_line_vars(5) - else - assert [%VarInfo{name: :abc, positions: [{1, 1}]}] = state |> get_line_vars(5) - end + assert [%VarInfo{name: :abc, positions: [{1, 1}, {3, 11}]}] = state |> get_line_vars(5) end test "in quote unquote_splicing" do @@ -998,16 +963,59 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert Map.has_key?(state.lines_to_env[8].versioned_vars, {:abc, nil}) - if Version.match?(System.version(), ">= 1.17.0-dev") and @compiler do - assert [ - %VarInfo{ - name: :abc, - positions: [{1, 1}, {3, 20}, {4, 21}, {4, 44}, {5, 25}, {6, 21}] - } - ] = state |> get_line_vars(8) - else - assert [%VarInfo{name: :abc, positions: [{1, 1}]}] = state |> get_line_vars(8) - end + assert [ + %VarInfo{ + name: :abc, + positions: [{1, 1}, {3, 20}, {4, 21}, {4, 44}, {5, 25}, {6, 21}] + } + ] = state |> get_line_vars(8) + end + + test "in unquote fragment" do + state = + """ + defmodule MyModuleWithFuns do + kv = [foo: 1, bar: 2] |> IO.inspect + Enum.each(kv, fn {k, v} -> + @spec unquote(k)() :: unquote(v) + @type unquote(k)() :: unquote(v) + defdelegate unquote(k)(), to: Foo + def unquote(k)() do + unquote(v) + record_env() + end + end) + + keys = [{:foo, [], nil}, {:bar, [], nil}] + @spec foo_splicing(unquote_splicing(keys)) :: :ok + @type foo_splicing(unquote_splicing(keys)) :: :ok + defdelegate foo_splicing(unquote_splicing(keys)), to: Foo + def foo_splicing(unquote_splicing(keys)) do + record_env() + end + end + """ + |> string_to_state + + assert Map.keys(state.lines_to_env[9].versioned_vars) == [{:k, nil}, {:kv, nil}, {:v, nil}] + + # TODO defquard on 1.18 + assert [ + %VarInfo{name: :k, positions: [{3, 21}, {4, 19}, {5, 19}, {6, 25}, {7, 17}]}, + %VarInfo{name: :kv, positions: [{2, 3}, {3, 13}]}, + %VarInfo{name: :v, positions: [{3, 24}, {4, 35}, {5, 35}, {8, 15}]} + ] = state |> get_line_vars(9) + + assert Map.keys(state.lines_to_env[18].versioned_vars) == [keys: nil, kv: nil] + + # TODO defquard on 1.18 + assert [ + %VarInfo{ + name: :keys, + positions: [{13, 3}, {14, 39}, {15, 39}, {16, 45}, {17, 37}] + }, + %VarInfo{name: :kv, positions: [{2, 3}, {3, 13}]} + ] = state |> get_line_vars(18) end test "in capture" do @@ -1024,34 +1032,18 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - if Version.match?(System.version(), ">= 1.17.0-dev") and @compiler do - assert Map.keys(state.lines_to_env[6].versioned_vars) == [{:"&1", nil}, {:abc, nil}] - - assert [ - %VarInfo{name: :"&1", positions: [{3, 3}]}, - %VarInfo{name: :abc, positions: [{1, 1}, {4, 3}]} - ] = state |> get_line_vars(6) - - assert Map.keys(state.lines_to_env[8].versioned_vars) == [{:abc, nil}] - - assert [ - %VarInfo{name: :abc, positions: [{1, 1}, {4, 3}]} - ] = state |> get_line_vars(8) - else - assert Map.keys(state.lines_to_env[6].versioned_vars) == [{:abc, nil}, {:cde, nil}] + assert [{:"&1", _}, {:abc, nil}] = Map.keys(state.lines_to_env[6].versioned_vars) - assert [ - %VarInfo{name: :abc, positions: [{1, 1}, {4, 3}]}, - %VarInfo{name: :cde, positions: [{5, 3}]} - ] = state |> get_line_vars(6) + assert [ + %VarInfo{name: :"&1", positions: [{3, 3}]}, + %VarInfo{name: :abc, positions: [{1, 1}, {4, 3}]} + ] = state |> get_line_vars(6) - assert Map.keys(state.lines_to_env[8].versioned_vars) == [{:abc, nil}, {:cde, nil}] + assert Map.keys(state.lines_to_env[8].versioned_vars) == [{:abc, nil}] - assert [ - %VarInfo{name: :abc, positions: [{1, 1}, {4, 3}]}, - %VarInfo{name: :cde, positions: [{5, 3}]} - ] = state |> get_line_vars(8) - end + assert [ + %VarInfo{name: :abc, positions: [{1, 1}, {4, 3}]} + ] = state |> get_line_vars(8) end test "module body" do @@ -1214,87 +1206,269 @@ defmodule ElixirSense.Core.MetadataBuilderTest do %VarInfo{name: :abc, positions: [{2, 11}, {3, 10}]} ] = state |> get_line_vars(3) end - end - @tag requires_source: true - test "build metadata from kernel.ex" do - assert get_subject_definition_line(Kernel, :defmodule, 2) =~ - "defmacro defmodule(alias, do_block)" - end + test "variables hygiene" do + state = + """ + defmodule MyModule do + import ElixirSenseExample.Math + def func do + squared(5) + IO.puts "" + end + end + """ + |> string_to_state - @tag requires_source: true - test "build metadata from kernel/special_forms.ex" do - assert get_subject_definition_line(Kernel.SpecialForms, :alias, 2) =~ - "defmacro alias(module, opts)" - end + assert [] == state |> get_line_vars(5) + end - test "build_metadata from a module" do - assert get_subject_definition_line( - ElixirSenseExample.ModuleWithFunctions, - :function_arity_zero, - 0 - ) =~ "def function_arity_zero" - end + test "variables are added to environment" do + state = + """ + defmodule MyModule do + def func do + var = :my_var + IO.puts "" + end + end + """ + |> string_to_state - test "closes all scopes" do - state = - """ - """ - |> string_to_state + assert [%VarInfo{scope_id: scope_id}] = state |> get_line_vars(4) + assert [%VarInfo{name: :var}] = state.vars_info_per_scope_id[scope_id] |> Map.values() + end - assert state.module == [] - assert state.scopes == [] - assert state.functions == [] - assert state.macros == [] - assert state.requires == [] - assert state.aliases == [] - assert state.attributes == [] - assert state.protocols == [] - assert state.scope_attributes == [] - assert state.vars_info == [] - assert state.scope_vars_info == [] - assert state.scope_ids == [] + test "vars defined inside a function without params" do + state = + """ + defmodule MyModule do + var_out1 = 1 + def func do + var_in1 = 1 + var_in2 = 1 + IO.puts "" + end + var_out2 = 1 + IO.puts "" + end + """ + |> string_to_state + + assert [ + %VarInfo{name: :var_in1, positions: [{4, 5}], scope_id: scope_id}, + %VarInfo{name: :var_in2, positions: [{5, 5}], scope_id: scope_id} + ] = state |> get_line_vars(6) + end end - describe "moduledoc positions" do - test "moduledoc heredoc version" do + describe "vars in ex_unit" do + test "variables are added to environment in ex_unit test" do state = """ - defmodule Outer do - @moduledoc \"\"\" - This is the here doc version - \"\"\" - defmodule Inner do - @moduledoc \"\"\" - This is the Inner modules moduledoc - \"\"\" + defmodule MyModuleTests do + use ExUnit.Case, async: true + IO.puts("") + test "it does what I want", %{some: some} do + IO.puts("") + end - foo() + describe "this" do + test "too does what I want" do + IO.puts("") + end end + + test "is not implemented" end """ |> string_to_state - assert %{Outer => {5, 3}, Outer.Inner => {9, 5}} = state.moduledoc_positions + assert [%VarInfo{name: :some}] = state |> get_line_vars(5) + + assert Map.has_key?( + state.mods_funs_to_positions, + {MyModuleTests, :"test it does what I want", 1} + ) + + assert Map.has_key?( + state.mods_funs_to_positions, + {MyModuleTests, :"test this too does what I want", 1} + ) + + assert Map.has_key?( + state.mods_funs_to_positions, + {MyModuleTests, :"test is not implemented", 1} + ) end - test "moduledoc boolean version" do + test "variables are added to environment in ex_unit setup" do state = """ - defmodule Outer do - @moduledoc false + defmodule MyModuleTests do + use ExUnit.Case, async: true - foo() + setup_all %{some: some} do + IO.puts("") + end + + setup %{some: other} do + IO.puts("") + end + + setup do + IO.puts("") + end + + setup :clean_up_tmp_directory + + setup [:clean_up_tmp_directory, :another_setup] + + setup {MyModule, :my_setup_function} end """ |> string_to_state - assert %{Outer => {3, 3}} = state.moduledoc_positions + assert [%VarInfo{name: :some}] = state |> get_line_vars(5) + + assert [%VarInfo{name: :other}] = state |> get_line_vars(9) + + # we do not generate defs - ExUnit.Callbacks.__setup__ is too complicated and generates def names with counters, e.g. + # :"__ex_unit_setup_#{counter}_#{length(setup)}" end end - test "module attributes" do - state = + describe "typespec vars" do + test "registers type parameters" do + state = + """ + defmodule A do + @type some(p) :: {p, list(p), integer} + end + """ + |> string_to_state + + assert [ + %VarInfo{name: :p, positions: [{2, 14}, {2, 21}, {2, 29}]} + ] = state.vars_info_per_scope_id[2] |> Map.values() + end + + test "registers spec parameters" do + state = + """ + defmodule A do + @callback some(p) :: {p, list(p), integer, q} when p: integer, q: {p} + end + """ + |> string_to_state + + # no position in guard, elixir parses guards as keyword list so p is an atom with no metadata + # we use when meta instead so the position is not exact... + assert [ + %VarInfo{name: :p, positions: [{2, 49}, {2, 18}, {2, 25}, {2, 33}, {2, 70}]}, + %VarInfo{name: :q, positions: [{2, 49}, {2, 46}]} + ] = state.vars_info_per_scope_id[2] |> Map.values() + end + + test "does not register annotated spec params as type variables" do + state = + """ + defmodule A do + @callback some(p :: integer) :: integer + end + """ + |> string_to_state + + assert %{} == state.vars_info_per_scope_id[2] + end + + test "does not register annotated type elements as variables" do + state = + """ + defmodule A do + @type color :: {red :: integer, green :: integer, blue :: integer} + end + """ + |> string_to_state + + assert %{} == state.vars_info_per_scope_id[2] + end + end + + @tag requires_source: true + test "build metadata from kernel.ex" do + assert get_subject_definition_line(Kernel, :defmodule, 2) =~ + "defmacro defmodule(alias, do_block)" + end + + @tag requires_source: true + test "build metadata from kernel/special_forms.ex" do + assert get_subject_definition_line(Kernel.SpecialForms, :alias, 2) =~ + "defmacro alias(module, opts)" + end + + test "build_metadata from a module" do + assert get_subject_definition_line( + ElixirSenseExample.ModuleWithFunctions, + :function_arity_zero, + 0 + ) =~ "def function_arity_zero" + end + + test "closes all scopes" do + state = + """ + """ + |> string_to_state + + assert state.attributes == [] + assert state.scope_attributes == [] + assert state.vars_info == [] + assert state.scope_ids == [] + assert state.doc_context == [] + assert state.typedoc_context == [] + assert state.optional_callbacks_context == [] + end + + describe "moduledoc positions" do + test "moduledoc heredoc version" do + state = + """ + defmodule Outer do + @moduledoc \"\"\" + This is the here doc version + \"\"\" + defmodule Inner do + @moduledoc \"\"\" + This is the Inner modules moduledoc + \"\"\" + + foo() + end + end + """ + |> string_to_state + + assert %{Outer => {5, 3}, Outer.Inner => {9, 5}} = state.moduledoc_positions + end + + test "moduledoc boolean version" do + state = + """ + defmodule Outer do + @moduledoc false + + foo() + end + """ + |> string_to_state + + assert %{Outer => {3, 3}} = state.moduledoc_positions + end + end + + test "module attributes" do + state = """ defmodule MyModule do @myattribute String @@ -1347,7 +1521,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] = get_line_attributes(state, 9) end - if @attribute_binding_support do + describe "binding" do test "module attributes binding" do state = """ @@ -1409,1067 +1583,1207 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } ] end - end - if @binding_support do - describe "binding" do - test "module attributes rebinding" do - state = - """ - defmodule MyModule do - @myattribute String - @myattribute List + test "module attributes rebinding" do + state = + """ + defmodule MyModule do + @myattribute String + IO.puts "" + @myattribute List + @myattribute + IO.puts "" + def a do @myattribute - IO.puts "" - def a do - @myattribute - end - IO.puts "" end - """ - |> string_to_state + IO.puts "" + end + """ + |> string_to_state - assert get_line_attributes(state, 5) == [ - %AttributeInfo{ - name: :myattribute, - positions: [{2, 3}, {3, 3}, {4, 3}], - type: {:atom, List} - } - ] + assert get_line_attributes(state, 3) == [ + %AttributeInfo{ + name: :myattribute, + positions: [{2, 3}], + type: {:atom, String} + } + ] - assert get_line_attributes(state, 9) == [ - %AttributeInfo{ - name: :myattribute, - positions: [{2, 3}, {3, 3}, {4, 3}, {7, 5}], - type: {:atom, List} - } - ] - end + assert get_line_attributes(state, 6) == [ + %AttributeInfo{ + name: :myattribute, + positions: [{2, 3}, {4, 3}, {5, 3}], + type: {:atom, List} + } + ] - test "module attributes value binding" do - state = - """ - defmodule MyModule do - @myattribute %{abc: String} - @some_attr @myattribute - IO.puts "" - end - """ - |> string_to_state - - assert get_line_attributes(state, 4) == [ - %AttributeInfo{ - name: :myattribute, - positions: [{2, 3}, {3, 14}], - type: {:map, [abc: {:atom, String}], nil} - }, - %AttributeInfo{ - name: :some_attr, - positions: [{3, 3}], - type: {:attribute, :myattribute} - } - ] - end + assert get_line_attributes(state, 10) == [ + %AttributeInfo{ + name: :myattribute, + positions: [{2, 3}, {4, 3}, {5, 3}, {8, 5}], + type: {:atom, List} + } + ] + end - test "module attributes value binding to and from variables" do - state = - """ - defmodule MyModule do - @myattribute %{abc: String} - var = @myattribute - @other var - IO.puts "" - end - """ - |> string_to_state - - assert get_line_attributes(state, 5) == [ - %AttributeInfo{ - name: :myattribute, - positions: [{2, 3}, {3, 9}], - type: {:map, [abc: {:atom, String}], nil} - }, - %AttributeInfo{ - name: :other, - positions: [{4, 3}], - type: {:variable, :var} - } - ] + test "module attributes value binding" do + state = + """ + defmodule MyModule do + @myattribute %{abc: String} + @some_attr @myattribute + IO.puts "" + end + """ + |> string_to_state - assert [ - %VarInfo{name: :var, type: {:attribute, :myattribute}} - ] = state |> get_line_vars(5) - end + assert get_line_attributes(state, 4) == [ + %AttributeInfo{ + name: :myattribute, + positions: [{2, 3}, {3, 14}], + type: {:map, [abc: {:atom, String}], nil} + }, + %AttributeInfo{ + name: :some_attr, + positions: [{3, 3}], + type: {:attribute, :myattribute} + } + ] + end - test "variable rebinding" do - state = - """ - def my() do - abc = 1 - some(abc) - abc = %Abc{cde: 1} - IO.puts "" - end - """ - |> string_to_state - - assert [ - %State.VarInfo{ - name: :abc, - type: {:integer, 1}, - is_definition: true, - positions: [{2, 3}, {3, 8}], - scope_id: 2 - }, - %State.VarInfo{ - name: :abc, - type: {:struct, [cde: {:integer, 1}], {:atom, Abc}, nil}, - is_definition: true, - positions: [{4, 3}], - scope_id: 2 - } - ] = state |> get_line_vars(5) - end + test "variable binding simple case" do + state = + """ + var = :my_var + IO.puts("") + """ + |> string_to_state - test "tuple destructuring" do - state = - """ - defmodule MyModule do - @myattribute {:ok, %{abc: nil}} - {:ok, var} = @myattribute - other = elem(@myattribute, 0) - IO.puts - q = {:a, :b, :c} - {_, _, q1} = q - IO.puts - end - """ - |> string_to_state + assert [%VarInfo{type: {:atom, :my_var}}] = state |> get_line_vars(2) + end - assert get_line_attributes(state, 4) == [ - %AttributeInfo{ - name: :myattribute, - positions: [{2, 3}, {3, 16}, {4, 16}], - type: {:tuple, 2, [{:atom, :ok}, {:map, [abc: {:atom, nil}], nil}]} - } - ] + test "variable binding simple case match context" do + state = + """ + case x do + var = :my_var -> + IO.puts("") + end + """ + |> string_to_state - assert [ - %VarInfo{ - name: :other, - type: {:local_call, :elem, [{:attribute, :myattribute}, {:integer, 0}]} - }, - %VarInfo{ - name: :var, - type: - {:tuple_nth, - {:intersection, - [{:attribute, :myattribute}, {:tuple, 2, [{:atom, :ok}, nil]}]}, 1} - } - ] = state |> get_line_vars(4) - - assert [ - %VarInfo{ - name: :q, - type: {:tuple, 3, [{:atom, :a}, {:atom, :b}, {:atom, :c}]} - }, - %VarInfo{ - name: :q1, - type: - {:tuple_nth, - {:intersection, - [{:variable, :q}, {:tuple, 3, [{:variable, :_}, {:variable, :_}, nil]}]}, - 2} - } - ] = - state - |> get_line_vars(8) - |> Enum.filter(&(&1.name |> Atom.to_string() |> String.starts_with?("q"))) + if Version.match?(System.version(), "< 1.15.0") do + assert [%VarInfo{type: {:intersection, [{:atom, :my_var}, {:local_call, :x, []}]}}] = + state |> get_line_vars(3) + else + assert [%VarInfo{type: {:atom, :my_var}}] = state |> get_line_vars(3) end + end - test "list destructuring" do - state = - """ - defmodule MyModule do - @a [] - @myattribute [:ok, :error, :other] - @other1 [:some, :error | @myattribute] - @other2 [:some | @myattribute] - [var, _var1, _var2] = @myattribute - [other | rest] = @myattribute - [a] = @other - [b] = [] - IO.puts - end - """ - |> string_to_state - - assert get_line_attributes(state, 5) == [ - %AttributeInfo{ - name: :a, - positions: [{2, 3}], - type: {:list, :empty} - }, - %AttributeInfo{ - name: :myattribute, - positions: [{3, 3}, {4, 28}, {5, 20}], - type: {:list, {:atom, :ok}} - }, - %AttributeInfo{ - name: :other1, - positions: [{4, 3}], - type: {:list, {:atom, :some}} - }, - %AttributeInfo{name: :other2, positions: [{5, 3}], type: {:list, {:atom, :some}}} - ] + test "variable binding simple case match context reverse order" do + state = + """ + case x do + :my_var = var -> + IO.puts("") + end + """ + |> string_to_state - assert [ - %VarInfo{ - name: :_var1, - type: {:list_head, {:list_tail, {:attribute, :myattribute}}} - }, - %VarInfo{ - name: :_var2, - type: {:list_head, {:list_tail, {:list_tail, {:attribute, :myattribute}}}} - }, - %VarInfo{ - name: :a, - type: {:list_head, {:attribute, :other}} - }, - %VarInfo{ - name: :b, - type: {:list_head, {:list, :empty}} - }, - %VarInfo{name: :other, type: {:list_head, {:attribute, :myattribute}}}, - %VarInfo{name: :rest, type: {:list_tail, {:attribute, :myattribute}}}, - %VarInfo{name: :var, type: {:list_head, {:attribute, :myattribute}}} - ] = state |> get_line_vars(10) + if Version.match?(System.version(), "< 1.15.0") do + assert [%VarInfo{type: {:intersection, [{:atom, :my_var}, {:local_call, :x, []}]}}] = + state |> get_line_vars(3) + else + assert [%VarInfo{type: {:atom, :my_var}}] = state |> get_line_vars(3) end + end - test "list destructuring for" do - state = - """ - defmodule MyModule do - @myattribute [:ok, :error, :other] - for a <- @myattribute do - b = a - IO.puts - end + test "variable binding simple case match context guard" do + state = + """ + receive do + [v = :ok, var] when is_map(var) -> + IO.puts("") + end + """ + |> string_to_state - for a <- @myattribute, a1 = @myattribute, a2 <- a1 do - b = a - IO.puts - end - end - """ - |> string_to_state + assert [%VarInfo{type: {:atom, :ok}}, %VarInfo{type: {:map, [], nil}}] = + state |> get_line_vars(3) + end - assert [ - %VarInfo{name: :a, type: {:for_expression, {:attribute, :myattribute}}}, - %VarInfo{name: :b, type: {:variable, :a}} - ] = state |> get_line_vars(5) + test "module attributes value binding to and from variables" do + state = + """ + defmodule MyModule do + @myattribute %{abc: String} + var = @myattribute + @other var + IO.puts "" + end + """ + |> string_to_state - assert [ - %VarInfo{name: :a, type: {:for_expression, {:attribute, :myattribute}}}, - %VarInfo{name: :a1, type: {:attribute, :myattribute}}, - %VarInfo{name: :a2, type: {:for_expression, {:variable, :a1}}}, - %VarInfo{name: :b, type: {:variable, :a}} - ] = state |> get_line_vars(10) - end + assert get_line_attributes(state, 5) == [ + %AttributeInfo{ + name: :myattribute, + positions: [{2, 3}, {3, 9}], + type: {:map, [abc: {:atom, String}], nil} + }, + %AttributeInfo{ + name: :other, + positions: [{4, 3}], + type: {:variable, :var, 0} + } + ] - test "map destructuring" do - state = - """ - defmodule MyModule do - @a %{} - @myattribute %{ok: :a, error: b, other: :c} - @other %{"a" => :a, "b" => b} - %{error: var1} = @myattribute - %{"a" => var2} = @other - IO.puts - end - """ - |> string_to_state - - assert [ - %VarInfo{ - name: :var1, - type: {:map_key, {:attribute, :myattribute}, {:atom, :error}} - }, - # TODO not atom keys currently not supported - %VarInfo{ - name: :var2, - type: {:map_key, {:attribute, :other}, nil} - } - ] = state |> get_line_vars(7) - end + assert [ + %VarInfo{name: :var, type: {:attribute, :myattribute}} + ] = state |> get_line_vars(5) + end - test "map destructuring for" do - state = - """ - defmodule MyModule do - @myattribute %{ok: :a, error: b, other: :c} - for {k, v} <- @myattribute do - IO.puts - end - end - """ - |> string_to_state + test "variable rebinding" do + state = + """ + abc = 1 + some(abc) + abc = %Abc{cde: 1} + IO.puts "" + """ + |> string_to_state - assert [ - %VarInfo{ - name: :k, - type: { - :tuple_nth, - { - :intersection, - [ - {:for_expression, {:attribute, :myattribute}}, - {:tuple, 2, [nil, {:variable, :v}]} - ] - }, - 0 - } - }, - %VarInfo{ - name: :v, - type: { - :tuple_nth, - { - :intersection, - [ - {:for_expression, {:attribute, :myattribute}}, - {:tuple, 2, [{:variable, :k}, nil]} - ] - }, - 1 - } - } - ] = state |> get_line_vars(4) - end + assert [ + %State.VarInfo{ + name: :abc, + type: {:struct, [cde: {:integer, 1}], {:atom, Abc}, nil}, + positions: [{3, 1}] + } + ] = state |> get_line_vars(4) + end - test "struct destructuring" do - state = - """ - defmodule MyModule do - @a %My{} - @myattribute %My{ok: :a, error: b, other: :c} - %{error: var1} = @myattribute - %My{error: other} = @myattribute - IO.puts - end - """ - |> string_to_state - - assert [ - %VarInfo{ - name: :other, - type: {:map_key, {:attribute, :myattribute}, {:atom, :error}} - }, - %VarInfo{ - name: :var1, - type: {:map_key, {:attribute, :myattribute}, {:atom, :error}} + test "tuple destructuring" do + state = + """ + defmodule MyModule do + @myattribute {:ok, %{abc: nil}} + {:ok, var} = @myattribute + other = elem(@myattribute, 0) + IO.puts + q = {:a, :b, :c} + {_, _, q1} = q + IO.puts + end + """ + |> string_to_state + + assert get_line_attributes(state, 4) == [ + %AttributeInfo{ + name: :myattribute, + positions: [{2, 3}, {3, 16}], + type: {:tuple, 2, [{:atom, :ok}, {:map, [abc: {:atom, nil}], nil}]} + } + ] + + assert [ + %VarInfo{ + name: :other, + type: { + :call, + {:atom, :erlang}, + :element, + [{:integer, 1}, {:attribute, :myattribute}] } - ] = state |> get_line_vars(6) - end + }, + %VarInfo{ + name: :var, + type: {:tuple_nth, {:attribute, :myattribute}, 1} + } + ] = state |> get_line_vars(5) - test "binding in with expression" do - state = - """ - defmodule MyModule do - @myattribute [:ok, :error, :other] - with a <- @myattribute do - b = a - IO.puts - end + assert [ + %VarInfo{ + name: :q, + type: {:tuple, 3, [{:atom, :a}, {:atom, :b}, {:atom, :c}]} + }, + %VarInfo{ + name: :q1, + type: {:tuple_nth, {:variable, :q, 2}, 2} + } + ] = + state + |> get_line_vars(8) + |> Enum.filter(&(&1.name |> Atom.to_string() |> String.starts_with?("q"))) + end + + test "list destructuring" do + state = + """ + defmodule MyModule do + @a [] + @myattribute [:ok, :error, :other] + @other1 [:some, :error | @myattribute] + @other2 [:some | @myattribute] + [var, _var1, _var2] = @myattribute + [other | rest] = @myattribute + [a] = @other + [b] = [] + IO.puts + end + """ + |> string_to_state + + assert get_line_attributes(state, 5) == [ + %AttributeInfo{ + name: :a, + positions: [{2, 3}], + type: {:list, :empty} + }, + %AttributeInfo{ + name: :myattribute, + positions: [{3, 3}, {4, 28}, {5, 20}], + type: {:list, {:atom, :ok}} + }, + %AttributeInfo{ + name: :other1, + positions: [{4, 3}], + type: {:list, {:atom, :some}} + }, + %AttributeInfo{name: :other2, positions: [{5, 3}], type: {:list, {:atom, :some}}} + ] + + assert [ + %VarInfo{ + name: :_var1, + type: {:list_head, {:list_tail, {:attribute, :myattribute}}} + }, + %VarInfo{ + name: :_var2, + type: {:list_head, {:list_tail, {:list_tail, {:attribute, :myattribute}}}} + }, + %VarInfo{ + name: :a, + type: {:list_head, {:attribute, :other}} + }, + %VarInfo{ + name: :b, + type: {:list_head, {:list, :empty}} + }, + %VarInfo{name: :other, type: {:list_head, {:attribute, :myattribute}}}, + %VarInfo{name: :rest, type: {:list_tail, {:attribute, :myattribute}}}, + %VarInfo{name: :var, type: {:list_head, {:attribute, :myattribute}}} + ] = state |> get_line_vars(10) + end + + test "list destructuring for" do + state = + """ + defmodule MyModule do + @myattribute [:ok, :error, :other] + for a <- @myattribute do + b = a + IO.puts end - """ - |> string_to_state - assert [ - %VarInfo{name: :a, type: {:attribute, :myattribute}}, - %VarInfo{name: :b, type: {:variable, :a}} - ] = state |> get_line_vars(5) - end + for a <- @myattribute, a1 = @myattribute, a2 <- a1 do + b = a + IO.puts + end + end + """ + |> string_to_state - test "vars defined inside a function without params" do - state = - """ - defmodule MyModule do - var_out1 = 1 - def func do - var_in1 = 1 - var_in2 = 1 - IO.puts "" - end - var_out2 = 1 - IO.puts "" + assert [ + %VarInfo{name: :a, type: {:for_expression, {:attribute, :myattribute}}}, + %VarInfo{name: :b, type: {:variable, :a, 0}} + ] = state |> get_line_vars(5) + + assert [ + %VarInfo{name: :a, type: {:for_expression, {:attribute, :myattribute}}}, + %VarInfo{name: :a1, type: {:attribute, :myattribute}}, + %VarInfo{name: :a2, type: {:for_expression, {:variable, :a1, 3}}}, + %VarInfo{name: :b, type: {:variable, :a, 2}} + ] = state |> get_line_vars(10) + end + + test "map destructuring" do + state = + """ + defmodule MyModule do + @a %{} + @myattribute %{ok: :a, error: b, other: :c} + @other %{"a" => :a, "b" => b} + %{error: var1} = @myattribute + %{"a" => var2} = @other + IO.puts + end + """ + |> string_to_state + + assert [ + %VarInfo{ + name: :var1, + type: {:map_key, {:attribute, :myattribute}, {:atom, :error}} + }, + # NOTE non atom keys currently not supported + %VarInfo{ + name: :var2, + type: {:map_key, {:attribute, :other}, nil} + } + ] = state |> get_line_vars(7) + end + + test "map destructuring for" do + state = + """ + defmodule MyModule do + @myattribute %{ok: :a, error: b, other: :c} + for {k, v} <- @myattribute do + IO.puts end - """ - |> string_to_state + end + """ + |> string_to_state - assert [ - %VarInfo{name: :var_in1, positions: [{4, 5}], scope_id: 4}, - %VarInfo{name: :var_in2, positions: [{5, 5}], scope_id: 4} - ] = state |> get_line_vars(6) - end + assert [ + %VarInfo{ + name: :k, + type: {:tuple_nth, {:for_expression, {:attribute, :myattribute}}, 0} + }, + %VarInfo{ + name: :v, + type: {:tuple_nth, {:for_expression, {:attribute, :myattribute}}, 1} + } + ] = state |> get_line_vars(4) + end - test "vars binding" do - state = - """ - defmodule MyModule do - def func do - var = String - IO.puts "" - var = Map - IO.puts "" - if abc do - IO.puts "" - var = List - IO.puts "" - var = Enum - IO.puts "" - end + test "struct destructuring" do + state = + """ + defmodule MyModule do + @a %My{} + @myattribute %My{ok: :a, error: b, other: :c} + %{error: var1} = @myattribute + %My{error: other} = @myattribute + IO.puts + end + """ + |> string_to_state + + assert [ + %VarInfo{ + name: :other, + type: {:map_key, {:attribute, :myattribute}, {:atom, :error}} + }, + %VarInfo{ + name: :var1, + type: {:map_key, {:attribute, :myattribute}, {:atom, :error}} + } + ] = state |> get_line_vars(6) + end + + test "binding in with expression" do + state = + """ + defmodule MyModule do + @myattribute [:ok, :error, :other] + with a <- @myattribute do + b = a + IO.puts + end + end + """ + |> string_to_state + + assert [ + %VarInfo{name: :a, type: {:attribute, :myattribute}}, + %VarInfo{name: :b, type: {:variable, :a, 0}} + ] = state |> get_line_vars(5) + end + + test "binding in with expression more complex" do + state = + """ + defmodule MyModule do + @myattribute [:ok, :error, :other] + with a <- @myattribute, + b = Date.utc_now(), + [c | _] <- a do + IO.puts + end + end + """ + |> string_to_state + + assert [ + %VarInfo{name: :a, type: {:attribute, :myattribute}}, + %VarInfo{name: :b, type: {:call, {:atom, Date}, :utc_now, []}}, + %VarInfo{name: :c, type: {:list_head, {:variable, :a, 0}}} + ] = state |> get_line_vars(6) + end + + test "binding in with expression with guard" do + state = + """ + defmodule MyModule do + @myattribute [:ok, :error, :other] + with [a | _] when is_atom(a) <- @myattribute do + IO.puts + end + end + """ + |> string_to_state + + assert [ + %VarInfo{ + name: :a, + type: {:intersection, [:atom, {:list_head, {:attribute, :myattribute}}]} + } + ] = state |> get_line_vars(4) + end + + test "binding in with expression else" do + state = + """ + defmodule MyModule do + @myattribute [:ok, :error, :other] + with a <- @myattribute do + b = a + IO.puts + else + a = :ok -> + IO.puts + end + end + """ + |> string_to_state + + assert [ + %VarInfo{name: :a, type: {:atom, :ok}} + ] = state |> get_line_vars(8) + end + + test "vars binding" do + state = + """ + defmodule MyModule do + def func do + var = String + IO.puts "" + var = Map + IO.puts "" + if abc do IO.puts "" - var = Atom + var = List IO.puts "" - other = var + var = Enum IO.puts "" end + IO.puts "" + var = Atom + IO.puts "" + other = var + IO.puts "" end - """ - |> string_to_state - - assert [%VarInfo{type: {:atom, String}}] = state |> get_line_vars(4) - - assert [%VarInfo{type: {:atom, String}}, %VarInfo{type: {:atom, Map}}] = - state |> get_line_vars(6) - - assert [%VarInfo{type: {:atom, String}}, %VarInfo{type: {:atom, Map}}] = - state |> get_line_vars(8) - - assert [ - %VarInfo{type: {:atom, String}, scope_id: 4}, - %VarInfo{type: {:atom, Map}, scope_id: 4}, - %VarInfo{type: {:atom, List}, scope_id: 5} - ] = state |> get_line_vars(10) - - assert [ - %VarInfo{type: {:atom, String}, scope_id: 4}, - %VarInfo{type: {:atom, Map}, scope_id: 4}, - %VarInfo{type: {:atom, List}, scope_id: 5}, - %VarInfo{type: {:atom, Enum}, scope_id: 5} - ] = state |> get_line_vars(12) - - assert [%VarInfo{type: {:atom, String}}, %VarInfo{type: {:atom, Map}}] = - state |> get_line_vars(14) - - assert [ - %VarInfo{type: {:atom, String}}, - %VarInfo{type: {:atom, Map}}, - %VarInfo{type: {:atom, Atom}} - ] = state |> get_line_vars(16) - - assert [ - %VarInfo{name: :other, type: {:variable, :var}}, - %VarInfo{type: {:atom, String}}, - %VarInfo{type: {:atom, Map}}, - %VarInfo{type: {:atom, Atom}} - ] = state |> get_line_vars(18) - end + end + """ + |> string_to_state - test "variables are added to environment" do - state = - """ - defmodule MyModule do - def func do - var = :my_var - end - end - """ - |> string_to_state + assert [%VarInfo{type: {:atom, String}}] = state |> get_line_vars(4) - assert [%VarInfo{type: {:atom, :my_var}, scope_id: scope_id}] = state |> get_line_vars(3) - assert [%VarInfo{name: :var}] = state.vars_info_per_scope_id[scope_id] - end + assert [%VarInfo{type: {:atom, Map}}] = + state |> get_line_vars(6) - test "variables are added to environment in ex_unit test" do - state = - """ - defmodule MyModuleTests do - use ExUnit.Case, async: true + assert [%VarInfo{type: {:atom, Map}}] = + state |> get_line_vars(8) - test "it does what I want", %{some: some} do - IO.puts("") - end + assert [ + %VarInfo{type: {:atom, List}} + ] = state |> get_line_vars(10) - describe "this" do - test "too does what I want" do - IO.puts("") - end - end + assert [ + %VarInfo{type: {:atom, Enum}} + ] = state |> get_line_vars(12) + + assert [%VarInfo{type: {:atom, Map}}] = + state |> get_line_vars(14) - test "is not implemented" + assert [ + %VarInfo{type: {:atom, Atom}} + ] = state |> get_line_vars(16) + + assert [ + %VarInfo{name: :other, type: {:variable, :var, 5}}, + %VarInfo{type: {:atom, Atom}} + ] = state |> get_line_vars(18) + end + + test "call binding" do + state = + """ + defmodule MyModule do + def remote_calls do + var1 = DateTime.now + var2 = :erlang.now() + var3 = __MODULE__.now(:abc) + var4 = "Etc/UTC" |> DateTime.now + IO.puts "" end - """ - |> string_to_state - assert [%VarInfo{type: nil, scope_id: scope_id}] = state |> get_line_vars(5) - assert [%VarInfo{name: :some}] = state.vars_info_per_scope_id[scope_id] + def local_calls do + var1 = now + var2 = now() + var3 = now(:abc) + var4 = :abc |> now + var5 = :abc |> now(5) + IO.puts "" + end - assert Map.has_key?( - state.mods_funs_to_positions, - {MyModuleTests, :"test it does what I want", 1} - ) + @attr %{qwe: String} + def map_field(var1, abc) do + var1 = var1.abc + var2 = @attr.qwe(0) + var3 = abc.cde.efg + IO.puts "" + end + end + """ + |> string_to_state - assert Map.has_key?( - state.mods_funs_to_positions, - {MyModuleTests, :"test this too does what I want", 1} - ) + assert [ + %VarInfo{name: :var1, type: {:call, {:atom, DateTime}, :now, []}}, + %VarInfo{name: :var2, type: {:call, {:atom, :erlang}, :now, []}}, + %VarInfo{name: :var3, type: {:call, {:atom, MyModule}, :now, [{:atom, :abc}]}}, + %VarInfo{name: :var4, type: {:call, {:atom, DateTime}, :now, [nil]}} + ] = state |> get_line_vars(7) - assert Map.has_key?( - state.mods_funs_to_positions, - {MyModuleTests, :"test is not implemented", 1} - ) + assert [ + %VarInfo{name: :var1, type: maybe_local_call}, + %VarInfo{name: :var2, type: {:local_call, :now, []}}, + %VarInfo{name: :var3, type: {:local_call, :now, [{:atom, :abc}]}}, + %VarInfo{name: :var4, type: {:local_call, :now, [{:atom, :abc}]}}, + %VarInfo{name: :var5, type: {:local_call, :now, [{:atom, :abc}, {:integer, 5}]}} + ] = state |> get_line_vars(16) + + if Version.match?(System.version(), "< 1.15.0") do + assert maybe_local_call == {:local_call, :now, []} + else + assert maybe_local_call == nil end - test "variables are added to environment in ex_unit setup" do - state = - """ - defmodule MyModuleTests do - use ExUnit.Case, async: true + assert [ + %VarInfo{name: :abc, type: nil}, + %VarInfo{name: :var1, type: {:call, {:variable, :var1, 0}, :abc, []}}, + %VarInfo{name: :var2, type: {:call, {:attribute, :attr}, :qwe, [{:integer, 0}]}}, + %VarInfo{ + name: :var3, + type: {:call, {:call, {:variable, :abc, 1}, :cde, []}, :efg, []} + } + ] = state |> get_line_vars(24) + end - setup_all %{some: some} do - IO.puts("") - end + test "map binding" do + state = + """ + defmodule MyModule do + def func do + var = %{asd: 5} + IO.puts "" + var = %{asd: 5, nested: %{wer: "asd"}} + IO.puts "" + var = %{"asd" => "dsds"} + IO.puts "" + var = %{asd: 5, zxc: String} + IO.puts "" + qwe = %{var | asd: 2, zxc: 5} + IO.puts "" + qwe = %{var | asd: 2} + IO.puts "" - setup %{some: other} do - IO.puts("") - end + end + end + """ + |> string_to_state - setup do - IO.puts("") - end + assert [%VarInfo{type: {:map, [asd: {:integer, 5}], nil}}] = state |> get_line_vars(4) - setup :clean_up_tmp_directory + assert [ + %VarInfo{ + type: {:map, [asd: {:integer, 5}, nested: {:map, [wer: nil], nil}], nil} + } + ] = state |> get_line_vars(6) - setup [:clean_up_tmp_directory, :another_setup] + assert [ + %VarInfo{type: {:map, [], nil}} + ] = state |> get_line_vars(8) - setup {MyModule, :my_setup_function} + assert [ + %VarInfo{type: {:map, [asd: {:integer, 5}, zxc: {:atom, String}], nil}} + ] = state |> get_line_vars(10) + + assert [ + %VarInfo{ + type: {:map, [asd: {:integer, 2}, zxc: {:integer, 5}], {:variable, :var, 3}} + } + ] = + state |> get_line_vars(12) |> Enum.filter(&(&1.name == :qwe)) + + assert [ + %VarInfo{type: {:map, [{:asd, {:integer, 2}}], {:variable, :var, 3}}} + ] = state |> get_line_vars(14) |> Enum.filter(&(&1.name == :qwe)) + end + + test "struct binding" do + state = + """ + defmodule MyModule do + def func(%MyStruct{} = var1, var2 = %:other_struct{}, var3 = %__MODULE__{}, + var4 = %__MODULE__.Sub{}, var7 = %_{}) do + IO.puts "" + end + + def some(a) do + asd = %Some{sub: Atom} + IO.puts "" + asd = %Other{a | sub: Atom} + IO.puts "" + asd = %{asd | other: 123} + IO.puts "" + z = x = asd + IO.puts "" end - """ - |> string_to_state + end + """ + |> string_to_state - assert [%VarInfo{type: nil, scope_id: scope_id}] = state |> get_line_vars(5) - assert [%VarInfo{name: :some}] = state.vars_info_per_scope_id[scope_id] + assert [ + %VarInfo{name: :var1, type: {:struct, [], {:atom, MyStruct}, nil}}, + %VarInfo{name: :var2, type: {:struct, [], {:atom, :other_struct}, nil}}, + %VarInfo{name: :var3, type: {:struct, [], {:atom, MyModule}, nil}}, + %VarInfo{name: :var4, type: {:struct, [], {:atom, MyModule.Sub}, nil}}, + %VarInfo{name: :var7, type: {:struct, [], nil, nil}} + ] = state |> get_line_vars(4) - assert [%VarInfo{type: nil, scope_id: scope_id}] = state |> get_line_vars(9) - assert [%VarInfo{name: :other}] = state.vars_info_per_scope_id[scope_id] + assert %VarInfo{name: :asd, type: {:struct, [{:sub, {:atom, Atom}}], {:atom, Some}, nil}} = + state |> get_line_vars(9) |> Enum.find(&(&1.name == :asd)) - # we do not generate defs - ExUnit.Callbacks.__setup__ is too complicated and generates def names with counters, e.g. - # :"__ex_unit_setup_#{counter}_#{length(setup)}" - end + assert [ + %VarInfo{ + name: :asd, + type: {:struct, [{:sub, {:atom, Atom}}], {:atom, Other}, {:variable, :a, 0}} + } + ] = state |> get_line_vars(11) |> Enum.filter(&(&1.name == :asd)) - test "variables from outside module are added to environment" do - state = - """ - var = :my_var - """ - |> string_to_state + assert [ + %VarInfo{ + name: :asd, + type: {:map, [{:other, {:integer, 123}}], {:variable, :asd, 2}} + } + ] = state |> get_line_vars(13) |> Enum.filter(&(&1.name == :asd)) - assert [%VarInfo{type: {:atom, :my_var}, scope_id: scope_id}] = state |> get_line_vars(1) - assert [%VarInfo{name: :var}] = state.vars_info_per_scope_id[scope_id] - end + assert [ + %VarInfo{name: :x, type: {:variable, :asd, 3}}, + %VarInfo{name: :z, type: {:variable, :asd, 3}} + ] = state |> get_line_vars(15) |> Enum.filter(&(&1.name in [:x, :z])) + end - test "call binding" do - state = - """ - defmodule MyModule do - def remote_calls do - var1 = DateTime.now - var2 = :erlang.now() - var3 = __MODULE__.now(:abc) - var4 = "Etc/UTC" |> DateTime.now - IO.puts "" - end + test "struct binding understands builtin sigils and ranges" do + state = + """ + defmodule MyModule do + def some() do + var1 = ~D[2000-01-01] + var2 = ~T[13:00:07] + var3 = ~U[2015-01-13 13:00:07Z] + var4 = ~N[2000-01-01 23:00:07] + var5 = ~r/foo/iu + var6 = ~R(f\#{1,3}o) + var7 = 12..34 + var8 = 12..34//1 + IO.puts "" + end + end + """ + |> string_to_state - def local_calls do - var1 = now - var2 = now() - var3 = now(:abc) - var4 = :abc |> now - var5 = :abc |> now(5) - IO.puts "" - end + assert [ + %VarInfo{name: :var1, type: {:struct, _, {:atom, Date}, nil}}, + %VarInfo{name: :var2, type: {:struct, _, {:atom, Time}, nil}}, + %VarInfo{name: :var3, type: {:struct, _, {:atom, DateTime}, nil}}, + %VarInfo{name: :var4, type: {:struct, _, {:atom, NaiveDateTime}, nil}}, + %VarInfo{name: :var5, type: {:struct, _, {:atom, Regex}, nil}}, + %VarInfo{name: :var6, type: {:struct, _, {:atom, Regex}, nil}}, + %VarInfo{name: :var7, type: {:struct, _, {:atom, Range}, nil}}, + %VarInfo{name: :var8, type: {:struct, _, {:atom, Range}, nil}} + ] = state |> get_line_vars(11) + end - @attr %{qwe: String} - def map_field(var1) do - var1 = var1.abc - var2 = @attr.qwe(0) - var3 = abc.cde.efg - IO.puts "" - end + test "struct binding understands stepped ranges" do + state = + """ + defmodule MyModule do + def some() do + var1 = 12..34//2 + IO.puts "" end - """ - |> string_to_state - - assert [ - %VarInfo{name: :var1, type: {:call, {:atom, DateTime}, :now, []}}, - %VarInfo{name: :var2, type: {:call, {:atom, :erlang}, :now, []}}, - %VarInfo{name: :var3, type: {:call, {:atom, MyModule}, :now, [{:atom, :abc}]}}, - %VarInfo{name: :var4, type: {:call, {:atom, DateTime}, :now, [nil]}} - ] = state |> get_line_vars(7) - - assert [ - %VarInfo{name: :var1, type: {:variable, :now}}, - %VarInfo{name: :var2, type: {:local_call, :now, []}}, - %VarInfo{name: :var3, type: {:local_call, :now, [{:atom, :abc}]}}, - %VarInfo{name: :var4, type: {:local_call, :now, [{:atom, :abc}]}}, - %VarInfo{name: :var5, type: {:local_call, :now, [{:atom, :abc}, {:integer, 5}]}} - ] = state |> get_line_vars(16) - - assert [ - %VarInfo{name: :var1, type: nil, scope_id: 7}, - %VarInfo{name: :var1, type: {:call, {:variable, :var1}, :abc, []}, scope_id: 8}, - %VarInfo{name: :var2, type: {:call, {:attribute, :attr}, :qwe, [{:integer, 0}]}}, - %VarInfo{ - name: :var3, - type: {:call, {:call, {:variable, :abc}, :cde, []}, :efg, []} - } - ] = state |> get_line_vars(24) - end + end + """ + |> string_to_state - test "map binding" do - state = - """ - defmodule MyModule do - def func do - var = %{asd: 5} - IO.puts "" - var = %{asd: 5, nested: %{wer: "asd"}} - IO.puts "" - var = %{"asd" => "dsds"} - IO.puts "" - var = %{asd: 5, zxc: String} - IO.puts "" - qwe = %{var | asd: 2, zxc: 5} - IO.puts "" - qwe = %{var | asd: 2} - IO.puts "" + assert [ + %VarInfo{ + name: :var1, + type: + {:struct, + [{:first, {:integer, 12}}, {:last, {:integer, 34}}, {:step, {:integer, 2}}], + {:atom, Range}, nil} + } + ] = state |> get_line_vars(4) + end + + test "two way refinement in match context" do + state = + """ + defmodule MyModule do + def some(%MyState{formatted: formatted} = state) do + IO.puts "" + case :ok do + %{foo: 1} = state = %{bar: 1} = x -> + IO.puts "" end end - """ - |> string_to_state + end + """ + |> string_to_state - assert [%VarInfo{type: {:map, [asd: {:integer, 5}], nil}}] = state |> get_line_vars(4) + assert [ + %VarInfo{ + name: :formatted, + type: nil + }, + %VarInfo{ + name: :state, + type: {:struct, [formatted: nil], {:atom, MyState}, nil} + } + ] = state |> get_line_vars(3) - assert [ - %VarInfo{type: {:map, [asd: {:integer, 5}], nil}}, - %VarInfo{ - type: {:map, [asd: {:integer, 5}, nested: {:map, [wer: nil], nil}], nil} + assert [ + %VarInfo{ + name: :formatted + }, + %VarInfo{ + name: :state, + type: { + :intersection, + [ + {:map, [bar: {:integer, 1}], nil}, + {:map, [foo: {:integer, 1}], nil}, + {:atom, :ok} + ] } - ] = state |> get_line_vars(6) - - assert [ - %VarInfo{type: {:map, [asd: {:integer, 5}], nil}}, - %VarInfo{ - type: {:map, [asd: {:integer, 5}, nested: {:map, [wer: nil], nil}], nil} - }, - %VarInfo{type: {:map, [], nil}} - ] = state |> get_line_vars(8) - - assert [ - %VarInfo{type: {:map, [asd: {:integer, 5}], nil}}, - %VarInfo{ - type: {:map, [asd: {:integer, 5}, nested: {:map, [wer: nil], nil}], nil} - }, - %VarInfo{type: {:map, [], nil}}, - %VarInfo{type: {:map, [asd: {:integer, 5}, zxc: {:atom, String}], nil}} - ] = state |> get_line_vars(10) - - assert [ - %VarInfo{ - type: {:map, [asd: {:integer, 2}, zxc: {:integer, 5}], {:variable, :var}} + }, + %VarInfo{ + name: :x, + type: { + :intersection, + [ + {:map, [bar: {:integer, 1}], nil}, + {:map, [foo: {:integer, 1}], nil}, + {:atom, :ok} + ] } - ] = - state |> get_line_vars(12) |> Enum.filter(&(&1.name == :qwe)) - - assert [ - %VarInfo{ - type: {:map, [asd: {:integer, 2}, zxc: {:integer, 5}], {:variable, :var}} - }, - %VarInfo{type: {:map, [{:asd, {:integer, 2}}], {:variable, :var}}} - ] = state |> get_line_vars(14) |> Enum.filter(&(&1.name == :qwe)) - end - - test "struct binding" do - state = - """ - defmodule MyModule do - def func(%MyStruct{} = var1, var2 = %:other_struct{}, var3 = %__MODULE__{}, - var4 = %__MODULE__.Sub{}, var7 = %_{}) do - IO.puts "" - end + } + ] = state |> get_line_vars(7) + end - def some(a) do - asd = %Some{sub: Atom} - IO.puts "" - asd = %Other{a | sub: Atom} - IO.puts "" - asd = %{asd | other: 123} - IO.puts "" - z = x = asd - IO.puts "" - end + test "two way refinement in match context nested" do + state = + """ + defmodule MyModule do + def some(%{foo: 1} = state = %{bar: 1} = x) do + IO.puts "" end - """ - |> string_to_state - - assert [ - %VarInfo{name: :var1, type: {:struct, [], {:atom, MyStruct}, nil}}, - %VarInfo{name: :var2, type: {:struct, [], {:atom, :other_struct}, nil}}, - %VarInfo{name: :var3, type: {:struct, [], {:atom, MyModule}, nil}}, - %VarInfo{name: :var4, type: {:struct, [], {:atom, MyModule.Sub}, nil}}, - %VarInfo{name: :var7, type: {:struct, [], nil, nil}} - ] = state |> get_line_vars(4) - - assert %VarInfo{name: :asd, type: {:struct, [{:sub, {:atom, Atom}}], {:atom, Some}, nil}} = - state |> get_line_vars(9) |> Enum.find(&(&1.name == :asd)) - - assert [ - %VarInfo{ - name: :asd, - type: {:struct, [{:sub, {:atom, Atom}}], {:atom, Some}, nil} - }, - %VarInfo{ - name: :asd, - type: {:struct, [{:sub, {:atom, Atom}}], {:atom, Other}, {:variable, :a}} + end + """ + |> string_to_state + + assert [ + %VarInfo{ + name: :state, + type: { + :intersection, + [{:map, [bar: {:integer, 1}], nil}, {:map, [foo: {:integer, 1}], nil}] } - ] = state |> get_line_vars(11) |> Enum.filter(&(&1.name == :asd)) - - assert [ - %VarInfo{ - name: :asd, - type: {:struct, [{:sub, {:atom, Atom}}], {:atom, Some}, nil} - }, - %VarInfo{ - name: :asd, - type: {:struct, [{:sub, {:atom, Atom}}], {:atom, Other}, {:variable, :a}} - }, - %VarInfo{ - name: :asd, - type: {:map, [{:other, {:integer, 123}}], {:variable, :asd}} + }, + %VarInfo{ + name: :x, + type: { + :intersection, + [{:map, [bar: {:integer, 1}], nil}, {:map, [foo: {:integer, 1}], nil}] } - ] = state |> get_line_vars(13) |> Enum.filter(&(&1.name == :asd)) - - assert [ - %VarInfo{name: :x, type: {:intersection, [{:variable, :z}, {:variable, :asd}]}}, - %VarInfo{name: :z, type: {:variable, :asd}} - ] = state |> get_line_vars(15) |> Enum.filter(&(&1.name in [:x, :z])) - end + } + ] = state |> get_line_vars(3) + end - test "struct binding understands builtin sigils and ranges" do - state = - """ - defmodule MyModule do - def some() do - var1 = ~D[2000-01-01] - var2 = ~T[13:00:07] - var3 = ~U[2015-01-13 13:00:07Z] - var4 = ~N[2000-01-01 23:00:07] - var5 = ~r/foo/iu - var6 = ~R(f\#{1,3}o) - var7 = 12..34 - IO.puts "" + test "two way refinement in match context nested case" do + state = + """ + defmodule MyModule do + def some(state) do + case :ok do + %{foo: 1} = state = %{bar: 1} = x -> + IO.puts "" end end - """ - |> string_to_state - - assert [ - %VarInfo{name: :var1, type: {:struct, [], {:atom, Date}}}, - %VarInfo{name: :var2, type: {:struct, [], {:atom, Time}}}, - %VarInfo{name: :var3, type: {:struct, [], {:atom, DateTime}}}, - %VarInfo{name: :var4, type: {:struct, [], {:atom, NaiveDateTime}}}, - %VarInfo{name: :var5, type: {:struct, [], {:atom, Regex}}}, - %VarInfo{name: :var6, type: {:struct, [], {:atom, Regex}}}, - %VarInfo{name: :var7, type: {:struct, [], {:atom, Range}}} - ] = state |> get_line_vars(10) - end + end + """ + |> string_to_state - test "struct binding understands stepped ranges" do - state = - """ - defmodule MyModule do - def some() do - var1 = 12..34//2 - IO.puts "" - end + assert [ + %VarInfo{ + name: :state, + type: + {:intersection, + [ + {:map, [bar: {:integer, 1}], nil}, + {:map, [foo: {:integer, 1}], nil}, + {:atom, :ok} + ]} + }, + %VarInfo{ + name: :x, + type: + {:intersection, + [ + {:map, [bar: {:integer, 1}], nil}, + {:map, [foo: {:integer, 1}], nil}, + {:atom, :ok} + ]} + } + ] = state |> get_line_vars(5) + end + + test "two way refinement in nested `=` binding" do + state = + """ + defmodule MyModule do + def some(socket) do + %MyState{formatted: formatted} = state = socket.assigns.state + IO.puts "" end - """ - |> string_to_state + end + """ + |> string_to_state - assert [ - %VarInfo{name: :var1, type: {:struct, [], {:atom, Range}}} - ] = state |> get_line_vars(4) - end + assert [ + %VarInfo{ + name: :formatted, + type: { + :map_key, + {:call, {:call, {:variable, :socket, 0}, :assigns, []}, :state, []}, + {:atom, :formatted} + } + }, + %ElixirSense.Core.State.VarInfo{ + name: :socket, + type: nil + }, + %VarInfo{ + name: :state, + type: + {:intersection, + [ + {:call, {:call, {:variable, :socket, 0}, :assigns, []}, :state, []}, + {:struct, [formatted: nil], {:atom, MyState}, nil} + ]} + } + ] = state |> get_line_vars(4) + end - test "nested `=` binding" do - state = - """ - defmodule MyModule do - def some() do - %State{formatted: formatted} = state = socket.assigns.state - IO.puts "" + test "case binding" do + state = + """ + defmodule MyModule do + def some() do + case Some.call() do + {:ok, x} -> + IO.puts "" end end - """ - |> string_to_state + end + """ + |> string_to_state - assert [ - %VarInfo{ - name: :formatted, - type: { - :map_key, - { - :call, - {:call, {:variable, :socket}, :assigns, []}, - :state, - [] - }, - {:atom, :formatted} - } - }, - %VarInfo{ - name: :state, - type: - {:intersection, - [ - {:struct, [formatted: {:variable, :formatted}], {:atom, Elixir.State}, - nil}, - {:call, {:call, {:variable, :socket}, :assigns, []}, :state, []} - ]} - } - ] = state |> get_line_vars(4) - end + assert [ + %VarInfo{ + name: :x, + type: {:tuple_nth, {:call, {:atom, Some}, :call, []}, 1} + } + ] = state |> get_line_vars(5) + end - test "case binding" do - state = - """ - defmodule MyModule do - def some() do - case Some.call() do - {:ok, x} -> - IO.puts "" - end + test "case binding with match" do + state = + """ + defmodule MyModule do + def some() do + case Some.call() do + {:ok, x} = res -> + IO.puts "" end end - """ - |> string_to_state + end + """ + |> string_to_state - assert [ - %VarInfo{ - name: :x, - type: - {:tuple_nth, - {:intersection, - [{:call, {:atom, Some}, :call, []}, {:tuple, 2, [{:atom, :ok}, nil]}]}, 1} - } - ] = state |> get_line_vars(5) - end + assert [ + %VarInfo{ + name: :res, + type: + {:intersection, + [ + {:tuple, 2, [{:atom, :ok}, nil]}, + {:call, {:atom, Some}, :call, []} + ]} + }, + %VarInfo{ + name: :x, + type: {:tuple_nth, {:call, {:atom, Some}, :call, []}, 1} + } + ] = state |> get_line_vars(5) + end - test "rescue binding" do - state = - """ - defmodule MyModule do - def some() do - try do - Some.call() - rescue - e0 in ArgumentError -> - :ok - e1 in [ArgumentError] -> - :ok - e2 in [RuntimeError, Enum.EmptyError] -> - :ok - e3 -> - :ok - else - a -> - :ok - end + test "rescue binding" do + state = + """ + defmodule MyModule do + def some() do + try do + Some.call() + rescue + e0 in ArgumentError -> + IO.puts "" + e1 in [ArgumentError] -> + IO.puts "" + e2 in [RuntimeError, Enum.EmptyError] -> + IO.puts "" + e3 in _ -> + IO.puts "" + e4 -> + IO.puts "" + else + a -> + IO.puts "" end end - """ - |> string_to_state + end + """ + |> string_to_state - assert [ - %VarInfo{ - name: :e0, - type: {:struct, [], {:atom, ArgumentError}, nil} - } - ] = state |> get_line_vars(6) + assert [ + %VarInfo{ + name: :e0, + type: {:struct, [], {:atom, ArgumentError}, nil} + } + ] = state |> get_line_vars(7) - assert [ - %VarInfo{ - name: :e1, - type: {:struct, [], {:atom, ArgumentError}, nil} - } - ] = state |> get_line_vars(8) + assert [ + %VarInfo{ + name: :e1, + type: {:struct, [], {:atom, ArgumentError}, nil} + } + ] = state |> get_line_vars(9) - assert [ - %VarInfo{ - name: :e2, - type: {:struct, [], {:atom, Exception}, nil} + assert [ + %VarInfo{ + name: :e2, + type: { + :union, + [ + {:struct, [], {:atom, RuntimeError}, nil}, + {:struct, [], {:atom, Enum.EmptyError}, nil} + ] } - ] = state |> get_line_vars(10) + } + ] = state |> get_line_vars(11) - assert [ - %VarInfo{ - name: :e3, - type: {:struct, [], {:atom, Exception}, nil} - } - ] = state |> get_line_vars(12) + assert [ + %VarInfo{ + name: :e3, + type: {:struct, [], {:atom, Exception}, nil} + } + ] = state |> get_line_vars(13) - assert [ - %VarInfo{ - name: :a, - type: nil - } - ] = state |> get_line_vars(15) - end + assert [ + %VarInfo{ + name: :e4, + type: {:struct, [], {:atom, Exception}, nil} + } + ] = state |> get_line_vars(15) - test "vars defined inside a function `after`/`rescue`/`catch`" do - state = - """ - defmodule MyModule do - var_out1 = 1 - def func(var_arg) do - var_in1 = 1 - var_in2 = 1 - IO.puts "" - after - var_after = 1 + assert [ + %VarInfo{ + name: :a, + type: nil + } + ] = state |> get_line_vars(18) + end + + test "def rescue binding" do + state = + """ + defmodule MyModule do + def some() do + Some.call() + rescue + e0 in ArgumentError -> IO.puts "" + end + end + """ + |> string_to_state + + assert [ + %VarInfo{ + name: :e0, + type: {:struct, [], {:atom, ArgumentError}, nil} + } + ] = state |> get_line_vars(6) + end + + test "vars binding by pattern matching with pin operators" do + state = + """ + defmodule MyModule do + def func(a) do + b = 1 + case a do + %{b: 2} = a1 -> + IO.puts "" + %{b: ^b} = a2 -> + IO.puts "" end end - """ - |> string_to_state + end + """ + |> string_to_state - assert [ - %VarInfo{name: :var_arg, positions: [{3, 12}], scope_id: 3}, - %VarInfo{name: :var_in1, positions: [{4, 5}], scope_id: 4}, - %VarInfo{name: :var_in2, positions: [{5, 5}], scope_id: 4} - ] = state |> get_line_vars(6) + vars = state |> get_line_vars(6) - assert [ - %VarInfo{name: :var_after, positions: [{8, 5}], scope_id: 5}, - %VarInfo{name: :var_arg, positions: [{3, 12}], scope_id: 3} - ] = state |> get_line_vars(9) - end + assert %VarInfo{ + name: :a1, + positions: [{5, 17}], + type: {:intersection, [{:map, [b: {:integer, 2}], nil}, {:variable, :a, 0}]} + } = Enum.find(vars, &(&1.name == :a1)) - test "vars defined inside a function with params" do - state = - """ - defmodule MyModule do - var_out1 = 1 - def func(%{key1: par1, key2: [par2|[par3, _]]}, par4, _par5) do - var_in1 = 1 - var_in2 = 1 - IO.puts "" - end - defp func1(arg), do: arg + 1 - var_out2 = 1 - end - """ - |> string_to_state - - assert [ - %VarInfo{name: :_par5, positions: [{3, 57}], scope_id: 3}, - %VarInfo{name: :par1, positions: [{3, 20}], scope_id: 3}, - %VarInfo{name: :par2, positions: [{3, 33}], scope_id: 3}, - %VarInfo{name: :par3, positions: [{3, 39}], scope_id: 3}, - %VarInfo{name: :par4, positions: [{3, 51}], scope_id: 3}, - %VarInfo{name: :var_in1, positions: [{4, 5}], scope_id: 4}, - %VarInfo{name: :var_in2, positions: [{5, 5}], scope_id: 4} - ] = state |> get_line_vars(6) - - assert [ - %VarInfo{name: :arg, positions: [{8, 14}, {8, 24}], scope_id: 5} - ] = state |> get_line_vars(8) - end + vars = state |> get_line_vars(8) - test "vars binding by pattern matching with pin operators" do - state = - """ - defmodule MyModule do - def func(a) do - b = 1 - case a do - %{b: ^2} = a1 -> 2 - %{b: ^b} = a2 -> b - end - end + assert %VarInfo{ + name: :a2, + positions: [{7, 18}], + type: { + :intersection, + [{:map, [b: {:variable, :b, 1}], nil}, {:variable, :a, 0}] + } + } = Enum.find(vars, &(&1.name == :a2)) + end + end + + describe "var" do + test "vars defined inside a function `after`/`rescue`/`catch`" do + state = + """ + defmodule MyModule do + var_out1 = 1 + def func(var_arg) do + var_in1 = 1 + var_in2 = 1 + IO.puts "" + after + var_after = 1 + IO.puts "" end - """ - |> string_to_state + end + """ + |> string_to_state - vars = state |> get_line_vars(5) + assert ([ + %VarInfo{name: :var_arg, positions: [{3, 12}], scope_id: scope_id_1}, + %VarInfo{name: :var_in1, positions: [{4, 5}], scope_id: scope_id_2}, + %VarInfo{name: :var_in2, positions: [{5, 5}], scope_id: scope_id_2} + ] + when scope_id_2 > scope_id_1) = state |> get_line_vars(6) - assert %VarInfo{ - name: :a1, - positions: [{5, 18}], - scope_id: 6, - is_definition: true, - type: {:map, [b: {:integer, 2}], nil} - } = Enum.find(vars, &(&1.name == :a1)) + assert ([ + %VarInfo{name: :var_after, positions: [{8, 5}], scope_id: scope_id_2}, + %VarInfo{name: :var_arg, positions: [{3, 12}], scope_id: scope_id_1} + ] + when scope_id_2 > scope_id_1) = state |> get_line_vars(9) + end - vars = state |> get_line_vars(6) + test "vars defined inside a function with params" do + state = + """ + defmodule MyModule do + var_out1 = 1 + def func(%{key1: par1, key2: [par2|[par3, _]]}, par4, _par5) do + var_in1 = 1 + var_in2 = 1 + IO.puts "" + end + defp func1(arg), do: arg + 1 + var_out2 = 1 + end + """ + |> string_to_state - assert %VarInfo{ - name: :a2, - positions: [{6, 18}], - scope_id: 7, - is_definition: true, - type: {:map, [b: {:variable, :b}], nil} - } = Enum.find(vars, &(&1.name == :a2)) - end + assert [ + %VarInfo{name: :_par5, positions: [{3, 57}], scope_id: scope_id_1}, + %VarInfo{name: :par1, positions: [{3, 20}], scope_id: scope_id_1}, + %VarInfo{name: :par2, positions: [{3, 33}], scope_id: scope_id_1}, + %VarInfo{name: :par3, positions: [{3, 39}], scope_id: scope_id_1}, + %VarInfo{name: :par4, positions: [{3, 51}], scope_id: scope_id_1}, + %VarInfo{name: :var_in1, positions: [{4, 5}], scope_id: scope_id_1}, + %VarInfo{name: :var_in2, positions: [{5, 5}], scope_id: scope_id_1} + ] = state |> get_line_vars(6) - test "rebinding vars" do - state = - """ - defmodule MyModule do - var1 = 1 - def func(%{var: var1, key: [_|[_, var1]]}) do - var1 = 1 - var1 = 2 - IO.puts "" - end + assert [ + %VarInfo{name: :arg, positions: [{8, 14}, {8, 24}]} + ] = state |> get_line_vars(8) + end + + test "rebinding vars" do + state = + """ + defmodule MyModule do + var1 = 1 + def func(%{var: var1, key: [_|[_, var1]]}) do var1 = 1 + var1 = 2 + IO.puts "" end - """ - |> string_to_state + var1 = 1 + end + """ + |> string_to_state - vars = state |> get_line_vars(6) + vars = state |> get_line_vars(6) - assert [ - %VarInfo{name: :var1, positions: [{3, 19}, {3, 37}], scope_id: 3}, - %VarInfo{name: :var1, positions: [{4, 5}], scope_id: 4}, - %VarInfo{name: :var1, positions: [{5, 5}], scope_id: 4} - ] = vars - end + assert [ + # %VarInfo{name: :var1, positions: [{3, 19}, {3, 37}], scope_id: scope_id_1}, + # %VarInfo{name: :var1, positions: [{4, 5}], scope_id: scope_id_2}, + %VarInfo{name: :var1, positions: [{5, 5}]} + ] = vars end - end - describe "var" do - test "vars defined inside a module" do + test "vars defined inside a module body" do state = """ defmodule MyModule do var_out1 = 1 def func do var_in = 1 + IO.puts "" end var_out2 = 1 IO.puts "" @@ -2477,15 +2791,23 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert Map.keys(state.lines_to_env[7].versioned_vars) == [ + assert Map.keys(state.lines_to_env[5].versioned_vars) == [ + {:var_in, nil} + ] + + assert [ + %VarInfo{name: :var_in, positions: [{4, 5}]} + ] = state |> get_line_vars(5) + + assert Map.keys(state.lines_to_env[8].versioned_vars) == [ {:var_out1, nil}, {:var_out2, nil} ] assert [ %VarInfo{name: :var_out1, positions: [{2, 3}], scope_id: scope_id}, - %VarInfo{name: :var_out2, positions: [{6, 3}], scope_id: scope_id} - ] = state |> get_line_vars(7) + %VarInfo{name: :var_out2, positions: [{7, 3}], scope_id: scope_id} + ] = state |> get_line_vars(8) end test "vars defined in a `for` comprehension" do @@ -2519,31 +2841,27 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert ([ %VarInfo{ - is_definition: true, name: :var_in, positions: [{5, 5}], - scope_id: scope_id_3 + scope_id: scope_id_2 }, %VarInfo{ - is_definition: true, name: :var_on, positions: [{4, 7}, {4, 24}, {4, 47}], scope_id: scope_id_2 }, %VarInfo{ - is_definition: true, name: :var_on1, positions: [{4, 37}], scope_id: scope_id_2 }, %VarInfo{ - is_definition: true, name: :var_out1, positions: [{2, 3}], scope_id: scope_id_1 } ] - when scope_id_2 > scope_id_1 and scope_id_3 > scope_id_2) = get_line_vars(state, 6) + when scope_id_2 > scope_id_1) = get_line_vars(state, 6) assert Map.keys(state.lines_to_env[9].versioned_vars) == [ {:var_out1, nil}, @@ -2587,31 +2905,27 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert ([ %VarInfo{ - is_definition: true, name: :var_in, positions: [{5, 5}], - scope_id: scope_id_3 + scope_id: scope_id_2 }, %VarInfo{ - is_definition: true, name: :var_on, positions: [{4, 8}, {4, 25}, {4, 48}], scope_id: scope_id_2 }, %VarInfo{ - is_definition: true, name: :var_on1, positions: [{4, 38}], scope_id: scope_id_2 }, %VarInfo{ - is_definition: true, name: :var_out1, positions: [{2, 3}], scope_id: scope_id_1 } ] - when scope_id_2 > scope_id_1 and scope_id_3 > scope_id_2) = get_line_vars(state, 6) + when scope_id_2 > scope_id_1) = get_line_vars(state, 6) assert Map.keys(state.lines_to_env[9].versioned_vars) == [ {:var_out1, nil}, @@ -2705,19 +3019,16 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert ([ %VarInfo{ - is_definition: true, name: :var_in, positions: [{4, 5}], scope_id: scope_id_2 }, %VarInfo{ - is_definition: true, name: :var_on, positions: [{3, 6}], scope_id: scope_id_2 }, %VarInfo{ - is_definition: true, name: :var_out1, positions: [{2, 3}], scope_id: scope_id_1 @@ -2765,25 +3076,21 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert ([ %VarInfo{ - is_definition: true, name: :var_in1, positions: [{5, 7}], scope_id: scope_id_2 }, %VarInfo{ - is_definition: true, name: :var_on0, positions: [{3, 8}], scope_id: scope_id_1 }, %VarInfo{ - is_definition: true, name: :var_on1, positions: [{4, 6}], scope_id: scope_id_2 }, %VarInfo{ - is_definition: true, name: :var_out1, positions: [{2, 3}, {3, 18}], scope_id: scope_id_1 @@ -3109,257 +3416,680 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] = get_line_vars(state, 4) end - test "inherited vars" do - state = - """ - top_level_var = 1 - IO.puts "" - defmodule OuterModule do - outer_module_var = 1 + test "inherited vars" do + state = + """ + top_level_var = 1 + IO.puts "" + defmodule OuterModule do + outer_module_var = 1 + IO.puts "" + defmodule InnerModule do + inner_module_var = 1 + IO.puts "" + def func do + func_var = 1 + IO.puts "" + end + IO.puts "" + end + IO.puts "" + end + IO.puts "" + """ + |> string_to_state + + assert Map.keys(state.lines_to_env[2].versioned_vars) == [{:top_level_var, nil}] + + assert [ + %VarInfo{name: :top_level_var, positions: [{1, 1}], scope_id: 0} + ] = get_line_vars(state, 2) + + assert Map.keys(state.lines_to_env[5].versioned_vars) == [ + {:outer_module_var, nil}, + {:top_level_var, nil} + ] + + assert ([ + %VarInfo{name: :outer_module_var, positions: [{4, 3}], scope_id: scope_id_2}, + %VarInfo{name: :top_level_var, positions: [{1, 1}], scope_id: scope_id_1} + ] + when scope_id_2 > scope_id_1) = get_line_vars(state, 5) + + assert Map.keys(state.lines_to_env[8].versioned_vars) == [ + {:inner_module_var, nil}, + {:outer_module_var, nil}, + {:top_level_var, nil} + ] + + assert ([ + %VarInfo{name: :inner_module_var, positions: [{7, 5}], scope_id: scope_id_3}, + %VarInfo{name: :outer_module_var, positions: [{4, 3}], scope_id: scope_id_2}, + %VarInfo{name: :top_level_var, positions: [{1, 1}], scope_id: scope_id_1} + ] + when scope_id_2 > scope_id_1 and scope_id_3 > scope_id_2) = get_line_vars(state, 8) + + assert Map.keys(state.lines_to_env[11].versioned_vars) == [{:func_var, nil}] + + assert [ + %VarInfo{name: :func_var, positions: [{10, 7}]} + ] = get_line_vars(state, 11) + + assert Map.keys(state.lines_to_env[13].versioned_vars) == [ + {:inner_module_var, nil}, + {:outer_module_var, nil}, + {:top_level_var, nil} + ] + + assert ([ + %VarInfo{name: :inner_module_var, positions: [{7, 5}], scope_id: scope_id_3}, + %VarInfo{name: :outer_module_var, positions: [{4, 3}], scope_id: scope_id_2}, + %VarInfo{name: :top_level_var, positions: [{1, 1}], scope_id: scope_id_1} + ] + when scope_id_2 > scope_id_1 and scope_id_3 > scope_id_2) = get_line_vars(state, 13) + + assert Map.keys(state.lines_to_env[15].versioned_vars) == [ + outer_module_var: nil, + top_level_var: nil + ] + + assert ([ + %VarInfo{name: :outer_module_var, positions: [{4, 3}], scope_id: scope_id_2}, + %VarInfo{name: :top_level_var, positions: [{1, 1}], scope_id: scope_id_1} + ] + when scope_id_2 > scope_id_1) = get_line_vars(state, 15) + + assert Map.keys(state.lines_to_env[17].versioned_vars) == [{:top_level_var, nil}] + + assert [ + %VarInfo{name: :top_level_var, positions: [{1, 1}]} + ] = get_line_vars(state, 17) + end + + test "vars as a struct type" do + state = + """ + defmodule MyModule do + def func(%my_var{}, %_my_other{}, %_{}, x) do + %abc{} = x + IO.puts "" + end + end + """ + |> string_to_state + + assert Map.keys(state.lines_to_env[4].versioned_vars) == [ + {:_my_other, nil}, + {:abc, nil}, + {:my_var, nil}, + {:x, nil} + ] + + assert [ + %VarInfo{ + name: :_my_other, + positions: [{2, 24}], + scope_id: scope_id_1 + }, + %VarInfo{ + name: :abc, + positions: [{3, 6}], + scope_id: scope_id_1 + }, + %VarInfo{ + name: :my_var, + positions: [{2, 13}], + scope_id: scope_id_1 + }, + %VarInfo{ + name: :x, + positions: [{2, 43}, {3, 14}], + scope_id: scope_id_1 + } + ] = state |> get_line_vars(4) + end + end + + describe "infer vars type information from guards" do + defp var_with_guards(guard) do + """ + defmodule MyModule do + def func(x) when #{guard} do + IO.puts "" + end + end + """ + |> string_to_state() + |> get_line_vars(3) + |> hd() + end + + test "guards in case clauses" do + buffer = """ + defmodule MyModule do + def func(x) do + IO.puts "" + case x do + {a, b} when is_nil(a) and is_integer(b) -> + IO.puts "" + _ when is_integer(x) -> + IO.puts "" + end + IO.puts "" + end + end + """ + + state = string_to_state(buffer) + + assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 3) + + assert [ + %VarInfo{ + name: :a, + type: {:intersection, [{:atom, nil}, {:tuple_nth, {:variable, :x, 0}, 0}]} + }, + %VarInfo{ + name: :b, + type: {:intersection, [:number, {:tuple_nth, {:variable, :x, 0}, 1}]} + }, + %VarInfo{name: :x, type: nil} + ] = get_line_vars(state, 6) + + assert [%VarInfo{name: :x, type: :number}] = get_line_vars(state, 8) + + assert [%VarInfo{name: :x, type: nil}] = + get_line_vars(state, 10) + end + + test "guards in case clauses more complicated" do + buffer = """ + defmodule MyModule do + def func(x) do + IO.puts "" + case {x, :foo} do + {a, ^x} when is_nil(a) -> + IO.puts "" + some_macro(c) -> + IO.puts "" + _ when is_integer(x) -> + IO.puts "" + end + IO.puts "" + end + end + """ + + state = string_to_state(buffer) + + assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 3) + + assert [ + %VarInfo{ + name: :a, + type: { + :intersection, + [ + {:atom, nil}, + { + :tuple_nth, + {:tuple, 2, [{:variable, :x, 0}, {:atom, :foo}]}, + 0 + } + ] + } + }, + %VarInfo{name: :x, type: nil} + ] = get_line_vars(state, 6) + + assert [%VarInfo{name: :c, type: nil}, %VarInfo{name: :x, type: nil}] = + get_line_vars(state, 8) + + assert [%VarInfo{name: :x, type: :number}] = get_line_vars(state, 10) + + assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 12) + end + + test "guards in with clauses" do + buffer = """ + defmodule MyModule do + def func(x) do + IO.puts "" + with {a, b} when is_nil(a) and is_integer(b) <- x do + IO.puts "" + else + {:error, e} when is_atom(e) -> + IO.puts "" + _ when is_integer(x) -> + IO.puts "" + end + IO.puts "" + end + end + """ + + state = string_to_state(buffer) + + assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 3) + + assert [ + %VarInfo{ + name: :a, + type: {:intersection, [{:atom, nil}, {:tuple_nth, {:variable, :x, 0}, 0}]} + }, + %VarInfo{ + name: :b, + type: {:intersection, [:number, {:tuple_nth, {:variable, :x, 0}, 1}]} + }, + %VarInfo{name: :x, type: nil} + ] = get_line_vars(state, 5) + + assert [%VarInfo{name: :e, type: :atom}, %VarInfo{name: :x, type: nil}] = + get_line_vars(state, 8) + + assert [%VarInfo{name: :x, type: :number}] = get_line_vars(state, 10) + + assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 12) + end + + test "guards in receive clauses" do + buffer = """ + defmodule MyModule do + def func(x) do + IO.puts "" + receive do + {a, b} when is_nil(a) and is_integer(b) -> + IO.puts "" + _ when is_integer(x) -> + IO.puts "" + end + IO.puts "" + end + end + """ + + state = string_to_state(buffer) + + assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 3) + + assert [ + %VarInfo{name: :a, type: {:atom, nil}}, + %VarInfo{name: :b, type: :number}, + %VarInfo{name: :x, type: nil} + ] = get_line_vars(state, 6) + + assert [%VarInfo{name: :x, type: :number}] = get_line_vars(state, 8) + + assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 10) + end + + test "guards in for generator clauses" do + buffer = """ + defmodule MyModule do + def func(x) do + IO.puts "" + for {a, b} when is_nil(a) and is_integer(b) <- x, y when is_integer(x) <- a do + IO.puts "" + end + IO.puts "" + end + end + """ + + state = string_to_state(buffer) + + assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 3) + + assert [ + %VarInfo{ + name: :a, + type: + {:intersection, + [{:atom, nil}, {:tuple_nth, {:for_expression, {:variable, :x, 0}}, 0}]} + }, + %VarInfo{ + name: :b, + type: + {:intersection, + [:number, {:tuple_nth, {:for_expression, {:variable, :x, 0}}, 1}]} + }, + %VarInfo{name: :x, type: :number}, + %VarInfo{name: :y, type: {:for_expression, {:variable, :a, 1}}} + ] = get_line_vars(state, 5) + + assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 7) + end + + test "guards in for aggregate clauses" do + buffer = """ + defmodule MyModule do + def func(x) do IO.puts "" - defmodule InnerModule do - inner_module_var = 1 - IO.puts "" - def func do - func_var = 1 + for a <- x, reduce: %{} do + b when is_integer(b) -> + IO.puts "" + c when is_atom(c) -> + IO.puts "" + _ when is_integer(x) -> IO.puts "" - end - IO.puts "" end IO.puts "" end - IO.puts "" - """ - |> string_to_state + end + """ - assert Map.keys(state.lines_to_env[2].versioned_vars) == [{:top_level_var, nil}] + state = string_to_state(buffer) + + assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 3) assert [ - %VarInfo{name: :top_level_var, positions: [{1, 1}], scope_id: 0} - ] = get_line_vars(state, 2) + %VarInfo{name: :a, type: {:for_expression, {:variable, :x, 0}}}, + %VarInfo{name: :b, type: :number}, + %VarInfo{name: :x, type: nil} + ] = get_line_vars(state, 6) - assert Map.keys(state.lines_to_env[5].versioned_vars) == [ - {:outer_module_var, nil}, - {:top_level_var, nil} - ] + assert [ + %VarInfo{name: :a, type: {:for_expression, {:variable, :x, 0}}}, + %VarInfo{name: :c, type: :atom}, + %VarInfo{name: :x, type: nil} + ] = get_line_vars(state, 8) - assert ([ - %VarInfo{name: :outer_module_var, positions: [{4, 3}], scope_id: scope_id_2}, - %VarInfo{name: :top_level_var, positions: [{1, 1}], scope_id: scope_id_1} - ] - when scope_id_2 > scope_id_1) = get_line_vars(state, 5) + assert [ + %VarInfo{name: :a, type: {:for_expression, {:variable, :x, 0}}}, + %VarInfo{name: :x, type: :number} + ] = get_line_vars(state, 10) - assert Map.keys(state.lines_to_env[8].versioned_vars) == [ - {:inner_module_var, nil}, - {:outer_module_var, nil}, - {:top_level_var, nil} - ] + assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 12) + end - assert ([ - %VarInfo{name: :inner_module_var, positions: [{7, 5}], scope_id: scope_id_3}, - %VarInfo{name: :outer_module_var, positions: [{4, 3}], scope_id: scope_id_2}, - %VarInfo{name: :top_level_var, positions: [{1, 1}], scope_id: scope_id_1} - ] - when scope_id_2 > scope_id_1 and scope_id_3 > scope_id_2) = get_line_vars(state, 8) + test "guards in try clauses" do + buffer = """ + defmodule MyModule do + def func(x) do + IO.puts "" + try do + foo() + catch + a, b when is_nil(a) and is_integer(b) -> + IO.puts "" + else + c when is_nil(c) when is_binary(c) -> + IO.puts "" + _ when is_integer(x) -> + IO.puts "" + end + IO.puts "" + end + end + """ - assert Map.keys(state.lines_to_env[11].versioned_vars) == [{:func_var, nil}] + state = string_to_state(buffer) + + assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 3) assert [ - %VarInfo{name: :func_var, positions: [{10, 7}]} + %VarInfo{name: :a, type: {:atom, nil}}, + %VarInfo{name: :b, type: :number}, + %VarInfo{name: :x, type: nil} + ] = get_line_vars(state, 8) + + assert [ + %VarInfo{name: :c, type: {:union, [{:atom, nil}, :binary]}}, + %VarInfo{name: :x, type: nil} ] = get_line_vars(state, 11) - assert Map.keys(state.lines_to_env[13].versioned_vars) == [ - {:inner_module_var, nil}, - {:outer_module_var, nil}, - {:top_level_var, nil} - ] + assert [%VarInfo{name: :x, type: :number}] = get_line_vars(state, 13) - assert ([ - %VarInfo{name: :inner_module_var, positions: [{7, 5}], scope_id: scope_id_3}, - %VarInfo{name: :outer_module_var, positions: [{4, 3}], scope_id: scope_id_2}, - %VarInfo{name: :top_level_var, positions: [{1, 1}], scope_id: scope_id_1} - ] - when scope_id_2 > scope_id_1 and scope_id_3 > scope_id_2) = get_line_vars(state, 13) + assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 15) + end - assert Map.keys(state.lines_to_env[15].versioned_vars) == [ - outer_module_var: nil, - top_level_var: nil - ] + test "guards in fn clauses" do + buffer = """ + defmodule MyModule do + def func(x) do + IO.puts "" + fn + a, b when is_nil(a) and is_integer(b) -> + IO.puts "" + c, _ when is_nil(c) when is_binary(c) -> + IO.puts "" + _, _ when is_integer(x) -> + IO.puts "" + end + IO.puts "" + end + end + """ - assert ([ - %VarInfo{name: :outer_module_var, positions: [{4, 3}], scope_id: scope_id_2}, - %VarInfo{name: :top_level_var, positions: [{1, 1}], scope_id: scope_id_1} - ] - when scope_id_2 > scope_id_1) = get_line_vars(state, 15) + state = string_to_state(buffer) - assert Map.keys(state.lines_to_env[17].versioned_vars) == [{:top_level_var, nil}] + assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 3) assert [ - %VarInfo{name: :top_level_var, positions: [{1, 1}]} - ] = get_line_vars(state, 17) + %VarInfo{name: :a, type: {:atom, nil}}, + %VarInfo{name: :b, type: :number}, + %VarInfo{name: :x, type: nil} + ] = get_line_vars(state, 6) + + assert [ + %VarInfo{name: :c, type: {:union, [{:atom, nil}, :binary]}}, + %VarInfo{name: :x, type: nil} + ] = get_line_vars(state, 8) + + assert [%VarInfo{name: :x, type: :number}] = get_line_vars(state, 10) + assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 12) end - test "vars as a struct type" do - state = - """ - defmodule MyModule do - def func(%my_var{}, %_my_other{}, %_{}, x) do - %abc{} = x - IO.puts "" - end - end - """ - |> string_to_state + test "number guards" do + assert %VarInfo{name: :x, type: :number} = var_with_guards("is_number(x)") + assert %VarInfo{name: :x, type: :number} = var_with_guards("is_float(x)") + assert %VarInfo{name: :x, type: :number} = var_with_guards("is_integer(x)") + assert %VarInfo{name: :x, type: :number} = var_with_guards("round(x)") + assert %VarInfo{name: :x, type: :number} = var_with_guards("trunc(x)") + assert %VarInfo{name: :x, type: :number} = var_with_guards("div(x, 1)") + assert %VarInfo{name: :x, type: :number} = var_with_guards("rem(x, 1)") + assert %VarInfo{name: :x, type: :number} = var_with_guards("abs(x)") + assert %VarInfo{name: :x, type: :number} = var_with_guards("ceil(x)") + assert %VarInfo{name: :x, type: :number} = var_with_guards("floor(x)") + end - assert Map.keys(state.lines_to_env[4].versioned_vars) == [ - {:_my_other, nil}, - {:abc, nil}, - {:my_var, nil}, - {:x, nil} - ] + test "binary guards" do + assert %VarInfo{name: :x, type: :binary} = var_with_guards("is_binary(x)") - assert ([ - %VarInfo{ - is_definition: true, - name: :_my_other, - positions: [{2, 24}], - scope_id: scope_id_1 - }, - %VarInfo{ - is_definition: true, - name: :abc, - positions: [{3, 6}], - scope_id: scope_id_2 - }, - %VarInfo{ - is_definition: true, - name: :my_var, - positions: [{2, 13}], - scope_id: scope_id_1 - }, - %VarInfo{ - is_definition: true, - name: :x, - positions: [{2, 43}, {3, 14}], - scope_id: scope_id_1 - } - ] - when scope_id_2 > scope_id_1) = state |> get_line_vars(4) + assert %VarInfo{name: :x, type: :binary} = + var_with_guards(~s/binary_part(x, 0, 1) == "a"/) end - end - if @binding_support do - describe "infer vars type information from guards" do - defp var_with_guards(guard) do - """ - defmodule MyModule do - def func(x) when #{guard} do - x - end - end - """ - |> string_to_state() - |> get_line_vars(3) - |> hd() - end + test "bitstring guards" do + assert %VarInfo{name: :x, type: :bitstring} = var_with_guards("is_bitstring(x)") + assert %VarInfo{name: :x, type: :bitstring} = var_with_guards("bit_size(x) == 1") + assert %VarInfo{name: :x, type: :bitstring} = var_with_guards("byte_size(x) == 1") + end - test "number guards" do - assert %VarInfo{name: :x, type: :number} = var_with_guards("is_number(x)") - assert %VarInfo{name: :x, type: :number} = var_with_guards("is_float(x)") - assert %VarInfo{name: :x, type: :number} = var_with_guards("is_integer(x)") - assert %VarInfo{name: :x, type: :number} = var_with_guards("round(x)") - assert %VarInfo{name: :x, type: :number} = var_with_guards("trunc(x)") - assert %VarInfo{name: :x, type: :number} = var_with_guards("div(x)") - assert %VarInfo{name: :x, type: :number} = var_with_guards("rem(x)") - assert %VarInfo{name: :x, type: :number} = var_with_guards("abs(x)") - end + test "multiple guards" do + assert %VarInfo{name: :x, type: {:union, [:bitstring, :number]}} = + var_with_guards("is_bitstring(x) when is_integer(x)") + end - test "binary guards" do - assert %VarInfo{name: :x, type: :binary} = var_with_guards("is_binary(x)") + test "list guards" do + assert %VarInfo{name: :x, type: :list} = var_with_guards("is_list(x)") + assert %VarInfo{name: :x, type: {:list, {:integer, 1}}} = var_with_guards("hd(x) == 1") + assert %VarInfo{name: :x, type: {:list, {:integer, 1}}} = var_with_guards("1 == hd(x)") + assert %VarInfo{name: :x, type: :list} = var_with_guards("tl(x) == [1]") + assert %VarInfo{name: :x, type: :list} = var_with_guards("length(x) == 1") + assert %VarInfo{name: :x, type: :list} = var_with_guards("1 == length(x)") + assert %VarInfo{name: :x, type: {:list, :boolean}} = var_with_guards("hd(x)") + end - assert %VarInfo{name: :x, type: :binary} = - var_with_guards(~s/binary_part(x, 0, 1) == "a"/) - end + test "tuple guards" do + assert %VarInfo{name: :x, type: :tuple} = var_with_guards("is_tuple(x)") - test "bitstring guards" do - assert %VarInfo{name: :x, type: :bitstring} = var_with_guards("is_bitstring(x)") - assert %VarInfo{name: :x, type: :bitstring} = var_with_guards("bit_size(x) == 1") - assert %VarInfo{name: :x, type: :bitstring} = var_with_guards("byte_size(x) == 1") - end + assert %VarInfo{name: :x, type: {:tuple, 1, [nil]}} = + var_with_guards("tuple_size(x) == 1") - test "list guards" do - assert %VarInfo{name: :x, type: :list} = var_with_guards("is_list(x)") - assert %VarInfo{name: :x, type: {:list, :number}} = var_with_guards("hd(x) == 1") - assert %VarInfo{name: :x, type: :list} = var_with_guards("tl(x) == [1]") - assert %VarInfo{name: :x, type: :list} = var_with_guards("length(x) == 1") - end + assert %VarInfo{name: :x, type: {:tuple, 1, [nil]}} = + var_with_guards("1 == tuple_size(x)") - test "tuple guards" do - assert %VarInfo{name: :x, type: :tuple} = var_with_guards("is_tuple(x)") + assert %VarInfo{name: :x, type: :tuple} = var_with_guards("elem(x, 0) == 1") + end - assert %VarInfo{name: :x, type: {:tuple, 1, [nil]}} = - var_with_guards("tuple_size(x) == 1") + test "atom guards" do + assert %VarInfo{name: :x, type: :atom} = var_with_guards("is_atom(x)") + end - assert %VarInfo{name: :x, type: :tuple} = var_with_guards("elem(x, 0) == 1") - end + test "boolean guards" do + assert %VarInfo{name: :x, type: :boolean} = var_with_guards("is_boolean(x)") + end - test "atom guards" do - assert %VarInfo{name: :x, type: :atom} = var_with_guards("is_atom(x)") - end + test "map guards" do + assert %VarInfo{name: :x, type: {:map, [], nil}} = var_with_guards("is_map(x)") - test "boolean guards" do - assert %VarInfo{name: :x, type: :boolean} = var_with_guards("is_boolean(x)") + if Version.match?(System.version(), ">= 1.17.0") do + assert %VarInfo{name: :x, type: {:map, [], nil}} = + var_with_guards("is_non_struct_map(x)") end - test "map guards" do - assert %VarInfo{name: :x, type: {:map, [], nil}} = var_with_guards("is_map(x)") - assert %VarInfo{name: :x, type: {:map, [], nil}} = var_with_guards("map_size(x) == 1") + assert %VarInfo{name: :x, type: {:map, [], nil}} = var_with_guards("map_size(x) == 1") + assert %VarInfo{name: :x, type: {:map, [], nil}} = var_with_guards("1 == map_size(x)") - assert %VarInfo{name: :x, type: {:map, [a: nil], nil}} = - var_with_guards("is_map_key(x, :a)") + assert %VarInfo{name: :x, type: {:map, [a: nil], nil}} = + var_with_guards("is_map_key(x, :a)") - assert %VarInfo{name: :x, type: {:map, [{"a", nil}], nil}} = - var_with_guards(~s/is_map_key(x, "a")/) - end + assert %VarInfo{name: :x, type: {:map, [{"a", nil}], nil}} = + var_with_guards(~s/is_map_key(x, "a")/) + end - test "struct guards" do - assert %VarInfo{name: :x, type: {:struct, [], nil, nil}} = var_with_guards("is_struct(x)") + test "struct guards" do + assert %VarInfo{ + name: :x, + type: { + :intersection, + [ + {:struct, [], nil, nil}, + {:map, [], nil} + ] + } + } = var_with_guards("is_struct(x)") - assert %VarInfo{name: :x, type: {:struct, [], {:atom, URI}, nil}} = - var_with_guards("is_struct(x, URI)") + assert %VarInfo{ + name: :x, + type: { + :intersection, + [ + {:struct, [], {:atom, URI}, nil}, + {:map, [], nil}, + {:struct, [], nil, nil} + ] + } + } = + var_with_guards("is_struct(x, URI)") - assert %VarInfo{name: :x, type: {:struct, [], {:atom, URI}, nil}} = - """ - defmodule MyModule do - alias URI, as: MyURI + assert %VarInfo{ + name: :x, + type: { + :intersection, + [ + {:struct, [], {:atom, URI}, nil}, + {:map, [], nil}, + {:struct, [], nil, nil} + ] + } + } = + """ + defmodule MyModule do + alias URI, as: MyURI - def func(x) when is_struct(x, MyURI) do - x - end + def func(x) when is_struct(x, MyURI) do + IO.puts "" end - """ - |> string_to_state() - |> get_line_vars(5) - |> hd() - end + end + """ + |> string_to_state() + |> get_line_vars(5) + |> hd() + end + + test "exception guards" do + assert %VarInfo{ + name: :x, + type: { + :intersection, + [ + {:map, [{:__exception__, {:atom, true}}], nil}, + {:map, [{:__exception__, nil}], nil}, + {:struct, [], nil, nil}, + {:map, [], nil} + ] + } + } = var_with_guards("is_exception(x)") + + assert %VarInfo{ + name: :x, + type: { + :intersection, + [ + {:map, [{:__exception__, {:atom, true}}], nil}, + {:map, [{:__exception__, nil}], nil}, + {:struct, [], {:atom, ArgumentError}, nil}, + {:map, [], nil}, + {:struct, [], nil, nil} + ] + } + } = + var_with_guards("is_exception(x, ArgumentError)") - test "and combination predicate guards can be merge" do - assert %VarInfo{name: :x, type: {:intersection, [:number, :boolean]}} = - var_with_guards("is_number(x) and x >= 1") + assert %VarInfo{ + name: :x, + type: { + :intersection, + [ + {:struct, [], {:atom, ArgumentError}, nil}, + {:map, [], nil}, + {:struct, [], nil, nil} + ] + } + } = + """ + defmodule MyModule do + alias ArgumentError, as: MyURI - assert %VarInfo{ - name: :x, - type: {:intersection, [{:map, [a: nil], nil}, {:map, [b: nil], nil}]} - } = var_with_guards("is_map_key(x, :a) and is_map_key(x, :b)") - end + def func(x) when is_struct(x, MyURI) do + IO.puts "" + end + end + """ + |> string_to_state() + |> get_line_vars(5) + |> hd() + end - test "or combination predicate guards can be merge into union type" do - assert %VarInfo{name: :x, type: {:union, [:number, :atom]}} = - var_with_guards("is_number(x) or is_atom(x)") + test "and combination predicate guards can be merged" do + assert %VarInfo{name: :x, type: :number} = + var_with_guards("is_number(x) and x >= 1") - assert %VarInfo{name: :x, type: {:union, [:number, :atom, :binary]}} = - var_with_guards("is_number(x) or is_atom(x) or is_binary(x)") - end + assert %VarInfo{ + name: :x, + type: {:intersection, [{:map, [a: nil], nil}, {:map, [b: nil], nil}]} + } = var_with_guards("is_map_key(x, :a) and is_map_key(x, :b)") + end + + test "or combination predicate guards can be merge into union type" do + assert %VarInfo{name: :x, type: {:union, [:number, :atom]}} = + var_with_guards("is_number(x) or is_atom(x)") + + assert %VarInfo{name: :x, type: {:union, [:number, :atom, :binary]}} = + var_with_guards("is_number(x) or is_atom(x) or is_binary(x)") + end + + test "negated guards cannot be used for inference" do + assert %VarInfo{name: :x, type: nil} = + var_with_guards("not is_map(x)") + + assert %VarInfo{name: :x, type: nil} = + var_with_guards("not is_map(x) or is_atom(x)") + + assert %VarInfo{name: :x, type: :atom} = + var_with_guards("not is_map(x) and is_atom(x)") end end @@ -3931,6 +4661,60 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert get_line_aliases(state, 3) == [{User, Foo.User}] assert get_line_aliases(state, 5) == [] end + + defmodule Macro.AliasTest.Definer do + defmacro __using__(_options) do + quote do + @before_compile unquote(__MODULE__) + end + end + + defmacro __before_compile__(_env) do + quote do + defmodule First do + defstruct foo: :bar + end + + defmodule Second do + defstruct baz: %First{} + end + end + end + end + + defmodule Macro.AliasTest.Aliaser do + defmacro __using__(_options) do + quote do + alias Some.First + end + end + end + + test "macro alias does not leak outside macro" do + state = + """ + defmodule MyModule do + use ElixirSense.Core.MetadataBuilderTest.Macro.AliasTest.Definer + use ElixirSense.Core.MetadataBuilderTest.Macro.AliasTest.Aliaser + IO.puts "" + end + """ + |> string_to_state + + assert [{First, {_, Some.First}}] = state.lines_to_env[4].macro_aliases + + assert %{ + MyModule.First => %StructInfo{ + fields: [foo: :bar, __struct__: MyModule.First] + }, + MyModule.Second => %StructInfo{ + fields: [ + baz: {:%, [], [MyModule.First, {:%{}, [], [{:foo, :bar}]}]}, + __struct__: MyModule.Second + ] + } + } = state.structs + end end describe "import" do @@ -4038,30 +4822,84 @@ defmodule ElixirSense.Core.MetadataBuilderTest do state = """ defmodule OuterModule do - import List - import List + import List + import List + IO.puts "" + end + """ + |> string_to_state + + {functions, _macros} = get_line_imports(state, 4) + assert Keyword.keys(functions) == [List, Kernel] + end + + test "imports aliased module" do + state = + """ + defmodule OuterModule do + alias Enum, as: S + import S + IO.puts "" + end + """ + |> string_to_state + + {functions, _} = get_line_imports(state, 4) + assert Keyword.has_key?(functions, Enum) + end + + test "imports current buffer module" do + state = + """ + defmodule ImportedModule do + def some_fun(a), do: a + def _some_fun_underscored(a), do: a + defp some_fun_priv(a), do: a + defguard my_guard(x) when x > 0 + defguardp my_guard_priv(x) when x > 0 + defdelegate to_list(map), to: Map + defmacro some(a, b) do + quote do: unquote(a) + unquote(b) + end + defmacrop some_priv(a, b) do + quote do: unquote(a) + unquote(b) + end + defmacro _some_underscored(a, b) do + quote do: unquote(a) + unquote(b) + end + end + + defmodule OuterModule do + import ImportedModule IO.puts "" end """ |> string_to_state - {functions, _macros} = get_line_imports(state, 4) - assert Keyword.keys(functions) == [List, Kernel] + {functions, macros} = get_line_imports(state, 21) + assert Keyword.has_key?(functions, ImportedModule) + assert functions[ImportedModule] == [{:some_fun, 1}, {:to_list, 1}] + + assert Keyword.has_key?(macros, ImportedModule) + assert macros[ImportedModule] == [{:my_guard, 1}, {:some, 2}] end - test "imports aliased module" do + test "imports inside protocol" do state = """ - defmodule OuterModule do - alias Enum, as: S - import S + defprotocol OuterModule do IO.puts "" end """ |> string_to_state - {functions, _} = get_line_imports(state, 4) - assert Keyword.has_key?(functions, Enum) + {_functions, macros} = get_line_imports(state, 2) + assert Keyword.keys(macros) == [Protocol, Kernel] + kernel_macros = Keyword.fetch!(macros, Kernel) + assert {:def, 1} not in kernel_macros + assert {:defmacro, 1} not in kernel_macros + assert {:defdelegate, 2} not in kernel_macros + assert {:def, 1} in Keyword.fetch!(macros, Protocol) end end @@ -4109,6 +4947,81 @@ defmodule ElixirSense.Core.MetadataBuilderTest do [Application, Kernel, Kernel.Typespec, Mod] |> maybe_reject_typespec end + test "defmodule emits require with :defined meta" do + state = + """ + IO.puts "" + defmodule Foo.Bar do + IO.puts "" + defmodule Some.Mod do + IO.puts "" + end + IO.puts "" + end + IO.puts "" + """ + |> string_to_state + + assert state.lines_to_env[1].context_modules == [] + assert state.lines_to_env[3].context_modules == [Foo.Bar] + assert state.lines_to_env[5].context_modules == [Foo.Bar.Some.Mod, Foo.Bar] + assert state.lines_to_env[7].context_modules == [Foo.Bar.Some.Mod, Foo.Bar] + assert state.lines_to_env[9].context_modules == [Foo.Bar] + assert state.runtime_modules == [] + end + + test "defmodule emits require with :defined meta - runtime module" do + state = + """ + IO.puts "" + defmodule Foo.Bar do + IO.puts "" + def a do + defmodule Some.Mod do + IO.puts "" + def b, do: :ok + end + IO.puts "" + Some.Mod.b() + IO.puts "" + end + IO.puts "" + end + IO.puts "" + """ + |> string_to_state + + assert state.lines_to_env[1].context_modules == [] + assert state.lines_to_env[3].context_modules == [Foo.Bar] + assert state.lines_to_env[6].context_modules == [Foo.Bar.Some.Mod, Foo.Bar] + assert state.lines_to_env[9].context_modules == [Foo.Bar.Some.Mod, Foo.Bar] + assert state.lines_to_env[11].context_modules == [Foo.Bar.Some.Mod, Foo.Bar] + assert state.lines_to_env[13].context_modules == [Foo.Bar] + assert state.lines_to_env[15].context_modules == [Foo.Bar] + assert state.runtime_modules == [Foo.Bar.Some.Mod] + + assert state.lines_to_env[9].aliases == [{Some, Foo.Bar.Some}] + end + + test "requires local module" do + state = + """ + defmodule Mod do + defmacro some, do: :ok + end + + defmodule MyModule do + require Mod + Mod.some() + IO.puts "" + end + """ + |> string_to_state + + assert get_line_requires(state, 8) == + [Application, Kernel, Kernel.Typespec, Mod] |> maybe_reject_typespec + end + test "requires with __MODULE__" do state = """ @@ -4467,316 +5380,358 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert get_line_protocol(state, 16) == nil end - if @protocol_support do - test "current module and protocol implementation" do - state = - """ - defprotocol My.Reversible do - def reverse(term) - IO.puts "" - end - - defimpl My.Reversible, for: String do - def reverse(term), do: String.reverse(term) - IO.puts "" - end - - defimpl My.Reversible, for: [Map, My.List] do - def reverse(term), do: Enum.reverse(term) - IO.puts "" - - defmodule OuterModule do - IO.puts "" - end - - defprotocol Other do - def other(term) - IO.puts "" - end - - defimpl Other, for: [Map, My.Map] do - def other(term), do: nil - IO.inspect(__ENV__.module) - end - end - """ - |> string_to_state - - # protocol and implementations create modules - assert get_line_module(state, 3) == My.Reversible - assert get_line_protocol(state, 3) == nil - assert get_line_module(state, 8) == My.Reversible.String - assert get_line_protocol(state, 8) == {My.Reversible, [String]} - assert get_line_module(state, 13) == My.Reversible.Map - assert get_line_protocol(state, 13) == {My.Reversible, [Map, My.List]} + test "current module and protocol" do + state = + """ + defprotocol My.Reversible do + def reverse(term) + IO.puts "" + end + """ + |> string_to_state - # implementation has behaviour - assert get_line_behaviours(state, 8) == [My.Reversible] + # protocol and implementations create modules + assert get_line_module(state, 3) == My.Reversible + assert get_line_protocol(state, 3) == nil + end - # multiple implementations create multiple modules - assert get_line_module(state, 16) == My.Reversible.Map.OuterModule + test "current module and protocol implementation - simple case" do + state = + """ + defimpl Inspect, for: Atom do + IO.puts("") + end + """ + |> string_to_state - assert get_line_protocol(state, 16) == nil + assert get_line_module(state, 2) == Inspect.Atom + assert get_line_protocol(state, 2) == {Inspect, [Atom]} + end - # protocol and implementations inside protocol implementation creates a cross product - assert get_line_module(state, 21) == My.Reversible.Map.Other - assert get_line_protocol(state, 21) == nil + test "current module and protocol implementation" do + state = + """ + defprotocol My.Reversible do + def reverse(term) + IO.puts "" + end - assert get_line_module(state, 26) == My.Reversible.Map.Other.Map + defimpl My.Reversible, for: String do + def reverse(term), do: String.reverse(term) + IO.puts "" + end - assert get_line_protocol(state, 26) == {My.Reversible.Map.Other, [Map, My.Map]} - end - end - end + defimpl My.Reversible, for: [Map, My.List] do + def reverse(term), do: Enum.reverse(term) + IO.puts "" - if @protocol_support do - describe "protocol implementation" do - test "protocol implementation for atom modules" do - state = - """ - defprotocol :my_reversible do - def reverse(term) + defmodule OuterModule do IO.puts "" end - defimpl :my_reversible, for: [String, :my_str, :"Elixir.MyStr"] do - def reverse(term), do: String.reverse(term) + defprotocol Other do + def other(term) IO.puts "" end - defprotocol :"Elixir.My.Reversible" do - def reverse(term) - IO.puts "" + defimpl Other, for: [Map, My.Map] do + def other(term), do: nil + IO.inspect(__ENV__.module) end + end + """ + |> string_to_state - defimpl :"Elixir.My.Reversible", for: [String, :my_str, :"Elixir.MyStr"] do - def reverse(term), do: String.reverse(term) - IO.puts "" - end - """ - |> string_to_state + # protocol and implementations create modules + assert get_line_module(state, 3) == My.Reversible + assert get_line_protocol(state, 3) == nil + assert get_line_module(state, 8) == My.Reversible.String + assert get_line_protocol(state, 8) == {My.Reversible, [String]} + assert get_line_module(state, 13) == My.Reversible.My.List + assert get_line_protocol(state, 13) == {My.Reversible, [Map, My.List]} - assert get_line_module(state, 3) == :my_reversible - assert get_line_protocol(state, 3) == nil + # implementation has behaviour + assert get_line_behaviours(state, 8) == [My.Reversible] - assert get_line_module(state, 8) == :"Elixir.my_reversible.String" + # multiple implementations create multiple modules + assert get_line_module(state, 16) == My.Reversible.My.List.OuterModule - assert get_line_protocol(state, 8) == {:my_reversible, [String, :my_str, MyStr]} + assert get_line_protocol(state, 16) == nil - assert get_line_module(state, 13) == My.Reversible - assert get_line_protocol(state, 13) == nil + # protocol and implementations inside protocol implementation creates a cross product + assert get_line_module(state, 21) == My.Reversible.My.List.Other + assert get_line_protocol(state, 21) == nil - assert get_line_module(state, 18) == My.Reversible.String + assert get_line_module(state, 26) == My.Reversible.My.List.Other.My.Map - assert get_line_protocol(state, 18) == {My.Reversible, [String, :my_str, MyStr]} - end + assert get_line_protocol(state, 26) == {My.Reversible.My.List.Other, [Map, My.Map]} + end + end - test "protocol implementation module naming rules" do - state = - """ - defprotocol NiceProto do - def reverse(term) - end + describe "protocol implementation" do + test "protocol implementation for atom modules" do + state = + """ + defprotocol :my_reversible do + def reverse(term) + IO.puts "" + end - defmodule NiceProtoImplementations do - defimpl NiceProto, for: String do - def reverse(term), do: String.reverse(term) - def a3, do: IO.puts "OuterModule " <> inspect(__ENV__.aliases) - end - def a3, do: IO.puts "OuterModule " <> inspect(__ENV__.aliases) + defimpl :my_reversible, for: [String, :my_str, :"Elixir.MyStr"] do + def reverse(term), do: String.reverse(term) + IO.puts "" + end - defmodule Some do - defstruct [a: nil] - end + defprotocol :"Elixir.My.Reversible" do + def reverse(term) + IO.puts "" + end - defimpl NiceProto, for: Some do - def reverse(term), do: String.reverse(term) - IO.inspect(__ENV__.module) - end + defimpl :"Elixir.My.Reversible", for: [String, :my_str, :"Elixir.MyStr"] do + def reverse(term), do: String.reverse(term) + IO.puts "" + end + """ + |> string_to_state - alias Enumerable.Date.Range, as: R - alias NiceProto, as: N + assert get_line_module(state, 3) == :my_reversible + assert get_line_protocol(state, 3) == nil - defimpl N, for: R do - def reverse(term), do: String.reverse(term) - IO.puts "" - end - end - """ - |> string_to_state + assert get_line_module(state, 8) == :"Elixir.my_reversible.MyStr" - # protocol implementation module name does not inherit enclosing module, only protocol - assert get_line_module(state, 8) == NiceProto.String - assert get_line_protocol(state, 8) == {NiceProto, [String]} - assert get_line_aliases(state, 8) == [] - assert get_line_aliases(state, 10) == [] + assert get_line_protocol(state, 8) == {:my_reversible, [String, :my_str, MyStr]} - # properly gets implementation name inherited from enclosing module - assert get_line_module(state, 18) == NiceProto.NiceProtoImplementations.Some - assert get_line_protocol(state, 18) == {NiceProto, [NiceProtoImplementations.Some]} + assert get_line_module(state, 13) == My.Reversible + assert get_line_protocol(state, 13) == nil - # aliases are expanded on protocol and implementation - assert get_line_module(state, 24) == NiceProto.Enumerable.Date.Range - assert get_line_protocol(state, 24) == {NiceProto, [Enumerable.Date.Range]} - end + assert get_line_module(state, 18) == My.Reversible.MyStr - test "protocol implementation using __MODULE__" do - state = - """ - defprotocol NiceProto do - def reverse(term) + assert get_line_protocol(state, 18) == {My.Reversible, [String, :my_str, MyStr]} + end + + test "protocol implementation module naming rules" do + state = + """ + defprotocol NiceProto do + def reverse(term) + end + + defmodule NiceProtoImplementations do + defimpl NiceProto, for: String do + def reverse(term), do: String.reverse(term) + def a3, do: IO.puts "OuterModule " <> inspect(__ENV__.aliases) end + def a3, do: IO.puts "OuterModule " <> inspect(__ENV__.aliases) - defmodule MyStruct do + defmodule Some do defstruct [a: nil] - - defimpl NiceProto, for: __MODULE__ do - def reverse(term), do: String.reverse(term) - end end - """ - |> string_to_state - # protocol implementation module name does not inherit enclosing module, only protocol - assert get_line_module(state, 8) == NiceProto.MyStruct - assert get_line_protocol(state, 8) == {NiceProto, [MyStruct]} - end + defimpl NiceProto, for: Some do + def reverse(term), do: String.reverse(term) + IO.inspect(__ENV__.module) + end - test "protocol implementation using __MODULE__ 2" do - state = - """ - defmodule Nice do - defprotocol Proto do - def reverse(term) - end + alias Enumerable.Date.Range, as: R + alias NiceProto, as: N - defimpl __MODULE__.Proto, for: String do - def reverse(term), do: String.reverse(term) - end + defimpl N, for: R do + def reverse(term), do: String.reverse(term) + IO.puts "" end - """ - |> string_to_state + end + """ + |> string_to_state - assert get_line_module(state, 7) == Nice.Proto.String - assert get_line_protocol(state, 7) == {Nice.Proto, [String]} - end + # protocol implementation module name does not inherit enclosing module, only protocol + assert get_line_module(state, 8) == NiceProto.String + assert get_line_protocol(state, 8) == {NiceProto, [String]} + assert get_line_aliases(state, 8) == [] + assert get_line_aliases(state, 10) == [] - test "protocol implementation for structs does not require for" do - state = - """ - defprotocol Proto do - def reverse(term) - end + # properly gets implementation name inherited from enclosing module + assert get_line_module(state, 18) == NiceProto.NiceProtoImplementations.Some + assert get_line_protocol(state, 18) == {NiceProto, [NiceProtoImplementations.Some]} - defmodule MyStruct do - defstruct [:field] + # aliases are expanded on protocol and implementation + assert get_line_module(state, 24) == NiceProto.Enumerable.Date.Range + assert get_line_protocol(state, 24) == {NiceProto, [Enumerable.Date.Range]} + end - defimpl Proto do - def reverse(term), do: String.reverse(term) - end + test "protocol implementation using __MODULE__" do + state = + """ + defprotocol NiceProto do + def reverse(term) + end + + defmodule MyStruct do + defstruct [a: nil] + + defimpl NiceProto, for: __MODULE__ do + def reverse(term), do: String.reverse(term) end - """ - |> string_to_state + end + """ + |> string_to_state - assert get_line_module(state, 9) == Proto.MyStruct - assert get_line_protocol(state, 9) == {Proto, [MyStruct]} - end + # protocol implementation module name does not inherit enclosing module, only protocol + assert get_line_module(state, 8) == NiceProto.MyStruct + assert get_line_protocol(state, 8) == {NiceProto, [MyStruct]} + end - test "protocol implementation by deriving" do - state = - """ + test "protocol implementation using __MODULE__ 2" do + state = + """ + defmodule Nice do defprotocol Proto do def reverse(term) end - defimpl Proto, for: Any do - def reverse(term), do: term + defimpl __MODULE__.Proto, for: String do + def reverse(term), do: String.reverse(term) end + end + """ + |> string_to_state - defmodule MyStruct do - @derive Proto - defstruct [:field] - IO.puts "" - end - IO.puts "" + assert get_line_module(state, 7) == Nice.Proto.String + assert get_line_protocol(state, 7) == {Nice.Proto, [String]} + end + + test "protocol implementation for structs does not require for" do + state = + """ + defprotocol Proto do + def reverse(term) + end + + defmodule MyStruct do + defstruct [:field] - defmodule MyOtherStruct do - @derive [{Proto, opt: 1}, Enumerable] - defstruct [:field] + defimpl Proto do + def reverse(term), do: String.reverse(term) end - """ - |> string_to_state + end + """ + |> string_to_state - assert %{ - {Enumerable.MyOtherStruct, nil, nil} => %ModFunInfo{ - params: [nil], - type: :defmodule - }, - {Proto.Any, nil, nil} => %ModFunInfo{ - params: [nil], - type: :defmodule - }, - {Proto.MyOtherStruct, nil, nil} => %ModFunInfo{ - params: [nil], - type: :defmodule - }, - {Proto.MyOtherStruct, :reverse, 1} => %ModFunInfo{ - params: [[{:term, [line: 6, column: 15], nil}]], - type: :def - }, - {Proto.MyStruct, nil, nil} => %ModFunInfo{ - params: [nil], - type: :defmodule - }, - {Proto.MyStruct, :reverse, 1} => %ModFunInfo{ - params: [[{:term, [line: 6, column: 15], nil}]], - type: :def - } - } = state.mods_funs_to_positions - end + assert get_line_module(state, 9) == Proto.MyStruct + assert get_line_protocol(state, 9) == {Proto, [MyStruct]} end - test "protocol registers callbacks from specs or generate dummy callbacks" do + test "protocol implementation by deriving" do state = """ defprotocol Proto do - @spec with_spec(t, integer) :: String.t - @spec with_spec(t, boolean) :: number - def with_spec(t, integer) + def reverse(term) + end + + defimpl Proto, for: Any do + def reverse(term), do: term + end - def without_spec(t, integer) + defmodule MyStruct do + @derive Proto + defstruct [:field] + IO.puts "" + end + IO.puts "" + + defmodule MyOtherStruct do + @derive [{Proto, opt: 1}, Enumerable] + defstruct [:field] end """ |> string_to_state assert %{ - {Proto, :with_spec, 2} => %ElixirSense.Core.State.SpecInfo{ - args: [["t", "boolean"], ["t", "integer"]], - kind: :callback, - name: :with_spec, - positions: [{3, 3}, {2, 3}], - end_positions: [{3, 40}, {2, 42}], - generated: [false, false], - specs: [ - "@callback with_spec(t, boolean) :: number", - "@callback with_spec(t, integer) :: String.t" <> _, - "@spec with_spec(t, boolean) :: number", - "@spec with_spec(t, integer) :: String.t" <> _ - ] + {Enumerable.MyOtherStruct, nil, nil} => %ModFunInfo{ + params: [nil], + type: :defmodule }, - {Proto, :without_spec, 2} => %ElixirSense.Core.State.SpecInfo{ - args: [["t", "integer"]], - kind: :callback, - name: :without_spec, - positions: [{6, 3}], - end_positions: [nil], - generated: [true], - specs: ["@callback without_spec(t, integer) :: term"] + {Proto.Any, nil, nil} => %ModFunInfo{ + params: [nil], + type: :defmodule + }, + {Proto.MyOtherStruct, nil, nil} => %ModFunInfo{ + params: [nil], + type: :defmodule + }, + {Proto.MyOtherStruct, :reverse, 1} => %ModFunInfo{ + params: [[{:term, _, nil}]], + type: :def + }, + {Proto.MyStruct, nil, nil} => %ModFunInfo{ + params: [nil], + type: :defmodule + }, + {Proto.MyStruct, :reverse, 1} => %ModFunInfo{ + params: [[{:term, _, nil}]], + type: :def } - } = state.specs + } = state.mods_funs_to_positions end end + test "protocol registers callbacks from specs or generate dummy callbacks" do + state = + """ + defprotocol Proto do + @spec with_spec(t, integer) :: String.t + @spec with_spec(t, boolean) :: number + def with_spec(t, integer) + + def without_spec(t, integer) + end + """ + |> string_to_state + + assert %{ + {Proto, :with_spec, 2} => %ElixirSense.Core.State.SpecInfo{ + args: [["t()", "boolean()"], ["t()", "integer()"]], + kind: :callback, + name: :with_spec, + positions: [{3, 3}, {2, 3}], + end_positions: [{3, 40}, {2, 42}], + generated: [false, false], + specs: [ + "@callback with_spec(t(), boolean()) :: number()", + "@callback with_spec(t(), integer()) :: String.t()", + "@spec with_spec(t(), boolean()) :: number()", + "@spec with_spec(t(), integer()) :: String.t()" + ] + }, + {Proto, :without_spec, 2} => %ElixirSense.Core.State.SpecInfo{ + args: [["t()", "term()"]], + kind: :callback, + name: :without_spec, + positions: [{6, 3}], + end_positions: [nil], + generated: [true], + specs: ["@callback without_spec(t(), term()) :: term()"] + }, + # there is raw unquote in spec... + {Proto, :__protocol__, 1} => %ElixirSense.Core.State.SpecInfo{ + kind: :spec, + specs: [ + "@spec __protocol__(:impls) :: :not_consolidated | {:consolidated, list(module())}", + "@spec __protocol__(:consolidated?) :: boolean()", + "@spec __protocol__(:functions) :: :__unknown__", + "@spec __protocol__(:module) :: Proto" + ] + }, + {Proto, :impl_for, 1} => %ElixirSense.Core.State.SpecInfo{ + kind: :spec, + specs: ["@spec impl_for(term()) :: atom() | nil"] + }, + {Proto, :impl_for!, 1} => %ElixirSense.Core.State.SpecInfo{ + kind: :spec, + specs: ["@spec impl_for!(term()) :: atom()"] + } + } = state.specs + end + test "registers positions" do state = """ @@ -4813,54 +5768,86 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } = state.mods_funs_to_positions end - if @protocol_support do - test "registers positions in protocol implementation" do - state = - """ - defprotocol Reversible do - def reverse(term) - IO.puts "" - end + test "registers def positions in protocol" do + state = + """ + defprotocol Reversible do + def reverse(term) + IO.puts "" + end + """ + |> string_to_state - defimpl Reversible, for: String do - def reverse(term), do: String.reverse(term) + assert %{ + {Reversible, :reverse, 1} => %ModFunInfo{ + params: [[{:term, _, nil}]], + positions: [{2, 3}], + type: :def + } + } = state.mods_funs_to_positions + end + + test "registers def positions in protocol implementation" do + state = + """ + defprotocol Reversible do + def reverse(term) + IO.puts "" + end + + defimpl Reversible, for: String do + def reverse(term), do: String.reverse(term) + IO.puts "" + end + + defmodule Impls do + alias Reversible, as: R + alias My.List, as: Ml + defimpl R, for: [Map, Ml] do + def reverse(term), do: Enum.reverse(term) IO.puts "" end + end + """ + |> string_to_state - defmodule Impls do - alias Reversible, as: R - alias My.List, as: Ml - defimpl R, for: [Map, Ml] do - def reverse(term), do: Enum.reverse(term) - IO.puts "" - end - end - """ - |> string_to_state + assert %{ + {Impls, nil, nil} => %ModFunInfo{ + params: [nil], + positions: [{11, 1}], + type: :defmodule + }, + {Reversible.String, :__impl__, 1} => %ElixirSense.Core.State.ModFunInfo{ + params: [[{:atom, [line: 6, column: 1], nil}]], + positions: [{6, 1}], + type: :def + } + } = state.mods_funs_to_positions + end - assert %{ - {Impls, nil, nil} => %ModFunInfo{ - params: [nil], - positions: [{11, 1}], - type: :defmodule - }, - {Reversible, :reverse, 1} => %ModFunInfo{ - params: [[{:term, [line: 2, column: 15], nil}]], - positions: [{2, 3}], - type: :def - }, - {Reversible.String, :__impl__, 1} => %ElixirSense.Core.State.ModFunInfo{ - params: [[{:atom, [line: 6, column: 1], nil}]], - positions: [{6, 1}], - type: :def - }, - {Reversible, :behaviour_info, 1} => %ElixirSense.Core.State.ModFunInfo{ - params: [[{:atom, [line: 1, column: 1], nil}]], - positions: [{1, 1}], - type: :def - } - } = state.mods_funs_to_positions - end + test "functions head" do + state = + """ + defmodule OuterModule do + def abc(a \\\\ nil) + def abc(1), do: :ok + def abc(nil), do: :error + IO.puts "" + end + """ + |> string_to_state + + assert %{ + {OuterModule, :abc, 1} => %ModFunInfo{ + params: [ + [nil], + [1], + [ + {:\\, _, [{:a, _, nil}, nil]} + ] + ] + } + } = state.mods_funs_to_positions end test "functions with default args" do @@ -4877,10 +5864,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do {OuterModule, :abc, 4} => %ModFunInfo{ params: [ [ - {:a, [line: 2, column: 11], nil}, - {:\\, [line: 2, column: 16], [{:b, [line: 2, column: 14], nil}, nil]}, - {:c, [line: 2, column: 24], nil}, - {:\\, [line: 2, column: 29], [{:d, [line: 2, column: 27], nil}, [1]]} + {:a, _, nil}, + {:\\, _, [{:b, _, nil}, nil]}, + {:c, _, nil}, + {:\\, _, [{:d, _, nil}, [1]]} ] ] } @@ -4896,7 +5883,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do use Application @behaviour SomeModule.SomeBehaviour IO.puts "" - defmodule InnerModuleWithUse do + defmodule InnerModuleWithUse1 do use GenServer IO.puts "" end @@ -5081,15 +6068,13 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert nil == get_line_function(state, 31) assert Reversible == get_line_module(state, 31) - if @protocol_support do - assert nil == get_line_typespec(state, 35) - assert nil == get_line_function(state, 35) - assert Reversible.Map == get_line_module(state, 35) + assert nil == get_line_typespec(state, 35) + assert nil == get_line_function(state, 35) + assert Reversible.My.List == get_line_module(state, 35) - assert nil == get_line_typespec(state, 37) - assert {:reverse, 1} == get_line_function(state, 37) - assert Reversible.Map == get_line_module(state, 37) - end + assert nil == get_line_typespec(state, 37) + assert {:reverse, 1} == get_line_function(state, 37) + assert Reversible.My.List == get_line_module(state, 37) end test "finds positions for guards" do @@ -5106,11 +6091,11 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert %{ {MyModule, :is_even, 1} => %{ - params: [[{:value, [line: 2, column: 20], nil}]], + params: [[{:value, _, nil}]], positions: [{2, 3}] }, {MyModule, :is_odd, 1} => %{ - params: [[{:value, [line: 3, column: 20], nil}]], + params: [[{:value, _, nil}]], positions: [{3, 3}] }, {MyModule, :useless, 0} => %{ @@ -5157,19 +6142,19 @@ defmodule ElixirSense.Core.MetadataBuilderTest do type: :defp }, {MyModuleWithFuns, :is_even, 1} => %ModFunInfo{ - params: [[{:value, [line: 16, column: 20], nil}]], + params: [[{:value, _, nil}]], type: :defguard }, {MyModuleWithFuns, :is_evenp, 1} => %ModFunInfo{ - params: [[{:value, [line: 17, column: 22], nil}]], + params: [[{:value, _, nil}]], type: :defguardp }, {MyModuleWithFuns, :macro1, 1} => %ModFunInfo{ - params: [[{:ast, [line: 10, column: 19], nil}]], + params: [[{:ast, _, nil}]], type: :defmacro }, {MyModuleWithFuns, :macro1p, 1} => %ModFunInfo{ - params: [[{:ast, [line: 13, column: 21], nil}]], + params: [[{:ast, _, nil}]], type: :defmacrop }, {MyModuleWithFuns, nil, nil} => %ModFunInfo{ @@ -5185,7 +6170,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do type: :defmodule }, {MyModuleWithFuns, :__info__, 1} => %ElixirSense.Core.State.ModFunInfo{ - params: [[{:atom, [line: 3, column: 1], nil}]], + params: [[{:atom, _, nil}]], type: :def }, {MyModuleWithFuns, :module_info, 0} => %ElixirSense.Core.State.ModFunInfo{ @@ -5193,11 +6178,11 @@ defmodule ElixirSense.Core.MetadataBuilderTest do type: :def }, {MyModuleWithFuns, :module_info, 1} => %ElixirSense.Core.State.ModFunInfo{ - params: [[{:atom, [line: 3, column: 1], nil}]], + params: [[{:atom, _, nil}]], type: :def }, {MyModuleWithFuns.Nested, :__info__, 1} => %ElixirSense.Core.State.ModFunInfo{ - params: [[{:atom, [line: 19, column: 3], nil}]], + params: [[{:atom, _, nil}]], type: :def }, {MyModuleWithFuns.Nested, :module_info, 0} => %ElixirSense.Core.State.ModFunInfo{ @@ -5205,11 +6190,11 @@ defmodule ElixirSense.Core.MetadataBuilderTest do type: :def }, {MyModuleWithFuns.Nested, :module_info, 1} => %ElixirSense.Core.State.ModFunInfo{ - params: [[{:atom, [line: 19, column: 3], nil}]], + params: [[{:atom, _, nil}]], type: :def }, {MyModuleWithoutFuns, :__info__, 1} => %ElixirSense.Core.State.ModFunInfo{ - params: [[{:atom, [line: 1, column: 1], nil}]], + params: [[{:atom, _, nil}]], type: :def }, {MyModuleWithoutFuns, :module_info, 0} => %ElixirSense.Core.State.ModFunInfo{ @@ -5217,7 +6202,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do type: :def }, {MyModuleWithoutFuns, :module_info, 1} => %ElixirSense.Core.State.ModFunInfo{ - params: [[{:atom, [line: 1, column: 1], nil}]], + params: [[{:atom, _, nil}]], type: :def } } = state.mods_funs_to_positions @@ -5232,136 +6217,279 @@ defmodule ElixirSense.Core.MetadataBuilderTest do defdelegate func_delegated_erlang(par), to: :erlang_module defdelegate func_delegated_as(par), to: __MODULE__.Sub, as: :my_func defdelegate func_delegated_alias(par), to: E + defdelegate func_delegated_defaults(par \\\\ 123), to: E end """ |> string_to_state assert %{ {MyModuleWithFuns, :func_delegated, 1} => %ModFunInfo{ - params: [[{:par, [line: 3, column: 30], nil}]], + params: [[{:par, _, nil}]], positions: [{3, 3}], target: {OtherModule, :func_delegated}, type: :defdelegate }, {MyModuleWithFuns, :func_delegated_alias, 1} => %ModFunInfo{ - params: [[{:par, [line: 6, column: 36], nil}]], + params: [[{:par, _, nil}]], positions: [{6, 3}], target: {Enum, :func_delegated_alias}, type: :defdelegate }, {MyModuleWithFuns, :func_delegated_as, 1} => %ModFunInfo{ - params: [[{:par, [line: 5, column: 33], nil}]], + params: [[{:par, _, nil}]], positions: [{5, 3}], target: {MyModuleWithFuns.Sub, :my_func}, type: :defdelegate }, {MyModuleWithFuns, :func_delegated_erlang, 1} => %ModFunInfo{ - params: [[{:par, [line: 4, column: 37], nil}]], + params: [[{:par, _, nil}]], positions: [{4, 3}], target: {:erlang_module, :func_delegated_erlang}, type: :defdelegate + }, + {MyModuleWithFuns, :func_delegated_defaults, 1} => %ModFunInfo{ + params: [[{:\\, _, [{:par, _, nil}, 123]}]], + positions: [{7, 3}], + target: {Enum, :func_delegated_defaults}, + type: :defdelegate + } + } = state.mods_funs_to_positions + end + + test "gracefully handles delegated with unquote fragment" do + state = + """ + defmodule MyModuleWithFuns do + dynamic = :dynamic_flatten + defdelegate unquote(dynamic)(list), to: List, as: :flatten + end + """ + |> string_to_state + + assert %{ + {MyModuleWithFuns, :__unknown__, 1} => %ModFunInfo{ + target: {List, :flatten}, + type: :defdelegate + } + } = state.mods_funs_to_positions + end + + test "registers defs with unquote fragments in body" do + state = + """ + defmodule MyModuleWithFuns do + kv = [foo: 1] + Enum.each(kv, fn {k, v} -> + def foo(), do: unquote(v) + end) + end + """ + |> string_to_state + + assert %{ + {MyModuleWithFuns, :foo, 0} => %ModFunInfo{ + params: [[]] } } = state.mods_funs_to_positions end - if @expand_eval do - test "registers defs with unquote fragments" do + test "registers unknown for defs with unquote fragments in call" do + state = + """ + defmodule MyModuleWithFuns do + kv = [foo: 1, bar: 2] + Enum.each(kv, fn {k, v} -> + def unquote(k)(), do: 123 + end) + end + """ + |> string_to_state + + assert Map.keys(state.mods_funs_to_positions) == [ + {MyModuleWithFuns, :__info__, 1}, + {MyModuleWithFuns, :__unknown__, 0}, + {MyModuleWithFuns, :module_info, 0}, + {MyModuleWithFuns, :module_info, 1}, + {MyModuleWithFuns, nil, nil} + ] + end + + test "registers unknown for defdelegate with unquote fragments in call" do + state = + """ + defmodule MyModuleWithFuns do + kv = [foo: 1, bar: 2] + Enum.each(kv, fn {k, v} -> + defdelegate unquote(k)(), to: Foo + end) + end + """ + |> string_to_state + + assert Map.keys(state.mods_funs_to_positions) == [ + {MyModuleWithFuns, :__info__, 1}, + {MyModuleWithFuns, :__unknown__, 0}, + {MyModuleWithFuns, :module_info, 0}, + {MyModuleWithFuns, :module_info, 1}, + {MyModuleWithFuns, nil, nil} + ] + end + + # TODO test defguard with unquote fragment on 1.18 + + test "registers builtin functions for protocols" do + state = + """ + defprotocol Reversible do + def reverse(term) + IO.puts "" + end + """ + |> string_to_state + + assert Map.has_key?(state.mods_funs_to_positions, {Reversible, :__protocol__, 1}) + assert Map.has_key?(state.mods_funs_to_positions, {Reversible, :impl_for, 1}) + assert Map.has_key?(state.mods_funs_to_positions, {Reversible, :impl_for!, 1}) + assert Map.has_key?(state.mods_funs_to_positions, {Reversible, :impl_for!, 1}) + assert Map.has_key?(state.mods_funs_to_positions, {Reversible, :__info__, 1}) + assert Map.has_key?(state.mods_funs_to_positions, {Reversible, :module_info, 0}) + assert Map.has_key?(state.mods_funs_to_positions, {Reversible, :module_info, 1}) + assert Map.has_key?(state.mods_funs_to_positions, {Reversible, :behaviour_info, 1}) + end + + test "registers builtin functions for protocol implementations" do + state = + """ + defmodule MyModuleWithoutFuns do + end + defmodule MyModuleWithFuns do + def func do + IO.puts "" + end + defp funcp do + IO.puts "" + end + defmacro macro1(ast) do + IO.puts "" + end + defmacrop macro1p(ast) do + IO.puts "" + end + defguard is_even(value) when is_integer(value) and rem(value, 2) == 0 + defguardp is_evenp(value) when is_integer(value) and rem(value, 2) == 0 + defdelegate func_delegated(par), to: OtherModule + defmodule Nested do + end + end + + defprotocol Reversible do + def reverse(term) + IO.puts "" + end + + defimpl Reversible, for: String do + def reverse(term), do: String.reverse(term) + IO.puts "" + end + + defmodule Impls do + alias Reversible, as: R + alias My.List, as: Ml + defimpl R, for: [Ml, Map] do + def reverse(term), do: Enum.reverse(term) + IO.puts "" + end + end + """ + |> string_to_state + + assert Map.has_key?(state.mods_funs_to_positions, {Impls, :__info__, 1}) + assert Map.has_key?(state.mods_funs_to_positions, {Reversible.My.List, :__impl__, 1}) + end + + describe "macro expansion" do + defmodule WithMacros do + IO.inspect(__ENV__.module) + + defmacro go do + quote do + def my_fun, do: :ok + end + end + end + + test "expands remote macro" do state = """ - defmodule MyModuleWithFuns do - def unquote(:foo)(), do: :ok - def bar(), do: unquote(:ok) - def baz(unquote(:abc)), do: unquote(:abc) + defmodule SomeMod do + require ElixirSense.Core.MetadataBuilderTest.WithMacros, as: WithMacros + WithMacros.go() end """ |> string_to_state - assert %{ - {MyModuleWithFuns, :foo, 0} => %ModFunInfo{ - params: [[]] - }, - {MyModuleWithFuns, :bar, 0} => %ModFunInfo{ - params: [[]] - }, - {MyModuleWithFuns, :baz, 1} => %ModFunInfo{ - params: [[:abc]] - } - } = state.mods_funs_to_positions + assert %{{SomeMod, :my_fun, 0} => _} = state.mods_funs_to_positions end - test "registers defs with unquote fragments with binding" do + test "expands remote imported macro" do state = """ - defmodule MyModuleWithFuns do - kv = [foo: 1, bar: 2] |> IO.inspect - Enum.each(kv, fn {k, v} -> - def unquote(k)(), do: unquote(v) - end) + defmodule SomeMod do + import ElixirSense.Core.MetadataBuilderTest.WithMacros + go() end """ |> string_to_state - assert %{ - {MyModuleWithFuns, :foo, 0} => %ModFunInfo{ - params: [[]] - }, - {MyModuleWithFuns, :bar, 0} => %ModFunInfo{ - params: [[]] - } - } = state.mods_funs_to_positions + assert %{{SomeMod, :my_fun, 0} => _} = state.mods_funs_to_positions end - end - if @protocol_support do - test "registers mods and func for protocols" do - state = - """ - defmodule MyModuleWithoutFuns do - end - defmodule MyModuleWithFuns do - def func do - IO.puts "" - end - defp funcp do - IO.puts "" - end - defmacro macro1(ast) do - IO.puts "" - end - defmacrop macro1p(ast) do - IO.puts "" - end - defguard is_even(value) when is_integer(value) and rem(value, 2) == 0 - defguardp is_evenp(value) when is_integer(value) and rem(value, 2) == 0 - defdelegate func_delegated(par), to: OtherModule - defmodule Nested do - end + defmodule SomeCompiledMod do + defmacro go do + quote do + self() end + end - defprotocol Reversible do - def reverse(term) - IO.puts "" + defmacrop go_priv do + quote do + self() end + end + end - defimpl Reversible, for: String do - def reverse(term), do: String.reverse(term) - IO.puts "" - end + test "expands public local macro from compiled module" do + # NOTE we optimistically assume the previously compiled module version has + # the same macro implementation as the currently expanded + state = + """ + defmodule ElixirSense.Core.MetadataBuilderTest.SomeCompiledMod do + defmacro go do + quote do + self() + end + end - defmodule Impls do - alias Reversible, as: R - alias My.List, as: Ml - defimpl R, for: [Ml, Map] do - def reverse(term), do: Enum.reverse(term) - IO.puts "" + defmacrop go_priv do + quote do + self() + end + end + + def foo do + go() + go_priv() end end """ |> string_to_state - assert Map.has_key?(state.mods_funs_to_positions, {Impls, :__info__, 1}) - assert Map.has_key?(state.mods_funs_to_positions, {Reversible, :__protocol__, 1}) - assert Map.has_key?(state.mods_funs_to_positions, {Reversible.My.List, :__impl__, 1}) + assert [%CallInfo{func: :self}, %CallInfo{func: :go}] = state.calls[15] + + # NOTE as of elixir 1.17 MacroEnv.expand_import relies on module.__info__(:macros) for locals + # this means only public local macros are returned in our case + # making it work for all locals would require hooking into :elixir_def and compiling the code + assert [%CallInfo{func: :go_priv}] = state.calls[16] end end @@ -5410,8 +6538,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do MyMacros.Nested, MyMacros.One, MyMacros.Two.Three, - # TODO why isn't this required? - # Some.List, :ets, :lists ] @@ -5436,188 +6562,164 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ]) assert [ + %AttributeInfo{name: :before_compile, positions: [{2, _}]}, %AttributeInfo{name: :my_attribute, positions: [{2, _}]} ] = get_line_attributes(state, 4) - if @protocol_support do - assert %{ - {InheritMod, :handle_call, 3} => %ModFunInfo{ - params: [ - [ - {:msg, _, _}, - {:_from, _, _}, - {:state, _, _} - ] - ], - positions: [{2, 3}], - type: :def - }, - {InheritMod, nil, nil} => %ModFunInfo{ - params: [nil], - positions: [{1, 1}], - type: :defmodule - }, - {InheritMod, :private_func, 0} => %ModFunInfo{ - params: [[]], - positions: [{2, 3}], - type: :defp - }, - {InheritMod, :private_func_arg, 1} => %ModFunInfo{ - params: [ - [{:a, _, _}], - [{:\\, _, [{:a, _, _}, nil]}] - ], - positions: [{2, 3}, {2, 3}], - type: :defp - }, - {InheritMod, :private_guard, 0} => %ModFunInfo{ - params: [[]], - positions: [{2, 3}], - type: :defguardp - }, - {InheritMod, :private_guard_arg, 1} => %ModFunInfo{ - params: [ - [ - {:a, _, _} - ] - ], - positions: [{2, 3}], - type: :defguardp - }, - {InheritMod, :private_macro, 0} => %ModFunInfo{ - params: [[]], - positions: [{2, 3}], - type: :defmacrop - }, - {InheritMod, :private_macro_arg, 1} => %ModFunInfo{ - params: [ - [ - {:a, _, _} - ] - ], - positions: [{2, 3}], - type: :defmacrop - }, - {InheritMod, :public_func, 0} => %ModFunInfo{ - params: [[]], - positions: [{2, 3}], - type: :def, - overridable: {true, ElixirSenseExample.ExampleBehaviour} - }, - {InheritMod, :public_func_arg, 2} => %ModFunInfo{ - params: [ - [ - {:b, _, _}, - {:\\, _, - [ - {:a, _, _}, - "def" - ]} - ] - ], - positions: [{2, 3}], - type: :def - }, - {InheritMod, :public_guard, 0} => %ModFunInfo{ - params: [[]], - positions: [{2, 3}], - type: :defguard - }, - {InheritMod, :public_guard_arg, 1} => %ModFunInfo{ - params: [ - [ - {:a, _, _} - ] - ], - positions: [{2, 3}], - type: :defguard - }, - {InheritMod, :public_macro, 0} => %ModFunInfo{ - params: [[]], - positions: [{2, 3}], - type: :defmacro - }, - {InheritMod, :public_macro_arg, 1} => %ModFunInfo{ - params: [ - [ - {:a, _, _} - ] - ], - positions: [{2, 3}], - type: :defmacro - }, - {InheritMod.Deeply.Nested, nil, nil} => %ModFunInfo{ - params: [nil], - positions: [{2, 3}], - type: :defmodule - }, - {InheritMod.Nested, nil, nil} => %ModFunInfo{ - params: [nil], - positions: [{2, 3}], - type: :defmodule - }, - {InheritMod.ProtocolEmbedded, nil, nil} => %ModFunInfo{ - params: [nil], - positions: [{2, 3}], - type: :defmodule - }, - {InheritMod, :behaviour_info, 1} => %ModFunInfo{ - params: [[{:atom, [line: 2, column: 3], nil}]], - positions: [{2, 3}], - target: nil, - type: :def - }, - {InheritMod.ProtocolEmbedded, :module_info, 1} => %ModFunInfo{} - } = state.mods_funs_to_positions + assert %{ + {InheritMod, :handle_call, 3} => %ModFunInfo{ + params: [ + [ + {:msg, _, _}, + {:_from, _, _}, + {:state, _, _} + ] + ], + type: :def + }, + {InheritMod, nil, nil} => %ModFunInfo{ + type: :defmodule + }, + {InheritMod, :private_func, 0} => %ModFunInfo{ + params: [[]], + type: :defp + }, + {InheritMod, :private_func_arg, 1} => %ModFunInfo{ + params: [ + [{:a, _, _}], + [{:\\, _, [{:a, _, _}, nil]}] + ], + type: :defp + }, + {InheritMod, :private_guard, 0} => %ModFunInfo{ + params: [[]], + type: :defguardp + }, + {InheritMod, :private_guard_arg, 1} => %ModFunInfo{ + params: [ + [ + {:a, _, _} + ] + ], + type: :defguardp + }, + {InheritMod, :private_macro, 0} => %ModFunInfo{ + params: [[]], + type: :defmacrop + }, + {InheritMod, :private_macro_arg, 1} => %ModFunInfo{ + params: [ + [ + {:a, _, _} + ] + ], + type: :defmacrop + }, + {InheritMod, :public_func, 0} => %ModFunInfo{ + params: [[]], + type: :def, + overridable: {true, ElixirSenseExample.ExampleBehaviour} + }, + {InheritMod, :public_func_arg, 2} => %ModFunInfo{ + params: [ + [ + {:b, _, _}, + {:\\, _, + [ + {:a, _, _}, + "def" + ]} + ] + ], + type: :def + }, + {InheritMod, :public_guard, 0} => %ModFunInfo{ + params: [[]], + type: :defguard + }, + {InheritMod, :public_guard_arg, 1} => %ModFunInfo{ + params: [ + [ + {:a, _, _} + ] + ], + type: :defguard + }, + {InheritMod, :public_macro, 0} => %ModFunInfo{ + params: [[]], + type: :defmacro + }, + {InheritMod, :public_macro_arg, 1} => %ModFunInfo{ + params: [ + [ + {:a, _, _} + ] + ], + type: :defmacro + }, + {InheritMod.Deeply.Nested, nil, nil} => %ModFunInfo{ + type: :defmodule + }, + {InheritMod.Nested, nil, nil} => %ModFunInfo{ + type: :defmodule + }, + {InheritMod.ProtocolEmbedded, nil, nil} => %ModFunInfo{ + type: :defmodule + }, + {InheritMod, :behaviour_info, 1} => %ModFunInfo{ + params: [[{:atom, _, nil}]], + type: :def + }, + {InheritMod.ProtocolEmbedded, :module_info, 1} => %ModFunInfo{} + } = state.mods_funs_to_positions - assert %{ - {InheritMod, :my_opaque_type, 0} => %State.TypeInfo{ - args: [[]], - kind: :opaque, - name: :my_opaque_type, - positions: [{2, 3}], - specs: ["@opaque my_opaque_type :: any"] - }, - {InheritMod, :my_priv_type, 0} => %State.TypeInfo{ - args: [[]], - kind: :typep, - name: :my_priv_type, - positions: [{2, 3}], - specs: ["@typep my_priv_type :: any"] - }, - {InheritMod, :my_pub_type, 0} => %State.TypeInfo{ - args: [[]], - kind: :type, - name: :my_pub_type, - positions: [{2, 3}], - specs: ["@type my_pub_type :: any"] - }, - {InheritMod, :my_pub_type_arg, 2} => %State.TypeInfo{ - args: [["a", "b"]], - kind: :type, - name: :my_pub_type_arg, - positions: [{2, 3}], - specs: ["@type my_pub_type_arg(a, b) :: {b, a}"] - } - } = state.types + assert %{ + {InheritMod, :my_opaque_type, 0} => %State.TypeInfo{ + args: [[]], + kind: :opaque, + name: :my_opaque_type, + # positions: [{2, 3}], + specs: ["@opaque my_opaque_type() :: any()"] + }, + {InheritMod, :my_priv_type, 0} => %State.TypeInfo{ + args: [[]], + kind: :typep, + name: :my_priv_type, + # positions: [{2, 3}], + specs: ["@typep my_priv_type() :: any()"] + }, + {InheritMod, :my_pub_type, 0} => %State.TypeInfo{ + args: [[]], + kind: :type, + name: :my_pub_type, + # positions: [{2, 3}], + specs: ["@type my_pub_type() :: any()"] + }, + {InheritMod, :my_pub_type_arg, 2} => %State.TypeInfo{ + args: [["a", "b"]], + kind: :type, + name: :my_pub_type_arg, + # positions: [{2, 3}], + specs: ["@type my_pub_type_arg(a, b) :: {b, a}"] + } + } = state.types - assert %{ - {InheritMod, :private_func, 0} => %State.SpecInfo{ - args: [[]], - kind: :spec, - name: :private_func, - positions: [{2, 3}], - specs: ["@spec private_func() :: String.t()"] - }, - {InheritMod, :some_callback, 1} => %State.SpecInfo{ - args: [["abc"]], - kind: :callback, - name: :some_callback, - positions: [{2, 3}], - specs: ["@callback some_callback(abc) :: :ok when abc: integer"] - } - } = state.specs - end + assert %{ + {InheritMod, :private_func, 0} => %State.SpecInfo{ + args: [[]], + kind: :spec, + name: :private_func, + # positions: [{2, 3}], + specs: ["@spec private_func() :: String.t()"] + }, + {InheritMod, :some_callback, 1} => %State.SpecInfo{ + args: [["abc"]], + kind: :callback, + name: :some_callback, + # positions: [{2, 3}], + specs: ["@callback some_callback(abc) :: :ok when abc: integer()"] + } + } = state.specs end test "use defining struct" do @@ -5749,6 +6851,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do defmodule MyStruct do defstruct [:some_field, a_field: 1] IO.puts "" + %Date{month: 1, day: 1, year: 1} end """ |> string_to_state @@ -5780,22 +6883,19 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } = state.mods_funs_to_positions end - if @expand_eval do - test "find struct fields from expression" do - state = - """ - defmodule MyStruct do - @fields_1 [a: nil] - defstruct [a_field: nil] ++ @fields_1 - end - """ - |> string_to_state + test "gracefully handles struct with expression fields" do + state = + """ + defmodule MyStruct do + @fields_1 [a: nil] + defstruct [a_field: nil] ++ @fields_1 + end + """ + |> string_to_state - # TODO expression is not supported - assert state.structs == %{ - MyStruct => %StructInfo{type: :defstruct, fields: [__struct__: MyStruct]} - } - end + assert state.structs == %{ + MyStruct => %StructInfo{type: :defstruct, fields: [__struct__: MyStruct]} + } end test "find exception" do @@ -5885,147 +6985,362 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } } = state.mods_funs_to_positions end + + test "expands local struct" do + state = + """ + defmodule MyStruct do + defstruct [:some_field, a_field: 1] + var = %MyStruct{some_field: 3} + var = %MyStruct{} + IO.puts "" + end + """ + |> string_to_state + + assert state.structs == %{ + MyStruct => %StructInfo{ + type: :defstruct, + fields: [some_field: nil, a_field: 1, __struct__: MyStruct] + } + } + end + + test "expands local not existing struct" do + state = + """ + defmodule MyStruct do + var = %MyStruct{some_field: 3} + IO.puts "" + end + """ + |> string_to_state + + assert state.structs == %{} + end + + test "expands remote not existing struct" do + state = + """ + defmodule MyStruct do + var = %FooStruct{some_field: 3} + IO.puts "" + end + """ + |> string_to_state + + assert state.structs == %{} + end + + test "expands local struct defined in other module" do + state = + """ + defmodule MyStruct do + defstruct [:some_field, a_field: 1] + end + + defmodule Foo do + var = %MyStruct{some_field: 3} + IO.puts "" + end + """ + |> string_to_state + + assert state.structs == %{ + MyStruct => %StructInfo{ + type: :defstruct, + fields: [some_field: nil, a_field: 1, __struct__: MyStruct] + } + } + end end describe "calls" do - defp sort_calls(calls) do - calls |> Enum.map(fn {k, v} -> {k, Enum.sort(v)} end) |> Map.new() + test "registers calls on default parameters" do + state = + """ + defmodule NyModule do + def func1(a, b \\\\ some(), c \\\\ Some.other()), do: :ok + end + """ + |> string_to_state + + assert [ + %CallInfo{ + arity: 0, + func: :other, + mod: Some, + position: {2, 39} + }, + %CallInfo{arity: 0, position: {2, 21}, func: :some, mod: nil}, + %CallInfo{arity: 2, position: {2, 3}, func: :def, mod: Kernel} + ] = state.calls[2] + end + + test "registers calls with __MODULE__" do + state = + """ + defmodule NyModule do + def func1, do: :ok + def func2(a), do: :ok + def func do + __MODULE__.func1 + __MODULE__.func1() + __MODULE__.func2(2) + __MODULE__.Sub.func2(2) + end + end + """ + |> string_to_state + + assert %{ + 5 => [%CallInfo{arity: 0, func: :func1, position: {5, 16}, mod: NyModule}], + 6 => [%CallInfo{arity: 0, func: :func1, position: {6, 16}, mod: NyModule}], + 7 => [%CallInfo{arity: 1, func: :func2, position: {7, 16}, mod: NyModule}], + 8 => [%CallInfo{arity: 1, func: :func2, position: {8, 20}, mod: NyModule.Sub}] + } = state.calls + end + + test "registers calls with erlang module" do + state = + """ + defmodule NyModule do + def func do + :erl_mod.func1 + :erl_mod.func1() + :erl_mod.func2(2) + end + end + """ + |> string_to_state + + assert %{ + 3 => [%CallInfo{arity: 0, func: :func1, position: {3, 14}, mod: :erl_mod}], + 4 => [%CallInfo{arity: 0, func: :func1, position: {4, 14}, mod: :erl_mod}], + 5 => [%CallInfo{arity: 1, func: :func2, position: {5, 14}, mod: :erl_mod}] + } = state.calls + end + + test "registers calls with atom module" do + state = + """ + defmodule NyModule do + def func do + :"Elixir.MyMod".func1 + :"Elixir.MyMod".func1() + :"Elixir.MyMod".func2(2) + end + end + """ + |> string_to_state + + assert %{ + 3 => [%CallInfo{arity: 0, func: :func1, position: {3, 21}, mod: MyMod}], + 4 => [%CallInfo{arity: 0, func: :func1, position: {4, 21}, mod: MyMod}], + 5 => [%CallInfo{arity: 1, func: :func2, position: {5, 21}, mod: MyMod}] + } = state.calls + end + + test "registers calls no arg no parens" do + state = + """ + defmodule NyModule do + def func do + MyMod.func + end + end + """ + |> string_to_state + + assert %{ + 3 => [%CallInfo{arity: 0, func: :func, position: {3, 11}, mod: MyMod}] + } = state.calls + end + + test "registers calls no arg" do + state = + """ + defmodule NyModule do + def func do + MyMod.func() + end + end + """ + |> string_to_state + + assert %{ + 3 => [%CallInfo{arity: 0, func: :func, position: {3, 11}, mod: MyMod}] + } = state.calls + end + + test "registers calls local no arg no parens" do + state = + """ + defmodule NyModule do + def func_1, do: :ok + def func do + func_1 + end + end + """ + |> string_to_state + + if Version.match?(System.version(), ">= 1.15.0") do + assert state.calls + |> Enum.flat_map(fn {_line, info} -> info end) + |> Enum.filter(fn info -> info.mod != Kernel end) == [] + else + assert %{ + 4 => [%CallInfo{arity: 0, func: :func_1, position: {4, 5}, mod: nil}] + } = state.calls + end end - test "registers calls with __MODULE__" do + test "registers macro calls" do state = """ defmodule NyModule do - def func1, do: :ok - def func2(a), do: :ok + @foo "123" + require Record + Record.defrecord(:user, name: "meg", age: "25") def func do - __MODULE__.func1 - __MODULE__.func1() - __MODULE__.func2(2) - __MODULE__.Sub.func2(2) + IO.inspect(binding()) + :ok end end """ |> string_to_state - assert state.calls == %{ - 5 => [%CallInfo{arity: 0, func: :func1, position: {5, 16}, mod: NyModule}], - 6 => [%CallInfo{arity: 0, func: :func1, position: {6, 16}, mod: NyModule}], - 7 => [%CallInfo{arity: 1, func: :func2, position: {7, 16}, mod: NyModule}], - 8 => [%CallInfo{arity: 1, func: :func2, position: {8, 20}, mod: NyModule.Sub}] - } + assert %{ + 1 => [%CallInfo{arity: 2, position: {1, 1}, func: :defmodule, mod: Kernel}], + 2 => [%CallInfo{arity: 1, position: {2, 3}, func: :@, mod: Kernel}], + 4 => [%CallInfo{arity: 2, position: {4, 10}, func: :defrecord, mod: Record}], + 5 => [%CallInfo{arity: 2, position: {5, 3}, func: :def, mod: Kernel}], + 6 => [ + %CallInfo{arity: 1, position: {6, 8}, func: :inspect, mod: IO}, + %CallInfo{arity: 0, position: {6, 16}, func: :binding, mod: Kernel} + ] + } == state.calls end - test "registers calls with erlang module" do + # TODO track Kernel.SpecialForms calls? + + test "registers typespec no parens calls" do state = """ defmodule NyModule do - def func do - :erl_mod.func1 - :erl_mod.func1() - :erl_mod.func2(2) - end + @type a :: integer end """ |> string_to_state - assert state.calls == %{ - 3 => [%CallInfo{arity: 0, func: :func1, position: {3, 14}, mod: :erl_mod}], - 4 => [%CallInfo{arity: 0, func: :func1, position: {4, 14}, mod: :erl_mod}], - 5 => [%CallInfo{arity: 1, func: :func2, position: {5, 14}, mod: :erl_mod}] - } + assert %{ + 2 => [ + %CallInfo{arity: 0, func: :integer, position: {2, 14}, mod: nil}, + _ + ] + } = state.calls end - test "registers calls with atom module" do + test "registers typespec parens calls" do state = """ defmodule NyModule do - def func do - :"Elixir.MyMod".func1 - :"Elixir.MyMod".func1() - :"Elixir.MyMod".func2(2) - end + @type a() :: integer() end """ |> string_to_state - assert state.calls == %{ - 3 => [%CallInfo{arity: 0, func: :func1, position: {3, 21}, mod: MyMod}], - 4 => [%CallInfo{arity: 0, func: :func1, position: {4, 21}, mod: MyMod}], - 5 => [%CallInfo{arity: 1, func: :func2, position: {5, 21}, mod: MyMod}] - } + assert %{ + 2 => [ + %CallInfo{arity: 0, func: :integer, position: {2, 16}, mod: nil}, + _ + ] + } = state.calls end - test "registers calls no arg no parens" do + test "registers typespec no parens remote calls" do state = """ defmodule NyModule do - def func do - MyMod.func - end + @type a :: Enum.t end """ |> string_to_state - assert state.calls == %{ - 3 => [%CallInfo{arity: 0, func: :func, position: {3, 11}, mod: MyMod}] - } + assert %{ + 2 => [ + %CallInfo{arity: 0, func: :t, position: {2, 19}, mod: Enum}, + _ + ] + } = state.calls end - test "registers calls no arg" do + test "registers typespec parens remote calls" do state = """ defmodule NyModule do - def func do - MyMod.func() - end + @type a() :: Enum.t() + @type a(x) :: {Enum.t(), x} end """ |> string_to_state - assert state.calls == %{ - 3 => [%CallInfo{arity: 0, func: :func, position: {3, 11}, mod: MyMod}] - } + assert %{ + 2 => [ + %CallInfo{arity: 0, func: :t, position: {2, 21}, mod: Enum}, + _ + ], + 3 => [ + %CallInfo{arity: 0, func: :t, position: {3, 23}, mod: Enum}, + _ + ] + } = state.calls end - test "registers calls local no arg no parens" do + test "registers typespec calls in specs with when guard" do state = """ defmodule NyModule do - def func_1, do: :ok - def func do - func_1 - end + @callback a(b, c, d) :: {b, integer(), c} when b: map(), c: var, d: pos_integer end """ |> string_to_state - if Version.match?(System.version(), ">= 1.15.0") do - assert state.calls == %{} - else - assert state.calls == %{ - 4 => [%CallInfo{arity: 0, func: :func_1, position: {4, 5}, mod: nil}] - } - end + # NOTE var is not a type but a special variable + assert %{ + 2 => [ + %CallInfo{arity: 0, func: :pos_integer, position: {2, 71}, mod: nil}, + %CallInfo{arity: 0, func: :map, position: {2, 53}, mod: nil}, + %CallInfo{arity: 0, func: :integer, position: {2, 31}, mod: nil}, + _ + ] + } = state.calls end - if @typespec_calls_support do - test "registers typespec no parens calls" do - state = - """ - defmodule NyModule do - @type a :: integer - end - """ - |> string_to_state + test "registers typespec calls in typespec with named args" do + state = + """ + defmodule NyModule do + @callback days_since_epoch(year :: integer, month :: integer, day :: integer) :: integer + @type color :: {red :: integer, green :: integer, blue :: integer} + end + """ + |> string_to_state - assert state.calls == %{ - 2 => [ - %CallInfo{arity: 0, func: :integer, position: {2, 14}, mod: nil}, - %CallInfo{arity: 0, func: :a, position: {2, 9}, mod: nil} - ] - } - end + assert %{ + 2 => [ + %CallInfo{arity: 0, func: :integer, position: {2, 84}, mod: nil}, + %CallInfo{arity: 0, func: :integer, position: {2, 72}, mod: nil}, + %CallInfo{arity: 0, func: :integer, position: {2, 56}, mod: nil}, + %CallInfo{arity: 0, func: :integer, position: {2, 38}, mod: nil} | _ + ], + 3 => [ + %CallInfo{arity: 0, func: :integer, position: {3, 61}, mod: nil}, + %CallInfo{arity: 0, func: :integer, position: {3, 44}, mod: nil}, + %CallInfo{arity: 0, func: :integer, position: {3, 26}, mod: nil} | _ + ] + } = state.calls end test "registers calls local no arg" do @@ -6040,9 +7355,9 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ + assert %{ 4 => [%CallInfo{arity: 0, func: :func_1, position: {4, 5}, mod: nil}] - } + } = state.calls end test "registers calls local arg" do @@ -6056,9 +7371,9 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ + assert %{ 3 => [%CallInfo{arity: 1, func: :func_1, position: {3, 5}, mod: nil}] - } + } = state.calls end test "registers calls arg" do @@ -6072,16 +7387,17 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ + assert %{ 3 => [%CallInfo{arity: 1, func: :func, position: {3, 11}, mod: MyMod}] - } + } = state.calls end test "registers calls on attribute and var with args" do state = """ defmodule NyModule do - def func do + @attr Some + def func(var) do @attr.func("test") var.func("test") end @@ -6089,33 +7405,23 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - if Version.match?(System.version(), ">= 1.15.0") do - assert state.calls == %{ - 3 => [ - %CallInfo{arity: 1, func: :func, position: {3, 11}, mod: {:attribute, :attr}} - ], - 4 => [ - %CallInfo{arity: 1, func: :func, position: {4, 9}, mod: {:variable, :var}} - ] - } - else - assert state.calls == %{ - 3 => [ - %CallInfo{arity: 1, func: :func, position: {3, 11}, mod: {:attribute, :attr}} - ], - 4 => [ - %CallInfo{arity: 0, func: :var, position: {4, 5}, mod: nil}, - %CallInfo{arity: 1, func: :func, position: {4, 9}, mod: {:variable, :var}} - ] - } - end + assert %{ + 4 => [ + %CallInfo{arity: 1, func: :func, position: {4, 11}, mod: {:attribute, :attr}}, + _ + ], + 5 => [ + %CallInfo{arity: 1, func: :func, position: {5, 9}, mod: {:variable, :var, 0}} + ] + } = state.calls end test "registers calls on attribute and var without args" do state = """ defmodule NyModule do - def func do + @attr (fn -> :ok end) + def func(var) do @attr.func var.func end @@ -6123,33 +7429,29 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - if Version.match?(System.version(), ">= 1.15.0") do - assert state.calls == %{ - 3 => [ - %CallInfo{arity: 0, func: :func, position: {3, 11}, mod: {:attribute, :attr}} - ], - 4 => [ - %CallInfo{arity: 0, func: :func, position: {4, 9}, mod: {:variable, :var}} - ] - } - else - assert state.calls == %{ - 3 => [ - %CallInfo{arity: 0, func: :func, position: {3, 11}, mod: {:attribute, :attr}} - ], - 4 => [ - %CallInfo{arity: 0, func: :var, position: {4, 5}, mod: nil}, - %CallInfo{arity: 0, func: :func, position: {4, 9}, mod: {:variable, :var}} - ] - } - end + Enum.any?( + state.calls[4], + &match?( + %CallInfo{arity: 0, func: :func, position: {4, 11}, mod: {:attribute, :attr}}, + &1 + ) + ) + + Enum.any?( + state.calls[4], + &match?( + %CallInfo{arity: 0, func: :func, position: {5, 9}, mod: {:variable, :var, 0}}, + &1 + ) + ) end test "registers calls on attribute and var anonymous" do state = """ defmodule NyModule do - def func do + @attr (fn -> :ok end) + def func(var) do @attr.() var.() end @@ -6157,24 +7459,15 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - if Version.match?(System.version(), ">= 1.15.0") do - assert state.calls == %{ - 3 => [ - %CallInfo{arity: 0, func: {:attribute, :attr}, position: {3, 11}, mod: nil} - ], - 4 => [%CallInfo{arity: 0, func: {:variable, :var}, position: {4, 9}, mod: nil}] - } - else - assert state.calls == %{ - 3 => [ - %CallInfo{arity: 0, func: {:attribute, :attr}, position: {3, 11}, mod: nil} - ], - 4 => [ - %CallInfo{arity: 0, func: :var, position: {4, 5}, mod: nil}, - %CallInfo{arity: 0, func: {:variable, :var}, position: {4, 9}, mod: nil} - ] - } - end + assert %{ + 4 => [ + %CallInfo{arity: 0, func: {:attribute, :attr}, position: {4, 11}, mod: nil}, + _ + ], + 5 => [ + %CallInfo{arity: 0, func: {:variable, :var, 0}, position: {5, 9}, mod: nil} + ] + } = state.calls end test "registers calls pipe with __MODULE__ operator no parens" do @@ -6188,9 +7481,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ - 3 => [%CallInfo{arity: 1, func: :func, position: {3, 26}, mod: NyModule}] - } + assert Enum.any?( + state.calls[3], + &match?(%CallInfo{arity: 1, position: {3, 26}, func: :func, mod: NyModule}, &1) + ) end test "registers calls pipe operator no parens" do @@ -6204,9 +7498,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ - 3 => [%CallInfo{arity: 1, func: :func, position: {3, 21}, mod: MyMod}] - } + assert Enum.any?( + state.calls[3], + &match?(%CallInfo{arity: 1, position: {3, 21}, func: :func, mod: MyMod}, &1) + ) end test "registers calls pipe operator" do @@ -6220,9 +7515,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ - 3 => [%CallInfo{arity: 1, func: :func, position: {3, 21}, mod: MyMod}] - } + assert Enum.any?( + state.calls[3], + &match?(%CallInfo{arity: 1, position: {3, 21}, func: :func, mod: MyMod}, &1) + ) end test "registers calls pipe operator with arg" do @@ -6236,9 +7532,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ - 3 => [%CallInfo{arity: 2, func: :func, position: {3, 21}, mod: MyMod}] - } + assert Enum.any?( + state.calls[3], + &match?(%CallInfo{arity: 2, position: {3, 21}, func: :func, mod: MyMod}, &1) + ) end test "registers calls pipe operator erlang module" do @@ -6252,9 +7549,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ - 3 => [%CallInfo{arity: 2, func: :func, position: {3, 23}, mod: :my_mod}] - } + assert Enum.any?( + state.calls[3], + &match?(%CallInfo{arity: 2, position: {3, 23}, func: :func, mod: :my_mod}, &1) + ) end test "registers calls pipe operator atom module" do @@ -6268,9 +7566,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ - 3 => [%CallInfo{arity: 2, func: :func, position: {3, 31}, mod: MyMod}] - } + assert Enum.any?( + state.calls[3], + &match?(%CallInfo{arity: 2, position: {3, 31}, func: :func, mod: MyMod}, &1) + ) end test "registers calls pipe operator local" do @@ -6284,9 +7583,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ - 3 => [%CallInfo{arity: 2, func: :func, position: {3, 15}, mod: nil}] - } + assert Enum.any?( + state.calls[3], + &match?(%CallInfo{arity: 2, position: {3, 15}, func: :func, mod: nil}, &1) + ) end test "registers calls pipe operator nested external into local" do @@ -6300,12 +7600,15 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ - 3 => [ - %CallInfo{arity: 1, position: {3, 21}, func: :func, mod: MyMod}, - %CallInfo{arity: 1, position: {3, 31}, func: :other, mod: nil} - ] - } + assert Enum.any?( + state.calls[3], + &match?(%CallInfo{arity: 1, position: {3, 21}, func: :func, mod: MyMod}, &1) + ) + + assert Enum.any?( + state.calls[3], + &match?(%CallInfo{arity: 1, position: {3, 31}, func: :other, mod: nil}, &1) + ) end test "registers calls pipe operator nested external into external" do @@ -6319,12 +7622,15 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls |> sort_calls == %{ - 3 => [ - %CallInfo{arity: 1, position: {3, 21}, func: :func, mod: MyMod}, - %CallInfo{arity: 1, position: {3, 37}, func: :other, mod: Other} - ] - } + assert Enum.any?( + state.calls[3], + &match?(%CallInfo{arity: 1, position: {3, 21}, func: :func, mod: MyMod}, &1) + ) + + assert Enum.any?( + state.calls[3], + &match?(%CallInfo{arity: 1, position: {3, 37}, func: :other, mod: Other}, &1) + ) end test "registers calls pipe operator nested local into external" do @@ -6338,12 +7644,15 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls |> sort_calls == %{ - 3 => [ - %CallInfo{arity: 1, position: {3, 15}, func: :func_1, mod: nil}, - %CallInfo{arity: 1, position: {3, 32}, func: :other, mod: Some} - ] - } + assert Enum.any?( + state.calls[3], + &match?(%CallInfo{arity: 1, position: {3, 15}, func: :func_1, mod: nil}, &1) + ) + + assert Enum.any?( + state.calls[3], + &match?(%CallInfo{arity: 1, position: {3, 32}, func: :other, mod: Some}, &1) + ) end test "registers calls pipe operator nested local into local" do @@ -6357,12 +7666,65 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ - 3 => [ - %CallInfo{arity: 1, position: {3, 15}, func: :func_1, mod: nil}, - %CallInfo{arity: 1, position: {3, 27}, func: :other, mod: nil} - ] - } + assert Enum.any?( + state.calls[3], + &match?(%CallInfo{arity: 1, position: {3, 15}, func: :func_1, mod: nil}, &1) + ) + + assert Enum.any?( + state.calls[3], + &match?(%CallInfo{arity: 1, position: {3, 27}, func: :other, mod: nil}, &1) + ) + end + + test "registers super call" do + state = + """ + defmodule My do + use ElixirSenseExample.OverridableFunctions + + def test(a, b) do + super(a, b) + end + end + """ + |> string_to_state + + assert state.calls[5] == [%CallInfo{arity: 2, position: {5, 5}, func: :test, mod: nil}] + end + + test "registers super capture expression" do + state = + """ + defmodule My do + use ElixirSenseExample.OverridableFunctions + + def test(a, b) do + a |> Enum.map(&super(&1, b)) + end + end + """ + |> string_to_state + + assert [_, %CallInfo{arity: 2, position: {5, 20}, func: :test, mod: nil}, _] = + state.calls[5] + end + + test "registers super capture" do + state = + """ + defmodule My do + use ElixirSenseExample.OverridableFunctions + + def test(a, b) do + a |> Enum.map_reduce([], &super/2) + end + end + """ + |> string_to_state + + assert [_, %CallInfo{arity: 2, position: {5, 31}, func: :test, mod: nil}, _] = + state.calls[5] end test "registers calls capture operator __MODULE__" do @@ -6377,10 +7739,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ + assert %{ 3 => [%CallInfo{arity: 1, position: {3, 17}, func: :func, mod: NyModule}], 4 => [%CallInfo{arity: 1, position: {4, 21}, func: :func, mod: NyModule.Sub}] - } + } = state.calls end test "registers calls capture operator external" do @@ -6394,9 +7756,109 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ + assert %{ 3 => [%CallInfo{arity: 1, position: {3, 12}, func: :func, mod: MyMod}] - } + } = state.calls + end + + test "registers calls capture required macro" do + state = + """ + defmodule Foo do + defmacro bar, do: :ok + end + + defmodule NyModule do + require Foo + require ElixirSenseExample.Math + def func do + &Foo.bar/0 + &ElixirSenseExample.Math.squared/1 + end + end + """ + |> string_to_state + + assert %{ + 9 => [%CallInfo{arity: 0, position: {9, 10}, func: :bar, mod: Foo}], + 10 => [ + _, + %CallInfo{ + arity: 1, + position: {10, 30}, + func: :squared, + mod: ElixirSenseExample.Math + } + ] + } = state.calls + end + + # TODO reenable when https://github.com/elixir-lang/elixir/issues/13878 is resolved + # test "registers calls capture quoted" do + # state = + # """ + # defmodule MyModule do + # def aaa, do: :ok + # defmacro bbb, do: :ok + # defmacro foo do + # quote do + # aaa() + # &aaa/0 + # bbb() + # &bbb/0 + # inspect(1) + # &inspect/1 + # Node.list() + # &Node.list/0 + # end + # end + + # def go do + # foo() + # end + + # def bar do + # aaa() + # &aaa/0 + # bbb() + # &bbb/0 + # inspect(1) + # &inspect/1 + # Node.list() + # &Node.list/0 + # end + # end + # """ + # |> string_to_state + + # assert %{ + # 9 => [%CallInfo{arity: 0, position: {9, 10}, func: :bar, mod: Foo}], + # 10 => [ + # _, + # %CallInfo{ + # arity: 1, + # position: {10, 30}, + # func: :squared, + # mod: ElixirSenseExample.Math + # } + # ] + # } = state.calls + # end + + test "registers calls capture expression external" do + state = + """ + defmodule NyModule do + def func do + &MyMod.func(1, &1) + end + end + """ + |> string_to_state + + assert %{ + 3 => [%CallInfo{arity: 2, position: {3, 12}, func: :func, mod: MyMod}] + } = state.calls end test "registers calls capture operator external erlang module" do @@ -6410,9 +7872,9 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ + assert %{ 3 => [%CallInfo{arity: 1, func: :func, position: {3, 15}, mod: :erl_mod}] - } + } = state.calls end test "registers calls capture operator external atom module" do @@ -6426,58 +7888,113 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ + assert %{ 3 => [%CallInfo{arity: 1, func: :func, position: {3, 22}, mod: MyMod}] - } + } = state.calls + end + + test "registers calls capture import" do + state = + """ + defmodule NyModule do + import Node + def func do + &list/0 + &binding/0 + end + end + """ + |> string_to_state + + assert %{ + 4 => [%CallInfo{arity: 0, func: :nodes, position: {4, 6}, mod: :erlang}], + 5 => [%CallInfo{arity: 0, func: :binding, position: {5, 6}, mod: Kernel}] + } = state.calls end test "registers calls capture operator local" do state = """ defmodule NyModule do + def foo, do: ok + defmacro bar, do: :ok def func do &func/1 + &func/0 + &foo/0 + &bar/0 end end """ |> string_to_state - assert state.calls == %{ - 3 => [%CallInfo{arity: 1, func: :func, position: {3, 6}, mod: nil}] - } + assert %{ + 5 => [%CallInfo{arity: 1, func: :func, position: {5, 6}, mod: nil}], + 6 => [%CallInfo{arity: 0, func: :func, position: {6, 6}, mod: nil}], + 7 => [%CallInfo{arity: 0, func: :foo, position: {7, 6}, mod: nil}], + 8 => [%CallInfo{arity: 0, func: :bar, position: {8, 6}, mod: nil}] + } = state.calls + end + + test "registers calls capture expression local" do + state = + """ + defmodule NyModule do + def func do + &func(1, &1) + end + end + """ + |> string_to_state + + assert %{ + 3 => [%CallInfo{arity: 2, func: :func, position: {3, 6}, mod: nil}] + } = state.calls end - if @macro_calls_support do - test "registers calls on ex_unit DSL" do - state = - """ - defmodule MyModuleTest do - use ExUnit.Case + test "registers calls on ex_unit DSL" do + state = + """ + defmodule MyModuleTests do + use ExUnit.Case + + describe "describe1" do + test "test1" do + end + end + + test "test2", %{some: param} do + end + + test "not implemented" + end + """ + |> string_to_state + + assert Enum.any?( + state.calls[2], + &match?(%CallInfo{arity: 2, position: {2, _}, func: :__register__}, &1) + ) - describe "describe1" do - test "test1" do - end - end + assert Enum.any?( + state.calls[4], + &match?(%CallInfo{arity: 2, position: {4, 3}, func: :describe}, &1) + ) - test "test2", %{some: param} do - end + assert Enum.any?( + state.calls[5], + &match?(%CallInfo{arity: 2, position: {5, 5}, func: :test}, &1) + ) - test "not implemented" - end - """ - |> string_to_state + assert Enum.any?( + state.calls[9], + &match?(%CallInfo{arity: 3, position: {9, 3}, func: :test}, &1) + ) - assert state.calls == %{ - 2 => [ - %CallInfo{arity: 2, position: {2, 3}, func: :__register__, mod: ExUnit.Case}, - %CallInfo{arity: 2, position: {2, 3}, func: :unless, mod: nil} - ], - 4 => [%CallInfo{arity: 2, position: {4, 3}, func: :describe, mod: nil}], - 5 => [%CallInfo{arity: 2, position: {5, 5}, func: :test, mod: nil}], - 9 => [%CallInfo{arity: 3, position: {9, 3}, func: :test, mod: nil}], - 12 => [%CallInfo{arity: 0, position: {12, 3}, func: :test, mod: nil}] - } - end + assert Enum.any?( + state.calls[12], + &match?(%CallInfo{arity: 1, position: {12, 3}, func: :test}, &1) + ) end end @@ -6504,7 +8021,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do positions: [{2, 3}], end_positions: [{2, 36}], generated: [false], - specs: ["@type no_arg_no_parens :: integer"] + specs: ["@type no_arg_no_parens() :: integer()"] }, {My, :no_args, 0} => %ElixirSense.Core.State.TypeInfo{ args: [[]], @@ -6513,7 +8030,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do positions: [{3, 3}], end_positions: [{3, 30}], generated: [false], - specs: ["@typep no_args() :: integer"] + specs: ["@typep no_args() :: integer()"] }, {My, :overloaded, 0} => %ElixirSense.Core.State.TypeInfo{ args: [[]], @@ -6522,7 +8039,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do positions: [{5, 3}], end_positions: [{5, 25}], generated: [false], - specs: ["@type overloaded :: {}"] + specs: ["@type overloaded() :: {}"] }, {My, :overloaded, 1} => %ElixirSense.Core.State.TypeInfo{ kind: :type, @@ -6546,27 +8063,127 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } = state.types end - if @protocol_support do - test "protocol exports type t" do - state = - """ - defprotocol Proto do - def reverse(term) - end - """ - |> string_to_state - - assert state.types == %{ - {Proto, :t, 0} => %ElixirSense.Core.State.TypeInfo{ - args: [[]], - kind: :type, - name: :t, - positions: [{1, 1}], - end_positions: [{3, 4}], - generated: [true], - specs: ["@type t :: term"] - } + test "registers types with unquote fragments in body" do + state = + """ + defmodule My do + kv = [foo: 1] + Enum.each(kv, fn {k, v} -> + @type foo :: unquote(v) + end) + end + """ + |> string_to_state + + assert %{ + {My, :foo, 0} => %ElixirSense.Core.State.TypeInfo{ + args: [[]], + kind: :type, + name: :foo, + positions: [{4, 5}], + end_positions: _, + generated: [false], + specs: ["@type foo() :: :__unknown__"] + } + } = state.types + end + + test "store types as unknown when unquote fragments in call" do + state = + """ + defmodule My do + kv = [foo: 1] + Enum.each(kv, fn {k, v} -> + @type unquote(k)() :: 123 + end) + end + """ + |> string_to_state + + assert %{ + {My, :__unknown__, 0} => %ElixirSense.Core.State.TypeInfo{ + name: :__unknown__, + args: [[]], + specs: ["@type __unknown__() :: 123"], + kind: :type, + positions: [{4, 5}], + end_positions: [_], + generated: [false], + doc: "", + meta: %{hidden: true} + } + } = state.types + end + + test "registers incomplete types" do + state = + """ + defmodule My do + @type foo + @type bar() + @type baz(a) + end + """ + |> string_to_state + + assert %{ + {My, :foo, 0} => %ElixirSense.Core.State.TypeInfo{ + name: :foo, + args: [[]], + specs: ["@type foo() :: nil"], + kind: :type, + positions: [{2, 3}], + end_positions: [{2, 12}], + generated: [false], + doc: "", + meta: %{} + }, + {My, :bar, 0} => %ElixirSense.Core.State.TypeInfo{ + name: :bar, + args: [[]], + specs: ["@type bar() :: nil"], + kind: :type, + positions: [{3, 3}], + end_positions: [{3, 14}], + generated: [false], + doc: "", + meta: %{} + }, + {My, :baz, 1} => %ElixirSense.Core.State.TypeInfo{ + name: :baz, + args: [["a"]], + specs: ["@type baz(a) :: nil"], + kind: :type, + positions: [{4, 3}], + end_positions: _, + generated: [false], + doc: "", + meta: %{} + } + } = state.types + end + + test "protocol exports type t" do + state = + """ + defprotocol Proto do + def reverse(term) + end + """ + |> string_to_state + + assert %{ + {Proto, :t, 0} => %ElixirSense.Core.State.TypeInfo{ + args: [[]], + kind: :type, + name: :t, + specs: ["@type t() :: term()"], + doc: doc } + } = state.types + + if Version.match?(System.version(), ">= 1.15.0") do + assert "All the types that implement this protocol" <> _ = doc end end @@ -6585,119 +8202,176 @@ defmodule ElixirSense.Core.MetadataBuilderTest do # if there are callbacks behaviour_info/1 is defined assert state.mods_funs_to_positions[{Proto, :behaviour_info, 1}] != nil - if Version.match?(System.version(), ">= 1.13.0") do - assert %{ - {Proto, :abc, 0} => %ElixirSense.Core.State.SpecInfo{ - args: [[], []], - kind: :spec, - name: :abc, - positions: [{3, 3}, {2, 3}], - end_positions: [{3, 25}, {2, 30}], - generated: [false, false], - specs: ["@spec abc :: reference", "@spec abc :: atom | integer"] - }, - {Proto, :my, 1} => %ElixirSense.Core.State.SpecInfo{ - kind: :callback, - name: :my, - args: [["a :: integer"]], - positions: [{4, 3}], - end_positions: [{4, 37}], - generated: [false], - specs: ["@callback my(a :: integer) :: atom"] - }, - {Proto, :other, 1} => %ElixirSense.Core.State.SpecInfo{ - kind: :macrocallback, - name: :other, - args: [["x"]], - positions: [{5, 3}], - end_positions: [_], - generated: [false], - specs: ["@macrocallback other(x) :: Macro.t() when x: integer"] - } - } = state.specs - else - assert %{ - {Proto, :abc, 0} => %ElixirSense.Core.State.SpecInfo{ - args: [[], []], - kind: :spec, - name: :abc, - positions: [{3, 3}, {2, 3}], - end_positions: [{3, 25}, {2, 30}], - generated: [false, false], - specs: ["@spec abc :: reference", "@spec abc :: atom | integer"] - }, - {Proto, :my, 1} => %ElixirSense.Core.State.SpecInfo{ - kind: :callback, - name: :my, - args: [["a :: integer"]], - positions: [{4, 3}], - end_positions: [{4, 37}], - generated: [false], - specs: ["@callback my(a :: integer) :: atom"] - }, - {Proto, :other, 1} => %ElixirSense.Core.State.SpecInfo{ - kind: :macrocallback, - name: :other, - args: [["x"]], - positions: [{5, 3}], - end_positions: [_], - generated: [false], - specs: ["@macrocallback other(x) :: Macro.t when x: integer"] - } - } = state.specs - end + assert %{ + {Proto, :abc, 0} => %ElixirSense.Core.State.SpecInfo{ + args: [[], []], + kind: :spec, + name: :abc, + positions: [{3, 3}, {2, 3}], + end_positions: [{3, 25}, {2, 30}], + generated: [false, false], + specs: ["@spec abc() :: reference()", "@spec abc() :: atom() | integer()"] + }, + {Proto, :my, 1} => %ElixirSense.Core.State.SpecInfo{ + kind: :callback, + name: :my, + args: [["a :: integer()"]], + positions: [{4, 3}], + end_positions: [{4, 37}], + generated: [false], + specs: ["@callback my(a :: integer()) :: atom()"] + }, + {Proto, :other, 1} => %ElixirSense.Core.State.SpecInfo{ + kind: :macrocallback, + name: :other, + args: [["x"]], + positions: [{5, 3}], + end_positions: [_], + generated: [false], + specs: ["@macrocallback other(x) :: Macro.t() when x: integer()"] + } + } = state.specs + end + + test "registers incomplete specs" do + state = + """ + defmodule My do + @spec foo + @spec bar() + @spec baz(number) + end + """ + |> string_to_state + + assert %{ + {My, :foo, 0} => %ElixirSense.Core.State.SpecInfo{ + name: :foo, + args: [[]], + specs: ["@spec foo() :: nil"], + kind: :spec, + positions: [{2, 3}], + end_positions: [{2, 12}], + generated: [false], + doc: "", + meta: %{} + }, + {My, :bar, 0} => %ElixirSense.Core.State.SpecInfo{ + name: :bar, + args: [[]], + specs: ["@spec bar() :: nil"], + kind: :spec, + positions: [{3, 3}], + end_positions: [{3, 14}], + generated: [false], + doc: "", + meta: %{} + }, + {My, :baz, 1} => %ElixirSense.Core.State.SpecInfo{ + name: :baz, + args: [["number()"]], + specs: ["@spec baz(number()) :: nil"], + kind: :spec, + positions: [{4, 3}], + end_positions: _, + generated: [false], + doc: "", + meta: %{} + } + } = state.specs end test "specs and types expand aliases" do state = """ + defmodule Model.User do + defstruct name: nil + end + + defmodule Model.UserOrder do + defstruct order: nil + end + defmodule Proto do alias Model.User alias Model.Order alias Model.UserOrder @type local_type() :: User.t - @spec abc({%User{}}) :: [%UserOrder{order: Order.t}, local_type()] + @spec abc({%User{}}) :: {%UserOrder{order: Order.t}, local_type()} end """ |> string_to_state - if Version.match?(System.version(), ">= 1.13.0") do - assert %{ - {Proto, :abc, 1} => %State.SpecInfo{ - args: [["{%Model.User{}}"]], - specs: [ - "@spec abc({%Model.User{}}) :: [%Model.UserOrder{order: Model.Order.t()}, local_type()]" - ] - } - } = state.specs - else - assert %{ - {Proto, :abc, 1} => %State.SpecInfo{ - args: [["{%Model.User{}}"]], - specs: [ - "@spec abc({%Model.User{}}) :: [%Model.UserOrder{order: Model.Order.t}, local_type()]" - ] - } - } = state.specs - end + assert %{ + {Proto, :abc, 1} => %State.SpecInfo{ + args: [["{%Model.User{name: term()}}"]], + specs: [ + "@spec abc({%Model.User{name: term()}}) :: {%Model.UserOrder{order: Model.Order.t()}, local_type()}" + ] + } + } = state.specs - if Version.match?(System.version(), ">= 1.13.0") do - assert %{ - {Proto, :local_type, 0} => %State.TypeInfo{ - specs: ["@type local_type() :: Model.User.t()"] - } - } = state.types - else - assert %{ - {Proto, :local_type, 0} => %State.TypeInfo{ - specs: ["@type local_type() :: Model.User.t"] - } - } = state.types + assert %{ + {Proto, :local_type, 0} => %State.TypeInfo{ + specs: ["@type local_type() :: Model.User.t()"] + } + } = state.types + end + + defmodule TypespecMacros do + defmacro some() do + quote do + Foo + end end end + + test "specs and types expand macros in remote type" do + state = + """ + defmodule Proto do + require ElixirSense.Core.MetadataBuilderTest.TypespecMacros, as: TypespecMacros + @type local_type() :: TypespecMacros.some().foo(integer()) + end + """ + |> string_to_state + + assert %{ + {Proto, :local_type, 0} => %State.TypeInfo{ + specs: ["@type local_type() :: Foo.foo(integer())"] + } + } = state.types + end + + test "specs and types expand attributes in remote type" do + state = + """ + defmodule Proto do + @some Remote.Module + @type local_type() :: @some.foo(integer()) + IO.puts "" + end + """ + |> string_to_state + + assert %{ + {Proto, :local_type, 0} => %State.TypeInfo{ + specs: ["@type local_type() :: Remote.Module.foo(integer())"] + } + } = state.types + + assert [ + %AttributeInfo{ + positions: [{2, 3}, {3, 25}] + } + ] = state.lines_to_env[4].attributes + + assert [%CallInfo{position: {3, 25}}, %CallInfo{position: {3, 3}}] = + state.calls[3] |> Enum.filter(&(&1.func == :@)) + end end - if @record_support do + describe "defrecord" do test "defrecord defines record macros" do state = """ @@ -6716,12 +8390,12 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert %{ {MyRecords, :user, 1} => %ModFunInfo{ params: [[{:\\, [], [{:args, [], nil}, []]}]], - positions: [{3, 9}], + positions: [{3, 10}], type: :defmacro }, {MyRecords, :user, 2} => %ModFunInfo{ params: [[{:record, [], nil}, {:args, [], nil}]], - positions: [{3, 9}], + positions: [{3, 10}], type: :defmacro }, {MyRecords, :userp, 1} => %ModFunInfo{type: :defmacrop}, @@ -6731,7 +8405,39 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert %{ {MyRecords, :user, 0} => %State.TypeInfo{ name: :user, - specs: ["@type user :: record(:user, name: String.t(), age: integer)"] + specs: ["@type user() :: record(:user, name: String.t(), age: integer())"] + } + } = state.types + end + + test "defrecord imported defines record macros" do + state = + """ + defmodule MyRecords do + import Record + defrecord(:user, name: "meg", age: "25") + @type user :: record(:user, name: String.t(), age: integer) + end + """ + |> string_to_state + + assert %{ + {MyRecords, :user, 1} => %ModFunInfo{ + params: [[{:\\, [], [{:args, [], nil}, []]}]], + positions: [{3, 3}], + type: :defmacro + }, + {MyRecords, :user, 2} => %ModFunInfo{ + params: [[{:record, [], nil}, {:args, [], nil}]], + positions: [{3, 3}], + type: :defmacro + } + } = state.mods_funs_to_positions + + assert %{ + {MyRecords, :user, 0} => %State.TypeInfo{ + name: :user, + specs: ["@type user() :: record(:user, name: String.t(), age: integer())"] } } = state.types end @@ -6740,7 +8446,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do test "gets ExUnit imports from `use ExUnit.Case`" do state = """ - defmodule MyTest do + defmodule MyModuleTest do use ExUnit.Case IO.puts "" end @@ -6754,7 +8460,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do test "gets ExUnit imports from case template" do state = """ - defmodule MyTest do + defmodule My1Test do use ElixirSenseExample.CaseTemplateExample IO.puts "" end @@ -6798,14 +8504,14 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert %{ {My, :required, 1} => %ModFunInfo{ - params: [[{:var, [{:line, 2} | _], _}]], + params: [[{:var, _, _}]], positions: [{2, _}], target: nil, type: :defmacro, overridable: {true, ElixirSenseExample.OverridableFunctions} }, {My, :test, 2} => %ModFunInfo{ - params: [[{:x, [{:line, 2} | _], _}, {:y, [{:line, 2} | _], _}]], + params: [[{:x, _, _}, {:y, _, _}]], positions: [{2, _}], target: nil, type: :def, @@ -6832,7 +8538,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do overridable: {true, ElixirSenseExample.OverridableImplementation} }, {My, :bar, 1} => %ModFunInfo{ - params: [[{:var, [{:line, 2} | _], _}]], + params: [[{:var, _, _}]], positions: [{2, _}], target: nil, type: :defmacro, @@ -6859,8 +8565,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert %{ {My, :required, 1} => %ModFunInfo{ params: [ - [{:baz, [line: 8, column: 21], nil}], - [{:var, [{:line, 2} | _], _}] + [{:baz, _, nil}], + [{:var, _, _}] ], positions: [{8, 3}, {2, _}], target: nil, @@ -6869,8 +8575,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do }, {My, :test, 2} => %ModFunInfo{ params: [ - [{:a, [line: 4, column: 12], nil}, {:b, [line: 4, column: 15], nil}], - [{:x, [{:line, 2} | _], _}, {:y, [{:line, 2} | _], _}] + [{:a, _, nil}, {:b, _, nil}], + [{:x, _, _}, {:y, _, _}] ], positions: [{4, 3}, {2, _}], target: nil, @@ -6905,8 +8611,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do }, {My, :bar, 1} => %ModFunInfo{ params: [ - [{:baz, [line: 8, column: 16], nil}], - [{:var, [{:line, 2} | _], _}] + [{:baz, _, nil}], + [{:var, _, _}] ], positions: [{8, 3}, {2, _}], target: nil, @@ -6934,8 +8640,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert %{ {My, :required, 1} => %ModFunInfo{ params: [ - [{:baz, [line: 8, column: 22], nil}], - [{:var, [{:line, 2} | _], _}] + [{:baz, _, nil}], + [{:var, _, _}] ], positions: [{8, 3}, {2, _}], target: nil, @@ -6944,8 +8650,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do }, {My, :test, 2} => %ModFunInfo{ params: [ - [{:a, [line: 4, column: 13], nil}, {:b, [line: 4, column: 16], nil}], - [{:x, [{:line, 2} | _], _}, {:y, [{:line, 2} | _], _}] + [{:a, _, nil}, {:b, _, nil}], + [{:x, _, _}, {:y, _, _}] ], positions: [{4, 3}, {2, _}], target: nil, @@ -7200,6 +8906,36 @@ defmodule ElixirSense.Core.MetadataBuilderTest do state.mods_funs_to_positions[{Some, :macro, 0}] end + test "doc is applied to next delegate" do + state = + """ + defmodule Some do + @doc "Some fun" + @doc since: "1.2.3" + defdelegate count(a), to: Enum + end + """ + |> string_to_state + + assert %{doc: "Some fun", meta: %{since: "1.2.3"}} = + state.mods_funs_to_positions[{Some, :count, 1}] + end + + test "doc is applied to next guard" do + state = + """ + defmodule Some do + @doc "Some fun" + @doc since: "1.2.3" + defguard foo(a) when is_integer(a) + end + """ + |> string_to_state + + assert %{doc: "Some fun", meta: %{since: "1.2.3"}} = + state.mods_funs_to_positions[{Some, :foo, 1}] + end + test "doc false is applied to next function" do state = """ @@ -7467,6 +9203,30 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert state end + describe "module callbacks" do + defmodule Callbacks do + defmacro __before_compile__(_arg) do + quote do + def constant, do: 1 + defoverridable constant: 0 + end + end + end + + test "before_compile" do + state = + """ + defmodule User do + @before_compile ElixirSense.Core.MetadataBuilderTest.Callbacks + end + """ + |> string_to_state + + assert %ModFunInfo{meta: %{overridable: true}} = + state.mods_funs_to_positions[{User, :constant, 0}] + end + end + defp string_to_state(string) do string |> Code.string_to_quoted(columns: true, token_metadata: true) @@ -7481,7 +9241,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do env -> env.vars - # state.vars_info_per_scope_id[env.scope_id] end |> Enum.sort() end diff --git a/test/elixir_sense/core/metadata_test.exs b/test/elixir_sense/core/metadata_test.exs index 045b581b..11e2aee8 100644 --- a/test/elixir_sense/core/metadata_test.exs +++ b/test/elixir_sense/core/metadata_test.exs @@ -332,8 +332,9 @@ defmodule ElixirSense.Core.MetadataTest do env = Metadata.get_env(metadata, {49, 1}) assert env.module == Pr - assert env.function == nil - assert env.typespec == nil + # TODO this test should check cursor_env + # assert env.function == nil + # assert env.typespec == nil env = Metadata.get_env(metadata, {50, 3}) assert env.module == Pr @@ -351,12 +352,12 @@ defmodule ElixirSense.Core.MetadataTest do assert env.typespec == nil env = Metadata.get_env(metadata, {54, 3}) - assert env.module == Pr.String + assert env.module == Pr.List assert env.function == nil assert env.typespec == nil env = Metadata.get_env(metadata, {55, 3}) - assert env.module == Pr.String + assert env.module == Pr.List assert env.function == {:x, 1} assert env.typespec == nil end diff --git a/test/elixir_sense/core/parser_test.exs b/test/elixir_sense/core/parser_test.exs index 632931e1..7077732f 100644 --- a/test/elixir_sense/core/parser_test.exs +++ b/test/elixir_sense/core/parser_test.exs @@ -1,9 +1,12 @@ defmodule ElixirSense.Core.ParserTest do use ExUnit.Case, async: true - import ExUnit.CaptureIO - import ElixirSense.Core.Parser - alias ElixirSense.Core.{Metadata, State.Env, State.VarInfo} + alias ElixirSense.Core.{Metadata, State.Env, State.VarInfo, State.CallInfo, Parser} + + defp parse(source, cursor) do + metadata = Parser.parse_string(source, true, false, cursor) + {metadata, Metadata.get_cursor_env(metadata, cursor)} + end test "parse_string creates a Metadata struct" do source = """ @@ -13,15 +16,11 @@ defmodule ElixirSense.Core.ParserTest do end """ - assert %Metadata{ - error: {:error, :env_not_found}, - mods_funs_to_positions: %{{MyModule, nil, nil} => %{positions: [{1, 1}]}}, - lines_to_env: %{ - 1 => %Env{}, - 3 => %Env{functions: functions} - }, - source: "defmodule MyModule" <> _ - } = parse_string(source, true, true, {3, 3}) + assert {%Metadata{ + error: nil, + mods_funs_to_positions: %{{MyModule, nil, nil} => %{positions: [{1, 1}]}}, + source: "defmodule MyModule" <> _ + }, %Env{functions: functions}} = parse(source, {3, 3}) assert Keyword.has_key?(functions, List) end @@ -34,14 +33,9 @@ defmodule ElixirSense.Core.ParserTest do end """ - assert %Metadata{ - error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{module: MyModule}, - 2 => %Env{functions: _functions2, module: MyModule}, - 3 => %Env{functions: functions3, module: MyModule} - } - } = parse_string(source, true, true, {3, 10}) + assert {%Metadata{ + error: {:error, :parse_error} + }, %Env{functions: functions3, module: MyModule}} = parse(source, {3, 10}) assert Keyword.has_key?(functions3, List) end @@ -54,13 +48,9 @@ defmodule ElixirSense.Core.ParserTest do end """ - assert %Metadata{ - error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{}, - 3 => %Env{functions: functions} - } - } = parse_string(source, true, true, {3, 20}) + assert {%Metadata{ + error: {:error, :parse_error} + }, %Env{functions: functions}} = parse(source, {3, 20}) assert Keyword.has_key?(functions, List) end @@ -73,13 +63,9 @@ defmodule ElixirSense.Core.ParserTest do end """ - assert %Metadata{ - error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{}, - 3 => %Env{functions: functions} - } - } = parse_string(source, true, true, {3, 8}) + assert {%Metadata{ + error: {:error, :parse_error} + }, %Env{functions: functions}} = parse(source, {3, 8}) assert Keyword.has_key?(functions, List) end @@ -92,13 +78,9 @@ defmodule ElixirSense.Core.ParserTest do end """ - assert %Metadata{ - error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{}, - 3 => %Env{functions: functions} - } - } = parse_string(source, true, true, {3, 11}) + assert {%Metadata{ + error: {:error, :parse_error} + }, %Env{functions: functions}} = parse(source, {3, 11}) assert Keyword.has_key?(functions, List) end @@ -111,13 +93,9 @@ defmodule ElixirSense.Core.ParserTest do end """ - assert %Metadata{ - error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{}, - 3 => %Env{functions: functions} - } - } = parse_string(source, true, true, {3, 12}) + assert {%Metadata{ + error: {:error, :parse_error} + }, %Env{functions: functions}} = parse(source, {3, 12}) assert Keyword.has_key?(functions, List) end @@ -130,15 +108,13 @@ defmodule ElixirSense.Core.ParserTest do end """ - assert %Metadata{ - error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{}, - 3 => %Env{functions: functions} - } - } = parse_string(source, true, true, {3, 10}) + assert {%Metadata{ + error: {:error, :parse_error} + }, %Env{functions: functions}} = parse(source, {3, 10}) - assert Keyword.has_key?(functions, List) + if Version.match?(System.version(), ">= 1.15.0") do + assert Keyword.has_key?(functions, List) + end end test "parse_string with missing terminator \"\'\"" do @@ -149,15 +125,13 @@ defmodule ElixirSense.Core.ParserTest do end """ - assert %Metadata{ - error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{}, - 3 => %Env{functions: functions} - } - } = parse_string(source, true, true, {3, 10}) + assert {%Metadata{ + error: {:error, :parse_error} + }, %Env{functions: functions}} = parse(source, {3, 10}) - assert Keyword.has_key?(functions, List) + if Version.match?(System.version(), ">= 1.15.0") do + assert Keyword.has_key?(functions, List) + end end test "parse_string with missing heredoc terminator" do @@ -168,13 +142,9 @@ defmodule ElixirSense.Core.ParserTest do end """ - assert %Metadata{ - error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{}, - 3 => %Env{functions: functions} - } - } = parse_string(source, true, true, {3, 12}) + assert {%Metadata{ + error: {:error, :parse_error} + }, %Env{functions: functions}} = parse(source, {3, 12}) assert Keyword.has_key?(functions, List) end @@ -187,13 +157,9 @@ defmodule ElixirSense.Core.ParserTest do end """ - assert %Metadata{ - error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{}, - 3 => %Env{functions: functions} - } - } = parse_string(source, true, true, {3, 12}) + assert {%Metadata{ + error: {:error, :parse_error} + }, %Env{functions: functions}} = parse(source, {3, 12}) assert Keyword.has_key?(functions, List) end @@ -206,13 +172,9 @@ defmodule ElixirSense.Core.ParserTest do end """ - assert %Metadata{ - error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{}, - 3 => %Env{functions: functions} - } - } = parse_string(source, true, true, {3, 12}) + assert {%Metadata{ + error: {:error, :parse_error} + }, %Env{functions: functions}} = parse(source, {3, 12}) assert Keyword.has_key?(functions, List) end @@ -225,13 +187,9 @@ defmodule ElixirSense.Core.ParserTest do end """ - assert %Metadata{ - error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{}, - 3 => %Env{functions: functions} - } - } = parse_string(source, true, true, {3, 14}) + assert {%Metadata{ + error: {:error, :parse_error} + }, %Env{functions: functions}} = parse(source, {3, 14}) assert Keyword.has_key?(functions, List) end @@ -246,30 +204,15 @@ defmodule ElixirSense.Core.ParserTest do end """ - # assert capture_io(:stderr, fn -> - result = parse_string(source, true, true, {3, 23}) - # send(self(), {:result, result}) - # end) =~ "an expression is always required on the right side of ->" - - # assert_received {:result, result} - - assert %Metadata{ - error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{ - module: MyModule, - scope_id: 1 - }, - 3 => %Env{ - module: MyModule, - requires: _, - scope_id: 4, - vars: [ - %VarInfo{name: :x} - ] - } - } - } = result + {_metadata, env} = parse(source, {3, 23}) + + if Version.match?(System.version(), ">= 1.17.0") do + assert %Env{ + vars: [ + %VarInfo{name: :x} + ] + } = env + end end test "parse_string with missing terminator \"end\" attempts to insert `end` at correct indentation" do @@ -278,13 +221,9 @@ defmodule ElixirSense.Core.ParserTest do """ - assert %Metadata{ - error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{module: MyModule}, - 2 => %Env{module: MyModule} - } - } = parse_string(source, true, true, {2, 3}) + assert {%Metadata{ + error: {:error, :parse_error} + }, %Env{module: MyModule}} = parse(source, {2, 3}) source = """ defmodule MyModule do @@ -293,45 +232,29 @@ defmodule ElixirSense.Core.ParserTest do end """ - assert %Metadata{ - error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{module: MyModule}, - 3 => %Env{module: _} - } - } = parse_string(source, true, true, {3, 1}) - - assert %Metadata{ - error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{module: MyModule}, - 2 => %Env{module: MyModule} - } - } = parse_string(source, true, true, {2, 1}) + assert {%Metadata{ + error: {:error, :parse_error} + }, %Env{module: MyModule}} = parse(source, {3, 1}) + + assert {%Metadata{ + error: {:error, :parse_error} + }, %Env{module: MyModule}} = parse(source, {2, 1}) source = """ defmodule MyModule do defmodule MyModule1 do + end """ - assert %Metadata{ - error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{module: MyModule}, - 2 => %Env{module: MyModule}, - 3 => %Env{module: MyModule.MyModule1} - } - } = parse_string(source, true, true, {2, 1}) - - assert %Metadata{ - error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{module: MyModule}, - 3 => %Env{module: MyModule.MyModule1} - } - } = parse_string(source, true, true, {3, 1}) + assert {%Metadata{ + error: {:error, :parse_error} + }, %Env{module: MyModule}} = parse(source, {2, 1}) + + assert {%Metadata{ + error: {:error, :parse_error} + }, %Env{module: MyModule.MyModule1}} = parse(source, {4, 5}) end test "parse_string with incomplete key for multiline keyword as argument" do @@ -345,10 +268,14 @@ defmodule ElixirSense.Core.ParserTest do end """ - capture_io(:stderr, fn -> - assert %Metadata{error: {:error, :parse_error}, lines_to_env: %{5 => _}} = - parse_string(source, true, true, {5, 10}) - end) + assert {%Metadata{ + error: {:error, :parse_error}, + calls: %{ + 2 => [%CallInfo{func: :inspect}] + } + }, + %Env{module: MyModule}} = + parse(source, {5, 10}) end test "parse_string with missing value for multiline keyword as argument" do @@ -362,8 +289,14 @@ defmodule ElixirSense.Core.ParserTest do end """ - %Metadata{error: {:error, :parse_error}, lines_to_env: %{6 => _}} = - parse_string(source, true, true, {5, 12}) + assert {%Metadata{ + error: {:error, :parse_error}, + calls: %{ + 2 => [%CallInfo{func: :inspect}] + } + }, + %Env{module: MyModule}} = + parse(source, {5, 12}) end @tag capture_log: true @@ -377,15 +310,11 @@ defmodule ElixirSense.Core.ParserTest do end """ - assert %Metadata{ - error: {:error, :env_not_found}, - mods_funs_to_positions: %{{MyModule, nil, nil} => %{positions: [{1, 1}]}}, - lines_to_env: %{ - 1 => %Env{}, - 4 => %Env{functions: functions} - }, - source: "defmodule MyModule" <> _ - } = parse_string(source, true, true, {5, 3}) + assert {%Metadata{ + error: nil, + mods_funs_to_positions: %{{MyModule, nil, nil} => %{positions: [{1, 1}]}}, + source: "defmodule MyModule" <> _ + }, %Env{functions: functions}} = parse(source, {5, 3}) assert Keyword.has_key?(functions, List) end @@ -395,12 +324,9 @@ defmodule ElixirSense.Core.ParserTest do defmodule MyModule, do """ - assert %ElixirSense.Core.Metadata{ - error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{module: MyModule} - } - } = parse_string(source, true, true, {1, 23}) + assert {%ElixirSense.Core.Metadata{ + error: {:error, :parse_error} + }, %Env{module: MyModule}} = parse(source, {1, 23}) end test "parse_string with literal strings" do @@ -415,16 +341,12 @@ defmodule ElixirSense.Core.ParserTest do end ''' - assert %ElixirSense.Core.Metadata{ - lines_to_env: %{ - 6 => %ElixirSense.Core.State.Env{ - attributes: [%ElixirSense.Core.State.AttributeInfo{name: :my_attr}] - } - } - } = parse_string(source, true, true, {6, 6}) + assert {%ElixirSense.Core.Metadata{}, + %ElixirSense.Core.State.Env{ + attributes: [%ElixirSense.Core.State.AttributeInfo{name: :my_attr}] + }} = parse(source, {6, 6}) end - @tag only_this: true test "parse_string with literal strings in sigils" do source = ~S''' defmodule MyMod do @@ -437,18 +359,22 @@ defmodule ElixirSense.Core.ParserTest do end ''' - assert %ElixirSense.Core.Metadata{ - lines_to_env: %{ - 5 => %ElixirSense.Core.State.Env{ - vars: vars - } - } - } = parse_string(source, true, true, {5, 14}) - - assert [ - %ElixirSense.Core.State.VarInfo{name: :x}, - %ElixirSense.Core.State.VarInfo{name: :y} - ] = Enum.sort(vars) + assert {%Metadata{}, + %Env{ + vars: vars + }} = parse(source, {5, 14}) + + if Version.match?(System.version(), "< 1.15.0") do + # container_cursor_to_quoted removes function body + assert [ + %ElixirSense.Core.State.VarInfo{name: :y} + ] = Enum.sort(vars) + else + assert [ + %ElixirSense.Core.State.VarInfo{name: :x}, + %ElixirSense.Core.State.VarInfo{name: :y} + ] = Enum.sort(vars) + end end test "parse struct" do @@ -462,11 +388,11 @@ defmodule ElixirSense.Core.ParserTest do end """ - assert %ElixirSense.Core.Metadata{ - calls: %{ - 4 => [%{func: :foo}] - } - } = parse_string(source, true, true, {4, 7}) + assert {%ElixirSense.Core.Metadata{ + calls: %{ + 4 => [%CallInfo{func: :foo}] + } + }, %Env{function: {:func, 0}}} = parse(source, {4, 7}) end test "parse struct with missing terminator" do @@ -480,10 +406,10 @@ defmodule ElixirSense.Core.ParserTest do end """ - assert %ElixirSense.Core.Metadata{ - calls: %{ - 4 => [%{func: :foo}] - } - } = parse_string(source, true, true, {4, 8}) + assert {%ElixirSense.Core.Metadata{ + calls: %{ + 4 => [%{func: :foo}] + } + }, %Env{function: {:func, 0}}} = parse(source, {4, 8}) end end diff --git a/test/elixir_sense/core/source_test.exs b/test/elixir_sense/core/source_test.exs index 1577e916..cb660525 100644 --- a/test/elixir_sense/core/source_test.exs +++ b/test/elixir_sense/core/source_test.exs @@ -198,7 +198,13 @@ defmodule ElixirSense.Core.SourceTest do assert nil == which_func("var = my_var.some(", %ElixirSense.Core.Binding{ - variables: [%{name: "my_var", type: {:atom, Some}}] + variables: [ + %ElixirSense.Core.State.VarInfo{ + name: "my_var", + version: 1, + type: {:atom, Some} + } + ] }) end @@ -682,6 +688,74 @@ defmodule ElixirSense.Core.SourceTest do end end + describe "split_at/2" do + test "empty list" do + code = """ + defmodule Abcd do + def go do + :ok + end + end + """ + + assert split_at(code, []) == [code] + end + + test "one element list" do + code = """ + defmodule Abcd do + def go do + :ok + end + end + """ + + parts = split_at(code, [{2, 3}]) + assert parts == ["defmodule Abcd do\n ", "def go do\n :ok\n end\nend\n"] + assert Enum.join(parts) == code + end + + test "two element list same line" do + code = """ + defmodule Abcd do + def go do + :ok + end + end + """ + + parts = split_at(code, [{2, 3}, {2, 6}]) + assert parts == ["defmodule Abcd do\n ", "def", " go do\n :ok\n end\nend\n"] + assert Enum.join(parts) == code + end + + test "two element list different lines" do + code = """ + defmodule Abcd do + def go do + :ok + end + end + """ + + parts = split_at(code, [{2, 3}, {4, 6}]) + assert parts == ["defmodule Abcd do\n ", "def go do\n :ok\n end", "\nend\n"] + assert Enum.join(parts) == code + end + + test "handles positions at start and end of code" do + code = "abcdef" + positions = [{1, 1}, {1, 7}] + assert split_at(code, positions) == ["", "abcdef", ""] + end + + test "handles positions beyond code length" do + code = "short" + positions = [{0, 0}, {10, 15}] + assert split_at(code, positions) == ["", "short", ""] + end + end + describe "which_struct" do test "map" do code = """ @@ -700,7 +774,7 @@ defmodule ElixirSense.Core.SourceTest do var = %{asd | """ - assert which_struct(code, MyMod) == {:map, [], {:variable, :asd}} + assert which_struct(code, MyMod) == {:map, [], {:variable, :asd, :any}} end test "map update attribute" do @@ -720,7 +794,7 @@ defmodule ElixirSense.Core.SourceTest do var = %{asd | qwe: "ds", """ - assert which_struct(code, MyMod) == {:map, [:qwe], {:variable, :asd}} + assert which_struct(code, MyMod) == {:map, [:qwe], {:variable, :asd, :any}} end test "patern match with _" do @@ -893,10 +967,10 @@ defmodule ElixirSense.Core.SourceTest do """ assert which_struct(text_before(code, 3, 23), MyMod) == - {{:atom, Mod}, [], false, {:variable, :par1}} + {{:atom, Mod}, [], false, {:variable, :par1, :any}} assert which_struct(text_before(code, 5, 7), MyMod) == - {{:atom, Mod}, [:field1], false, {:variable, :par1}} + {{:atom, Mod}, [:field1], false, {:variable, :par1, :any}} end test "struct update attribute syntax" do @@ -1111,4 +1185,88 @@ defmodule ElixirSense.Core.SourceTest do assert "integer-un" == bitstring_options(text) end end + + describe "prefix/3" do + test "returns empty string when no prefix is found" do + code = "def example do\n :ok\nend" + assert "" == prefix(code, 2, 3) + end + + test "returns the correct prefix" do + code = "defmodule Test do\n def example_func do\n :ok\n end\nend" + assert "example_f" == prefix(code, 2, 16) + end + + test "handles line shorter than column" do + code = "short" + assert "" == prefix(code, 1, 10) + end + + test "handles line outside range" do + code = "short" + assert "" == prefix(code, 3, 1) + end + + test "returns prefix with special characters" do + code = "def example?!:@&^~+-<>=*/|\\() do\n :ok\nend" + assert "example?!:@&^~+-<>=*/|\\" == prefix(code, 1, 28) + end + + test "returns prefix at the end of line" do + code = "def example\ndef another" + assert "example" == prefix(code, 1, 12) + end + + test "handles empty lines" do + code = "\n\ndef example" + assert "" == prefix(code, 2, 1) + end + + test "returns prefix with numbers" do + code = "variable123 = 42" + assert "variable12" == prefix(code, 1, 11) + end + end + + describe "prefix_suffix/3" do + test "returns empty string when no prefix is found" do + code = "def example do\n :ok\nend" + assert {"", ":ok"} == prefix_suffix(code, 2, 3) + end + + test "returns the correct prefix" do + code = "defmodule Test do\n def example_func do\n :ok\n end\nend" + assert {"example_f", "unc"} == prefix_suffix(code, 2, 16) + end + + test "handles line shorter than column" do + code = "short" + assert {"", ""} == prefix_suffix(code, 1, 10) + end + + test "handles line outside range" do + code = "short" + assert {"", ""} == prefix_suffix(code, 3, 1) + end + + test "returns prefix with special characters" do + code = "def example?!:@&^~+-<>=*/|\\() do\n :ok\nend" + assert {"example?!:@&^~+-<>=*/", "|\\"} == prefix_suffix(code, 1, 26) + end + + test "returns prefix at the end of line" do + code = "def example\ndef another" + assert {"example", ""} == prefix_suffix(code, 1, 12) + end + + test "handles empty lines" do + code = "\n\ndef example" + assert {"", ""} == prefix_suffix(code, 2, 1) + end + + test "returns prefix with numbers" do + code = "variable123 = 42" + assert {"variable12", "3"} == prefix_suffix(code, 1, 11) + end + end end diff --git a/test/elixir_sense/core/type_inference/guard_test.exs b/test/elixir_sense/core/type_inference/guard_test.exs new file mode 100644 index 00000000..a89535ea --- /dev/null +++ b/test/elixir_sense/core/type_inference/guard_test.exs @@ -0,0 +1,323 @@ +defmodule ElixirSense.Core.TypeInference.GuardTest do + use ExUnit.Case, async: true + alias ElixirSense.Core.TypeInference.Guard + + defp wrap(guard) do + {_ast, vars} = + Macro.prewalk(guard, [], fn + {atom, _meta, var_context} = node, acc when is_atom(atom) and is_atom(var_context) -> + {node, [node | acc]} + + node, acc -> + {node, acc} + end) + + vars = + case Enum.uniq(vars) do + [var] -> var + list -> list + end + + {:fn, [], + [ + {:->, [], + [ + [ + {:when, [], + [ + vars, + guard + ]} + ], + :ok + ]} + ]} + end + + defp unwrap( + {:fn, [], + [ + {:->, [], + [ + [ + {:when, _, [_, guard]} + ], + _ + ]} + ]} + ) do + guard + end + + defp expand(ast) do + ast = wrap(ast) + env = :elixir_env.new() + {ast, _, _} = :elixir_expand.expand(ast, :elixir_env.env_to_ex(env), env) + unwrap(ast) + end + + describe "type_information_from_guards/1" do + test "infers type from naked var" do + guard_expr = quote(do: x) |> expand() + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => {:atom, true}} + end + + # 1. Simple guards + test "infers type from simple guard: is_number/1" do + guard_expr = quote(do: is_number(x)) |> expand() + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => :number} + end + + test "infers type from simple guard: is_binary/1" do + guard_expr = quote(do: is_binary(x)) |> expand() + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => :binary} + end + + test "infers type from simple guard: is_atom/1" do + guard_expr = quote(do: is_atom(x)) |> expand() + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => :atom} + end + + test "infers type from simple guard: is_nil/1" do + guard_expr = quote(do: is_nil(x)) |> expand() + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => {:atom, nil}} + end + + test "infers type from simple guard: == integer" do + guard_expr = quote(do: x == 5) |> expand() + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => {:integer, 5}} + end + + test "infers type from simple guard: == atom" do + guard_expr = quote(do: x == :foo) |> expand() + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => {:atom, :foo}} + end + + test "infers type from simple guard: == alias" do + guard_expr = quote(do: x == Some.Mod) |> expand() + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => {:atom, Some.Mod}} + end + + test "infers type from simple guard: == list empty" do + guard_expr = quote(do: x == []) |> expand() + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => {:list, :empty}} + end + + test "infers type from simple guard: == list" do + guard_expr = quote(do: x == [1]) |> expand() + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => {:list, {:integer, 1}}} + end + + test "infers type from simple guard: == map" do + guard_expr = quote(do: x == %{a: :b}) |> expand() + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => {:map, [a: {:atom, :b}], nil}} + end + + test "infers type from simple guard: == tuple empty" do + guard_expr = quote(do: x == {}) |> expand() + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => {:tuple, 0, []}} + end + + # 2. Guards with and + test "infers type from guard with and: is_number/1 and is_atom/1" do + guard_expr = quote(do: is_number(x) and is_atom(x)) |> expand() + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => {:intersection, [:number, :atom]}} + end + + # 3. Guards with or + test "infers type from guard with or: is_number/1 or is_binary/1" do + guard_expr = quote(do: is_number(x) or is_binary(x)) |> expand() + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => {:union, [:number, :binary]}} + end + + # 4. Guards with tuples + test "infers type from guard with tuple: is_tuple/1" do + guard_expr = quote(do: is_tuple(x)) |> expand + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => :tuple} + end + + test "infers type from guard with tuple_size/1" do + guard_expr = quote(do: tuple_size(x) == 2) |> expand + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => {:tuple, 2, [nil, nil]}} + end + + # 5. Guards with lists + test "infers type from guard with list: is_list/1" do + guard_expr = quote(do: is_list(x)) |> expand + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => :list} + end + + test "infers type from guard with list: hd/1 and tl/1" do + guard_expr = quote(do: hd(x) == 1 and tl(x) == [2]) |> expand + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => {:intersection, [{:list, {:integer, 1}}, :list]}} + end + + # 6. Guards with structs + test "infers type from guard with struct: is_map/1 and map_get/2" do + guard_expr = quote(do: is_struct(x, MyStruct)) |> expand() + result = Guard.type_information_from_guards(guard_expr) + + assert result == %{ + {:x, 0} => { + :intersection, + [ + {:struct, [], {:atom, MyStruct}, nil}, + {:map, [], nil}, + {:struct, [], nil, nil} + ] + } + } + end + + test "infers type from guard with struct: is_map_key/2" do + guard_expr = quote(do: is_map_key(x, :key)) |> expand() + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => {:map, [{:key, nil}], nil}} + end + end + + describe "type_information_from_guards not" do + test "handles not guard" do + guard_expr = quote(do: not is_number(x)) |> expand() + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => nil} + end + + # for simplicity we do not traverse not guards in the guard tree + # this should return :number type + test "handles nested not guards" do + guard = quote(do: not not is_number(x)) |> expand() + result = Guard.type_information_from_guards(guard) + assert result == %{{:x, 0} => nil} + end + + test "handles multiple variables in not guard" do + guard = quote(do: not (is_integer(x) and is_atom(y))) |> expand() + result = Guard.type_information_from_guards(guard) + assert result == %{{:x, 1} => nil, {:y, 0} => nil} + end + end + + describe "type_information_from_guards and" do + test "handles and with two guards" do + guard = quote(do: is_number(x) and is_atom(x)) |> expand() + + result = Guard.type_information_from_guards(guard) + + assert result == %{ + {:x, 0} => {:intersection, [:number, :atom]} + } + end + + test "handles nested and guards" do + guard = quote(do: is_number(x) and is_atom(x) and is_nil(x)) |> expand() + + result = Guard.type_information_from_guards(guard) + + assert result == %{{:x, 0} => {:intersection, [{:atom, nil}, :number, :atom]}} + end + + test "handles and with different variables" do + guard = quote(do: is_integer(x) and is_binary(y)) |> expand() + + result = Guard.type_information_from_guards(guard) + + assert result == %{{:x, 1} => :number, {:y, 0} => :binary} + end + end + + describe "type_information_from_guards or" do + test "handles or with simple types" do + guard = quote(do: is_integer(x) or is_binary(x)) |> expand() + + result = Guard.type_information_from_guards(guard) + assert result == %{{:x, 0} => {:union, [:number, :binary]}} + end + + test "handles nested or" do + guard = quote(do: is_number(x) or is_atom(x) or is_nil(x)) |> expand() + + result = Guard.type_information_from_guards(guard) + assert result == %{{:x, 0} => {:union, [:number, :atom, {:atom, nil}]}} + end + + test "handles or with different variables" do + guard = quote(do: is_integer(x) or is_binary(y)) |> expand() + + result = Guard.type_information_from_guards(guard) + assert result == %{{:x, 1} => nil, {:y, 0} => nil} + end + + test "handles or with existing unions" do + guard = quote(do: is_number(x) or is_atom(x) or (is_nil(x) or x)) |> expand() + + result = Guard.type_information_from_guards(guard) + assert result == %{{:x, 0} => {:union, [:number, :atom, {:atom, nil}, {:atom, true}]}} + end + + test "handles nested when" do + guard = quote(do: is_integer(x) when is_binary(x)) |> expand() + + result = Guard.type_information_from_guards(guard) + assert result == %{{:x, 0} => {:union, [:number, :binary]}} + end + end + + describe "guard on map field" do + test "naked" do + guard = quote(do: x.foo) |> expand() + + result = Guard.type_information_from_guards(guard) + assert result == %{{:x, 0} => {:map, [{:foo, {:atom, true}}], []}} + end + + test "naked nested" do + guard = quote(do: x.foo.bar) |> expand() + + result = Guard.type_information_from_guards(guard) + assert result == %{{:x, 0} => {:map, [{:foo, {:map, [{:bar, {:atom, true}}], []}}], []}} + end + + test "simple" do + guard = quote(do: is_atom(x.foo)) |> expand() + + result = Guard.type_information_from_guards(guard) + assert result == %{{:x, 0} => {:map, [{:foo, :atom}], []}} + end + + test "nested" do + guard = quote(do: is_atom(x.foo.bar.baz)) |> expand() + + result = Guard.type_information_from_guards(guard) + + assert result == %{ + {:x, 0} => {:map, [{:foo, {:map, [{:bar, {:map, [{:baz, :atom}], []}}], []}}], []} + } + end + + test "with operator" do + guard = quote(do: x.foo == 1) |> expand() + + result = Guard.type_information_from_guards(guard) + assert result == %{{:x, 0} => {:map, [{:foo, {:integer, 1}}], []}} + end + end +end diff --git a/test/elixir_sense/core/type_inference_test.exs b/test/elixir_sense/core/type_inference_test.exs new file mode 100644 index 00000000..ca63badb --- /dev/null +++ b/test/elixir_sense/core/type_inference_test.exs @@ -0,0 +1,547 @@ +defmodule ElixirSense.Core.TypeInferenceTest do + use ExUnit.Case, async: true + alias ElixirSense.Core.TypeInference + + describe "find_typed_vars" do + defp find_typed_vars_in(code, match_context \\ nil, context \\ nil) do + ast = + Code.string_to_quoted!(code) + |> Macro.prewalk(fn + {:__aliases__, _, list} -> + Module.concat(list) + + {atom, _meta, var_context} = node when is_atom(atom) and is_atom(var_context) -> + Macro.update_meta(node, &Keyword.put(&1, :version, 1)) + + node -> + node + end) + + TypeInference.find_typed_vars(ast, match_context, context) + end + + test "finds simple variable" do + assert find_typed_vars_in("a", nil, :match) == [{{:a, 1}, nil}] + assert find_typed_vars_in("a", nil) == [] + end + + test "finds simple variable with match context" do + assert find_typed_vars_in("a", {:integer, 1}, :match) == [{{:a, 1}, {:integer, 1}}] + assert find_typed_vars_in("a", {:integer, 1}) == [] + end + + test "does not find special variables" do + assert find_typed_vars_in("__MODULE__") == [] + assert find_typed_vars_in("__MODULE__", nil, :match) == [] + end + + test "does not find _" do + assert find_typed_vars_in("_") == [] + assert find_typed_vars_in("_", nil, :match) == [] + end + + test "does not find other primitives" do + assert find_typed_vars_in("1") == [] + assert find_typed_vars_in("1.3") == [] + assert find_typed_vars_in("\"as\"") == [] + end + + test "does not find pinned variables" do + assert find_typed_vars_in("^a") == [] + assert find_typed_vars_in("^a", nil, :match) == [] + end + + test "does not find variables in guard" do + assert find_typed_vars_in("_ when is_integer(a)", nil, :match) == [] + end + + # TODO should it find variables in bitstring size specifiers guard? + + test "finds variables in tuple" do + assert find_typed_vars_in("{}", nil, :match) == [] + assert find_typed_vars_in("{a}", nil, :match) == [{{:a, 1}, nil}] + assert find_typed_vars_in("{a}", :none, :match) == [{{:a, 1}, :none}] + assert find_typed_vars_in("{a}") == [] + + assert find_typed_vars_in("{a, b}", nil, :match) == [ + {{:a, 1}, nil}, + {{:b, 1}, nil} + ] + + assert find_typed_vars_in("{a, b}") == [] + end + + test "finds variables in tuple with match context" do + assert find_typed_vars_in("{a}", {:integer, 1}, :match) == [ + {{:a, 1}, {:tuple_nth, {:integer, 1}, 0}} + ] + + assert find_typed_vars_in("{a, b}", {:integer, 1}, :match) == [ + { + {:a, 1}, + {:tuple_nth, {:integer, 1}, 0} + }, + { + {:b, 1}, + {:tuple_nth, {:integer, 1}, 1} + } + ] + end + + test "finds variables in list" do + assert find_typed_vars_in("[]", nil, :match) == [] + assert find_typed_vars_in("[a]", nil, :match) == [{{:a, 1}, nil}] + assert find_typed_vars_in("[a]", :none, :match) == [{{:a, 1}, :none}] + assert find_typed_vars_in("[a]", nil) == [] + + assert find_typed_vars_in("[a, b]", nil, :match) == [ + {{:a, 1}, nil}, + {{:b, 1}, nil} + ] + + assert find_typed_vars_in("[a | b]", nil, :match) == [ + {{:a, 1}, nil}, + {{:b, 1}, nil} + ] + end + + test "finds variables in list with match context" do + assert find_typed_vars_in("[a]", {:integer, 1}, :match) == [ + {{:a, 1}, {:list_head, {:integer, 1}}} + ] + + assert find_typed_vars_in("[a, b]", {:integer, 1}, :match) == [ + {{:a, 1}, {:list_head, {:integer, 1}}}, + {{:b, 1}, {:list_head, {:list_tail, {:integer, 1}}}} + ] + + assert find_typed_vars_in("[a | b]", {:integer, 1}, :match) == [ + {{:a, 1}, {:list_head, {:integer, 1}}}, + {{:b, 1}, {:list_tail, {:integer, 1}}} + ] + + assert find_typed_vars_in("[1, a | b]", {:integer, 1}, :match) == [ + {{:a, 1}, {:list_head, {:list_tail, {:integer, 1}}}}, + {{:b, 1}, {:list_tail, {:list_tail, {:integer, 1}}}} + ] + + assert find_typed_vars_in("[a | 1]", {:integer, 1}, :match) == [ + {{:a, 1}, {:list_head, {:integer, 1}}} + ] + end + + test "finds variables in list operator" do + assert find_typed_vars_in(":erlang.++([a], [5])", {:integer, 1}, :match) == [ + {{:a, 1}, {:list_head, {:integer, 1}}} + ] + + assert find_typed_vars_in(":erlang.++([a], 5)", {:integer, 1}, :match) == [ + {{:a, 1}, {:list_head, {:integer, 1}}} + ] + + assert find_typed_vars_in(":erlang.++([2, a], [5])", {:integer, 1}, :match) == [ + {{:a, 1}, {:list_head, {:list_tail, {:integer, 1}}}} + ] + + assert find_typed_vars_in(":erlang.++([5], [a])", {:integer, 1}, :match) == [ + {{:a, 1}, {:list_head, {:list_tail, {:integer, 1}}}} + ] + + assert find_typed_vars_in(":erlang.++([5], [2, a])", {:integer, 1}, :match) == [ + {{:a, 1}, {:list_head, {:list_tail, {:list_tail, {:integer, 1}}}}} + ] + + assert find_typed_vars_in(":erlang.++([5], a)", {:integer, 1}, :match) == [ + {{:a, 1}, {:list_tail, {:integer, 1}}} + ] + + assert find_typed_vars_in(":erlang.++([5, 6], a)", {:integer, 1}, :match) == [ + {{:a, 1}, {:list_tail, {:list_tail, {:integer, 1}}}} + ] + end + + # TODO should it find vars in bitstring? + + test "finds variables in map" do + assert find_typed_vars_in("%{}", nil, :match) == [] + assert find_typed_vars_in("%{a: a}", nil, :match) == [{{:a, 1}, nil}] + assert find_typed_vars_in("%{a: a}", :none, :match) == [{{:a, 1}, :none}] + assert find_typed_vars_in("%{a: a}", nil) == [] + assert find_typed_vars_in("%{\"a\" => a}", nil, :match) == [{{:a, 1}, nil}] + # NOTE variable keys are forbidden in match + assert find_typed_vars_in("%{a => 1}", nil, :match) == [] + assert find_typed_vars_in("%{a => 1}", nil) == [] + # NOTE map update is forbidden in match + assert find_typed_vars_in("%{a | b: b}", nil, :match) == [] + assert find_typed_vars_in("%{a | b: b}", nil) == [] + end + + test "finds variables in map with match context" do + assert find_typed_vars_in("%{a: a}", {:integer, 1}, :match) == [ + {{:a, 1}, {:map_key, {:integer, 1}, {:atom, :a}}} + ] + end + + test "finds variables in struct" do + assert find_typed_vars_in("%Foo{}", nil, :match) == [] + assert find_typed_vars_in("%Foo{a: a}", nil, :match) == [{{:a, 1}, nil}] + assert find_typed_vars_in("%Foo{a: a}", :none, :match) == [{{:a, 1}, :none}] + assert find_typed_vars_in("%Foo{a: a}", nil) == [] + assert find_typed_vars_in("%bar{a: a}", nil) == [] + assert find_typed_vars_in("%bar{a: a}", nil, :match) == [{{:a, 1}, nil}, {{:bar, 1}, nil}] + assert find_typed_vars_in("%_{a: a}", nil, :match) == [{{:a, 1}, nil}] + assert find_typed_vars_in("%Foo{a | b: b}", nil) == [] + assert find_typed_vars_in("%Foo{a | b: b}", nil, :match) == [] + end + + test "finds variables in struct with match context" do + assert find_typed_vars_in("%Foo{a: a}", {:integer, 1}, :match) == [ + {{:a, 1}, {:map_key, {:integer, 1}, {:atom, :a}}} + ] + + assert find_typed_vars_in("%bar{a: a}", {:integer, 1}, :match) == [ + {{:a, 1}, {:map_key, {:integer, 1}, {:atom, :a}}}, + {{:bar, 1}, {:map_key, {:integer, 1}, {:atom, :__struct__}}} + ] + end + + test "finds variables in match" do + assert find_typed_vars_in("a = b", nil, :match) == [{{:b, 1}, nil}, {{:a, 1}, nil}] + assert find_typed_vars_in("a = b", nil) == [{{:a, 1}, {:variable, :b, 1}}] + assert find_typed_vars_in("^a = b", nil) == [] + + assert find_typed_vars_in("a = a", nil, :match) == [{{:a, 1}, nil}] + assert find_typed_vars_in("a = a", nil) == [{{:a, 1}, {:variable, :a, 1}}] + + assert find_typed_vars_in("a = b = c", nil, :match) == [ + {{:c, 1}, nil}, + {{:b, 1}, nil}, + {{:a, 1}, nil} + ] + + assert find_typed_vars_in("[a] = b", nil) == [{{:a, 1}, {:list_head, {:variable, :b, 1}}}] + + assert find_typed_vars_in("[a] = b", nil, :match) == [ + {{:b, 1}, {:list, nil}}, + {{:a, 1}, nil} + ] + + assert find_typed_vars_in("[a] = b", {:variable, :x}, :match) == [ + {{:b, 1}, {:intersection, [variable: :x, list: nil]}}, + {{:a, 1}, {:list_head, {:variable, :x}}} + ] + + assert find_typed_vars_in("{a} = b", nil) == [ + {{:a, 1}, {:tuple_nth, {:variable, :b, 1}, 0}} + ] + + assert find_typed_vars_in("{a} = b", nil, :match) == [ + {{:b, 1}, {:tuple, 1, [nil]}}, + {{:a, 1}, nil} + ] + + assert find_typed_vars_in("{a} = b", {:variable, :x}, :match) == [ + {{:b, 1}, {:intersection, [{:variable, :x}, {:tuple, 1, [nil]}]}}, + {{:a, 1}, {:tuple_nth, {:variable, :x}, 0}} + ] + + assert find_typed_vars_in("%{foo: a} = b", nil) == [ + {{:a, 1}, {:map_key, {:variable, :b, 1}, {:atom, :foo}}} + ] + + assert find_typed_vars_in("%{foo: a} = b", nil, :match) == [ + {{:b, 1}, {:map, [foo: nil], nil}}, + {{:a, 1}, nil} + ] + + assert find_typed_vars_in("%{foo: a} = b", {:variable, :x}, :match) == [ + {{:b, 1}, {:intersection, [{:variable, :x}, {:map, [foo: nil], nil}]}}, + {{:a, 1}, {:map_key, {:variable, :x}, {:atom, :foo}}} + ] + + assert find_typed_vars_in("%Foo{foo: a} = b", nil) == [ + {{:a, 1}, {:map_key, {:variable, :b, 1}, {:atom, :foo}}} + ] + + assert find_typed_vars_in("%Foo{foo: a} = b", nil, :match) == [ + {{:b, 1}, {:struct, [foo: nil], {:atom, Foo}, nil}}, + {{:a, 1}, nil} + ] + + assert find_typed_vars_in("%Foo{foo: a} = b", {:variable, :x}, :match) == [ + {{:b, 1}, + {:intersection, [{:variable, :x}, {:struct, [foo: nil], {:atom, Foo}, nil}]}}, + {{:a, 1}, {:map_key, {:variable, :x}, {:atom, :foo}}} + ] + + assert find_typed_vars_in("%{foo: a} = %{bar: b} = c", nil) == [ + { + {:a, 1}, + { + :map_key, + {:intersection, [{:map, [bar: nil], nil}, {:variable, :c, 1}]}, + {:atom, :foo} + } + }, + {{:b, 1}, + {:map_key, {:intersection, [{:map, [foo: nil], nil}, {:variable, :c, 1}]}, + {:atom, :bar}}} + ] + + assert find_typed_vars_in("%{foo: a} = %{bar: b} = c", nil, :match) == [ + { + {:c, 1}, + {:intersection, [{:map, [foo: nil], nil}, {:map, [bar: nil], nil}]} + }, + {{:a, 1}, {:map_key, {:map, [bar: nil], nil}, {:atom, :foo}}}, + {{:b, 1}, {:map_key, {:map, [foo: nil], nil}, {:atom, :bar}}} + ] + + assert find_typed_vars_in("%{foo: a} = %{bar: b} = c", {:variable, :x}, :match) == [ + { + {:c, 1}, + { + :intersection, + [{:map, [bar: nil], nil}, {:variable, :x}, {:map, [foo: nil], nil}] + } + }, + { + {:a, 1}, + { + :map_key, + {:intersection, [{:variable, :x}, {:map, [bar: nil], nil}]}, + {:atom, :foo} + } + }, + { + {:b, 1}, + { + :map_key, + {:intersection, [{:variable, :x}, {:map, [foo: nil], nil}]}, + {:atom, :bar} + } + } + ] + end + end + + describe "type_of" do + defp type_of(code, context \\ nil) do + # NOTE type_of works on expanded AST so it expects aliases expanded to atoms + ast = + Code.string_to_quoted!(code) + |> Macro.prewalk(fn + {:__aliases__, _, list} -> + Module.concat(list) + + {atom, _meta, var_context} = node when is_atom(atom) and is_atom(var_context) -> + Macro.update_meta(node, &Keyword.put(&1, :version, 1)) + + node -> + node + end) + + TypeInference.type_of(ast, context) + end + + test "atom" do + assert type_of(":a") == {:atom, :a} + assert type_of("My.Module") == {:atom, My.Module} + assert type_of("nil") == {:atom, nil} + assert type_of("true") == {:atom, true} + assert type_of("false") == {:atom, false} + end + + test "variable" do + assert type_of("a") == {:variable, :a, 1} + assert type_of("a", :match) == nil + assert type_of("^a", :match) == {:variable, :a, 1} + assert type_of("^a") == :none + assert type_of("_", :match) == nil + assert type_of("_") == :none + end + + test "attribute" do + assert type_of("@a") == {:attribute, :a} + end + + test "integer" do + assert type_of("1") == {:integer, 1} + end + + test "list" do + assert type_of("[]") == {:list, :empty} + assert type_of("[a]") == {:list, {:variable, :a, 1}} + assert type_of("[a | 1]") == {:list, {:variable, :a, 1}} + assert type_of("[a]", :match) == {:list, nil} + assert type_of("[a | 1]", :match) == {:list, nil} + assert type_of("[^a]", :match) == {:list, {:variable, :a, 1}} + assert type_of("[[1]]") == {:list, {:list, {:integer, 1}}} + # TODO union a | b? + assert type_of("[a, b]") == {:list, {:variable, :a, 1}} + assert type_of("[a | b]") == {:list, {:variable, :a, 1}} + assert type_of("[a, b | c]") == {:list, {:variable, :a, 1}} + end + + test "list operators" do + assert type_of(":erlang.++([a], [b])") == + {:call, {:atom, :erlang}, :++, + [list: {:variable, :a, 1}, list: {:variable, :b, 1}]} + + assert type_of(":erlang.--([a], [b])") == + {:call, {:atom, :erlang}, :--, + [list: {:variable, :a, 1}, list: {:variable, :b, 1}]} + end + + test "tuple" do + assert type_of("{}") == {:tuple, 0, []} + assert type_of("{a}") == {:tuple, 1, [{:variable, :a, 1}]} + assert type_of("{a, b}") == {:tuple, 2, [{:variable, :a, 1}, {:variable, :b, 1}]} + end + + test "map" do + assert type_of("%{}") == {:map, [], nil} + assert type_of("%{asd: a}") == {:map, [{:asd, {:variable, :a, 1}}], nil} + # NOTE non atom keys are not supported + assert type_of("%{\"asd\" => a}") == {:map, [], nil} + + assert type_of("%{b | asd: a}") == + {:map, [{:asd, {:variable, :a, 1}}], {:variable, :b, 1}} + + assert type_of("%{b | asd: a}", :match) == :none + end + + test "map with __struct__ key" do + assert type_of("%{__struct__: Foo}") == {:struct, [], {:atom, Foo}, nil} + + assert type_of("%{__struct__: Foo, asd: a}") == + {:struct, [{:asd, {:variable, :a, 1}}], {:atom, Foo}, nil} + + assert type_of("%{b | __struct__: Foo, asd: a}") == + {:struct, [{:asd, {:variable, :a, 1}}], {:atom, Foo}, {:variable, :b, 1}} + end + + test "struct" do + assert type_of("%Foo{}") == {:struct, [], {:atom, Foo}, nil} + assert type_of("%a{}") == {:struct, [], {:variable, :a, 1}, nil} + assert type_of("%@a{}") == {:struct, [], {:attribute, :a}, nil} + + assert type_of("%Foo{asd: a}") == + {:struct, [{:asd, {:variable, :a, 1}}], {:atom, Foo}, nil} + + assert type_of("%Foo{b | asd: a}") == + {:struct, [{:asd, {:variable, :a, 1}}], {:atom, Foo}, {:variable, :b, 1}} + + assert type_of("%Foo{b | asd: a}", :match) == :none + end + + test "range" do + assert type_of("a..b") == + {:struct, + [ + {:first, {:variable, :a, 1}}, + {:last, {:variable, :b, 1}}, + {:step, {:integer, 1}} + ], {:atom, Range}, nil} + + assert type_of("a..b//2") == + {:struct, + [ + {:first, {:variable, :a, 1}}, + {:last, {:variable, :b, 1}}, + {:step, {:integer, 2}} + ], {:atom, Range}, nil} + end + + test "sigil" do + # NOTE we do not attempt to parse sigils + assert type_of("~r//") == {:struct, [], {:atom, Regex}, nil} + assert type_of("~R//") == {:struct, [], {:atom, Regex}, nil} + assert type_of("~N//") == {:struct, [], {:atom, NaiveDateTime}, nil} + assert type_of("~U//") == {:struct, [], {:atom, DateTime}, nil} + assert type_of("~T//") == {:struct, [], {:atom, Time}, nil} + assert type_of("~D//") == {:struct, [], {:atom, Date}, nil} + end + + test "local call" do + assert type_of("foo(a)") == {:local_call, :foo, [{:variable, :a, 1}]} + end + + test "remote call" do + assert type_of(":foo.bar(a)") == {:call, {:atom, :foo}, :bar, [{:variable, :a, 1}]} + end + + test "match" do + assert type_of("a = 5") == {:integer, 5} + assert type_of("5 = a") == {:intersection, [{:integer, 5}, {:variable, :a, 1}]} + assert type_of("b = 5 = a") == {:intersection, [{:integer, 5}, {:variable, :a, 1}]} + assert type_of("5 = 5") == {:integer, 5} + + assert type_of("%{foo: a} = %{bar: b}") == + {:intersection, [{:map, [foo: nil], nil}, {:map, [bar: {:variable, :b, 1}], nil}]} + + assert type_of("%{foo: a} = %{bar: b}", :match) == + {:intersection, [{:map, [foo: nil], nil}, {:map, [bar: nil], nil}]} + end + + test "other" do + assert type_of("\"asd\"") == nil + assert type_of("1.23") == nil + end + + test "__STACKTRACE__ returns {:list, nil}" do + assert type_of("__STACKTRACE__") == {:list, nil} + end + + test "anonymous function returns nil" do + assert type_of("fn -> a end") == nil + assert type_of("fn x -> x + 1 end") == nil + assert type_of("fn x, y -> x * y end") == nil + end + end + + describe "block expressions" do + test "non-empty block returns type of last expression" do + assert type_of("(a = 1; b = 2; c = 3)") == {:integer, 3} + + assert type_of(""" + ( + a = 1 + b = 2 + c = 3 + ) + """) == {:integer, 3} + end + + test "empty block returns nil" do + assert type_of("( )") == nil + end + + test "__CALLER__ returns {:struct, [], {:atom, Macro.Env}, nil}" do + assert type_of("__CALLER__") == {:struct, [], {:atom, Macro.Env}, nil} + end + end + + describe "special forms" do + special_forms = [ + "case a do\n :ok -> 1\n :error -> 2\nend", + "cond do\n a -> 1\n b -> 2\nend", + "try do\n risky_operation()\nrescue\n e -> handle(e)\nend", + "receive do\n {:msg, msg} -> process(msg)\nend", + "for x <- list, do: x * 2", + "with {:ok, a} <- fetch_a(), {:ok, b} <- fetch_b(a), do: a + b", + "quote do: a + b", + "unquote(expr)", + "unquote_splicing(expr)", + "import Module", + "alias Module.SubModule", + "require Module" + ] + + for form <- special_forms do + test "special form: #{inspect(form)} returns nil" do + assert type_of(unquote(form)) == nil + end + end + end +end diff --git a/test/support/example_behaviour.ex b/test/support/example_behaviour.ex index e6cb30e0..872ba851 100644 --- a/test/support/example_behaviour.ex +++ b/test/support/example_behaviour.ex @@ -73,7 +73,6 @@ defmodule ElixirSenseExample.ExampleBehaviour do """ end - # TODO: Remove this on Elixir v2.0 @before_compile UseWithCallbacks @doc false diff --git a/test/support/fixtures/metadata_builder/import/only_sigils.ex b/test/support/fixtures/metadata_builder/import/only_sigils.ex index d6cfea7f..e0047037 100644 --- a/test/support/fixtures/metadata_builder/import/only_sigils.ex +++ b/test/support/fixtures/metadata_builder/import/only_sigils.ex @@ -1,13 +1,6 @@ -if Version.match?(System.version(), ">= 1.13.0") do - defmodule ElixirSenseExample.Fixtures.MetadataBuilder.Import.ImportOnlySigils do - import ElixirSenseExample.Fixtures.MetadataBuilder.Imported, only: :sigils +defmodule ElixirSenseExample.Fixtures.MetadataBuilder.Import.ImportOnlySigils do + import ElixirSenseExample.Fixtures.MetadataBuilder.Imported, only: :sigils - @env __ENV__ - def env, do: @env - end -else - defmodule ElixirSenseExample.Fixtures.MetadataBuilder.Import.ImportOnlySigils do - @env __ENV__ - def env, do: @env - end + @env __ENV__ + def env, do: @env end diff --git a/test/support/macro_hygiene.ex b/test/support/macro_hygiene.ex new file mode 100644 index 00000000..13a5b71b --- /dev/null +++ b/test/support/macro_hygiene.ex @@ -0,0 +1,8 @@ +defmodule ElixirSenseExample.Math do + defmacro squared(x) do + quote do + x = unquote(x) + x * x + end + end +end diff --git a/test/support/plugins/ecto/fake_schemas.ex b/test/support/plugins/ecto/fake_schemas.ex deleted file mode 100644 index 006f15ae..00000000 --- a/test/support/plugins/ecto/fake_schemas.ex +++ /dev/null @@ -1,77 +0,0 @@ -defmodule ElixirSense.Plugins.Ecto.FakeSchemas.User do - @moduledoc """ - Fake User schema. - - The docs. - """ - - def __schema__(:fields), do: [:id, :name, :email] - def __schema__(:associations), do: [:assoc1, :assoc2] - def __schema__(:type, :id), do: :id - def __schema__(:type, :name), do: :string - def __schema__(:type, :email), do: :string - - def __schema__(:association, :assoc1), - do: %{related: FakeAssoc1, owner: __MODULE__, owner_key: :assoc1_id} - - def __schema__(:association, :assoc2), - do: %{related: FakeAssoc2, owner: __MODULE__, owner_key: :assoc2_id} -end - -defmodule ElixirSense.Plugins.Ecto.FakeSchemas.Comment do - @moduledoc """ - Fake Comment schema. - """ - - alias ElixirSense.Plugins.Ecto.FakeSchemas.Post - - def __schema__(:fields), do: [:content, :date] - def __schema__(:associations), do: [:post] - def __schema__(:type, :content), do: :string - def __schema__(:type, :date), do: :date - - def __schema__(:association, :post), - do: %{related: Post, owner: __MODULE__, owner_key: :post_id} -end - -defmodule ElixirSense.Plugins.Ecto.FakeSchemas.Post do - @moduledoc """ - Fake Post schema. - """ - - alias ElixirSense.Plugins.Ecto.FakeSchemas.User - alias ElixirSense.Plugins.Ecto.FakeSchemas.Comment - - def __schema__(:fields), do: [:id, :title, :text, :date, :user_id] - def __schema__(:associations), do: [:user, :comments, :tags] - def __schema__(:type, :id), do: :id - def __schema__(:type, :user_id), do: :id - def __schema__(:type, :title), do: :string - def __schema__(:type, :text), do: :string - def __schema__(:type, :date), do: :date - - def __schema__(:association, :user), - do: %{related: User, related_key: :id, owner: __MODULE__, owner_key: :user_id} - - def __schema__(:association, :comments), - do: %{related: Comment, related_key: :post_id, owner: __MODULE__, owner_key: :id} - - def __schema__(:association, :tags), - do: %{related: Tag, owner: __MODULE__, owner_key: :id} -end - -defmodule ElixirSense.Plugins.Ecto.FakeSchemas.Tag do - @moduledoc """ - Fake Tag schema. - """ - - alias ElixirSense.Plugins.Ecto.FakeSchemas.Post - - def __schema__(:fields), do: [:id, :name] - def __schema__(:associations), do: [:posts] - def __schema__(:type, :id), do: :id - def __schema__(:type, :name), do: :string - - def __schema__(:association, :posts), - do: %{related: Post, owner: __MODULE__, owner_key: :id} -end diff --git a/test/support/plugins/ecto/migration.ex b/test/support/plugins/ecto/migration.ex deleted file mode 100644 index c733a454..00000000 --- a/test/support/plugins/ecto/migration.ex +++ /dev/null @@ -1,12 +0,0 @@ -defmodule Ecto.Migration do - @moduledoc ~S""" - Fake Migration module. - """ - - @doc """ - Defines a field on the schema with given name and type. - """ - def add(column, type, opts \\ []) when is_atom(column) and is_list(opts) do - {column, type, opts} - end -end diff --git a/test/support/plugins/ecto/query.ex b/test/support/plugins/ecto/query.ex deleted file mode 100644 index c9696ac0..00000000 --- a/test/support/plugins/ecto/query.ex +++ /dev/null @@ -1,117 +0,0 @@ -defmodule Ecto.Query do - @moduledoc ~S""" - Fake Query module. - """ - - @doc """ - Creates a query. - """ - defmacro from(expr, kw \\ []) do - {expr, kw} - end - - @doc """ - A select query expression. - - Selects which fields will be selected from the schema and any transformations - that should be performed on the fields. Any expression that is accepted in a - query can be a select field. - - ## Keywords examples - - from(c in City, select: c) # returns the schema as a struct - from(c in City, select: {c.name, c.population}) - from(c in City, select: [c.name, c.county]) - - It is also possible to select a struct and limit the returned - fields at the same time: - - from(City, select: [:name]) - - The syntax above is equivalent to: - - from(city in City, select: struct(city, [:name])) - - ## Expressions examples - - City |> select([c], c) - City |> select([c], {c.name, c.country}) - City |> select([c], %{"name" => c.name}) - - """ - defmacro select(query, binding \\ [], expr) do - {query, binding, expr} - end - - @doc """ - Mergeable select query expression. - - This macro is similar to `select/3` except it may be specified - multiple times as long as every entry is a map. This is useful - for merging and composing selects. For example: - - query = from p in Post, select: %{} - - query = - if include_title? do - from p in query, select_merge: %{title: p.title} - else - query - end - - query = - if include_visits? do - from p in query, select_merge: %{visits: p.visits} - else - query - end - - In the example above, the query is built little by little by merging - into a final map. If both conditions above are true, the final query - would be equivalent to: - - from p in Post, select: %{title: p.title, visits: p.visits} - - If `:select_merge` is called and there is no value selected previously, - it will default to the source, `p` in the example above. - """ - defmacro select_merge(query, binding \\ [], expr) do - {query, binding, expr} - end - - @doc """ - A distinct query expression. - - When true, only keeps distinct values from the resulting - select expression. - - ## Keywords examples - - # Returns the list of different categories in the Post schema - from(p in Post, distinct: true, select: p.category) - - # If your database supports DISTINCT ON(), - # you can pass expressions to distinct too - from(p in Post, - distinct: p.category, - order_by: [p.date]) - - # The DISTINCT ON() also supports ordering similar to ORDER BY. - from(p in Post, - distinct: [desc: p.category], - order_by: [p.date]) - - # Using atoms - from(p in Post, distinct: :category, order_by: :date) - - ## Expressions example - - Post - |> distinct(true) - |> order_by([p], [p.category, p.author]) - - """ - defmacro distinct(query, binding \\ [], expr) do - {query, binding, expr} - end -end diff --git a/test/support/plugins/ecto/schema.ex b/test/support/plugins/ecto/schema.ex deleted file mode 100644 index ecf5a38b..00000000 --- a/test/support/plugins/ecto/schema.ex +++ /dev/null @@ -1,32 +0,0 @@ -defmodule Ecto.Schema do - @moduledoc ~S""" - Fake Schema module. - """ - - @doc """ - Defines a field on the schema with given name and type. - """ - defmacro field(name, type \\ :string, opts \\ []) do - {name, type, opts} - end - - defmacro schema(source, do: block) do - {source, block} - end - - defmacro has_many(name, queryable, opts \\ []) do - {name, queryable, opts} - end - - defmacro has_one(name, queryable, opts \\ []) do - {name, queryable, opts} - end - - defmacro belongs_to(name, queryable, opts \\ []) do - {name, queryable, opts} - end - - defmacro many_to_many(name, queryable, opts \\ []) do - {name, queryable, opts} - end -end diff --git a/test/support/plugins/ecto/uuid.ex b/test/support/plugins/ecto/uuid.ex deleted file mode 100644 index 050cc02d..00000000 --- a/test/support/plugins/ecto/uuid.ex +++ /dev/null @@ -1,19 +0,0 @@ -defmodule Ecto.Type do - @moduledoc """ - Fake Ecto.Type - """ - - @callback fake :: true -end - -defmodule Ecto.UUID do - @moduledoc """ - Fake Ecto.UUID - """ - - @behaviour Ecto.Type - - def fake() do - true - end -end diff --git a/test/support/plugins/phoenix/page_controller.ex b/test/support/plugins/phoenix/page_controller.ex deleted file mode 100644 index 3c70e506..00000000 --- a/test/support/plugins/phoenix/page_controller.ex +++ /dev/null @@ -1,10 +0,0 @@ -defmodule ExampleWeb.PageController do - def call(_conn, _params) do - end - - def action(_conn, _params) do - end - - def home(_conn, _params) do - end -end diff --git a/test/support/plugins/phoenix/router.ex b/test/support/plugins/phoenix/router.ex deleted file mode 100644 index fa0d3064..00000000 --- a/test/support/plugins/phoenix/router.ex +++ /dev/null @@ -1,4 +0,0 @@ -defmodule Phoenix.Router do - def get(_route, _plug, _plut_opts, _opts \\ []) do - end -end