From 4ff3d09cc632c630c0736ea6434a9a26d372858c Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 17 Feb 2024 14:51:52 +0100 Subject: [PATCH] provide docs and signatures on builtin functions in more cases workaround missing docs on Exception callbacks Fixes https://github.com/elixir-lsp/elixir_sense/issues/271 --- lib/elixir_sense/core/builtin_functions.ex | 146 ++++++++++++++++++ lib/elixir_sense/core/metadata.ex | 21 +++ lib/elixir_sense/core/normalized/code.ex | 7 + lib/elixir_sense/providers/docs.ex | 14 +- .../providers/suggestion/complete.ex | 5 +- test/elixir_sense/docs_test.exs | 12 +- .../providers/suggestion/complete_test.exs | 4 +- .../providers/suggestion_test.exs | 6 +- test/elixir_sense/signature_test.exs | 20 +-- 9 files changed, 207 insertions(+), 28 deletions(-) diff --git a/lib/elixir_sense/core/builtin_functions.ex b/lib/elixir_sense/core/builtin_functions.ex index e0de2a37..72401c64 100644 --- a/lib/elixir_sense/core/builtin_functions.ex +++ b/lib/elixir_sense/core/builtin_functions.ex @@ -70,6 +70,144 @@ defmodule ElixirSense.Core.BuiltinFunctions do } } + @docs %{ + {:module_info, 0} => """ + The `module_info/0` function in each module, returns a list of `{Key,Value}` + tuples with information about the module. Currently, the list contain tuples with the following + `Keys`: `module`, `attributes`, `compile`, `exports`, `md5` and `native`. The order and number + of tuples may change without prior notice. + """, + {:module_info, 1} => """ + The call `module_info(Key)`, where `Key` is an atom, returns a single piece of information about the module. + + The following values are allowed for Key: + + - **`module`** + Returns an atom representing the module name. + + - **`attributes`** + Returns a list of `{AttributeName,ValueList}` tuples, where `AttributeName` is the name of an attribute, + and `ValueList` is a list of values. Notice that a given attribute can occur more than once in the list + with different values if the attribute occurs more than once in the module. + The list of attributes becomes empty if the module is stripped with the + [`beam_lib(3)`](https://www.erlang.org/doc/man/beam_lib#strip-1) module (in STDLIB). + + - **`compile`** + Returns a list of tuples with information about how the module was compiled. This list is empty + if the module has been stripped with the [`beam_lib(3)`](https://www.erlang.org/doc/man/beam_lib#strip-1) + module (in STDLIB). + + - **`md5`** + Returns a binary representing the MD5 checksum of the module. + + - **`exports`** + Returns a list of `{Name,Arity}` tuples with all exported functions in the module. + + - **`functions`** + Returns a list of `{Name,Arity}` tuples with all functions in the module. + + - **`nifs`** + Returns a list of `{Name,Arity}` tuples with all NIF functions in the module. + + - **`native`** + Return `true` if the module has native compiled code. Return `false` otherwise. In a system compiled + without HiPE support, the result is always `false` + """, + {:behaviour_info, 1} => """ + The `behaviour_info(Key)` function, where `Key` is an atom, retrieves specific information related to the Erlang + behaviour module's callbacks. + + This function supports two distinct keys: + + - **`callbacks`** + Returns a list of `{Name,Arity}` tuples, where each tuple represents a callback function within the behaviour. + This list is generated based on the `-callback` attributes defined in the behaviour module. + + - **`optional_callbacks`** + Returns a list of `{OptName,OptArity}` tuples, detailing the optional callback functions for the behaviour. + These optional callbacks are defined using the `-optional_callbacks` attribute in conjunction with the `-callback` attribute. + """, + {:__info__, 1} => + Code.fetch_docs(Module) + |> elem(6) + |> Enum.find(fn t -> elem(t, 0) == {:callback, :__info__, 1} end) + |> elem(3) + |> Map.fetch!("en"), + {:__struct__, 0} => """ + Returns the struct + """, + {:__struct__, 1} => """ + Returns a new struct filled from the given keyword list. + """, + {:impl_for, 1} => """ + Returns the module that implements the protocol for the given argument, `nil` otherwise. + + For example, for the `Enumerable` protocol we have: + + iex> Enumerable.impl_for([]) + Enumerable.List + + iex> Enumerable.impl_for(42) + nil + """, + {:impl_for!, 1} => """ + Returns the module that implements the protocol for the given argument, raises `Protocol.UndefinedError` + if an implementation is not found + """, + {:__protocol__, 1} => """ + Returns the protocol information. + + The function takes one of the following atoms: + + * `:consolidated?` - returns whether the protocol is consolidated + + * `:functions` - returns a keyword list of protocol functions and their arities + + * `:impls` - if consolidated, returns `{:consolidated, modules}` with the list of modules + implementing the protocol, otherwise `:not_consolidated` + + * `:module` - the protocol module atom name + + For example, for the `Enumerable` protocol we have: + + iex> Enumerable.__protocol__(:functions) + [count: 1, member?: 2, reduce: 3, slice: 1] + """, + {:__impl__, 1} => """ + Returns the protocol implementation information. + + The function takes one of the following atoms: + + * `:for` - returns the module responsible for the data structure of the + protocol implementation + + * `:protocol` - returns the protocol module for which this implementation + is provided + + For example, the module implementing the `Enumerable` protocol for lists is + `Enumerable.List`. Therefore, we can invoke `__impl__/1` on this module: + + iex(1)> Enumerable.List.__impl__(:for) + List + + iex(2)> Enumerable.List.__impl__(:protocol) + Enumerable + """, + {:exception, 1} => """ + Receives the arguments given to `raise/2` and returns the exception struct. + + The default implementation accepts either a set of keyword arguments that is merged into + the struct or a string to be used as the exception's message. + """, + {:message, 1} => """ + Receives the exception struct and must return its message. + + Most commonly exceptions have a message field which by default is accessed by this function. + However, if an exception does not have a message field, this function must be explicitly + implemented. + """ + } + @all Map.keys(@functions) for {{f, a}, %{specs: specs}} <- @functions do @@ -82,6 +220,14 @@ defmodule ElixirSense.Core.BuiltinFunctions do do: unquote(args) end + for {{f, a}, doc} <- @docs do + def get_docs({unquote(f), unquote(a)}), + do: unquote(doc) + end + + # TODO exception message + def get_docs(_), do: "" + def all, do: @all def erlang_builtin_functions(:erlang), do: [{:andalso, 2}, {:orelse, 2}] diff --git a/lib/elixir_sense/core/metadata.ex b/lib/elixir_sense/core/metadata.ex index 3e324d7f..7f5bd3d0 100644 --- a/lib/elixir_sense/core/metadata.ex +++ b/lib/elixir_sense/core/metadata.ex @@ -7,6 +7,7 @@ defmodule ElixirSense.Core.Metadata do alias ElixirSense.Core.Introspection alias ElixirSense.Core.Normalized.Code, as: NormalizedCode alias ElixirSense.Core.State + alias ElixirSense.Core.BuiltinFunctions @type t :: %ElixirSense.Core.Metadata{ source: String.t(), @@ -308,7 +309,27 @@ defmodule ElixirSense.Core.Metadata do end) end + @builtin_functions BuiltinFunctions.all() + |> Enum.map(&elem(&1, 0)) + |> Kernel.--([:exception, :message]) + @spec get_function_signatures(__MODULE__.t(), module, atom) :: [signature_t] + def get_function_signatures(%__MODULE__{} = _metadata, module, function) + when module != nil and function in @builtin_functions do + for {f, a} <- BuiltinFunctions.all(), f == function do + spec = BuiltinFunctions.get_specs({f, a}) |> Enum.join("\n") + args = BuiltinFunctions.get_args({f, a}) + docs = BuiltinFunctions.get_docs({f, a}) + + %{ + name: Atom.to_string(function), + params: args, + documentation: Introspection.extract_summary_from_docs(docs), + spec: spec + } + end + end + def get_function_signatures(%__MODULE__{} = metadata, module, function) when not is_nil(module) and not is_nil(function) do metadata.mods_funs_to_positions diff --git a/lib/elixir_sense/core/normalized/code.ex b/lib/elixir_sense/core/normalized/code.ex index 62c44146..83ba2c14 100644 --- a/lib/elixir_sense/core/normalized/code.ex +++ b/lib/elixir_sense/core/normalized/code.ex @@ -198,6 +198,13 @@ defmodule ElixirSense.Core.Normalized.Code do ) ) |> Stream.map(fn {{_kind, name, arity}, _anno, signatures, docs, metadata} -> + docs = + if module == Exception and name in [:exception, :message] do + %{"en" => ElixirSense.Core.BuiltinFunctions.get_docs({name, arity})} + else + docs + end + {{name, arity}, {signatures, docs, metadata |> Map.put(:implementing, module) |> Map.put(:implementing_module_app, app), diff --git a/lib/elixir_sense/providers/docs.ex b/lib/elixir_sense/providers/docs.ex index 68e434f4..8474bdd8 100644 --- a/lib/elixir_sense/providers/docs.ex +++ b/lib/elixir_sense/providers/docs.ex @@ -62,6 +62,10 @@ defmodule ElixirSense.Providers.Docs do @type doc :: module_doc | function_doc | type_doc | variable_doc | attribute_doc | keyword_doc + @builtin_functions BuiltinFunctions.all() + |> Enum.map(&elem(&1, 0)) + |> Kernel.--([:exception, :message]) + @spec all( any, State.Env.t(), @@ -197,7 +201,7 @@ defmodule ElixirSense.Providers.Docs do doc_infos = metadata.mods_funs_to_positions |> Enum.filter(fn - {{^mod, ^fun, a}, fun_info} when not is_nil(a) -> + {{^mod, ^fun, a}, fun_info} when not is_nil(a) and fun not in @builtin_functions -> default_args = fun_info.params |> Enum.at(-1) |> Introspection.count_defaults() Introspection.matches_arity_with_defaults?(a, default_args, arity) @@ -417,10 +421,11 @@ defmodule ElixirSense.Providers.Docs do # TODO spec @spec get_func_docs(nil | module, atom, non_neg_integer | :any) :: list(function_doc()) def get_func_docs(mod, fun, arity) - when mod != nil and fun in [:module_info, :behaviour_info, :__info__] do + when mod != nil and fun in @builtin_functions do for {f, a} <- BuiltinFunctions.all(), f == fun, Introspection.matches_arity?(a, arity) do - spec = BuiltinFunctions.get_specs({f, a}) + spec = BuiltinFunctions.get_specs({f, a}) |> dbg args = BuiltinFunctions.get_args({f, a}) + docs = BuiltinFunctions.get_docs({f, a}) metadata = %{builtin: true} @@ -432,8 +437,7 @@ defmodule ElixirSense.Providers.Docs do args: args, metadata: metadata, specs: spec, - # TODO provide docs - docs: "" + docs: docs } end end diff --git a/lib/elixir_sense/providers/suggestion/complete.ex b/lib/elixir_sense/providers/suggestion/complete.ex index e240e474..fdcc3b8e 100644 --- a/lib/elixir_sense/providers/suggestion/complete.ex +++ b/lib/elixir_sense/providers/suggestion/complete.ex @@ -1427,8 +1427,9 @@ defmodule ElixirSense.Providers.Suggestion.Complete do fa = {name |> String.to_atom(), a} - if fa in BuiltinFunctions.all() do + if fa in (BuiltinFunctions.all() -- [exception: 1, message: 1]) do args = BuiltinFunctions.get_args(fa) + docs = BuiltinFunctions.get_docs(fa) %{ type: kind, @@ -1441,7 +1442,7 @@ defmodule ElixirSense.Providers.Suggestion.Complete do needed_require: nil, needed_import: nil, origin: mod_name, - summary: "Built-in function", + summary: Introspection.extract_summary_from_docs(docs), metadata: %{builtin: true}, spec: BuiltinFunctions.get_specs(fa) |> Enum.join("\n"), snippet: nil diff --git a/test/elixir_sense/docs_test.exs b/test/elixir_sense/docs_test.exs index 64e7707b..8b5b90e6 100644 --- a/test/elixir_sense/docs_test.exs +++ b/test/elixir_sense/docs_test.exs @@ -1110,7 +1110,7 @@ defmodule ElixirSense.DocsTest do docs: [doc] } = ElixirSense.docs(buffer, 2, 42) - assert doc == %{ + assert %{ args: [], function: :module_info, module: ElixirSenseExample.ModuleWithFunctions, @@ -1119,15 +1119,15 @@ defmodule ElixirSense.DocsTest do specs: [ "@spec module_info :: [{:module | :attributes | :compile | :exports | :md5 | :native, term}]" ], - docs: "", + docs: "The `module_info/0` function in each module" <> _, kind: :function - } + } = doc assert %{ docs: [doc] } = ElixirSense.docs(buffer, 4, 42) - assert doc == %{ + assert %{ args: ["key"], arity: 1, function: :module_info, @@ -1140,9 +1140,9 @@ defmodule ElixirSense.DocsTest do "@spec module_info(:exports | :functions | :nifs) :: [{atom, non_neg_integer}]", "@spec module_info(:native) :: boolean" ], - docs: "", + docs: "The call `module_info(Key)`" <> _, kind: :function - } + } = doc assert %{docs: [%{function: :__info__}]} = ElixirSense.docs(buffer, 6, 42) diff --git a/test/elixir_sense/providers/suggestion/complete_test.exs b/test/elixir_sense/providers/suggestion/complete_test.exs index a701088c..b2729b91 100644 --- a/test/elixir_sense/providers/suggestion/complete_test.exs +++ b/test/elixir_sense/providers/suggestion/complete_test.exs @@ -1952,7 +1952,7 @@ defmodule ElixirSense.Providers.Suggestion.CompleteTest do name: "message", type: :function, arity: 1, - spec: "@spec message(Exception.t()) :: String.t()" + spec: "@callback message(t()) :: String.t()" } ] = expand(~c"ArgumentError.mes") @@ -1963,7 +1963,7 @@ defmodule ElixirSense.Providers.Suggestion.CompleteTest do name: "exception", type: :function, arity: 1, - spec: "@spec exception(term) :: Exception.t()" + spec: "@callback exception(term()) :: t()" } ] = expand(~c"ArgumentError.exce") diff --git a/test/elixir_sense/providers/suggestion_test.exs b/test/elixir_sense/providers/suggestion_test.exs index 5e98a877..2e214fbb 100644 --- a/test/elixir_sense/providers/suggestion_test.exs +++ b/test/elixir_sense/providers/suggestion_test.exs @@ -42,7 +42,7 @@ defmodule ElixirSense.Providers.SuggestionTest do origin: "ElixirSenseExample.EmptyModule", spec: "@spec __info__(:attributes) :: keyword()\n@spec __info__(:compile) :: [term()]\n@spec __info__(:functions) :: [{atom, non_neg_integer}]\n@spec __info__(:macros) :: [{atom, non_neg_integer}]\n@spec __info__(:md5) :: binary()\n@spec __info__(:module) :: module()", - summary: "Built-in function", + summary: "Provides runtime information" <> _, type: :function, metadata: %{builtin: true}, snippet: nil, @@ -58,7 +58,7 @@ defmodule ElixirSense.Providers.SuggestionTest do origin: "ElixirSenseExample.EmptyModule", spec: "@spec module_info :: [{:module | :attributes | :compile | :exports | :md5 | :native, term}]", - summary: "Built-in function", + summary: "The `module_info/0` function" <> _, type: :function, metadata: %{builtin: true}, snippet: nil, @@ -74,7 +74,7 @@ defmodule ElixirSense.Providers.SuggestionTest do origin: "ElixirSenseExample.EmptyModule", spec: "@spec module_info(:module) :: atom\n@spec module_info(:attributes | :compile) :: [{atom, term}]\n@spec module_info(:md5) :: binary\n@spec module_info(:exports | :functions | :nifs) :: [{atom, non_neg_integer}]\n@spec module_info(:native) :: boolean", - summary: "Built-in function", + summary: "The call `module_info(Key)`" <> _, type: :function, metadata: %{builtin: true}, snippet: nil, diff --git a/test/elixir_sense/signature_test.exs b/test/elixir_sense/signature_test.exs index 9b35c12d..813b2cc6 100644 --- a/test/elixir_sense/signature_test.exs +++ b/test/elixir_sense/signature_test.exs @@ -1420,18 +1420,18 @@ defmodule ElixirSense.SignatureTest do end """ - assert ElixirSense.signature(buffer, 2, 54) == %{ + assert %{ active_param: 0, signatures: [ %{ - documentation: "Built-in function", + documentation: "The `module_info/0` function" <> _, name: "module_info", params: [], spec: "@spec module_info :: [{:module | :attributes | :compile | :exports | :md5 | :native, term}]" }, %{ - documentation: "Built-in function", + documentation: "The call `module_info(Key)`" <> _, name: "module_info", params: ["key"], spec: """ @@ -1443,13 +1443,13 @@ defmodule ElixirSense.SignatureTest do """ } ] - } + } = ElixirSense.signature(buffer, 2, 54) - assert ElixirSense.signature(buffer, 4, 51) == %{ + assert %{ active_param: 0, signatures: [ %{ - documentation: "Built-in function", + documentation: "Provides runtime informatio" <> _, name: "__info__", params: ["atom"], spec: """ @@ -1462,20 +1462,20 @@ defmodule ElixirSense.SignatureTest do """ } ] - } + } = ElixirSense.signature(buffer, 4, 51) - assert ElixirSense.signature(buffer, 6, 54) == %{ + assert %{ active_param: 0, signatures: [ %{ - documentation: "Built-in function", + documentation: "The `behaviour_info(Key)`" <> _, name: "behaviour_info", params: ["key"], spec: "@spec behaviour_info(:callbacks | :optional_callbacks) :: [{atom, non_neg_integer}]" } ] - } + } = ElixirSense.signature(buffer, 6, 54) end test "built-in functions cannot be called locally" do