%% @doc %% Gradualizer command line interface. %% @end -module(gradualizer_cli). -export([main/1, handle_args/1]). -spec main([string()]) -> ok. main(Args) -> case handle_args(Args) of help -> print_usage(); version -> print_version(); {error, Message} -> io:format(standard_error, "~s~n", [Message]), halt(1); {ok, Files, Opts} -> start_application(Opts), CheckResult = gradualizer:type_check_files(Files, Opts), gradualizer_tracer:flush(), case CheckResult of ok -> ok; nok -> halt(1) end end. start_application(Opts) -> %% An explicit load makes sure any options defined in a *.config file are set before %% we call `application:set_env/3'. %% A load after set_env overrides anything set with set_env. %% If gradualizer is run as an escript this should not be necessary, but better safe than sorry. ok = application:load(gradualizer), application:set_env(gradualizer, options, Opts), %% We could start the tracer based on a CLI flag, but it's config is compile-time anyway. %gradualizer_tracer:start(), {ok, _} = application:ensure_all_started(gradualizer). -spec handle_args([string()]) -> help | version | {error, string()} | {ok, [string()], gradualizer:options()}. handle_args([]) -> help; handle_args(Args) -> try parse_opts(Args, []) of {Rest, Opts} -> HasHelp = proplists:get_bool(help, Opts), HasVersion = proplists:get_bool(version, Opts), if HasHelp -> help; HasVersion -> version; Rest =:= [] -> {error, "No files specified to check (try --)"}; true -> {ok, Rest, Opts} end catch error:Message when is_list(Message) -> {error, Message} end. -spec get_ver(atom()) -> string(). get_ver(App) -> {_, _, Ver} = lists:keyfind(App, 1, application:loaded_applications()), Ver. print_version() -> application:load(gradualizer), io:format("Gradualizer v~s~n", [get_ver(gradualizer)]). print_usage() -> io:format("Usage: gradualizer [options] [PATH...]~n"), io:format("A type checker for Erlang/Elixir~n~n"), io:format(" PATH Files or directories to type check~n"), io:format(" -- Signals that no more options will follow. The following~n"), io:format(" arguments are treated as filenames, even if~n"), io:format(" they start with hyphens.~n"), io:format(" -h, --help display this help and exit~n"), io:format(" --infer Infer type information from literals and other~n"), io:format(" language constructs~n"), io:format(" --no_infer Only use type information from function specs~n"), io:format(" - the default behaviour~n"), io:format(" --verbose Show what Gradualizer is doing~n"), io:format(" -pa, --path_add Add the specified directory to the beginning of~n"), io:format(" the code path; see erl -pa [string]~n"), io:format(" -I Include path for Erlang source files; see -I in~n"), io:format(" the manual page erlc(1)~n"), io:format(" --stop_on_first_error stop type checking at the first error~n"), io:format(" --no_stop_on_first_error inverse of --stop-on-first-error~n"), io:format(" - the default behaviour~n"), io:format(" --no_prelude Do not override OTP specs.~n"), io:format(" --specs_override_dir Add specs overrides from the *.specs.erl files in~n"), io:format(" this directory.~n"), io:format(" --fmt_location How to format location when pretty printing errors~n"), io:format(" (Column is only available if analyzing from source)~n"), io:format(" - 'none': no location for easier comparison~n"), io:format(" - 'brief': for machine processing~n"), io:format(" (\"LINE:COLUMN:\" before message text)~n"), io:format(" - 'verbose' (default): for human readers~n"), io:format(" (\"on line LINE at column COLUMN\" within the message text)~n"), io:format(" --color [ COLOR ] - Use colors when printing fancy messages. An optional~n"), io:format(" argument is `always | never | auto'. However, auto-~n"), io:format(" detection of a TTY doesn't work when running as an escript.~n"), io:format(" --no_color - Alias for `--color never'~n"), io:format(" --fancy - Use fancy error messages when possible (on by default)~n"), io:format(" --no_fancy - Don't use fancy error messages.~n"), io:format(" --union_size_limit - Performance hack: Unions larger than this value~n"), io:format(" are replaced by any() in normalization (default: 30)~n"). -spec parse_opts([string()], gradualizer:options()) -> {[string()], gradualizer:options()}. parse_opts([], Opts) -> {[], Opts}; parse_opts([A | Args], Opts) -> case A of "-h" -> {[], [help]}; "--help" -> {[], [help]}; "--infer" -> parse_opts(Args, [infer | Opts]); "--no_infer" -> parse_opts(Args, [{infer, false} | Opts]); "--verbose" -> parse_opts(Args, [verbose | Opts]); "-pa" -> handle_path_add(A, Args, Opts); "--path_add" -> handle_path_add(A, Args, Opts); "-I" -> handle_include_path(A, Args, Opts); "--stop_on_first_error" -> parse_opts(Args, [stop_on_first_error | Opts]); "--no_stop_on_first_error" -> parse_opts(Args, [{stop_on_first_error, false} | Opts]); "--crash_on_error" -> parse_opts(Args, [crash_on_error | Opts]); "--no_crash_on_error" -> parse_opts(Args, [{crash_on_error, false} | Opts]); "--version" -> {[], [version]}; "--no_prelude" -> parse_opts(Args, [{prelude, false}| Opts]); "--specs_override_dir" -> handle_specs_override(A, Args, Opts); "--fmt_location" -> handle_fmt_location(Args, Opts); "--color" -> handle_color(Args, Opts); "--no_color" -> parse_opts(Args, [{color, never} | Opts]); "--fancy" -> parse_opts(Args, [fancy | Opts]); "--no_fancy" -> parse_opts(Args, [{fancy, false} | Opts]); "--union_size_limit" -> handle_union_size_limit(A, Args, Opts); "--" -> {Args, Opts}; "-" ++ _ -> erlang:error(string:join(["Unknown parameter:", A], " ")); _ -> {[A | Args], Opts} end. -spec handle_path_add(string(), [string()], gradualizer:options()) -> {[string()], gradualizer:options()}. handle_path_add(A, Args, Opts) -> {Paths, RestArgs} = lists:splitwith(fun no_start_dash/1, Args), case Paths of [] -> erlang:error(string:join(["Missing argument for", A], " ")); _ -> code:add_pathsa(Paths) end, parse_opts(RestArgs, Opts). -spec handle_include_path(string(), [string()], gradualizer:options()) -> {[string()], gradualizer:options()}. handle_include_path(_, [Dir | Args], Opts) -> parse_opts(Args, [{i, Dir} | Opts]); handle_include_path(A, [], _Opts) -> erlang:error(string:join(["Missing argument for", A], " ")). -spec handle_specs_override(string(), [string()], gradualizer:options()) -> {[string()], gradualizer:options()}. handle_specs_override(_, [Dir | Args], Opts) -> parse_opts(Args, [{specs_override, Dir} | Opts]); handle_specs_override(A, [], _Opts) -> erlang:error(string:join(["Missing argument for", A], " ")). handle_fmt_location([FmtTypeStr | Args], Opts) -> try list_to_existing_atom(FmtTypeStr) of FmtType when FmtType =:= none; FmtType =:= brief; FmtType =:= verbose -> parse_opts(Args, [{fmt_location, FmtType}|Opts]); _ -> erlang:error(lists:append(["Bad value for fmt-location: ", FmtTypeStr])) catch _:_ -> erlang:error(lists:append(["Bad value for fmt-location: ", FmtTypeStr])) end. %% Handle Args after --color. -spec handle_color([string()], gradualizer:options()) -> {[string()], gradualizer:options()}. handle_color(["always"|Args], Opts) -> parse_opts(Args, [{color, always} | Opts]); handle_color(["never" |Args], Opts) -> parse_opts(Args, [{color, never} | Opts]); handle_color(["auto" |Args], Opts) -> parse_opts(Args, [{color, auto} | Opts]); handle_color(Args, Opts) -> parse_opts(Args, [{color, always} | Opts]). handle_union_size_limit(_, [LimitS | Args], _Opts) -> Limit = list_to_integer(LimitS), parse_opts(Args, [{union_size_limit, Limit}]); handle_union_size_limit(A, [], _Opts) -> erlang:error(string:join(["Missing argument for", A], " ")). no_start_dash("-" ++ _) -> false; no_start_dash(_) -> true.