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.

383 lines
13 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. {ok, State1} = update_proj_plt(State, Plt, Apps),
  60. succ_typings(State1, Plt, Apps)
  61. catch
  62. throw:{dialyzer_error, Error} ->
  63. {error, {?MODULE, {error_processing_apps, Error, Apps}}}
  64. end.
  65. -spec format_error(any()) -> iolist().
  66. format_error({error_processing_apps, Error, _Apps}) ->
  67. io_lib:format("Error in dialyzing apps: ~s", [Error]);
  68. format_error(Reason) ->
  69. io_lib:format("~p", [Reason]).
  70. %% Internal functions
  71. get_plt_location(State) ->
  72. BuildDir = rebar_state:get(State, base_dir, ?DEFAULT_BASE_DIR),
  73. DefaultPlt = filename:join([BuildDir, default_plt()]),
  74. rebar_state:get(State, dialyzer_plt, DefaultPlt).
  75. default_plt() ->
  76. ".rebar3.otp-" ++ otp_version() ++ ".plt".
  77. otp_version() ->
  78. Release = erlang:system_info(otp_release),
  79. try otp_version(Release) of
  80. Vsn ->
  81. Vsn
  82. catch
  83. error:_ ->
  84. Release
  85. end.
  86. otp_version(Release) ->
  87. File = filename:join([code:root_dir(), "releases", Release, "OTP_VERSION"]),
  88. {ok, Contents} = file:read_file(File),
  89. [Vsn] = binary:split(Contents, [<<$\n>>], [global, trim]),
  90. [_ | _] = unicode:characters_to_list(Vsn).
  91. update_proj_plt(State, Plt, Apps) ->
  92. {Args, _} = rebar_state:command_parsed_args(State),
  93. case proplists:get_value(update_plt, Args) of
  94. false ->
  95. {ok, State};
  96. _ ->
  97. do_update_proj_plt(State, Plt, Apps)
  98. end.
  99. do_update_proj_plt(State, Plt, Apps) ->
  100. ?INFO("Updating plt...", []),
  101. Files = get_plt_files(State, Apps),
  102. case read_plt(State, Plt) of
  103. {ok, OldFiles} ->
  104. check_plt(State, Plt, OldFiles, Files);
  105. {error, no_such_file} ->
  106. build_proj_plt(State, Plt, Files)
  107. end.
  108. get_plt_files(State, Apps) ->
  109. BasePltApps = rebar_state:get(State, dialyzer_base_plt_apps,
  110. default_plt_apps()),
  111. PltApps = rebar_state:get(State, dialyzer_plt_apps, []),
  112. DepApps = lists:flatmap(fun rebar_app_info:applications/1, Apps),
  113. get_plt_files(BasePltApps ++ PltApps ++ DepApps, Apps, [], []).
  114. default_plt_apps() ->
  115. [erts,
  116. crypto,
  117. kernel,
  118. stdlib].
  119. get_plt_files([], _, _, Files) ->
  120. Files;
  121. get_plt_files([AppName | DepApps], Apps, PltApps, Files) ->
  122. case lists:member(AppName, PltApps) orelse app_member(AppName, Apps) of
  123. true ->
  124. get_plt_files(DepApps, Apps, PltApps, Files);
  125. false ->
  126. {DepApps2, Files2} = app_name_to_info(AppName),
  127. DepApps3 = DepApps2 ++ DepApps,
  128. Files3 = Files2 ++ Files,
  129. get_plt_files(DepApps3, Apps, [AppName | PltApps], Files3)
  130. end.
  131. app_member(AppName, Apps) ->
  132. case rebar_app_utils:find(ec_cnv:to_binary(AppName), Apps) of
  133. {ok, _App} ->
  134. true;
  135. error ->
  136. false
  137. end.
  138. app_name_to_info(AppName) ->
  139. case app_name_to_ebin(AppName) of
  140. {error, _} ->
  141. ?CONSOLE("Unknown application ~s", [AppName]),
  142. {[], []};
  143. EbinDir ->
  144. ebin_to_info(EbinDir, AppName)
  145. end.
  146. app_name_to_ebin(AppName) ->
  147. case code:lib_dir(AppName, ebin) of
  148. {error, bad_name} ->
  149. search_ebin(AppName);
  150. EbinDir ->
  151. check_ebin(EbinDir, AppName)
  152. end.
  153. check_ebin(EbinDir, AppName) ->
  154. case filelib:is_dir(EbinDir) of
  155. true ->
  156. EbinDir;
  157. false ->
  158. search_ebin(AppName)
  159. end.
  160. search_ebin(AppName) ->
  161. case code:where_is_file(atom_to_list(AppName) ++ ".app") of
  162. non_existing ->
  163. {error, bad_name};
  164. AppFile ->
  165. filename:dirname(AppFile)
  166. end.
  167. ebin_to_info(EbinDir, AppName) ->
  168. AppFile = filename:join(EbinDir, atom_to_list(AppName) ++ ".app"),
  169. case file:consult(AppFile) of
  170. {ok, [{application, AppName, AppDetails}]} ->
  171. DepApps = proplists:get_value(applications, AppDetails, []),
  172. IncApps = proplists:get_value(included_applications, AppDetails,
  173. []),
  174. Modules = proplists:get_value(modules, AppDetails, []),
  175. Files = modules_to_files(Modules, EbinDir),
  176. {IncApps ++ DepApps, Files};
  177. {error, enoent} when AppName =:= erts ->
  178. {[], ebin_files(EbinDir)};
  179. _ ->
  180. Error = io_lib:format("Could not parse ~p", [AppFile]),
  181. throw({dialyzer_error, Error})
  182. end.
  183. modules_to_files(Modules, EbinDir) ->
  184. Ext = code:objfile_extension(),
  185. Mod2File = fun(Module) -> module_to_file(Module, EbinDir, Ext) end,
  186. rebar_utils:filtermap(Mod2File, Modules).
  187. module_to_file(Module, EbinDir, Ext) ->
  188. File = filename:join(EbinDir, atom_to_list(Module) ++ Ext),
  189. case filelib:is_file(File) of
  190. true ->
  191. {true, File};
  192. false ->
  193. ?CONSOLE("Unknown module ~s", [Module]),
  194. false
  195. end.
  196. ebin_files(EbinDir) ->
  197. Wildcard = "*" ++ code:objfile_extension(),
  198. [filename:join(EbinDir, File) ||
  199. File <- filelib:wildcard(Wildcard, EbinDir)].
  200. read_plt(_State, Plt) ->
  201. case dialyzer:plt_info(Plt) of
  202. {ok, Info} ->
  203. Files = proplists:get_value(files, Info, []),
  204. {ok, Files};
  205. {error, no_such_file} = Error ->
  206. Error;
  207. {error, read_error} ->
  208. Error = io_lib:format("Could not read the PLT file ~p", [Plt]),
  209. throw({dialyzer_error, Error})
  210. end.
  211. check_plt(State, Plt, OldList, FilesList) ->
  212. Old = sets:from_list(OldList),
  213. Files = sets:from_list(FilesList),
  214. Remove = sets:subtract(Old, Files),
  215. {ok, State1} = remove_plt(State, Plt, sets:to_list(Remove)),
  216. Check = sets:intersection(Files, Old),
  217. {ok, State2} = check_plt(State1, Plt, sets:to_list(Check)),
  218. Add = sets:subtract(Files, Old),
  219. add_plt(State2, Plt, sets:to_list(Add)).
  220. remove_plt(State, _Plt, []) ->
  221. {ok, State};
  222. remove_plt(State, Plt, Files) ->
  223. ?INFO("Removing ~b files from ~p...", [length(Files), Plt]),
  224. run_plt(State, Plt, plt_remove, Files).
  225. check_plt(State, _Plt, []) ->
  226. {ok, State};
  227. check_plt(State, Plt, Files) ->
  228. ?INFO("Checking ~b files in ~p...", [length(Files), Plt]),
  229. run_plt(State, Plt, plt_check, Files).
  230. add_plt(State, _Plt, []) ->
  231. {ok, State};
  232. add_plt(State, Plt, Files) ->
  233. ?INFO("Adding ~b files to ~p...", [length(Files), Plt]),
  234. run_plt(State, Plt, plt_add, Files).
  235. run_plt(State, Plt, Analysis, Files) ->
  236. GetWarnings = rebar_state:get(State, dialyzer_plt_warnings, false),
  237. Opts = [{analysis_type, Analysis},
  238. {get_warnings, GetWarnings},
  239. {init_plt, Plt},
  240. {from, byte_code},
  241. {files, Files}],
  242. run_dialyzer(State, Opts).
  243. build_proj_plt(State, Plt, Files) ->
  244. BasePlt = get_base_plt_location(State),
  245. BaseFiles = get_base_plt_files(State),
  246. {ok, State1} = update_base_plt(State, BasePlt, BaseFiles),
  247. ?INFO("Copying ~p to ~p...", [BasePlt, Plt]),
  248. _ = filelib:ensure_dir(Plt),
  249. case file:copy(BasePlt, Plt) of
  250. {ok, _} ->
  251. check_plt(State1, Plt, BaseFiles, Files);
  252. {error, Reason} ->
  253. Error = io_lib:format("Could not copy PLT from ~p to ~p: ~p",
  254. [BasePlt, Plt, file:format_error(Reason)]),
  255. throw({dialyzer_error, Error})
  256. end.
  257. get_base_plt_location(State) ->
  258. Home = rebar_dir:home_dir(),
  259. GlobalConfigDir = filename:join(Home, ?CONFIG_DIR),
  260. BaseDir = rebar_state:get(State, dialyzer_base_plt_dir, GlobalConfigDir),
  261. BasePlt = rebar_state:get(State, dialyzer_base_plt, default_plt()),
  262. filename:join(BaseDir, BasePlt).
  263. get_base_plt_files(State) ->
  264. BasePltApps = rebar_state:get(State, dialyzer_base_plt_apps,
  265. default_plt_apps()),
  266. app_names_to_files(BasePltApps).
  267. app_names_to_files(AppNames) ->
  268. ToFiles = fun(AppName) ->
  269. {_, Files} = app_name_to_info(AppName),
  270. Files
  271. end,
  272. lists:flatmap(ToFiles, AppNames).
  273. update_base_plt(State, BasePlt, BaseFiles) ->
  274. ?INFO("Updating base plt...", []),
  275. case read_plt(State, BasePlt) of
  276. {ok, OldBaseFiles} ->
  277. check_plt(State, BasePlt, OldBaseFiles, BaseFiles);
  278. {error, no_such_file} ->
  279. _ = filelib:ensure_dir(BasePlt),
  280. build_plt(State, BasePlt, BaseFiles)
  281. end.
  282. build_plt(State, Plt, Files) ->
  283. ?INFO("Adding ~b files to ~p...", [length(Files), Plt]),
  284. GetWarnings = rebar_state:get(State, dialyzer_plt_warnings, false),
  285. Opts = [{analysis_type, plt_build},
  286. {get_warnings, GetWarnings},
  287. {output_plt, Plt},
  288. {files, Files}],
  289. run_dialyzer(State, Opts).
  290. succ_typings(State, Plt, Apps) ->
  291. {Args, _} = rebar_state:command_parsed_args(State),
  292. case proplists:get_value(succ_typings, Args) of
  293. false ->
  294. {ok, State};
  295. _ ->
  296. do_succ_typings(State, Plt, Apps)
  297. end.
  298. do_succ_typings(State, Plt, Apps) ->
  299. ?INFO("Doing success typing analysis...", []),
  300. Files = apps_to_files(Apps),
  301. ?INFO("Analyzing ~b files with ~p...", [length(Files), Plt]),
  302. Opts = [{analysis_type, succ_typings},
  303. {get_warnings, true},
  304. {from, byte_code},
  305. {files, Files},
  306. {init_plt, Plt}],
  307. run_dialyzer(State, Opts).
  308. apps_to_files(Apps) ->
  309. lists:flatmap(fun app_to_files/1, Apps).
  310. app_to_files(App) ->
  311. AppName = ec_cnv:to_atom(rebar_app_info:name(App)),
  312. {_, Files} = app_name_to_info(AppName),
  313. Files.
  314. run_dialyzer(State, Opts) ->
  315. Warnings = rebar_state:get(State, dialyzer_warnings, default_warnings()),
  316. Opts2 = [{warnings, Warnings} | Opts],
  317. _ = [?CONSOLE("~s", [format_warning(Warning)])
  318. || Warning <- dialyzer:run(Opts2)],
  319. {ok, State}.
  320. format_warning(Warning) ->
  321. string:strip(dialyzer_format_warning(Warning), right, $\n).
  322. dialyzer_format_warning(Warning) ->
  323. case dialyzer:format_warning(Warning) of
  324. ":0: " ++ Warning2 ->
  325. Warning2;
  326. Warning2 ->
  327. Warning2
  328. end.
  329. default_warnings() ->
  330. [error_handling,
  331. unmatched_returns,
  332. underspecs].