%% -*- erlang-indent-level: 2 -*-
%%-----------------------------------------------------------------------
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2006-2011. All Rights Reserved.
%%
%% The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
%% compliance with the License. You should have received a copy of the
%% Erlang Public License along with this software. If not, it can be
%% retrieved online at http://www.erlang.org/.
%%
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%%
%% %CopyrightEnd%
%%
%%-----------------------------------------------------------------------
%% File : typer.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.
%% Description : An Erlang/OTP application that shows type information
%% for Erlang modules to the user. Additionally, it can
%% annotates the code of files with such type information.
%%-----------------------------------------------------------------------
-module(typer).
-export([start/0]).
-export([fatal_error/1, compile_error/1]). % for error reporting
-export([map__new/0, map__insert/2, map__lookup/2, map__from_list/1, map__remove/2, map__fold/3]).
%%-----------------------------------------------------------------------
-define(SHOW, show).
-define(SHOW_EXPORTED, show_exported).
-define(ANNOTATE, annotate).
-define(ANNOTATE_INC_FILES, annotate_inc_files).
-type mode() :: ?SHOW | ?SHOW_EXPORTED | ?ANNOTATE | ?ANNOTATE_INC_FILES.
%%-----------------------------------------------------------------------
-record(typer_analysis,
{mode :: mode(),
macros = [] :: [{atom(), term()}], % {macro_name, value}
includes = [] :: [file:filename()],
%% --- for dialyzer ---
code_server = dialyzer_codeserver:new():: dialyzer_codeserver:codeserver(),
callgraph = dialyzer_callgraph:new() :: dialyzer_callgraph:callgraph(),
ana_files = [] :: [file:filename()], % absolute filenames
plt = none :: 'none' | file:filename(),
no_spec = false :: boolean(),
%% --- for typer ---
t_files = [] :: [file:filename()],
%% For choosing between contracts or comments
contracts = true :: boolean(),
%% Files in 'final_files' are compilable with option 'to_pp'; we keep
%% them as {FileName, ModuleName} in case the ModuleName is different
final_files = [] :: [{file:filename(), module()}],
ex_func = map__new() :: map(),
record = map__new() :: map(),
func = map__new() :: map(),
inc_func = map__new() :: map(),
trust_plt = dialyzer_plt:new() :: dialyzer_plt:plt()}).
-type analysis() :: #typer_analysis{}.
-record(args, {files = [] :: [file:filename()],
files_r = [] :: [file:filename()],
trusted = [] :: [file:filename()]}).
%%--------------------------------------------------------------------
-spec start() -> no_return().
start() ->
{Args, Analysis} = typer_options:process(),
%% io:format("Args: ~p\n", [Args]),
%% io:format("Analysis: ~p\n", [Analysis]),
TrustedFiles = typer_preprocess:get_all_files(Args, trust),
Analysis1 = Analysis#typer_analysis{t_files = TrustedFiles},
Analysis2 = extract(Analysis1),
All_Files = typer_preprocess:get_all_files(Args, analysis),
%% io:format("All_Files: ~p\n", [All_Files]),
Analysis3 = Analysis2#typer_analysis{ana_files = All_Files},
Analysis4 = typer_info:collect(Analysis3),
%% io:format("Final: ~p\n", [Analysis4#typer_analysis.final_files]),
TypeInfo = get_type_info(Analysis4),
typer_annotator:annotate(TypeInfo),
%% io:format("\nTyper analysis finished\n"),
erlang:halt(0).
%%--------------------------------------------------------------------
-spec extract(analysis()) -> analysis().
extract(#typer_analysis{macros = Macros, includes = Includes,
t_files = TFiles, trust_plt = TrustPLT} = Analysis) ->
%% io:format("--- Extracting trusted typer_info... "),
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_abstract_code_from_src(File, CompOpts) of
{ok, AbstractCode} ->
case dialyzer_utils:get_record_and_type_info(AbstractCode) of
{ok, RecDict} ->
Mod = list_to_atom(filename:basename(File, ".erl")),
case dialyzer_utils:get_spec_info(Mod, AbstractCode, RecDict) of
{ok, SpecDict} ->
CS1 = dialyzer_codeserver:store_temp_records(Mod, RecDict, CS),
dialyzer_codeserver:store_temp_contracts(Mod, SpecDict, CS1);
{error, Reason} -> compile_error([Reason])
end;
{error, Reason} -> compile_error([Reason])
end;
{error, Reason} -> compile_error(Reason)
end
end,
CodeServer1 = lists:foldl(Fun, CodeServer, TFiles),
%% Process remote types
NewCodeServer =
try
NewRecords = dialyzer_codeserver:get_temp_records(CodeServer1),
OldRecords = dialyzer_plt:get_types(TrustPLT), % XXX change to the PLT?
MergedRecords = dialyzer_utils:merge_records(NewRecords, OldRecords),
CodeServer2 = dialyzer_codeserver:set_temp_records(MergedRecords, CodeServer1),
CodeServer3 = dialyzer_utils:process_record_remote_types(CodeServer2),
dialyzer_contracts:process_contract_remote_types(CodeServer3)
catch
throw:{error, ErrorMsg} ->
compile_error(ErrorMsg)
end,
%% Create TrustPLT
Contracts = dialyzer_codeserver:get_contracts(NewCodeServer),
Modules = dict:fetch_keys(Contracts),
FoldFun =
fun(Module, TmpPlt) ->
{ok, ModuleContracts} = dict:find(Module, Contracts),
SpecList = [{MFA, Contract}
|| {MFA, {_FileLine, Contract}} <- dict:to_list(ModuleContracts)],
dialyzer_plt:insert_contract_list(TmpPlt, SpecList)
end,
NewTrustPLT = lists:foldl(FoldFun, TrustPLT, Modules),
Analysis#typer_analysis{trust_plt = NewTrustPLT}.
%%--------------------------------------------------------------------
-spec get_type_info(analysis()) -> analysis().
get_type_info(#typer_analysis{callgraph = CallGraph,
trust_plt = TrustPLT,
code_server = CodeServer} = Analysis) ->
StrippedCallGraph = remove_external(CallGraph, TrustPLT),
%% io:format("--- Analyzing callgraph... "),
try
NewPlt = dialyzer_succ_typings:analyze_callgraph(StrippedCallGraph,
TrustPLT, CodeServer),
Analysis#typer_analysis{callgraph = StrippedCallGraph, trust_plt = NewPlt}
catch
error:What ->
fatal_error(io_lib:format("Analysis failed with message: ~p",
[{What, erlang:get_stacktrace()}]));
throw:{dialyzer_succ_typing_error, Msg} ->
fatal_error(io_lib:format("Analysis failed with message: ~s", [Msg]))
end.
-spec remove_external(dialyzer_callgraph:callgraph(), dialyzer_plt:plt()) -> dialyzer_callgraph:callgraph().
remove_external(CallGraph, PLT) ->
{StrippedCG0, Ext} = dialyzer_callgraph:remove_external(CallGraph),
StrippedCG = dialyzer_callgraph:finalize(StrippedCG0),
case get_external(Ext, PLT) of
[] -> ok;
Externals ->
msg(io_lib:format(" Unknown functions: ~p\n", [lists:usort(Externals)])),
ExtTypes = rcv_ext_types(),
case ExtTypes of
[] -> ok;
_ ->
msg(io_lib:format(" Unknown types: ~p\n", [ExtTypes]))
end
end,
StrippedCG.
-spec get_external([{mfa(), mfa()}], dialyzer_plt: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).
%%--------------------------------------------------------------------
%% Utilities for error reporting.
%%--------------------------------------------------------------------
-spec fatal_error(string()) -> no_return().
fatal_error(Slogan) ->
msg(io_lib:format("typer: ~s\n", [Slogan])),
erlang:halt(1).
-spec compile_error([string()]) -> no_return().
compile_error(Reason) ->
JoinedString = lists:flatten([X ++ "\n" || X <- Reason]),
Msg = "Analysis failed with error report:\n" ++ JoinedString,
fatal_error(Msg).
-spec msg(string()) -> 'ok'.
msg(Msg) ->
case os:type() of
{unix, _} -> % Output a message on 'stderr', if possible
P = open_port({fd, 0, 2}, [out]),
port_command(P, Msg),
true = port_close(P),
ok;
_ -> % win32, vxworks
io:format("~s", [Msg])
end.
%%--------------------------------------------------------------------
%% 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.
%%--------------------------------------------------------------------
%% A convenient abstraction of a Key-Value mapping data structure
%%--------------------------------------------------------------------
-type map() :: dict().
-spec map__new() -> map().
map__new() ->
dict:new().
-spec map__insert({term(), term()}, map()) -> map().
map__insert(Object, Map) ->
{Key, Value} = Object,
dict:store(Key, Value, Map).
-spec map__lookup(term(), map()) -> term().
map__lookup(Key, Map) ->
try dict:fetch(Key, Map) catch error:_ -> none end.
-spec map__from_list([{term(), term()}]) -> map().
map__from_list(List) ->
dict:from_list(List).
-spec map__remove(term(), map()) -> map().
map__remove(Key, Dict) ->
dict:erase(Key, Dict).
-spec map__fold(fun((term(), term(), term()) -> term()), term(), map()) -> term().
map__fold(Fun, Acc0, Dict) ->
dict:fold(Fun, Acc0, Dict).