diff options
Diffstat (limited to 'src/rebar_prv_dialyzer.erl')
-rw-r--r-- | src/rebar_prv_dialyzer.erl | 243 |
1 files changed, 107 insertions, 136 deletions
diff --git a/src/rebar_prv_dialyzer.erl b/src/rebar_prv_dialyzer.erl index 96e2277..1cf7b71 100644 --- a/src/rebar_prv_dialyzer.erl +++ b/src/rebar_prv_dialyzer.erl @@ -14,6 +14,7 @@ -define(PROVIDER, dialyzer). -define(DEPS, [compile]). +-define(PLT_PREFIX, "rebar3"). %% =================================================================== %% Public API @@ -25,7 +26,7 @@ init(State) -> {succ_typings, $s, "succ-typings", boolean, "Enable success typing analysis. Default: true"}], State1 = rebar_state:add_provider(State, providers:create([{name, ?PROVIDER}, {module, ?MODULE}, - {bare, false}, + {bare, true}, {deps, ?DEPS}, {example, "rebar3 dialyzer"}, {short_desc, short_desc()}, @@ -39,33 +40,42 @@ desc() -> "This command will build, and keep up-to-date, a suitable PLT and will use " "it to carry out success typing analysis on the current project.\n" "\n" - "The following (optional) configurations can be added to a rebar.config:\n" - "`dialyzer_warnings` - a list of dialyzer warnings\n" - "`dialyzer_plt` - the PLT file to use\n" - "`dialyzer_plt_apps` - a list of applications to include in the PLT file*\n" - "`dialyzer_plt_warnings` - display warnings when updating a PLT file " - "(boolean)\n" - "`dialyzer_base_plt` - the base PLT file to use**\n" - "`dialyzer_base_plt_dir` - the base PLT directory**\n" - "`dialyzer_base_plt_apps` - a list of applications to include in the base " - "PLT file**\n" + "The following (optional) configurations can be added to a `proplist` of " + "options `dialyzer` in rebar.config:\n" + "`warnings` - a list of dialyzer warnings\n" + "`get_warnings` - display warnings when altering a PLT file (boolean)\n" + "`plt_extra_apps` - a list of applications to include in the PLT file*\n" + "`plt_location` - the location of the PLT file, `local` to store in the " + "profile's base directory (default) or a custom directory.\n" + "`plt_prefix` - the prefix to the PLT file, defaults to \"rebar3\"**\n" + "`base_plt_apps` - a list of applications to include in the base " + "PLT file***\n" + "`base_plt_location` - the location of base PLT file, `global` to store in " + "$HOME/.cache/rebar3 (default) or a custom directory***\n" + "`base_plt_prefix` - the prefix to the base PLT file, defaults to " + "\"rebar3\"** ***\n" + "\n" + "For example, to warn on unmatched returns: \n" + "{dialyzer, [{warnings, [unmatched_returns]}]}.\n" "\n" "*The applications in `dialyzer_base_plt_apps` and any `applications` and " "`included_applications` listed in their .app files will be added to the " "list.\n" - "**The base PLT is a PLT containing the core OTP applications often " - "required for a project's PLT. One base PLT is created per OTP version and " - "stored in `dialyzer_base_plt_dir` (defaults to $HOME/.rebar3/). A base " - "PLT is used to create a project's initial PLT.". + "**PLT files are named \"<prefix>_<otp_release>_plt\".\n" + "***The base PLT is a PLT containing the core applications often required " + "for a project's PLT. One base PLT is created per OTP version and " + "stored in `base_plt_location`. A base PLT is used to build project PLTs." + "\n". short_desc() -> "Run the Dialyzer analyzer on the project.". -spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}. do(State) -> + maybe_fix_env(), ?INFO("Dialyzer starting, this may take a while...", []), code:add_pathsa(rebar_state:code_paths(State, all_deps)), - Plt = get_plt_location(State), + Plt = get_plt(State), try do(State, Plt) @@ -74,17 +84,28 @@ do(State) -> ?PRV_ERROR({error_processing_apps, Error}); throw:{dialyzer_warnings, Warnings} -> ?PRV_ERROR({dialyzer_warnings, Warnings}); + throw:{unknown_application, _} = Error -> + ?PRV_ERROR(Error); throw:{output_file_error, _, _} = Error -> ?PRV_ERROR(Error) after rebar_utils:cleanup_code_path(rebar_state:code_paths(State, default)) end. +%% This is used to workaround dialyzer quirk discussed here +%% https://github.com/rebar/rebar3/pull/489#issuecomment-107953541 +%% Dialyzer gets default plt location wrong way by peeking HOME environment +%% variable which usually is not defined on Windows. +maybe_fix_env() -> + os:putenv("DIALYZER_PLT", filename:join(rebar_dir:home_dir(), ".dialyzer_plt")). + -spec format_error(any()) -> iolist(). format_error({error_processing_apps, Error}) -> io_lib:format("Error in dialyzing apps: ~s", [Error]); format_error({dialyzer_warnings, Warnings}) -> io_lib:format("Warnings occured running dialyzer: ~b", [Warnings]); +format_error({unknown_application, App}) -> + io_lib:format("Could not find application: ~s", [App]); format_error({output_file_error, File, Error}) -> Error1 = file:format_error(Error), io_lib:format("Failed to write to ~s: ~s", [File, Error1]); @@ -93,13 +114,19 @@ format_error(Reason) -> %% Internal functions -get_plt_location(State) -> - BaseDir = rebar_dir:base_dir(State), - DefaultPlt = filename:join(BaseDir, default_plt()), - rebar_state:get(State, dialyzer_plt, DefaultPlt). +get_plt(State) -> + Prefix = get_config(State, plt_prefix, ?PLT_PREFIX), + Name = plt_name(Prefix), + case get_config(State, plt_location, local) of + local -> + BaseDir = rebar_dir:base_dir(State), + filename:join(BaseDir, Name); + Dir -> + filename:join(Dir, Name) + end. -default_plt() -> - rebar_utils:otp_release() ++ ".plt". +plt_name(Prefix) -> + Prefix ++ "_" ++ rebar_utils:otp_release() ++ "_plt". do(State, Plt) -> Output = get_output_file(State), @@ -138,21 +165,17 @@ update_proj_plt(State, Plt, Output) -> do_update_proj_plt(State, Plt, Output) -> ?INFO("Updating plt...", []), - {Files, Warnings} = proj_plt_files(State), - Warnings2 = format_warnings(Output, Warnings), - {Warnings3, State2} = case read_plt(State, Plt) of - {ok, OldFiles} -> - check_plt(State, Plt, Output, OldFiles, - Files); - {error, no_such_file} -> - build_proj_plt(State, Plt, Output, Files) - end, - {Warnings2 + Warnings3, State2}. + Files = proj_plt_files(State), + case read_plt(State, Plt) of + {ok, OldFiles} -> + check_plt(State, Plt, Output, OldFiles, Files); + {error, no_such_file} -> + build_proj_plt(State, Plt, Output, Files) + end. proj_plt_files(State) -> - BasePltApps = rebar_state:get(State, dialyzer_base_plt_apps, - default_plt_apps()), - PltApps = rebar_state:get(State, dialyzer_plt_apps, []), + BasePltApps = get_config(State, base_plt_apps, default_plt_apps()), + PltApps = get_config(State, plt_extra_apps, []), Apps = rebar_state:project_apps(State), DepApps = lists:flatmap(fun rebar_app_info:applications/1, Apps), get_plt_files(BasePltApps ++ PltApps ++ DepApps, Apps). @@ -165,23 +188,18 @@ default_plt_apps() -> get_plt_files(DepApps, Apps) -> ?INFO("Resolving files...", []), - get_plt_files(DepApps, Apps, [], [], []). + get_plt_files(DepApps, Apps, [], []). -get_plt_files([], _, _, Files, Warnings) -> - {Files, Warnings}; -get_plt_files([AppName | DepApps], Apps, PltApps, Files, Warnings) -> +get_plt_files([], _, _, Files) -> + Files; +get_plt_files([AppName | DepApps], Apps, PltApps, Files) -> case lists:member(AppName, PltApps) orelse app_member(AppName, Apps) of true -> - get_plt_files(DepApps, Apps, PltApps, Files, Warnings); + get_plt_files(DepApps, Apps, PltApps, Files); false -> - {DepApps2, Files2, Warnings2} = app_name_to_info(AppName), - ?DEBUG("~s dependencies: ~p", [AppName, DepApps2]), + Files2 = app_files(AppName), ?DEBUG("~s files: ~p", [AppName, Files2]), - DepApps3 = DepApps2 ++ DepApps, - PltApps2 = [AppName | PltApps], - Files3 = Files2 ++ Files, - Warnings3 = Warnings2 ++ Warnings, - get_plt_files(DepApps3, Apps, PltApps2, Files3, Warnings3) + get_plt_files(DepApps, Apps, [AppName | PltApps], Files2 ++ Files) end. app_member(AppName, Apps) -> @@ -192,71 +210,28 @@ app_member(AppName, Apps) -> false end. -app_name_to_info(AppName) -> - case app_name_to_ebin(AppName) of - {error, _} -> - {[], [], [{unknown_application, {"", 0}, [AppName]}]}; - EbinDir -> - ebin_to_info(EbinDir, AppName) +app_files(AppName) -> + case app_ebin(AppName) of + {ok, EbinDir} -> + ebin_files(EbinDir); + {error, bad_name} -> + throw({unknown_application, AppName}) end. -app_name_to_ebin(AppName) -> +app_ebin(AppName) -> case code:lib_dir(AppName, ebin) of - {error, bad_name} -> - search_ebin(AppName); + {error, bad_name} = Error -> + Error; EbinDir -> - check_ebin(EbinDir, AppName) + check_ebin(EbinDir) end. -check_ebin(EbinDir, AppName) -> +check_ebin(EbinDir) -> case filelib:is_dir(EbinDir) of true -> - EbinDir; - false -> - search_ebin(AppName) - end. - -search_ebin(AppName) -> - case code:where_is_file(atom_to_list(AppName) ++ ".app") of - non_existing -> - {error, bad_name}; - AppFile -> - filename:dirname(AppFile) - end. - -ebin_to_info(EbinDir, AppName) -> - AppFile = filename:join(EbinDir, atom_to_list(AppName) ++ ".app"), - ?DEBUG("Consulting app file ~p", [AppFile]), - case file:consult(AppFile) of - {ok, [{application, AppName, AppDetails}]} -> - DepApps = proplists:get_value(applications, AppDetails, []), - IncApps = proplists:get_value(included_applications, AppDetails, - []), - Modules = proplists:get_value(modules, AppDetails, []), - {Files, Warnings} = modules_to_files(Modules, EbinDir), - {IncApps ++ DepApps, Files, Warnings}; - {error, enoent} when AppName =:= erts -> - {[], ebin_files(EbinDir), []}; - _ -> - Error = io_lib:format("Could not parse ~p", [AppFile]), - throw({dialyzer_error, Error}) - end. - -modules_to_files(Modules, EbinDir) -> - Ext = code:objfile_extension(), - Result = [module_to_file(Module, EbinDir, Ext) || Module <- Modules], - Files = [File || {_, File} <- Result, File =/= unknown], - Warnings = [{unknown_module, {"", 0}, [Module]} || - {Module, unknown} <- Result], - {Files, Warnings}. - -module_to_file(Module, EbinDir, Ext) -> - File = filename:join(EbinDir, atom_to_list(Module) ++ Ext), - case filelib:is_file(File) of - true -> - {Module, File}; + {ok, EbinDir}; false -> - {Module, unknown} + {error, bad_name} end. ebin_files(EbinDir) -> @@ -306,43 +281,46 @@ add_plt(State, Plt, Output, Files) -> run_plt(State, Plt, Output, plt_add, Files). run_plt(State, Plt, Output, Analysis, Files) -> - GetWarnings = rebar_state:get(State, dialyzer_plt_warnings, false), + GetWarnings = get_config(State, get_warnings, false), Opts = [{analysis_type, Analysis}, {get_warnings, GetWarnings}, {init_plt, Plt}, + {output_plt, Plt}, {from, byte_code}, {files, Files}], run_dialyzer(State, Opts, Output). build_proj_plt(State, Plt, Output, Files) -> - BasePlt = get_base_plt_location(State), + BasePlt = get_base_plt(State), ?INFO("Updating base plt...", []), - {BaseFiles, BaseWarnings} = base_plt_files(State), - BaseWarnings2 = format_warnings(Output, BaseWarnings), - {BaseWarnings3, State1} = update_base_plt(State, BasePlt, Output, - BaseFiles), + BaseFiles = base_plt_files(State), + {BaseWarnings, State1} = update_base_plt(State, BasePlt, Output, BaseFiles), ?INFO("Copying ~p to ~p...", [BasePlt, Plt]), _ = filelib:ensure_dir(Plt), case file:copy(BasePlt, Plt) of {ok, _} -> {CheckWarnings, State2} = check_plt(State1, Plt, Output, BaseFiles, Files), - {BaseWarnings2 + BaseWarnings3 + CheckWarnings, State2}; + {BaseWarnings + CheckWarnings, State2}; {error, Reason} -> Error = io_lib:format("Could not copy PLT from ~p to ~p: ~p", [BasePlt, Plt, file:format_error(Reason)]), throw({dialyzer_error, Error}) end. -get_base_plt_location(State) -> - GlobalCacheDir = rebar_dir:global_cache_dir(State), - BaseDir = rebar_state:get(State, dialyzer_base_plt_dir, GlobalCacheDir), - BasePlt = rebar_state:get(State, dialyzer_base_plt, default_plt()), - filename:join(BaseDir, BasePlt). +get_base_plt(State) -> + Prefix = get_config(State, base_plt_prefix, ?PLT_PREFIX), + Name = plt_name(Prefix), + case get_config(State, base_plt_location, global) of + global -> + GlobalCacheDir = rebar_dir:global_cache_dir(State), + filename:join(GlobalCacheDir, Name); + Dir -> + filename:join(Dir, Name) + end. base_plt_files(State) -> - BasePltApps = rebar_state:get(State, dialyzer_base_plt_apps, - default_plt_apps()), + BasePltApps = get_config(State, base_plt_apps, default_plt_apps()), Apps = rebar_state:project_apps(State), get_plt_files(BasePltApps, Apps). @@ -357,7 +335,7 @@ update_base_plt(State, BasePlt, Output, BaseFiles) -> build_plt(State, Plt, Output, Files) -> ?INFO("Adding ~b files to ~p...", [length(Files), Plt]), - GetWarnings = rebar_state:get(State, dialyzer_plt_warnings, false), + GetWarnings = get_config(State, get_warnings, false), Opts = [{analysis_type, plt_build}, {get_warnings, GetWarnings}, {output_plt, Plt}, @@ -376,36 +354,29 @@ succ_typings(State, Plt, Output) -> succ_typings(State, Plt, Output, Apps) -> ?INFO("Doing success typing analysis...", []), - {Files, Warnings} = apps_to_files(Apps), - Warnings2 = format_warnings(Output, Warnings), + Files = apps_to_files(Apps), ?INFO("Analyzing ~b files with ~p...", [length(Files), Plt]), Opts = [{analysis_type, succ_typings}, {get_warnings, true}, {from, byte_code}, {files, Files}, {init_plt, Plt}], - {Warnings3, State2} = run_dialyzer(State, Opts, Output), - {Warnings2 + Warnings3, State2}. + run_dialyzer(State, Opts, Output). apps_to_files(Apps) -> ?INFO("Resolving files...", []), - Result = [{Files, Warnings} || - App <- Apps, - {Files, Warnings} <- [app_to_files(App)]], - Files = [File || {Files, _} <- Result, File <- Files], - Warnings = [Warning || {_, Warnings} <- Result, Warning <- Warnings], - {Files, Warnings}. + [File || App <- Apps, + File <- app_to_files(App)]. app_to_files(App) -> AppName = ec_cnv:to_atom(rebar_app_info:name(App)), - {_, Files, Warnings} = app_name_to_info(AppName), - {Files, Warnings}. + app_files(AppName). run_dialyzer(State, Opts, Output) -> %% dialyzer may return callgraph warnings when get_warnings is false case proplists:get_bool(get_warnings, Opts) of true -> - WarningsList = rebar_state:get(State, dialyzer_warnings, []), + WarningsList = get_config(State, warnings, []), Opts2 = [{warnings, WarningsList}, {check_plt, false} | Opts], @@ -417,7 +388,7 @@ run_dialyzer(State, Opts, Output) -> {check_plt, false} | Opts], ?DEBUG("Running dialyzer with options: ~p~n", [Opts2]), - _ = dialyzer:run(Opts2), + dialyzer:run(Opts2), {0, State} end. @@ -430,10 +401,6 @@ format_warnings(Output, Warnings) -> format_warnings(Warnings) -> [format_warning(Warning) || Warning <- Warnings]. -format_warning({unknown_application, _, [AppName]}) -> - io_lib:format("Unknown application: ~s", [AppName]); -format_warning({unknown_module, _, [Module]}) -> - io_lib:format("Unknown module: ~s", [Module]); format_warning(Warning) -> case strip(dialyzer:format_warning(Warning, fullpath)) of ":0: " ++ Unknown -> @@ -471,3 +438,7 @@ no_warnings() -> no_contracts, no_behaviours, no_undefined_callbacks]. + +get_config(State, Key, Default) -> + Config = rebar_state:get(State, dialyzer, []), + proplists:get_value(Key, Config, Default). |