Skip to content

Commit

Permalink
Clean up Lexical.RemoteControl.CodeIntelligence.Docs
Browse files Browse the repository at this point in the history
This moves all of the fetching of docs, specs, types, and callbacks into
`Lexical.RemoteControl.Modules`. Those functions now also operate on
BEAM object code, which means the object code doesn't need to be
repeatedly read from disk. The object code for a module also appears to
be available immediately after calling `Code.ensure_compiled(module)`,
which allows us to get rid of a timeout/loop that was waiting for it to
be written to disk.
  • Loading branch information
zachallaun committed Sep 5, 2023
1 parent f3f2d08 commit d1edb31
Show file tree
Hide file tree
Showing 2 changed files with 150 additions and 98 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ defmodule Lexical.RemoteControl.CodeIntelligence.Docs do
"""

alias Lexical.RemoteControl.CodeIntelligence.Docs.Entry
alias Lexical.RemoteControl.Modules

defstruct [:module, :doc, functions_and_macros: [], callbacks: [], types: []]

Expand All @@ -20,30 +21,32 @@ defmodule Lexical.RemoteControl.CodeIntelligence.Docs do
"""
@spec for_module(module()) :: {:ok, t} | {:error, any()}
def for_module(module) when is_atom(module) do
with :ok <- ensure_ready(module),
{:docs_v1, _anno, _lang, _fmt, module_doc, _meta, docs} <- Code.fetch_docs(module) do
{:ok, parse_docs(module, module_doc, docs)}
with {:ok, beam} <- Modules.ensure_beam(module) do
{:ok, parse_docs(module, beam)}
end
end

defp parse_docs(module, module_doc, entries) do
entries_by_kind = Enum.group_by(entries, &doc_kind/1)
function_entries = Map.get(entries_by_kind, :function, [])
macro_entries = Map.get(entries_by_kind, :macro, [])
callback_entries = Map.get(entries_by_kind, :callback, [])
type_entries = Map.get(entries_by_kind, :type, [])

spec_defs = get_spec_defs(module)
callback_defs = get_callback_defs(module)
type_defs = get_type_defs(module)

%__MODULE__{
module: module,
doc: Entry.parse_doc(module_doc),
functions_and_macros: parse_entries(module, function_entries ++ macro_entries, spec_defs),
callbacks: parse_entries(module, callback_entries, callback_defs),
types: parse_entries(module, type_entries, type_defs)
}
defp parse_docs(module, beam) do
with {:ok, {:docs_v1, _anno, _lang, _format, module_doc, _meta, entries}} <-
Modules.fetch_docs(beam) do
entries_by_kind = Enum.group_by(entries, &doc_kind/1)
function_entries = Map.get(entries_by_kind, :function, [])
macro_entries = Map.get(entries_by_kind, :macro, [])
callback_entries = Map.get(entries_by_kind, :callback, [])
type_entries = Map.get(entries_by_kind, :type, [])

spec_defs = beam |> Modules.fetch_specs() |> ok_or([])
callback_defs = beam |> Modules.fetch_callbacks() |> ok_or([])
type_defs = beam |> Modules.fetch_types() |> ok_or([])

%__MODULE__{
module: module,
doc: Entry.parse_doc(module_doc),
functions_and_macros: parse_entries(module, function_entries ++ macro_entries, spec_defs),
callbacks: parse_entries(module, callback_entries, callback_defs),
types: parse_entries(module, type_entries, type_defs)
}
end
end

defp doc_kind({{kind, _name, _arity}, _anno, _sig, _doc, _meta}) do
Expand All @@ -54,8 +57,8 @@ defmodule Lexical.RemoteControl.CodeIntelligence.Docs do
defs_by_name_arity =
Enum.group_by(
defs,
fn {name, arity, _formatted} -> {name, arity} end,
fn {_name, _arity, formatted} -> formatted end
fn {name, arity, _formatted, _quoted} -> {name, arity} end,
fn {_name, _arity, formatted, _quoted} -> formatted end
)

raw_entries
Expand All @@ -67,79 +70,6 @@ defmodule Lexical.RemoteControl.CodeIntelligence.Docs do
|> Enum.group_by(& &1.name)
end

defp ensure_ready(module) do
with {:module, _} <- Code.ensure_compiled(module),
path when is_list(path) and path != [] <- :code.which(module) do
ensure_file_exists(path)
else
_ -> {:error, :not_found}
end
end

@timeout 10
defp ensure_file_exists(path, attempts \\ 10)

defp ensure_file_exists(_, 0) do
{:error, :beam_file_timeout}
end

defp ensure_file_exists(path, attempts) do
if File.exists?(path) do
:ok
else
Process.sleep(@timeout)
ensure_file_exists(path, attempts - 1)
end
end

defp get_spec_defs(module) do
case Code.Typespec.fetch_specs(module) do
{:ok, specs} ->
for {{name, arity}, defs} <- specs,
def <- defs do
formatted = name |> Code.Typespec.spec_to_quoted(def) |> format_def()
{name, arity, formatted}
end

_ ->
[]
end
end

defp get_callback_defs(module) do
case Code.Typespec.fetch_callbacks(module) do
{:ok, callbacks} ->
for {{name, arity}, defs} <- callbacks,
def <- defs do
formatted = name |> Code.Typespec.spec_to_quoted(def) |> format_def()
{name, arity, formatted}
end

_ ->
[]
end
end

defp get_type_defs(module) do
case Code.Typespec.fetch_types(module) do
{:ok, types} ->
for {kind, {name, _body, args} = type} <- types do
arity = length(args)
quoted_type = Code.Typespec.type_to_quoted(type)
quoted = {:@, [], [{kind, [], [quoted_type]}]}

{name, arity, format_def(quoted)}
end

_ ->
[]
end
end

defp format_def(quoted) do
quoted
|> Future.Code.quoted_to_algebra()
|> Inspect.Algebra.format(60)
|> IO.iodata_to_binary()
end
defp ok_or({:ok, value}, _default), do: value
defp ok_or(_, default), do: default
end
122 changes: 122 additions & 0 deletions apps/remote_control/lib/lexical/remote_control/modules.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ defmodule Lexical.RemoteControl.Modules do
@moduledoc """
Utilities for dealing with modules on the remote control node
"""

defmodule Predicate.Syntax do
@moduledoc """
Syntax helpers for the predicate syntax
Expand Down Expand Up @@ -47,8 +48,129 @@ defmodule Lexical.RemoteControl.Modules do
end
end

@typedoc "Module documentation record as defined by EEP-48"
@type docs_v1 :: tuple()

@typedoc "A type, spec, or callback definition"
@type definition ::
{name :: atom(), arity :: arity(), formatted :: String.t(), quoted :: Macro.t()}

@cache_timeout Application.compile_env(:remote_control, :modules_cache_expiry, {10, :second})

@doc """
Ensure the given module is compiled, returning the BEAM object code if successful.
"""
@spec ensure_beam(module()) ::
{:ok, beam :: binary()} | {:error, reason}
when reason:
:embedded
| :badfile
| :nofile
| :on_load_failure
| :unavailable
| :get_object_code_failed
def ensure_beam(module) when is_atom(module) do
with {:module, _} <- Code.ensure_compiled(module),
{_module, beam, _filename} <- :code.get_object_code(module) do
{:ok, beam}
else
:error -> {:error, :get_object_code_failed}
{:error, error} -> {:error, error}
end
end

@doc """
Fetch the docs chunk from BEAM object code.
"""
@spec fetch_docs(beam :: binary()) :: {:ok, docs_v1()} | :error
@docs_chunk ~c"Docs"
def fetch_docs(beam) when is_binary(beam) do
case :beam_lib.chunks(beam, [@docs_chunk]) do
{:ok, {_module, [{@docs_chunk, bin}]}} ->
{:ok, :erlang.binary_to_term(bin)}

_ ->
:error
end
end

@doc """
Fetch the specs from BEAM object code.
"""
@spec fetch_specs(beam :: binary()) :: {:ok, [definition()]} | :error
def fetch_specs(beam) when is_binary(beam) do
case Code.Typespec.fetch_specs(beam) do
{:ok, specs} ->
defs =
for {{name, arity}, defs} <- specs,
def <- defs do
quoted = Code.Typespec.spec_to_quoted(name, def)
formatted = format_definition(quoted)

{name, arity, formatted, quoted}
end

{:ok, defs}

_ ->
:error
end
end

@doc """
Fetch the types from BEAM object code.
"""
@spec fetch_types(beam :: binary()) :: {:ok, [definition()]} | :error
def fetch_types(beam) when is_binary(beam) do
case Code.Typespec.fetch_types(beam) do
{:ok, types} ->
defs =
for {kind, {name, _body, args} = type} <- types do
arity = length(args)
quoted_type = Code.Typespec.type_to_quoted(type)
quoted = {:@, [], [{kind, [], [quoted_type]}]}
formatted = format_definition(quoted)

{name, arity, formatted, quoted}
end

{:ok, defs}

_ ->
:error
end
end

@doc """
Fetch the specs from BEAM object code.
"""
@spec fetch_callbacks(beam :: binary()) :: {:ok, [definition()]} | :error
def fetch_callbacks(beam) when is_binary(beam) do
case Code.Typespec.fetch_callbacks(beam) do
{:ok, callbacks} ->
defs =
for {{name, arity}, defs} <- callbacks,
def <- defs do
quoted = Code.Typespec.type_to_quoted(def)
formatted = format_definition(quoted)

{name, arity, formatted, quoted}
end

{:ok, defs}

_ ->
:error
end
end

defp format_definition(quoted) do
quoted
|> Future.Code.quoted_to_algebra()
|> Inspect.Algebra.format(60)
|> IO.iodata_to_binary()
end

@doc """
Returns all modules matching a prefix
Expand Down

0 comments on commit d1edb31

Please sign in to comment.