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.

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