Current section

Files

Jump to
docsh src docsh_lib.erl
Raw

src/docsh_lib.erl

-module(docsh_lib).
-export([beam_diff/2,
convert/3,
debug/3,
format_error/1,
get/2, get/3,
get_abstract_code/1,
get_docs/1,
get_source_file/1,
group_by/2,
has_docs/1,
is_module_available/1,
make_docs/1,
print/2, print/3,
stick_module/1,
unstick_module/1]).
-export([compile_info_source_file/1,
guessed_source_file/1]).
-export_type([compiled_module/0]).
-type k() :: any().
-type v() :: any().
%% A `compiled_module()' is a subtype of `beam_lib:beam()' representing the in-memory
%% assembled chunks of a module.
%% You can get one e.g. by reading a .beam file directly or by calling `beam_lib:build_module/1'.
-type compiled_module() :: binary().
-spec convert(Readers, Writer, Beam) -> docsh:external() when
Readers :: [module()],
Writer :: module(),
Beam :: docsh_beam:t().
convert(Readers, Writer, Beam) ->
InternalDocs = lists:flatmap(fun convert_one/1, [ {R, Beam} || R <- Readers ]),
Merged = docsh_internal:merge(InternalDocs),
Writer:from_internal(Merged).
convert_one({Reader, Mod}) ->
case Reader:to_internal(Mod) of
{error, _R, _Stacktrace} = Error ->
%% TODO: this is a *_lib module - shouldn't we bubble it up?
print(standard_error, "~s\n", [format_error(Error)]),
[];
{ok, InternalDoc} ->
[InternalDoc]
end.
%% Error if Key is not found!
-spec get(k(), [{k(), v()}]) -> v().
get(Key, Props) ->
case lists:keyfind(Key, 1, Props) of
false -> erlang:error(not_found, [Key, Props]);
{Key, Val} -> Val
end.
-spec get(k(), [{k(), v()}], v()) -> v().
get(Key, Props, Default) ->
case lists:keyfind(Key, 1, Props) of
false -> Default;
{Key, Val} -> Val
end.
-spec print(io:format(), [term()]) -> ok.
print(Fmt, Args) ->
print(standard_io, Fmt, Args).
-spec print(io:device(), io:format(), [term()]) -> ok.
print(Handle, Fmt, Args) ->
io:format(Handle, Fmt, Args).
-spec debug(atom(), io:format(), [term()] | fun(() -> [term()])) -> ok.
debug(Tag, Fmt, Args) ->
case os:getenv("DOCSH_DEBUG") of
false -> ok;
Tags -> debug_matching(string:tokens(Tags, ","),
atom_to_list(Tag), Fmt, Args)
end.
debug_matching(Tags, Tag, Fmt, Args) ->
case lists:member(Tag, Tags) of
false -> ok;
true -> print(Fmt, if
is_function(Args, 0) -> Args();
is_list(Args) -> Args
end)
end.
%% @doc Show the difference between chunk sets of two modules.
-spec beam_diff(BEAM, BEAM) -> [{BEAM, module(), list()}] when
BEAM :: beam_lib:beam().
beam_diff(BEAM1, BEAM2) ->
{ok, Name1, Chunks1} = beam_lib:all_chunks(BEAM1),
{ok, Name2, Chunks2} = beam_lib:all_chunks(BEAM2),
Keys1 = [ K || {K, _} <- Chunks1 ],
Keys2 = [ K || {K, _} <- Chunks2 ],
[{BEAM1, Name1, Keys1 -- Keys2},
{BEAM2, Name2, Keys2 -- Keys1}].
-spec has_docs(beam_lib:beam()) -> boolean().
has_docs(BEAMFile) ->
{ok, _, Chunks} = beam_lib:all_chunks(BEAMFile),
case catch ([ throw(true) || {"Docs", _} <- Chunks ]) of
true -> true;
_ -> false
end.
-spec get_abstract_code(file:filename()) -> docsh_beam:debug_info() | false.
get_abstract_code(BEAMFile) ->
case beam_lib:chunks(BEAMFile, [debug_info]) of
{ok, {_Module, [{debug_info, DbgiV1}]}} ->
{ok, debug_info_v1(DbgiV1)};
_ ->
case beam_lib:chunks(BEAMFile, [abstract_code]) of
{ok, {_Module, [{abstract_code, RawAbstV1}]}} when is_tuple(RawAbstV1) ->
{ok, raw_abstract_v1(RawAbstV1)};
_ ->
false
end
end.
raw_abstract_v1({raw_abstract_v1, Forms}) ->
Forms.
debug_info_v1({debug_info_v1, _, {Forms, _CompileInfo}}) ->
Forms.
-spec get_source_file(file:filename()) -> {ok, file:filename()} | false.
get_source_file(BEAMFile) ->
lists:foldl(fun check_source_file/2,
false,
lists:concat([compile_info_source_file(BEAMFile),
guessed_source_file(BEAMFile)])).
check_source_file(_SourceFile, {ok, File}) ->
{ok, File};
%% This is a special case for Erlang prebuilt .beam files.
%% Calling filelib:is_regular/1 on non-existent files under /net might
%% take ~20 seconds on Mac - we want to avoid it.
check_source_file("/net" ++ _, false) ->
false;
check_source_file(SourceFile, false) ->
case filelib:is_regular(SourceFile) of
true -> {ok, SourceFile};
false -> false
end.
-spec compile_info_source_file(file:filename()) -> [file:filename()].
compile_info_source_file(BEAMFile) ->
case beam_lib:chunks(BEAMFile, ["CInf"]) of
{ok, {_, [{_, CInf}]}} ->
case lists:keyfind(source, 1, erlang:binary_to_term(CInf)) of
{source, File} -> [File];
false -> []
end;
_ ->
[]
end.
-spec guessed_source_file(file:filename() | compiled_module()) -> [file:filename()].
guessed_source_file(CompiledModule) when is_binary(CompiledModule) ->
%% Can't guess source file for an in-memory compiled module,
%% as it might've not been read from any on-disk .beam file.
[];
guessed_source_file(BEAMFile) ->
EbinPrefix = ebin_prefix(BEAMFile),
SrcSuffix = src_suffix(BEAMFile),
[filename:join(EbinPrefix ++ SrcSuffix)].
ebin_prefix(BEAMFile) ->
Components = string:tokens(BEAMFile, "/"),
case BEAMFile of
"/" ++ _ -> ["/"];
_ -> [""]
end ++ lists:takewhile(fun not_ebin/1, Components).
not_ebin("ebin") -> false;
not_ebin(_) -> true.
src_suffix(BEAMFile) ->
[CInfSourceFile] = compile_info_source_file(BEAMFile),
Components = string:tokens(CInfSourceFile, "/"),
lists:dropwhile(fun not_src/1, Components).
not_src("src") -> false;
not_src(_) -> true.
-spec format_error(any()) -> iolist().
format_error({no_debug_info, Mod}) ->
io_lib:format("Abstract code for ~s is not available.\n", [Mod]);
format_error({no_src, Mod}) ->
io_lib:format("Source file for ~s is not available.\n", [Mod]);
format_error(docs_present) ->
<<"Docs chunk already present">>;
format_error(no_debug_info_no_src) ->
<<"neither debug_info nor .erl available">>;
format_error(Reason) when is_list(Reason);
is_binary(Reason) ->
Reason;
format_error({error, Reason, Stacktrace}) ->
io_lib:format("docsh error: ~p~n~p~n", [Reason, Stacktrace]).
-spec available_readers(docsh_beam:t()) -> [docsh_reader:t()].
available_readers(Beam) ->
docsh_edoc:available(Beam) ++
docsh_syntax:available(Beam).
-spec is_module_available(module()) -> boolean().
is_module_available(Mod) ->
try
Mod:module_info(),
true
catch
_:undef -> false
end.
-spec group_by(fun(), list()) -> dict:dict().
group_by(F, L) ->
lists:foldr(fun({K,V}, D) -> dict:append(K, V, D) end,
dict:new(), [ {F(X), X} || X <- L ]).
-spec get_docs(module()) -> {ok, docsh_format:t()} | {error, any()}.
get_docs(M) ->
case docsh_beam:from_loaded_module(M) of
{error, _} = E -> E;
{ok, B} ->
MakeDocs = application:get_env(docsh, compile_on_demand, compile_if_missing),
AvailableDocs = do_get_docs(B),
dispatch_docs_extraction(B, MakeDocs, AvailableDocs)
end.
do_get_docs(B) ->
try
{ok, docsh_beam:docs(B)}
catch _:R ->
{error, R}
end.
dispatch_docs_extraction(B, never, AvailableDocs) ->
%% just for troubleshooting
{ok, [B, never, AvailableDocs]};
dispatch_docs_extraction(_, compile_if_missing, {ok, Docs}) ->
{ok, Docs};
dispatch_docs_extraction(B, compile_if_missing, {error, _} = Err) ->
%% TODO: log Reason?
dispatch_docs_extraction(B, always, Err);
dispatch_docs_extraction(B, always, _) ->
dispatch_docs_extraction_(B).
dispatch_docs_extraction_(B) ->
{ok, Docs, Warnings} = make_docs(B),
[ print("~s", [docsh_lib:format_error({W, docsh_beam:name(B)})]) || W <- Warnings ],
%% TODO: enable cache at some point
%cache_docs(B, Docs),
{ok, Docs}.
-spec make_docs(docsh_beam:t()) -> {ok, docsh_format:t(), [Warning]} when
Warning :: no_debug_info | no_src.
make_docs(Beam) ->
BEAMFile = docsh_beam:beam_file(Beam),
has_docs(BEAMFile)
andalso erlang:error(docs_present, [BEAMFile]),
case {docsh_beam:abstract_code(Beam), docsh_beam:source_file(Beam)} of
{false, false} ->
erlang:error({no_debug_info_no_src, BEAMFile}, [BEAMFile]);
{_, false} ->
{ok, do_make_docs(Beam), [no_src]};
{false, _} ->
{ok, do_make_docs(Beam), [no_debug_info]};
_ ->
{ok, do_make_docs(Beam), []}
end.
-spec do_make_docs(docsh_beam:t()) -> {string(), binary()}.
do_make_docs(Beam) ->
FromMods = get_readers(Beam),
FromMods == []
andalso erlang:error(no_readers_available),
ToMod = application:get_env(docsh, docsh_writer, default_writer()),
convert(FromMods, ToMod, Beam).
default_writer() ->
docsh_docs_v1.
get_readers(Beam) ->
application:get_env(docsh, readers, available_readers(Beam)).
-spec unstick_module(module()) -> any().
unstick_module(Module) -> unstick_module(Module, code:is_sticky(Module)).
unstick_module(Module, true) -> code:unstick_mod(Module);
unstick_module(_,_) -> false.
-spec stick_module(module()) -> any().
stick_module(Module) -> stick_module(Module, code:is_sticky(Module)).
stick_module(Module, false) -> code:stick_mod(Module);
stick_module(_,_) -> false.