Skip to content

Commit f089571

Browse files
authored
Warn on unused require (#14779)
1 parent 811767f commit f089571

File tree

8 files changed

+110
-8
lines changed

8 files changed

+110
-8
lines changed

lib/elixir/lib/inspect.ex

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -275,8 +275,6 @@ defprotocol Inspect do
275275
end
276276

277277
defimpl Inspect, for: Atom do
278-
require Macro
279-
280278
def inspect(atom, opts) do
281279
color_doc(Macro.inspect_atom(:literal, atom), color_key(atom), opts)
282280
end

lib/elixir/lib/kernel/lexical_tracker.ex

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ defmodule Kernel.LexicalTracker do
5757
:gen_server.cast(pid, {:add_export, module})
5858
end
5959

60+
@doc false
61+
def add_require(pid, module, meta) when is_atom(module) do
62+
:gen_server.cast(pid, {:add_require, module, meta})
63+
end
64+
6065
@doc false
6166
def add_import(pid, module, fas, meta, warn) when is_atom(module) do
6267
:gen_server.cast(pid, {:add_import, module, fas, meta, warn})
@@ -119,12 +124,18 @@ defmodule Kernel.LexicalTracker do
119124
:gen_server.call(pid, :unused_aliases, @timeout)
120125
end
121126

127+
@doc false
128+
def collect_unused_requires(pid) do
129+
:gen_server.call(pid, :unused_requires, @timeout)
130+
end
131+
122132
# Callbacks
123133

124134
def init(:ok) do
125135
state = %{
126136
aliases: %{},
127137
imports: %{},
138+
requires: %{},
128139
references: %{},
129140
exports: %{},
130141
cache: %{},
@@ -150,6 +161,17 @@ defmodule Kernel.LexicalTracker do
150161
{:reply, Enum.sort(imports), state}
151162
end
152163

164+
def handle_call(:unused_requires, _from, state) do
165+
unused_requires =
166+
for {module, meta} <- state.requires,
167+
Map.get(state.references, module) != :compile,
168+
Keyword.get(meta[:opts], :warn, true) != false do
169+
{module, meta}
170+
end
171+
172+
{:reply, Enum.sort(unused_requires), state}
173+
end
174+
153175
def handle_call(:references, _from, state) do
154176
{compile, runtime} = partition(Map.to_list(state.references), [], [])
155177
{:reply, {compile, Map.keys(state.exports), runtime, state.compile_env}, state}
@@ -245,6 +267,10 @@ defmodule Kernel.LexicalTracker do
245267
{:noreply, put_in(state.imports[module][@warn_key], true)}
246268
end
247269

270+
def handle_cast({:add_require, module, meta}, state) do
271+
{:noreply, put_in(state.requires[module], meta)}
272+
end
273+
248274
@doc false
249275
def handle_info(_msg, state) do
250276
{:noreply, state}

lib/elixir/lib/protocol.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1032,7 +1032,7 @@ defmodule Protocol do
10321032
# expression (and therefore most likely a compile-time one).
10331033
behaviour =
10341034
if is_atom(protocol) do
1035-
quote(do: require(unquote(protocol)))
1035+
quote(do: require(unquote(protocol), warn: false))
10361036
else
10371037
quote(do: protocol)
10381038
end

lib/elixir/src/elixir_import.erl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import(Meta, Ref, Opts, E, Warn, Trace, InfoCallback) ->
2020
{Functions, Macros, Added} ->
2121
Trace andalso elixir_env:trace({import, Meta, Ref, Opts}, E),
2222
EI = E#{functions := Functions, macros := Macros},
23-
{ok, Added, elixir_aliases:require(Meta, Ref, Opts, EI, Trace)};
23+
{ok, Added, elixir_aliases:require(Meta, Ref, [{warn, false} | Opts], EI, Trace)};
2424

2525
{error, Reason} ->
2626
{error, Reason}

lib/elixir/src/elixir_lexical.erl

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ run(#{tracers := Tracers} = E, ExecutionCallback, AfterExecutionCallback) ->
1818
Res ->
1919
warn_unused_aliases(Pid, LexEnv),
2020
warn_unused_imports(Pid, LexEnv),
21+
warn_unused_requires(Pid, LexEnv),
2122
AfterExecutionCallback(LexEnv),
2223
Res
2324
after
@@ -33,10 +34,12 @@ run(#{tracers := Tracers} = E, ExecutionCallback, AfterExecutionCallback) ->
3334
trace({alias_expansion, _Meta, Lookup, _Result}, #{lexical_tracker := Pid}) ->
3435
?tracker:alias_dispatch(Pid, Lookup),
3536
ok;
36-
trace({require, Meta, Module, _Opts}, #{lexical_tracker := Pid}) ->
37+
trace({require, Meta, Module, Opts}, #{lexical_tracker := Pid, module := CurrentModule}) ->
3738
case lists:keyfind(from_macro, 1, Meta) of
3839
{from_macro, true} -> ?tracker:remote_dispatch(Pid, Module, compile);
39-
_ -> ?tracker:add_export(Pid, Module)
40+
_ ->
41+
?tracker:add_export(Pid, Module),
42+
?tracker:add_require(Pid, Module, [{module, CurrentModule}, {opts, Opts} | Meta])
4043
end,
4144
ok;
4245
trace({struct_expansion, _Meta, Module, _Keys}, #{lexical_tracker := Pid}) ->
@@ -95,6 +98,11 @@ warn_unused_imports(Pid, E) ->
9598
{ModOrMFA, Meta} <- unused_imports_for_module(Module, Imports)],
9699
ok.
97100

101+
warn_unused_requires(Pid, E) ->
102+
[elixir_errors:file_warn(Meta, ?key(E, file), ?MODULE, {unused_require, Module})
103+
|| {Module, Meta} <- ?tracker:collect_unused_requires(Pid)],
104+
ok.
105+
98106
unused_imports_for_module(Module, Imports) ->
99107
case Imports of
100108
#{Module := Meta} -> [{Module, Meta}];
@@ -111,4 +119,6 @@ format_error({unused_alias, Module}) ->
111119
format_error({unused_import, {Module, Function, Arity}}) ->
112120
io_lib:format("unused import ~ts.~ts/~w", [elixir_aliases:inspect(Module), Function, Arity]);
113121
format_error({unused_import, Module}) ->
114-
io_lib:format("unused import ~ts", [elixir_aliases:inspect(Module)]).
122+
io_lib:format("unused import ~ts", [elixir_aliases:inspect(Module)]);
123+
format_error({unused_require, Module}) ->
124+
io_lib:format("unused require ~ts", [elixir_aliases:inspect(Module)]).

lib/elixir/test/elixir/code_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ defmodule CodeTest do
364364
end
365365

366366
test "with defguard" do
367-
require Integer
367+
require Integer, warn: false
368368
env = Code.env_for_eval(__ENV__)
369369
quoted = quote do: Integer.is_even(1)
370370
{false, binding, env} = Code.eval_quoted_with_env(quoted, [], env, prune_binding: true)

lib/elixir/test/elixir/kernel/lexical_tracker_test.exs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,53 @@ defmodule Kernel.LexicalTrackerTest do
9191
assert D.collect_unused_imports(config[:pid]) == [{String, %{}}]
9292
end
9393

94+
test "unused requires", config do
95+
D.add_require(config[:pid], String, module: TestModule, opts: [])
96+
assert D.collect_unused_requires(config[:pid]) == [{String, [module: TestModule, opts: []]}]
97+
end
98+
99+
test "used requires are not unused", config do
100+
D.add_require(config[:pid], String, module: TestModule, opts: [])
101+
D.remote_dispatch(config[:pid], String, :compile)
102+
assert D.collect_unused_requires(config[:pid]) == []
103+
end
104+
105+
test "requires with warn: false are not unused", config do
106+
D.add_require(config[:pid], String, module: TestModule, opts: [warn: false])
107+
assert D.collect_unused_requires(config[:pid]) == []
108+
end
109+
110+
test "requires with warn: true are unused when not referenced", config do
111+
D.add_require(config[:pid], String, module: TestModule, opts: [warn: true])
112+
113+
assert D.collect_unused_requires(config[:pid]) == [
114+
{String, [module: TestModule, opts: [warn: true]]}
115+
]
116+
end
117+
118+
test "multiple unused requires", config do
119+
D.add_require(config[:pid], String, module: TestModule, opts: [])
120+
D.add_require(config[:pid], List, module: TestModule, opts: [])
121+
122+
assert D.collect_unused_requires(config[:pid]) == [
123+
{List, [module: TestModule, opts: []]},
124+
{String, [module: TestModule, opts: []]}
125+
]
126+
end
127+
128+
test "mixed used and unused requires", config do
129+
D.add_require(config[:pid], String, module: TestModule, opts: [])
130+
D.add_require(config[:pid], List, module: TestModule, opts: [])
131+
D.remote_dispatch(config[:pid], String, :compile)
132+
assert D.collect_unused_requires(config[:pid]) == [{List, [module: TestModule, opts: []]}]
133+
end
134+
135+
test "function calls do not count as macro usage", config do
136+
D.add_require(config[:pid], String, module: TestModule, opts: [])
137+
D.remote_dispatch(config[:pid], String, :runtime)
138+
assert D.collect_unused_requires(config[:pid]) == [{String, [module: TestModule, opts: []]}]
139+
end
140+
94141
test "imports with no warn are not unused", config do
95142
D.add_import(config[:pid], String, [], 1, false)
96143
assert D.collect_unused_imports(config[:pid]) == []

lib/elixir/test/elixir/kernel/warning_test.exs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2269,6 +2269,27 @@ defmodule Kernel.WarningTest do
22692269
Enum.each(list, &purge/1)
22702270
end
22712271

2272+
test "unused require" do
2273+
assert_warn_compile(
2274+
["nofile:2:3", "unused require Application"],
2275+
"""
2276+
defmodule Sample do
2277+
require Application
2278+
def a, do: nil
2279+
end
2280+
"""
2281+
)
2282+
2283+
assert_warn_compile(
2284+
["nofile:1:1", "unused require Logger"],
2285+
"""
2286+
require Logger
2287+
"""
2288+
)
2289+
after
2290+
purge(Sample)
2291+
end
2292+
22722293
defp purge(module) when is_atom(module) do
22732294
:code.purge(module)
22742295
:code.delete(module)

0 commit comments

Comments
 (0)