Skip to content

Commit

Permalink
Use erl_lint to detect compile errors
Browse files Browse the repository at this point in the history
  • Loading branch information
zuiderkwast committed Dec 20, 2021
1 parent a438368 commit ad1305a
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 52 deletions.
54 changes: 40 additions & 14 deletions src/gradualizer.erl
Original file line number Diff line number Diff line change
Expand Up @@ -56,21 +56,47 @@ type_check_file(File) ->
%% @doc Type check a source or beam file
-spec type_check_file(file:filename(), options()) -> ok | nok | [{file:filename(), any()}].
type_check_file(File, Opts) ->
ParsedFile =
case filename:extension(File) of
".erl" ->
Includes = proplists:get_all_values(i, Opts),
gradualizer_file_utils:get_forms_from_erl(File, Includes);
".beam" ->
gradualizer_file_utils:get_forms_from_beam(File);
Ext ->
throw({unknown_file_extension, Ext})
end,
case ParsedFile of
{ok, Forms} ->
case filename:extension(File) of
".erl" ->
Includes = proplists:get_all_values(i, Opts),
case gradualizer_file_utils:get_forms_from_erl(File, Includes) of
{ok, Forms} ->
lint_and_check_forms(Forms, File, Opts);
Error ->
throw(Error)
end;
".beam" ->
case gradualizer_file_utils:get_forms_from_beam(File) of
{ok, Forms} ->
type_check_forms(File, Forms, Opts);
Error ->
throw(Error)
end;
Ext ->
throw({unknown_file_extension, Ext})
end.

%% @doc Runs an erl_lint pass, to check if the forms can be compiled at all,
%% before running the type checker.
-spec lint_and_check_forms([erl_parse:abstract_form()], file:filename(), options()) ->
ok | nok | [{file:filename(), any()}].
lint_and_check_forms(Forms, File, Opts) ->
case erl_lint:module(Forms, File, [return_errors]) of
{ok, _Warnings} ->
type_check_forms(File, Forms, Opts);
Error ->
throw(Error)
{error, Errors, _Warnings} ->
%% If there are lint errors (i.e. compile errors like undefined
%% variables) we don't even try to type check.
case proplists:get_bool(return_errors, Opts) of
true ->
[{Filename, ErrorInfo} || {Filename, ErrorInfos} <- Errors,
ErrorInfo <- ErrorInfos];
false ->
[gradualizer_fmt:print_errors(ErrorInfos,
[{filename, Filename} | Opts])
|| {Filename, ErrorInfos} <- Errors],
nok
end
end.

%% @doc Type check a module
Expand Down
13 changes: 13 additions & 0 deletions src/gradualizer_fmt.erl
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,19 @@ format_type_error({bad_type_annotation, TypeLit}, Opts) ->
[format_location(TypeLit, brief, Opts),
pp_expr(TypeLit, Opts),
format_location(TypeLit, verbose, Opts)]);
format_type_error({Location, Module, ErrorDescription}, Opts)
when is_integer(Location) orelse is_tuple(Location),
is_atom(Module) ->
%% OTP compiler style error descriptor
io_lib:format(
"~s~s~s~n",
[format_location(Location, brief, Opts),
Module:format_error(ErrorDescription),
format_location(Location, verbose, Opts)]);
format_type_error({none, Module, ErrorDescription}, _Opts)
when is_atom(Module) ->
%% OTP compiler style error descriptor, without location
io_lib:format("~s~n", [Module:format_error(ErrorDescription)]);
format_type_error(type_error, _) ->
io_lib:format("TYPE ERROR~n", []).

Expand Down
5 changes: 2 additions & 3 deletions src/typechecker.erl
Original file line number Diff line number Diff line change
Expand Up @@ -3283,11 +3283,10 @@ get_bounded_fun_type_list(Name, Arity, Env, P) ->
{ok, Types} = gradualizer_db:get_spec(erlang, Name, Arity),
lists:map(fun typelib:remove_pos/1, Types);
false ->
%% If it's not imported, the file doesn't compile.
case get_imported_bounded_fun_type_list(Name, Arity, Env, P) of
{ok, Types} ->
Types;
error ->
throw({call_undef, P, Name, Arity})
Types
end
end
end.
Expand Down
43 changes: 43 additions & 0 deletions test/misc/lint_errors.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
%% This module doesn't even compile. The errors here are caught by erl_lint.
-module(lint_errors).
-export([local_type/0,
local_call/0,
one_more_for_the_record/0,
local_record/1,
record_field/1,
illegal_binary_segment/1,
invalid_record_info/0,
illegal_pattern/1]).

-spec local_type() -> undefined_type().
local_type() -> ok.

-spec local_call() -> ok.
local_call() -> undefined_call().

-record(r, {a :: #s{}}).

%% The number of expected errors are the number of exported functions,
%% so we create a function without errors, to account for the error in
%% the record definition above.
one_more_for_the_record() -> ok.

-spec local_record(#r{}) -> boolean().
local_record(R) -> if
(R#r.a)#s.a == c -> true;
true -> false
end.

-spec record_field(#r{}) -> boolean().
record_field(R) -> if
R#r.b == c -> true;
true -> false
end.

illegal_binary_segment(X) ->
<<X:42/utf8>>. %% Size not allowed with utf8/16/32

invalid_record_info() -> record_info(foo, bar).

-spec illegal_pattern(gradualizer:top()) -> gradualizer:top().
illegal_pattern(1 + A) -> ok.
27 changes: 3 additions & 24 deletions test/misc/undefined_errors.erl
Original file line number Diff line number Diff line change
@@ -1,41 +1,20 @@
-module(undefined_errors).
-export([local_type/0, remote_type/0,
local_call/0, remote_call/0,
local_record/1, remote_record/0,
record_field/1,
-export([remote_type/0,
remote_call/0,
remote_record/0,
normalize_remote_type/0,
not_exported/0]).

-spec local_type() -> undefined_type().
local_type() -> ok.

-spec remote_type() -> undefined_errors_helper:j().
remote_type() -> ok.

-spec local_call() -> ok.
local_call() -> undefined_call().

-spec remote_call() -> ok.
remote_call() -> undefined_errors_helper:undefined_call().

-record(r, {a :: #s{}}).

-spec local_record(#r{}) -> boolean().
local_record(R) -> if
(R#r.a)#s.a == c -> true;
true -> false
end.

-record(defined_record, {a, b, c}).
-spec remote_record() -> #defined_record{}.
remote_record() -> undefined_errors_helper:und_rec().

-spec record_field(#r{}) -> boolean().
record_field(R) -> if
R#r.b == c -> true;
true -> false
end.

-spec normalize_remote_type() -> ok.
normalize_remote_type() -> undefined_errors_helper:und_ty().

Expand Down
23 changes: 12 additions & 11 deletions test/undefined_errors_test.erl
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ undefined_errors_test_() ->
[ok = application:stop(App) || App <- Apps],
ok
end,
?_test(begin
File = "test/misc/undefined_errors.erl",
Errors = gradualizer:type_check_file(File, [return_errors]),
%% Test that error formatting doesn't crash
Opts = [{fmt_location, brief},
{fmt_expr_fun, fun erl_prettypr:format/1}],
lists:foreach(fun({_, Error}) -> gradualizer_fmt:handle_type_error(Error, Opts) end, Errors),
{ok, Forms} = gradualizer_file_utils:get_forms_from_erl(File, []),
ExpectedErrors = typechecker:number_of_exported_functions(Forms),
?assertEqual(ExpectedErrors, length(Errors))
end)}.
[?_test(check_file("test/misc/undefined_errors.erl")),
?_test(check_file("test/misc/lint_errors.erl"))]}.

check_file(File) ->
Errors = gradualizer:type_check_file(File, [return_errors]),
%% Test that error formatting doesn't crash
Opts = [{fmt_location, brief},
{fmt_expr_fun, fun erl_prettypr:format/1}],
lists:foreach(fun({_, Error}) -> gradualizer_fmt:handle_type_error(Error, Opts) end, Errors),
{ok, Forms} = gradualizer_file_utils:get_forms_from_erl(File, []),
ExpectedErrors = typechecker:number_of_exported_functions(Forms),
?assertEqual(ExpectedErrors, length(Errors)).

0 comments on commit ad1305a

Please sign in to comment.