Current section
Files
Jump to
Current section
Files
src/gradualizer.erl
%%% @doc Main external API of the Gradualizer
%%%
%%% The functions `type_check(file|module|dir)' accept the following options:
%%% - `{i, Dir}': Include path for `-include' and `-include_lib' when checking
%%% Erlang source files. Specify multiple times for multiple include paths.
%%% - `stop_on_first_error': if `true' stop type checking at the first error,
%%% if `false' continue checking all functions in the given file and all files
%%% in the given directory.
%%% - `{print_file, true | false | module | basename}': if `true' prefix error
%%% printouts with the file name the error is from. Default `false'.
%%% - `crash_on_error': if `true' crash on the first produced error
%%% - `return_errors': if `true', turns off error printing and errors
%%% (in their internal format) are returned in a list instead of being
%%% condensed into a single ok | nok.
%%% - `fmt_location': how to format location when pretty printing errors
%%% - `none': no location for easier comparison
%%% - `brief': for machine processing ("LINE:COLUMN:" before message text)
%%% - `verbose' (default): for human readers
%%% ("on line LINE at column COLUMN" within the message text)
%%% - `fmt_expr_fun': function to pretty print an expression AST
%%% (useful to support other languages)
%%% - `fmt_type_fun': function to pretty print a type AST
%%% (useful to support other languages)
%%% - `{color, always | never | auto}': Use colors when printing fancy messages.
%%% Auto is the default but auto-detection of tty doesn't work when running
%%% as an escript. It works when running from the Erlang shell though.
%%% - `{fancy, boolean()}': Use fancy error messages when possible. True by
%%% default. Doesn't work when a custom `fmt_expr_fun' is used.
-module(gradualizer).
-export([type_check_file/1,
type_check_file/2,
type_check_module/1,
type_check_module/2,
type_check_dir/1,
type_check_dir/2,
type_check_files/1,
type_check_files/2,
type_check_forms/2
]).
-export_type([options/0, top/0]).
-type options() :: proplists:proplist().
%% This type is the top of the subtyping lattice.
-opaque top() :: any().
%% API functions
%% @doc Type check a source or beam file
-spec type_check_file(file:filename()) -> ok | nok | [{file:filename(), any()}].
type_check_file(File) ->
type_check_file(File, []).
%% @doc Type check a source or beam file
-spec type_check_file(file:filename(), options()) -> ok | nok | [{file:filename(), any()}].
type_check_file(File, Opts) ->
ParsedFile =
case filename:extension(File) of
".erl" ->
Includes = proplists:get_all_values(i, Opts),
gradualizer_file_utils:get_forms_from_erl(File, Includes);
".beam" ->
gradualizer_file_utils:get_forms_from_beam(File);
Ext ->
case filelib:is_dir(File) of
true -> type_check_dir(File, Opts);
false -> throw({unknown_file_extension, Ext})
end
end,
case ParsedFile of
{ok, Forms} ->
Opts2 = add_filename_to_opts(File, Opts),
type_check_forms(File, Forms, Opts2);
Error ->
throw(Error)
end.
%% @doc Prepends `{filename, string()}', depending on whether and how the
%% filename should be printed according to the print_file option in Opts.
add_filename_to_opts(Filename, Opts) ->
case proplists:get_value(print_file, Opts, false) of
false -> Opts;
true -> [{filename, Filename} | Opts];
basename -> [{filename, filename:basename(Filename)} | Opts];
module -> [{filename, filename:rootname(
filename:basename(Filename))} | Opts]
end.
%% @doc Type check a module
-spec type_check_module(module()) -> ok | nok | [{file:filename(), any()}].
type_check_module(Module) ->
type_check_module(Module, []).
%% @doc Type check a module
-spec type_check_module(module(), options()) ->
ok | nok | [{file:filename(), any()}].
type_check_module(Module, Opts) when is_atom(Module) ->
case code:which(Module) of
File when is_list(File) ->
type_check_file(File, Opts);
Error when is_atom(Error) ->
throw({beam_not_found, Error})
end.
%% @doc Type check all source or beam files in a directory.
-spec type_check_dir(file:filename()) -> ok | nok | [{file:filename(), any()}].
type_check_dir(Dir) ->
type_check_dir(Dir, []).
%% @doc Type check all source or beam files in a directory.
-spec type_check_dir(file:filename(), options()) ->
ok | nok | [{file:filename(), any()}].
type_check_dir(Dir, Opts) ->
case filelib:is_dir(Dir) of
true ->
type_check_files(filelib:wildcard(filename:join(Dir, "*.{erl,beam}")), Opts);
false ->
throw({dir_not_found, Dir})
end.
%% @doc Type check a source or beam file
-spec type_check_files([file:filename()]) ->
ok | nok | [{file:filename(), any()}].
type_check_files(Files) ->
type_check_files(Files, []).
%% @doc Type check a source or beam
-spec type_check_files([file:filename()], options()) ->
ok | nok | [{file:filename(), any()}].
type_check_files(Files, Opts) ->
StopOnFirstError = proplists:get_bool(stop_on_first_error, Opts),
ReturnErrors = proplists:get_bool(return_errors, Opts),
if ReturnErrors ->
lists:foldl(
fun(File, Errors) when Errors =:= [];
not StopOnFirstError ->
type_check_file_or_dir(File, Opts) ++ Errors;
(_, Errors) ->
Errors
end, [], Files);
true ->
lists:foldl(
fun(File, Res) when Res =:= ok;
not StopOnFirstError ->
case type_check_file_or_dir(File, Opts) of
ok -> Res;
nok -> nok
end;
(_, nok) ->
nok
end, ok, Files)
end.
-spec type_check_file_or_dir(file:filename(), options()) ->
ok | nok | [{file:filename(), any()}].
type_check_file_or_dir(File, Opts) ->
IsRegular = filelib:is_regular(File),
IsDir = filelib:is_dir(File),
if
IsDir -> type_check_dir(File, Opts);
IsRegular -> type_check_file(File, Opts);
true -> throw({file_not_found, File}) % TODO: better errors
end.
%% @doc Type check an abstract syntax tree of a module. This can be useful
%% for tools where the abstract forms are generated in memory.
%%
%% If the first form is a file attribute (as in forms retuned by e.g.
%% epp:parse_file/1,2), that filename will be used in error messages.
%% The second form is typically the module attribute.
-spec type_check_forms([erl_parse:abstract_form()], options()) ->
ok | nok | [{file:filename(), any()}].
type_check_forms(Forms, Opts) ->
File = case Forms of
[{attribute, _, file, {F, _}} | _] -> F;
_ -> "no filename"
end,
type_check_forms(File, Forms, Opts).
%% Helper
-spec type_check_forms(file:filename(), [erl_parse:abstract_form()], options()) ->
ok | nok | [{file:filename(), any()}].
type_check_forms(File, Forms, Opts) ->
ReturnErrors = proplists:get_bool(return_errors, Opts),
OptsForModule =
options_from_forms(Forms) ++
Opts ++
[prelude], % using prelude is the default if nothing else is specified
Errors = typechecker:type_check_forms(Forms, OptsForModule),
case {ReturnErrors, Errors} of
{true, _ } ->
lists:map(fun(Error) -> {File, Error} end, Errors);
{false, []} ->
ok;
{false, [_|_]} ->
Opts1 = add_source_file_and_forms_to_opts(File, Forms, Opts),
typechecker:print_errors(Errors, Opts1),
nok
end.
add_source_file_and_forms_to_opts(File, Forms, Opts) ->
Opts1 = [{forms, Forms}|Opts],
case filename:extension(File) == ".erl" andalso filelib:is_file(File) of
true -> [{source_file, File} | Opts1];
false -> Opts1
end.
%% Extract -gradualizer(Options) from AST
-spec options_from_forms([erl_parse:abstract_form()]) -> options().
options_from_forms([{attribute, _L, gradualizer, Opts} | Fs]) when is_list(Opts) ->
Opts ++ options_from_forms(Fs);
options_from_forms([{attribute, _L, gradualizer, Opt} | Fs]) ->
[Opt | options_from_forms(Fs)];
options_from_forms([_F | Fs]) -> options_from_forms(Fs);
options_from_forms([]) -> [].