%% @doc %% Gradualizer is a static type checker for Erlang %% with support for gradual typing. %% %% A static type checker detects errors thanks to type analysis of a given program. %% Gradualizer can infer some types, but mostly relies on function specs. %% In a nutshell, we could say that it checks for consistency of a function body with its %% declared spec and for consistency of a callee's spec with passed in arguments. %% Specifically, Gradualizer does not perform entire program flow analysis. %% %% Gradual typing means that full type information is not required to raise warnings - any type %% information is better than no type information. It allows for a _gradual_ transition between %% fully static typing, which offers the best correctness guarantee, and fully dynamic typing, %% which means the least overhead of writing specs, the full Erlang expressiveness, %% and _letting it crash_ at runtime. %% We can choose the right balance between the static/dynamic extremes depending on the application, %% project maturity, team size, the need for reusability or for documentation consistency. %% %% %% If you are interested in using Gradualizer to type check your code, %% then you might be looking for the Rebar3 plugin {@link rebar_prv_gradualizer} %% or the command-line interface {@link gradualizer_cli}. %% %% %% This module contains entry points for calling into Gradualizer as a library, %% as well as some Erlang shell utilities for working directly with the type checker internals. %% %% Let's try out the shell utilities by running: %% %% ``` %% $ rebar3 shell %% ''' %% %% Now, we can play with some examples: %% %% ``` %% > gradualizer:type_of("[ a || _ <- lists:seq(1, 5) ]"). %% {type,0,list,[{atom,0,a}]} %% > typelib:pp_type(v(-1)). %% "[a]" %% > typechecker:normalize(gradualizer:type("a()"), gradualizer:env("-type a() :: integer().", [])). %% {type,0,integer,[]} %% > typelib:pp_type(v(-1)). %% "integer()" %% > gradualizer:type_of("fun (A) -> #{tag => my_map, list_of_as => [ A || _ <- lists:seq(1, 5) ]} end"). %% {type,0,'fun', %% [{type,0,product,[{type,0,any,[]}]}, %% {type,0,map, %% [{type,0,map_field_assoc,[{atom,0,tag},{atom,0,my_map}]}, %% {type,0,map_field_assoc, %% [{atom,0,list_of_as},{type,0,list,[{type,0,any,[]}]}]}]}]} %% > typelib:pp_type(v(-1)). %% "fun((any()) -> #{tag => my_map, list_of_as => [any()]})" %% ''' %% %% The main library API of Gradualizer are `type_check_(file|module|dir)' functions, %% which accept options described by the {@link options()} type. %% @end -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/1, env/0, env/1, env/2, type_of/1, type_of/2]). -export_type([options/0, top/0]). -type options() :: proplists:proplist(). %% The options accepted by the Gradualizer API. %% %% %% This type is the top of the subtyping lattice. It's never expanded. %% The definition can be anything apart from any(), %% so that we don't run into the "opaque type underspecified and therefore meaningless" warning. -opaque top() :: none(). -include("gradualizer.hrl"). -include("typechecker.hrl"). %% 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) -> ExcludeModules = proplists:get_value(exclude_modules, Opts, []), AtomBaseFile = list_to_atom(filename:rootname(filename:basename(File))), case lists:member(AtomBaseFile, ExcludeModules) of true -> ok; false -> case filename:extension(File) of ".erl" -> Includes = proplists:get_all_values(i, Opts), case gradualizer_file_utils:get_forms_from_erl(File, Includes) of {ok, Forms} -> lint_and_check_forms(Forms, File, Opts); Error -> throw(Error) end; ".beam" -> case gradualizer_file_utils:get_forms_from_beam(File) of {ok, Forms} -> type_check_forms(File, Forms, Opts); Error -> throw(Error) end; Ext -> throw({unknown_file_extension, Ext}) end end. %% @doc Runs an erl_lint pass, to check if the forms can be compiled at all, %% before running the type checker. -spec lint_and_check_forms(Forms, file:filename(), options()) -> R when Forms :: gradualizer_file_utils:abstract_forms(), R :: ok | nok | [{file:filename(), any()}]. lint_and_check_forms(Forms, File, Opts) -> case erl_lint:module(Forms, File, [return_errors]) of {ok, _Warnings} -> type_check_forms(File, Forms, Opts); {error, Errors, _Warnings} -> %% If there are lint errors (i.e. compile errors like undefined %% variables) we don't even try to type check. case proplists:get_bool(return_errors, Opts) of true -> [{Filename, ErrorInfo} || {Filename, ErrorInfos} <- Errors, ErrorInfo <- ErrorInfos]; false -> [gradualizer_fmt:print_errors(ErrorInfos, [{filename, Filename} | Opts]) || {Filename, ErrorInfos} <- Errors], nok end 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 -> Pattern = filename:join(Dir, "*.{erl,beam}"), type_check_files(filelib:wildcard(Pattern), 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 returned 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(), Forms, options()) -> R when Forms :: gradualizer_file_utils:abstract_forms(), R :: ok | nok | [{file:filename(), any()}]. type_check_forms(File, Forms, Opts) -> ReturnErrors = proplists:get_bool(return_errors, Opts), OptsForModule = options_from_forms(Forms) ++ Opts, 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), gradualizer_fmt:print_errors(Errors, Opts1), nok end. add_source_file_and_forms_to_opts(File, Forms, Opts) -> Opts1 = [{filename, File}, {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(gradualizer_file_utils:abstract_forms()) -> 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([]) -> []. %% @doc Return a Gradualizer type for the passed in Erlang type definition. %% %% ``` %% > gradualizer:type("[integer()]"). %% {type,0,list,[{type,0,integer,[]}]} %% ''' -spec type(string()) -> typechecker:type(). type(Type) -> typelib:remove_pos(typelib:parse_type(Type)). %% @see env/2 -spec env() -> typechecker:env(). env() -> env([]). %% @see env/2 -spec env(gradualizer:options()) -> typechecker:env(). env(Opts) -> env("", Opts). %% @doc Create a type checker environment populated by types defined in a source code snippet %% via `-type ...' and `-record(...)' attributes. %% %% Currently, it's not possible to define variable bindings in the environment. %% %% ``` %% > rr(typechecker). %% > gradualizer:env("-type a() :: integer().", []). %% #env{fenv = #{},imported = #{},venv = #{}, %% tenv = #{module => undefined,records => #{}, %% types => #{{a,0} => {[],{type,0,integer,[]}}}}, %% infer = false,verbose = false,exhaust = true, %% clauses_stack = [],union_size_limit = 30, %% current_spec = none} %% > gradualizer:env("-record(r, {f}).", []). %% #env{ %% fenv = #{},imported = #{},venv = #{}, %% tenv = %% #{module => undefined, %% records => %% #{r => %% [{typed_record_field, %% {record_field,0,{atom,0,f},{atom,1,undefined}}, %% {type,0,any,[]}}]}, %% types => #{}}, %% infer = false,verbose = false,exhaust = true, %% clauses_stack = [],union_size_limit = 30, %% current_spec = none} %% ''' -spec env(string(), gradualizer:options()) -> typechecker:env(). env(ErlSource, Opts) -> Forms = gradualizer_lib:ensure_form_list(merl:quote(lists:flatten(ErlSource))), ErlParseForms = lists:map(fun erl_syntax:revert/1, Forms), ParseData = typechecker:collect_specs_types_opaques_and_functions(ErlParseForms), typechecker:create_env(ParseData, Opts). %% @see type_of/2 -spec type_of(string()) -> typechecker:type(). type_of(Expr) -> type_of(Expr, env([infer])). %% @doc Infer type of an Erlang expression. %% %% ``` %% > gradualizer:type_of("[ a || _ <- lists:seq(1, 5) ]"). %% {type,0,list,[{atom,0,a}]} %% > gradualizer:type_of("case 5 of 1 -> one; _ -> more end"). %% {type,0,union,[{atom,0,more},{atom,0,one}]} %% > Env = gradualizer:env([{union_size_limit, 1}]). %% > gradualizer:type_of("case 5 of 1 -> one; _ -> more end", Env). %% {type,0,any,[]} %% ''' -spec type_of(string(), typechecker:env()) -> typechecker:type(). type_of(Expr, Env) -> AlwaysInfer = Env#env{infer = true}, {Ty, _Env, _Cs} = typechecker:type_check_expr(AlwaysInfer, merl:quote(Expr)), Ty.