defmodule RpcElixir.Codegen.SourceLinks do
@moduledoc false
# Composes a procedure's doc text with a clickable link to the handler source,
# resolved by reading the handler's BEAM abstract code for the function's line number.
import RpcElixir.Codegen.Shared, only: [sanitize_doc: 1]
def handler_jsdoc(proc, indent) do
doc_text =
if proc[:doc] && proc[:doc] != "", do: sanitize_doc(String.trim(proc[:doc])), else: nil
link_comment =
with {:ok, source_file} <- handler_source_file(proc.handler_mod),
{:ok, line} <- handler_function_line(proc.handler_mod, proc.handler_fun, 2) do
display = "#{Path.basename(source_file)}:#{line}"
mfa = "#{inspect(proc.handler_mod)}.#{proc.handler_fun}/2"
"[#{display}](#{source_file}#L#{line}) — `#{mfa}`"
else
_ -> nil
end
case {doc_text, link_comment} do
{nil, nil} ->
""
{nil, link} ->
"#{indent}/** #{link} */\n"
{doc, nil} ->
"#{indent}/** #{doc} */\n"
{doc, link} ->
doc_lines =
doc
|> String.split("\n")
|> Enum.map_join("\n", &"#{indent} * #{&1}")
"#{indent}/**\n#{doc_lines}\n#{indent} *\n#{indent} * #{link}\n#{indent} */\n"
end
end
defp handler_source_file(module) do
case module.module_info(:compile)[:source] do
nil ->
:error
source ->
abs = List.to_string(source)
{:ok, source_link_path(abs)}
end
rescue
_ -> :error
end
# For real generation (an output path is known) we emit an absolute `file://`
# URI: VS Code's link detector makes those clickable in the editor, whereas a
# relative path is not detected. The generated client is typically gitignored,
# so absolute paths cause no diff churn. Without an output path (e.g. tests
# calling generate/1) we emit a deterministic repo-root-relative path so
# fixtures never contain a machine-specific absolute path.
defp source_link_path(abs_path) do
case Process.get(:__rpc_out_dir__) do
nil -> relative_to_root(abs_path)
_out_dir -> "file://" <> Path.expand(abs_path)
end
end
defp relative_to_root(abs_path) do
root = project_root()
if String.starts_with?(abs_path, root <> "/") do
String.slice(abs_path, String.length(root) + 1, String.length(abs_path))
else
abs_path
end
end
defp project_root do
case System.cmd("git", ["rev-parse", "--show-toplevel"], stderr_to_stdout: true) do
{path, 0} -> String.trim(path)
_ -> File.cwd!()
end
end
defp handler_function_line(module, fun, arity) do
beam = :code.which(module)
if beam == :non_existing do
:error
else
case :beam_lib.chunks(beam, [:abstract_code]) do
{:ok, {_, [{:abstract_code, {:raw_abstract_v1, forms}}]}} ->
find_function_line(forms, fun, arity)
{:ok, {_, [{:abstract_code, :no_abstract_code}]}} ->
track_missing_abstract_code(module)
:error
_ ->
:error
end
end
rescue
_ -> :error
end
defp find_function_line(forms, fun, arity) do
Enum.find_value(forms, :error, fn
{:function, loc, ^fun, ^arity, _} ->
case loc do
{line, _} when is_integer(line) -> {:ok, line}
line when is_integer(line) -> {:ok, line}
_ -> nil
end
_ ->
nil
end)
end
defp track_missing_abstract_code(module) do
current = Process.get(:__rpc_missing_abstract_code__, [])
unless module in current do
Process.put(:__rpc_missing_abstract_code__, [module | current])
end
end
end