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.

576 regels
21 KiB

10 jaren geleden
10 jaren geleden
10 jaren geleden
10 jaren geleden
10 jaren geleden
10 jaren geleden
10 jaren geleden
10 jaren geleden
10 jaren geleden
10 jaren geleden
10 jaren geleden
10 jaren geleden
10 jaren geleden
10 jaren geleden
9 jaren geleden
8 jaren geleden
8 jaren geleden
  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_apps` - the strategy for determining the applications which included "
  40. "in the PLT file, `top_level_deps` to include just the direct dependencies "
  41. "or `all_deps` to include all nested dependencies*\n"
  42. "`plt_extra_apps` - a list of extra applications to include in the PLT "
  43. "file\n"
  44. "`plt_extra_mods` - a list of extra modules to includes in the PLT file\n"
  45. "`plt_location` - the location of the PLT file, `local` to store in the "
  46. "profile's base directory (default) or a custom directory.\n"
  47. "`plt_prefix` - the prefix to the PLT file, defaults to \"rebar3\"**\n"
  48. "`base_plt_apps` - a list of applications to include in the base "
  49. "PLT file***\n"
  50. "`base_plt_mods` - a list of modules to include in the base "
  51. "PLT file***\n"
  52. "`base_plt_location` - the location of base PLT file, `global` to store in "
  53. "$HOME/.cache/rebar3 (default) or a custom directory***\n"
  54. "`base_plt_prefix` - the prefix to the base PLT file, defaults to "
  55. "\"rebar3\"** ***\n"
  56. "`exclude_apps` - a list of applications to exclude from PLT files and "
  57. "success typing analysis, `plt_extra_mods` and `base_plt_mods` can add "
  58. "modules from excluded applications\n"
  59. "`exclude_mods` - a list of modules to exclude from PLT files and "
  60. "success typing analysis\n"
  61. "\n"
  62. "For example, to warn on unmatched returns: \n"
  63. "{dialyzer, [{warnings, [unmatched_returns]}]}.\n"
  64. "\n"
  65. "*The direct dependent applications are listed in `applications` and "
  66. "`included_applications` of their .app files.\n"
  67. "**PLT files are named \"<prefix>_<otp_release>_plt\".\n"
  68. "***The base PLT is a PLT containing the core applications often required "
  69. "for a project's PLT. One base PLT is created per OTP version and "
  70. "stored in `base_plt_location`. A base PLT is used to build project PLTs."
  71. "\n".
  72. short_desc() ->
  73. "Run the Dialyzer analyzer on the project.".
  74. -spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}.
  75. do(State) ->
  76. maybe_fix_env(),
  77. ?INFO("Dialyzer starting, this may take a while...", []),
  78. code:add_pathsa(rebar_state:code_paths(State, all_deps)),
  79. Plt = get_plt(State),
  80. try
  81. do(State, Plt)
  82. catch
  83. throw:{dialyzer_error, Error} ->
  84. ?PRV_ERROR({error_processing_apps, Error});
  85. throw:{dialyzer_warnings, Warnings} ->
  86. ?PRV_ERROR({dialyzer_warnings, Warnings});
  87. throw:{unknown_application, _} = Error ->
  88. ?PRV_ERROR(Error);
  89. throw:{unknown_module, _} = Error ->
  90. ?PRV_ERROR(Error);
  91. throw:{duplicate_module, _, _, _} = Error ->
  92. ?PRV_ERROR(Error);
  93. throw:{output_file_error, _, _} = Error ->
  94. ?PRV_ERROR(Error)
  95. after
  96. rebar_utils:cleanup_code_path(rebar_state:code_paths(State, default))
  97. end.
  98. %% This is used to workaround dialyzer quirk discussed here
  99. %% https://github.com/erlang/rebar3/pull/489#issuecomment-107953541
  100. %% Dialyzer gets default plt location wrong way by peeking HOME environment
  101. %% variable which usually is not defined on Windows.
  102. maybe_fix_env() ->
  103. os:putenv("DIALYZER_PLT", filename:join(rebar_dir:home_dir(), ".dialyzer_plt")).
  104. -spec format_error(any()) -> iolist().
  105. format_error({error_processing_apps, Error}) ->
  106. io_lib:format("Error in dialyzing apps: ~s", [Error]);
  107. format_error({dialyzer_warnings, Warnings}) ->
  108. io_lib:format("Warnings occurred running dialyzer: ~b", [Warnings]);
  109. format_error({unknown_application, App}) ->
  110. io_lib:format("Could not find application: ~s", [App]);
  111. format_error({unknown_module, Mod}) ->
  112. io_lib:format("Could not find module: ~s", [Mod]);
  113. format_error({duplicate_module, Mod, File1, File2}) ->
  114. io_lib:format("Duplicates of module ~s: ~s ~s", [Mod, File1, File2]);
  115. format_error({output_file_error, File, Error}) ->
  116. Error1 = file:format_error(Error),
  117. io_lib:format("Failed to write to ~s: ~s", [File, Error1]);
  118. format_error(Reason) ->
  119. io_lib:format("~p", [Reason]).
  120. %% Internal functions
  121. get_plt(State) ->
  122. Prefix = get_config(State, plt_prefix, ?PLT_PREFIX),
  123. Name = plt_name(Prefix),
  124. case get_config(State, plt_location, local) of
  125. local ->
  126. BaseDir = rebar_dir:base_dir(State),
  127. filename:join(BaseDir, Name);
  128. Dir ->
  129. filename:join(Dir, Name)
  130. end.
  131. plt_name(Prefix) ->
  132. Prefix ++ "_" ++ rebar_utils:otp_release() ++ "_plt".
  133. do(State, Plt) ->
  134. Output = get_output_file(State),
  135. {PltWarnings, State1} = update_proj_plt(State, Plt, Output),
  136. {Warnings, State2} = succ_typings(State1, Plt, Output),
  137. case PltWarnings + Warnings of
  138. 0 ->
  139. {ok, State2};
  140. TotalWarnings ->
  141. ?INFO("Warnings written to ~s", [Output]),
  142. throw({dialyzer_warnings, TotalWarnings})
  143. end.
  144. get_output_file(State) ->
  145. BaseDir = rebar_dir:base_dir(State),
  146. Output = filename:join(BaseDir, default_output_file()),
  147. case file:open(Output, [write]) of
  148. {ok, File} ->
  149. ok = file:close(File),
  150. Output;
  151. {error, Reason} ->
  152. throw({output_file_error, Output, Reason})
  153. end.
  154. default_output_file() ->
  155. rebar_utils:otp_release() ++ ".dialyzer_warnings".
  156. update_proj_plt(State, Plt, Output) ->
  157. {Args, _} = rebar_state:command_parsed_args(State),
  158. case proplists:get_value(update_plt, Args) of
  159. false ->
  160. {0, State};
  161. _ ->
  162. do_update_proj_plt(State, Plt, Output)
  163. end.
  164. do_update_proj_plt(State, Plt, Output) ->
  165. ?INFO("Updating plt...", []),
  166. Files = proj_plt_files(State),
  167. case read_plt(State, Plt) of
  168. {ok, OldFiles} ->
  169. check_plt(State, Plt, Output, OldFiles, Files);
  170. error ->
  171. build_proj_plt(State, Plt, Output, Files)
  172. end.
  173. proj_plt_files(State) ->
  174. BasePltApps = base_plt_apps(State),
  175. PltApps = get_config(State, plt_extra_apps, []) ++ BasePltApps,
  176. BasePltMods = get_config(State, base_plt_mods, []),
  177. PltMods = get_config(State, plt_extra_mods, []) ++ BasePltMods,
  178. Apps = proj_apps(State),
  179. DepApps = proj_deps(State),
  180. get_files(State, DepApps ++ PltApps, Apps -- PltApps, PltMods, []).
  181. proj_apps(State) ->
  182. [ec_cnv:to_atom(rebar_app_info:name(App)) ||
  183. App <- rebar_state:project_apps(State)].
  184. proj_deps(State) ->
  185. Apps = rebar_state:project_apps(State),
  186. DepApps = lists:flatmap(fun rebar_app_info:applications/1, Apps),
  187. case get_config(State, plt_apps, top_level_deps) of
  188. top_level_deps -> DepApps;
  189. all_deps -> collect_nested_dependent_apps(DepApps)
  190. end.
  191. get_files(State, Apps, SkipApps, Mods, SkipMods) ->
  192. ?INFO("Resolving files...", []),
  193. ExcludeApps = get_config(State, exclude_apps, []),
  194. Files = apps_files(Apps, ExcludeApps ++ SkipApps, dict:new()),
  195. ExcludeMods = get_config(State, exclude_mods, []),
  196. Files2 = mods_files(Mods, ExcludeMods ++ SkipMods, Files),
  197. dict:fold(fun(_, File, Acc) -> [File | Acc] end, [], Files2).
  198. apps_files([], _, Files) ->
  199. Files;
  200. apps_files([AppName | DepApps], SkipApps, Files) ->
  201. case lists:member(AppName, SkipApps) of
  202. true ->
  203. apps_files(DepApps, SkipApps, Files);
  204. false ->
  205. AppFiles = app_files(AppName),
  206. ?DEBUG("~s modules: ~p", [AppName, dict:fetch_keys(AppFiles)]),
  207. Files2 = merge_files(Files, AppFiles),
  208. apps_files(DepApps, [AppName | SkipApps], Files2)
  209. end.
  210. app_files(AppName) ->
  211. case app_ebin(AppName) of
  212. {ok, EbinDir} ->
  213. ebin_files(EbinDir);
  214. {error, bad_name} ->
  215. throw({unknown_application, AppName})
  216. end.
  217. app_ebin(AppName) ->
  218. case code:lib_dir(AppName, ebin) of
  219. {error, bad_name} = Error ->
  220. Error;
  221. EbinDir ->
  222. check_ebin(EbinDir)
  223. end.
  224. check_ebin(EbinDir) ->
  225. case filelib:is_dir(EbinDir) of
  226. true ->
  227. {ok, EbinDir};
  228. false ->
  229. {error, bad_name}
  230. end.
  231. ebin_files(EbinDir) ->
  232. Ext = code:objfile_extension(),
  233. Wildcard = "*" ++ Ext,
  234. Files = filelib:wildcard(Wildcard, EbinDir),
  235. Store = fun(File, Mods) ->
  236. Mod = list_to_atom(filename:basename(File, Ext)),
  237. Absname = filename:join(EbinDir, File),
  238. dict:store(Mod, Absname, Mods)
  239. end,
  240. lists:foldl(Store, dict:new(), Files).
  241. merge_files(Files1, Files2) ->
  242. Duplicate = fun(Mod, File1, File2) ->
  243. throw({duplicate_module, Mod, File1, File2})
  244. end,
  245. dict:merge(Duplicate, Files1, Files2).
  246. mods_files(Mods, SkipMods, Files) ->
  247. Keep = fun(File) -> File end,
  248. Ensure = fun(Mod, Acc) ->
  249. case lists:member(Mod, SkipMods) of
  250. true ->
  251. Acc;
  252. false ->
  253. dict:update(Mod, Keep, mod_file(Mod), Acc)
  254. end
  255. end,
  256. Files2 = lists:foldl(Ensure, Files, Mods),
  257. lists:foldl(fun dict:erase/2, Files2, SkipMods).
  258. mod_file(Mod) ->
  259. File = atom_to_list(Mod) ++ code:objfile_extension(),
  260. case code:where_is_file(File) of
  261. non_existing -> throw({unknown_module, Mod});
  262. Absname -> Absname
  263. end.
  264. read_plt(_State, Plt) ->
  265. Vsn = dialyzer_version(),
  266. case plt_files(Plt) of
  267. {ok, Files} when Vsn < {2, 9, 0} ->
  268. % Before dialyzer-2.9 (OTP 18.3) removing a beam file from the PLT
  269. % that no longer exists would crash. Therefore force a rebuild of
  270. % PLT if any files no longer exist.
  271. read_plt_files(Plt, Files);
  272. {ok, _} = Result when Vsn >= {2, 9, 0} ->
  273. Result;
  274. {error, no_such_file} ->
  275. error;
  276. {error, not_valid} ->
  277. error;
  278. {error, read_error} ->
  279. Error = io_lib:format("Could not read the PLT file ~p", [Plt]),
  280. throw({dialyzer_error, Error})
  281. end.
  282. plt_files(Plt) ->
  283. case dialyzer:plt_info(Plt) of
  284. {ok, Info} ->
  285. {ok, proplists:get_value(files, Info, [])};
  286. {error, _} = Error ->
  287. Error
  288. end.
  289. %% If any file no longer exists dialyzer will fail when updating the PLT.
  290. read_plt_files(Plt, Files) ->
  291. case [File || File <- Files, not filelib:is_file(File)] of
  292. [] ->
  293. {ok, Files};
  294. Missing ->
  295. ?INFO("Could not find ~p files in ~p...", [length(Missing), Plt]),
  296. ?DEBUG("Could not find files: ~p", [Missing]),
  297. error
  298. end.
  299. check_plt(State, Plt, Output, OldList, FilesList) ->
  300. Old = sets:from_list(OldList),
  301. Files = sets:from_list(FilesList),
  302. Remove = sets:to_list(sets:subtract(Old, Files)),
  303. {RemWarnings, State1} = remove_plt(State, Plt, Output, Remove),
  304. Check = sets:to_list(sets:intersection(Files, Old)),
  305. {CheckWarnings, State2} = check_plt(State1, Plt, Output, Check),
  306. Add = sets:to_list(sets:subtract(Files, Old)),
  307. {AddWarnings, State3} = add_plt(State2, Plt, Output, Add),
  308. {RemWarnings + CheckWarnings + AddWarnings, State3}.
  309. remove_plt(State, _Plt, _Output, []) ->
  310. {0, State};
  311. remove_plt(State, Plt, Output, Files) ->
  312. ?INFO("Removing ~b files from ~p...", [length(Files), Plt]),
  313. run_plt(State, Plt, Output, plt_remove, Files).
  314. check_plt(State, _Plt, _Output, []) ->
  315. {0, State};
  316. check_plt(State, Plt, Output, Files) ->
  317. ?INFO("Checking ~b files in ~p...", [length(Files), Plt]),
  318. run_plt(State, Plt, Output, plt_check, Files).
  319. add_plt(State, _Plt, _Output, []) ->
  320. {0, State};
  321. add_plt(State, Plt, Output, Files) ->
  322. ?INFO("Adding ~b files to ~p...", [length(Files), Plt]),
  323. run_plt(State, Plt, Output, plt_add, Files).
  324. run_plt(State, Plt, Output, Analysis, Files) ->
  325. GetWarnings = get_config(State, get_warnings, false),
  326. Opts = [{analysis_type, Analysis},
  327. {get_warnings, GetWarnings},
  328. {init_plt, Plt},
  329. {output_plt, Plt},
  330. {from, byte_code},
  331. {files, Files}],
  332. run_dialyzer(State, Opts, Output).
  333. build_proj_plt(State, Plt, Output, Files) ->
  334. BasePlt = get_base_plt(State),
  335. ?INFO("Updating base plt...", []),
  336. BaseFiles = base_plt_files(State),
  337. {BaseWarnings, State1} = update_base_plt(State, BasePlt, Output, BaseFiles),
  338. ?INFO("Copying ~p to ~p...", [BasePlt, Plt]),
  339. _ = filelib:ensure_dir(Plt),
  340. case file:copy(BasePlt, Plt) of
  341. {ok, _} ->
  342. {CheckWarnings, State2} = check_plt(State1, Plt, Output, BaseFiles,
  343. Files),
  344. {BaseWarnings + CheckWarnings, State2};
  345. {error, Reason} ->
  346. Error = io_lib:format("Could not copy PLT from ~p to ~p: ~p",
  347. [BasePlt, Plt, file:format_error(Reason)]),
  348. throw({dialyzer_error, Error})
  349. end.
  350. get_base_plt(State) ->
  351. Prefix = get_config(State, base_plt_prefix, ?PLT_PREFIX),
  352. Name = plt_name(Prefix),
  353. case get_config(State, base_plt_location, global) of
  354. global ->
  355. GlobalCacheDir = rebar_dir:global_cache_dir(rebar_state:opts(State)),
  356. filename:join(GlobalCacheDir, Name);
  357. Dir ->
  358. filename:join(Dir, Name)
  359. end.
  360. base_plt_files(State) ->
  361. BasePltApps = base_plt_apps(State),
  362. BasePltMods = get_config(State, base_plt_mods, []),
  363. get_files(State, BasePltApps, [], BasePltMods, []).
  364. base_plt_apps(State) ->
  365. get_config(State, base_plt_apps, [erts, crypto, kernel, stdlib]).
  366. update_base_plt(State, BasePlt, Output, BaseFiles) ->
  367. case read_plt(State, BasePlt) of
  368. {ok, OldBaseFiles} ->
  369. check_plt(State, BasePlt, Output, OldBaseFiles, BaseFiles);
  370. error ->
  371. _ = filelib:ensure_dir(BasePlt),
  372. build_plt(State, BasePlt, Output, BaseFiles)
  373. end.
  374. build_plt(State, Plt, _, []) ->
  375. ?INFO("Building with no files in ~p...", [Plt]),
  376. Opts = [{get_warnings, false},
  377. {output_plt, Plt},
  378. {apps, [erts]}],
  379. % Create a PLT with erts files and then remove erts files to be left with an
  380. % empty PLT. Dialyzer will crash when trying to build a PLT with an empty
  381. % file list.
  382. _ = dialyzer:run([{analysis_type, plt_build} | Opts]),
  383. _ = dialyzer:run([{analysis_type, plt_remove}, {init_plt, Plt} | Opts]),
  384. {0, State};
  385. build_plt(State, Plt, Output, Files) ->
  386. ?INFO("Building with ~b files in ~p...", [length(Files), Plt]),
  387. GetWarnings = get_config(State, get_warnings, false),
  388. Opts = [{analysis_type, plt_build},
  389. {get_warnings, GetWarnings},
  390. {output_plt, Plt},
  391. {files, Files}],
  392. run_dialyzer(State, Opts, Output).
  393. succ_typings(State, Plt, Output) ->
  394. {Args, _} = rebar_state:command_parsed_args(State),
  395. case proplists:get_value(succ_typings, Args) of
  396. false ->
  397. {0, State};
  398. _ ->
  399. ?INFO("Doing success typing analysis...", []),
  400. Files = proj_files(State),
  401. succ_typings(State, Plt, Output, Files)
  402. end.
  403. succ_typings(State, Plt, _, []) ->
  404. ?INFO("Analyzing no files with ~p...", [Plt]),
  405. {0, State};
  406. succ_typings(State, Plt, Output, Files) ->
  407. ?INFO("Analyzing ~b files with ~p...", [length(Files), Plt]),
  408. Opts = [{analysis_type, succ_typings},
  409. {get_warnings, true},
  410. {from, byte_code},
  411. {files, Files},
  412. {init_plt, Plt}],
  413. run_dialyzer(State, Opts, Output).
  414. proj_files(State) ->
  415. Apps = proj_apps(State),
  416. BasePltApps = get_config(State, base_plt_apps, []),
  417. PltApps = get_config(State, plt_extra_apps, []) ++ BasePltApps,
  418. BasePltMods = get_config(State, base_plt_mods, []),
  419. PltMods = get_config(State, plt_extra_mods, []) ++ BasePltMods,
  420. get_files(State, Apps, PltApps, [], PltMods).
  421. run_dialyzer(State, Opts, Output) ->
  422. %% dialyzer may return callgraph warnings when get_warnings is false
  423. case proplists:get_bool(get_warnings, Opts) of
  424. true ->
  425. WarningsList = get_config(State, warnings, []),
  426. Opts2 = [{warnings, legacy_warnings(WarningsList)},
  427. {check_plt, false} |
  428. Opts],
  429. ?DEBUG("Running dialyzer with options: ~p~n", [Opts2]),
  430. Warnings = format_warnings(rebar_state:opts(State),
  431. Output, dialyzer:run(Opts2)),
  432. {Warnings, State};
  433. false ->
  434. Opts2 = [{warnings, no_warnings()},
  435. {check_plt, false} |
  436. Opts],
  437. ?DEBUG("Running dialyzer with options: ~p~n", [Opts2]),
  438. dialyzer:run(Opts2),
  439. {0, State}
  440. end.
  441. legacy_warnings(Warnings) ->
  442. case dialyzer_version() of
  443. TupleVsn when TupleVsn < {2, 8, 0} ->
  444. [Warning || Warning <- Warnings, Warning =/= unknown];
  445. _ ->
  446. Warnings
  447. end.
  448. format_warnings(Opts, Output, Warnings) ->
  449. Warnings1 = rebar_dialyzer_format:format_warnings(Opts, Warnings),
  450. console_warnings(Warnings1),
  451. file_warnings(Output, Warnings),
  452. length(Warnings).
  453. console_warnings(Warnings) ->
  454. _ = [?CONSOLE("~s", [Warning]) || Warning <- Warnings],
  455. ok.
  456. file_warnings(_, []) ->
  457. ok;
  458. file_warnings(Output, Warnings) ->
  459. Warnings1 = [[dialyzer:format_warning(Warning, fullpath), $\n] || Warning <- Warnings],
  460. case file:write_file(Output, Warnings1, [append]) of
  461. ok ->
  462. ok;
  463. {error, Reason} ->
  464. throw({output_file_error, Output, Reason})
  465. end.
  466. no_warnings() ->
  467. [no_return,
  468. no_unused,
  469. no_improper_lists,
  470. no_fun_app,
  471. no_match,
  472. no_opaque,
  473. no_fail_call,
  474. no_contracts,
  475. no_behaviours,
  476. no_undefined_callbacks].
  477. get_config(State, Key, Default) ->
  478. Config = rebar_state:get(State, dialyzer, []),
  479. proplists:get_value(Key, Config, Default).
  480. -spec collect_nested_dependent_apps([atom()]) -> [atom()].
  481. collect_nested_dependent_apps(RootApps) ->
  482. Deps = lists:foldl(fun collect_nested_dependent_apps/2, sets:new(), RootApps),
  483. sets:to_list(Deps).
  484. -spec collect_nested_dependent_apps(atom(), rebar_set()) -> rebar_set().
  485. collect_nested_dependent_apps(App, Seen) ->
  486. case sets:is_element(App, Seen) of
  487. true ->
  488. Seen;
  489. false ->
  490. Seen1 = sets:add_element(App, Seen),
  491. case code:lib_dir(App) of
  492. {error, _} ->
  493. throw({unknown_application, App});
  494. AppDir ->
  495. case rebar_app_discover:find_app(AppDir, all) of
  496. false ->
  497. throw({unknown_application, App});
  498. {true, AppInfo} ->
  499. lists:foldl(fun collect_nested_dependent_apps/2,
  500. Seen1,
  501. rebar_app_info:applications(AppInfo))
  502. end
  503. end
  504. end.
  505. dialyzer_version() ->
  506. _ = application:load(dialyzer),
  507. {ok, Vsn} = application:get_key(dialyzer, vsn),
  508. case string:tokens(Vsn, ".") of
  509. [Major, Minor] ->
  510. version_tuple(Major, Minor, "0");
  511. [Major, Minor, Patch | _] ->
  512. version_tuple(Major, Minor, Patch)
  513. end.
  514. version_tuple(Major, Minor, Patch) ->
  515. {list_to_integer(Major), list_to_integer(Minor), list_to_integer(Patch)}.