diff options
Diffstat (limited to 'lib/dialyzer/src/typer_core.erl')
-rw-r--r-- | lib/dialyzer/src/typer_core.erl | 1049 |
1 files changed, 1049 insertions, 0 deletions
diff --git a/lib/dialyzer/src/typer_core.erl b/lib/dialyzer/src/typer_core.erl new file mode 100644 index 0000000000..c9051f825e --- /dev/null +++ b/lib/dialyzer/src/typer_core.erl @@ -0,0 +1,1049 @@ +%% -*- 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 : typer_core.erl +%% Author(s) : The first version of typer was written by Bingwen He +%% with guidance from Kostis Sagonas and Tobias Lindahl. +%% Since June 2008 typer is maintained by Kostis Sagonas. +%% On 2022, Brujo Benavides, Pablo Costas, and Mackenzie +%% Morgan from NextRoll started working on the rebar3 +%% plugin for typer and, with that in mind, splitted typer +%% and typer_core apart. +%% Description : An Erlang/OTP module that shows type information +%% for Erlang modules to the user. Additionally, it can +%% annotate the code of files with such type information. +%%----------------------------------------------------------------------- + +-module(typer_core). + +-export([run/1]). + +%%----------------------------------------------------------------------- + +-define(SHOW, show). +-define(SHOW_EXPORTED, show_exported). +-define(ANNOTATE, annotate). +-define(ANNOTATE_IN_PLACE, annotate_in_place). +-define(ANNOTATE_INC_FILES, annotate_inc_files). + +-type mode() :: ?SHOW | ?SHOW_EXPORTED | ?ANNOTATE | ?ANNOTATE_IN_PLACE | ?ANNOTATE_INC_FILES. + +%%----------------------------------------------------------------------- + +-type files() :: [file:filename()]. +-type callgraph() :: dialyzer_callgraph:callgraph(). +-type codeserver() :: dialyzer_codeserver:codeserver(). +-type plt() :: dialyzer_plt:plt(). +-type printer(Return) :: fun((io:format(), [term()]) -> Return). +-type io() :: + #{debug := printer(_), + info := printer(_), + warn := printer(_), + abort := printer(no_return())}. +-type opts() :: + #{mode := mode(), + show_succ => boolean(), + no_spec => boolean(), + edoc => boolean(), + plt => file:filename(), + trusted => files(), + files => files(), + files_r => files(), + macros => [{atom(), term()}], + includes => files(), + io => io()}. + +-export_type([mode/0, opts/0]). + +-record(analysis, + {mode :: mode() | 'undefined', + macros = [] :: [{atom(), term()}], + includes = [] :: files(), + codeserver = dialyzer_codeserver:new():: codeserver(), + callgraph = dialyzer_callgraph:new() :: callgraph(), + files = [] :: files(), % absolute names + plt = none :: 'none' | file:filename(), + no_spec = false :: boolean(), + show_succ = false :: boolean(), + %% For choosing between specs or edoc @spec comments + edoc = false :: boolean(), + %% Files in 'fms' are compilable with option 'to_pp'; we keep them + %% as {FileName, ModuleName} in case the ModuleName is different + fms = [] :: [{file:filename(), module()}], + ex_func = maps:new() :: #{file:filename() => [func_info()]}, + record = maps:new() :: #{file:filename() => erl_types:type_table()}, + func = maps:new() :: #{file:filename() => [func_info()]}, + inc_func = maps:new() :: #{file:filename() => [inc_file_info()]}, + trust_plt = dialyzer_plt:new() :: plt(), + io = default_io() :: io()}). +-type analysis() :: #analysis{}. + +-record(args, {files = [] :: files(), + files_r = [] :: files(), + trusted = [] :: files()}). +-type args() :: #args{}. + +%%-------------------------------------------------------------------- + +-spec run(opts()) -> ok. + +run(Opts) -> + {Args, Analysis} = process_cl_args(Opts), + msg(debug, "Opts: ~p\nArgs: ~p\nAnalysis: ~p", [Opts, Args, Analysis], Analysis), + Timer = dialyzer_timing:init(false), + TrustedFiles = filter_fd(Args#args.trusted, [], fun is_erl_file/1, Analysis), + Analysis2 = extract(Analysis, TrustedFiles), + All_Files = get_all_files(Args, Analysis2), + msg(debug, "All_Files: ~tp", [All_Files], Analysis2), + Analysis3 = Analysis2#analysis{files = All_Files}, + Analysis4 = collect_info(Analysis3), + msg(debug, "Final: ~p", [Analysis4#analysis.fms], Analysis3), + TypeInfo = get_type_info(Analysis4), + dialyzer_timing:stop(Timer), + show_or_annotate(TypeInfo), + msg(debug, "\nTyper analysis finished", [], Analysis4), + ok. + +%%-------------------------------------------------------------------- + +-spec extract(analysis(), files()) -> analysis(). + +extract(#analysis{macros = Macros, + includes = Includes, + trust_plt = TrustPLT} = Analysis, TrustedFiles) -> + msg(debug, "Extracting trusted typer info...", [], Analysis), + Ds = [{d, Name, Value} || {Name, Value} <- Macros], + CodeServer = dialyzer_codeserver:new(), + Fun = + fun(File, CS) -> + %% We include one more dir; the one above the one we are trusting + %% E.g, for /home/tests/typer_ann/test.ann.erl, we should include + %% /home/tests/ rather than /home/tests/typer_ann/ + AllIncludes = [filename:dirname(filename:dirname(File)) | Includes], + Is = [{i, Dir} || Dir <- AllIncludes], + CompOpts = dialyzer_utils:src_compiler_opts() ++ Is ++ Ds, + case dialyzer_utils:get_core_from_src(File, CompOpts) of + {ok, Core} -> + case dialyzer_utils:get_record_and_type_info(Core) of + {ok, RecDict} -> + Mod = list_to_atom(filename:basename(File, ".erl")), + case dialyzer_utils:get_spec_info(Mod, Core, RecDict) of + {ok, SpecDict, CbDict} -> + CS1 = dialyzer_codeserver:store_temp_records(Mod, RecDict, CS), + dialyzer_codeserver:store_temp_contracts(Mod, SpecDict, CbDict, CS1); + {error, Reason} -> compile_error([Reason], Analysis) + end; + {error, Reason} -> compile_error([Reason], Analysis) + end; + {error, Reason} -> compile_error(Reason, Analysis) + end + end, + CodeServer1 = lists:foldl(Fun, CodeServer, TrustedFiles), + %% Process remote types + NewCodeServer = + try + CodeServer2 = + dialyzer_utils:merge_types(CodeServer1, + TrustPLT), % XXX change to the PLT? + NewExpTypes = dialyzer_codeserver:get_temp_exported_types(CodeServer1), + case sets:size(NewExpTypes) of 0 -> ok end, + CodeServer3 = dialyzer_codeserver:finalize_exported_types(NewExpTypes, CodeServer2), + CodeServer4 = dialyzer_utils:process_record_remote_types(CodeServer3), + dialyzer_contracts:process_contract_remote_types(CodeServer4) + catch + throw:{error, ErrorMsg} -> + compile_error(ErrorMsg, Analysis) + end, + %% Create TrustPLT + ContractsDict = dialyzer_codeserver:get_contracts(NewCodeServer), + Contracts = orddict:from_list(dict:to_list(ContractsDict)), + NewTrustPLT = dialyzer_plt:insert_contract_list(TrustPLT, Contracts), + Analysis#analysis{trust_plt = NewTrustPLT}. + +%%-------------------------------------------------------------------- + +-spec get_type_info(analysis()) -> analysis(). + +get_type_info(#analysis{callgraph = CallGraph, + trust_plt = TrustPLT, + codeserver = CodeServer} = Analysis) -> + StrippedCallGraph = remove_external(CallGraph, TrustPLT, Analysis), + msg(debug, "Analyizing callgraph...", [], Analysis), + try + NewPlt = dialyzer_succ_typings:analyze_callgraph(StrippedCallGraph, + TrustPLT, + CodeServer, + none, []), + Analysis#analysis{callgraph = StrippedCallGraph, trust_plt = NewPlt} + catch + error:What:Stacktrace -> + fatal_error(io_lib:format("Analysis failed with message: ~tp", + [{What, Stacktrace}]), Analysis); + throw:{dialyzer_succ_typing_error, Msg} -> + fatal_error(io_lib:format("Analysis failed with message: ~ts", [Msg]), Analysis) + end. + +-spec remove_external(callgraph(), plt(), analysis()) -> callgraph(). + +remove_external(CallGraph, PLT, Analysis) -> + {StrippedCG0, Ext} = dialyzer_callgraph:remove_external(CallGraph), + case get_external(Ext, PLT) of + [] -> ok; + Externals -> + msg(warn, " Unknown functions: ~tp", [lists:usort(Externals)], Analysis), + ExtTypes = rcv_ext_types(), + case ExtTypes of + [] -> ok; + _ -> msg(warn, " Unknown types: ~tp", [ExtTypes], Analysis) + end + end, + StrippedCG0. + +-spec get_external([{mfa(), mfa()}], plt()) -> [mfa()]. + +get_external(Exts, Plt) -> + Fun = fun ({_From, To = {M, F, A}}, Acc) -> + case dialyzer_plt:contains_mfa(Plt, To) of + false -> + case erl_bif_types:is_known(M, F, A) of + true -> Acc; + false -> [To|Acc] + end; + true -> Acc + end + end, + lists:foldl(Fun, [], Exts). + +%%-------------------------------------------------------------------- +%% Showing type information or annotating files with such information. +%%-------------------------------------------------------------------- + +-define(TYPER_ANN_DIR, "typer_ann"). + +-type line() :: non_neg_integer(). +-type func_info() :: {line(), atom(), arity()}. +-type func_name() :: {atom(), arity()}. +-type func_type() :: + {contract, dialyzer_contract()} | + {erl_types:erl_type(), [erl_types:erl_type()] | arity()}. +-type func_types() :: #{func_name() => func_type()}. +%% This is a record (#contract{}) defined in dialyzer.hrl +%% but we can't include_lib that file since it also defines +%% #analysis +-type dialyzer_contract() :: tuple(). + +-record(info, {records = maps:new() :: erl_types:type_table(), + functions = [] :: [func_info()], + types = maps:new() :: func_types(), + edoc = false :: boolean()}). +-record(inc, + {map = maps:new() :: #{file:filename() => [{func_name(), {pos_integer(), func_type()}}]}, + filter = [] :: [file:filename()]}). +-type inc() :: #inc{}. + +-spec show_or_annotate(analysis()) -> 'ok'. + +show_or_annotate(#analysis{mode = Mode, fms = Files} = Analysis) -> + case Mode of + ?SHOW -> show(Analysis); + ?SHOW_EXPORTED -> show(Analysis); + Mode when Mode == ?ANNOTATE; Mode == ?ANNOTATE_IN_PLACE -> + Fun = fun ({File, Module}) -> + Info = get_final_info(File, Module, Analysis), + write_typed_file(File, Info, Analysis) + end, + lists:foreach(Fun, Files); + ?ANNOTATE_INC_FILES -> + IncInfo = write_and_collect_inc_info(Analysis), + write_inc_files(IncInfo, Analysis) + end. + +write_and_collect_inc_info(Analysis) -> + Fun = fun ({File, Module}, Inc) -> + Info = get_final_info(File, Module, Analysis), + write_typed_file(File, Info, Analysis), + IncFuns = get_inc_functions(File, Analysis), + collect_imported_functions(IncFuns, Info#info.types, Inc, Analysis) + end, + NewInc = lists:foldl(Fun, #inc{}, Analysis#analysis.fms), + clean_inc(NewInc). + +write_inc_files(Inc, Analysis) -> + Fun = + fun (File) -> + Val = maps:get(File, Inc#inc.map, none), + %% Val is function with its type info + %% in form [{{Line,F,A},Type}] + Functions = [Key || {Key, _} <- Val], + Val1 = [{{F,A},Type} || {{_Line,F,A},Type} <- Val], + Info = #info{types = maps:from_list(Val1), + records = maps:new(), + %% Note we need to sort functions here! + functions = lists:keysort(1, Functions)}, + msg(debug, "Types ~tp", [Info#info.types], Analysis), + msg(debug, "Functions ~tp", [Info#info.functions], Analysis), + msg(debug, "Records ~tp", [Info#info.records], Analysis), + write_typed_file(File, Info, Analysis) + end, + lists:foreach(Fun, maps:keys(Inc#inc.map)). + +show(Analysis) -> + Fun = fun ({File, Module}) -> + Info = get_final_info(File, Module, Analysis), + show_type_info(File, Info, Analysis) + end, + lists:foreach(Fun, Analysis#analysis.fms). + +get_final_info(File, Module, Analysis) -> + Records = get_records(File, Analysis), + Types = get_types(Module, Analysis, Records), + Functions = get_functions(File, Analysis), + Edoc = Analysis#analysis.edoc, + #info{records = Records, functions = Functions, types = Types, edoc = Edoc}. + +collect_imported_functions(Functions, Types, Inc, Analysis) -> + %% Coming from other sourses, including: + %% FIXME: How to deal with yecc-generated file???? + %% --.yrl (yecc-generated file)??? + %% -- yeccpre.hrl (yecc-generated file)??? + %% -- other cases + Fun = fun ({File, _} = Obj, I) -> + case is_yecc_gen(File, I) of + {true, NewI} -> NewI; + {false, NewI} -> + check_imported_functions(Obj, NewI, Types, Analysis) + end + end, + lists:foldl(Fun, Inc, Functions). + +-spec is_yecc_gen(file:filename(), inc()) -> {boolean(), inc()}. + +is_yecc_gen(File, #inc{filter = Fs} = Inc) -> + case lists:member(File, Fs) of + true -> {true, Inc}; + false -> + case filename:extension(File) of + ".yrl" -> + Rootname = filename:rootname(File, ".yrl"), + Obj = Rootname ++ ".erl", + case lists:member(Obj, Fs) of + true -> {true, Inc}; + false -> + NewInc = Inc#inc{filter = [Obj|Fs]}, + {true, NewInc} + end; + _ -> + case filename:basename(File) of + "yeccpre.hrl" -> {true, Inc}; + _ -> {false, Inc} + end + end + end. + +check_imported_functions({File, {Line, F, A}}, Inc, Types, Analysis) -> + IncMap = Inc#inc.map, + FA = {F, A}, + Type = get_type_info(FA, Types, Analysis), + case IncMap of + #{File := Val} -> %% File is already in. Check. + case lists:keyfind(FA, 1, Val) of + false -> + %% Function is not in; add it + Inc#inc{map = IncMap#{File => Val ++ [{FA, {Line, Type}}]}}; + {FA, {_, Type}} -> + %% Function is in and with same type + Inc; + _ -> + %% Function is in but with diff type + inc_warning(FA, File, Analysis), + Elem = lists:keydelete(FA, 1, Val), + NewMap = + case Elem of + [] -> maps:remove(File, IncMap); + _ -> IncMap#{File => Elem} + end, + Inc#inc{map = NewMap} + end; + _ -> %% File is not added. Add it + NewMap = IncMap#{File => [{FA, {Line, Type}}]}, + Inc#inc{map = NewMap} + end. + +inc_warning({F, A}, File, Analysis) -> + msg(warn, + " ***Warning: Skip function ~tp/~p " + "in file ~tp because of inconsistent type", + [F, A, File], + Analysis). + +clean_inc(Inc) -> + Inc1 = remove_yecc_generated_file(Inc), + normalize_obj(Inc1). + +remove_yecc_generated_file(#inc{filter = Filter} = Inc) -> + Fun = fun(Key, #inc{map = Map} = I) -> I#inc{map = maps:remove(Key, Map)} end, + lists:foldl(Fun, Inc, Filter). + +normalize_obj(TmpInc) -> + Fun = fun (Key, Val, Inc) -> + NewVal = [{{Line,F,A},Type} || {{F,A},{Line,Type}} <- Val], + Inc#{Key => NewVal} + end, + TmpInc#inc{map = maps:fold(Fun, maps:new(), TmpInc#inc.map)}. + +get_records(File, Analysis) -> + maps:get(File, Analysis#analysis.record, none). + +get_types(Module, Analysis, Records) -> + TypeInfoPlt = Analysis#analysis.trust_plt, + TypeInfo = + case dialyzer_plt:lookup_module(TypeInfoPlt, Module) of + none -> []; + {value, List} -> List + end, + CodeServer = Analysis#analysis.codeserver, + TypeInfoList = + case Analysis#analysis.show_succ of + true -> + [convert_type_info(I) || I <- TypeInfo]; + false -> + [get_type(I, CodeServer, Records, Analysis) || I <- TypeInfo] + end, + maps:from_list(TypeInfoList). + +convert_type_info({{_M, F, A}, Range, Arg}) -> + {{F, A}, {Range, Arg}}. + +get_type({{M, F, A} = MFA, Range, Arg}, CodeServer, Records, Analysis) -> + case dialyzer_codeserver:lookup_mfa_contract(MFA, CodeServer) of + error -> + {{F, A}, {Range, Arg}}; + {ok, {_FileLine, Contract, _Xtra}} -> + Sig = erl_types:t_fun(Arg, Range), + case dialyzer_contracts:check_contract(Contract, Sig) of + ok -> {{F, A}, {contract, Contract}}; + {range_warnings, _} -> + {{F, A}, {contract, Contract}}; + {error, {overlapping_contract, []}} -> + {{F, A}, {contract, Contract}}; + {error, invalid_contract} -> + CString = dialyzer_contracts:contract_to_string(Contract), + SigString = dialyzer_utils:format_sig(Sig, Records), + Msg = io_lib:format("Error in contract of function ~w:~tw/~w\n" + "\t The contract is: " ++ CString ++ "\n" ++ + "\t but the inferred signature is: ~ts", + [M, F, A, SigString]), + fatal_error(Msg, Analysis); + {error, ErrorStr} when is_list(ErrorStr) -> % ErrorStr is a string() + Msg = io_lib:format("Error in contract of function ~w:~tw/~w: ~ts", + [M, F, A, ErrorStr]), + fatal_error(Msg, Analysis) + end + end. + +get_functions(File, Analysis) -> + case Analysis#analysis.mode of + ?SHOW -> + Funcs = maps:get(File, Analysis#analysis.func, none), + IncFuncs = maps:get(File, Analysis#analysis.inc_func, none), + remove_module_info(Funcs) ++ normalize_inc_funcs(IncFuncs); + ?SHOW_EXPORTED -> + ExFuncs = maps:get(File, Analysis#analysis.ex_func, none), + remove_module_info(ExFuncs); + Mode when Mode == ?ANNOTATE; Mode == ?ANNOTATE_IN_PLACE -> + Funcs = maps:get(File, Analysis#analysis.func, none), + remove_module_info(Funcs); + ?ANNOTATE_INC_FILES -> + normalize_inc_funcs(maps:get(File, Analysis#analysis.inc_func, none)) + end. + +-spec get_inc_functions(file:filename(), analysis()) -> [{file:filename(), func_info()}]. +get_inc_functions(File, Analysis) -> + case Analysis#analysis.mode of + show -> + Funcs = maps:get(File, Analysis#analysis.func, none), + IncFuncs = maps:get(File, Analysis#analysis.inc_func, none), + extend_functions(File, remove_module_info(Funcs)) ++ IncFuncs; + show_exported -> + ExFuncs = maps:get(File, Analysis#analysis.ex_func, none), + extend_functions(File, remove_module_info(ExFuncs)); + Mode when Mode =:= annotate orelse Mode =:= annotate_in_place -> + Funcs = maps:get(File, Analysis#analysis.func, none), + extend_functions(File, remove_module_info(Funcs)); + annotate_inc_files -> + maps:get(File, Analysis#analysis.inc_func, none) + end. + +extend_functions(FileName, Functions) -> + [{FileName, FunInfo} || FunInfo <- Functions]. + +normalize_inc_funcs(Functions) -> + [FunInfo || {_FileName, FunInfo} <- Functions]. + +-spec remove_module_info([func_info()]) -> [func_info()]. + +remove_module_info(FunInfoList) -> + F = fun ({_,module_info,0}) -> false; + ({_,module_info,1}) -> false; + ({Line,F,A}) when is_integer(Line), is_atom(F), is_integer(A) -> true + end, + lists:filter(F, FunInfoList). + +write_typed_file(File, Info, Analysis) -> + msg(info, " Processing file: ~tp", [File], Analysis), + Dir = filename:dirname(File), + RootName = filename:basename(filename:rootname(File)), + Ext = filename:extension(File), + case Analysis#analysis.mode of + ?ANNOTATE_IN_PLACE -> + write_typed_file(File, Info, File, Analysis); + _ -> + TyperAnnDir = filename:join(Dir, ?TYPER_ANN_DIR), + TmpNewFilename = lists:concat([RootName, ".ann", Ext]), + NewFileName = filename:join(TyperAnnDir, TmpNewFilename), + case file:make_dir(TyperAnnDir) of + {error, Reason} -> + case Reason of + eexist -> %% TypEr dir exists; remove old typer files if they exist + delete_file(NewFileName, Analysis), + write_typed_file(File, Info, NewFileName, Analysis); + enospc -> + Msg = io_lib:format("Not enough space in ~tp", [Dir]), + fatal_error(Msg, Analysis); + eacces -> + Msg = io_lib:format("No write permission in ~tp", [Dir]), + fatal_error(Msg, Analysis); + _ -> + Msg = io_lib:format("Unhandled error ~ts when writing ~tp", + [Reason, Dir]), + fatal_error(Msg, Analysis) + end; + ok -> %% Typer dir does NOT exist + write_typed_file(File, Info, NewFileName, Analysis) + end + end. + +-spec delete_file(file:filename_all(), analysis()) -> ok. +delete_file(File, Analysis) -> + case file:delete(File) of + ok -> + ok; + {error, enoent} -> + ok; + {error, _} -> + Msg = io_lib:format("Error in deleting file ~ts", [File]), + fatal_error(Msg, Analysis) + end. + +write_typed_file(File, Info, NewFileName, Analysis) -> + {ok, Binary} = file:read_file(File), + case Analysis#analysis.mode of + ?ANNOTATE_IN_PLACE -> + delete_file(NewFileName, Analysis); + _ -> + ok + end, + Chars = unicode:characters_to_list(Binary), + write_typed_file(Chars, NewFileName, Info, 1, [], Analysis), + msg(info, " Saved as: ~tp", [NewFileName], Analysis). + +write_typed_file(Chars, File, #info{functions = []}, _LNo, _Acc, _Analysis) -> + ok = file:write_file(File, unicode:characters_to_binary(Chars), [append]); +write_typed_file([Ch|Chs] = Chars, File, Info, LineNo, Acc, Analysis) -> + [{Line,F,A}|RestFuncs] = Info#info.functions, + case Line of + 1 -> %% This will happen only for inc files + ok = raw_write(F, A, Info, File, [], Analysis), + NewInfo = Info#info{functions = RestFuncs}, + NewAcc = [], + write_typed_file(Chars, File, NewInfo, Line, NewAcc, Analysis); + _ -> + case Ch of + 10 -> + NewLineNo = LineNo + 1, + {NewInfo, NewAcc} = + case NewLineNo of + Line -> + ok = raw_write(F, A, Info, File, [Ch|Acc], Analysis), + {Info#info{functions = RestFuncs}, []}; + _ -> + {Info, [Ch|Acc]} + end, + write_typed_file(Chs, File, NewInfo, NewLineNo, NewAcc, Analysis); + _ -> + write_typed_file(Chs, File, Info, LineNo, [Ch|Acc], Analysis) + end + end. + +raw_write(F, A, Info, File, Content, #analysis{mode = Mode} = Analysis) -> + TypeInfo = get_type_string(F, A, Info, file, Analysis), + ContentList = + case {TypeInfo, Mode} of + %% TypeInfo will be an empty string for functions that already have specs. + %% In this case, when annotating directly on the files (with annotate_in_place), + %% we don't want to add a newline character on the spec line, as presumably it should + %% already have one. + {"", ?ANNOTATE_IN_PLACE} -> + lists:reverse(Content) ++ TypeInfo; + _ -> + lists:reverse(Content) ++ TypeInfo ++ "\n" + end, + ContentBin = unicode:characters_to_binary(ContentList), + file:write_file(File, ContentBin, [append]). + +get_type_string(F, A, Info, Mode, Analysis) -> + Type = get_type_info({F,A}, Info#info.types, Analysis), + TypeStr = + case Type of + {contract, C} -> + dialyzer_contracts:contract_to_string(C); + {RetType, ArgType} -> + Sig = erl_types:t_fun(ArgType, RetType), + dialyzer_utils:format_sig(Sig, Info#info.records) + end, + case Info#info.edoc of + false -> + case {Mode, Type} of + {file, {contract, _}} -> ""; + _ -> + Prefix = lists:concat(["-spec ", erl_types:atom_to_string(F)]), + lists:concat([Prefix, TypeStr, "."]) + end; + true -> + Prefix = lists:concat(["%% @spec ", F]), + lists:concat([Prefix, TypeStr, "."]) + end. + +show_type_info(File, Info, Analysis) -> + msg(info, "\n%% File: ~tp", [File], Analysis), + OutputString = lists:concat(["~.", length(File) + 8, "c"]), + msg(info, [$%, $%, $\s | OutputString], [$-], Analysis), + Fun = fun ({_LineNo, F, A}) -> + TypeInfo = get_type_string(F, A, Info, show, Analysis), + msg(info, "~ts", [TypeInfo], Analysis) + end, + lists:foreach(Fun, Info#info.functions). + +get_type_info(Func, Types, Analysis) -> + case Types of + #{Func := {contract, _Fun} = C} -> + C; + #{Func := {_RetType, _ArgType} = RA} -> + RA; + _ -> + %% Note: Typeinfo of any function should exist in + %% the result offered by dialyzer, otherwise there + %% *must* be something wrong with the analysis + Msg = io_lib:format("No type info for function: ~tp\n", [Func]), + fatal_error(Msg, Analysis) + end. + +%%-------------------------------------------------------------------- +%% Processing of command-line options and arguments. +%%-------------------------------------------------------------------- + +-spec process_cl_args(opts()) -> {args(), analysis()}. + +process_cl_args(Opts) -> + analyze_args(maps:to_list(Opts), #args{}, #analysis{}). + +analyze_args([], Args, Analysis) -> + {Args, Analysis}; +analyze_args([Result | Rest], Args, Analysis) -> + {NewArgs, NewAnalysis} = analyze_result(Result, Args, Analysis), + analyze_args(Rest, NewArgs, NewAnalysis). + +%% Get information about files that the user trusts and wants to analyze +analyze_result({files, Val}, Args, Analysis) -> + NewVal = Args#args.files ++ Val, + {Args#args{files = NewVal}, Analysis}; +analyze_result({files_r, Val}, Args, Analysis) -> + NewVal = Args#args.files_r ++ Val, + {Args#args{files_r = NewVal}, Analysis}; +analyze_result({trusted, Val}, Args, Analysis) -> + NewVal = Args#args.trusted ++ Val, + {Args#args{trusted = NewVal}, Analysis}; +analyze_result({edoc, Value}, Args, Analysis) -> + {Args, Analysis#analysis{edoc = Value}}; +analyze_result({io, Val}, Args, Analysis) -> + {Args, Analysis#analysis{io = Val}}; +%% Get useful information for actual analysis +analyze_result({mode, Mode}, Args, Analysis) -> + {Args, Analysis#analysis{mode = Mode}}; +analyze_result({macros, Macros}, Args, Analysis) -> + {Args, Analysis#analysis{macros = Macros}}; +analyze_result({includes, Includes}, Args, Analysis) -> + {Args, Analysis#analysis{includes = Includes}}; +analyze_result({plt, Plt}, Args, Analysis) -> + {Args, Analysis#analysis{plt = Plt}}; +analyze_result({show_succ, Value}, Args, Analysis) -> + {Args, Analysis#analysis{show_succ = Value}}; +analyze_result({no_spec, Value}, Args, Analysis) -> + {Args, Analysis#analysis{no_spec = Value}}. + +%%-------------------------------------------------------------------- +%% File processing. +%%-------------------------------------------------------------------- + +-spec get_all_files(args(), analysis()) -> [file:filename(), ...]. + +get_all_files(#args{files = Fs, files_r = Ds}, Analysis) -> + case filter_fd(Fs, Ds, fun test_erl_file_exclude_ann/1, Analysis) of + [] -> + fatal_error("no file(s) to analyze", Analysis); + AllFiles -> + AllFiles + end. + +-spec test_erl_file_exclude_ann(file:filename()) -> boolean(). + +test_erl_file_exclude_ann(File) -> + case is_erl_file(File) of + true -> %% Exclude files ending with ".ann.erl" + case re:run(File, "[\.]ann[\.]erl$", [unicode]) of + {match, _} -> false; + nomatch -> true + end; + false -> false + end. + +-spec is_erl_file(file:filename()) -> boolean(). + +is_erl_file(File) -> + filename:extension(File) =:= ".erl". + +-type test_file_fun() :: fun((file:filename()) -> boolean()). + +-spec filter_fd(files(), files(), test_file_fun(), analysis()) -> files(). + +filter_fd(File_Dir, Dir_R, Fun, Analysis) -> + All_File_1 = process_file_and_dir(File_Dir, Fun, Analysis), + All_File_2 = process_dir_rec(Dir_R, Fun, Analysis), + remove_dup(All_File_1 ++ All_File_2). + +-spec process_file_and_dir(files(), test_file_fun(), analysis()) -> files(). + +process_file_and_dir(File_Dir, TestFun, Analysis) -> + Fun = + fun (Elem, Acc) -> + case filelib:is_regular(Elem) of + true -> process_file(Elem, TestFun, Acc); + false -> check_dir(Elem, false, Acc, TestFun, Analysis) + end + end, + lists:foldl(Fun, [], File_Dir). + +-spec process_dir_rec(files(), test_file_fun(), analysis()) -> files(). + +process_dir_rec(Dirs, TestFun, Analysis) -> + Fun = fun (Dir, Acc) -> check_dir(Dir, true, Acc, TestFun, Analysis) end, + lists:foldl(Fun, [], Dirs). + +-spec check_dir(file:filename(), boolean(), files(), test_file_fun(), analysis()) -> files(). + +check_dir(Dir, Recursive, Acc, Fun, Analysis) -> + case file:list_dir(Dir) of + {ok, Files} -> + {TmpDirs, TmpFiles} = split_dirs_and_files(Files, Dir), + case Recursive of + false -> + FinalFiles = process_file_and_dir(TmpFiles, Fun, Analysis), + Acc ++ FinalFiles; + true -> + TmpAcc1 = process_file_and_dir(TmpFiles, Fun, Analysis), + TmpAcc2 = process_dir_rec(TmpDirs, Fun, Analysis), + Acc ++ TmpAcc1 ++ TmpAcc2 + end; + {error, eacces} -> + fatal_error("no access permission to dir \""++Dir++"\"", Analysis); + {error, enoent} -> + fatal_error("cannot access "++Dir++": No such file or directory", Analysis); + {error, _Reason} -> + fatal_error("error involving a use of file:list_dir/1", Analysis) + end. + +%% Same order as the input list +-spec process_file(file:filename(), test_file_fun(), files()) -> files(). + +process_file(File, TestFun, Acc) -> + case TestFun(File) of + true -> Acc ++ [File]; + false -> Acc + end. + +%% Same order as the input list +-spec split_dirs_and_files(files(), file:filename()) -> {files(), files()}. + +split_dirs_and_files(Elems, Dir) -> + Test_Fun = + fun (Elem, {DirAcc, FileAcc}) -> + File = filename:join(Dir, Elem), + case filelib:is_regular(File) of + false -> {[File|DirAcc], FileAcc}; + true -> {DirAcc, [File|FileAcc]} + end + end, + {Dirs, Files} = lists:foldl(Test_Fun, {[], []}, Elems), + {lists:reverse(Dirs), lists:reverse(Files)}. + +%% Removes duplicate filenames but keeps the order of the input list +-spec remove_dup(files()) -> files(). + +remove_dup(Files) -> + Test_Dup = fun (File, Acc) -> + case lists:member(File, Acc) of + true -> Acc; + false -> [File|Acc] + end + end, + Reversed_Elems = lists:foldl(Test_Dup, [], Files), + lists:reverse(Reversed_Elems). + +%%-------------------------------------------------------------------- +%% Collect information. +%%-------------------------------------------------------------------- + +-type inc_file_info() :: {file:filename(), func_info()}. + +-record(tmpAcc, {file :: file:filename(), + module :: atom(), + funcAcc = [] :: [func_info()], + incFuncAcc = [] :: [inc_file_info()], + dialyzerObj = [] :: [{mfa(), {_, _}}]}). + +-spec collect_info(analysis()) -> analysis(). + +collect_info(Analysis) -> + NewPlt = + try get_dialyzer_plt(Analysis) of + DialyzerPlt -> + dialyzer_plt:merge_plts([Analysis#analysis.trust_plt, DialyzerPlt]) + catch + throw:{dialyzer_error,_Reason} -> + fatal_error("Dialyzer's PLT is missing or is not up-to-date; please (re)create it", + Analysis) + end, + NewAnalysis = lists:foldl(fun collect_one_file_info/2, + Analysis#analysis{trust_plt = NewPlt}, + Analysis#analysis.files), + %% Process Remote Types + TmpCServer = NewAnalysis#analysis.codeserver, + NewCServer = + try + TmpCServer1 = dialyzer_utils:merge_types(TmpCServer, NewPlt), + NewExpTypes = dialyzer_codeserver:get_temp_exported_types(TmpCServer), + OldExpTypes = dialyzer_plt:get_exported_types(NewPlt), + MergedExpTypes = sets:union(NewExpTypes, OldExpTypes), + TmpCServer2 = + dialyzer_codeserver:finalize_exported_types(MergedExpTypes, TmpCServer1), + TmpCServer3 = dialyzer_utils:process_record_remote_types(TmpCServer2), + dialyzer_contracts:process_contract_remote_types(TmpCServer3) + catch + throw:{error, ErrorMsg} -> + fatal_error(ErrorMsg, NewAnalysis) + end, + NewAnalysis#analysis{codeserver = NewCServer}. + +collect_one_file_info(File, Analysis) -> + Ds = [{d,Name,Val} || {Name,Val} <- Analysis#analysis.macros], + %% Current directory should also be included in "Includes". + Includes = [filename:dirname(File)|Analysis#analysis.includes], + Is = [{i,Dir} || Dir <- Includes], + Options = dialyzer_utils:src_compiler_opts() ++ Is ++ Ds, + case dialyzer_utils:get_core_from_src(File, Options) of + {error, Reason} -> + msg(debug, "File=~tp\n,Options=~p\n,Error=~p", [File, Options, Reason], Analysis), + compile_error(Reason, Analysis); + {ok, Core} -> + case dialyzer_utils:get_record_and_type_info(Core) of + {error, Reason} -> compile_error([Reason], Analysis); + {ok, Records} -> + Mod = cerl:concrete(cerl:module_name(Core)), + case dialyzer_utils:get_spec_info(Mod, Core, Records) of + {error, Reason} -> compile_error([Reason], Analysis); + {ok, SpecInfo, CbInfo} -> + ExpTypes = get_exported_types_from_core(Core), + analyze_core_tree(Core, Records, SpecInfo, CbInfo, + ExpTypes, Analysis, File) + end + end + end. + +analyze_core_tree(Core, Records, SpecInfo, CbInfo, ExpTypes, Analysis, File) -> + Module = cerl:concrete(cerl:module_name(Core)), + TmpTree = cerl:from_records(Core), + CS1 = Analysis#analysis.codeserver, + NextLabel = dialyzer_codeserver:get_next_core_label(CS1), + {Tree, NewLabel} = cerl_trees:label(TmpTree, NextLabel), + CS2 = dialyzer_codeserver:insert(Module, Tree, CS1), + CS3 = dialyzer_codeserver:set_next_core_label(NewLabel, CS2), + CS4 = dialyzer_codeserver:store_temp_records(Module, Records, CS3), + CS5 = + case Analysis#analysis.no_spec of + true -> CS4; + false -> + dialyzer_codeserver:store_temp_contracts(Module, SpecInfo, CbInfo, CS4) + end, + OldExpTypes = dialyzer_codeserver:get_temp_exported_types(CS5), + MergedExpTypes = sets:union(ExpTypes, OldExpTypes), + CS6 = dialyzer_codeserver:insert_temp_exported_types(MergedExpTypes, CS5), + ExFuncs = [{0,F,A} || {_,_,{F,A}} <- cerl:module_exports(Tree)], + CG = Analysis#analysis.callgraph, + {V, E} = dialyzer_callgraph:scan_core_tree(Tree, CG), + dialyzer_callgraph:add_edges(E, V, CG), + Fun = fun analyze_one_function/2, + All_Defs = cerl:module_defs(Tree), + Acc = lists:foldl(Fun, #tmpAcc{file = File, module = Module}, All_Defs), + ExportedFuncMap = maps:put(File, ExFuncs, Analysis#analysis.ex_func), + %% we must sort all functions in the file which + %% originate from this file by *numerical order* of lineNo + SortedFunctions = lists:keysort(1, Acc#tmpAcc.funcAcc), + FuncMap = maps:put(File, SortedFunctions, Analysis#analysis.func), + %% we do not need to sort functions which are imported from included files + IncFuncMap = maps:put(File, Acc#tmpAcc.incFuncAcc, Analysis#analysis.inc_func), + FMs = Analysis#analysis.fms ++ [{File, Module}], + RecordMap = maps:put(File, Records, Analysis#analysis.record), + Analysis#analysis{fms = FMs, + callgraph = CG, + codeserver = CS6, + ex_func = ExportedFuncMap, + inc_func = IncFuncMap, + record = RecordMap, + func = FuncMap}. + +analyze_one_function({Var, FunBody} = Function, Acc) -> + F = cerl:fname_id(Var), + A = cerl:fname_arity(Var), + TmpDialyzerObj = {{Acc#tmpAcc.module, F, A}, Function}, + NewDialyzerObj = Acc#tmpAcc.dialyzerObj ++ [TmpDialyzerObj], + Anno = cerl:get_ann(FunBody), + LineNo = get_line(Anno), + FileName = get_file(Anno), + BaseName = filename:basename(FileName), + FuncInfo = {LineNo, F, A}, + OriginalName = Acc#tmpAcc.file, + {FuncAcc, IncFuncAcc} = + case (FileName =:= OriginalName) orelse (BaseName =:= OriginalName) of + true -> %% Coming from original file + {Acc#tmpAcc.funcAcc ++ [FuncInfo], Acc#tmpAcc.incFuncAcc}; + false -> + %% Coming from other sourses, including: + %% -- .yrl (yecc-generated file) + %% -- yeccpre.hrl (yecc-generated file) + %% -- other cases + {Acc#tmpAcc.funcAcc, Acc#tmpAcc.incFuncAcc ++ [{FileName, FuncInfo}]} + end, + Acc#tmpAcc{funcAcc = FuncAcc, + incFuncAcc = IncFuncAcc, + dialyzerObj = NewDialyzerObj}. + +get_line([Line|_]) when is_integer(Line) -> Line; +get_line([{Line, _Column} | _Tail]) when is_integer(Line) -> Line; +get_line([_|Tail]) -> get_line(Tail); +get_line([]) -> -1. + +get_file([{file,File}|_]) -> File; +get_file([_|T]) -> get_file(T); +get_file([]) -> "no_file". % should not happen + +-spec get_dialyzer_plt(analysis()) -> plt(). + +get_dialyzer_plt(#analysis{plt = PltFile0}) -> + PltFile = + case PltFile0 =:= none of + true -> dialyzer_plt:get_default_plt(); + false -> PltFile0 + end, + dialyzer_plt:from_file(PltFile). + +%% Exported Types + +get_exported_types_from_core(Core) -> + Attrs = cerl:module_attrs(Core), + ExpTypes1 = [cerl:concrete(L2) || {L1, L2} <- Attrs, + cerl:is_literal(L1), + cerl:is_literal(L2), + cerl:concrete(L1) =:= 'export_type'], + ExpTypes2 = lists:flatten(ExpTypes1), + M = cerl:atom_val(cerl:module_name(Core)), + sets:from_list([{M, F, A} || {F, A} <- ExpTypes2]). + +%%-------------------------------------------------------------------- +%% Utilities for error reporting. +%%-------------------------------------------------------------------- + +-spec default_io() -> io(). + +default_io() -> + #{debug => fun swallow_output/2, + info => fun format/2, + warn => fun format_on_stderr/2, + abort => fun format_and_halt/2}. + +-spec fatal_error(string(), analysis()) -> no_return(). + +fatal_error(Slogan, Analysis) -> + msg(abort, "typer: ~ts", [Slogan], Analysis). + +-spec compile_error([string()], analysis()) -> no_return(). + +compile_error(Reason, Analysis) -> + JoinedString = lists:flatten([X ++ "\n" || X <- Reason]), + Msg = "Analysis failed with error report:\n" ++ JoinedString, + fatal_error(Msg, Analysis). + +-spec msg(debug | info | warn | abort, io:format(), [term()], analysis()) -> _. + +msg(Level, Format, Data, #analysis{io = Io}) -> + Printer = maps:get(Level, Io, fun swallow_output/2), + Printer(Format, Data). + +-spec format(io:format(), [term()]) -> ok. + +format(Format, Data) -> + io:format(Format ++ "\n", Data). + +-spec swallow_output(io:format(), [term()]) -> ok. + +swallow_output(_Format, _Data) -> + ok. + +-spec format_on_stderr(io:format(), [term()]) -> ok. + +format_on_stderr(Format, Data) -> + io:format(standard_error, Format ++ "\n", Data). + +-spec format_and_halt(io:format(), [term()]) -> no_return(). + +format_and_halt(Format, Data) -> + format_on_stderr(Format, Data), + erlang:halt(1). + +%%-------------------------------------------------------------------- +%% Handle messages. +%%-------------------------------------------------------------------- + +rcv_ext_types() -> + Self = self(), + Self ! {Self, done}, + rcv_ext_types(Self, []). + +rcv_ext_types(Self, ExtTypes) -> + receive + {Self, ext_types, ExtType} -> + rcv_ext_types(Self, [ExtType|ExtTypes]); + {Self, done} -> + lists:usort(ExtTypes) + end. |