Current section

Files

Jump to
spectra src spectra_transform.erl
Raw

src/spectra_transform.erl

-module(spectra_transform).
-export([parse_transform/2]).
-ignore_xref([parse_transform/2]).
-define(TYPE_INFO_FUNCTION, '__spectra_type_info__').
-type forms() :: [erl_parse:abstract_form() | erl_parse:form_info()].
-doc """
Parse transform that injects `__spectra_type_info__/0` into the module being
compiled. Opt in per module with `-compile({parse_transform, spectra_transform}).`.
The generated function returns the same `spectra:type_info()` value that
`spectra_abstract_code:types_in_module/1` would produce at runtime, but without
requiring the BEAM to carry `abstract_code` debug info.
The export attribute, spec and function body are injected independently so
that hand-written pieces of `__spectra_type_info__/0` are always preserved:
if the function body is already defined (plus an `-export` or
`-compile(export_all)`), the forms are returned unchanged; otherwise only
the missing pieces are added.
""".
-spec parse_transform(forms(), list()) -> forms().
parse_transform(Forms, _Opts) ->
maybe
{ok, Module, Anno} ?= module_header(Forms),
false ?= skip_module(Module) orelse fully_defined(Forms),
{ok, Injected} ?= try_inject(Forms, Module, Anno),
Injected
else
{error, ErrAnno, Reason} ->
[{error, {erl_anno:line(ErrAnno), ?MODULE, Reason}} | Forms];
true ->
Forms
end.
-spec try_inject(forms(), module(), erl_anno:anno()) ->
{ok, forms()} | {error, erl_anno:anno(), term()}.
try_inject(Forms, Module, Anno) ->
try spectra_abstract_code:types_in_forms(Module, Forms) of
TypeInfo ->
{ok, inject(Forms, TypeInfo, Anno)}
catch
Class:Reason:Stack ->
{error, Anno, {transform_failed, Module, Class, Reason, Stack}}
end.
-spec module_header(forms()) ->
{ok, module(), erl_anno:anno()} | {error, erl_anno:anno(), missing_module_attribute}.
module_header([{attribute, Anno, module, M} | _]) when is_atom(M) -> {ok, M, Anno};
module_header([_ | Rest]) -> module_header(Rest);
module_header([]) -> {error, erl_anno:new(0), missing_module_attribute}.
-spec skip_module(module()) -> boolean().
skip_module(spectra_transform) -> true;
skip_module(spectra_abstract_code) -> true;
skip_module(spectra_type) -> true;
skip_module(spectra_type_info) -> true;
skip_module(_) -> false.
-spec fully_defined(forms()) -> boolean().
fully_defined(Forms) ->
has_function_form(Forms) andalso has_export(Forms).
-spec has_function_form(forms()) -> boolean().
has_function_form(Forms) ->
lists:any(
fun
({function, _, ?TYPE_INFO_FUNCTION, 0, _}) -> true;
(_) -> false
end,
Forms
).
-spec has_spec(forms()) -> boolean().
has_spec(Forms) ->
lists:any(
fun
({attribute, _, spec, {{?TYPE_INFO_FUNCTION, 0}, _}}) -> true;
(_) -> false
end,
Forms
).
-spec has_export(forms()) -> boolean().
has_export(Forms) ->
lists:any(fun is_type_info_export/1, Forms).
-spec is_type_info_export(term()) -> boolean().
is_type_info_export({attribute, _, export, List}) when is_list(List) ->
lists:member({?TYPE_INFO_FUNCTION, 0}, List);
is_type_info_export({attribute, _, compile, Opts}) ->
compile_exports_all(Opts);
is_type_info_export(_) ->
false.
-spec compile_exports_all(term()) -> boolean().
compile_exports_all(export_all) -> true;
compile_exports_all(Opts) when is_list(Opts) -> lists:member(export_all, Opts);
compile_exports_all(_) -> false.
-spec inject(forms(), spectra:type_info(), erl_anno:anno()) -> forms().
inject(Forms, TypeInfo, Anno) ->
ExportForms = [
{attribute, Anno, export, [{?TYPE_INFO_FUNCTION, 0}]}
|| not has_export(Forms)
],
SpecForms = [
{attribute, Anno, spec,
{{?TYPE_INFO_FUNCTION, 0}, [
{type, Anno, 'fun', [
{type, Anno, product, []},
{remote_type, Anno, [
{atom, Anno, spectra},
{atom, Anno, type_info},
[]
]}
]}
]}}
|| not has_spec(Forms)
],
FunctionForms =
case has_function_form(Forms) of
true ->
[];
false ->
Body = erl_parse:abstract(TypeInfo, [{line, erl_anno:line(Anno)}]),
[
{function, Anno, ?TYPE_INFO_FUNCTION, 0, [
{clause, Anno, [], [], [Body]}
]}
]
end,
insert_forms(Forms, ExportForms, SpecForms ++ FunctionForms).
-spec insert_forms(forms(), [erl_parse:abstract_form()], [erl_parse:abstract_form()]) ->
forms().
insert_forms([{attribute, _, module, _} = ModForm | Rest], ExportForms, TrailingForms) ->
[ModForm | ExportForms ++ insert_before_eof(Rest, TrailingForms)];
insert_forms([Form | Rest], ExportForms, TrailingForms) ->
[Form | insert_forms(Rest, ExportForms, TrailingForms)];
insert_forms([], _ExportForms, TrailingForms) ->
TrailingForms.
-spec insert_before_eof(forms(), [erl_parse:abstract_form()]) -> forms().
insert_before_eof([], TrailingForms) ->
TrailingForms;
insert_before_eof([{eof, _} | _] = All, TrailingForms) ->
TrailingForms ++ All;
insert_before_eof([Form | Rest], TrailingForms) ->
[Form | insert_before_eof(Rest, TrailingForms)].