Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

415 рядки
14 KiB

10 роки тому
10 роки тому
10 роки тому
10 роки тому
10 роки тому
10 роки тому
10 роки тому
10 роки тому
10 роки тому
10 роки тому
10 роки тому
10 роки тому
10 роки тому
10 роки тому
10 роки тому
10 роки тому
10 роки тому
10 роки тому
10 роки тому
  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. %% ===================================================================
  13. %% Public API
  14. %% ===================================================================
  15. -spec init(rebar_state:t()) -> {ok, rebar_state:t()}.
  16. init(State) ->
  17. Opts = [{update_plt, $u, "update-plt", boolean, "Enable updating the PLT. Default: true"},
  18. {succ_typings, $s, "succ-typings", boolean, "Enable success typing analysis. Default: true"}],
  19. State1 = rebar_state:add_provider(State, providers:create([{name, ?PROVIDER},
  20. {module, ?MODULE},
  21. {bare, false},
  22. {deps, ?DEPS},
  23. {example, "rebar3 dialyzer"},
  24. {short_desc, short_desc()},
  25. {desc, desc()},
  26. {opts, Opts}])),
  27. {ok, State1}.
  28. desc() ->
  29. short_desc() ++ "\n"
  30. "\n"
  31. "This command will build, and keep up-to-date, a suitable PLT and will use "
  32. "it to carry out success typing analysis on the current project.\n"
  33. "\n"
  34. "The following (optional) configurations can be added to a rebar.config:\n"
  35. "`dialyzer_warnings` - a list of dialyzer warnings\n"
  36. "`dialyzer_plt` - the PLT file to use\n"
  37. "`dialyzer_plt_apps` - a list of applications to include in the PLT file*\n"
  38. "`dialyzer_plt_warnings` - display warnings when updating a PLT file "
  39. "(boolean)\n"
  40. "`dialyzer_base_plt` - the base PLT file to use**\n"
  41. "`dialyzer_base_plt_dir` - the base PLT directory**\n"
  42. "`dialyzer_base_plt_apps` - a list of applications to include in the base "
  43. "PLT file**\n"
  44. "\n"
  45. "*The applications in `dialyzer_base_plt_apps` and any `applications` and "
  46. "`included_applications` listed in their .app files will be added to the "
  47. "list.\n"
  48. "**The base PLT is a PLT containing the core OTP applications often "
  49. "required for a project's PLT. One base PLT is created per OTP version and "
  50. "stored in `dialyzer_base_plt_dir` (defaults to $HOME/.rebar3/). A base "
  51. "PLT is used to create a project's initial PLT.".
  52. short_desc() ->
  53. "Run the Dialyzer analyzer on the project.".
  54. -spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}.
  55. do(State) ->
  56. ?INFO("Dialyzer starting, this may take a while...", []),
  57. code:add_pathsa(rebar_state:code_paths(State, all_deps)),
  58. Plt = get_plt_location(State),
  59. Apps = rebar_state:project_apps(State),
  60. try
  61. do(State, Plt, Apps)
  62. catch
  63. throw:{dialyzer_error, Error} ->
  64. ?PRV_ERROR({error_processing_apps, Error});
  65. throw:{dialyzer_warnings, Warnings} ->
  66. ?PRV_ERROR({dialyzer_warnings, Warnings})
  67. after
  68. rebar_utils:cleanup_code_path(rebar_state:code_paths(State, default))
  69. end.
  70. -spec format_error(any()) -> iolist().
  71. format_error({error_processing_apps, Error}) ->
  72. io_lib:format("Error in dialyzing apps: ~s", [Error]);
  73. format_error({dialyzer_warnings, Warnings}) ->
  74. io_lib:format("Warnings occured running dialyzer: ~b", [Warnings]);
  75. format_error(Reason) ->
  76. io_lib:format("~p", [Reason]).
  77. %% Internal functions
  78. get_plt_location(State) ->
  79. BaseDir = rebar_dir:base_dir(State),
  80. DefaultPlt = filename:join(BaseDir, default_plt()),
  81. rebar_state:get(State, dialyzer_plt, DefaultPlt).
  82. default_plt() ->
  83. rebar_utils:otp_release() ++ ".plt".
  84. do(State, Plt, Apps) ->
  85. {PltWarnings, State1} = update_proj_plt(State, Plt, Apps),
  86. {Warnings, State2} = succ_typings(State1, Plt, Apps),
  87. case PltWarnings + Warnings of
  88. 0 ->
  89. {ok, State2};
  90. TotalWarnings ->
  91. throw({dialyzer_warnings, TotalWarnings})
  92. end.
  93. update_proj_plt(State, Plt, Apps) ->
  94. {Args, _} = rebar_state:command_parsed_args(State),
  95. case proplists:get_value(update_plt, Args) of
  96. false ->
  97. {0, State};
  98. _ ->
  99. do_update_proj_plt(State, Plt, Apps)
  100. end.
  101. do_update_proj_plt(State, Plt, Apps) ->
  102. ?INFO("Updating plt...", []),
  103. Files = get_plt_files(State, Apps),
  104. case read_plt(State, Plt) of
  105. {ok, OldFiles} ->
  106. check_plt(State, Plt, OldFiles, Files);
  107. {error, no_such_file} ->
  108. build_proj_plt(State, Plt, Files)
  109. end.
  110. get_plt_files(State, Apps) ->
  111. BasePltApps = rebar_state:get(State, dialyzer_base_plt_apps,
  112. default_plt_apps()),
  113. PltApps = rebar_state:get(State, dialyzer_plt_apps, []),
  114. DepApps = lists:flatmap(fun rebar_app_info:applications/1, Apps),
  115. get_plt_files(BasePltApps ++ PltApps ++ DepApps, Apps, [], []).
  116. default_plt_apps() ->
  117. [erts,
  118. crypto,
  119. kernel,
  120. stdlib].
  121. get_plt_files([], _, _, Files) ->
  122. Files;
  123. get_plt_files([AppName | DepApps], Apps, PltApps, Files) ->
  124. case lists:member(AppName, PltApps) orelse app_member(AppName, Apps) of
  125. true ->
  126. get_plt_files(DepApps, Apps, PltApps, Files);
  127. false ->
  128. {DepApps2, Files2} = app_name_to_info(AppName),
  129. ?DEBUG("~s dependencies: ~p", [AppName, DepApps2]),
  130. ?DEBUG("~s files: ~p", [AppName, Files2]),
  131. DepApps3 = DepApps2 ++ DepApps,
  132. Files3 = Files2 ++ Files,
  133. get_plt_files(DepApps3, Apps, [AppName | PltApps], Files3)
  134. end.
  135. app_member(AppName, Apps) ->
  136. case rebar_app_utils:find(ec_cnv:to_binary(AppName), Apps) of
  137. {ok, _App} ->
  138. true;
  139. error ->
  140. false
  141. end.
  142. app_name_to_info(AppName) ->
  143. case app_name_to_ebin(AppName) of
  144. {error, _} ->
  145. ?CONSOLE("Unknown application ~s", [AppName]),
  146. {[], []};
  147. EbinDir ->
  148. ebin_to_info(EbinDir, AppName)
  149. end.
  150. app_name_to_ebin(AppName) ->
  151. case code:lib_dir(AppName, ebin) of
  152. {error, bad_name} ->
  153. search_ebin(AppName);
  154. EbinDir ->
  155. check_ebin(EbinDir, AppName)
  156. end.
  157. check_ebin(EbinDir, AppName) ->
  158. case filelib:is_dir(EbinDir) of
  159. true ->
  160. EbinDir;
  161. false ->
  162. search_ebin(AppName)
  163. end.
  164. search_ebin(AppName) ->
  165. case code:where_is_file(atom_to_list(AppName) ++ ".app") of
  166. non_existing ->
  167. {error, bad_name};
  168. AppFile ->
  169. filename:dirname(AppFile)
  170. end.
  171. ebin_to_info(EbinDir, AppName) ->
  172. AppFile = filename:join(EbinDir, atom_to_list(AppName) ++ ".app"),
  173. ?DEBUG("Consulting app file ~p", [AppFile]),
  174. case file:consult(AppFile) of
  175. {ok, [{application, AppName, AppDetails}]} ->
  176. DepApps = proplists:get_value(applications, AppDetails, []),
  177. IncApps = proplists:get_value(included_applications, AppDetails,
  178. []),
  179. Modules = proplists:get_value(modules, AppDetails, []),
  180. Files = modules_to_files(Modules, EbinDir),
  181. {IncApps ++ DepApps, Files};
  182. {error, enoent} when AppName =:= erts ->
  183. {[], ebin_files(EbinDir)};
  184. _ ->
  185. Error = io_lib:format("Could not parse ~p", [AppFile]),
  186. throw({dialyzer_error, Error})
  187. end.
  188. modules_to_files(Modules, EbinDir) ->
  189. Ext = code:objfile_extension(),
  190. Mod2File = fun(Module) -> module_to_file(Module, EbinDir, Ext) end,
  191. rebar_utils:filtermap(Mod2File, Modules).
  192. module_to_file(Module, EbinDir, Ext) ->
  193. File = filename:join(EbinDir, atom_to_list(Module) ++ Ext),
  194. case filelib:is_file(File) of
  195. true ->
  196. {true, File};
  197. false ->
  198. ?CONSOLE("Unknown module ~s", [Module]),
  199. false
  200. end.
  201. ebin_files(EbinDir) ->
  202. Wildcard = "*" ++ code:objfile_extension(),
  203. [filename:join(EbinDir, File) ||
  204. File <- filelib:wildcard(Wildcard, EbinDir)].
  205. read_plt(_State, Plt) ->
  206. case dialyzer:plt_info(Plt) of
  207. {ok, Info} ->
  208. Files = proplists:get_value(files, Info, []),
  209. {ok, Files};
  210. {error, no_such_file} = Error ->
  211. Error;
  212. {error, read_error} ->
  213. Error = io_lib:format("Could not read the PLT file ~p", [Plt]),
  214. throw({dialyzer_error, Error})
  215. end.
  216. check_plt(State, Plt, OldList, FilesList) ->
  217. Old = sets:from_list(OldList),
  218. Files = sets:from_list(FilesList),
  219. Remove = sets:subtract(Old, Files),
  220. {RemWarnings, State1} = remove_plt(State, Plt, sets:to_list(Remove)),
  221. Check = sets:intersection(Files, Old),
  222. {CheckWarnings, State2} = check_plt(State1, Plt, sets:to_list(Check)),
  223. Add = sets:subtract(Files, Old),
  224. {AddWarnings, State3} = add_plt(State2, Plt, sets:to_list(Add)),
  225. {RemWarnings + CheckWarnings + AddWarnings, State3}.
  226. remove_plt(State, _Plt, []) ->
  227. {0, State};
  228. remove_plt(State, Plt, Files) ->
  229. ?INFO("Removing ~b files from ~p...", [length(Files), Plt]),
  230. run_plt(State, Plt, plt_remove, Files).
  231. check_plt(State, _Plt, []) ->
  232. {0, State};
  233. check_plt(State, Plt, Files) ->
  234. ?INFO("Checking ~b files in ~p...", [length(Files), Plt]),
  235. run_plt(State, Plt, plt_check, Files).
  236. add_plt(State, _Plt, []) ->
  237. {0, State};
  238. add_plt(State, Plt, Files) ->
  239. ?INFO("Adding ~b files to ~p...", [length(Files), Plt]),
  240. run_plt(State, Plt, plt_add, Files).
  241. run_plt(State, Plt, Analysis, Files) ->
  242. GetWarnings = rebar_state:get(State, dialyzer_plt_warnings, false),
  243. Opts = [{analysis_type, Analysis},
  244. {get_warnings, GetWarnings},
  245. {init_plt, Plt},
  246. {from, byte_code},
  247. {files, Files}],
  248. run_dialyzer(State, Opts).
  249. build_proj_plt(State, Plt, Files) ->
  250. BasePlt = get_base_plt_location(State),
  251. BaseFiles = get_base_plt_files(State),
  252. {BaseWarnings, State1} = update_base_plt(State, BasePlt, BaseFiles),
  253. ?INFO("Copying ~p to ~p...", [BasePlt, Plt]),
  254. _ = filelib:ensure_dir(Plt),
  255. case file:copy(BasePlt, Plt) of
  256. {ok, _} ->
  257. {CheckWarnings, State2} = check_plt(State1, Plt, BaseFiles, Files),
  258. {BaseWarnings + CheckWarnings, State2};
  259. {error, Reason} ->
  260. Error = io_lib:format("Could not copy PLT from ~p to ~p: ~p",
  261. [BasePlt, Plt, file:format_error(Reason)]),
  262. throw({dialyzer_error, Error})
  263. end.
  264. get_base_plt_location(State) ->
  265. GlobalCacheDir = rebar_dir:global_cache_dir(State),
  266. BaseDir = rebar_state:get(State, dialyzer_base_plt_dir, GlobalCacheDir),
  267. BasePlt = rebar_state:get(State, dialyzer_base_plt, default_plt()),
  268. filename:join(BaseDir, BasePlt).
  269. get_base_plt_files(State) ->
  270. BasePltApps = rebar_state:get(State, dialyzer_base_plt_apps,
  271. default_plt_apps()),
  272. app_names_to_files(BasePltApps).
  273. app_names_to_files(AppNames) ->
  274. ToFiles = fun(AppName) ->
  275. {_, Files} = app_name_to_info(AppName),
  276. Files
  277. end,
  278. lists:flatmap(ToFiles, AppNames).
  279. update_base_plt(State, BasePlt, BaseFiles) ->
  280. ?INFO("Updating base plt...", []),
  281. case read_plt(State, BasePlt) of
  282. {ok, OldBaseFiles} ->
  283. check_plt(State, BasePlt, OldBaseFiles, BaseFiles);
  284. {error, no_such_file} ->
  285. _ = filelib:ensure_dir(BasePlt),
  286. build_plt(State, BasePlt, BaseFiles)
  287. end.
  288. build_plt(State, Plt, Files) ->
  289. ?INFO("Adding ~b files to ~p...", [length(Files), Plt]),
  290. GetWarnings = rebar_state:get(State, dialyzer_plt_warnings, false),
  291. Opts = [{analysis_type, plt_build},
  292. {get_warnings, GetWarnings},
  293. {output_plt, Plt},
  294. {files, Files}],
  295. run_dialyzer(State, Opts).
  296. succ_typings(State, Plt, Apps) ->
  297. {Args, _} = rebar_state:command_parsed_args(State),
  298. case proplists:get_value(succ_typings, Args) of
  299. false ->
  300. {0, State};
  301. _ ->
  302. do_succ_typings(State, Plt, Apps)
  303. end.
  304. do_succ_typings(State, Plt, Apps) ->
  305. ?INFO("Doing success typing analysis...", []),
  306. Files = apps_to_files(Apps),
  307. ?INFO("Analyzing ~b files with ~p...", [length(Files), Plt]),
  308. Opts = [{analysis_type, succ_typings},
  309. {get_warnings, true},
  310. {from, byte_code},
  311. {files, Files},
  312. {init_plt, Plt}],
  313. run_dialyzer(State, Opts).
  314. apps_to_files(Apps) ->
  315. lists:flatmap(fun app_to_files/1, Apps).
  316. app_to_files(App) ->
  317. AppName = ec_cnv:to_atom(rebar_app_info:name(App)),
  318. {_, Files} = app_name_to_info(AppName),
  319. Files.
  320. run_dialyzer(State, Opts) ->
  321. %% dialyzer may return callgraph warnings when get_warnings is false
  322. case proplists:get_bool(get_warnings, Opts) of
  323. true ->
  324. WarningsList = rebar_state:get(State, dialyzer_warnings, []),
  325. Opts2 = [{warnings, WarningsList},
  326. {check_plt, false} |
  327. Opts],
  328. ?DEBUG("Running dialyzer with options: ~p~n", [Opts2]),
  329. {Unknowns, Warnings} = format_warnings(dialyzer:run(Opts2)),
  330. _ = [?CONSOLE("~s", [Unknown]) || Unknown <- Unknowns],
  331. _ = [?CONSOLE("~s", [Warning]) || Warning <- Warnings],
  332. {length(Warnings), State};
  333. false ->
  334. Opts2 = [{warnings, no_warnings()},
  335. {check_plt, false} |
  336. Opts],
  337. ?DEBUG("Running dialyzer with options: ~p~n", [Opts2]),
  338. _ = dialyzer:run(Opts2),
  339. {0, State}
  340. end.
  341. format_warnings(Warnings) ->
  342. format_warnings(Warnings, [], []).
  343. format_warnings([Warning | Rest], Unknowns, Warnings) ->
  344. case dialyzer:format_warning(Warning, fullpath) of
  345. ":0: " ++ Unknown ->
  346. format_warnings(Rest, [strip(Unknown) | Unknowns], Warnings);
  347. Warning2 ->
  348. format_warnings(Rest, Unknowns, [strip(Warning2) | Warnings])
  349. end;
  350. format_warnings([], Unknowns, Warnings) ->
  351. {Unknowns, Warnings}.
  352. strip(Warning) ->
  353. string:strip(Warning, right, $\n).
  354. no_warnings() ->
  355. [no_return,
  356. no_unused,
  357. no_improper_lists,
  358. no_fun_app,
  359. no_match,
  360. no_opaque,
  361. no_fail_call,
  362. no_contracts,
  363. no_behaviours,
  364. no_undefined_callbacks].