From 784f845e2d1ec66b9505339b4b0e43d55c2d49a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20L=C3=A5ng?= Date: Mon, 4 Jul 2016 14:12:07 +0200 Subject: dialyzer: Suppress warns on generated case stmts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Warnings about clauses that cannot match and are also compiler generated are suppressed unless none of the clauses return. This feature is useful for non-Erlang BEAM languages (such as Elixir) that compile to Erlang and expand certain language constructs into case statements. In that case, as long as the language construct can succeed, these warnings are undesired and appear spurious to users that do not check the Erlang code that their program expands into. Thanks to José Valim for the test (slightly modified). --- lib/dialyzer/src/dialyzer_dataflow.erl | 62 +++++++++++--------- lib/dialyzer/test/Makefile | 1 + lib/dialyzer/test/abstract_SUITE.erl | 102 +++++++++++++++++++++++++++++++++ lib/dialyzer/test/dialyzer_common.erl | 11 +++- 4 files changed, 147 insertions(+), 29 deletions(-) create mode 100644 lib/dialyzer/test/abstract_SUITE.erl diff --git a/lib/dialyzer/src/dialyzer_dataflow.erl b/lib/dialyzer/src/dialyzer_dataflow.erl index 9399789464..963c953447 100644 --- a/lib/dialyzer/src/dialyzer_dataflow.erl +++ b/lib/dialyzer/src/dialyzer_dataflow.erl @@ -978,12 +978,21 @@ handle_case(Tree, Map, State) -> false -> State1 end, Map2 = join_maps_begin(Map1), - {MapList, State3, Type} = + {MapList, State3, Type, Warns} = handle_clauses(Clauses, Arg, ArgType, ArgType, State2, - [], Map2, [], []), + [], Map2, [], [], []), + %% Non-Erlang BEAM languages, such as Elixir, expand language constructs + %% into case statements. In that case, we do not want to warn on + %% individual clauses not matching unless none of them can. + SupressForced = is_compiler_generated(cerl:get_ann(Tree)) + andalso not (t_is_none(Type)), + State4 = lists:foldl(fun({T,R,M,F}, S) -> + state__add_warning( + S,T,R,M,F andalso (not SupressForced)) + end, State3, Warns), Map3 = join_maps_end(MapList, Map2), debug_pp_map(Map3), - {State3, Map3, Type} + {State4, Map3, Type} end. %%---------------------------------------- @@ -1082,22 +1091,24 @@ handle_receive(Tree, Map, State) -> RaceListSize + 1, State); false -> State end, - {MapList, State2, ReceiveType} = + {MapList, State2, ReceiveType, Warns} = handle_clauses(Clauses, ?no_arg, t_any(), t_any(), State1, [], Map, - [], []), + [], [], []), + State3 = lists:foldl(fun({T,R,M,F}, S) -> state__add_warning(S,T,R,M,F) end, + State2, Warns), Map1 = join_maps(MapList, Map), - {State3, Map2, TimeoutType} = traverse(Timeout, Map1, State2), - Opaques = State3#state.opaques, + {State4, Map2, TimeoutType} = traverse(Timeout, Map1, State3), + Opaques = State4#state.opaques, case (t_is_atom(TimeoutType, Opaques) andalso (t_atom_vals(TimeoutType, Opaques) =:= ['infinity'])) of true -> - {State3, Map2, ReceiveType}; + {State4, Map2, ReceiveType}; false -> Action = cerl:receive_action(Tree), - {State4, Map3, ActionType} = traverse(Action, Map, State3), + {State5, Map3, ActionType} = traverse(Action, Map, State4), Map4 = join_maps([Map3, Map1], Map), Type = t_sup(ReceiveType, ActionType), - {State4, Map4, Type} + {State5, Map4, Type} end. %%---------------------------------------- @@ -1245,7 +1256,7 @@ handle_tuple(Tree, Map, State) -> %% Clauses %% handle_clauses([C|Left], Arg, ArgType, OrigArgType, State, CaseTypes, MapIn, - Acc, ClauseAcc) -> + Acc, ClauseAcc, WarnAcc0) -> IsRaceAnalysisEnabled = is_race_analysis_enabled(State), State1 = case IsRaceAnalysisEnabled of @@ -1258,8 +1269,8 @@ handle_clauses([C|Left], Arg, ArgType, OrigArgType, State, CaseTypes, MapIn, State); false -> State end, - {State2, ClauseMap, BodyType, NewArgType} = - do_clause(C, Arg, ArgType, OrigArgType, MapIn, State1), + {State2, ClauseMap, BodyType, NewArgType, WarnAcc} = + do_clause(C, Arg, ArgType, OrigArgType, MapIn, State1, WarnAcc0), {NewClauseAcc, State3} = case IsRaceAnalysisEnabled of true -> @@ -1277,9 +1288,9 @@ handle_clauses([C|Left], Arg, ArgType, OrigArgType, State, CaseTypes, MapIn, false -> {[BodyType|CaseTypes], [ClauseMap|Acc]} end, handle_clauses(Left, Arg, NewArgType, OrigArgType, State3, - NewCaseTypes, MapIn, NewAcc, NewClauseAcc); + NewCaseTypes, MapIn, NewAcc, NewClauseAcc, WarnAcc); handle_clauses([], _Arg, _ArgType, _OrigArgType, State, CaseTypes, _MapIn, Acc, - ClauseAcc) -> + ClauseAcc, WarnAcc) -> State1 = case is_race_analysis_enabled(State) of true -> @@ -1289,9 +1300,9 @@ handle_clauses([], _Arg, _ArgType, _OrigArgType, State, CaseTypes, _MapIn, Acc, RaceListSize + 1, State); false -> State end, - {lists:reverse(Acc), State1, t_sup(CaseTypes)}. + {lists:reverse(Acc), State1, t_sup(CaseTypes), WarnAcc}. -do_clause(C, Arg, ArgType0, OrigArgType, Map, State) -> +do_clause(C, Arg, ArgType0, OrigArgType, Map, State, Warns) -> Pats = cerl:clause_pats(C), Guard = cerl:clause_guard(C), Body = cerl:clause_body(C), @@ -1323,7 +1334,7 @@ do_clause(C, Arg, ArgType0, OrigArgType, Map, State) -> [cerl_prettypr:format(C), format_type(ArgType0, State1)]), case state__warning_mode(State1) of false -> - {State1, Map, t_none(), ArgType0}; + {State1, Map, t_none(), ArgType0, Warns}; true -> {Msg, Force} = case t_is_none(ArgType0) of @@ -1403,8 +1414,7 @@ do_clause(C, Arg, ArgType0, OrigArgType, Map, State) -> {record_match, _} -> ?WARN_MATCHING; {pattern_match_cov, _} -> ?WARN_MATCHING end, - {state__add_warning(State1, WarnType, C, Msg, Force), - Map, t_none(), ArgType0} + {State1, Map, t_none(), ArgType0, [{WarnType, C, Msg, Force}|Warns]} end; {Map2, PatTypes} -> Map3 = @@ -1437,9 +1447,9 @@ do_clause(C, Arg, ArgType0, OrigArgType, Map, State) -> false -> {guard_fail_pat, [PatString, format_type(ArgType0, State1)]} end, - State2 = + Warn = case Reason of - none -> state__add_warning(State1, ?WARN_MATCHING, C, DefaultMsg); + none -> {?WARN_MATCHING, C, DefaultMsg, false}; {FailGuard, Msg} -> case is_compiler_generated(cerl:get_ann(FailGuard)) of false -> @@ -1448,15 +1458,15 @@ do_clause(C, Arg, ArgType0, OrigArgType, Map, State) -> {neg_guard_fail, _} -> ?WARN_MATCHING; {opaque_guard, _} -> ?WARN_OPAQUE end, - state__add_warning(State1, WarnType, FailGuard, Msg); + {WarnType, FailGuard, Msg, false}; true -> - state__add_warning(State1, ?WARN_MATCHING, C, Msg) + {?WARN_MATCHING, C, Msg, false} end end, - {State2, Map, t_none(), NewArgType}; + {State1, Map, t_none(), NewArgType, [Warn|Warns]}; Map4 -> {RetState, RetMap, BodyType} = traverse(Body, Map4, State1), - {RetState, RetMap, BodyType, NewArgType} + {RetState, RetMap, BodyType, NewArgType, Warns} end end. diff --git a/lib/dialyzer/test/Makefile b/lib/dialyzer/test/Makefile index f43e04dd59..0d8fba438c 100644 --- a/lib/dialyzer/test/Makefile +++ b/lib/dialyzer/test/Makefile @@ -12,6 +12,7 @@ AUXILIARY_FILES=\ dialyzer_common.erl\ file_utils.erl\ dialyzer_SUITE.erl\ + abstract_SUITE.erl\ plt_SUITE.erl # ---------------------------------------------------- diff --git a/lib/dialyzer/test/abstract_SUITE.erl b/lib/dialyzer/test/abstract_SUITE.erl new file mode 100644 index 0000000000..269db3e836 --- /dev/null +++ b/lib/dialyzer/test/abstract_SUITE.erl @@ -0,0 +1,102 @@ +%% This suite contains cases that cannot be written +%% in Erlang itself and must be done via the abstract +%% format. + +-module(abstract_SUITE). + +-include_lib("common_test/include/ct.hrl"). +-include("dialyzer_test_constants.hrl"). + +-export([suite/0, all/0, init_per_suite/0, init_per_suite/1]). +-export([generated_case/1]). + +suite() -> + [{timetrap, {minutes, 1}}]. +all() -> + [generated_case]. + +init_per_suite() -> + [{timetrap, ?plt_timeout}]. +init_per_suite(Config) -> + OutDir = ?config(priv_dir, Config), + case dialyzer_common:check_plt(OutDir) of + fail -> {skip, "Plt creation/check failed."}; + ok -> [{dialyzer_options, []}|Config] + end. + +generated_case(Config) when is_list(Config) -> + %% Equivalent to: + %% + %% -module(foo). + %% -export(bar). + %% bar() -> + %% Arg = sample, + %% case Arg of + %% #{} -> map; + %% _ -> Arg:fn() + %% end. + %% + %% Except the case statement and its clauses are marked as autogenerated. + [] = + test([{attribute,1,module,foo}, + {attribute,2,export,[{bar,0}]}, + {function,3,bar,0, + [{clause,3,[],[], + [{match,4,{var,4,'Arg'},{atom,4,sample}}, + {'case',[{location,5},{generated,true}],{var,5,'Arg'}, + [{clause,[{location,6},{generated,true}],[{map,6,[]}],[], + [{atom,6,map}]}, + {clause,[{location,7},{generated,true}],[{var,7,'_'}],[], + [{call,7,{remote,7,{var,7,'Arg'},{atom,7,fn}},[]}]}]}]}]}], + Config, [], []), + %% With the first clause not auto-generated + [{warn_matching,{_,6},_}] = + test([{attribute,1,module,foo}, + {attribute,2,export,[{bar,0}]}, + {function,3,bar,0, + [{clause,3,[],[], + [{match,4,{var,4,'Arg'},{atom,4,sample}}, + {'case',[{location,5},{generated,true}],{var,5,'Arg'}, + [{clause,6,[{map,6,[]}],[], + [{atom,6,map}]}, + {clause,[{location,7},{generated,true}],[{var,7,'_'}],[], + [{call,7,{remote,7,{var,7,'Arg'},{atom,7,fn}},[]}]}]}]}]}], + Config, [], []), + %% With Arg set to [] so neither clause matches + [{warn_return_no_exit,{_,3},_}, + {warn_matching,{_,6},_}, + {warn_failing_call,{_,7},_}] = + test([{attribute,1,module,foo}, + {attribute,2,export,[{bar,0}]}, + {function,3,bar,0, + [{clause,3,[],[], + [{match,4,{var,4,'Arg'},{nil,4}}, + {'case',[{location,5},{generated,true}],{var,5,'Arg'}, + [{clause,[{location,6},{generated,true}],[{map,6,[]}],[], + [{atom,6,map}]}, + {clause,[{location,7},{generated,true}],[{var,7,'_'}],[], + [{call,7,{remote,7,{var,7,'Arg'},{atom,7,fn}},[]}]}]}]}]}], + Config, [], []), + ok. + +test(Prog, Config, COpts, DOpts) -> + {ok, BeamFile} = compile(Config, Prog, COpts), + run_dialyzer(Config, succ_typings, [BeamFile], DOpts). + +compile(Config, Prog, CompileOpts) -> + OutDir = ?config(priv_dir, Config), + Opts = [{outdir, OutDir}, debug_info, return_errors | CompileOpts], + {ok, Module, Source} = compile:forms(Prog, Opts), + BeamFile = filename:join([OutDir, lists:concat([Module, ".beam"])]), + ok = file:write_file(BeamFile, Source), + {ok, BeamFile}. + +run_dialyzer(Config, Analysis, Files, Opts) -> + OutDir = ?config(priv_dir, Config), + PltFilename = dialyzer_common:plt_file(OutDir), + dialyzer:run([{analysis_type, Analysis}, + {files, Files}, + {init_plt, PltFilename}, + {check_plt, false}, + {from, byte_code} | + Opts]). diff --git a/lib/dialyzer/test/dialyzer_common.erl b/lib/dialyzer/test/dialyzer_common.erl index d2b1026c06..48083a2731 100644 --- a/lib/dialyzer/test/dialyzer_common.erl +++ b/lib/dialyzer/test/dialyzer_common.erl @@ -7,7 +7,7 @@ -module(dialyzer_common). --export([check_plt/1, check/4, create_all_suites/0, new_tests/2]). +-export([check_plt/1, check/4, create_all_suites/0, new_tests/2, plt_file/1]). -include_lib("kernel/include/file.hrl"). @@ -39,7 +39,7 @@ check_plt(OutDir) -> io:format("Checking plt:"), - PltFilename = filename:join(OutDir, ?plt_filename), + PltFilename = plt_file(OutDir), case file:read_file_info(PltFilename) of {ok, _} -> dialyzer_check_plt(PltFilename); {error, _ } -> @@ -63,6 +63,11 @@ check_plt(OutDir) -> end end. +-spec plt_file(string()) -> string(). + +plt_file(OutDir) -> + filename:join(OutDir, ?plt_filename). + dialyzer_check_plt(PltFilename) -> try dialyzer:run([{analysis_type, plt_check}, {init_plt, PltFilename}]) of @@ -119,7 +124,7 @@ build_plt(PltFilename) -> 'same' | {differ, [term()]}. check(TestCase, Opts, Dir, OutDir) -> - PltFilename = filename:join(OutDir, ?plt_filename), + PltFilename = plt_file(OutDir), SrcDir = filename:join(Dir, ?input_files_directory), ResDir = filename:join(Dir, ?result_files_directory), Filename = filename:join(SrcDir, atom_to_list(TestCase)), -- cgit v1.2.3