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.

444 lines
15 KiB

пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 10 година
пре 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. -include_lib("providers/include/providers.hrl").
  10. -define(PROVIDER, dialyzer).
  11. -define(DEPS, [compile]).
  12. -define(PLT_PREFIX, "rebar3").
  13. %% ===================================================================
  14. %% Public API
  15. %% ===================================================================
  16. -spec init(rebar_state:t()) -> {ok, rebar_state:t()}.
  17. init(State) ->
  18. Opts = [{update_plt, $u, "update-plt", boolean, "Enable updating the PLT. Default: true"},
  19. {succ_typings, $s, "succ-typings", boolean, "Enable success typing analysis. Default: true"}],
  20. State1 = rebar_state:add_provider(State, providers:create([{name, ?PROVIDER},
  21. {module, ?MODULE},
  22. {bare, true},
  23. {deps, ?DEPS},
  24. {example, "rebar3 dialyzer"},
  25. {short_desc, short_desc()},
  26. {desc, desc()},
  27. {opts, Opts}])),
  28. {ok, State1}.
  29. desc() ->
  30. short_desc() ++ "\n"
  31. "\n"
  32. "This command will build, and keep up-to-date, a suitable PLT and will use "
  33. "it to carry out success typing analysis on the current project.\n"
  34. "\n"
  35. "The following (optional) configurations can be added to a `proplist` of "
  36. "options `dialyzer` in rebar.config:\n"
  37. "`warnings` - a list of dialyzer warnings\n"
  38. "`get_warnings` - display warnings when altering a PLT file (boolean)\n"
  39. "`plt_extra_apps` - a list of applications to include in the PLT file*\n"
  40. "`plt_location` - the location of the PLT file, `local` to store in the "
  41. "profile's base directory (default) or a custom directory.\n"
  42. "`plt_prefix` - the prefix to the PLT file, defaults to \"rebar3\"**\n"
  43. "`base_plt_apps` - a list of applications to include in the base "
  44. "PLT file***\n"
  45. "`base_plt_location` - the location of base PLT file, `global` to store in "
  46. "$HOME/.cache/rebar3 (default) or a custom directory***\n"
  47. "`base_plt_prefix` - the prefix to the base PLT file, defaults to "
  48. "\"rebar3\"** ***\n"
  49. "\n"
  50. "For example, to warn on unmatched returns: \n"
  51. "{dialyzer, [{warnings, [unmatched_returns]}]}.\n"
  52. "\n"
  53. "*The applications in `dialyzer_base_plt_apps` and any `applications` and "
  54. "`included_applications` listed in their .app files will be added to the "
  55. "list.\n"
  56. "**PLT files are named \"<prefix>_<otp_release>_plt\".\n"
  57. "***The base PLT is a PLT containing the core applications often required "
  58. "for a project's PLT. One base PLT is created per OTP version and "
  59. "stored in `base_plt_location`. A base PLT is used to build project PLTs."
  60. "\n".
  61. short_desc() ->
  62. "Run the Dialyzer analyzer on the project.".
  63. -spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}.
  64. do(State) ->
  65. maybe_fix_env(),
  66. ?INFO("Dialyzer starting, this may take a while...", []),
  67. code:add_pathsa(rebar_state:code_paths(State, all_deps)),
  68. Plt = get_plt(State),
  69. try
  70. do(State, Plt)
  71. catch
  72. throw:{dialyzer_error, Error} ->
  73. ?PRV_ERROR({error_processing_apps, Error});
  74. throw:{dialyzer_warnings, Warnings} ->
  75. ?PRV_ERROR({dialyzer_warnings, Warnings});
  76. throw:{unknown_application, _} = Error ->
  77. ?PRV_ERROR(Error);
  78. throw:{output_file_error, _, _} = Error ->
  79. ?PRV_ERROR(Error)
  80. after
  81. rebar_utils:cleanup_code_path(rebar_state:code_paths(State, default))
  82. end.
  83. %% This is used to workaround dialyzer quirk discussed here
  84. %% https://github.com/rebar/rebar3/pull/489#issuecomment-107953541
  85. %% Dialyzer gets default plt location wrong way by peeking HOME environment
  86. %% variable which usually is not defined on Windows.
  87. maybe_fix_env() ->
  88. os:putenv("DIALYZER_PLT", filename:join(rebar_dir:home_dir(), ".dialyzer_plt")).
  89. -spec format_error(any()) -> iolist().
  90. format_error({error_processing_apps, Error}) ->
  91. io_lib:format("Error in dialyzing apps: ~s", [Error]);
  92. format_error({dialyzer_warnings, Warnings}) ->
  93. io_lib:format("Warnings occured running dialyzer: ~b", [Warnings]);
  94. format_error({unknown_application, App}) ->
  95. io_lib:format("Could not find application: ~s", [App]);
  96. format_error({output_file_error, File, Error}) ->
  97. Error1 = file:format_error(Error),
  98. io_lib:format("Failed to write to ~s: ~s", [File, Error1]);
  99. format_error(Reason) ->
  100. io_lib:format("~p", [Reason]).
  101. %% Internal functions
  102. get_plt(State) ->
  103. Prefix = get_config(State, plt_prefix, ?PLT_PREFIX),
  104. Name = plt_name(Prefix),
  105. case get_config(State, plt_location, local) of
  106. local ->
  107. BaseDir = rebar_dir:base_dir(State),
  108. filename:join(BaseDir, Name);
  109. Dir ->
  110. filename:join(Dir, Name)
  111. end.
  112. plt_name(Prefix) ->
  113. Prefix ++ "_" ++ rebar_utils:otp_release() ++ "_plt".
  114. do(State, Plt) ->
  115. Output = get_output_file(State),
  116. {PltWarnings, State1} = update_proj_plt(State, Plt, Output),
  117. {Warnings, State2} = succ_typings(State1, Plt, Output),
  118. case PltWarnings + Warnings of
  119. 0 ->
  120. {ok, State2};
  121. TotalWarnings ->
  122. ?INFO("Warnings written to ~s", [Output]),
  123. throw({dialyzer_warnings, TotalWarnings})
  124. end.
  125. get_output_file(State) ->
  126. BaseDir = rebar_dir:base_dir(State),
  127. Output = filename:join(BaseDir, default_output_file()),
  128. case file:open(Output, [write]) of
  129. {ok, File} ->
  130. ok = file:close(File),
  131. Output;
  132. {error, Reason} ->
  133. throw({output_file_error, Output, Reason})
  134. end.
  135. default_output_file() ->
  136. rebar_utils:otp_release() ++ ".dialyzer_warnings".
  137. update_proj_plt(State, Plt, Output) ->
  138. {Args, _} = rebar_state:command_parsed_args(State),
  139. case proplists:get_value(update_plt, Args) of
  140. false ->
  141. {0, State};
  142. _ ->
  143. do_update_proj_plt(State, Plt, Output)
  144. end.
  145. do_update_proj_plt(State, Plt, Output) ->
  146. ?INFO("Updating plt...", []),
  147. Files = proj_plt_files(State),
  148. case read_plt(State, Plt) of
  149. {ok, OldFiles} ->
  150. check_plt(State, Plt, Output, OldFiles, Files);
  151. {error, no_such_file} ->
  152. build_proj_plt(State, Plt, Output, Files)
  153. end.
  154. proj_plt_files(State) ->
  155. BasePltApps = get_config(State, base_plt_apps, default_plt_apps()),
  156. PltApps = get_config(State, plt_extra_apps, []),
  157. Apps = rebar_state:project_apps(State),
  158. DepApps = lists:flatmap(fun rebar_app_info:applications/1, Apps),
  159. get_plt_files(BasePltApps ++ PltApps ++ DepApps, Apps).
  160. default_plt_apps() ->
  161. [erts,
  162. crypto,
  163. kernel,
  164. stdlib].
  165. get_plt_files(DepApps, Apps) ->
  166. ?INFO("Resolving files...", []),
  167. get_plt_files(DepApps, Apps, [], []).
  168. get_plt_files([], _, _, Files) ->
  169. Files;
  170. get_plt_files([AppName | DepApps], Apps, PltApps, Files) ->
  171. case lists:member(AppName, PltApps) orelse app_member(AppName, Apps) of
  172. true ->
  173. get_plt_files(DepApps, Apps, PltApps, Files);
  174. false ->
  175. Files2 = app_files(AppName),
  176. ?DEBUG("~s files: ~p", [AppName, Files2]),
  177. get_plt_files(DepApps, Apps, [AppName | PltApps], Files2 ++ Files)
  178. end.
  179. app_member(AppName, Apps) ->
  180. case rebar_app_utils:find(ec_cnv:to_binary(AppName), Apps) of
  181. {ok, _App} ->
  182. true;
  183. error ->
  184. false
  185. end.
  186. app_files(AppName) ->
  187. case app_ebin(AppName) of
  188. {ok, EbinDir} ->
  189. ebin_files(EbinDir);
  190. {error, bad_name} ->
  191. throw({unknown_application, AppName})
  192. end.
  193. app_ebin(AppName) ->
  194. case code:lib_dir(AppName, ebin) of
  195. {error, bad_name} = Error ->
  196. Error;
  197. EbinDir ->
  198. check_ebin(EbinDir)
  199. end.
  200. check_ebin(EbinDir) ->
  201. case filelib:is_dir(EbinDir) of
  202. true ->
  203. {ok, EbinDir};
  204. false ->
  205. {error, bad_name}
  206. end.
  207. ebin_files(EbinDir) ->
  208. Wildcard = "*" ++ code:objfile_extension(),
  209. [filename:join(EbinDir, File) ||
  210. File <- filelib:wildcard(Wildcard, EbinDir)].
  211. read_plt(_State, Plt) ->
  212. case dialyzer:plt_info(Plt) of
  213. {ok, Info} ->
  214. Files = proplists:get_value(files, Info, []),
  215. {ok, Files};
  216. {error, no_such_file} = Error ->
  217. Error;
  218. {error, read_error} ->
  219. Error = io_lib:format("Could not read the PLT file ~p", [Plt]),
  220. throw({dialyzer_error, Error})
  221. end.
  222. check_plt(State, Plt, Output, OldList, FilesList) ->
  223. Old = sets:from_list(OldList),
  224. Files = sets:from_list(FilesList),
  225. Remove = sets:to_list(sets:subtract(Old, Files)),
  226. {RemWarnings, State1} = remove_plt(State, Plt, Output, Remove),
  227. Check = sets:to_list(sets:intersection(Files, Old)),
  228. {CheckWarnings, State2} = check_plt(State1, Plt, Output, Check),
  229. Add = sets:to_list(sets:subtract(Files, Old)),
  230. {AddWarnings, State3} = add_plt(State2, Plt, Output, Add),
  231. {RemWarnings + CheckWarnings + AddWarnings, State3}.
  232. remove_plt(State, _Plt, _Output, []) ->
  233. {0, State};
  234. remove_plt(State, Plt, Output, Files) ->
  235. ?INFO("Removing ~b files from ~p...", [length(Files), Plt]),
  236. run_plt(State, Plt, Output, plt_remove, Files).
  237. check_plt(State, _Plt, _Output, []) ->
  238. {0, State};
  239. check_plt(State, Plt, Output, Files) ->
  240. ?INFO("Checking ~b files in ~p...", [length(Files), Plt]),
  241. run_plt(State, Plt, Output, plt_check, Files).
  242. add_plt(State, _Plt, _Output, []) ->
  243. {0, State};
  244. add_plt(State, Plt, Output, Files) ->
  245. ?INFO("Adding ~b files to ~p...", [length(Files), Plt]),
  246. run_plt(State, Plt, Output, plt_add, Files).
  247. run_plt(State, Plt, Output, Analysis, Files) ->
  248. GetWarnings = get_config(State, get_warnings, false),
  249. Opts = [{analysis_type, Analysis},
  250. {get_warnings, GetWarnings},
  251. {init_plt, Plt},
  252. {output_plt, Plt},
  253. {from, byte_code},
  254. {files, Files}],
  255. run_dialyzer(State, Opts, Output).
  256. build_proj_plt(State, Plt, Output, Files) ->
  257. BasePlt = get_base_plt(State),
  258. ?INFO("Updating base plt...", []),
  259. BaseFiles = base_plt_files(State),
  260. {BaseWarnings, State1} = update_base_plt(State, BasePlt, Output, 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, Output, BaseFiles,
  266. Files),
  267. {BaseWarnings + CheckWarnings, State2};
  268. {error, Reason} ->
  269. Error = io_lib:format("Could not copy PLT from ~p to ~p: ~p",
  270. [BasePlt, Plt, file:format_error(Reason)]),
  271. throw({dialyzer_error, Error})
  272. end.
  273. get_base_plt(State) ->
  274. Prefix = get_config(State, base_plt_prefix, ?PLT_PREFIX),
  275. Name = plt_name(Prefix),
  276. case get_config(State, base_plt_location, global) of
  277. global ->
  278. GlobalCacheDir = rebar_dir:global_cache_dir(State),
  279. filename:join(GlobalCacheDir, Name);
  280. Dir ->
  281. filename:join(Dir, Name)
  282. end.
  283. base_plt_files(State) ->
  284. BasePltApps = get_config(State, base_plt_apps, default_plt_apps()),
  285. Apps = rebar_state:project_apps(State),
  286. get_plt_files(BasePltApps, Apps).
  287. update_base_plt(State, BasePlt, Output, BaseFiles) ->
  288. case read_plt(State, BasePlt) of
  289. {ok, OldBaseFiles} ->
  290. check_plt(State, BasePlt, Output, OldBaseFiles, BaseFiles);
  291. {error, no_such_file} ->
  292. _ = filelib:ensure_dir(BasePlt),
  293. build_plt(State, BasePlt, Output, BaseFiles)
  294. end.
  295. build_plt(State, Plt, Output, Files) ->
  296. ?INFO("Adding ~b files to ~p...", [length(Files), Plt]),
  297. GetWarnings = get_config(State, get_warnings, false),
  298. Opts = [{analysis_type, plt_build},
  299. {get_warnings, GetWarnings},
  300. {output_plt, Plt},
  301. {files, Files}],
  302. run_dialyzer(State, Opts, Output).
  303. succ_typings(State, Plt, Output) ->
  304. {Args, _} = rebar_state:command_parsed_args(State),
  305. case proplists:get_value(succ_typings, Args) of
  306. false ->
  307. {0, State};
  308. _ ->
  309. Apps = rebar_state:project_apps(State),
  310. succ_typings(State, Plt, Output, Apps)
  311. end.
  312. succ_typings(State, Plt, Output, Apps) ->
  313. ?INFO("Doing success typing analysis...", []),
  314. Files = apps_to_files(Apps),
  315. ?INFO("Analyzing ~b files with ~p...", [length(Files), Plt]),
  316. Opts = [{analysis_type, succ_typings},
  317. {get_warnings, true},
  318. {from, byte_code},
  319. {files, Files},
  320. {init_plt, Plt}],
  321. run_dialyzer(State, Opts, Output).
  322. apps_to_files(Apps) ->
  323. ?INFO("Resolving files...", []),
  324. [File || App <- Apps,
  325. File <- app_to_files(App)].
  326. app_to_files(App) ->
  327. AppName = ec_cnv:to_atom(rebar_app_info:name(App)),
  328. app_files(AppName).
  329. run_dialyzer(State, Opts, Output) ->
  330. %% dialyzer may return callgraph warnings when get_warnings is false
  331. case proplists:get_bool(get_warnings, Opts) of
  332. true ->
  333. WarningsList = get_config(State, warnings, []),
  334. Opts2 = [{warnings, WarningsList},
  335. {check_plt, false} |
  336. Opts],
  337. ?DEBUG("Running dialyzer with options: ~p~n", [Opts2]),
  338. Warnings = format_warnings(Output, dialyzer:run(Opts2)),
  339. {Warnings, State};
  340. false ->
  341. Opts2 = [{warnings, no_warnings()},
  342. {check_plt, false} |
  343. Opts],
  344. ?DEBUG("Running dialyzer with options: ~p~n", [Opts2]),
  345. dialyzer:run(Opts2),
  346. {0, State}
  347. end.
  348. format_warnings(Output, Warnings) ->
  349. Warnings1 = format_warnings(Warnings),
  350. console_warnings(Warnings1),
  351. file_warnings(Output, Warnings1),
  352. length(Warnings1).
  353. format_warnings(Warnings) ->
  354. [format_warning(Warning) || Warning <- Warnings].
  355. format_warning(Warning) ->
  356. case strip(dialyzer:format_warning(Warning, fullpath)) of
  357. ":0: " ++ Unknown ->
  358. Unknown;
  359. Warning1 ->
  360. Warning1
  361. end.
  362. console_warnings(Warnings) ->
  363. _ = [?CONSOLE("~s", [Warning]) || Warning <- Warnings],
  364. ok.
  365. file_warnings(_, []) ->
  366. ok;
  367. file_warnings(Output, Warnings) ->
  368. Warnings1 = [[Warning, $\n] || Warning <- Warnings],
  369. case file:write_file(Output, Warnings1, [append]) of
  370. ok ->
  371. ok;
  372. {error, Reason} ->
  373. throw({output_file_error, Output, Reason})
  374. end.
  375. strip(Warning) ->
  376. string:strip(Warning, right, $\n).
  377. no_warnings() ->
  378. [no_return,
  379. no_unused,
  380. no_improper_lists,
  381. no_fun_app,
  382. no_match,
  383. no_opaque,
  384. no_fail_call,
  385. no_contracts,
  386. no_behaviours,
  387. no_undefined_callbacks].
  388. get_config(State, Key, Default) ->
  389. Config = rebar_state:get(State, dialyzer, []),
  390. proplists:get_value(Key, Config, Default).