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 regels
17 KiB

10 jaren geleden
10 jaren geleden
10 jaren geleden
10 jaren geleden
10 jaren geleden
10 jaren geleden
10 jaren geleden
10 jaren geleden
10 jaren geleden
10 jaren geleden
10 jaren geleden
10 jaren geleden
10 jaren geleden
10 jaren geleden
10 jaren geleden
9 jaren geleden
  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.