Skip to content

Commit

Permalink
provide docs and signatures on builtin functions in more cases
Browse files Browse the repository at this point in the history
workaround missing docs on Exception callbacks
Fixes #271
  • Loading branch information
lukaszsamson committed Feb 17, 2024
1 parent c80cbcd commit 4ff3d09
Show file tree
Hide file tree
Showing 9 changed files with 207 additions and 28 deletions.
146 changes: 146 additions & 0 deletions lib/elixir_sense/core/builtin_functions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}]
Expand Down
21 changes: 21 additions & 0 deletions lib/elixir_sense/core/metadata.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions lib/elixir_sense/core/normalized/code.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
14 changes: 9 additions & 5 deletions lib/elixir_sense/providers/docs.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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

Check failure on line 426 in lib/elixir_sense/providers/docs.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.12.x | Erlang/OTP 24.x)

** (CompileError) lib/elixir_sense/providers/docs.ex:426: undefined function dbg/1

Check failure on line 426 in lib/elixir_sense/providers/docs.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 22.x)

** (CompileError) lib/elixir_sense/providers/docs.ex:426: undefined function dbg/1 (expected ElixirSense.Providers.Docs to define such a function or for it to be imported, but none are available)

Check failure on line 426 in lib/elixir_sense/providers/docs.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 23.x)

** (CompileError) lib/elixir_sense/providers/docs.ex:426: undefined function dbg/1 (expected ElixirSense.Providers.Docs to define such a function or for it to be imported, but none are available)

Check failure on line 426 in lib/elixir_sense/providers/docs.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.12.x | Erlang/OTP 22.x)

** (CompileError) lib/elixir_sense/providers/docs.ex:426: undefined function dbg/1

Check failure on line 426 in lib/elixir_sense/providers/docs.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.12.x | Erlang/OTP 23.x)

** (CompileError) lib/elixir_sense/providers/docs.ex:426: undefined function dbg/1

Check failure on line 426 in lib/elixir_sense/providers/docs.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 24.x)

** (CompileError) lib/elixir_sense/providers/docs.ex:426: undefined function dbg/1 (expected ElixirSense.Providers.Docs to define such a function or for it to be imported, but none are available)

Check failure on line 426 in lib/elixir_sense/providers/docs.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 25.x)

** (CompileError) lib/elixir_sense/providers/docs.ex:426: undefined function dbg/1 (expected ElixirSense.Providers.Docs to define such a function or for it to be imported, but none are available)

Check failure on line 426 in lib/elixir_sense/providers/docs.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 25.x)

** (CompileError) lib/elixir_sense/providers/docs.ex:426: undefined function dbg/1 (expected ElixirSense.Providers.Docs to define such a function or for it to be imported, but none are available)

Check failure on line 426 in lib/elixir_sense/providers/docs.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 22.x)

** (CompileError) lib/elixir_sense/providers/docs.ex:426: undefined function dbg/1 (expected ElixirSense.Providers.Docs to define such a function or for it to be imported, but none are available)

Check failure on line 426 in lib/elixir_sense/providers/docs.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 24.x)

** (CompileError) lib/elixir_sense/providers/docs.ex:426: undefined function dbg/1 (expected ElixirSense.Providers.Docs to define such a function or for it to be imported, but none are available)

Check failure on line 426 in lib/elixir_sense/providers/docs.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 23.x)

** (CompileError) lib/elixir_sense/providers/docs.ex:426: undefined function dbg/1 (expected ElixirSense.Providers.Docs to define such a function or for it to be imported, but none are available)
args = BuiltinFunctions.get_args({f, a})
docs = BuiltinFunctions.get_docs({f, a})

metadata = %{builtin: true}

Expand All @@ -432,8 +437,7 @@ defmodule ElixirSense.Providers.Docs do
args: args,
metadata: metadata,
specs: spec,
# TODO provide docs
docs: ""
docs: docs
}
end
end
Expand Down
5 changes: 3 additions & 2 deletions lib/elixir_sense/providers/suggestion/complete.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
12 changes: 6 additions & 6 deletions test/elixir_sense/docs_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions test/elixir_sense/providers/suggestion/complete_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -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")

Expand Down
6 changes: 3 additions & 3 deletions test/elixir_sense/providers/suggestion_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
Loading

0 comments on commit 4ff3d09

Please sign in to comment.