diff options
author | Eric Meadows-Jönsson <eric.meadows.jonsson@gmail.com> | 2019-06-28 16:11:20 +0200 |
---|---|---|
committer | Eric Meadows-Jönsson <eric.meadows.jonsson@gmail.com> | 2019-06-28 16:11:20 +0200 |
commit | 05bf681a22bfd7f15978ec881b1a7b66bc854f5b (patch) | |
tree | d8d932266d344326a3da6dbee373acbacd373fce | |
parent | 8ab017ea1dd911c42e23c9dcfe6dbb7ba61d91c0 (diff) | |
download | elixir-05bf681a22bfd7f15978ec881b1a7b66bc854f5b.tar.gz |
Move deprecation check to group pass
-rw-r--r-- | lib/elixir/lib/code.ex | 6 | ||||
-rw-r--r-- | lib/elixir/lib/module/checker.ex | 51 | ||||
-rw-r--r-- | lib/elixir/src/elixir.erl | 3 | ||||
-rw-r--r-- | lib/elixir/test/elixir/module/checker_test.exs | 689 | ||||
-rw-r--r-- | lib/mix/lib/mix/tasks/compile.elixir.ex | 4 |
5 files changed, 438 insertions, 315 deletions
diff --git a/lib/elixir/lib/code.ex b/lib/elixir/lib/code.ex index b71e73c0a..aaa793a99 100644 --- a/lib/elixir/lib/code.ex +++ b/lib/elixir/lib/code.ex @@ -38,7 +38,7 @@ defmodule Code do :warnings_as_errors ] - @list_compiler_options [:no_warn_undefined] + @list_compiler_options [:no_warn_undefined, :no_warn_deprecated] @available_compiler_options @boolean_compiler_options ++ @list_compiler_options @@ -914,6 +914,10 @@ defmodule Code do at compilation time. This can be useful when doing dynamic compilation. Defaults to `[]`. + * `:no_warn_deprecated` - list of modules and `{Mod, fun, arity}` tuples + that will not emit warnings that a function within the module or the + specific function is deprecated. Defaults to `[]`. + It returns the new map of compiler options. ## Examples diff --git a/lib/elixir/lib/module/checker.ex b/lib/elixir/lib/module/checker.ex index a9252c09a..dd9343b6a 100644 --- a/lib/elixir/lib/module/checker.ex +++ b/lib/elixir/lib/module/checker.ex @@ -42,9 +42,9 @@ defmodule Module.Checker do end # Mod.fun(...) - defp check_body({{:., meta, [module, fun]}, _, args}, state) + defp check_body({{:., meta, [module, fun]}, call_meta, args}, state) when is_atom(module) and is_atom(fun) do - check_remote(module, fun, length(args), meta, state) + check_remote(module, fun, length(args), meta ++ call_meta, state) end # Function call @@ -68,21 +68,21 @@ defmodule Module.Checker do defp check_remote(module, fun, arity, meta, state) do cond do - not warn_if_undefined?(module, fun, arity, state) -> - [] - # TODO: In the future we may want to warn for modules defined # in the local context Keyword.get(meta, :context_module, false) and state.module != module -> [] - not Code.ensure_loaded?(module) -> + not Code.ensure_loaded?(module) and warn_undefined?(module, fun, arity, state) -> warn(meta, state, {:undefined_module, module, fun, arity}) - not function_exported?(module, fun, arity) -> + not function_exported?(module, fun, arity) and warn_undefined?(module, fun, arity, state) -> exports = exports_for(module) warn(meta, state, {:undefined_function, module, fun, arity, exports}) + (reason = deprecated(module, fun, arity)) && warn_deprecated?(module, fun, arity, state) -> + warn(meta, state, {:deprecated, module, fun, arity, reason}) + true -> [] end @@ -90,19 +90,38 @@ defmodule Module.Checker do # TODO: Do not warn inside guards # TODO: Properly handle protocols - defp warn_if_undefined?(_module, :__impl__, 1, _state), do: false - defp warn_if_undefined?(:erlang, :orelse, 2, _state), do: false - defp warn_if_undefined?(:erlang, :andalso, 2, _state), do: false + defp warn_undefined?(_module, :__impl__, 1, _state), do: false + defp warn_undefined?(:erlang, :orelse, 2, _state), do: false + defp warn_undefined?(:erlang, :andalso, 2, _state), do: false + + defp warn_undefined?(module, fun, arity, state) do + warn_compile_opts?(module, fun, arity, state.compile_opts, :no_warn_undefined) + end + + defp warn_deprecated?(module, fun, arity, state) do + warn_compile_opts?(module, fun, arity, state.compile_opts, :no_warn_deprecated) + end - defp warn_if_undefined?(module, fun, arity, state) do + defp warn_compile_opts?(module, fun, arity, compile_opts, option) do for( - {:no_warn_undefined, values} <- state.compile_opts, + {^option, values} <- compile_opts, value <- List.wrap(values), value == module or value == {module, fun, arity}, do: :skip ) == [] end + defp deprecated(module, fun, arity) do + if function_exported?(module, :__info__, 1) do + case List.keyfind(module.__info__(:deprecated), {fun, arity}, 0) do + {_key, reason} -> reason + nil -> nil + end + else + nil + end + end + defp exports_for(module) do try do module.__info__(:macros) ++ module.__info__(:functions) @@ -156,6 +175,14 @@ defmodule Module.Checker do ] end + defp format_warning({:deprecated, module, function, arity, reason}) do + [ + Exception.format_mfa(module, function, arity), + " is deprecated. ", + reason + ] + end + defp format_locations([location]) do format_location(location) end diff --git a/lib/elixir/src/elixir.erl b/lib/elixir/src/elixir.erl index 99bdb3445..b7237314a 100644 --- a/lib/elixir/src/elixir.erl +++ b/lib/elixir/src/elixir.erl @@ -57,7 +57,8 @@ start(_Type, _Args) -> debug_info => true, warnings_as_errors => false, relative_paths => true, - no_warn_undefined => [] + no_warn_undefined => [], + no_warn_deprecated => [] }, {ok, [[Home] | _]} = init:get_argument(home), diff --git a/lib/elixir/test/elixir/module/checker_test.exs b/lib/elixir/test/elixir/module/checker_test.exs index 6a795444b..4bc2e76a4 100644 --- a/lib/elixir/test/elixir/module/checker_test.exs +++ b/lib/elixir/test/elixir/module/checker_test.exs @@ -11,409 +11,498 @@ defmodule Module.CheckerTest do on_exit(fn -> Application.put_env(:elixir, :ansi_enabled, previous) end) end - test "handles Erlang modules" do - files = %{ - "a.ex" => """ - defmodule A do - def a, do: :not_a_module.no_module - def b, do: :lists.no_func - end - """ - } + describe "undefined" do + test "handles Erlang modules" do + files = %{ + "a.ex" => """ + defmodule A do + def a, do: :not_a_module.no_module + def b, do: :lists.no_func + end + """ + } - warning = """ - warning: :not_a_module.no_module/0 is undefined (module :not_a_module is not available or is yet to be defined) - a.ex:2: A.a/0 + warning = """ + warning: :not_a_module.no_module/0 is undefined (module :not_a_module is not available or is yet to be defined) + a.ex:2: A.a/0 - warning: :lists.no_func/0 is undefined or private - a.ex:3: A.b/0 + warning: :lists.no_func/0 is undefined or private + a.ex:3: A.b/0 - """ + """ - assert_warnings(files, warning) - end + assert_warnings(files, warning) + end - test "handles module body conditionals" do - files = %{ - "a.ex" => """ - defmodule A do - if function_exported?(List, :flatten, 1) do - List.flatten([1, 2, 3]) - else - List.old_flatten([1, 2, 3]) + test "handles module body conditionals" do + files = %{ + "a.ex" => """ + defmodule A do + if function_exported?(List, :flatten, 1) do + List.flatten([1, 2, 3]) + else + List.old_flatten([1, 2, 3]) + end + + if function_exported?(List, :flatten, 1) do + def flatten(arg), do: List.flatten(arg) + else + def flatten(arg), do: List.old_flatten(arg) + end + + if function_exported?(List, :flatten, 1) do + def flatten2(arg), do: List.old_flatten(arg) + else + def flatten2(arg), do: List.flatten(arg) + end end + """ + } - if function_exported?(List, :flatten, 1) do - def flatten(arg), do: List.flatten(arg) - else - def flatten(arg), do: List.old_flatten(arg) - end + warning = """ + warning: List.old_flatten/1 is undefined or private. Did you mean one of: + + * flatten/1 + * flatten/2 + + a.ex:15: A.flatten2/1 - if function_exported?(List, :flatten, 1) do - def flatten2(arg), do: List.old_flatten(arg) - else - def flatten2(arg), do: List.flatten(arg) - end - end """ - } - warning = """ - warning: List.old_flatten/1 is undefined or private. Did you mean one of: + assert_warnings(files, warning) + end - * flatten/1 - * flatten/2 + test "aliases" do + files = %{ + "a.ex" => """ + defmodule A do + alias Enum, as: E - a.ex:15: A.flatten2/1 + def a(a, b), do: E.map2(a, b) + def b, do: &E.map2/2 - """ + @file "external_source.ex" + def c do + alias Enum, as: EE + &EE.map2/2 + end + end + """ + } - assert_warnings(files, warning) - end + warning = """ + warning: Enum.map2/2 is undefined or private. Did you mean one of: - test "aliases" do - files = %{ - "a.ex" => """ - defmodule A do - alias Enum, as: E + * map/2 - def a(a, b), do: E.map2(a, b) - def b, do: &E.map2/2 + Found at 3 locations: + a.ex:4: A.a/2 + a.ex:5: A.b/0 + external_source.ex:10: A.c/0 - @file "external_source.ex" - def c do - alias Enum, as: EE - &EE.map2/2 - end - end """ - } - warning = """ - warning: Enum.map2/2 is undefined or private. Did you mean one of: - - * map/2 + assert_warnings(files, warning) + end - Found at 3 locations: - a.ex:4: A.a/2 - a.ex:5: A.b/0 - external_source.ex:10: A.c/0 + test "reports missing functions" do + files = %{ + "a.ex" => """ + defmodule A do + def a, do: A.no_func + def b, do: A.a() - """ + @file "external_source.ex" + def c, do: &A.no_func/1 + end + """ + } - assert_warnings(files, warning) - end + warning = """ + warning: A.no_func/0 is undefined or private + a.ex:2: A.a/0 - test "reports missing functions" do - files = %{ - "a.ex" => """ - defmodule A do - def a, do: A.no_func - def b, do: A.a() + warning: A.no_func/1 is undefined or private + external_source.ex:6: A.c/0 - @file "external_source.ex" - def c, do: &A.no_func/1 - end """ - } - warning = """ - warning: A.no_func/0 is undefined or private - a.ex:2: A.a/0 + assert_warnings(files, warning) + end - warning: A.no_func/1 is undefined or private - external_source.ex:6: A.c/0 + test " reports missing functions respecting arity" do + files = %{ + "a.ex" => """ + defmodule A do + def a, do: :ok + def b, do: A.a(1) - """ + @file "external_source.ex" + def c, do: A.b(1) + end + """ + } - assert_warnings(files, warning) - end + warning = """ + warning: A.a/1 is undefined or private. Did you mean one of: - test " reports missing functions respecting arity" do - files = %{ - "a.ex" => """ - defmodule A do - def a, do: :ok - def b, do: A.a(1) + * a/0 - @file "external_source.ex" - def c, do: A.b(1) - end - """ - } + a.ex:3: A.b/0 - warning = """ - warning: A.a/1 is undefined or private. Did you mean one of: + warning: A.b/1 is undefined or private. Did you mean one of: - * a/0 + * b/0 - a.ex:3: A.b/0 + external_source.ex:6: A.c/0 - warning: A.b/1 is undefined or private. Did you mean one of: + """ - * b/0 + assert_warnings(files, warning) + end - external_source.ex:6: A.c/0 + test "reports missing modules" do + files = %{ + "a.ex" => """ + defmodule A do + def a, do: D.no_module - """ + @file "external_source.ex" + def c, do: E.no_module + end + """ + } - assert_warnings(files, warning) - end + warning = """ + warning: D.no_module/0 is undefined (module D is not available or is yet to be defined) + a.ex:2: A.a/0 - test "reports missing modules" do - files = %{ - "a.ex" => """ - defmodule A do - def a, do: D.no_module + warning: E.no_module/0 is undefined (module E is not available or is yet to be defined) + external_source.ex:5: A.c/0 - @file "external_source.ex" - def c, do: E.no_module - end """ - } - warning = """ - warning: D.no_module/0 is undefined (module D is not available or is yet to be defined) - a.ex:2: A.a/0 + assert_warnings(files, warning) + end - warning: E.no_module/0 is undefined (module E is not available or is yet to be defined) - external_source.ex:5: A.c/0 + test "reports missing captures" do + files = %{ + "a.ex" => """ + defmodule A do + def a, do: &A.no_func/0 - """ + @file "external_source.ex" + def c, do: &A.no_func/1 + end + """ + } - assert_warnings(files, warning) - end + warning = """ + warning: A.no_func/0 is undefined or private + a.ex:2: A.a/0 - test "reports missing captures" do - files = %{ - "a.ex" => """ - defmodule A do - def a, do: &A.no_func/0 + warning: A.no_func/1 is undefined or private + external_source.ex:5: A.c/0 - @file "external_source.ex" - def c, do: &A.no_func/1 - end """ - } - warning = """ - warning: A.no_func/0 is undefined or private - a.ex:2: A.a/0 + assert_warnings(files, warning) + end - warning: A.no_func/1 is undefined or private - external_source.ex:5: A.c/0 + test "doesn't report missing funcs at compile time" do + files = %{ + "a.ex" => """ + Enum.map([], fn _ -> BadReferencer.no_func4() end) - """ + if function_exported?(List, :flatten, 1) do + List.flatten([1, 2, 3]) + else + List.old_flatten([1, 2, 3]) + end + """ + } - assert_warnings(files, warning) - end - - test "doesn't report missing funcs at compile time" do - files = %{ - "a.ex" => """ - Enum.map([], fn _ -> BadReferencer.no_func4() end) + assert_no_warnings(files) + end - if function_exported?(List, :flatten, 1) do - List.flatten([1, 2, 3]) - else - List.old_flatten([1, 2, 3]) + test "handles multiple modules in one file" do + files = %{ + "a.ex" => """ + defmodule A do + def a, do: B.no_func + def b, do: B.a end - """ - } + """, + "b.ex" => """ + defmodule B do + def a, do: A.no_func + def b, do: A.b + end + """ + } - assert_no_warnings(files) - end + warning = """ + warning: B.no_func/0 is undefined or private + a.ex:2: A.a/0 + + warning: A.no_func/0 is undefined or private + b.ex:2: B.a/0 - test "handles multiple modules in one file" do - files = %{ - "a.ex" => """ - defmodule A do - def a, do: B.no_func - def b, do: B.a - end - """, - "b.ex" => """ - defmodule B do - def a, do: A.no_func - def b, do: A.b - end """ - } - warning = """ - warning: B.no_func/0 is undefined or private - a.ex:2: A.a/0 + assert_warnings(files, warning) + end - warning: A.no_func/0 is undefined or private - b.ex:2: B.a/0 + test "groups multiple warnings in one file" do + files = %{ + "a.ex" => """ + defmodule A do + def a, do: A.no_func - """ + @file "external_source.ex" + def b, do: A2.no_func - assert_warnings(files, warning) - end + def c, do: A.no_func + def d, do: A2.no_func + end + """ + } - test "groups multiple warnings in one file" do - files = %{ - "a.ex" => """ - defmodule A do - def a, do: A.no_func + warning = """ + warning: A2.no_func/0 is undefined (module A2 is not available or is yet to be defined) + Found at 2 locations: + a.ex:8: A.d/0 + external_source.ex:5: A.b/0 - @file "external_source.ex" - def b, do: A2.no_func + warning: A.no_func/0 is undefined or private + Found at 2 locations: + a.ex:2: A.a/0 + a.ex:7: A.c/0 - def c, do: A.no_func - def d, do: A2.no_func - end """ - } - warning = """ - warning: A2.no_func/0 is undefined (module A2 is not available or is yet to be defined) - Found at 2 locations: - a.ex:8: A.d/0 - external_source.ex:5: A.b/0 + assert_warnings(files, warning) + end - warning: A.no_func/0 is undefined or private - Found at 2 locations: - a.ex:2: A.a/0 - a.ex:7: A.c/0 + test "protocols are checked, ignoring missing built-in impls" do + files = %{ + "a.ex" => """ + defprotocol AProtocol do + def func(arg) + end - """ + defmodule AImplementation do + defimpl AProtocol do + def func(_), do: B.no_func + end + end + """ + } - assert_warnings(files, warning) - end + warning = """ + warning: B.no_func/0 is undefined (module B is not available or is yet to be defined) + a.ex:7: AProtocol.AImplementation.func/1 + + """ - test "protocols are checked, ignoring missing built-in impls" do - files = %{ - "a.ex" => """ - defprotocol AProtocol do - def func(arg) - end + assert_warnings(files, warning) + end - defmodule AImplementation do - defimpl AProtocol do - def func(_), do: B.no_func + test "handles Erlang ops" do + files = %{ + "a.ex" => """ + defmodule A do + def a(a, b), do: a and b + def b(a, b), do: a or b end - end - """ - } + """ + } - warning = """ - warning: B.no_func/0 is undefined (module B is not available or is yet to be defined) - a.ex:7: AProtocol.AImplementation.func/1 + assert_no_warnings(files) + end - """ + test "hints exclude deprecated functions" do + files = %{ + "a.ex" => """ + defmodule A do + def to_charlist(a), do: a - assert_warnings(files, warning) - end + @deprecated "Use String.to_charlist/1 instead" + def to_char_list(a), do: a - test "handles Erlang ops" do - files = %{ - "a.ex" => """ - defmodule A do - def a(a, b), do: a and b - def b(a, b), do: a or b - end - """ - } + def c(a), do: A.to_list(a) + end + """ + } - assert_no_warnings(files) - end + warning = """ + warning: A.to_list/1 is undefined or private. Did you mean one of: - test "hints exclude deprecated functions" do - files = %{ - "a.ex" => """ - defmodule A do - def to_charlist(a), do: a + * to_charlist/1 - @deprecated "Use String.to_charlist/1 instead" - def to_char_list(a), do: a + a.ex:7: A.c/1 - def c(a), do: A.to_list(a) - end """ - } - warning = """ - warning: A.to_list/1 is undefined or private. Did you mean one of: + assert_warnings(files, warning) + end - * to_charlist/1 + test "imports" do + files = %{ + "a.ex" => """ + defmodule A do + import Record - a.ex:7: A.c/1 + def a(a, b), do: extract(a, b) + def b(arg), do: is_record(arg) + end + """ + } - """ + assert_no_warnings(files) + end - assert_warnings(files, warning) - end + test "requires" do + files = %{ + "a.ex" => """ + defmodule A do + require Integer - test "imports" do - files = %{ - "a.ex" => """ - defmodule A do - import Record + def a(a), do: Integer.is_even(a) + end + """ + } - def a(a, b), do: extract(a, b) - def b(arg), do: is_record(arg) - end - """ - } + assert_no_warnings(files) + end - assert_no_warnings(files) - end + test "excludes no_warn_undefined" do + files = %{ + "a.ex" => """ + defmodule A do + @compile {:no_warn_undefined, [MissingModule, {MissingModule2, :func, 2}]} + @compile {:no_warn_undefined, {B, :func, 2}} + + def a, do: MissingModule.func(1) + def b, do: MissingModule2.func(1, 2) + def c, do: MissingModule2.func(1) + def d, do: MissingModule3.func(1, 2) + def e, do: B.func(1) + def f, do: B.func(1, 2) + def g, do: B.func(1, 2, 3) + end + """, + "b.ex" => """ + defmodule B do + def func(_), do: :ok + end + """ + } + + warning = """ + warning: MissingModule2.func/1 is undefined (module MissingModule2 is not available or is yet to be defined) + a.ex:7: A.c/0 + + warning: MissingModule3.func/2 is undefined (module MissingModule3 is not available or is yet to be defined) + a.ex:8: A.d/0 - test "requires" do - files = %{ - "a.ex" => """ - defmodule A do - require Integer + warning: B.func/3 is undefined or private. Did you mean one of: + + * func/1 + + a.ex:11: A.g/0 - def a(a), do: Integer.is_even(a) - end """ - } - assert_no_warnings(files) + assert_warnings(files, warning) + end end - test "excludes no_warn_undefined" do - files = %{ - "a.ex" => """ - defmodule A do - @compile {:no_warn_undefined, [MissingModule, {MissingModule2, :func, 2}]} - @compile {:no_warn_undefined, {B, :func, 2}} - - def a, do: MissingModule.func(1) - def b, do: MissingModule2.func(1, 2) - def c, do: MissingModule2.func(1) - def d, do: MissingModule3.func(1, 2) - def e, do: B.func(1) - def f, do: B.func(1, 2) - def g, do: B.func(1, 2, 3) - end - """, - "b.ex" => """ - defmodule B do - def func(_), do: :ok - end - """ - } + describe "deprecated" do + test "reports deprecated functions" do + files = %{ + "a.ex" => """ + defmodule A do + @deprecated "oops" + def a, do: A.a + end + """ + } - warning = """ - warning: MissingModule2.func/1 is undefined (module MissingModule2 is not available or is yet to be defined) - a.ex:7: A.c/0 + warning = """ + warning: A.a/0 is deprecated. oops + a.ex:3: A.a/0 - warning: MissingModule3.func/2 is undefined (module MissingModule3 is not available or is yet to be defined) - a.ex:8: A.d/0 + """ - warning: B.func/3 is undefined or private. Did you mean one of: + assert_warnings(files, warning) + end - * func/1 + # test "reports deprecated structs" do + # files = %{ + # "a.ex" => """ + # defmodule A do + # @deprecated "oops" + # defstruct [:x, :y] + # def match(%A{}), do: :ok + # def build(:ok), do: %A{} + # end + # """, + # "b.ex" => """ + # defmodule B do + # def match(%A{}), do: :ok + # def build(:ok), do: %A{} + # end + # """ + # } + # + # warning = """ + # """ + # + # assert_warnings(files, warning) + # end + + test "excludes specified modules and MFAs" do + files = %{ + "a.ex" => """ + defmodule A do + @compile {:no_warn_deprecated, [DeprecatedModule, {DeprecatedModule2, :func, 2}]} + + def a, do: DeprecatedModule.func(1) + def b, do: DeprecatedModule2.func(1, 2) + def c, do: DeprecatedModule2.func(1) + def d, do: DeprecatedModule3.func(1, 2) + end + """, + "deprecated_modules.ex" => """ + defmodule DeprecatedModule do + @deprecated "message" + def func(_), do: :ok + end + defmodule DeprecatedModule2 do + @deprecated "message" + def func(_), do: :ok + @deprecated "message" + def func(_, _), do: :ok + end + defmodule DeprecatedModule3 do + @deprecated "message" + def func(_, _), do: :ok + end + """ + } + + warning = """ + warning: DeprecatedModule2.func/1 is deprecated. message + a.ex:6: A.c/0 - a.ex:11: A.g/0 + warning: DeprecatedModule3.func/2 is deprecated. message + a.ex:7: A.d/0 - """ + """ - assert_warnings(files, warning) + assert_warnings(files, warning) + end end defp assert_warnings(files, expected) do diff --git a/lib/mix/lib/mix/tasks/compile.elixir.ex b/lib/mix/lib/mix/tasks/compile.elixir.ex index 08dc46b6c..56518c49d 100644 --- a/lib/mix/lib/mix/tasks/compile.elixir.ex +++ b/lib/mix/lib/mix/tasks/compile.elixir.ex @@ -90,7 +90,9 @@ defmodule Mix.Tasks.Compile.Elixir do if exclude == [] do opts else - Keyword.update(opts, :no_warn_undefined, exclude, &(List.wrap(&1) ++ exclude)) + opts + |> Keyword.update(:no_warn_undefined, exclude, &(List.wrap(&1) ++ exclude)) + |> Keyword.update(:no_warn_deprecated, exclude, &(List.wrap(&1) ++ exclude)) end end end |