diff options
Diffstat (limited to 'lib/dialyzer/src/dialyzer_iplt.erl')
-rw-r--r-- | lib/dialyzer/src/dialyzer_iplt.erl | 594 |
1 files changed, 594 insertions, 0 deletions
diff --git a/lib/dialyzer/src/dialyzer_iplt.erl b/lib/dialyzer/src/dialyzer_iplt.erl new file mode 100644 index 0000000000..c48e9be48f --- /dev/null +++ b/lib/dialyzer/src/dialyzer_iplt.erl @@ -0,0 +1,594 @@ +%% -*- erlang-indent-level: 2 -*- +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. + +%%%------------------------------------------------------------------- +%%% File : dialyzer_iplt.erl +%%% Description : Interface to display information in the incremental +%%% persistent lookup tables stored in files. +%%% Incremental PLTs index files by module name, rather +%%% than absolute path, for portability, and track warnings +%%% generated per module, in order to cache them for later +%%% analyses. +%%%------------------------------------------------------------------- +-module(dialyzer_iplt). + +-export([check_incremental_plt/3, + included_modules/1, + from_file/1, + get_default_iplt_filename/0, + merge_warnings/3, + plt_and_info_from_file/1, + to_file/4, + is_iplt/1, + to_file_custom_vsn/6 % Used for testing certain kinds of backwards compatibility + ]). + +%% Debug utilities +-export([pp_non_returning/0, pp_mod/1]). + +-export_type([module_md5/0, warning_map/0]). + +-include_lib("stdlib/include/ms_transform.hrl"). + +%%---------------------------------------------------------------------- + +-type deep_string() :: string() | [deep_string()]. + +%%---------------------------------------------------------------------- + +-record(incremental_data, {mod_deps :: dialyzer_callgraph:mod_deps(), + warning_map = none :: warning_map(), + legal_warnings = none :: none | dial_warn_tags()}). + +-include("dialyzer.hrl"). + +-type warning_map() :: none | #{module() := [raw_warning()]}. +-type module_md5() :: {module(), binary()}. +-type module_file_path_lookup() :: #{module() => file:filename()}. + +-record(ifile_plt, + {version = "" :: string(), + module_md5_list = [] :: [module_md5()], + info = term_to_binary(#{}) :: binary(), %% encoded map() + contracts = term_to_binary(#{}) :: binary(), %% encoded map() + callbacks = term_to_binary(#{}) :: binary(), %% encoded map() + types = term_to_binary(#{}) :: binary(), %% encoded map() + exported_types = term_to_binary(#{}) :: binary(), %% encoded sets:set() + incremental_data = term_to_binary(#incremental_data{}) :: #incremental_data{} | binary(), %% encoded #incremental_data{} + implementation_md5 = [] :: [module_md5()]}). + +%%---------------------------------------------------------------------- + + +-spec get_default_iplt_filename() -> file:filename(). + +get_default_iplt_filename() -> + case os:getenv("DIALYZER_IPLT") of + false -> + CacheDir = filename:basedir(user_cache, "erlang"), + filename:join(CacheDir, ".dialyzer_iplt"); + UserSpecPlt -> UserSpecPlt + end. + +-spec plt_and_info_from_file(file:filename()) -> {dialyzer_plt:plt(), #iplt_info{}}. + +plt_and_info_from_file(FileName) -> + from_file(FileName, true). + +-spec from_file(file:filename()) -> dialyzer_plt:plt(). + +from_file(FileName) -> + from_file(FileName, false). + +from_file(FileName, ReturnInfo) -> + Plt = dialyzer_plt:new(), + Fun = fun() -> from_file1(Plt, FileName, ReturnInfo) end, + case subproc(Fun) of + {ok, Return} -> + Return; + {error, Msg} -> + dialyzer_plt:delete(Plt), + plt_error(Msg) + end. + +from_file1(Plt, FileName, ReturnInfo) -> + case get_record_from_file(FileName) of + {ok, Rec} -> + case check_version(Rec) of + error -> + Msg = io_lib:format("Old IPLT file ~ts\n", [FileName]), + {error, Msg}; + ok -> + #ifile_plt{info = CompressedInfo, + contracts = CompressedContracts, + callbacks = CompressedCallbacks, + types = CompressedTypes, + exported_types = CompressedExpTypes} = Rec, + FileInfo = binary_to_term(CompressedInfo), + FileContracts = binary_to_term(CompressedContracts), + FileCallbacks = binary_to_term(CompressedCallbacks), + FileTypes = binary_to_term(CompressedTypes), + FileExpTypes = binary_to_term(CompressedExpTypes), + CallbacksList = maps:to_list(FileCallbacks), + CallbacksByModule = + [{M, [Cb || {{M1,_,_},_} = Cb <- CallbacksList, M1 =:= M]} || + M <- lists:usort([M || {{M,_,_},_} <- CallbacksList])], + #plt{info = ETSInfo, + types = ETSTypes, + contracts = ETSContracts, + callbacks = ETSCallbacks, + exported_types = ETSExpTypes} = Plt, + [true, true, true] = + [ets:insert(ETS, Data) || + {ETS, Data} <- [{ETSInfo, maps:to_list(FileInfo)}, + {ETSTypes, FileTypes}, + {ETSContracts, maps:to_list(FileContracts)}]], + true = ets:insert(ETSCallbacks, CallbacksByModule), + true = ets:insert(ETSExpTypes, [{ET} || + ET <- sets:to_list(FileExpTypes)]), + case ReturnInfo of + false -> {ok, Plt}; + true -> + IncrementalData = get_incremental_data(Rec), + PltInfo = + #iplt_info{files = Rec#ifile_plt.module_md5_list, + mod_deps = IncrementalData#incremental_data.mod_deps, + warning_map = IncrementalData#incremental_data.warning_map, + legal_warnings = IncrementalData#incremental_data.legal_warnings}, + {ok, {Plt, PltInfo}} + end + end; + {error, Reason} -> + Msg = io_lib:format("Could not read IPLT file ~ts: ~p\n", + [FileName, Reason]), + {error, Msg} + end. + +-spec get_incremental_data(#ifile_plt{}) -> #incremental_data{}. +get_incremental_data(#ifile_plt{incremental_data = Data}) -> + case Data of + CompressedData when is_binary(CompressedData) -> + binary_to_term(CompressedData); + UncompressedData = #incremental_data{} -> % To support older PLTs that didn't have this field compressed + UncompressedData + end. + +-type err_rsn() :: 'not_valid' | 'no_such_file' | 'read_error'. + +-spec included_modules(file:filename()) -> {'ok', [module()]} + | {'error', err_rsn()}. + +included_modules(FileName) -> + Fun = fun() -> included_modules1(FileName) end, + subproc(Fun). + +included_modules1(FileName) -> + case get_record_from_file(FileName) of + {ok, #ifile_plt{module_md5_list = Md5}} -> + {ok, [ModuleName || {ModuleName, _} <- Md5]}; + {error, _What} = Error -> + Error + end. + +check_version(#ifile_plt{version = ?VSN, implementation_md5 = ImplMd5}) -> + case compute_new_md5(ImplMd5, [], [], implementation_module_paths()) of + ok -> ok; + {differ, _, _} -> error; + {error, _} -> error + end; +check_version(#ifile_plt{}) -> error. + +get_record_from_file(FileName) -> + case file:read_file(FileName) of + {ok, Bin} -> + try binary_to_term(Bin) of + #ifile_plt{} = FilePLT -> {ok, FilePLT}; + _ -> {error, not_valid} + catch + _:_ -> {error, not_valid} + end; + {error, enoent} -> + {error, no_such_file}; + {error, _} -> + {error, read_error} + end. + +-spec is_iplt(file:filename()) -> boolean(). +is_iplt(FileName) -> + case get_record_from_file(FileName) of + {ok, _} -> true; + {error, _} -> false + end. + +-spec to_file(file:filename(), dialyzer_plt:plt(), dialyzer_callgraph:mod_deps(), #iplt_info{}) -> 'ok'. + +%% Write the PLT to file, and deletes the PLT. +to_file(FileName, Plt, ModDeps, PLTInfo) -> + Fun = fun() -> to_file1(FileName, Plt, ModDeps, PLTInfo) end, + Return = subproc(Fun), + dialyzer_plt:delete(Plt), + case Return of + ok -> ok; + {error, Msg} -> plt_error(Msg) + end. + +to_file1(FileName, Plt, ModDeps, PltInfo) -> + to_file_custom_vsn(FileName, Plt, ModDeps, PltInfo, none, none). + +-spec to_file_custom_vsn( + file:filename(), + dialyzer_plt:plt(), + dialyzer_callgraph:mod_deps(), + #iplt_info{}, + none | string(), + none | [module_md5()]) -> 'ok'. + +to_file_custom_vsn(FileName, Plt, ModDeps, PltInfo, none, ImplMd5) -> + to_file_custom_vsn(FileName, Plt, ModDeps, PltInfo, ?VSN, ImplMd5); + +to_file_custom_vsn(FileName, Plt, ModDeps, PltInfo, Vsn, none) -> + to_file_custom_vsn(FileName, Plt, ModDeps, PltInfo, Vsn, compute_implementation_md5()); + +to_file_custom_vsn( + FileName, + #plt{info = ETSInfo, types = ETSTypes, contracts = ETSContracts, + callbacks = ETSCallbacks, exported_types = ETSExpTypes}, + NewModDeps, + #iplt_info{files = MD5, mod_deps = OldModDeps, warning_map=NewWarningMap, legal_warnings=LegalWarnings}, + Vsn, + ImplMd5) -> + CombinedModDeps = dict:merge(fun(_Key, OldVal, NewVal) -> + ordsets:union(OldVal, NewVal) + end, + OldModDeps, NewModDeps), + IncrementalData = + #incremental_data{mod_deps=CombinedModDeps, warning_map=NewWarningMap, legal_warnings=LegalWarnings}, + Callbacks = + #{K => V || + {_M, Cbs} <- dialyzer_utils:ets_tab2list(ETSCallbacks), + {K, V} <- Cbs}, + Info = maps:from_list(dialyzer_utils:ets_tab2list(ETSInfo)), + Types = dialyzer_utils:ets_tab2list(ETSTypes), + Contracts = maps:from_list(dialyzer_utils:ets_tab2list(ETSContracts)), + ExpTypes = sets:from_list([E || {E} <- dialyzer_utils:ets_tab2list(ETSExpTypes)], [{version, 2}]), + Record = #ifile_plt{version = Vsn, + module_md5_list = MD5, + info = term_to_binary(Info, [{compressed,9}]), + contracts = term_to_binary(Contracts, [{compressed,9}]), + callbacks = term_to_binary(Callbacks, [{compressed,9}]), + types = term_to_binary(Types, [{compressed,9}]), + exported_types = term_to_binary(ExpTypes, [{compressed,9}]), + incremental_data = term_to_binary(IncrementalData, [{compressed,9}]), + implementation_md5 = ImplMd5}, + Bin = term_to_binary(Record), + case file:write_file(FileName, Bin) of + ok -> ok; + {error, Reason} -> + Msg = io_lib:format("Could not write IPLT file ~ts: ~w\n", + [FileName, Reason]), + {error, Msg} + end. + +-spec merge_warnings(none | [raw_warning], [{module(), raw_warning()}], none | warning_map()) -> none | warning_map(). +merge_warnings(none, _, OldWarningMap) -> OldWarningMap; +merge_warnings(NewWarnings, UnknownWarnings, none) -> + convert_to_warning_map(NewWarnings, UnknownWarnings); +merge_warnings(NewWarnings, UnknownWarnings, OldWarningMap) -> + maps:merge(convert_to_warning_map(NewWarnings, UnknownWarnings), OldWarningMap). + +convert_to_warning_map(WarningList, UnknownWarnings) -> + Temp = lists:foldl( + fun({_, {_, _, MorMFA}, _} = Warn, Acc) -> + Update = fun(Old) -> [Warn|Old] end, + maps:update_with(get_module(MorMFA), Update, [Warn], Acc) + end, + #{}, + WarningList), + lists:foldl(fun({M, Warn}, Acc) -> + Update = fun(Old) -> [Warn|Old] end, + maps:update_with(M, Update, [Warn], Acc) + end, + Temp, + UnknownWarnings). + +get_module({M,_F,_A}) -> M; +get_module(M) -> M. + +-type md5_diff() :: [{'differ', atom()} | {'removed', atom()}]. +-type check_error() :: err_rsn() | {'no_file_to_remove', file:filename()}. + +-spec check_incremental_plt(file:filename(), #options{}, [file:filename()]) -> + {'ok', #iplt_info{}, module_file_path_lookup()} | + {'old_version', [module_md5()], module_file_path_lookup()} | + {'new_file', [module_md5()], module_file_path_lookup()} | + {'differ', [module_md5()], md5_diff(), dialyzer_callgraph:mod_deps(), warning_map(), module_file_path_lookup()} | + {'legal_warnings_changed', [module_md5()], module_file_path_lookup()} | + {'error', check_error()}. +check_incremental_plt(FileName, Opts, PltFiles) -> + Fun = fun() -> check_incremental_plt1(FileName, Opts, PltFiles) end, + subproc(Fun). + +check_incremental_plt1(FileName, Opts, PltFiles) -> + PltModulePathLookup = #{beam_file_to_module(PltFile) => PltFile || PltFile <- PltFiles}, + case get_record_from_file(FileName) of + {ok, #ifile_plt{module_md5_list = Md5} = Rec} -> + {RemoveModules, AddModules} = find_files_to_remove_and_add(Md5, maps:keys(PltModulePathLookup)), + IncrementalData = get_incremental_data(Rec), + PltLegalWarnings = IncrementalData#incremental_data.legal_warnings, + LegalWarnings = Opts#options.legal_warnings, + LegalWarningsMatch = PltLegalWarnings /= none andalso lists:usort(PltLegalWarnings) =:= lists:usort(LegalWarnings), + case check_version_and_compute_md5(Rec, RemoveModules, AddModules, PltModulePathLookup) of + ok when not LegalWarningsMatch -> + {legal_warnings_changed, Md5, PltModulePathLookup}; + {differ, NewMd5, _, _, _} when not LegalWarningsMatch -> + {legal_warnings_changed, NewMd5, PltModulePathLookup}; + ok -> + {ok, #iplt_info{files = Md5, + mod_deps = IncrementalData#incremental_data.mod_deps, + warning_map = IncrementalData#incremental_data.warning_map, + legal_warnings = IncrementalData#incremental_data.legal_warnings}, + PltModulePathLookup}; + {old_version, Md5} -> + {old_version, Md5}; + {differ, NewMd5, DiffMd5, ModDeps, _} -> + {differ, NewMd5, DiffMd5, ModDeps, IncrementalData#incremental_data.warning_map, PltModulePathLookup}; + {old_version, NewMd5} -> + {old_version, NewMd5, PltModulePathLookup}; + {error, Error} -> + {error, Error} + end; + {error, no_such_file} -> + {new_file, compute_md5_from_files(PltModulePathLookup), PltModulePathLookup}; + Error -> Error + end. + +find_files_to_remove_and_add(Md5, PltModules) -> + OldPltFiles = gb_sets:from_list([Name || {Name,_Md5Bin} <- Md5]), + NewPltFiles = gb_sets:from_list(PltModules), + {gb_sets:to_list(gb_sets:subtract(OldPltFiles, NewPltFiles)), + gb_sets:to_list(gb_sets:subtract(NewPltFiles, OldPltFiles))}. + +check_version_and_compute_md5(Rec, RemoveFiles, AddFiles, ModuleToPathLookup) -> + Md5 = Rec#ifile_plt.module_md5_list, + case check_version(Rec) of + ok -> + case compute_new_md5(Md5, RemoveFiles, AddFiles, ModuleToPathLookup) of + ok -> ok; + {differ, NewMd5, DiffMd5} -> + IncrementalData = get_incremental_data(Rec), + {differ, + NewMd5, + DiffMd5, + IncrementalData#incremental_data.mod_deps, + IncrementalData#incremental_data.warning_map}; + {error, _What} = Err -> Err + end; + error -> + case compute_new_md5(Md5, RemoveFiles, AddFiles, ModuleToPathLookup) of + ok -> {old_version, Md5}; + {differ, NewMd5, _DiffMd5} -> {old_version, NewMd5}; + {error, _What} = Err -> Err + end + end. + +compute_new_md5(Md5, [], [], ModuleToPathLookup) -> + compute_new_md5_1(Md5, [], ModuleToPathLookup); +compute_new_md5(Md5, RemoveFiles0, AddFiles0, ModuleToPathLookup) -> + %% Assume that files are first removed and then added. Files that + %% are both removed and added will be checked for consistency in the + %% normal way. + RemoveFiles = RemoveFiles0 -- AddFiles0, + AddFiles = AddFiles0 -- RemoveFiles0, + InitDiffList = init_diff_list(RemoveFiles, AddFiles), + case init_md5_list(Md5, RemoveFiles, AddFiles) of + {ok, NewMd5} -> compute_new_md5_1(NewMd5, InitDiffList, ModuleToPathLookup); + {error, _What} = Error -> Error + end. + +compute_new_md5_1(Entries, InitDiffs, ModuleToPathLookup) -> + Modules = [Module || {Module, _Md5} <- Entries], + ExistingHashes = [Md5 || {_Module, Md5} <- Entries], + Files = [maps:get(Module, ModuleToPathLookup) || Module <- Modules], + NewHashes = dialyzer_utils:p_map(fun compute_md5_from_file/1, Files), + Diffs = + lists:zipwith3( + fun (Module, BeforeHash, AfterHash) -> + case BeforeHash of + AfterHash -> + none; + _ -> + {differ, Module} + end + end, + Modules, + ExistingHashes, + NewHashes), + Diffs1 = InitDiffs ++ lists:filter(fun ({differ,_}) -> true; (none) -> false end, Diffs), + case Diffs1 of + [] -> + ok; + _ -> + ModuleHashes = lists:zip(Modules, NewHashes), + {differ, lists:keysort(1, ModuleHashes), Diffs1} + end. + +-spec implementation_module_paths() -> module_file_path_lookup(). +implementation_module_paths() -> + Dir = code:lib_dir(dialyzer), + Files1 = ["erl_bif_types.beam", "erl_types.beam"], + Files2 = [filename:join([Dir, "ebin", F]) || F <- Files1], + #{beam_file_to_module(File) => File || File <- Files2}. + +-spec compute_implementation_md5() -> [module_md5()]. + +compute_implementation_md5() -> + Modules = implementation_module_paths(), + compute_md5_from_files(Modules). + +-spec compute_md5_from_files(module_file_path_lookup()) -> [module_md5()]. + +compute_md5_from_files(ModuleToPathLookup) -> + {Modules,Files} = lists:unzip(maps:to_list(ModuleToPathLookup)), + Hashes = dialyzer_utils:p_map(fun compute_md5_from_file/1, Files), + lists:keysort(1, lists:zip(Modules, Hashes)). + +compute_md5_from_file(File) -> + case beam_lib:chunks(File, [debug_info]) of + {ok, {ModuleName, [{debug_info, {debug_info_v1, Backend, Data}}]}} -> + %% We cannot use beam_lib:md5 because it includes + %% non-portable or otherwise irrelvant data that would + %% cause the PLT to be invalidated needlessly too often + case Backend:debug_info(erlang_v1, ModuleName, Data, []) of + {ok, Code} -> + StabilisedCode = lists:filtermap(fun (Form) -> make_stable(ModuleName, Form) end, Code), + StabilisedCodeBin = erlang:term_to_binary(StabilisedCode), + erlang:md5(StabilisedCodeBin); + {error, Reason} -> + Msg = io_lib:format("Could not compute MD5 for .beam (debug_info error) - did you forget to set the debug_info compilation option? ~ts ~tw\n", [File, Reason]), + throw({dialyzer_error, Msg}) + end; + {ok, {_, [{debug_info, no_debug_info}]}} -> + Msg = io_lib:format("Could not compute MD5 for .beam (debug_info missing): ~ts\n", [File]), + throw({dialyzer_error, Msg}); + {error, beam_lib, {file_error, _, enoent}} -> + Msg = io_lib:format("File not found: ~ts\n", [File]), + plt_error(Msg); + {error, beam_lib, _} -> + Msg = io_lib:format("Could not compute MD5 for .beam: ~ts\n", [File]), + plt_error(Msg) + end. + +%% Absolute paths in -file attributes make beam file hashes brittle, since the +%% same beam built elsewhere will contain a different absolute path, despite +%% being semantically identical. +%% +%% Here, we replace the full path with just the basename. This is very similar +%% to the effect of the +deterministic option, by by doing this rewriting here, +%% we gain some of those determinism benefits even when the build is not run +%% with +deterministic. +make_stable(_, {attribute, Anno, file, {SrcFilePath, Line}}) -> + {true, {attribute, Anno, file, {filename:basename(SrcFilePath), Line}}}; + +make_stable(_, Attr) -> + {true, Attr}. + +init_diff_list(RemoveFiles, AddFiles) -> + RemoveSet0 = sets:from_list([beam_file_to_module(F) || F <- RemoveFiles]), + AddSet0 = sets:from_list([beam_file_to_module(F) || F <- AddFiles]), + DiffSet = sets:intersection(AddSet0, RemoveSet0), + RemoveSet = sets:subtract(RemoveSet0, DiffSet), + %% Added files and diff files will appear as diff files from the md5 check. + [{removed, F} || F <- sets:to_list(RemoveSet)]. + +init_md5_list(Md5, RemoveFiles, AddFiles) -> + Mods = [{remove, beam_file_to_module(F)} || F <- RemoveFiles] ++ [{add, beam_file_to_module(F)} || F <- AddFiles], + DiffMods = lists:keysort(2, Mods), + Md5Sorted = lists:keysort(1, Md5), + init_md5_list_1(Md5Sorted, DiffMods, []). + +init_md5_list_1([{Mod, _Md5}|Md5Left], [{remove, Mod}|DiffLeft], Acc) -> + init_md5_list_1(Md5Left, DiffLeft, Acc); +init_md5_list_1([{Mod, _Md5} = Entry|Md5Left], [{add, Mod}|DiffLeft], Acc) -> + init_md5_list_1(Md5Left, DiffLeft, [Entry|Acc]); +init_md5_list_1([{Mod1, _Md5} = Entry|Md5Left] = Md5List, + [{Tag, Mod2}|DiffLeft] = DiffList, Acc) -> + case Mod1 < Mod2 of + true -> init_md5_list_1(Md5Left, DiffList, [Entry|Acc]); + false -> + %% Just an assert. + true = Mod1 > Mod2, + case Tag of + add -> init_md5_list_1(Md5List, DiffLeft, [{Mod2, <<>>}|Acc]); + remove -> {error, {no_file_to_remove, Mod2}} + end + end; +init_md5_list_1([], DiffList, Acc) -> + AddMods = [{M, <<>>} || {add, M} <- DiffList], + {ok, lists:reverse(Acc, AddMods)}; +init_md5_list_1(Md5List, [], Acc) -> + {ok, lists:reverse(Acc, Md5List)}. + + +subproc(Fun) -> + F = fun() -> + exit(try Fun() + catch throw:T -> + {thrown, T} + end) + end, + {Pid, Ref} = erlang:spawn_monitor(F), + receive {'DOWN', Ref, process, Pid, Return} -> + case Return of + {thrown, T} -> throw(T); + _ -> Return + end + end. + + +beam_file_to_module(Filename) -> + list_to_atom(filename:basename(Filename, ".beam")). + + +-spec plt_error(deep_string()) -> no_return(). + +plt_error(Msg) -> + throw({dialyzer_error, lists:flatten(Msg)}). + + +%%--------------------------------------------------------------------------- +%% Debug utilities. + +-spec pp_non_returning() -> 'ok'. + +pp_non_returning() -> + PltFile = get_default_iplt_filename(), + Plt = from_file(PltFile), + List = ets:tab2list(Plt#plt.info), + Unit = [{MFA, erl_types:t_fun(Args, Ret)} || {MFA, {Ret, Args}} <- List, + erl_types:t_is_unit(Ret)], + None = [{MFA, erl_types:t_fun(Args, Ret)} || {MFA, {Ret, Args}} <- List, + erl_types:t_is_none(Ret)], + io:format("=========================================\n"), + io:format("= Loops =\n"), + io:format("=========================================\n\n"), + lists:foreach(fun({{M, F, _}, Type}) -> + io:format("~w:~tw~ts.\n", + [M, F, dialyzer_utils:format_sig(Type)]) + end, lists:sort(Unit)), + io:format("\n"), + io:format("=========================================\n"), + io:format("= Errors =\n"), + io:format("=========================================\n\n"), + lists:foreach(fun({{M, F, _}, Type}) -> + io:format("~w:~w~s.\n", + [M, F, dialyzer_utils:format_sig(Type)]) + end, lists:sort(None)), + dialyzer_plt:delete(Plt). + +-spec pp_mod(atom()) -> 'ok'. + +pp_mod(Mod) when is_atom(Mod) -> + PltFile = get_default_iplt_filename(), + Plt = from_file(PltFile), + case dialyzer_plt:lookup_module(Plt, Mod) of + {value, List} -> + lists:foreach(fun({{_, F, _}, Ret, Args}) -> + T = erl_types:t_fun(Args, Ret), + S = dialyzer_utils:format_sig(T), + io:format("-spec ~tw~ts.\n", [F, S]) + end, lists:sort(List)); + none -> + io:format("dialyzer: Found no module named '~s' in the IPLT\n", [Mod]) + end, + dialyzer_plt:delete(Plt). |