diff --git a/lib/elixir_sense/core/introspection.ex b/lib/elixir_sense/core/introspection.ex index c7b2268c..dee6a005 100644 --- a/lib/elixir_sense/core/introspection.ex +++ b/lib/elixir_sense/core/introspection.ex @@ -45,6 +45,16 @@ defmodule ElixirSense.Core.Introspection do :application => Application } + defguard matches_arity?(is, expected) + when is != nil and + (expected == :any or is == expected or + (is_tuple(expected) and elem(expected, 0) == :gte and is >= elem(expected, 1))) + + defguard matches_arity_with_defaults?(is, defaults, expected) + when is != nil and + (expected == :any or (is_integer(expected) and expected in (is - defaults)..is) or + (is_tuple(expected) and elem(expected, 0) == :gte and is >= elem(expected, 1))) + @spec get_exports(module) :: [{atom, {non_neg_integer, :macro | :function}}] def get_exports(Elixir), do: [] @@ -122,17 +132,122 @@ defmodule ElixirSense.Core.Introspection do Keyword.has_key?(get_exports(module), fun) end - def get_all_docs({mod, nil, _}, :mod_fun) do - get_docs_md(mod) + def get_all_docs({mod, nil, _}, metadata, :mod_fun) do + doc_info = + metadata.mods_funs_to_positions + |> Enum.find_value(fn + {{^mod, nil, nil}, _fun_info} -> + %{ + module: mod, + # TODO + metadata: %{}, + doc: nil + } + + _ -> + false + end) + + if doc_info == nil do + get_docs_md(mod) + else + doc_info + end + |> format_module_doc end - def get_all_docs({mod, fun, arity}, :mod_fun) do - get_func_docs_md(mod, fun, arity) + def get_all_docs({mod, fun, arity}, metadata, :mod_fun) do + doc_infos = + metadata.mods_funs_to_positions + |> Enum.filter(fn + {{^mod, ^fun, a}, fun_info} when not is_nil(a) -> + default_args = fun_info.params |> Enum.at(-1) |> count_defaults() + + matches_arity_with_defaults?(a, default_args, arity) + + _ -> + false + end) + |> Enum.sort_by(fn {{^mod, ^fun, a}, _fun_info} -> a end) + |> Enum.map(fn {{^mod, ^fun, a}, fun_info} -> + fun_args_text = + fun_info.params |> List.last() |> Enum.with_index() |> Enum.map(¶m_to_var/1) + + specs = + case metadata.specs[{mod, fun, a}] do + nil -> + [] + + %State.SpecInfo{specs: specs} -> + specs |> Enum.reverse() + end + + %{ + module: mod, + function: fun, + # TODO + metadata: %{}, + specs: specs, + # args: args + fun_args_text: fun_args_text, + # TODO provide docs + docs: nil + } + end) + + doc_infos = + if doc_infos == [] do + get_func_docs_md(mod, fun, arity) + else + doc_infos + end + + doc_infos + |> Enum.map(&format_func_docs/1) |> join_docs end - def get_all_docs({mod, fun, arity}, :type) do - get_type_docs_md(mod, fun, arity) + def get_all_docs({mod, fun, arity}, metadata, :type) do + doc_infos = + metadata.types + |> Enum.filter(fn + {{^mod, ^fun, a}, _type_info} when not is_nil(a) -> + matches_arity?(a, arity) + + _ -> + false + end) + |> Enum.sort_by(fn {{_mod, _fun, a}, _fun_info} -> a end) + |> Enum.map(fn {_, type_info} -> + args = type_info.args |> List.last() |> Enum.join(", ") + + spec = + case type_info.kind do + :opaque -> "@opaque #{fun}(#{args})" + _ -> List.last(type_info.specs) + end + + %{ + module: mod, + type: fun, + # TODO + metadata: %{}, + spec: spec, + args: args, + # TODO provide docs + docs: nil + } + end) + + doc_infos = + if doc_infos == [] do + get_type_docs_md(mod, fun, arity) + else + doc_infos + end + + doc_infos + |> Enum.map(&format_type_docs/1) |> join_docs end @@ -261,15 +376,22 @@ defmodule ElixirSense.Core.Introspection do end end - defguard matches_arity?(is, expected) - when is != nil and - (expected == :any or is == expected or - (is_tuple(expected) and elem(expected, 0) == :gte and is >= elem(expected, 1))) + defp format_func_docs(info) do + mod_str = inspect(info.module) + fun_str = Atom.to_string(info.function) + # fun_args_text = Enum.join(info.args, ", ") + # spec_text = "### Specs\n\n```\n#{info.specs |> Enum.join("\n")}\n```\n\n" - defguard matches_arity_with_defaults?(is, defaults, expected) - when is != nil and - (expected == :any or (is_integer(expected) and expected in (is - defaults)..is) or - (is_tuple(expected) and elem(expected, 0) == :gte and is >= elem(expected, 1))) + spec_text = + if info.specs != [] do + joined = Enum.join(info.specs, "\n") + "### Specs\n\n```\n#{joined}\n```\n\n" + else + "" + end + + "> #{mod_str}.#{fun_str}(#{info.fun_args_text})\n\n#{get_metadata_md(info.metadata)}#{spec_text}#{info.docs || ""}" + end @spec get_func_docs_md(nil | module, atom, non_neg_integer | :any) :: list(markdown) def get_func_docs_md(mod, fun, arity) @@ -279,14 +401,18 @@ defmodule ElixirSense.Core.Introspection do args = BuiltinFunctions.get_args({f, a}) fun_args_text = Enum.join(args, ", ") - - mod_str = inspect(mod) - fun_str = Atom.to_string(fun) - - spec_text = "### Specs\n\n```\n#{spec |> Enum.join("\n")}\n```\n\n" metadata = %{builtin: true} - "> #{mod_str}.#{fun_str}(#{fun_args_text})\n\n#{get_metadata_md(metadata)}#{spec_text}" + %{ + module: mod, + function: fun, + metadata: metadata, + specs: spec, + # args: args + fun_args_text: fun_args_text, + # TODO provide docs + docs: nil + } end end @@ -313,6 +439,7 @@ defmodule ElixirSense.Core.Introspection do |> String.replace("\\\\", "\\\\\\\\") else # as of otp 23 erlang callback implementation do not have signature metadata + # TODO get that from behaviour typespec? if arity == 0, do: "", else: Enum.map_join(1..arity, ", ", fn _ -> "term" end) end @@ -323,7 +450,15 @@ defmodule ElixirSense.Core.Introspection do TypeInfo.extract_params(params) |> Enum.map_join(", ", &Atom.to_string/1) end - "> #{inspect(mod)}.#{fun}(#{fun_args_text})\n\n#{get_metadata_md(metadata)}#{get_spec_text(mod, fun, arity, kind, metadata)}#{text}" + %{ + module: mod, + function: fun, + metadata: metadata, + # args: args, + specs: get_specs_text(mod, fun, arity, kind, metadata), + fun_args_text: fun_args_text, + docs: text + } end case results do @@ -342,7 +477,15 @@ defmodule ElixirSense.Core.Introspection do TypeInfo.get_function_specs(mod, fun, call_arity) do fun_args_text = TypeInfo.extract_params(params) |> Enum.map_join(", ", &Atom.to_string/1) - "> #{inspect(mod)}.#{fun}(#{fun_args_text})\n\n#{get_metadata_md(%{})}#{get_spec_text(mod, fun, arity, :function, %{})}" + %{ + module: mod, + function: fun, + metadata: %{}, + # args: args, + specs: get_specs_text(mod, fun, arity, :function, %{}), + fun_args_text: fun_args_text, + docs: nil + } end case results do @@ -373,20 +516,42 @@ defmodule ElixirSense.Core.Introspection do %{} end - "> #{inspect(mod)}.#{fun}(#{fun_args_text})\n\n#{get_metadata_md(metadata)}#{get_spec_text(mod, fun, arity, :function, metadata)}" + %{ + module: mod, + function: fun, + metadata: metadata, + # args: args, + specs: get_specs_text(mod, fun, arity, :function, metadata), + fun_args_text: fun_args_text, + docs: nil + } end end - def get_docs_md(mod) when is_atom(mod) do - mod_str = inspect(mod) + defp format_module_doc(nil), do: nil + defp format_module_doc(info) do + mod_str = inspect(info.module) + doc = info.doc || "" + "> #{mod_str}\n\n" <> get_metadata_md(info.metadata) <> doc + end + + def get_docs_md(mod) when is_atom(mod) do case NormalizedCode.get_docs(mod, :moduledoc) do {_line, doc, metadata} when is_binary(doc) -> - "> #{mod_str}\n\n" <> get_metadata_md(metadata) <> doc + %{ + module: mod, + metadata: metadata, + doc: doc + } _ -> if Code.ensure_loaded?(mod) do - "> #{mod_str}\n\n" + %{ + module: mod, + metadata: %{}, + doc: nil + } end end end @@ -472,6 +637,20 @@ defmodule ElixirSense.Core.Introspection do "**#{key}**\n#{value}" end + defp format_type_docs(info) do + formatted_spec = "```\n#{info.spec}\n```" + + mod_formatted = + case info.module do + nil -> "" + atom -> inspect(atom) <> "." + end + + docs = info.docs || "" + + "> #{mod_formatted}#{info.type}(#{info.args})\n\n#{get_metadata_md(info.metadata)}### Definition\n\n#{formatted_spec}\n\n#{docs}" + end + @spec get_type_docs_md(nil | module, atom, non_neg_integer | :any) :: list(markdown) defp get_type_docs_md(nil, fun, arity) do for info <- BuiltinTypes.get_builtin_type_info(fun), @@ -489,7 +668,14 @@ defmodule ElixirSense.Core.Introspection do {"@type #{fun}()", ""} end - format_type_doc_md({nil, fun}, args, info[:doc], spec, %{builtin: true}) + %{ + module: nil, + type: fun, + args: args, + metadata: %{builtin: true}, + spec: spec, + docs: info[:doc] + } end end @@ -504,7 +690,14 @@ defmodule ElixirSense.Core.Introspection do type_args = Enum.map_join(args, ", ", &(&1 |> elem(2) |> Atom.to_string())) - format_type_doc_md({mod, fun}, type_args, "", spec, %{}) + %{ + module: mod, + type: fun, + args: type_args, + metadata: %{}, + spec: spec, + docs: nil + } end docs -> @@ -516,13 +709,14 @@ defmodule ElixirSense.Core.Introspection do {_kind, {_name, _def, args}} = spec type_args = Enum.map_join(args, ", ", &(&1 |> elem(2) |> Atom.to_string())) - format_type_doc_md( - {mod, fun}, - type_args, - text, - TypeInfo.format_type_spec(spec), - metadata - ) + %{ + module: mod, + type: fun, + args: type_args, + metadata: metadata, + spec: TypeInfo.format_type_spec(spec), + docs: text + } end end end @@ -1006,19 +1200,11 @@ defmodule ElixirSense.Core.Introspection do TypeInfo.get_spec(module, function, arity) |> spec_to_string() end - def get_spec_text(mod, fun, arity, kind, metadata) do - specs = - for arity <- (arity - Map.get(metadata, :defaults, 0))..arity, - spec = get_spec_as_string(mod, fun, arity, kind, metadata), - spec != "", - do: spec - - if specs != [] do - joined = Enum.join(specs, "\n") - "### Specs\n\n```\n#{joined}\n```\n\n" - else - "" - end + def get_specs_text(mod, fun, arity, kind, metadata) do + for arity <- (arity - Map.get(metadata, :defaults, 0))..arity, + spec = get_spec_as_string(mod, fun, arity, kind, metadata), + spec != "", + do: spec end # This function is used only for protocols so no macros @@ -1420,18 +1606,6 @@ defmodule ElixirSense.Core.Introspection do false end - defp format_type_doc_md({mod, fun}, type_args, doc, spec, metadata) when is_binary(type_args) do - formatted_spec = "```\n#{spec}\n```" - - mod_formatted = - case mod do - nil -> "" - atom -> inspect(atom) <> "." - end - - "> #{mod_formatted}#{fun}(#{type_args})\n\n#{get_metadata_md(metadata)}### Definition\n\n#{formatted_spec}\n\n#{doc}" - end - def is_pub(type), do: type in [:def, :defmacro, :defdelegate, :defguard] def is_priv(type), do: type in [:defp, :defmacrop, :defguardp] def is_function_type(type), do: type in [:def, :defp, :defdelegate] diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 60b92f84..6860311e 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -1122,6 +1122,19 @@ defmodule ElixirSense.Core.State do %__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, type_name, type_args, spec, kind, pos, end_pos, options) do arg_names = type_args @@ -1142,25 +1155,12 @@ defmodule ElixirSense.Core.State do specs = current_module_variants |> Enum.reduce(state.specs, fn current_module, acc -> - info = - case acc[{current_module, type_name, nil}] do - nil -> - type_info - - %SpecInfo{positions: positions, args: args, specs: specs} = ti -> - %SpecInfo{ - ti - | positions: [pos | positions], - end_positions: [end_pos | ti.end_positions], - generated: [Keyword.get(options, :generated, false) | ti.generated], - args: [arg_names | args], - specs: [spec | specs] - } - end + nil_info = combine_specs(acc[{current_module, type_name, nil}], type_info) + arity_info = combine_specs(acc[{current_module, type_name, length(arg_names)}], type_info) acc - |> Map.put({current_module, type_name, nil}, info) - |> Map.put({current_module, type_name, length(arg_names)}, type_info) + |> Map.put({current_module, type_name, nil}, nil_info) + |> Map.put({current_module, type_name, length(arg_names)}, arity_info) end) %__MODULE__{state | specs: specs} diff --git a/lib/elixir_sense/core/surround_context.ex b/lib/elixir_sense/core/surround_context.ex index ead43ee8..fd49a439 100644 --- a/lib/elixir_sense/core/surround_context.ex +++ b/lib/elixir_sense/core/surround_context.ex @@ -13,7 +13,13 @@ defmodule ElixirSense.Core.SurroundContext do def to_binding({:local_or_var, ~c"__MODULE__"}, current_module) do if current_module not in [nil, Elixir] do - {:atom, current_module} + {{:atom, current_module}, nil} + end + end + + def to_binding({:alias, {:local_or_var, ~c"__MODULE__"}, charlist}, current_module) do + if current_module not in [nil, Elixir] do + {{:atom, :"#{current_module}.#{charlist}"}, nil} end end diff --git a/lib/elixir_sense/core/type_info.ex b/lib/elixir_sense/core/type_info.ex index 8ee9acce..529c4db8 100644 --- a/lib/elixir_sense/core/type_info.ex +++ b/lib/elixir_sense/core/type_info.ex @@ -113,10 +113,19 @@ defmodule ElixirSense.Core.TypeInfo do for( {key, value} <- BuiltinTypes.all(), - type_ast <- [value[:spec]], - spec <- [format_type_spec_ast(type_ast, :type, line_length: @param_option_spec_line_length)], + spec = + case value do + %{spec: spec} -> + format_type_spec_ast(spec, :type, line_length: @param_option_spec_line_length) + + %{signature: signature} -> + "@type #{signature}" + + _ -> + "@type #{key}()" + end, signature <- [ - value[:signature] || TypeAst.extract_signature(type_ast) || "#{key}()" + value[:signature] || TypeAst.extract_signature(value[:spec]) || "#{key}()" ], {name, arity} = extract_name_and_arity.(key), doc = value[:doc] || "", diff --git a/lib/elixir_sense/providers/docs.ex b/lib/elixir_sense/providers/docs.ex index 546cc5d9..e1ed43e8 100644 --- a/lib/elixir_sense/providers/docs.ex +++ b/lib/elixir_sense/providers/docs.ex @@ -103,28 +103,6 @@ defmodule ElixirSense.Providers.Docs do end end - defp mod_fun_docs( - {:variable, name} = type, - context, - binding_env, - env, - metadata - ) do - case Binding.expand(binding_env, type) do - :none -> - mod_fun_docs( - {nil, name}, - context, - binding_env, - env, - metadata - ) - - _ -> - nil - end - end - defp mod_fun_docs( {mod, fun}, context, @@ -149,7 +127,7 @@ defmodule ElixirSense.Providers.Docs do {mod, fun, true, kind} -> {line, column} = context.end call_arity = Metadata.get_call_arity(metadata, mod, fun, line, column) || :any - markdown = Introspection.get_all_docs({mod, fun, call_arity}, kind) + markdown = Introspection.get_all_docs({mod, fun, call_arity}, metadata, kind) if markdown do {mod_fun_to_string({mod, fun}), markdown} diff --git a/lib/elixir_sense/providers/suggestion/reducers/type_specs.ex b/lib/elixir_sense/providers/suggestion/reducers/type_specs.ex index 4a6c44c9..9009c301 100644 --- a/lib/elixir_sense/providers/suggestion/reducers/type_specs.ex +++ b/lib/elixir_sense/providers/suggestion/reducers/type_specs.ex @@ -104,6 +104,7 @@ defmodule ElixirSense.Providers.Suggestion.Reducers.TypeSpecs do defp find_builtin_types({nil, hint}) do TypeInfo.find_all_builtin(&Matcher.match?("#{&1.name}", hint)) |> Enum.map(&type_info_to_suggestion(&1, nil)) + |> Enum.sort_by(fn %{name: name, arity: arity} -> {name, arity} end) end defp find_builtin_types({_mod, _hint}), do: [] @@ -113,6 +114,7 @@ defmodule ElixirSense.Providers.Suggestion.Reducers.TypeSpecs do |> Kernel.++(TypeInfo.find_all(actual_mod, &Matcher.match?("#{&1.name}", hint))) |> Enum.map(&type_info_to_suggestion(&1, actual_mod)) |> Enum.uniq_by(fn %{name: name, arity: arity} -> {name, arity} end) + |> Enum.sort_by(fn %{name: name, arity: arity} -> {name, arity} end) end defp find_metadata_types(actual_mod, {mod, hint}, metadata_types, module) do diff --git a/test/elixir_sense/core/introspection_test.exs b/test/elixir_sense/core/introspection_test.exs index 33e373df..35bccd6a 100644 --- a/test/elixir_sense/core/introspection_test.exs +++ b/test/elixir_sense/core/introspection_test.exs @@ -1,6 +1,7 @@ defmodule ElixirSense.Core.IntrospectionTest do use ExUnit.Case, async: true doctest ElixirSense.Core.Introspection + alias ElixirSense.Core.Metadata alias ElixirSense.Core.TypeInfo import ElixirSense.Core.Introspection @@ -763,6 +764,7 @@ defmodule ElixirSense.Core.IntrospectionTest do assert docs = get_all_docs( {ElixirSenseExample.ModuleWithDelegates, :delegated_fun, 2}, + %Metadata{}, :mod_fun ) @@ -778,7 +780,12 @@ defmodule ElixirSense.Core.IntrospectionTest do end test "returns since metadata on functions" do - assert docs = get_all_docs({ElixirSenseExample.ModuleWithDocs, :some_fun, 2}, :mod_fun) + assert docs = + get_all_docs( + {ElixirSenseExample.ModuleWithDocs, :some_fun, 2}, + %Metadata{}, + :mod_fun + ) assert docs == """ > ElixirSenseExample.ModuleWithDocs.some_fun(a, b \\\\\\\\ nil) @@ -795,6 +802,7 @@ defmodule ElixirSense.Core.IntrospectionTest do assert docs = get_all_docs( {ElixirSenseExample.ModuleWithDocs, :soft_deprecated_fun, 1}, + %Metadata{}, :mod_fun ) @@ -810,7 +818,12 @@ defmodule ElixirSense.Core.IntrospectionTest do end test "returns since metadata on types" do - assert docs = get_all_docs({ElixirSenseExample.ModuleWithDocs, :some_type, 0}, :type) + assert docs = + get_all_docs( + {ElixirSenseExample.ModuleWithDocs, :some_type, 0}, + %Metadata{}, + :type + ) assert docs == """ > ElixirSenseExample.ModuleWithDocs.some_type() @@ -830,7 +843,8 @@ defmodule ElixirSense.Core.IntrospectionTest do end test "returns since metadata on modules" do - assert docs = get_all_docs({ElixirSenseExample.ModuleWithDocs, nil, :any}, :mod_fun) + assert docs = + get_all_docs({ElixirSenseExample.ModuleWithDocs, nil, :any}, %Metadata{}, :mod_fun) assert docs == """ > ElixirSenseExample.ModuleWithDocs diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 475ded6b..458c3833 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -2863,15 +2863,17 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert state.specs == %{ {Proto, :with_spec, 2} => %ElixirSense.Core.State.SpecInfo{ - args: [["t", "boolean"]], + args: [["t", "boolean"], ["t", "integer"]], kind: :callback, name: :with_spec, - positions: [{3, 3}], - end_positions: [{3, 40}], - generated: [false], + positions: [{3, 3}, {2, 3}], + end_positions: [{3, 40}, {2, 42}], + generated: [false, false], specs: [ "@callback with_spec(t, boolean) :: number", - "@spec 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, :with_spec, nil} => %ElixirSense.Core.State.SpecInfo{ @@ -4674,13 +4676,13 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert state.specs == %{ {Proto, :abc, 0} => %ElixirSense.Core.State.SpecInfo{ - args: [[]], + args: [[], []], kind: :spec, name: :abc, - positions: [{3, 3}], - end_positions: [{3, 25}], - generated: [false], - specs: ["@spec abc :: reference"] + positions: [{3, 3}, {2, 3}], + end_positions: [{3, 25}, {2, 30}], + generated: [false, false], + specs: ["@spec abc :: reference", "@spec abc :: atom | integer"] }, {Proto, :abc, nil} => %ElixirSense.Core.State.SpecInfo{ kind: :spec, diff --git a/test/elixir_sense/definition_test.exs b/test/elixir_sense/definition_test.exs index 2f65ecca..38c84bcf 100644 --- a/test/elixir_sense/definition_test.exs +++ b/test/elixir_sense/definition_test.exs @@ -1046,6 +1046,26 @@ defmodule ElixirSense.Providers.DefinitionTest do } end + test "find definition of local __MODULE__" do + buffer = """ + defmodule MyModule do + def my_fun(), do: :ok + + def a do + my_fun1 = 1 + __MODULE__.my_fun() + end + end + """ + + assert ElixirSense.definition(buffer, 6, 6) == %Location{ + type: :module, + file: nil, + line: 1, + column: 1 + } + end + test "find definition of local functions with __MODULE__" do buffer = """ defmodule MyModule do @@ -1089,6 +1109,29 @@ defmodule ElixirSense.Providers.DefinitionTest do } end + @tag requires_elixir_1_14: true + test "find definition of local __MODULE__ submodule" do + buffer = """ + defmodule MyModule do + defmodule Sub do + def my_fun(), do: :ok + end + + def a do + my_fun1 = 1 + __MODULE__.Sub.my_fun() + end + end + """ + + assert ElixirSense.definition(buffer, 8, 17) == %Location{ + type: :module, + file: nil, + line: 2, + column: 3 + } + end + @tag requires_elixir_1_14: true test "find definition of local functions with @attr" do buffer = """ diff --git a/test/elixir_sense/docs_test.exs b/test/elixir_sense/docs_test.exs index 9abc4690..20556c09 100644 --- a/test/elixir_sense/docs_test.exs +++ b/test/elixir_sense/docs_test.exs @@ -25,7 +25,7 @@ defmodule ElixirSense.DocsTest do assert docs == "> ElixirSenseExample.ModuleWithDocFalse\n\n" end - test "retrieve documentation" do + test "retrieve documentation from Kernel macro" do buffer = """ defmodule MyModule do @@ -44,6 +44,26 @@ defmodule ElixirSense.DocsTest do """ end + test "retrieve documentation from Kernel.SpecialForm macro" do + buffer = """ + defmodule MyModule do + import List + ^ + end + """ + + %{ + actual_subject: actual_subject, + docs: docs + } = ElixirSense.docs(buffer, 2, 4) + + assert actual_subject == "Kernel.SpecialForms.import" + + assert docs =~ """ + Imports functions and macros\ + """ + end + test "retrieve function documentation" do buffer = """ defmodule MyModule do @@ -73,6 +93,213 @@ defmodule ElixirSense.DocsTest do """ end + test "retrieve metadata function documentation" do + buffer = """ + defmodule MyLocalModule do + @doc "Sample doc" + @doc since: "1.2.3" + @spec flatten(list()) :: list() + def flatten(list) do + [] + end + end + + defmodule MyModule do + def func(list) do + MyLocalModule.flatten(list) + end + end + """ + + %{ + actual_subject: actual_subject, + docs: docs + } = ElixirSense.docs(buffer, 12, 20) + + assert actual_subject == "MyLocalModule.flatten" + + assert docs =~ """ + > MyLocalModule.flatten(list) + + ### Specs + + ``` + @spec flatten(list) :: list + ``` + """ + + # TODO docs and metadata + end + + test "retrieve local private metadata function documentation" do + buffer = """ + defmodule MyLocalModule do + @doc "Sample doc" + @doc since: "1.2.3" + @spec flatten(list()) :: list() + defp flatten(list) do + [] + end + + def func(list) do + flatten(list) + end + end + """ + + %{ + actual_subject: actual_subject, + docs: docs + } = ElixirSense.docs(buffer, 10, 7) + + assert actual_subject == "MyLocalModule.flatten" + + assert docs =~ """ + > MyLocalModule.flatten(list) + + ### Specs + + ``` + @spec flatten(list) :: list + ``` + """ + + # TODO docs and metadata + end + + test "retrieve local private metadata function documentation on __MODULE__ call" do + buffer = """ + defmodule MyLocalModule do + @doc "Sample doc" + @doc since: "1.2.3" + @spec flatten(list()) :: list() + def flatten(list) do + [] + end + + def func(list) do + __MODULE__.flatten(list) + end + end + """ + + %{ + actual_subject: actual_subject, + docs: docs + } = ElixirSense.docs(buffer, 10, 17) + + assert actual_subject == "MyLocalModule.flatten" + + assert docs =~ """ + > MyLocalModule.flatten(list) + + ### Specs + + ``` + @spec flatten(list) :: list + ``` + """ + + # TODO docs and metadata + end + + @tag requires_elixir_1_14: true + test "retrieve local private metadata function documentation on __MODULE__ submodule call" do + buffer = """ + defmodule MyLocalModule do + defmodule Sub do + @doc "Sample doc" + @doc since: "1.2.3" + @spec flatten(list()) :: list() + def flatten(list) do + [] + end + end + + def func(list) do + __MODULE__.Sub.flatten(list) + end + end + """ + + %{ + actual_subject: actual_subject, + docs: docs + } = ElixirSense.docs(buffer, 12, 20) + + assert actual_subject == "MyLocalModule.Sub.flatten" + + assert docs =~ """ + > MyLocalModule.Sub.flatten(list) + + ### Specs + + ``` + @spec flatten(list) :: list + ``` + """ + + # TODO docs and metadata + end + + test "does not retrieve remote private metadata function documentation" do + buffer = """ + defmodule MyLocalModule do + @doc "Sample doc" + @doc since: "1.2.3" + @spec flatten(list()) :: list() + defp flatten(list) do + [] + end + end + + defmodule MyModule do + def func(list) do + MyLocalModule.flatten(list) + end + end + """ + + assert nil == ElixirSense.docs(buffer, 12, 20) + end + + test "retrieve metadata function documentation with @doc false" do + buffer = """ + defmodule MyLocalModule do + @doc false + @spec flatten(list()) :: list() + def flatten(list) do + [] + end + end + + defmodule MyModule do + def func(list) do + MyLocalModule.flatten(list) + end + end + """ + + %{ + actual_subject: actual_subject, + docs: docs + } = ElixirSense.docs(buffer, 11, 20) + + assert actual_subject == "MyLocalModule.flatten" + + assert docs =~ """ + > MyLocalModule.flatten(list) + + ### Specs + + ``` + @spec flatten(list) :: list + ``` + """ + + # TODO mark as hidden in metadata + end + test "retrieve function documentation on @attr call" do buffer = """ defmodule MyModule do @@ -206,7 +433,7 @@ defmodule ElixirSense.DocsTest do end @tag requires_elixir_1_14: true - test "retrieve function documentation with __MODULE__" do + test "retrieve function documentation with __MODULE__ submodule call" do buffer = """ defmodule Inspect do def func(list) do @@ -275,22 +502,6 @@ defmodule ElixirSense.DocsTest do """ end - test "request for defmacro" do - buffer = """ - defmodule MyModule do - defmacro my_macro do - end - end - """ - - %{actual_subject: actual_subject, docs: docs} = ElixirSense.docs(buffer, 2, 5) - - assert actual_subject == "Kernel.defmacro" - - assert docs =~ "Kernel.defmacro(" - assert docs =~ "macro with the given name and body." - end - test "retrieve documentation from modules" do buffer = """ defmodule MyModule do @@ -318,6 +529,116 @@ defmodule ElixirSense.DocsTest do """ end + test "retrieve documentation from metadata modules" do + buffer = """ + defmodule MyLocalModule do + @moduledoc "Some example doc" + @moduledoc since: "1.2.3" + + @callback some() :: :ok + end + + defmodule MyModule do + @behaviour MyLocalModule + end + """ + + %{ + actual_subject: actual_subject, + docs: docs + } = ElixirSense.docs(buffer, 9, 15) + + assert actual_subject == "MyLocalModule" + + assert docs =~ """ + > MyLocalModule + """ + + # TODO doc and metadata + end + + test "retrieve documentation from metadata modules on __MODULE__" do + buffer = """ + defmodule MyLocalModule do + @moduledoc "Some example doc" + @moduledoc since: "1.2.3" + + def self() do + __MODULE__ + end + end + """ + + %{ + actual_subject: actual_subject, + docs: docs + } = ElixirSense.docs(buffer, 6, 6) + + assert actual_subject == "MyLocalModule" + + assert docs =~ """ + > MyLocalModule + """ + + # TODO doc and metadata + end + + @tag requires_elixir_1_14: true + test "retrieve documentation from metadata modules on __MODULE__ submodule" do + buffer = """ + defmodule MyLocalModule do + defmodule Sub do + @moduledoc "Some example doc" + @moduledoc since: "1.2.3" + end + + def self() do + __MODULE__.Sub + end + end + """ + + %{ + actual_subject: actual_subject, + docs: docs + } = ElixirSense.docs(buffer, 8, 17) + + assert actual_subject == "MyLocalModule.Sub" + + assert docs =~ """ + > MyLocalModule.Sub + """ + + # TODO doc and metadata + end + + test "retrieve documentation from metadata modules with @moduledoc false" do + buffer = """ + defmodule MyLocalModule do + @moduledoc false + + @callback some() :: :ok + end + + defmodule MyModule do + @behaviour MyLocalModule + end + """ + + %{ + actual_subject: actual_subject, + docs: docs + } = ElixirSense.docs(buffer, 8, 15) + + assert actual_subject == "MyLocalModule" + + assert docs =~ """ + > MyLocalModule + """ + + # TODO mark as hidden + end + test "retrieve documentation from erlang modules" do buffer = """ defmodule MyModule do @@ -417,6 +738,164 @@ defmodule ElixirSense.DocsTest do """ end + test "retrieve metadata type documentation" do + buffer = """ + defmodule MyLocalModule do + @typedoc "My example type" + @typedoc since: "1.2.3" + @type some(a) :: {a} + end + + defmodule MyModule do + @type my_list :: MyLocalModule.some(:a) + # ^ + end + """ + + %{ + actual_subject: actual_subject, + docs: docs + } = ElixirSense.docs(buffer, 8, 35) + + assert actual_subject == "MyLocalModule.some" + + assert docs == """ + > MyLocalModule.some(a) + + ### Definition + + ``` + @type some(a) :: {a} + ``` + + + """ + + # TODO docs and metadata + end + + test "retrieve local private metadata type documentation" do + buffer = """ + defmodule MyLocalModule do + @typedoc "My example type" + @typedoc since: "1.2.3" + @typep some(a) :: {a} + + @type my_list :: some(:a) + # ^ + end + """ + + %{ + actual_subject: actual_subject, + docs: docs + } = ElixirSense.docs(buffer, 6, 22) + + assert actual_subject == "MyLocalModule.some" + + assert docs == """ + > MyLocalModule.some(a) + + ### Definition + + ``` + @typep some(a) :: {a} + ``` + + + """ + + # TODO docs and metadata + end + + test "does not retrieve remote private metadata type documentation" do + buffer = """ + defmodule MyLocalModule do + @typedoc "My example type" + @typedoc since: "1.2.3" + @typep some(a) :: {a} + end + + defmodule MyModule do + @type my_list :: MyLocalModule.some(:a) + # ^ + end + """ + + assert nil == ElixirSense.docs(buffer, 8, 35) + end + + test "does not reveal details for opaque metadata type" do + buffer = """ + defmodule MyLocalModule do + @typedoc "My example type" + @typedoc since: "1.2.3" + @opaque some(a) :: {a} + end + + defmodule MyModule do + @type my_list :: MyLocalModule.some(:a) + # ^ + end + """ + + %{ + actual_subject: actual_subject, + docs: docs + } = ElixirSense.docs(buffer, 8, 35) + + assert actual_subject == "MyLocalModule.some" + + assert docs == """ + > MyLocalModule.some(a) + + ### Definition + + ``` + @opaque some(a) + ``` + + + """ + + # TODO docs and metadata + end + + test "retrieve metadata type documentation with @typedoc false" do + buffer = """ + defmodule MyLocalModule do + @typedoc false + @type some(a) :: {a} + end + + defmodule MyModule do + @type my_list :: MyLocalModule.some(:a) + # ^ + end + """ + + %{ + actual_subject: actual_subject, + docs: docs + } = ElixirSense.docs(buffer, 7, 35) + + assert actual_subject == "MyLocalModule.some" + + assert docs == """ + > MyLocalModule.some(a) + + ### Definition + + ``` + @type some(a) :: {a} + ``` + + + """ + + # TODO mark as hidden + end + test "does not reveal opaque type details" do buffer = """ defmodule MyModule do @@ -1019,21 +1498,11 @@ defmodule ElixirSense.DocsTest do end """ - # TODO requires metadata docs support - - # assert %{actual_subject: "ElixirSenseExample.Some.some_local"} = - # ElixirSense.docs(buffer, 6, 20) - assert nil == + assert %{actual_subject: "ElixirSenseExample.Some.some_local"} = ElixirSense.docs(buffer, 6, 20) - # TODO assert type - # assert %{actual_subject: "ElixirSenseExample.Some.some_local"} = - # ElixirSense.docs(buffer, 9, 9) - - assert nil == + assert %{actual_subject: "ElixirSenseExample.Some.some_local"} = ElixirSense.docs(buffer, 9, 9) - - # TODO assert function end test "retrieves documentation for correct arity function" do diff --git a/test/elixir_sense/references_test.exs b/test/elixir_sense/references_test.exs index 4951a6f5..6967d56f 100644 --- a/test/elixir_sense/references_test.exs +++ b/test/elixir_sense/references_test.exs @@ -1036,7 +1036,9 @@ defmodule ElixirSense.Providers.ReferencesTest do end @tag requires_elixir_1_14: true - test "find references when module with __MODULE__ special form submodule", %{trace: trace} do + test "find references when module with __MODULE__ special form submodule function", %{ + trace: trace + } do buffer = """ defmodule ElixirSense.Providers.ReferencesTest.Modules do def func() do @@ -1062,7 +1064,27 @@ defmodule ElixirSense.Providers.ReferencesTest do end @tag requires_elixir_1_14: true - test "find references when module with __MODULE__ special form", %{trace: trace} do + test "find references when module with __MODULE__ special form submodule", %{trace: trace} do + buffer = """ + defmodule MyLocalModule do + defmodule Some do + def func() do + :ok + end + end + __MODULE__.Some.func() + end + """ + + references = ElixirSense.references(buffer, 7, 15, trace) + + assert references == [ + %{range: %{start: %{column: 19, line: 7}, end: %{column: 23, line: 7}}, uri: nil} + ] + end + + @tag requires_elixir_1_14: true + test "find references when module with __MODULE__ special form function", %{trace: trace} do buffer = """ defmodule ElixirSense.Providers.ReferencesTest.Modules do def func() do @@ -1085,6 +1107,29 @@ defmodule ElixirSense.Providers.ReferencesTest do ] end + test "find references when module with __MODULE__ special form", %{trace: trace} do + buffer = """ + defmodule MyLocalModule do + def func() do + __MODULE__.func() + # ^ + end + end + """ + + references = ElixirSense.references(buffer, 3, 10, trace) + + assert references == [ + %{ + uri: nil, + range: %{ + end: %{column: 20, line: 3}, + start: %{column: 16, line: 3} + } + } + ] + end + test "find references of variables", %{trace: trace} do buffer = """ defmodule MyModule do diff --git a/test/elixir_sense/suggestions_test.exs b/test/elixir_sense/suggestions_test.exs index 2fdb0911..cfdfc974 100644 --- a/test/elixir_sense/suggestions_test.exs +++ b/test/elixir_sense/suggestions_test.exs @@ -3424,13 +3424,25 @@ defmodule ElixirSense.SuggestionsTest do [_, suggestion | _] = suggestions_by_type(:type_spec, buffer) - assert suggestion.spec == "" + assert suggestion.spec == "@type list(t)" assert suggestion.signature == "list(t)" assert suggestion.arity == 1 assert suggestion.doc == "Proper list ([]-terminated)" assert suggestion.origin == nil end + test "builtin types - retrieve info from basic types" do + buffer = "defmodule My, do: @type my_type :: int" + + [_, suggestion | _] = suggestions_by_type(:type_spec, buffer) + + assert suggestion.spec == "@type integer()" + assert suggestion.signature == "integer()" + assert suggestion.arity == 0 + assert suggestion.doc == "An integer number" + assert suggestion.origin == nil + end + test "erlang types" do buffer = "defmodule My, do: @type my_type :: :erlang.time_"