Skip to content

Commit

Permalink
Add documentation retrieval for module's built-in attributes (#233)
Browse files Browse the repository at this point in the history
Closes #227

Co-authored-by: Łukasz Samson <[email protected]>
  • Loading branch information
Goose97 and lukaszsamson committed Jul 7, 2023
1 parent 9e56e55 commit 8a55365
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 63 deletions.
33 changes: 32 additions & 1 deletion lib/elixir_sense/core/builtin_attributes.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ defmodule ElixirSense.Core.BuiltinAttributes do
impl
derive
enforce_keys
struct
compile
deprecated
dialyzer
Expand All @@ -28,7 +27,39 @@ defmodule ElixirSense.Core.BuiltinAttributes do
typedoc
doc
moduledoc
for
protocol
)a

def all, do: @list

def docs(attribute) when attribute in @list do
case Module.reserved_attributes() do
%{^attribute => %{doc: doc}} ->
doc

_ ->
# Older Elixir versions haven't document these attributes
# Reference: https://github.com/elixir-lang/elixir/commit/14c55d15afbf08b0d8289a4399a15b4109b6ac5a
case attribute do
:enforce_keys ->
"Ensures the given keys are always set when building the struct defined in the current module."

:fallback_to_any ->
"If set to `true` generates a default protocol implementation " <>
"for all types (inside `defprotocol`)."

:for ->
"The current module/type a protocol implementation is being defined for (inside `defimpl`)."

:protocol ->
"The current protocol being implemented (inside `defimpl`)."

_ ->
nil
end
end
end

def docs(_), do: nil
end
32 changes: 19 additions & 13 deletions lib/elixir_sense/providers/docs.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ defmodule ElixirSense.Providers.Docs do
Doc Provider
"""
alias ElixirSense.Core.Binding
alias ElixirSense.Core.BuiltinAttributes
alias ElixirSense.Core.Introspection
alias ElixirSense.Core.State
alias ElixirSense.Core.SurroundContext
Expand Down Expand Up @@ -31,17 +32,24 @@ defmodule ElixirSense.Providers.Docs do

type = SurroundContext.to_binding(context, module)

mod_fun_docs(
type,
binding_env,
imports,
requires,
aliases,
module,
mods_funs,
metadata_types,
scope
)
case type do
{:attribute, attribute} ->
docs = BuiltinAttributes.docs(attribute)
if docs, do: {"@" <> Atom.to_string(attribute), %{docs: docs}}

_ ->
mod_fun_docs(
type,
binding_env,
imports,
requires,
aliases,
module,
mods_funs,
metadata_types,
scope
)
end
end

defp mod_fun_docs(
Expand Down Expand Up @@ -72,8 +80,6 @@ defmodule ElixirSense.Providers.Docs do
end
end

# TODO attr

defp mod_fun_docs(
{mod, fun},
binding_env,
Expand Down
14 changes: 10 additions & 4 deletions lib/elixir_sense/providers/suggestion/complete.ex
Original file line number Diff line number Diff line change
Expand Up @@ -412,10 +412,16 @@ defmodule ElixirSense.Providers.Suggestion.Complete do
attribute_name when is_atom(attribute_name) <- attribute_names,
name = Atom.to_string(attribute_name),
Matcher.match?(name, hint),
do: name
do: attribute_name
)
|> Enum.sort()
|> Enum.map(&%{kind: :attribute, name: &1})
|> Enum.map(
&%{
kind: :attribute,
name: Atom.to_string(&1),
summary: BuiltinAttributes.docs(&1)
}
)
|> format_expansion()
end

Expand Down Expand Up @@ -1042,8 +1048,8 @@ defmodule ElixirSense.Providers.Suggestion.Complete do
[%{type: :variable, name: name}]
end

defp to_entries(%{kind: :attribute, name: name}) do
[%{type: :attribute, name: "@" <> name}]
defp to_entries(%{kind: :attribute, name: name, summary: summary}) do
[%{type: :attribute, name: "@" <> name, summary: summary}]
end

defp to_entries(%{
Expand Down
3 changes: 2 additions & 1 deletion lib/elixir_sense/providers/suggestion/reducers/common.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ defmodule ElixirSense.Providers.Suggestion.Reducers.Common do

@type attribute :: %{
type: :attribute,
name: String.t()
name: String.t(),
summary: String.t()
}

@type variable :: %{
Expand Down
25 changes: 25 additions & 0 deletions test/elixir_sense/docs_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1016,4 +1016,29 @@ defmodule ElixirSense.DocsTest do
Flattens the given `list` of nested lists.
"""
end

test "retrieve reserved module attributes documentation" do
buffer = """
defmodule MyModule do
@on_load :on_load
def on_load(), do: :ok
end
"""

assert %{
actual_subject: "@on_load",
docs: %{docs: "A hook that will be invoked whenever the module is loaded."}
} = ElixirSense.docs(buffer, 2, 6)
end

test "retrieve unreserved module attributes documentation" do
buffer = """
defmodule MyModule do
@my_attribute nil
end
"""

refute ElixirSense.docs(buffer, 2, 6)
end
end
21 changes: 15 additions & 6 deletions test/elixir_sense/providers/suggestion/complete_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1036,16 +1036,19 @@ defmodule ElixirSense.Providers.Suggestion.CompleteTest do
scope: {:some, 0}
}

assert expand(~c"@numb", env) == [%{type: :attribute, name: "@number"}]
assert expand(~c"@numb", env) == [%{type: :attribute, name: "@number", summary: nil}]

assert expand(~c"@num", env) ==
[%{type: :attribute, name: "@number"}, %{type: :attribute, name: "@numeral"}]
[
%{type: :attribute, name: "@number", summary: nil},
%{type: :attribute, name: "@numeral", summary: nil}
]

assert expand(~c"@", env) ==
[
%{name: "@nothing", type: :attribute},
%{type: :attribute, name: "@number"},
%{type: :attribute, name: "@numeral"}
%{name: "@nothing", type: :attribute, summary: nil},
%{type: :attribute, name: "@number", summary: nil},
%{type: :attribute, name: "@numeral", summary: nil}
]
end

Expand All @@ -1069,7 +1072,13 @@ defmodule ElixirSense.Providers.Suggestion.CompleteTest do
assert expand(~c"@befo", env_outside_module) == []

assert expand(~c"@befo", env_module) ==
[%{type: :attribute, name: "@before_compile"}]
[
%{
type: :attribute,
name: "@before_compile",
summary: "A hook that will be invoked before the module is compiled."
}
]
end

test "kernel special form completion" do
Expand Down
115 changes: 77 additions & 38 deletions test/elixir_sense/suggestions_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1513,53 +1513,92 @@ defmodule ElixirSense.SuggestionsTest do
] = ElixirSense.suggestions(buffer, 5, 9)
end

test "lists attributes" do
buffer = """
defmodule MyModule do
@my_attribute1 true
@my_attribute2 false
@
end
"""
describe "suggestions for module attributes" do
test "lists attributes" do
buffer = """
defmodule MyModule do
@my_attribute1 true
@my_attribute2 false
@
end
"""

list =
ElixirSense.suggestions(buffer, 4, 4)
|> Enum.filter(fn s -> s.type == :attribute and s.name |> String.starts_with?("@my") end)
list =
ElixirSense.suggestions(buffer, 4, 4)
|> Enum.filter(fn s -> s.type == :attribute and s.name |> String.starts_with?("@my") end)
|> Enum.map(fn %{name: name} -> name end)

assert list == [
%{name: "@my_attribute1", type: :attribute},
%{name: "@my_attribute2", type: :attribute}
]
end
assert list == ["@my_attribute1", "@my_attribute2"]
end

test "lists module attributes in module scope" do
buffer = """
defmodule MyModule do
@myattr "asd"
@moduledoc "asdf"
def some do
@m
test "lists module attributes in module scope" do
buffer = """
defmodule MyModule do
@myattr "asd"
@moduledoc "asdf"
def some do
@m
end
end
"""

list =
ElixirSense.suggestions(buffer, 2, 5)
|> Enum.filter(fn s -> s.type == :attribute end)
|> Enum.map(fn %{name: name} -> name end)

assert list == ["@macrocallback", "@moduledoc", "@myattr"]

list =
ElixirSense.suggestions(buffer, 5, 7)
|> Enum.filter(fn s -> s.type == :attribute end)
|> Enum.map(fn %{name: name} -> name end)

assert list == ["@myattr"]
end
"""

list =
ElixirSense.suggestions(buffer, 2, 5)
|> Enum.filter(fn s -> s.type == :attribute end)
test "built-in attributes should include documentation" do
buffer = """
defmodule MyModule do
@call
@enfor
end
"""

assert list == [
%{name: "@macrocallback", type: :attribute},
%{name: "@moduledoc", type: :attribute},
%{name: "@myattr", type: :attribute}
]
list =
ElixirSense.suggestions(buffer, 2, 7)
|> Enum.filter(fn s -> s.type == :attribute end)

list =
ElixirSense.suggestions(buffer, 5, 7)
|> Enum.filter(fn s -> s.type == :attribute end)
assert [%{summary: "Provides a specification for a behaviour callback."}] = list

assert list == [
%{name: "@myattr", type: :attribute}
]
list =
ElixirSense.suggestions(buffer, 3, 8)
|> Enum.filter(fn s -> s.type == :attribute end)

assert [
%{
summary:
"Ensures the given keys are always set when building the struct defined in the current module."
}
] = list
end

test "non built-in attributes should not include documentation" do
buffer = """
defmodule MyModule do
@myattr "asd"
def some do
@m
end
end
"""

list =
ElixirSense.suggestions(buffer, 4, 6)
|> Enum.filter(fn s -> s.type == :attribute end)

assert [%{summary: nil}] = list
end
end

test "lists builtin module attributes on incomplete code" do
Expand Down

0 comments on commit 8a55365

Please sign in to comment.