Current section
Files
Jump to
Current section
Files
lib/beam_file.ex
defmodule BeamFile do
@moduledoc """
An interface to the BEAM file format.
This module is mainly a wrapper around Erlangs `:beam_lib`.
Furthermore, different code representations can be generated from the file.
- `BeamFile.abstract_code/1`
- `BeamFile.byte_code/1`
- `BeamFile.erl_code/1`
- `BeamFile.elixir_code/2`
"""
@type info :: [
file: Path.t(),
module: module(),
chunks: [{charlist(), non_neg_integer(), non_neg_integer()}]
]
@type chunk_name :: charlist()
@type chunk_id ::
:abstract_code
| :atoms
| :attributes
| :compile_info
| :debug_info
| :docs
| :elixir_checker
| :exports
| :imports
| :indexed_imports
| :labeled_exports
| :labeled_locals
| :locals
@type chunk :: chunk_id | chunk_name
@default_lang "en"
@blank " "
@new_line "\n"
@new_paragraph "\n\n"
@doc_ -2
# @spec_ -1
@fun 0
@args 1
@super 2
@guards 3
@block 4
@chunk_names [
:abstract_code,
# 'Atom'
:atoms,
# 'Attr'
:attributes,
# 'CInf'
:compile_info,
# 'Dbgi'
:debug_info,
# 'ExpT'
:exports,
# 'ImpT'
:imports,
# 'ImpT'
:indexed_imports,
# 'ExpT'
:labeled_exports,
# 'LocT'
:labeled_locals,
# 'LocT'
:locals
]
@extra_chunk_names %{
docs: 'Docs',
elixir_checker: 'ExCk'
}
@extra_chunk_keys Map.keys(@extra_chunk_names)
@extra_chunk_values Map.values(@extra_chunk_names)
@doc """
Returns the absolute filename for the `module`.
If the module cannot be found, `{:error, :non_existing}` is returned.
If the module is preloaded, `{:error, :preloaded}` is returned.
If the module is Cover-compiled, `{:error, :cover_compiled}` is returned.
"""
@spec which(module()) ::
{:ok, String.t()} | {:error, :non_existing | :preloaded | :cover_compiled}
def which(module) do
case :code.which(module) do
[_ | _] = path -> {:ok, IO.chardata_to_string(path)}
error -> {:error, error}
end
end
@doc """
Returns a keyword list containing some information about a BEAM file.
- `:file`: The name of the BEAM file, or the binary from which the information
was extracted.
- `:module`: The name of the module.
- `:chunks`: For each chunk, the identifier and the position and size of the
chunk data, in bytes.
"""
@spec info(Path.t() | module()) :: info()
def info(input) do
with {:ok, path} <- path(input) do
path
|> :beam_lib.info()
|> Keyword.update!(:file, fn path -> IO.chardata_to_string(path) end)
end
end
@doc """
Returns infos for the given chunk reference.
"""
@spec chunk(Path.t() | module(), chunk()) :: {:ok, term()} | {:error, any()}
def chunk(input, chunk) do
with {:ok, path} <- path(input),
{:ok, chunk} <- to_chunk(chunk),
{:ok, {_module, [{_chunk, data}]}} <- fetch_chunks(path, [chunk]) do
data =
case chunk in @extra_chunk_values do
true -> :erlang.binary_to_term(data)
false -> data
end
{:ok, data}
else
{:error, :beam_lib, reason} -> {:error, reason}
{:error, _} = error -> error
end
end
@doc """
Returns chunk data for all chunks.
"""
@spec chunks(Path.t() | module()) :: {:ok, map()} | {:error, any()}
def chunks(input) do
with {:ok, path} <- path(input),
{:ok, _module, chunks} <- :beam_lib.all_chunks(path) do
{:ok, Enum.into(chunks, %{})}
end
end
@doc """
Returns chunk data for all chunk names.
"""
def chunks(input, :named) do
with {:ok, path} <- path(input) do
@chunk_names
|> Enum.concat(@extra_chunk_keys)
|> Enum.into(%{}, fn name ->
case chunk(path, name) do
{:ok, chunk} -> {name, chunk}
error -> {name, error}
end
end)
end
end
@doc """
Returns the Erlang code for the BEAM file.
"""
@spec erl_code(Path.t() | module()) :: {:ok, String.t()} | {:error, any()}
def erl_code(input) do
with {:ok, abstract_code} <- abstract_code(input) do
code =
abstract_code
|> :erl_syntax.form_list()
|> :erl_prettypr.format()
|> to_string()
{:ok, code}
end
end
@doc """
Returns the byte code for the BEAM file.
"""
@spec byte_code(Path.t() | module()) :: {:ok, String.t()} | {:error, any()}
def byte_code(input) do
with {:ok, data} <- read(input) do
{:ok, :beam_disasm.file(data)}
end
end
@doc """
Returns the binary for the given BEAM file.
"""
@spec read(Path.t() | module()) :: {:ok, binary()} | {:error, File.posix()}
def read(input) do
with {:ok, path} <- path(input) do
File.read(path)
end
end
@doc """
Returns elixir code recreated from the `debug_info` chunk.
The recreated code comes with resolved macros and references.
For now, types and specs will not be recreated.
Options:
`:docs`: With `docs: false` the docs will not be created.
"""
@spec elixir_code(Path.t() | module(), keyword()) :: {:ok, String.t()} | {:error, any}
def elixir_code(input, opts \\ []) do
with {:ok, {debug_info, _meta}} <- debug_info(input) do
code =
debug_info
|> definitions()
|> docs(input, opts)
|> to_code(debug_info)
{:ok, code}
end
end
@doc """
Returns the `Dbgi`/`:debug_info` chunk.
"""
@spec debug_info(module()) :: {:ok, term()} | {:error, any}
def debug_info(module) when is_atom(module) do
case chunk(module, :debug_info) do
{:ok, {:debug_info_v1, _backend, {:elixir_v1, debug_info, data}}} ->
{:ok, {debug_info, data}}
{:ok, result} ->
{:error, {:unexpected, result}}
error ->
error
end
end
@doc """
Returns the `:abstract_code` chunk.
"""
@spec abstract_code(Path.t() | module()) :: {:ok, term()} | {:error, any}
def abstract_code(input) do
case chunk(input, :abstract_code) do
{:ok, {:raw_abstract_v1, abstract_code}} ->
{:ok, abstract_code}
{:ok, result} ->
{:error, {:unexpected, result}}
error ->
error
end
end
@doc """
Returns the `:docs` chunk.
"""
@spec docs(Path.t() | module()) :: {:ok, term()} | {:error, any}
def docs(input) do
case chunk(input, :docs) do
{:ok, {:docs_v1, _, :elixir, "text/markdown", doc, meta, docs}} ->
{:ok, {doc, meta, docs}}
{:ok, result} ->
{:error, {:unexpected, result}}
error ->
error
end
end
defp fetch_chunks(path, chunks), do: :beam_lib.chunks(path, chunks, [:allow_missing_chunks])
defp to_chunk(chunk) when chunk in @extra_chunk_keys do
{:ok, Map.fetch!(@extra_chunk_names, chunk)}
end
defp to_chunk(chunk) when chunk in @chunk_names, do: {:ok, chunk}
defp to_chunk(chunk) when is_binary(chunk), do: {:ok, to_charlist(chunk)}
defp to_chunk(chunk) when is_list(chunk), do: {:ok, chunk}
defp to_chunk(_), do: {:error, :invalid_chunk}
defp path(path) when is_binary(path) do
{:ok, to_charlist(path)}
end
defp path(module) when is_atom(module) do
case :code.which(module) do
[_ | _] = path ->
{:ok, path}
[] ->
{:error, :beam_file_not_found}
error ->
{:error, error}
end
end
defp path(path) when is_list(path), do: {:ok, path}
defp path(_), do: {:error, :not_found}
defp definitions(%{definitions: definitions}) do
definitions
|> Enum.with_index()
|> Enum.flat_map(fn {{{name, _arity}, kind, _meta, clauses}, def_index} ->
clauses
|> Enum.with_index()
|> Enum.flat_map(fn {{meta, args, guards, block}, clause_index} ->
index = def_index + clause_index / (length(clauses) + 1)
meta =
meta
|> Keyword.put(:name, name)
|> Keyword.put(:kind, kind)
[
fun(meta, index),
args(args, meta, index),
guards(guards, meta, index),
block(block, meta, index)
]
end)
end)
end
defp fun(meta, index) do
line = line(meta)
kind = meta[:kind]
name = meta[:name]
code = [to_string(kind), @blank, to_string(name)]
{code, {line, index, @fun}}
end
defp args(args, meta, index) do
line = line(meta)
code =
args
|> Enum.map(&code_to_string/1)
|> case do
[] -> @blank
code -> ["(", Enum.join(code, ", "), ")", @blank]
end
{code, {line, index, @args}}
end
defp guards([], _, _), do: :none
defp guards(guards, meta, index) do
line = line(meta)
code = Enum.map(guards, &code_to_string/1)
{[@blank, "when ", code, @blank], {line, index, @guards}}
end
defp block({:__block__, [], block}, meta, index), do: do_block(block, meta, index)
defp block({:super, context, args}, meta, index) do
line = line(meta)
code = code_to_string({meta[:name], context, args})
code = ["do", @new_line, code, @new_line, "end", @new_paragraph]
{code, {line, index, @super}}
end
defp block(block, meta, index) do
line = line(meta)
code = ["do", @new_line, code_to_string(block), @new_line, "end", @new_paragraph]
{code, {line, index, @block}}
end
defp do_block(block, meta, index) when is_list(block) do
line = line(meta)
code =
block
|> Enum.map(&code_to_string/1)
|> Enum.join(@new_line)
code = ["do", @new_line, code, @new_line, "end", @new_paragraph]
{code, {line, index, @block}}
end
defp docs(code, module, opts) do
with {:docs, true} <- {:docs, Keyword.get(opts, :docs, true)},
{:ok, docs} <- docs(module) do
Enum.concat([
code,
moduledoc(docs),
fundocs(docs)
])
else
_ -> code
end
end
defp fundocs({_doc, _meta, []}), do: [:none]
defp fundocs({_doc, _meta, docs}) do
Enum.map(docs, fn
{_fun, _line, _code, :none, _meta} ->
:none
{_fun, _line, _code, :hidden, _meta} ->
:none
{_fun, line, _code, doc, _meta} ->
case Map.fetch(doc, @default_lang) do
:error ->
:none
{:ok, doc} ->
doc = """
@doc \"""
#{doc}
\"""
"""
{doc, {line, 0, @doc_}}
end
end)
end
defp moduledoc({doc, _, _}) when doc in [:none, :hidden], do: [:none]
defp moduledoc({doc, _, _}) do
case Map.fetch(doc, @default_lang) do
{:ok, str} ->
str = """
\"""
#{str}
\"""
"""
code = ["@moduledoc", @blank, str, @new_paragraph]
{code, {0, 0, @doc_}}
:error ->
:none
end
|> List.wrap()
end
defp line(meta), do: Keyword.get(meta, :line, 0)
defp code_to_string(code), do: Macro.to_string(code)
defp to_code(data, debug_info) do
module = Map.get(debug_info, :module) |> to_string()
code =
data
|> Enum.filter(fn
:none -> false
_ -> true
end)
|> sort()
|> strip()
|> IO.iodata_to_binary()
"""
defmodule #{module} do
#{code}
end
"""
|> Code.format_string!()
|> IO.iodata_to_binary()
end
defp sort(data) do
Enum.sort(data, fn
{_, {line, index, ord_a}}, {_, {line, index, ord_b}} -> ord_a <= ord_b
{_, {line, index_a, _}}, {_, {line, index_b, _}} -> index_a <= index_b
{_, {line_a, _, _}}, {_, {line_b, _, _}} -> line_a <= line_b
end)
end
defp strip(data), do: Enum.map(data, fn {iodata, _} -> iodata end)
end