diff --git a/lib/elixir_sense/core/introspection.ex b/lib/elixir_sense/core/introspection.ex index 153bb6d4..c7f1722f 100644 --- a/lib/elixir_sense/core/introspection.ex +++ b/lib/elixir_sense/core/introspection.ex @@ -234,9 +234,7 @@ defmodule ElixirSense.Core.Introspection do case get_callbacks_and_docs(mod) do {callbacks, []} -> Enum.map(callbacks, fn {{name, arity}, [spec | _]} -> - spec_ast = - Typespec.spec_to_quoted(name, spec) - |> Macro.prewalk(&drop_macro_env/1) + spec_ast = Typespec.spec_to_quoted(name, spec) signature = get_typespec_signature(spec_ast, arity) definition = format_spec_ast(spec_ast) @@ -315,7 +313,6 @@ defmodule ElixirSense.Core.Introspection do def format_spec_ast(spec_ast) do parts = spec_ast - |> Macro.prewalk(&drop_macro_env/1) |> extract_spec_ast_parts name_str = Macro.to_string(parts.name) @@ -359,9 +356,7 @@ defmodule ElixirSense.Core.Introspection do case found do {{name, _}, [spec | _]} -> - ast = - Typespec.spec_to_quoted(name, spec) - |> Macro.prewalk(&drop_macro_env/1) + ast = Typespec.spec_to_quoted(name, spec) get_returns_from_spec_ast(ast) @@ -444,9 +439,7 @@ defmodule ElixirSense.Core.Introspection do } {_, [spec | _]} -> - spec_ast = - Typespec.spec_to_quoted(spec_name, spec) - |> Macro.prewalk(&drop_macro_env/1) + spec_ast = Typespec.spec_to_quoted(spec_name, spec) spec_ast = if kind == :macrocallback do @@ -477,11 +470,6 @@ defmodule ElixirSense.Core.Introspection do {callbacks, docs || []} end - defp drop_macro_env({name, meta, [{:"::", _, [_, {{:., _, [Macro.Env, :t]}, _, _}]} | args]}), - do: {name, meta, args} - - defp drop_macro_env(other), do: other - defp get_typespec_signature({:when, _, [{:"::", _, [{name, meta, args}, _]}, _]}, arity) do to_string_with_parens({name, meta, strip_types(args, arity)}) end @@ -787,9 +775,7 @@ defmodule ElixirSense.Core.Introspection do specs |> Enum.map_join("\n", fn spec -> - quoted = - Typespec.spec_to_quoted(name, spec) - |> Macro.prewalk(&drop_macro_env/1) + quoted = Typespec.spec_to_quoted(name, spec) quoted = if is_macro do diff --git a/lib/elixir_sense/core/options.ex b/lib/elixir_sense/core/options.ex new file mode 100644 index 00000000..fd83cd8a --- /dev/null +++ b/lib/elixir_sense/core/options.ex @@ -0,0 +1,459 @@ +defmodule ElixirSense.Core.Options do + alias ElixirSense.Core.Normalized.Typespec, as: NormalizedTypespec + alias ElixirSense.Core.Introspection + alias ElixirSense.Core.State.ModFunInfo + + defp get_spec_ast_from_info(spec_info) do + for spec <- spec_info.specs do + case Code.string_to_quoted(spec) do + {:ok, {:@, _, [{_kind, _, [ast]}]}} -> ast + _ -> nil + end + end + |> Enum.reject(&is_nil/1) + end + + defp maybe_unpack_caller(type, :function), do: type + + defp maybe_unpack_caller( + {:type, line1, :fun, [{:type, line2, :product, [_ | args]} | tail]}, + :macro + ) do + {:type, line1, :fun, [{:type, line2, :product, args} | tail]} + end + + defp maybe_unpack_caller({:type, line, :bounded_fun, [head | tail]}, :macro) do + {:type, line, :bounded_fun, [maybe_unpack_caller(head, :macro) | tail]} + end + + defp get_spec(module, function, arity, kind, behaviours, metadata) do + spec_info = Map.get(metadata.specs, {module, function, arity}) + + if spec_info do + get_spec_ast_from_info(spec_info) + else + Enum.find_value(behaviours, fn behaviour -> + if Map.has_key?(metadata.mods_funs_to_positions, {behaviour, nil, nil}) do + spec_info = Map.get(metadata.specs, {behaviour, function, arity}) + + if spec_info do + get_spec_ast_from_info(spec_info) + end + else + if Code.ensure_loaded?(behaviour) do + behaviour_specs = ElixirSense.Core.TypeInfo.get_module_callbacks(behaviour) + + {modified_function, modified_arity} = + case kind do + :function -> + {function, arity} + + :macro -> + # modify name and arity for callback retrieval + # caller arg is dropped in maybe_unpack_caller + {:"MACRO-#{function}", arity + 1} + end + + callback_specs = + for {{f, a}, {_, spec_entries}} <- behaviour_specs, + f == modified_function, + a == modified_arity, + spec <- spec_entries do + spec = maybe_unpack_caller(spec, kind) + NormalizedTypespec.spec_to_quoted(function, spec) + end + + callback_specs + end + end + end) + end + end + + def get_param_options(module, function, arity, env, metadata) do + behaviours = env.behaviours + + candidates = + metadata.mods_funs_to_positions + |> Enum.map(fn + {{^module, ^function, ^arity}, info} -> + kind = ModFunInfo.get_category(info) + specs = get_spec(module, function, arity, kind, behaviours, metadata) + + if specs do + {specs, (arity - 1)..(arity - 1)} + end + + {{^module, ^function, a}, info} when a > arity -> + kind = ModFunInfo.get_category(info) + # assume function head is first in code and last in metadata + head_params = Enum.at(info.params, -1) + default_args = Introspection.count_defaults(head_params) + + if a - default_args <= arity do + specs = get_spec(module, function, a, kind, behaviours, metadata) + + if specs do + # we can guess the position of keyword argument in params + {specs, (arity - 1)..min(arity - 1 + default_args, a - 1)} + end + end + + _ -> + nil + end) + |> Enum.reject(&is_nil/1) + + case candidates do + [] -> + if Code.ensure_loaded?(module) do + candidates = + ElixirSense.Core.Normalized.Code.get_docs(module, :docs) + |> Enum.map(fn + {{^function, ^arity}, _, kind, _, _, _meta} -> + {kind, arity, (arity - 1)..(arity - 1)} + + {{^function, a}, _, kind, _, _, %{defaults: default_args}} + when a > arity and a - default_args <= arity -> + # we can guess the position of keyword argument in params + {kind, a, (arity - 1)..min(arity - 1 + default_args, a - 1)} + + _ -> + nil + end) + |> Enum.reject(&is_nil/1) + + for candidate <- candidates do + {kind, modified_function, modified_arity, parameter_position_range} = + case candidate do + {:function, arity, parameter_position_range} -> + {:function, function, arity, parameter_position_range} + + {:macro, arity, parameter_position_range} -> + # we need to add macro argument for typespec retrieval + # position range remains unchanged as we drop it in maybe_unpack_caller + {:macro, :"MACRO-#{function}", arity + 1, parameter_position_range} + end + + {_behaviour, specs} = + ElixirSense.Core.TypeInfo.get_function_specs( + module, + modified_function, + modified_arity + ) + + for {_, spec_entries} <- specs, spec <- spec_entries do + spec = maybe_unpack_caller(spec, kind) + + NormalizedTypespec.spec_to_quoted(function, spec) + |> get_params_and_named_args(parameter_position_range) + end + |> Enum.flat_map(fn + {:ok, params, named_args} -> + extract_from_params(params, named_args, metadata, module) + + _ -> + [] + end) + end + |> List.flatten() + else + [] + end + + list -> + for {specs, parameter_position_range} <- list, + spec <- specs do + get_params_and_named_args(spec, parameter_position_range) + end + |> Enum.flat_map(fn + {:ok, params, named_args} -> + extract_from_params(params, named_args, metadata, module) + + _ -> + [] + end) + end + end + + defp extract_from_params(params, named_args, metadata, module) do + for param <- params do + param + |> expand_type(metadata, module, named_args) + |> extract_options([]) + end + |> List.flatten() + end + + defp get_params_and_named_args({:"::", _, [{_fun, _, params}, _]}, parameter_position_range) + when is_list(params) do + {:ok, Enum.slice(params, parameter_position_range), []} + end + + defp get_params_and_named_args( + {:when, _, [{:"::", _, [{_fun, _, params}, _]}, named_args]}, + parameter_position_range + ) + when is_list(params) and is_list(named_args) do + {:ok, Enum.slice(params, parameter_position_range), named_args} + end + + defp get_params_and_named_args(_, _), do: :error + + defp extract_options({:"::", _meta, [{name, _, context}, type]}, acc) + when is_atom(name) and is_atom(context), + do: extract_options(type, acc) + + defp extract_options({:list, _meta, [arg]}, acc), + do: extract_options(arg, acc) + + defp extract_options({:|, _, [atom1, atom2]}, acc) + when is_atom(atom1) and is_atom(atom2) do + [atom2, atom1 | acc] + end + + defp extract_options({:|, _, [atom1, {atom2, type2}]}, acc) + when is_atom(atom1) and is_atom(atom2) do + [{atom2, type2}, atom1 | acc] + end + + defp extract_options({:|, _, [{atom1, type1}, atom2]}, acc) + when is_atom(atom1) and is_atom(atom2) do + [atom2, {atom1, type1} | acc] + end + + defp extract_options({:|, _, [{atom1, type1}, {atom2, type2}]}, acc) + when is_atom(atom1) and is_atom(atom2) do + [{atom2, type2}, {atom1, type1} | acc] + end + + defp extract_options({:|, _, [atom, rest]}, acc) + when is_atom(atom) do + extract_options(rest, [atom | acc]) + end + + defp extract_options({:|, _, [{atom, type}, rest]}, acc) + when is_atom(atom) do + extract_options(rest, [{atom, type} | acc]) + end + + defp extract_options({:|, _, [atom1, {:{}, _, [atom2, type2]}]}, acc) + when is_atom(atom1) and is_atom(atom2) do + [{atom2, type2}, atom1 | acc] + end + + defp extract_options({:|, _, [{:{}, _, [atom1, type1]}, atom2]}, acc) + when is_atom(atom1) and is_atom(atom2) do + [atom2, {atom1, type1} | acc] + end + + defp extract_options({:|, _, [atom1, atom2]}, acc) + when is_atom(atom1) and is_atom(atom2) do + [atom2, atom1 | acc] + end + + defp extract_options({:|, _, [{:{}, _, [atom1, type1]}, {:{}, _, [atom2, type2]}]}, acc) + when is_atom(atom1) and is_atom(atom2) do + [{atom2, type2}, {atom1, type1} | acc] + end + + defp extract_options({:|, _, [atom, rest]}, acc) + when is_atom(atom) do + extract_options(rest, [atom | acc]) + end + + defp extract_options({:|, _, [{:{}, _, [atom, type]}, rest]}, acc) + when is_atom(atom) do + extract_options(rest, [{atom, type} | acc]) + end + + defp extract_options({:|, _meta, [other, rest]}, acc) do + acc = extract_options(other, acc) + extract_options(rest, acc) + end + + defp extract_options([{:{}, _, [atom, type]}], acc) + when is_atom(atom) do + [{atom, type} | acc] |> Enum.sort() + end + + defp extract_options([atom | rest], acc) + when is_atom(atom) do + extract_options(rest, [atom | acc]) + end + + defp extract_options([{atom, type} | rest], acc) + when is_atom(atom) do + extract_options(rest, [{atom, type} | acc]) + end + + defp extract_options([rest], acc) do + extract_options(rest, acc) + end + + defp extract_options(atom, acc) when is_atom(atom) do + [atom | acc] |> Enum.sort() + end + + defp extract_options({atom, type}, acc) when is_atom(atom) do + [{atom, type} | acc] |> Enum.sort() + end + + defp extract_options(_other, acc), do: Enum.sort(acc) + + def expand_type(type, metadata, module, named_args, stack \\ []) do + if type in stack do + type + else + do_expand_type(type, metadata, module, named_args, [type | stack]) + end + end + + def do_expand_type( + {:"::", meta, [{name, name_meta, context}, right]}, + metadata, + module, + named_args, + stack + ) + when is_atom(name) and is_atom(context) do + {:"::", meta, + [{name, name_meta, context}, expand_type(right, metadata, module, named_args, stack)]} + end + + def do_expand_type( + {{:., dot_meta, [remote, type]}, call_meta, args}, + metadata, + module, + named_args, + stack + ) do + remote = + case remote do + atom when is_atom(atom) -> remote + {:__aliases__, _, list} -> Module.concat(list) + end + + case find_type(remote, type, args, metadata) do + {:ok, type, new_named_args} -> + expand_type(type, metadata, module, new_named_args ++ named_args, stack) + + :error -> + {{:., dot_meta, [remote, type]}, call_meta, args} + end + end + + def do_expand_type({name, meta, args}, metadata, module, named_args, stack) + when is_atom(name) do + args = (args || []) |> Enum.map(&expand_type(&1, metadata, module, named_args, stack)) + named_arg = Keyword.fetch(named_args, name) + + cond do + match?({:ok, _}, named_arg) -> + {:ok, expanded_arg} = named_arg + expand_type(expanded_arg, metadata, module, named_args, stack) + + :erl_internal.is_type(name, length(args)) -> + {name, meta, args} + + name in [ + :{}, + :%{}, + :%, + :optional, + :required, + :__aliases__, + :..., + :->, + :as_boolean, + :|, + :charlist, + :char_list, + :nonempty_charlist, + :struct, + :keyword, + :string, + :nonempty_string, + :__block__, + :+, + :-, + :__MODULE__, + :__STACKTRACE__, + :__CALLER__, + :__ENV__, + :__DIR__, + :.. + ] -> + {name, meta, args} + + true -> + case find_type(module, name, args, metadata) do + {:ok, type, new_named_args} -> + expand_type(type, metadata, module, new_named_args ++ named_args, stack) + + :error -> + {name, meta, args} + end + end + end + + def do_expand_type(list, metadata, module, named_args, stack) when is_list(list) do + list |> Enum.map(&expand_type(&1, metadata, module, named_args, stack)) + end + + def do_expand_type({left, right}, metadata, module, named_args, stack) do + {expand_type(left, metadata, module, named_args, stack), + expand_type(right, metadata, module, named_args, stack)} + end + + def do_expand_type(literal, _metadata, _module, _named_args, _stack), do: literal + + defp find_type(module, name, args, metadata) do + args = args || [] + + case metadata.types[{module, name, length(args)}] do + nil -> + if Code.ensure_loaded?(module) do + case ElixirSense.Core.TypeInfo.get_type_spec(module, name, length(args)) do + {_kind, spec} -> + {:"::", _, + [ + {_name, _, arg_names}, + type + ]} = + spec + |> NormalizedTypespec.type_to_quoted() + + arg_names = + for {arg_name, _, context} when is_atom(context) <- arg_names, do: arg_name + + {:ok, type, Enum.zip(arg_names, args)} + + _ -> + :error + end + else + :error + end + + %ElixirSense.Core.State.TypeInfo{specs: [spec | _]} -> + with {:ok, + {:@, _, + [ + {_kind, _, + [ + {:"::", _, + [ + {_name, _, arg_names}, + type + ]} + ]} + ]}} <- Code.string_to_quoted(spec) do + arg_names = for {arg_name, _, context} when is_atom(context) <- arg_names, do: arg_name + {:ok, type, Enum.zip(arg_names, args)} + else + _ -> :error + end + end + end +end diff --git a/lib/elixir_sense/core/source.ex b/lib/elixir_sense/core/source.ex index 029bc833..ed97b8b7 100644 --- a/lib/elixir_sense/core/source.ex +++ b/lib/elixir_sense/core/source.ex @@ -517,32 +517,28 @@ defmodule ElixirSense.Core.Source do [keyword_list | rest] when is_list(keyword_list) -> case Enum.reverse(keyword_list) do [{:__cursor__, _, []} | kl_rest] -> - if Keyword.keyword?(kl_rest) do - {:ok, - %{ - call: call, - params: Enum.reverse(rest), - npar: length(rest), - meta: meta, - options: Enum.reverse(kl_rest) |> Enum.map(&elem(&1, 0)), - cursor_at_option: true, - option: nil - }} - end + {:ok, + %{ + call: call, + params: Enum.reverse(rest), + npar: length(rest), + meta: meta, + options: take_options(kl_rest), + cursor_at_option: true, + option: nil + }} [{atom, {:__cursor__, _, []}} | kl_rest] when is_atom(atom) -> - if Keyword.keyword?(kl_rest) do - {:ok, - %{ - call: call, - params: Enum.reverse(rest), - npar: length(rest), - meta: meta, - options: Enum.reverse(kl_rest) |> Enum.map(&elem(&1, 0)), - cursor_at_option: false, - option: atom - }} - end + {:ok, + %{ + call: call, + params: Enum.reverse(rest), + npar: length(rest), + meta: meta, + options: take_options(kl_rest), + cursor_at_option: false, + option: atom + }} _ -> nil @@ -553,6 +549,16 @@ defmodule ElixirSense.Core.Source do end end + defp take_options(kl_rest) do + Enum.reduce_while(kl_rest, [], fn + {atom, _value}, acc when is_atom(atom) -> + {:cont, [atom | acc]} + + _, acc -> + {:halt, acc} + end) + end + def get_mod_fun(atom, _binding_env) when is_atom(atom), do: {{nil, false}, atom} def get_mod_fun([{:__aliases__, _, list}, fun], binding_env) do diff --git a/lib/elixir_sense/core/type_ast.ex b/lib/elixir_sense/core/type_ast.ex index 6e6ca214..af2fa513 100644 --- a/lib/elixir_sense/core/type_ast.ex +++ b/lib/elixir_sense/core/type_ast.ex @@ -13,7 +13,6 @@ defmodule ElixirSense.Core.TypeAst do def extract_signature(ast) do ast - |> Macro.prewalk(&drop_macro_env/1) |> extract_spec_ast_parts |> Map.get(:name) |> Macro.to_string() @@ -34,9 +33,4 @@ defmodule ElixirSense.Core.TypeAst do defp extract_return_part(ast, returns) do [ast | returns] end - - defp drop_macro_env({name, meta, [{:"::", _, [{:env, _, _}, _ | _]} | args]}), - do: {name, meta, args} - - defp drop_macro_env(other), do: other end diff --git a/lib/elixir_sense/core/type_info.ex b/lib/elixir_sense/core/type_info.ex index 4af09189..002569d6 100644 --- a/lib/elixir_sense/core/type_info.ex +++ b/lib/elixir_sense/core/type_info.ex @@ -308,37 +308,45 @@ defmodule ElixirSense.Core.TypeInfo do # does not drop MACRO- prefix def get_function_specs(module, function, arity) when is_atom(module) and is_atom(function) do - callback_specs = - module - |> Behaviours.get_module_behaviours() - |> Enum.reduce_while(nil, fn behaviour, acc -> - behaviour_specs = behaviour |> get_module_callbacks() - - callback_specs = - for {{f, a}, spec} <- behaviour_specs, f == function, arity == :any or a == arity do - spec - end + module_specs = module |> get_module_specs() - if callback_specs != [] do - {:halt, {behaviour, callback_specs}} - else - {:cont, acc} - end - end) - - if callback_specs == nil do - module_specs = module |> get_module_specs() - - function_specs = - for {{f, a}, spec} <- module_specs, - f == function, - Introspection.matches_arity?(a, arity) do - spec - end + function_specs = + for {{f, a}, spec} <- module_specs, + f == function, + Introspection.matches_arity?(a, arity) do + spec + end + if function_specs != [] do {nil, function_specs} else - callback_specs + # TODO this will not work correctly for :any arity in case many functions with the same name and different arities + # are implement different behaviours + callback_specs = + module + |> Behaviours.get_module_behaviours() + |> Enum.reduce_while(nil, fn behaviour, acc -> + behaviour_specs = behaviour |> get_module_callbacks() + + callback_specs = + for {{f, a}, spec} <- behaviour_specs, + f == function, + Introspection.matches_arity?(a, arity) do + spec + end + + if callback_specs != [] do + {:halt, {behaviour, callback_specs}} + else + {:cont, acc} + end + end) + + if callback_specs do + callback_specs + else + {nil, []} + end end end @@ -393,308 +401,10 @@ defmodule ElixirSense.Core.TypeInfo do quoted end - def extract_param_options(mod, fun, npar) do - # does not drop MACRO- prefix - {_behaviour, specs} = get_function_specs(mod, fun, :any) - - specs - |> get_param_type_specs(npar) - |> expand_type_specs(mod) - |> Enum.filter(&list_type_spec?/1) - |> extract_list_type_spec_options() - end - - def get_type_info(module, type, original_module) do - module - |> extract_type_def_info(type) - |> build_type_info(type, original_module) - end - - # Built-in types - defp build_type_info({nil, name, n_args}, type, _) do - spec_ast = BuiltinTypes.get_builtin_type_spec(name, n_args) - spec = format_type_spec_ast(spec_ast, :type, line_length: @param_option_spec_line_length) - doc = BuiltinTypes.get_builtin_type_doc(to_string(name), n_args) - - %{ - origin: "", - type_spec: type_str(type), - doc: doc, - expanded_spec: spec - } - end - - # Custom Types - defp build_type_info({module, name, n_args}, type, original_module) do - {mod, expanded_type} = expand_type_spec(type, module) - - type_spec = - if original_module == module || match?({:remote_type, _, _}, type) do - type_str(type) - else - "#{inspect(mod)}.#{type_str(type)}" - end - - {docs, _metadata} = - case NormalizedCode.get_docs(module, :type_docs) do - docs when is_list(docs) -> - get_type_doc_desc(docs, name, n_args) - - _ -> - {"", %{}} - end - - %{ - origin: inspect(module), - type_spec: type_spec, - doc: docs, - expanded_spec: - expanded_type |> format_type_spec(line_length: @param_option_spec_line_length) - } - end - - # Inline, non-existent - defp build_type_info(_, type, _) do - %{ - origin: "", - type_spec: type_str(type), - doc: "", - expanded_spec: "" - } - end - def type_str(type) do typespec_to_quoted(type) |> Introspection.to_string_with_parens() end - defp extract_list_type_spec_options(list_type_specs) do - Enum.flat_map(list_type_specs, fn type_spec -> - type_spec - |> expand_list_type_spec() - |> extract_union_options_name_and_type() - end) - end - - defp get_param_type_specs(func_specs, npar) do - for func_spec <- func_specs, - params_types <- extract_params_types_variants(func_spec), - length(params_types) > npar do - params_types |> Enum.at(npar) - end - end - - defp expand_type_specs(types, module) do - types |> Enum.map(fn type -> expand_type_spec(type, module) end) - end - - defp expand_type_spec({:ann_type, _, [{:var, _, _}, type]}, module) do - expand_type_spec(type, module) - end - - defp expand_type_spec({:user_type, _, type_name, type_args} = type, module) do - case get_type_spec(module, type_name, length(type_args)) do - nil -> - {:not_found, type} - - {:opaque, {name, _type, args}} -> - {module, {:opaque, {name, nil, args}}} - - type_found -> - {module, type_found} - end - end - - defp expand_type_spec({:type, _, :list, [_ | _]} = type, module) do - {module, type} - end - - defp expand_type_spec({:type, _, _, _} = type, module) do - {module, {:not_found, {nil, type, []}}} - end - - defp expand_type_spec( - {:remote_type, _, [{:atom, _, remote_mod}, {:atom, _, type_name}, type_args]} = type, - _module - ) do - case get_type_spec(remote_mod, type_name, length(type_args)) do - nil -> - {:not_found, type} - - {:opaque, {name, _type, args}} -> - {remote_mod, {:opaque, {name, nil, args}}} - - type_found -> - {remote_mod, type_found} - end - end - - defp expand_type_spec(type, module) do - {module, type} - end - - defp expand_list_type_spec({mod, {:type, _, :list, [type]}}) do - expand_type_spec(type, mod) - end - - defp expand_list_type_spec({mod, {_kind, {_name, {:type, _, :list, [type]}, _}}}) do - expand_type_spec(type, mod) - end - - # More than one option (union) - defp extract_union_options_name_and_type( - {mod, {_kind, {_name, {:type, _, :union, options_types}, _}}} - ) do - options_types - |> Enum.map(&extract_tagged_tuple_name_and_type({mod, &1})) - |> List.flatten() - end - - # Only one option (not actually a union) - defp extract_union_options_name_and_type( - {mod, {_kind, {_name, {:type, _, :tuple, _} = type, _}}} - ) do - extract_tagged_tuple_name_and_type({mod, type}) - end - - defp extract_union_options_name_and_type({mod, {_kind, {_, {:atom, _, name}, _}}}) do - [{mod, name}] - end - - defp extract_union_options_name_and_type( - {mod, {_kind, {_name, {:remote_type, _, _} = type, _}}} - ) do - extract_tagged_tuple_name_and_type({mod, type}) - end - - defp extract_union_options_name_and_type( - {mod, {_kind, {_name, {:user_type, _, _, _} = type, _}}} - ) do - extract_tagged_tuple_name_and_type({mod, type}) - end - - defp extract_union_options_name_and_type({mod, {:atom, _, atom}}) when is_atom(atom) do - [{mod, atom}] - end - - # skip unknown type - defp extract_union_options_name_and_type(_) do - [] - end - - defp extract_tagged_tuple_name_and_type({mod, {:type, _, :tuple, [{:atom, _, name}, type]}}) do - [{mod, name, type}] - end - - defp extract_tagged_tuple_name_and_type({mod, type}) do - case expand_type_spec(type, mod) do - {_mod, {_kind, {_name, {:type, _, :union, _}, _}}} = expanded_type -> - extract_union_options_name_and_type(expanded_type) - - {mod, {:atom, _, name}} -> - [{mod, name}] - - _ -> - [] - end - end - - defp extract_type_def_info(_mod, {:type, _, :list, [_ | _]}) do - :inline_list - end - - defp extract_type_def_info(_mod, {:type, _, type_name, args}) when is_list(args) do - {nil, type_name, length(args)} - end - - defp extract_type_def_info(_mod, {:type, _, type_name}) do - {nil, type_name, 0} - end - - defp extract_type_def_info( - _mod, - {:remote_type, _, [{:atom, _, remote_mod}, {:atom, _, name}, args]} - ) - when is_list(args) do - remote_mod = if remote_mod == :elixir, do: nil, else: remote_mod - {remote_mod, name, length(args)} - end - - defp extract_type_def_info(mod, {_, _, type_name, args}) when is_list(args) do - {mod, type_name, length(args)} - end - - defp extract_type_def_info(_, _) do - :not_found - end - - defp list_type_spec?({_mod, {:type, _, :list, [_]}}) do - true - end - - defp list_type_spec?({_mod, {_, {_, {:type, _, :list, [_]}, _}}}) do - true - end - - defp list_type_spec?(_) do - false - end - - defp extract_params_types_variants({_, list}) do - list - |> Enum.map(&extract_params_types/1) - end - - defp extract_params_types({:type, _, :fun, [{:type, _, :product, params_types}, _]}) do - params_types - end - - defp extract_params_types({:type, _, :bounded_fun, [type, constraints]}) do - {:type, _, :fun, [{:type, _, :product, params}, _]} = type - - vars_types = - for {:type, _, :constraint, [{:atom, _, :is_subtype}, [{:var, _, var}, var_type]]} <- - constraints, - into: %{} do - {var, var_type} - end - - params - |> Enum.map(&expand_var_types(&1, vars_types, [])) - # reject failed expansions - |> Enum.reject(&is_nil/1) - end - - defp expand_var_types(var_type, vars_types, expanded_types) do - if var_type in expanded_types do - # break recursive type expansion - nil - else - do_expand_var_types(var_type, vars_types, [var_type | expanded_types]) - end - end - - defp do_expand_var_types({:var, _, name}, vars_types, expanded_types) do - expand_var_types(vars_types[name], vars_types, expanded_types) - end - - defp do_expand_var_types({:type, l, kind, tuple_elements}, vars_types, expanded_types) - when kind in [:list, :tuple, :union] and is_list(tuple_elements) do - expanded = - for(element <- tuple_elements, do: expand_var_types(element, vars_types, expanded_types)) - # reject failed expansions - |> Enum.reject(&is_nil/1) - - {:type, l, kind, expanded} - end - - defp do_expand_var_types({:ann_type, _l, [{:var, _, _}, type]}, vars_types, expanded_types) do - expand_var_types(type, vars_types, expanded_types) - end - - defp do_expand_var_types(type, _vars_types, _expanded_types) do - type - end - def extract_params(type) do quoted = Typespec.spec_to_quoted(:dummy, type) diff --git a/lib/elixir_sense/providers/completion/reducers/params.ex b/lib/elixir_sense/providers/completion/reducers/params.ex index 4e9aeeb2..da407549 100644 --- a/lib/elixir_sense/providers/completion/reducers/params.ex +++ b/lib/elixir_sense/providers/completion/reducers/params.ex @@ -5,11 +5,11 @@ defmodule ElixirSense.Providers.Completion.Reducers.Params do alias ElixirSense.Core.Introspection alias ElixirSense.Core.Metadata alias ElixirSense.Core.Source - alias ElixirSense.Core.TypeInfo alias ElixirSense.Providers.Utils.Matcher @type param_option :: %{ type: :param_option, + subtype: :keyword | :atom, name: String.t(), origin: String.t(), type_spec: String.t(), @@ -30,6 +30,8 @@ defmodule ElixirSense.Providers.Completion.Reducers.Params do with %{ candidate: {mod, fun}, elixir_prefix: elixir_prefix, + options_so_far: options_so_far, + cursor_at_option: cursor_at_option, npar: npar } <- Source.which_func(prefix, binding_env), @@ -39,19 +41,46 @@ defmodule ElixirSense.Providers.Completion.Reducers.Params do env, mods_funs, metadata_types, - {1, 1}, + cursor_context.cursor_position, not elixir_prefix ) do list = - if Code.ensure_loaded?(mod) do - TypeInfo.extract_param_options(mod, fun, npar) - |> Kernel.++(TypeInfo.extract_param_options(mod, :"MACRO-#{fun}", npar + 1)) - |> options_to_suggestions(mod) - |> Enum.filter(&Matcher.match?(&1.name, hint)) - else - # TODO metadata? - [] + for opt <- + ElixirSense.Core.Options.get_param_options(mod, fun, npar + 1, env, buffer_metadata) do + case opt do + {name, type} -> + # match on atom: + if Matcher.match?(to_string(name) <> ":", hint) do + expanded_spec = Introspection.to_string_with_parens(type) + + %{ + doc: "", + expanded_spec: expanded_spec, + name: name |> Atom.to_string(), + origin: inspect(mod), + type: :param_option, + subtype: :keyword, + type_spec: expanded_spec + } + end + + name -> + # match on :atom + if options_so_far == [] and cursor_at_option == true and + Matcher.match?(inspect(name), hint) do + %{ + doc: "", + expanded_spec: "", + name: name |> Atom.to_string(), + origin: inspect(mod), + type: :param_option, + subtype: :atom, + type_spec: "" + } + end + end end + |> Enum.reject(&is_nil/1) {:cont, %{acc | result: acc.result ++ list}} else @@ -59,22 +88,4 @@ defmodule ElixirSense.Providers.Completion.Reducers.Params do {:cont, acc} end end - - defp options_to_suggestions(options, original_module) do - Enum.map(options, fn - {mod, name, type} -> - TypeInfo.get_type_info(mod, type, original_module) - |> Map.merge(%{type: :param_option, name: name |> Atom.to_string()}) - - {mod, name} -> - %{ - doc: "", - expanded_spec: "", - name: name |> Atom.to_string(), - origin: inspect(mod), - type: :param_option, - type_spec: "" - } - end) - end end diff --git a/test/elixir_sense/core/introspection_test.exs b/test/elixir_sense/core/introspection_test.exs index 3fd890ad..1ee1d4b5 100644 --- a/test/elixir_sense/core/introspection_test.exs +++ b/test/elixir_sense/core/introspection_test.exs @@ -40,6 +40,31 @@ defmodule ElixirSense.Core.IntrospectionTest do ] end + test "get_callbacks_with_docs for macrocallbacks" do + assert get_callbacks_with_docs(ElixirSenseExample.BehaviourWithMacrocallback) == [ + %{ + name: :optional, + arity: 1, + callback: """ + @macrocallback optional(a) :: Macro.t() when a: atom()\ + """, + signature: "optional(a)", + doc: "An optional macrocallback\n", + metadata: %{optional: true, app: :elixir_sense}, + kind: :macrocallback + }, + %{ + arity: 1, + name: :required, + signature: "required(atom)", + callback: "@macrocallback required(atom()) :: Macro.t()", + metadata: %{app: :elixir_sense, optional: false}, + doc: "A required macrocallback\n", + kind: :macrocallback + } + ] + end + test "get_callbacks_with_docs for erlang behaviours" do assert [ %{ diff --git a/test/elixir_sense/core/options_test.exs b/test/elixir_sense/core/options_test.exs new file mode 100644 index 00000000..a9285701 --- /dev/null +++ b/test/elixir_sense/core/options_test.exs @@ -0,0 +1,347 @@ +defmodule ElixirSense.Core.OptionsTest do + use ExUnit.Case, async: true + alias ElixirSense.Core.Options + alias ElixirSense.Core.Parser + + defp get_options(code, module, function, arity) do + metadata = Parser.parse_string(code, true, true, {1, 1}) + Options.get_param_options(module, function, arity, %ElixirSense.Core.State.Env{}, metadata) + end + + describe "metadata" do + test "gets options only one option" do + code = """ + defmodule Foo do + @spec bar([{:option1, integer()}]) :: :ok + def bar(options), do: :ok + end + """ + + assert [ + option1: {:integer, _, []} + ] = get_options(code, Foo, :bar, 1) + end + + test "gets options union" do + code = """ + defmodule Foo do + @spec bar([{:option1, integer()} | {:option2, String.t()}]) :: :ok + def bar(options), do: :ok + end + """ + + assert [ + option2: {:binary, _, []}, + option1: {:integer, _, []} + ] = get_options(code, Foo, :bar, 1) + end + + test "gets options list" do + code = """ + defmodule Foo do + @spec bar([{:option1, integer()}, {:option2, String.t()}]) :: :ok + def bar(options), do: :ok + end + """ + + assert [ + option2: {:binary, _, []}, + option1: {:integer, _, []} + ] = get_options(code, Foo, :bar, 1) + end + + test "gets options keyword" do + code = """ + defmodule Foo do + @spec bar([option1: integer(), option2: String.t()]) :: :ok + def bar(options), do: :ok + end + """ + + assert [ + option2: {:binary, _, []}, + option1: {:integer, _, []} + ] = get_options(code, Foo, :bar, 1) + end + + test "non option types in list" do + code = """ + defmodule Foo do + @spec bar([:foo | {:option1, integer()} | 1]) :: :ok + def bar(options), do: :ok + end + """ + + assert [:foo, option1: {:integer, _, []}] = get_options(code, Foo, :bar, 1) + end + + test "skips non option types" do + code = """ + defmodule Foo do + @spec bar(keyword()) :: :ok + def bar(options), do: :ok + end + """ + + assert [] == get_options(code, Foo, :bar, 1) + end + + test "skips functions without spec" do + code = """ + defmodule Foo do + def bar(options), do: :ok + end + """ + + assert [] == get_options(code, Foo, :bar, 1) + end + + test "gets options by expanding list type" do + code = """ + defmodule Foo do + @type options_t() :: [{:option1, integer()} | {:option2, String.t()}] + @spec bar(options_t()) :: :ok + def bar(options), do: :ok + end + """ + + assert [ + option2: {:binary, _, []}, + option1: {:integer, _, []} + ] = get_options(code, Foo, :bar, 1) + end + + test "gets options by expanding with" do + code = """ + defmodule Foo do + @spec bar(x) :: :ok when x: [{:option1, integer()} | {:option2, String.t()}] + def bar(options), do: :ok + end + """ + + assert [ + option2: {:binary, _, []}, + option1: {:integer, _, []} + ] = get_options(code, Foo, :bar, 1) + end + + test "gets options by expanding option type" do + code = """ + defmodule Foo do + @type option1_t() :: {:option1, integer()} + @type option2_t() :: {:option2, String.t()} + @spec bar([option1_t() | option2_t()]) :: :ok + def bar(options), do: :ok + end + """ + + assert [ + option2: {:binary, _, []}, + option1: {:integer, _, []} + ] = get_options(code, Foo, :bar, 1) + end + end + + describe "typescpec chunk" do + test "gets options only one option" do + assert [ + option1: {:integer, _, []} + ] = get_options("", ElixirSenseExample.Options.Foo1, :bar, 1) + end + + test "gets options" do + assert [ + option1: {:integer, _, []}, + option2: {:binary, _, []} + ] = get_options("", ElixirSenseExample.Options.Foo, :bar, 1) + end + + test "gets options expands with" do + assert [ + option1: {:integer, _, []}, + option2: {:binary, _, []} + ] = get_options("", ElixirSenseExample.Options.With, :bar, 1) + end + end + + describe "expand type" do + defp expand(code, type, module) do + metadata = Parser.parse_string(code, true, true, {1, 1}) + Options.expand_type(Code.string_to_quoted!(type), metadata, module, []) + end + + test "builtin metadata type" do + code = """ + defmodule Foo do + end + """ + + assert {:integer, _, []} = expand(code, "integer()", Foo) + end + + test "local metadata type" do + code = """ + defmodule Foo do + @type foo() :: :bar + end + """ + + assert expand(code, "foo()", Foo) == :bar + end + + test "local metadata type with arg" do + code = """ + defmodule Foo do + @type foo(t) :: t + end + """ + + assert {:integer, _, []} = expand(code, "foo(integer())", Foo) + end + + test "undefined local metadata type" do + code = """ + defmodule Foo do + end + """ + + assert {:foo, _, []} = expand(code, "foo()", Foo) + end + + test "remote metadata type" do + code = """ + defmodule Foo do + @type foo(t) :: t + end + """ + + assert expand(code, "Foo.foo(:bar)", Bar) == :bar + end + + test "undefined remote metadata type" do + code = """ + defmodule Foo do + end + """ + + assert {{:., _, [Foo, :foo]}, _, []} = expand(code, "Foo.foo()", Bar) + end + + test "nested metadata type" do + code = """ + defmodule Foo do + @type bar() :: :baz + @type foo() :: bar() + end + """ + + assert expand(code, "foo()", Foo) == :baz + end + + test "list" do + code = """ + defmodule Foo do + @type bar() :: :baz + @type foo() :: [bar()] + end + """ + + assert {:list, _, [:baz]} = expand(code, "foo()", Foo) + end + + test "keyword" do + code = """ + defmodule Foo do + @type bar() :: :baz + @type foo() :: [a: bar(), b: :ok] + end + """ + + assert {:list, _, [{:|, _, [a: :baz, b: :ok]}]} = expand(code, "foo()", Foo) + end + + test "tuple" do + code = """ + defmodule Foo do + @type bar() :: :baz + @type foo() :: {bar(), :a, :b} + end + """ + + assert {:{}, [line: 1], [:baz, :a, :b]} = expand(code, "foo()", Foo) + end + + test "tuple 2 elements" do + code = """ + defmodule Foo do + @type bar() :: :baz + @type foo() :: {bar(), :a} + end + """ + + assert {:baz, :a} == expand(code, "foo()", Foo) + end + + test ":: operator" do + code = """ + defmodule Foo do + @type bar() :: :baz + @type foo() :: {some :: bar(), other :: :a} + end + """ + + assert { + {:"::", [line: 1], [{:some, [line: 1], nil}, :baz]}, + {:"::", [line: 1], [{:other, [line: 1], nil}, :a]} + } = expand(code, "foo()", Foo) + end + + test "union" do + code = """ + defmodule Foo do + @type bar() :: :baz + @type foo() :: bar() | atom + end + """ + + assert {:|, [line: 1], [:baz, {:atom, [line: 1], []}]} = expand(code, "foo()", Foo) + end + + test "map" do + code = """ + defmodule Foo do + @type bar() :: :baz + @type foo() :: %{optional(atom) => bar()} + end + """ + + assert { + :%{}, + [line: 1], + [{{:optional, [line: 1], [{:atom, [line: 1], []}]}, :baz}] + } = expand(code, "foo()", Foo) + end + + test "struct" do + code = """ + defmodule My do + defstruct [:x] + end + + defmodule Foo do + @type bar() :: :baz + @type foo() :: %My{x: bar()} + end + """ + + assert { + :%, + [line: 1], + [ + {:__aliases__, [line: 1], [:My]}, + {:%{}, [line: 1], [x: :baz]} + ] + } = expand(code, "foo()", Foo) + end + end +end diff --git a/test/elixir_sense/core/source_test.exs b/test/elixir_sense/core/source_test.exs index cb660525..88256037 100644 --- a/test/elixir_sense/core/source_test.exs +++ b/test/elixir_sense/core/source_test.exs @@ -264,10 +264,37 @@ defmodule ElixirSense.Core.SourceTest do options_so_far: [] } = which_func("var = Enum.map([1") - assert nil == which_func("var = Enum.map([1,") - assert nil == which_func("var = Enum.map([1, ") - assert nil == which_func("var = Enum.map([1, 2") - assert nil == which_func("var = Enum.map([1,2,3") + assert %{ + candidate: {Enum, :map}, + cursor_at_option: true, + npar: 0, + option: nil, + options_so_far: [] + } = which_func("var = Enum.map([1,") + + assert %{ + candidate: {Enum, :map}, + cursor_at_option: true, + npar: 0, + option: nil, + options_so_far: [] + } = which_func("var = Enum.map([1, ") + + assert %{ + candidate: {Enum, :map}, + cursor_at_option: true, + npar: 0, + option: nil, + options_so_far: [] + } = which_func("var = Enum.map([1, 2") + + assert %{ + candidate: {Enum, :map}, + cursor_at_option: true, + npar: 0, + option: nil, + options_so_far: [] + } = which_func("var = Enum.map([1,2,3") end test "inside a keyword list" do @@ -350,10 +377,49 @@ defmodule ElixirSense.Core.SourceTest do option: :b, options_so_far: [:a] } = which_func("var = Enum.map([a: 1, b: ") + + assert %{ + candidate: {Enum, :map}, + cursor_at_option: true, + npar: 0, + option: nil, + options_so_far: [] + } = which_func("var = Enum.map([:a") + + assert %{ + candidate: {Enum, :map}, + cursor_at_option: true, + npar: 0, + option: nil, + options_so_far: [] + } = which_func("var = Enum.map([:a,") + + assert %{ + candidate: {Enum, :map}, + cursor_at_option: true, + npar: 0, + option: nil, + options_so_far: [] + } = which_func("var = Enum.map([:a, ") + + assert %{ + candidate: {Enum, :map}, + cursor_at_option: false, + npar: 0, + option: :b, + options_so_far: [] + } = which_func("var = Enum.map([:a, b: ") end test "inside a list with a list before" do - assert nil == which_func("var = Enum.map([1,2], [1, ") + assert %{ + params: [[1, 2]], + candidate: {Enum, :map}, + npar: 1, + cursor_at_option: true, + option: nil, + options_so_far: [] + } = which_func("var = Enum.map([1,2], [1, ") end test "inside a keyword list as last arg" do diff --git a/test/elixir_sense/core/type_info_test.exs b/test/elixir_sense/core/type_info_test.exs index 4cac6e8a..343be50b 100644 --- a/test/elixir_sense/core/type_info_test.exs +++ b/test/elixir_sense/core/type_info_test.exs @@ -3,372 +3,6 @@ defmodule ElixirSense.Core.TypeInfoTest do alias ElixirSense.Core.TypeInfo alias ElixirSenseExample.ModuleWithTypespecs.{Local, Remote} - @tag timeout: :infinity - @tag requires_source: true - test "does not crash on standard library" do - for {application, _, _} <- Application.started_applications() do - {:ok, modules} = :application.get_key(application, :modules) - - for mod <- modules, {fun, ar} <- mod.module_info(:exports), i <- 0..ar do - TypeInfo.extract_param_options(mod, fun, i) - end - end - end - - test "func_with_options" do - assert [ - {Local, :local_o, {:user_type, _, :local_t, []}}, - {Local, :local_with_params_o, - {:user_type, _, :local_t, [{:type, _, :atom, []}, {:type, _, :integer, []}]}}, - {Local, :union_o, {:user_type, _, :union_t, []}}, - {Local, :inline_union_o, {:type, _, :union, [{:atom, _, :a}, {:atom, _, :b}]}}, - {Local, :list_o, {:user_type, _, :list_t, []}}, - {Local, :inline_list_o, - {:type, _, :list, [{:type, _, :union, [{:atom, _, :trace}, {:atom, _, :log}]}]}}, - {Local, :basic_o, {:type, _, :pid, []}}, - {Local, :basic_with_params_o, {:type, _, :nonempty_list, [{:type, _, :atom, []}]}}, - {Local, :builtin_o, - {:remote_type, _, [{:atom, _, :elixir}, {:atom, _, :keyword}, []]}}, - {Local, :builtin_with_params_o, - {:remote_type, _, - [{:atom, _, :elixir}, {:atom, _, :keyword}, [{:type, _, :term, []}]]}}, - {Local, :remote_o, - {:remote_type, _, [{:atom, _, Remote}, {:atom, _, :remote_t}, []]}}, - {Local, :remote_with_params_o, - {:remote_type, _, - [ - {:atom, _, Remote}, - {:atom, _, :remote_t}, - [{:type, _, :atom, []}, {:type, _, :integer, []}] - ]}}, - {Local, :remote_aliased_o, {:user_type, _, :remote_aliased_t, []}}, - {Local, :remote_aliased_inline_o, - {:remote_type, _, [{:atom, _, Remote}, {:atom, _, :remote_t}, []]}}, - {Local, :private_o, {:user_type, _, :private_t, []}}, - {Local, :opaque_o, {:user_type, _, :opaque_t, []}}, - {Local, :non_existent_o, - {:remote_type, _, [{:atom, _, Remote}, {:atom, _, :non_existent}, []]}}, - {Local, :large_o, {:user_type, _, :large_t, []}} - ] = TypeInfo.extract_param_options(Local, :func_with_options, 0) - end - - test "func_with_union_of_options" do - assert [ - {Local, :local_o, {:user_type, _, :local_t, []}}, - {Local, :local_with_params_o, - {:user_type, _, :local_t, [{:type, _, :atom, []}, {:type, _, :integer, []}]}}, - {Local, :union_o, {:user_type, _, :union_t, []}}, - {Local, :inline_union_o, {:type, _, :union, [{:atom, _, :a}, {:atom, _, :b}]}}, - {Local, :list_o, {:user_type, _, :list_t, []}}, - {Local, :inline_list_o, - {:type, _, :list, [{:type, _, :union, [{:atom, _, :trace}, {:atom, _, :log}]}]}}, - {Local, :basic_o, {:type, _, :pid, []}}, - {Local, :basic_with_params_o, {:type, _, :nonempty_list, [{:type, _, :atom, []}]}}, - {Local, :builtin_o, - {:remote_type, _, [{:atom, _, :elixir}, {:atom, _, :keyword}, []]}}, - {Local, :builtin_with_params_o, - {:remote_type, _, - [{:atom, _, :elixir}, {:atom, _, :keyword}, [{:type, _, :term, []}]]}}, - {Local, :remote_o, - {:remote_type, _, [{:atom, _, Remote}, {:atom, _, :remote_t}, []]}}, - {Local, :remote_with_params_o, - {:remote_type, _, - [ - {:atom, _, Remote}, - {:atom, _, :remote_t}, - [{:type, _, :atom, []}, {:type, _, :integer, []}] - ]}}, - {Local, :remote_aliased_o, {:user_type, _, :remote_aliased_t, []}}, - {Local, :remote_aliased_inline_o, - {:remote_type, _, [{:atom, _, Remote}, {:atom, _, :remote_t}, []]}}, - {Local, :private_o, {:user_type, _, :private_t, []}}, - {Local, :opaque_o, {:user_type, _, :opaque_t, []}}, - {Local, :non_existent_o, - {:remote_type, _, [{:atom, _, Remote}, {:atom, _, :non_existent}, []]}}, - {Local, :large_o, {:user_type, _, :large_t, []}}, - {Local, :option_1, {:type, _, :atom, []}}, - {Local, :option_2, {:type, _, :integer, []}} - ] = TypeInfo.extract_param_options(Local, :func_with_union_of_options, 0) - end - - test "func_with_union_of_options_as_type" do - assert [ - {Local, :option_1, {:type, _, :boolean, []}}, - {Local, :option_2, {:type, _, :timeout, []}}, - {Remote, :remote_option_1, {:user_type, _, :remote_t, []}}, - {Remote, :remote_option_2, {:user_type, _, :remote_list_t, []}} - ] = TypeInfo.extract_param_options(Local, :func_with_union_of_options_as_type, 0) - end - - test "func_with_union_of_options_inline" do - assert [ - {Local, :option_1, {:type, _, :atom, []}}, - {Local, :option_2, {:type, _, :integer, []}}, - {Local, :local_o, {:user_type, _, :local_t, []}}, - {Local, :local_with_params_o, - {:user_type, _, :local_t, [{:type, _, :atom, []}, {:type, _, :integer, []}]}}, - {Local, :union_o, {:user_type, _, :union_t, []}}, - {Local, :inline_union_o, {:type, _, :union, [{:atom, _, :a}, {:atom, _, :b}]}}, - {Local, :list_o, {:user_type, _, :list_t, []}}, - {Local, :inline_list_o, - {:type, _, :list, [{:type, _, :union, [{:atom, _, :trace}, {:atom, _, :log}]}]}}, - {Local, :basic_o, {:type, _, :pid, []}}, - {Local, :basic_with_params_o, {:type, _, :nonempty_list, [{:type, _, :atom, []}]}}, - {Local, :builtin_o, - {:remote_type, _, [{:atom, _, :elixir}, {:atom, _, :keyword}, []]}}, - {Local, :builtin_with_params_o, - {:remote_type, _, - [{:atom, _, :elixir}, {:atom, _, :keyword}, [{:type, _, :term, []}]]}}, - {Local, :remote_o, - {:remote_type, _, [{:atom, _, Remote}, {:atom, _, :remote_t}, []]}}, - {Local, :remote_with_params_o, - {:remote_type, _, - [ - {:atom, _, Remote}, - {:atom, _, :remote_t}, - [{:type, _, :atom, []}, {:type, _, :integer, []}] - ]}}, - {Local, :remote_aliased_o, {:user_type, _, :remote_aliased_t, []}}, - {Local, :remote_aliased_inline_o, - {:remote_type, _, [{:atom, _, Remote}, {:atom, _, :remote_t}, []]}}, - {Local, :private_o, {:user_type, _, :private_t, []}}, - {Local, :opaque_o, {:user_type, _, :opaque_t, []}}, - {Local, :non_existent_o, - {:remote_type, _, [{:atom, _, Remote}, {:atom, _, :non_existent}, []]}}, - {Local, :large_o, {:user_type, _, :large_t, []}} - ] = TypeInfo.extract_param_options(Local, :func_with_union_of_options_inline, 0) - end - - test "func_with_named_options" do - assert [ - {Local, :local_o, {:user_type, _, :local_t, []}}, - {Local, :local_with_params_o, - {:user_type, _, :local_t, [{:type, _, :atom, []}, {:type, _, :integer, []}]}}, - {Local, :union_o, {:user_type, _, :union_t, []}}, - {Local, :inline_union_o, {:type, _, :union, [{:atom, _, :a}, {:atom, _, :b}]}}, - {Local, :list_o, {:user_type, _, :list_t, []}}, - {Local, :inline_list_o, - {:type, _, :list, [{:type, _, :union, [{:atom, _, :trace}, {:atom, _, :log}]}]}}, - {Local, :basic_o, {:type, _, :pid, []}}, - {Local, :basic_with_params_o, {:type, _, :nonempty_list, [{:type, _, :atom, []}]}}, - {Local, :builtin_o, - {:remote_type, _, [{:atom, _, :elixir}, {:atom, _, :keyword}, []]}}, - {Local, :builtin_with_params_o, - {:remote_type, _, - [{:atom, _, :elixir}, {:atom, _, :keyword}, [{:type, _, :term, []}]]}}, - {Local, :remote_o, - {:remote_type, _, [{:atom, _, Remote}, {:atom, _, :remote_t}, []]}}, - {Local, :remote_with_params_o, - {:remote_type, _, - [ - {:atom, _, Remote}, - {:atom, _, :remote_t}, - [{:type, _, :atom, []}, {:type, _, :integer, []}] - ]}}, - {Local, :remote_aliased_o, {:user_type, _, :remote_aliased_t, []}}, - {Local, :remote_aliased_inline_o, - {:remote_type, _, [{:atom, _, Remote}, {:atom, _, :remote_t}, []]}}, - {Local, :private_o, {:user_type, _, :private_t, []}}, - {Local, :opaque_o, {:user_type, _, :opaque_t, []}}, - {Local, :non_existent_o, - {:remote_type, _, [{:atom, _, Remote}, {:atom, _, :non_existent}, []]}}, - {Local, :large_o, {:user_type, _, :large_t, []}} - ] = TypeInfo.extract_param_options(Local, :func_with_named_options, 0) - end - - test "func_with_options_as_inline_list" do - assert [ - {Local, :local_o, {:user_type, _, :local_t, []}}, - {Local, :builtin_o, - {:remote_type, _, [{:atom, _, :elixir}, {:atom, _, :keyword}, []]}} - ] = TypeInfo.extract_param_options(Local, :func_with_options_as_inline_list, 0) - end - - test "func_with_option_var_defined_in_when" do - assert [ - {Local, :local_o, {:user_type, _, :local_t, []}}, - {Local, :local_with_params_o, - {:user_type, _, :local_t, [{:type, _, :atom, []}, {:type, _, :integer, []}]}}, - {Local, :union_o, {:user_type, _, :union_t, []}}, - {Local, :inline_union_o, {:type, _, :union, [{:atom, _, :a}, {:atom, _, :b}]}}, - {Local, :list_o, {:user_type, _, :list_t, []}}, - {Local, :inline_list_o, - {:type, _, :list, [{:type, _, :union, [{:atom, _, :trace}, {:atom, _, :log}]}]}}, - {Local, :basic_o, {:type, _, :pid, []}}, - {Local, :basic_with_params_o, {:type, _, :nonempty_list, [{:type, _, :atom, []}]}}, - {Local, :builtin_o, - {:remote_type, _, [{:atom, _, :elixir}, {:atom, _, :keyword}, []]}}, - {Local, :builtin_with_params_o, - {:remote_type, _, - [{:atom, _, :elixir}, {:atom, _, :keyword}, [{:type, _, :term, []}]]}}, - {Local, :remote_o, - {:remote_type, _, [{:atom, _, Remote}, {:atom, _, :remote_t}, []]}}, - {Local, :remote_with_params_o, - {:remote_type, _, - [ - {:atom, _, Remote}, - {:atom, _, :remote_t}, - [{:type, _, :atom, []}, {:type, _, :integer, []}] - ]}}, - {Local, :remote_aliased_o, {:user_type, _, :remote_aliased_t, []}}, - {Local, :remote_aliased_inline_o, - {:remote_type, _, [{:atom, _, Remote}, {:atom, _, :remote_t}, []]}}, - {Local, :private_o, {:user_type, _, :private_t, []}}, - {Local, :opaque_o, {:user_type, _, :opaque_t, []}}, - {Local, :non_existent_o, - {:remote_type, _, [{:atom, _, Remote}, {:atom, _, :non_existent}, []]}}, - {Local, :large_o, {:user_type, _, :large_t, []}} - ] = TypeInfo.extract_param_options(Local, :func_with_option_var_defined_in_when, 0) - end - - test "func_with_options_var_defined_in_when" do - assert [ - {Local, :local_o, {:user_type, _, :local_t, []}}, - {Local, :local_with_params_o, - {:user_type, _, :local_t, [{:type, _, :atom, []}, {:type, _, :integer, []}]}}, - {Local, :union_o, {:user_type, _, :union_t, []}}, - {Local, :inline_union_o, {:type, _, :union, [{:atom, _, :a}, {:atom, _, :b}]}}, - {Local, :list_o, {:user_type, _, :list_t, []}}, - {Local, :inline_list_o, - {:type, _, :list, [{:type, _, :union, [{:atom, _, :trace}, {:atom, _, :log}]}]}}, - {Local, :basic_o, {:type, _, :pid, []}}, - {Local, :basic_with_params_o, {:type, _, :nonempty_list, [{:type, _, :atom, []}]}}, - {Local, :builtin_o, - {:remote_type, _, [{:atom, _, :elixir}, {:atom, _, :keyword}, []]}}, - {Local, :builtin_with_params_o, - {:remote_type, _, - [{:atom, _, :elixir}, {:atom, _, :keyword}, [{:type, _, :term, []}]]}}, - {Local, :remote_o, - {:remote_type, _, [{:atom, _, Remote}, {:atom, _, :remote_t}, []]}}, - {Local, :remote_with_params_o, - {:remote_type, _, - [ - {:atom, _, Remote}, - {:atom, _, :remote_t}, - [{:type, _, :atom, []}, {:type, _, :integer, []}] - ]}}, - {Local, :remote_aliased_o, {:user_type, _, :remote_aliased_t, []}}, - {Local, :remote_aliased_inline_o, - {:remote_type, _, [{:atom, _, Remote}, {:atom, _, :remote_t}, []]}}, - {Local, :private_o, {:user_type, _, :private_t, []}}, - {Local, :opaque_o, {:user_type, _, :opaque_t, []}}, - {Local, :non_existent_o, - {:remote_type, _, [{:atom, _, Remote}, {:atom, _, :non_existent}, []]}}, - {Local, :large_o, {:user_type, _, :large_t, []}} - ] = TypeInfo.extract_param_options(Local, :func_with_options_var_defined_in_when, 0) - end - - test "func_with_one_option" do - assert [ - {Local, :local_o, {:user_type, _, :local_t, []}}, - {Local, :local_with_params_o, - {:user_type, _, :local_t, [{:type, _, :atom, []}, {:type, _, :integer, []}]}}, - {Local, :union_o, {:user_type, _, :union_t, []}}, - {Local, :inline_union_o, {:type, _, :union, [{:atom, _, :a}, {:atom, _, :b}]}}, - {Local, :list_o, {:user_type, _, :list_t, []}}, - {Local, :inline_list_o, - {:type, _, :list, [{:type, _, :union, [{:atom, _, :trace}, {:atom, _, :log}]}]}}, - {Local, :basic_o, {:type, _, :pid, []}}, - {Local, :basic_with_params_o, {:type, _, :nonempty_list, [{:type, _, :atom, []}]}}, - {Local, :builtin_o, - {:remote_type, _, [{:atom, _, :elixir}, {:atom, _, :keyword}, []]}}, - {Local, :builtin_with_params_o, - {:remote_type, _, - [{:atom, _, :elixir}, {:atom, _, :keyword}, [{:type, _, :term, []}]]}}, - {Local, :remote_o, - {:remote_type, _, [{:atom, _, Remote}, {:atom, _, :remote_t}, []]}}, - {Local, :remote_with_params_o, - {:remote_type, _, - [ - {:atom, _, Remote}, - {:atom, _, :remote_t}, - [{:type, _, :atom, []}, {:type, _, :integer, []}] - ]}}, - {Local, :remote_aliased_o, {:user_type, _, :remote_aliased_t, []}}, - {Local, :remote_aliased_inline_o, - {:remote_type, _, [{:atom, _, Remote}, {:atom, _, :remote_t}, []]}}, - {Local, :private_o, {:user_type, _, :private_t, []}}, - {Local, :opaque_o, {:user_type, _, :opaque_t, []}}, - {Local, :non_existent_o, - {:remote_type, _, [{:atom, _, Remote}, {:atom, _, :non_existent}, []]}}, - {Local, :large_o, {:user_type, _, :large_t, []}} - ] = TypeInfo.extract_param_options(Local, :func_with_named_options, 0) - end - - test "fun_without_options" do - assert [] = TypeInfo.extract_param_options(Local, :fun_without_options, 0) - end - - test "fun_with_atom_option" do - assert [{Local, :option_name}] == - TypeInfo.extract_param_options(Local, :fun_with_atom_option, 0) - end - - test "fun_with_atom_option_in_when" do - assert [{Local, :option_name}] == - TypeInfo.extract_param_options(Local, :fun_with_atom_option_in_when, 0) - end - - test "fun_with_recursive_remote_type_option" do - assert [ - {Remote, :remote_option_1, {:user_type, _, :remote_t, []}}, - {Remote, :remote_option_2, {:user_type, _, :remote_list_t, []}} - ] = TypeInfo.extract_param_options(Local, :fun_with_recursive_remote_type_option, 0) - end - - test "fun_with_recursive_user_type_option" do - assert [ - {Local, :option_1, {:type, _, :atom, []}}, - {Local, :option_2, {:type, _, :integer, []}} - ] = TypeInfo.extract_param_options(Local, :fun_with_recursive_user_type_option, 0) - end - - test "fun_with_tuple_option_in_when" do - assert [{Local, :opt_name, {:atom, _, :opt_value}}] = - TypeInfo.extract_param_options(Local, :fun_with_tuple_option_in_when, 0) - end - - test "fun_with_tuple_option" do - assert [{Local, :opt_name, {:atom, _, :opt_value}}] = - TypeInfo.extract_param_options(Local, :fun_with_tuple_option, 0) - end - - test "fun_with_atom_user_type_option_in_when" do - assert [{Local, :atom_opt}] == - TypeInfo.extract_param_options(Local, :fun_with_atom_user_type_option_in_when, 0) - end - - test "fun_with_atom_user_type_option" do - assert [{Local, :atom_opt}] == - TypeInfo.extract_param_options(Local, :fun_with_atom_user_type_option, 0) - end - - test "fun_with_list_of_lists" do - assert [] == TypeInfo.extract_param_options(Local, :fun_with_list_of_lists, 0) - end - - test "fun_with_recursive_type" do - assert [] == TypeInfo.extract_param_options(Local, :fun_with_recursive_type, 0) - end - - test "fun_with_multiple_specs" do - assert [{Local, :opt_name, {:atom, _, :opt_value}}] = - TypeInfo.extract_param_options(Local, :fun_with_multiple_specs, 0) - end - - test "fun_with_multiple_specs_when" do - assert [{Local, :opt_name, {:atom, _, :opt_value}}] = - TypeInfo.extract_param_options(Local, :fun_with_multiple_specs_when, 0) - end - - test "fun_with_local_opaque" do - assert [] = TypeInfo.extract_param_options(Local, :fun_with_local_opaque, 0) - end - - test "fun_with_remote_opaque" do - assert [] = TypeInfo.extract_param_options(Local, :fun_with_remote_opaque, 0) - end - test "builtin_type_documentation" do assert [%{name: "any", params: [], spec: "@type any"}] = TypeInfo.get_signatures(nil, :any, nil) diff --git a/test/support/options.ex b/test/support/options.ex new file mode 100644 index 00000000..ce15debe --- /dev/null +++ b/test/support/options.ex @@ -0,0 +1,14 @@ +defmodule ElixirSenseExample.Options.Foo1 do + @spec bar([{:option1, integer()}]) :: :ok + def bar(options), do: :ok +end + +defmodule ElixirSenseExample.Options.Foo do + @spec bar([{:option1, integer()} | {:option2, String.t()}]) :: :ok + def bar(options), do: :ok +end + +defmodule ElixirSenseExample.Options.With do + @spec bar(x) :: :ok when x: [{:option1, integer()} | {:option2, String.t()}] + def bar(options), do: :ok +end