Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

399 строки
14 KiB

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. -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].