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.

399 lines
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. _ ->
  190. Error = io_lib:format("Could not parse ~p", [AppFile]),
  191. throw({dialyzer_error, Error})
  192. end.
  193. modules_to_files(Modules, EbinDir) ->
  194. Ext = code:objfile_extension(),
  195. Mod2File = fun(Module) -> module_to_file(Module, EbinDir, Ext) end,
  196. rebar_utils:filtermap(Mod2File, Modules).
  197. module_to_file(Module, EbinDir, Ext) ->
  198. File = filename:join(EbinDir, atom_to_list(Module) ++ Ext),
  199. case filelib:is_file(File) of
  200. true ->
  201. {true, File};
  202. false ->
  203. ?CONSOLE("Unknown module ~s", [Module]),
  204. false
  205. end.
  206. read_plt(_State, Plt) ->
  207. case dialyzer:plt_info(Plt) of
  208. {ok, Info} ->
  209. Files = proplists:get_value(files, Info, []),
  210. {ok, Files};
  211. {error, no_such_file} = Error ->
  212. Error;
  213. {error, read_error} ->
  214. Error = io_lib:format("Could not read the PLT file ~p", [Plt]),
  215. throw({dialyzer_error, Error})
  216. end.
  217. check_plt(State, Plt, OldList, FilesList) ->
  218. Old = sets:from_list(OldList),
  219. Files = sets:from_list(FilesList),
  220. Remove = sets:subtract(Old, Files),
  221. {RemWarnings, State1} = remove_plt(State, Plt, sets:to_list(Remove)),
  222. Check = sets:intersection(Files, Old),
  223. {CheckWarnings, State2} = check_plt(State1, Plt, sets:to_list(Check)),
  224. Add = sets:subtract(Files, Old),
  225. {AddWarnings, State3} = add_plt(State2, Plt, sets:to_list(Add)),
  226. ?DEBUG("~p", [[RemWarnings, CheckWarnings, AddWarnings]]),
  227. {RemWarnings + CheckWarnings + AddWarnings, State3}.
  228. remove_plt(State, _Plt, []) ->
  229. {0, State};
  230. remove_plt(State, Plt, Files) ->
  231. ?INFO("Removing ~b files from ~p...", [length(Files), Plt]),
  232. run_plt(State, Plt, plt_remove, Files).
  233. check_plt(State, _Plt, []) ->
  234. {0, State};
  235. check_plt(State, Plt, Files) ->
  236. ?INFO("Checking ~b files in ~p...", [length(Files), Plt]),
  237. run_plt(State, Plt, plt_check, Files).
  238. add_plt(State, _Plt, []) ->
  239. {0, State};
  240. add_plt(State, Plt, Files) ->
  241. ?INFO("Adding ~b files to ~p...", [length(Files), Plt]),
  242. run_plt(State, Plt, plt_add, Files).
  243. run_plt(State, Plt, Analysis, Files) ->
  244. GetWarnings = rebar_state:get(State, dialyzer_plt_warnings, false),
  245. Opts = [{analysis_type, Analysis},
  246. {get_warnings, GetWarnings},
  247. {init_plt, Plt},
  248. {from, byte_code},
  249. {files, Files}],
  250. run_dialyzer(State, Opts).
  251. build_proj_plt(State, Plt, Files) ->
  252. BasePlt = get_base_plt_location(State),
  253. BaseFiles = get_base_plt_files(State),
  254. {BaseWarnings, State1} = update_base_plt(State, BasePlt, BaseFiles),
  255. ?INFO("Copying ~p to ~p...", [BasePlt, Plt]),
  256. _ = filelib:ensure_dir(Plt),
  257. case file:copy(BasePlt, Plt) of
  258. {ok, _} ->
  259. {CheckWarnings, State2} = check_plt(State1, Plt, BaseFiles, Files),
  260. {BaseWarnings + CheckWarnings, State2};
  261. {error, Reason} ->
  262. Error = io_lib:format("Could not copy PLT from ~p to ~p: ~p",
  263. [BasePlt, Plt, file:format_error(Reason)]),
  264. throw({dialyzer_error, Error})
  265. end.
  266. get_base_plt_location(State) ->
  267. Home = rebar_dir:home_dir(),
  268. GlobalConfigDir = filename:join(Home, ?CONFIG_DIR),
  269. BaseDir = rebar_state:get(State, dialyzer_base_plt_dir, GlobalConfigDir),
  270. BasePlt = rebar_state:get(State, dialyzer_base_plt, default_plt()),
  271. filename:join(BaseDir, BasePlt).
  272. get_base_plt_files(State) ->
  273. BasePltApps = rebar_state:get(State, dialyzer_base_plt_apps,
  274. default_plt_apps()),
  275. app_names_to_files(BasePltApps).
  276. app_names_to_files(AppNames) ->
  277. ToFiles = fun(AppName) ->
  278. {_, Files} = app_name_to_info(AppName),
  279. Files
  280. end,
  281. lists:flatmap(ToFiles, AppNames).
  282. update_base_plt(State, BasePlt, BaseFiles) ->
  283. ?INFO("Updating base plt...", []),
  284. case read_plt(State, BasePlt) of
  285. {ok, OldBaseFiles} ->
  286. check_plt(State, BasePlt, OldBaseFiles, BaseFiles);
  287. {error, no_such_file} ->
  288. _ = filelib:ensure_dir(BasePlt),
  289. build_plt(State, BasePlt, BaseFiles)
  290. end.
  291. build_plt(State, Plt, Files) ->
  292. ?INFO("Adding ~b files to ~p...", [length(Files), Plt]),
  293. GetWarnings = rebar_state:get(State, dialyzer_plt_warnings, false),
  294. Opts = [{analysis_type, plt_build},
  295. {get_warnings, GetWarnings},
  296. {output_plt, Plt},
  297. {files, Files}],
  298. run_dialyzer(State, Opts).
  299. succ_typings(State, Plt, Apps) ->
  300. {Args, _} = rebar_state:command_parsed_args(State),
  301. case proplists:get_value(succ_typings, Args) of
  302. false ->
  303. {ok, State};
  304. _ ->
  305. do_succ_typings(State, Plt, Apps)
  306. end.
  307. do_succ_typings(State, Plt, Apps) ->
  308. ?INFO("Doing success typing analysis...", []),
  309. Files = apps_to_files(Apps),
  310. ?INFO("Analyzing ~b files with ~p...", [length(Files), Plt]),
  311. Opts = [{analysis_type, succ_typings},
  312. {get_warnings, true},
  313. {from, byte_code},
  314. {files, Files},
  315. {init_plt, Plt}],
  316. run_dialyzer(State, Opts).
  317. apps_to_files(Apps) ->
  318. lists:flatmap(fun app_to_files/1, Apps).
  319. app_to_files(App) ->
  320. AppName = ec_cnv:to_atom(rebar_app_info:name(App)),
  321. {_, Files} = app_name_to_info(AppName),
  322. Files.
  323. run_dialyzer(State, Opts) ->
  324. WarningsList = rebar_state:get(State, dialyzer_warnings, default_warnings()),
  325. Opts2 = [{warnings, WarningsList} | Opts],
  326. {Unknowns, Warnings} = format_warnings(dialyzer:run(Opts2)),
  327. _ = [?CONSOLE("~s", [Unknown]) || Unknown <- Unknowns],
  328. _ = [?CONSOLE("~s", [Warning]) || Warning <- Warnings],
  329. {length(Warnings), State}.
  330. format_warnings(Warnings) ->
  331. format_warnings(Warnings, [], []).
  332. format_warnings([Warning | Rest], Unknowns, Warnings) ->
  333. case dialyzer:format_warning(Warning, fullpath) of
  334. ":0: " ++ Unknown ->
  335. format_warnings(Rest, [strip(Unknown) | Unknowns], Warnings);
  336. Warning2 ->
  337. format_warnings(Rest, Unknowns, [strip(Warning2) | Warnings])
  338. end;
  339. format_warnings([], Unknowns, Warnings) ->
  340. {Unknowns, Warnings}.
  341. strip(Warning) ->
  342. string:strip(Warning, right, $\n).
  343. default_warnings() ->
  344. [error_handling,
  345. unmatched_returns,
  346. underspecs].