You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

477 lines
17 KiB

  1. %% -*- erlang-indent-level: 4;indent-tabs-mode: nil -*-
  2. %% ex: ts=4 sw=4 et
  3. -module(rebar_prv_dialyzer).
  4. -behaviour(provider).
  5. -export([init/1,
  6. do/1,
  7. format_error/1]).
  8. -include("rebar.hrl").
  9. -include_lib("providers/include/providers.hrl").
  10. -define(PROVIDER, dialyzer).
  11. -define(DEPS, [compile]).
  12. -define(PLT_PREFIX, "rebar3").
  13. %% ===================================================================
  14. %% Public API
  15. %% ===================================================================
  16. -spec init(rebar_state:t()) -> {ok, rebar_state:t()}.
  17. init(State) ->
  18. Opts = [{update_plt, $u, "update-plt", boolean, "Enable updating the PLT. Default: true"},
  19. {succ_typings, $s, "succ-typings", boolean, "Enable success typing analysis. Default: true"}],
  20. State1 = rebar_state:add_provider(State, providers:create([{name, ?PROVIDER},
  21. {module, ?MODULE},
  22. {bare, true},
  23. {deps, ?DEPS},
  24. {example, "rebar3 dialyzer"},
  25. {short_desc, short_desc()},
  26. {desc, desc()},
  27. {opts, Opts}])),
  28. {ok, State1}.
  29. desc() ->
  30. short_desc() ++ "\n"
  31. "\n"
  32. "This command will build, and keep up-to-date, a suitable PLT and will use "
  33. "it to carry out success typing analysis on the current project.\n"
  34. "\n"
  35. "The following (optional) configurations can be added to a `proplist` of "
  36. "options `dialyzer` in rebar.config:\n"
  37. "`warnings` - a list of dialyzer warnings\n"
  38. "`get_warnings` - display warnings when altering a PLT file (boolean)\n"
  39. "`plt_apps` - the strategy for determining the applications which included "
  40. "in the PLT file, `top_level_deps` to include just the direct dependencies "
  41. "or `all_deps` to include all nested dependencies*\n"
  42. "`plt_extra_apps` - a list of applications to include in the PLT file**\n"
  43. "`plt_location` - the location of the PLT file, `local` to store in the "
  44. "profile's base directory (default) or a custom directory.\n"
  45. "`plt_prefix` - the prefix to the PLT file, defaults to \"rebar3\"***\n"
  46. "`base_plt_apps` - a list of applications to include in the base "
  47. "PLT file****\n"
  48. "`base_plt_location` - the location of base PLT file, `global` to store in "
  49. "$HOME/.cache/rebar3 (default) or a custom directory****\n"
  50. "`base_plt_prefix` - the prefix to the base PLT file, defaults to "
  51. "\"rebar3\"*** ****\n"
  52. "\n"
  53. "For example, to warn on unmatched returns: \n"
  54. "{dialyzer, [{warnings, [unmatched_returns]}]}.\n"
  55. "\n"
  56. "*The direct dependent applications are listed in `applications` and "
  57. "`included_applications` of their .app files.\n"
  58. "**The applications in `base_plt_apps` will be added to the "
  59. "list. \n"
  60. "***PLT files are named \"<prefix>_<otp_release>_plt\".\n"
  61. "****The base PLT is a PLT containing the core applications often required "
  62. "for a project's PLT. One base PLT is created per OTP version and "
  63. "stored in `base_plt_location`. A base PLT is used to build project PLTs."
  64. "\n".
  65. short_desc() ->
  66. "Run the Dialyzer analyzer on the project.".
  67. -spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}.
  68. do(State) ->
  69. maybe_fix_env(),
  70. ?INFO("Dialyzer starting, this may take a while...", []),
  71. code:add_pathsa(rebar_state:code_paths(State, all_deps)),
  72. Plt = get_plt(State),
  73. try
  74. do(State, Plt)
  75. catch
  76. throw:{dialyzer_error, Error} ->
  77. ?PRV_ERROR({error_processing_apps, Error});
  78. throw:{dialyzer_warnings, Warnings} ->
  79. ?PRV_ERROR({dialyzer_warnings, Warnings});
  80. throw:{unknown_application, _} = Error ->
  81. ?PRV_ERROR(Error);
  82. throw:{output_file_error, _, _} = Error ->
  83. ?PRV_ERROR(Error)
  84. after
  85. rebar_utils:cleanup_code_path(rebar_state:code_paths(State, default))
  86. end.
  87. %% This is used to workaround dialyzer quirk discussed here
  88. %% https://github.com/rebar/rebar3/pull/489#issuecomment-107953541
  89. %% Dialyzer gets default plt location wrong way by peeking HOME environment
  90. %% variable which usually is not defined on Windows.
  91. maybe_fix_env() ->
  92. os:putenv("DIALYZER_PLT", filename:join(rebar_dir:home_dir(), ".dialyzer_plt")).
  93. -spec format_error(any()) -> iolist().
  94. format_error({error_processing_apps, Error}) ->
  95. io_lib:format("Error in dialyzing apps: ~s", [Error]);
  96. format_error({dialyzer_warnings, Warnings}) ->
  97. io_lib:format("Warnings occured running dialyzer: ~b", [Warnings]);
  98. format_error({unknown_application, App}) ->
  99. io_lib:format("Could not find application: ~s", [App]);
  100. format_error({output_file_error, File, Error}) ->
  101. Error1 = file:format_error(Error),
  102. io_lib:format("Failed to write to ~s: ~s", [File, Error1]);
  103. format_error(Reason) ->
  104. io_lib:format("~p", [Reason]).
  105. %% Internal functions
  106. get_plt(State) ->
  107. Prefix = get_config(State, plt_prefix, ?PLT_PREFIX),
  108. Name = plt_name(Prefix),
  109. case get_config(State, plt_location, local) of
  110. local ->
  111. BaseDir = rebar_dir:base_dir(State),
  112. filename:join(BaseDir, Name);
  113. Dir ->
  114. filename:join(Dir, Name)
  115. end.
  116. plt_name(Prefix) ->
  117. Prefix ++ "_" ++ rebar_utils:otp_release() ++ "_plt".
  118. do(State, Plt) ->
  119. Output = get_output_file(State),
  120. {PltWarnings, State1} = update_proj_plt(State, Plt, Output),
  121. {Warnings, State2} = succ_typings(State1, Plt, Output),
  122. case PltWarnings + Warnings of
  123. 0 ->
  124. {ok, State2};
  125. TotalWarnings ->
  126. ?INFO("Warnings written to ~s", [Output]),
  127. throw({dialyzer_warnings, TotalWarnings})
  128. end.
  129. get_output_file(State) ->
  130. BaseDir = rebar_dir:base_dir(State),
  131. Output = filename:join(BaseDir, default_output_file()),
  132. case file:open(Output, [write]) of
  133. {ok, File} ->
  134. ok = file:close(File),
  135. Output;
  136. {error, Reason} ->
  137. throw({output_file_error, Output, Reason})
  138. end.
  139. default_output_file() ->
  140. rebar_utils:otp_release() ++ ".dialyzer_warnings".
  141. update_proj_plt(State, Plt, Output) ->
  142. {Args, _} = rebar_state:command_parsed_args(State),
  143. case proplists:get_value(update_plt, Args) of
  144. false ->
  145. {0, State};
  146. _ ->
  147. do_update_proj_plt(State, Plt, Output)
  148. end.
  149. do_update_proj_plt(State, Plt, Output) ->
  150. ?INFO("Updating plt...", []),
  151. Files = proj_plt_files(State),
  152. case read_plt(State, Plt) of
  153. {ok, OldFiles} ->
  154. check_plt(State, Plt, Output, OldFiles, Files);
  155. error ->
  156. build_proj_plt(State, Plt, Output, Files)
  157. end.
  158. proj_plt_files(State) ->
  159. BasePltApps = get_config(State, base_plt_apps, default_plt_apps()),
  160. PltApps = get_config(State, plt_extra_apps, []),
  161. Apps = rebar_state:project_apps(State),
  162. DepApps = lists:flatmap(fun rebar_app_info:applications/1, Apps),
  163. DepApps1 =
  164. case get_config(State, plt_apps, top_level_deps) of
  165. top_level_deps -> DepApps;
  166. all_deps -> collect_nested_dependent_apps(DepApps)
  167. end,
  168. get_plt_files(BasePltApps ++ PltApps ++ DepApps1, Apps).
  169. default_plt_apps() ->
  170. [erts,
  171. crypto,
  172. kernel,
  173. stdlib].
  174. get_plt_files(DepApps, Apps) ->
  175. ?INFO("Resolving files...", []),
  176. get_plt_files(DepApps, Apps, [], []).
  177. get_plt_files([], _, _, Files) ->
  178. Files;
  179. get_plt_files([AppName | DepApps], Apps, PltApps, Files) ->
  180. case lists:member(AppName, PltApps) orelse app_member(AppName, Apps) of
  181. true ->
  182. get_plt_files(DepApps, Apps, PltApps, Files);
  183. false ->
  184. Files2 = app_files(AppName),
  185. ?DEBUG("~s files: ~p", [AppName, Files2]),
  186. get_plt_files(DepApps, Apps, [AppName | PltApps], Files2 ++ Files)
  187. end.
  188. app_member(AppName, Apps) ->
  189. case rebar_app_utils:find(ec_cnv:to_binary(AppName), Apps) of
  190. {ok, _App} ->
  191. true;
  192. error ->
  193. false
  194. end.
  195. app_files(AppName) ->
  196. case app_ebin(AppName) of
  197. {ok, EbinDir} ->
  198. ebin_files(EbinDir);
  199. {error, bad_name} ->
  200. throw({unknown_application, AppName})
  201. end.
  202. app_ebin(AppName) ->
  203. case code:lib_dir(AppName, ebin) of
  204. {error, bad_name} = Error ->
  205. Error;
  206. EbinDir ->
  207. check_ebin(EbinDir)
  208. end.
  209. check_ebin(EbinDir) ->
  210. case filelib:is_dir(EbinDir) of
  211. true ->
  212. {ok, EbinDir};
  213. false ->
  214. {error, bad_name}
  215. end.
  216. ebin_files(EbinDir) ->
  217. Wildcard = "*" ++ code:objfile_extension(),
  218. [filename:join(EbinDir, File) ||
  219. File <- filelib:wildcard(Wildcard, EbinDir)].
  220. read_plt(_State, Plt) ->
  221. case dialyzer:plt_info(Plt) of
  222. {ok, Info} ->
  223. Files = proplists:get_value(files, Info, []),
  224. read_plt_files(Plt, Files);
  225. {error, no_such_file} ->
  226. error;
  227. {error, read_error} ->
  228. Error = io_lib:format("Could not read the PLT file ~p", [Plt]),
  229. throw({dialyzer_error, Error})
  230. end.
  231. %% If any file no longer exists dialyzer will fail when updating the PLT.
  232. read_plt_files(Plt, Files) ->
  233. case [File || File <- Files, not filelib:is_file(File)] of
  234. [] ->
  235. {ok, Files};
  236. Missing ->
  237. ?INFO("Could not find ~p files in ~p...", [length(Missing), Plt]),
  238. ?DEBUG("Could not find files: ~p", [Missing]),
  239. error
  240. end.
  241. check_plt(State, Plt, Output, OldList, FilesList) ->
  242. Old = sets:from_list(OldList),
  243. Files = sets:from_list(FilesList),
  244. Remove = sets:to_list(sets:subtract(Old, Files)),
  245. {RemWarnings, State1} = remove_plt(State, Plt, Output, Remove),
  246. Check = sets:to_list(sets:intersection(Files, Old)),
  247. {CheckWarnings, State2} = check_plt(State1, Plt, Output, Check),
  248. Add = sets:to_list(sets:subtract(Files, Old)),
  249. {AddWarnings, State3} = add_plt(State2, Plt, Output, Add),
  250. {RemWarnings + CheckWarnings + AddWarnings, State3}.
  251. remove_plt(State, _Plt, _Output, []) ->
  252. {0, State};
  253. remove_plt(State, Plt, Output, Files) ->
  254. ?INFO("Removing ~b files from ~p...", [length(Files), Plt]),
  255. run_plt(State, Plt, Output, plt_remove, Files).
  256. check_plt(State, _Plt, _Output, []) ->
  257. {0, State};
  258. check_plt(State, Plt, Output, Files) ->
  259. ?INFO("Checking ~b files in ~p...", [length(Files), Plt]),
  260. run_plt(State, Plt, Output, plt_check, Files).
  261. add_plt(State, _Plt, _Output, []) ->
  262. {0, State};
  263. add_plt(State, Plt, Output, Files) ->
  264. ?INFO("Adding ~b files to ~p...", [length(Files), Plt]),
  265. run_plt(State, Plt, Output, plt_add, Files).
  266. run_plt(State, Plt, Output, Analysis, Files) ->
  267. GetWarnings = get_config(State, get_warnings, false),
  268. Opts = [{analysis_type, Analysis},
  269. {get_warnings, GetWarnings},
  270. {init_plt, Plt},
  271. {output_plt, Plt},
  272. {from, byte_code},
  273. {files, Files}],
  274. run_dialyzer(State, Opts, Output).
  275. build_proj_plt(State, Plt, Output, Files) ->
  276. BasePlt = get_base_plt(State),
  277. ?INFO("Updating base plt...", []),
  278. BaseFiles = base_plt_files(State),
  279. {BaseWarnings, State1} = update_base_plt(State, BasePlt, Output, BaseFiles),
  280. ?INFO("Copying ~p to ~p...", [BasePlt, Plt]),
  281. _ = filelib:ensure_dir(Plt),
  282. case file:copy(BasePlt, Plt) of
  283. {ok, _} ->
  284. {CheckWarnings, State2} = check_plt(State1, Plt, Output, BaseFiles,
  285. Files),
  286. {BaseWarnings + CheckWarnings, State2};
  287. {error, Reason} ->
  288. Error = io_lib:format("Could not copy PLT from ~p to ~p: ~p",
  289. [BasePlt, Plt, file:format_error(Reason)]),
  290. throw({dialyzer_error, Error})
  291. end.
  292. get_base_plt(State) ->
  293. Prefix = get_config(State, base_plt_prefix, ?PLT_PREFIX),
  294. Name = plt_name(Prefix),
  295. case get_config(State, base_plt_location, global) of
  296. global ->
  297. GlobalCacheDir = rebar_dir:global_cache_dir(rebar_state:opts(State)),
  298. filename:join(GlobalCacheDir, Name);
  299. Dir ->
  300. filename:join(Dir, Name)
  301. end.
  302. base_plt_files(State) ->
  303. BasePltApps = get_config(State, base_plt_apps, default_plt_apps()),
  304. Apps = rebar_state:project_apps(State),
  305. get_plt_files(BasePltApps, Apps).
  306. update_base_plt(State, BasePlt, Output, BaseFiles) ->
  307. case read_plt(State, BasePlt) of
  308. {ok, OldBaseFiles} ->
  309. check_plt(State, BasePlt, Output, OldBaseFiles, BaseFiles);
  310. error ->
  311. _ = filelib:ensure_dir(BasePlt),
  312. build_plt(State, BasePlt, Output, BaseFiles)
  313. end.
  314. build_plt(State, Plt, Output, Files) ->
  315. ?INFO("Adding ~b files to ~p...", [length(Files), Plt]),
  316. GetWarnings = get_config(State, get_warnings, false),
  317. Opts = [{analysis_type, plt_build},
  318. {get_warnings, GetWarnings},
  319. {output_plt, Plt},
  320. {files, Files}],
  321. run_dialyzer(State, Opts, Output).
  322. succ_typings(State, Plt, Output) ->
  323. {Args, _} = rebar_state:command_parsed_args(State),
  324. case proplists:get_value(succ_typings, Args) of
  325. false ->
  326. {0, State};
  327. _ ->
  328. Apps = rebar_state:project_apps(State),
  329. succ_typings(State, Plt, Output, Apps)
  330. end.
  331. succ_typings(State, Plt, Output, Apps) ->
  332. ?INFO("Doing success typing analysis...", []),
  333. Files = apps_to_files(Apps),
  334. ?INFO("Analyzing ~b files with ~p...", [length(Files), Plt]),
  335. Opts = [{analysis_type, succ_typings},
  336. {get_warnings, true},
  337. {from, byte_code},
  338. {files, Files},
  339. {init_plt, Plt}],
  340. run_dialyzer(State, Opts, Output).
  341. apps_to_files(Apps) ->
  342. ?INFO("Resolving files...", []),
  343. [File || App <- Apps,
  344. File <- app_to_files(App)].
  345. app_to_files(App) ->
  346. AppName = ec_cnv:to_atom(rebar_app_info:name(App)),
  347. app_files(AppName).
  348. run_dialyzer(State, Opts, Output) ->
  349. %% dialyzer may return callgraph warnings when get_warnings is false
  350. case proplists:get_bool(get_warnings, Opts) of
  351. true ->
  352. WarningsList = get_config(State, warnings, []),
  353. Opts2 = [{warnings, WarningsList},
  354. {check_plt, false} |
  355. Opts],
  356. ?DEBUG("Running dialyzer with options: ~p~n", [Opts2]),
  357. Warnings = format_warnings(Output, dialyzer:run(Opts2)),
  358. {Warnings, State};
  359. false ->
  360. Opts2 = [{warnings, no_warnings()},
  361. {check_plt, false} |
  362. Opts],
  363. ?DEBUG("Running dialyzer with options: ~p~n", [Opts2]),
  364. dialyzer:run(Opts2),
  365. {0, State}
  366. end.
  367. format_warnings(Output, Warnings) ->
  368. Warnings1 = rebar_dialyzer_format:format_warnings(Warnings),
  369. console_warnings(Warnings1),
  370. file_warnings(Output, Warnings),
  371. length(Warnings).
  372. console_warnings(Warnings) ->
  373. _ = [?CONSOLE("~s", [Warning]) || Warning <- Warnings],
  374. ok.
  375. file_warnings(_, []) ->
  376. ok;
  377. file_warnings(Output, Warnings) ->
  378. Warnings1 = [[dialyzer:format_warning(Warning, fullpath), $\n] || Warning <- Warnings],
  379. case file:write_file(Output, Warnings1, [append]) of
  380. ok ->
  381. ok;
  382. {error, Reason} ->
  383. throw({output_file_error, Output, Reason})
  384. end.
  385. no_warnings() ->
  386. [no_return,
  387. no_unused,
  388. no_improper_lists,
  389. no_fun_app,
  390. no_match,
  391. no_opaque,
  392. no_fail_call,
  393. no_contracts,
  394. no_behaviours,
  395. no_undefined_callbacks].
  396. get_config(State, Key, Default) ->
  397. Config = rebar_state:get(State, dialyzer, []),
  398. proplists:get_value(Key, Config, Default).
  399. -spec collect_nested_dependent_apps([atom()]) -> [atom()].
  400. collect_nested_dependent_apps(RootApps) ->
  401. Deps = lists:foldl(fun collect_nested_dependent_apps/2, sets:new(), RootApps),
  402. sets:to_list(Deps).
  403. -spec collect_nested_dependent_apps(atom(), rebar_set()) -> rebar_set().
  404. collect_nested_dependent_apps(App, Seen) ->
  405. case sets:is_element(App, Seen) of
  406. true ->
  407. Seen;
  408. false ->
  409. Seen1 = sets:add_element(App, Seen),
  410. case code:lib_dir(App) of
  411. {error, _} ->
  412. throw({unknown_application, App});
  413. AppDir ->
  414. case rebar_app_discover:find_app(AppDir, all) of
  415. false ->
  416. throw({unknown_application, App});
  417. {true, AppInfo} ->
  418. lists:foldl(fun collect_nested_dependent_apps/2,
  419. Seen1,
  420. rebar_app_info:applications(AppInfo))
  421. end
  422. end
  423. end.