%% This suite is the only hand made and simply
%% checks if we can build and update a plt.
-module(plt_SUITE).
-include_lib("common_test/include/ct.hrl").
-include("dialyzer_test_constants.hrl").
-export([suite/0, all/0, build_plt/1, beam_tests/1, update_plt/1,
local_fun_same_as_callback/1,
remove_plt/1, run_plt_check/1, run_succ_typings/1,
bad_dialyzer_attr/1, merge_plts/1, bad_record_type/1]).
suite() ->
[{timetrap, ?plt_timeout}].
all() -> [build_plt, beam_tests, update_plt, run_plt_check,
remove_plt, run_succ_typings, local_fun_same_as_callback,
bad_dialyzer_attr, merge_plts, bad_record_type].
build_plt(Config) ->
OutDir = ?config(priv_dir, Config),
case dialyzer_common:check_plt(OutDir) of
ok -> ok;
fail -> ct:fail(plt_build_fail)
end.
beam_tests(Config) when is_list(Config) ->
PrivDir = ?config(priv_dir, Config),
Plt = filename:join(PrivDir, "beam_tests.plt"),
Prog = <<"
-module(no_auto_import).
%% Copied from erl_lint_SUITE.erl, clash6
-export([size/1]).
size([]) ->
0;
size({N,_}) ->
N;
size([_|T]) ->
1+size(T).
">>,
Opts = [no_auto_import],
{ok, BeamFile} = compile(Config, Prog, no_auto_import, Opts),
[] = run_dialyzer(plt_build, [BeamFile], [{output_plt, Plt}]),
ok.
run_plt_check(Config) when is_list(Config) ->
PrivDir = ?config(priv_dir, Config),
Plt = filename:join(PrivDir, "run_plt_check.plt"),
Mod1 = <<"
-module(run_plt_check1).
">>,
Mod2A = <<"
-module(run_plt_check2).
">>,
{ok, BeamFile1} = compile(Config, Mod1, run_plt_check1, []),
{ok, BeamFile2} = compile(Config, Mod2A, run_plt_check2, []),
[] = run_dialyzer(plt_build, [BeamFile1, BeamFile2], [{output_plt, Plt}]),
Mod2B = <<"
-module(run_plt_check2).
-export([call/1]).
call(X) -> run_plt_check1:call(X).
">>,
{ok, BeamFile2} = compile(Config, Mod2B, run_plt_check2, []),
% callgraph warning as run_plt_check2:call/1 makes a call to unexported
% function run_plt_check1:call/1.
[_] = run_dialyzer(plt_check, [], [{init_plt, Plt}]),
ok.
run_succ_typings(Config) when is_list(Config) ->
PrivDir = ?config(priv_dir, Config),
Plt = filename:join(PrivDir, "run_succ_typings.plt"),
Mod1A = <<"
-module(run_succ_typings1).
-export([call/0]).
call() -> a.
">>,
{ok, BeamFile1} = compile(Config, Mod1A, run_succ_typings1, []),
[] = run_dialyzer(plt_build, [BeamFile1], [{output_plt, Plt}]),
Mod1B = <<"
-module(run_succ_typings1).
-export([call/0]).
call() -> b.
">>,
Mod2 = <<"
-module(run_succ_typings2).
-export([call/0]).
-spec call() -> b.
call() -> run_succ_typings1:call().
">>,
{ok, BeamFile1} = compile(Config, Mod1B, run_succ_typings1, []),
{ok, BeamFile2} = compile(Config, Mod2, run_succ_typings2, []),
% contract types warning as run_succ_typings2:call/0 makes a call to
% run_succ_typings1:call/0, which returns a (not b) in the PLT.
[_] = run_dialyzer(succ_typings, [BeamFile2],
[{check_plt, false}, {init_plt, Plt}]),
% warning not returned as run_succ_typings1 is updated in the PLT.
[] = run_dialyzer(succ_typings, [BeamFile2],
[{check_plt, true}, {init_plt, Plt}]),
ok.
%%% [James Fish:]
%%% If a function is removed from a module and the module has previously
%%% been added to a PLT, the function will not be removed from PLT when
%%% the PLT is checked. This results in dialyzer failing to produce a
%%% callgraph warning when doing success typings analysis if the remove
%%% function is still called in another module
%%% As the function is not removed from the PLT a prior warning, such as a
%%% contract types warning, might be emitted when the removed function
%%% nolonger exists.
update_plt(Config) ->
PrivDir = ?config(priv_dir, Config),
Prog1 = <<"-module(plt_gc).
-export([one/0]).
one() ->
one.">>,
{ok, Beam} = compile(Config, Prog1, plt_gc, []),
ErlangBeam = case code:where_is_file("erlang.beam") of
non_existing ->
filename:join([code:root_dir(),
"erts", "preloaded", "ebin",
"erlang.beam"]);
EBeam ->
EBeam
end,
Plt = filename:join(PrivDir, "plt_gc.plt"),
Opts = [{check_plt, true}, {from, byte_code}],
[] = dialyzer:run([{analysis_type, plt_build},
{files, [Beam, ErlangBeam]},
{output_plt, Plt}] ++ Opts),
Prog2 = <<"-module(plt_gc).
-export([two/0]).
two() ->
two.">>,
{ok, Beam} = compile(Config, Prog2, plt_gc, []),
Test = <<"-module(test).
-export([test/0]).
-spec test() -> test.
test() ->
plt_gc:one().">>,
{ok, TestBeam} = compile(Config, Test, test, []),
[{warn_callgraph, _, {call_to_missing, [plt_gc, one, 0]}}] =
dialyzer:run([{analysis_type, succ_typings},
{files, [TestBeam]},
{init_plt, Plt}] ++ Opts),
ok.
%%% If a behaviour module contains an non-exported function with the same name
%%% as one of the behaviour's callbacks, the callback info was inadvertently
%%% deleted from the PLT as the dialyzer_plt:delete_list/2 function was cleaning
%%% up the callback table. This bug was reported by Brujo Benavides.
local_fun_same_as_callback(Config) when is_list(Config) ->
PrivDir = ?config(priv_dir, Config),
Prog1 =
<<"-module(bad_behaviour).
-callback bad() -> bad.
-export([publicly_bad/0]).
%% @doc This function is here just to avoid the 'unused' warning for bad/0
publicly_bad() -> bad().
%% @doc This function overlaps with the callback with the same name, and
%% that was an issue for dialyzer since it's a private function.
bad() -> bad.">>,
{ok, Beam} = compile(Config, Prog1, bad_behaviour, []),
ErlangBeam = case code:where_is_file("erlang.beam") of
non_existing ->
filename:join([code:root_dir(),
"erts", "preloaded", "ebin",
"erlang.beam"]);
EBeam ->
EBeam
end,
Plt = filename:join(PrivDir, "plt_bad_behaviour.plt"),
Opts = [{check_plt, true}, {from, byte_code}],
[] = dialyzer:run([{analysis_type, plt_build},
{files, [Beam, ErlangBeam]},
{output_plt, Plt}] ++ Opts),
Prog2 =
<<"-module(bad_child).
-behaviour(bad_behaviour).
-export([bad/0]).
%% @doc This function incorreclty implements bad_behaviour.
bad() -> not_bad.">>,
{ok, TestBeam} = compile(Config, Prog2, bad_child, []),
[{warn_behaviour, _,
{callback_type_mismatch,
[bad_behaviour,bad,0,"'not_bad'","'bad'"]}}] =
dialyzer:run([{analysis_type, succ_typings},
{files, [TestBeam]},
{init_plt, Plt}] ++ Opts),
ok.
%%% [James Fish:]
%%% Dialyzer always asserts that files and directories passed in its
%%% options exist. Therefore it is not possible to remove a beam/module
%%% from a PLT when the beam file no longer exists. Dialyzer should not to
%%% check files exist on disk when removing from the PLT.
remove_plt(Config) ->
PrivDir = ?config(priv_dir, Config),
Prog1 = <<"-module(m1).
-export([t/0]).
t() ->
m2:a(a).">>,
{ok, Beam1} = compile(Config, Prog1, m1, []),
Prog2 = <<"-module(m2).
-export([a/1]).
a(A) when is_integer(A) -> A.">>,
{ok, Beam2} = compile(Config, Prog2, m2, []),
Plt = filename:join(PrivDir, "remove.plt"),
Opts = [{check_plt, true}, {from, byte_code}],
[{warn_return_no_exit, _, {no_return,[only_normal,t,0]}},
{warn_failing_call, _, {call, [m2,a,"('a')",_,_,_,_,_]}}] =
dialyzer:run([{analysis_type, plt_build},
{files, [Beam1, Beam2]},
{get_warnings, true},
{output_plt, Plt}] ++ Opts),
[] = dialyzer:run([{init_plt, Plt},
{files, [Beam2]},
{analysis_type, plt_remove}]),
[] = dialyzer:run([{analysis_type, succ_typings},
{files, [Beam1]},
{init_plt, Plt}] ++ Opts),
ok.
%% ERL-283, OTP-13979. As of OTP-14323 this test no longer does what
%% it is designed to do--the linter stops every attempt to run the
%% checks of Dialyzer's on bad dialyzer attributes. For the time
%% being, the linter's error message are checked instead. The test
%% needs to be updated when/if the Dialyzer can analyze Core Erlang
%% without compiling abstract code.
bad_dialyzer_attr(Config) ->
PrivDir = ?config(priv_dir, Config),
Source = lists:concat([dial, ".erl"]),
Filename = filename:join(PrivDir, Source),
ok = dialyzer_common:check_plt(PrivDir),
PltFilename = dialyzer_common:plt_file(PrivDir),
Opts = [{files, [Filename]},
{check_plt, false},
{from, src_code},
{init_plt, PltFilename}],
Prog1 = <<"-module(dial).
-dialyzer({no_return, [undef/0]}).">>,
ok = file:write_file(Filename, Prog1),
{dialyzer_error,
"Analysis failed with error:\n" ++ Str1} =
(catch dialyzer:run(Opts)),
P1 = string:str(Str1, "dial.erl:2: function undef/0 undefined"),
true = P1 > 0,
Prog2 = <<"-module(dial).
-dialyzer({no_return, [{undef,1,2}]}).">>,
ok = file:write_file(Filename, Prog2),
{dialyzer_error,
"Analysis failed with error:\n" ++ Str2} =
(catch dialyzer:run(Opts)),
P2 = string:str(Str2, "dial.erl:2: badly formed dialyzer "
"attribute: {no_return,{undef,1,2}}"),
true = P2 > 0,
ok.
merge_plts(Config) ->
%% A few checks of merging PLTs.
fun() ->
{Mod1, Mod2} = types(),
{BeamFiles, Plt1, Plt2} = create_plts(Mod1, Mod2, Config),
{dialyzer_error,
"Could not merge PLTs since they are not disjoint"++_} =
(catch run_dialyzer(succ_typings, BeamFiles,
[{plts, [Plt1, Plt1]}])),
[{warn_contract_types,_,_}] =
run_dialyzer(succ_typings, BeamFiles,
[{warnings, [unknown]},
{plts, [Plt1, Plt2]}])
end(),
fun() ->
{Mod1, Mod2} = callbacks(),
{BeamFiles, Plt1, Plt2} = create_plts(Mod1, Mod2, Config),
{dialyzer_error,
"Could not merge PLTs since they are not disjoint"++_} =
(catch run_dialyzer(succ_typings, BeamFiles,
[{plts, [Plt1, Plt1]}])),
[] =
run_dialyzer(succ_typings, BeamFiles,
[{warnings, [unknown]},
{plts, [Plt1, Plt2]}])
end(),
ok.
types() ->
Mod1 = <<"-module(merge_plts_1).
-export([f/0]).
-export_type([t/0]).
-type t() :: merge_plts_2:t().
-spec f() -> t().
f() -> 1. % Not an atom().
">>,
Mod2 = <<"-module(merge_plts_2).
-export_type([t/0]).
-type t() :: atom().
">>,
{Mod1, Mod2}.
callbacks() -> % A very shallow test.
Mod1 = <<"-module(merge_plts_1).
-callback t() -> merge_plts_2:t().
">>,
Mod2 = <<"-module(merge_plts_2).
-export_type([t/0]).
-type t() :: atom().
">>,
{Mod1, Mod2}.
create_plts(Mod1, Mod2, Config) ->
PrivDir = ?config(priv_dir, Config),
Plt1 = filename:join(PrivDir, "merge_plts_1.plt"),
Plt2 = filename:join(PrivDir, "merge_plts_2.plt"),
ErlangBeam = erlang_beam(),
{ok, BeamFile1} = compile(Config, Mod1, merge_plts_1, []),
[] = run_dialyzer(plt_build, [ErlangBeam,BeamFile1], [{output_plt,Plt1}]),
{ok, BeamFile2} = compile(Config, Mod2, merge_plts_2, []),
[] = run_dialyzer(plt_build, [BeamFile2], [{output_plt, Plt2}]),
{[BeamFile1, BeamFile2], Plt1, Plt2}.
%% End of merge_plts().
bad_record_type(Config) ->
PrivDir = ?config(priv_dir, Config),
Source = lists:concat([bad_record_type, ".erl"]),
Filename = filename:join(PrivDir, Source),
PltFilename = dialyzer_common:plt_file(PrivDir),
Opts = [{files, [Filename]},
{check_plt, false},
{from, src_code},
{init_plt, PltFilename}],
Prog = <<"-module(bad_record_type).
-export([r/0]).
-record(r, {f = 3 :: integer()}).
-spec r() -> #r{f :: atom()}.
r() ->
#r{}.">>,
ok = file:write_file(Filename, Prog),
{dialyzer_error,
"Analysis failed with error:\n" ++ Str} =
(catch dialyzer:run(Opts)),
P = string:str(Str,
"bad_record_type.erl:4: Illegal declaration of #r{f}"),
true = P > 0,
ok.
erlang_beam() ->
case code:where_is_file("erlang.beam") of
non_existing ->
filename:join([code:root_dir(),
"erts", "preloaded", "ebin",
"erlang.beam"]);
EBeam ->
EBeam
end.
compile(Config, Prog, Module, CompileOpts) ->
Source = lists:concat([Module, ".erl"]),
PrivDir = ?config(priv_dir,Config),
Filename = filename:join([PrivDir, Source]),
ok = file:write_file(Filename, Prog),
Opts = [{outdir, PrivDir}, debug_info | CompileOpts],
{ok, Module} = compile:file(Filename, Opts),
{ok, filename:join([PrivDir, lists:concat([Module, ".beam"])])}.
run_dialyzer(Analysis, Files, Opts) ->
dialyzer:run([{analysis_type, Analysis},
{files, Files},
{from, byte_code} |
Opts]).