Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide a Gradualizer diagnostic #1117

Merged
merged 23 commits into from
Oct 22, 2021
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions apps/els_lsp/priv/code_navigation/src/diagnostics_gradualizer.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-module(diagnostics_gradualizer).

-export([g/1]).

-spec f(integer()) -> integer().
f(N) ->
N.

-spec g(boolean()) -> boolean().
g(N) ->
f(N).
1 change: 1 addition & 0 deletions apps/els_lsp/src/els_diagnostics.erl
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ available_diagnostics() ->
, <<"compiler">>
, <<"crossref">>
, <<"dialyzer">>
, <<"gradualizer">>
, <<"elvis">>
, <<"unused_includes">>
, <<"unused_macros">>
Expand Down
106 changes: 106 additions & 0 deletions apps/els_lsp/src/els_gradualizer_diagnostics.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
%%==============================================================================
%% Compiler diagnostics
%%==============================================================================
-module(els_gradualizer_diagnostics).

%%==============================================================================
%% Behaviours
%%==============================================================================
-behaviour(els_diagnostics).

%%==============================================================================
%% Exports
%%==============================================================================
-export([ is_default/0
, run/1
, source/0
]).

%%==============================================================================
%% Includes
%%==============================================================================
-include("els_lsp.hrl").
-include_lib("kernel/include/logger.hrl").

%%==============================================================================
%% Type Definitions
%%==============================================================================
%% -type macro_config() :: #{string() => string()}.
erszcz marked this conversation as resolved.
Show resolved Hide resolved
%% -type macro_option() :: {atom()} | {atom(), any()}.

%%==============================================================================
%% Callback Functions
%%==============================================================================

-spec is_default() -> boolean().
is_default() ->
false.

-spec run(uri()) -> [els_diagnostics:diagnostic()].
run(Uri) ->
case start_and_load() of
true ->
Path = unicode:characters_to_list(els_uri:path(Uri)),
Includes = [{i, I} || I <- els_config:get(include_paths)],
Opts = [return_errors] ++ Includes,
Errors = gradualizer:type_check_files([Path], Opts),
lists:flatmap(fun analyzer_error/1, Errors);
false ->
[]
end.

-spec source() -> binary().
source() ->
<<"Gradualizer">>.

%%==============================================================================
%% Internal Functions
%%==============================================================================

-spec start_and_load() -> boolean().
start_and_load() ->
try application:ensure_all_started(gradualizer) of
{ok, [gradualizer]} ->
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In principle this could fail if, for example, gradualizer had an additional dependency which was not started. Maybe relax the matching to an {ok, _} or similar?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I wonder if we shouldn't execute this only once during the negotiation phase between client and server.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gradualizer is written with no runtime dependencies other than OTP libs. This check, together with the second clause {ok, []}, effectively makes this code run only the first time start_and_load is called.

Files = lists:flatmap(
fun(Dir) ->
filelib:wildcard(filename:join(Dir, "*.erl"))
end,
els_config:get(apps_paths) ++ els_config:get(deps_paths)),
case erlang:function_exported(
erszcz marked this conversation as resolved.
Show resolved Hide resolved
gradualizer_db, import_erl_files, 2) of
true ->
ok = gradualizer_db:import_erl_files(
Files,
els_config:get(include_paths));
false ->
ok = gradualizer_db:import_erl_files(Files)
end,
true;
{ok, []} ->
true
catch E:R ->
?LOG_ERROR("Could not start gradualizer: ~p ~p", [E, R]),
false
end.

-spec analyzer_error(any()) -> any().
analyzer_error({_Path, Error}) ->
FmtOpts = [{fmt_location, brief}, {color, never}],
FmtError = gradualizer_fmt:format_type_error(Error, FmtOpts),
case re:run(FmtError, "([0-9]+):([0-9]+:)? (.*)",
[{capture, all_but_first, binary}, dotall]) of
{match, [BinLine, _BinCol, Msg]} ->
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't the column be used for the diagnostic?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We had some trouble making this work reliably, but I don't remember the details. I think this is something worth working out in the future, but is not critical for getting the warnings - they're just reported at line granularity for now.

Line = case binary_to_integer(BinLine) of
0 -> 1;
L -> L
end,
Range = els_protocol:range(
#{ from => {Line, 1},
to => {Line + 1, 1} }),
[#{ range => Range,
erszcz marked this conversation as resolved.
Show resolved Hide resolved
message => Msg,
severity => ?DIAGNOSTIC_WARNING,
source => source() }];
_ ->
[]
end.
3 changes: 2 additions & 1 deletion apps/els_lsp/src/els_lsp.app.src
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
uuid,
getopt,
erlfmt,
els_core
els_core,
gradualizer
]},
{env, []},
{modules, []},
Expand Down
33 changes: 32 additions & 1 deletion apps/els_lsp/test/els_diagnostics_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
, exclude_unused_includes/1
, unused_macros/1
, unused_record_fields/1
, gradualizer/1
]).

%%==============================================================================
Expand Down Expand Up @@ -120,6 +121,11 @@ init_per_testcase(TestCase, Config) when TestCase =:= compiler_telemetry ->
els_mock_diagnostics:setup(),
mock_compiler_telemetry_enabled(),
els_test_utils:init_per_testcase(TestCase, Config);
init_per_testcase(TestCase, Config) when TestCase =:= gradualizer ->
meck:new(els_gradualizer_diagnostics, [passthrough, no_link]),
meck:expect(els_gradualizer_diagnostics, is_default, 0, true),
els_mock_diagnostics:setup(),
els_test_utils:init_per_testcase(TestCase, Config);
init_per_testcase(TestCase, Config) ->
els_mock_diagnostics:setup(),
els_test_utils:init_per_testcase(TestCase, Config).
Expand Down Expand Up @@ -156,6 +162,11 @@ end_per_testcase(TestCase, Config) when TestCase =:= compiler_telemetry ->
els_test_utils:end_per_testcase(TestCase, Config),
els_mock_diagnostics:teardown(),
ok;
end_per_testcase(TestCase, Config) when TestCase =:= gradualizer ->
meck:unload(els_gradualizer_diagnostics),
els_test_utils:end_per_testcase(TestCase, Config),
els_mock_diagnostics:teardown(),
ok;
end_per_testcase(TestCase, Config) ->
els_test_utils:end_per_testcase(TestCase, Config),
els_mock_diagnostics:teardown(),
Expand All @@ -169,7 +180,9 @@ bound_var_in_pattern(Config) ->
Uri = ?config(diagnostics_bound_var_in_pattern_uri, Config),
els_mock_diagnostics:subscribe(),
ok = els_client:did_save(Uri),
Diagnostics = els_mock_diagnostics:wait_until_complete(),
Diagnostics = lists:filter(fun (#{source := <<"BoundVarInPattern">>}) -> true;
(_) -> false end,
els_mock_diagnostics:wait_until_complete()),
Expected =
[ #{message => <<"Bound variable in pattern: Var1">>,
range =>
Expand Down Expand Up @@ -778,6 +791,24 @@ unused_record_fields(Config) ->
?assertEqual(Expected, lists:sort(F, Diagnostics)),
ok.

-spec gradualizer(config()) -> ok.
gradualizer(Config) ->
Uri = ?config(diagnostics_gradualizer_uri, Config),
els_mock_diagnostics:subscribe(),
ok = els_client:did_save(Uri),
Diagnostics = els_mock_diagnostics:wait_until_complete(),
Diagnostics == []
andalso ct:fail("Diagnostics should not be empty - is Gradualizer "
"available in the code path?"),
Expected = [#{message => <<"The variable N is expected to have type "
"integer() but it has type false | true\n">>,
range => #{'end' => #{character => 0, line => 11},
start => #{character => 0, line => 10}},
severity => 2, source => <<"Gradualizer">>}],
F = fun(#{message := M1}, #{message := M2}) -> M1 =< M2 end,
?assertEqual(Expected, lists:sort(F, Diagnostics)),
ok.

%%==============================================================================
%% Internal Functions
%%==============================================================================
Expand Down
1 change: 1 addition & 0 deletions rebar.config
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
, {ephemeral, "2.0.4"}
, {tdiff, "0.1.2"}
, {uuid, "2.0.1", {pkg, uuid_erl}}
, {gradualizer, {git, "https://github.com/josefs/Gradualizer.git", {ref, "0510404"}}}
]
}.

Expand Down
4 changes: 4 additions & 0 deletions rebar.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
{ref,"2e93fc4a646111357642b0179a2a63151868d890"}},
0},
{<<"getopt">>,{pkg,<<"getopt">>,<<"1.0.1">>},2},
{<<"gradualizer">>,
{git,"https://github.com/josefs/Gradualizer.git",
{ref,"051040400924dfcfe59f7a8936beb34b43278191"}},
0},
{<<"jsx">>,{pkg,<<"jsx">>,<<"3.0.0">>},0},
{<<"katana_code">>,{pkg,<<"katana_code">>,<<"0.2.1">>},1},
{<<"providers">>,{pkg,<<"providers">>,<<"1.8.1">>},1},
Expand Down