Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

415 linhas
14 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. %% ===================================================================
  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].