diff options
author | Eric Meadows-Jönsson <eric.meadows.jonsson@gmail.com> | 2020-09-12 18:19:19 +0200 |
---|---|---|
committer | Eric Meadows-Jönsson <eric.meadows.jonsson@gmail.com> | 2020-09-12 18:23:38 +0200 |
commit | 023db086f953e942be0c15c32fd26d5b773caa10 (patch) | |
tree | 9f625efb43d2d462698adf91e431803f2d2e2939 | |
parent | 556bd254db79ae0c3287a0d6773dce3e96882691 (diff) | |
download | elixir-emj/move-type-warnings.tar.gz |
Move type warningsemj/move-type-warnings
No longer needs to compile modules to test type warnings. Reduces type tests execution time from 1.0s to 0.6s.
-rw-r--r-- | lib/elixir/lib/module/types.ex | 2 | ||||
-rw-r--r-- | lib/elixir/test/elixir/module/types/integration_test.exs | 539 | ||||
-rw-r--r-- | lib/elixir/test/elixir/module/types/type_helper.exs | 6 | ||||
-rw-r--r-- | lib/elixir/test/elixir/module/types/types_test.exs | 437 |
4 files changed, 441 insertions, 543 deletions
diff --git a/lib/elixir/lib/module/types.ex b/lib/elixir/lib/module/types.ex index efbd0502f..1094d5987 100644 --- a/lib/elixir/lib/module/types.ex +++ b/lib/elixir/lib/module/types.ex @@ -193,7 +193,7 @@ defmodule Module.Types do ## ERROR TO WARNING # Collect relevant information from context and traces to report error - defp error_to_warning(:unable_unify, {left, right, stack}, context) do + def error_to_warning(:unable_unify, {left, right, stack}, context) do {fun, arity} = context.function line = get_meta(stack.last_expr)[:line] location = {context.file, line, {context.module, fun, arity}} diff --git a/lib/elixir/test/elixir/module/types/integration_test.exs b/lib/elixir/test/elixir/module/types/integration_test.exs index fc4ac5569..31cb87987 100644 --- a/lib/elixir/test/elixir/module/types/integration_test.exs +++ b/lib/elixir/test/elixir/module/types/integration_test.exs @@ -727,545 +727,6 @@ defmodule Module.Types.IntegrationTest do end end - describe "function header inference" do - test "warns on literals" do - files = %{ - "a.ex" => """ - defmodule A do - def a(var = 123, var = "abc"), do: var - end - """ - } - - warning = """ - warning: incompatible types: - - integer() !~ binary() - - in expression: - - # a.ex:2 - var = "abc" - - where "var" was given the type integer() in: - - # a.ex:2 - var = 123 - - where "var" was given the type binary() in: - - # a.ex:2 - var = "abc" - - Conflict found at - a.ex:2: A.a/2 - - """ - - assert_warnings(files, warning) - end - - test "warns on binary patterns" do - files = %{ - "a.ex" => """ - defmodule A do - def a(<<var::integer, var::binary>>), do: var - end - """ - } - - warning = """ - warning: incompatible types: - - integer() !~ binary() - - in expression: - - # a.ex:2 - <<..., var::binary()>> - - where "var" was given the type integer() in: - - # a.ex:2 - <<var::integer(), ...>> - - where "var" was given the type binary() in: - - # a.ex:2 - <<..., var::binary()>> - - Conflict found at - a.ex:2: A.a/1 - - """ - - assert_warnings(files, warning) - end - - test "warns on recursive patterns" do - files = %{ - "a.ex" => """ - defmodule A do - def a({var} = var), do: var - end - """ - } - - warning = """ - warning: incompatible types: - - {var0} !~ var0 - - in expression: - - # a.ex:2 - {var} = var - - where "var" was given the type {var0} in: - - # a.ex:2 - {var} = var - - Conflict found at - a.ex:2: A.a/1 - - """ - - assert_warnings(files, warning) - end - - test "warns on guards" do - files = %{ - "a.ex" => """ - defmodule A do - def a(var) when is_integer(var) and is_binary(var), do: var - end - """ - } - - warning = """ - warning: incompatible types: - - integer() !~ binary() - - in expression: - - # a.ex:2 - is_integer(var) and is_binary(var) - - where "var" was given the type integer() in: - - # a.ex:2 - is_integer(var) - - where "var" was given the type binary() in: - - # a.ex:2 - is_binary(var) - - Conflict found at - a.ex:2: A.a/1 - - """ - - assert_warnings(files, warning) - end - - test "warns on guards with multiple variables" do - files = %{ - "a.ex" => """ - defmodule A do - def a(x = y) when is_integer(x) and is_binary(y), do: {x, y} - end - """ - } - - warning = """ - warning: incompatible types: - - integer() !~ binary() - - in expression: - - # a.ex:2 - is_integer(x) and is_binary(y) - - where "y" was given the same type as "x" in: - - # a.ex:2 - x = y - - where "y" was given the type binary() in: - - # a.ex:2 - is_binary(y) - - where "x" was given the type integer() in: - - # a.ex:2 - is_integer(x) - - Conflict found at - a.ex:2: A.a/1 - - """ - - assert_warnings(files, warning) - end - - test "only show relevant traces in warning" do - files = %{ - "a.ex" => """ - defmodule A do - def a(x = y, z) when is_integer(x) and is_binary(y) and is_boolean(z), do: {x, y, z} - end - """ - } - - warning = """ - warning: incompatible types: - - integer() !~ binary() - - in expression: - - # a.ex:2 - is_integer(x) and is_binary(y) and is_boolean(z) - - where "y" was given the same type as "x" in: - - # a.ex:2 - x = y - - where "y" was given the type binary() in: - - # a.ex:2 - is_binary(y) - - where "x" was given the type integer() in: - - # a.ex:2 - is_integer(x) - - Conflict found at - a.ex:2: A.a/2 - - """ - - assert_warnings(files, warning) - end - - test "check body" do - files = %{ - "a.ex" => """ - defmodule A do - def a(x) when is_integer(x), do: :foo = x - end - """ - } - - warning = """ - warning: incompatible types: - - integer() !~ :foo - - in expression: - - # a.ex:2 - :foo = x - - where "x" was given the type integer() in: - - # a.ex:2 - is_integer(x) - - where "x" was given the type :foo in: - - # a.ex:2 - :foo = x - - Conflict found at - a.ex:2: A.a/1 - - """ - - assert_warnings(files, warning) - end - - test "check binary" do - files = %{ - "a.ex" => """ - defmodule A do - def a(foo) when is_binary(foo), do: <<foo>> - end - """ - } - - warning = """ - warning: incompatible types: - - binary() !~ integer() - - in expression: - - # a.ex:2 - <<foo>> - - where "foo" was given the type binary() in: - - # a.ex:2 - is_binary(foo) - - where "foo" was given the type integer() in: - - # a.ex:2 - <<foo>> - - HINT: all expressions given to binaries are assumed to be of type \ - integer() unless said otherwise. For example, <<expr>> assumes "expr" \ - is an integer. Pass a modifier, such as <<expr::float>> or <<expr::binary>>, \ - to change the default behaviour. - - Conflict found at - a.ex:2: A.a/1 - - """ - - assert_warnings(files, warning) - - files = %{ - "a.ex" => """ - defmodule A do - def a(foo) when is_binary(foo), do: <<foo::integer>> - end - """ - } - - warning = """ - warning: incompatible types: - - binary() !~ integer() - - in expression: - - # a.ex:2 - <<foo::integer()>> - - where "foo" was given the type binary() in: - - # a.ex:2 - is_binary(foo) - - where "foo" was given the type integer() in: - - # a.ex:2 - <<foo::integer()>> - - Conflict found at - a.ex:2: A.a/1 - - """ - - assert_warnings(files, warning) - end - end - - describe "map checks" do - test "show map() when comparing against non-map" do - files = %{ - "a.ex" => """ - defmodule A do - def a(foo) do - foo.bar - :atom = foo - end - end - """ - } - - warning = """ - warning: incompatible types: - - map() !~ :atom - - in expression: - - # a.ex:4 - :atom = foo - - where "foo" was given the type map() (due to calling var.field) in: - - # a.ex:3 - foo.bar - - where "foo" was given the type :atom in: - - # a.ex:4 - :atom = foo - - HINT: "var.field" (without parentheses) implies "var" is a map() while \ - "var.fun()" (with parentheses) implies "var" is an atom() - - Conflict found at - a.ex:4: A.a/1 - - """ - - assert_warnings(files, warning) - end - - test "use module as map (without parentheses)" do - files = %{ - "a.ex" => """ - defmodule A do - def a(foo) do - %module{} = foo - module.__struct__ - end - end - """ - } - - warning = """ - warning: incompatible types: - - map() !~ atom() - - in expression: - - # a.ex:4 - module.__struct__ - - where "module" was given the type atom() in: - - # a.ex:3 - %module{} - - where "module" was given the type map() (due to calling var.field) in: - - # a.ex:4 - module.__struct__ - - HINT: "var.field" (without parentheses) implies "var" is a map() while \ - "var.fun()" (with parentheses) implies "var" is an atom() - - Conflict found at - a.ex:4: A.a/1 - - """ - - assert_warnings(files, warning) - end - - test "use map as module (with parentheses)" do - files = %{ - "a.ex" => """ - defmodule A do - def a(foo) when is_map(foo) do - foo.__struct__() - end - end - """ - } - - warning = """ - warning: incompatible types: - - map() !~ atom() - - in expression: - - # a.ex:3 - foo.__struct__() - - where "foo" was given the type map() in: - - # a.ex:2 - is_map(foo) - - where "foo" was given the type atom() (due to calling var.fun()) in: - - # a.ex:3 - foo.__struct__() - - HINT: "var.field" (without parentheses) implies "var" is a map() while \ - "var.fun()" (with parentheses) implies "var" is an atom() - - Conflict found at - a.ex:3: A.a/1 - - """ - - assert_warnings(files, warning) - end - - test "non-existant map field warning" do - files = %{ - "a.ex" => """ - defmodule A do - def a() do - map = %{foo: 1} - map.bar - end - end - """ - } - - warning = """ - warning: undefined field "bar" in expression: - - # a.ex:4 - map.bar - - expected one of the following fields: foo - - where "map" was given the type map() in: - - # a.ex:3 - map = %{foo: 1} - - Conflict found at - a.ex:4: A.a/0 - - """ - - assert_warnings(files, warning) - end - - test "non-existant struct field warning" do - files = %{ - "a.ex" => """ - defmodule A do - def a(foo) do - %URI{} = foo - foo.bar - end - end - """ - } - - warning = """ - warning: undefined field "bar" in expression: - - # a.ex:4 - foo.bar - - expected one of the following fields: __struct__, authority, fragment, host, path, port, query, scheme, userinfo - - where "foo" was given the type %URI{} in: - - # a.ex:3 - %URI{} = foo - - Conflict found at - a.ex:4: A.a/1 - - """ - - assert_warnings(files, warning) - end - end - defp assert_warnings(files, expected) when is_binary(expected) do assert capture_compile_warnings(files) == expected end diff --git a/lib/elixir/test/elixir/module/types/type_helper.exs b/lib/elixir/test/elixir/module/types/type_helper.exs index 54bcb9704..26ae45b4c 100644 --- a/lib/elixir/test/elixir/module/types/type_helper.exs +++ b/lib/elixir/test/elixir/module/types/type_helper.exs @@ -23,7 +23,7 @@ defmodule TypeHelper do end end - defp expand_expr(patterns, guards, expr, env) do + def expand_expr(patterns, guards, expr, env) do fun = quote do fn unquote(patterns) when unquote(guards) -> unquote(expr) end @@ -34,11 +34,11 @@ defmodule TypeHelper do {patterns, guards, body} end - defp new_context() do + def new_context() do Types.context("types_test.ex", TypesTest, {:test, 0}, [], Module.ParallelChecker.test_cache()) end - defp new_stack() do + def new_stack() do %{ Types.stack() | last_expr: {:foo, [], nil} diff --git a/lib/elixir/test/elixir/module/types/types_test.exs b/lib/elixir/test/elixir/module/types/types_test.exs index 8e940bd24..e79482bea 100644 --- a/lib/elixir/test/elixir/module/types/types_test.exs +++ b/lib/elixir/test/elixir/module/types/types_test.exs @@ -3,6 +3,59 @@ Code.require_file("../../test_helper.exs", __DIR__) defmodule Module.Types.TypesTest do use ExUnit.Case, async: true alias Module.Types + alias Module.Types.{Pattern, Expr} + + defmacro warning(patterns \\ [], guards \\ [], body) do + min_line = min_line(patterns ++ guards ++ [body]) + patterns = reset_line(patterns, min_line) + guards = reset_line(guards, min_line) + body = reset_line(body, min_line) + expr = TypeHelper.expand_expr(patterns, guards, body, __CALLER__) + + quote do + unquote(Macro.escape(expr)) + |> Module.Types.TypesTest.__expr__() + |> to_warning() + end + end + + def __expr__({patterns, guards, body}) do + with {:ok, _types, context} <- + Pattern.of_head(patterns, guards, TypeHelper.new_stack(), TypeHelper.new_context()), + {:ok, type, context} <- Expr.of_expr(body, TypeHelper.new_stack(), context) do + flunk("expexted error, got: #{inspect(Types.lift_type(type, context))}") + else + {:error, {type, reason, context}} -> + {:error, {type, reason, context}} + end + end + + defp reset_line(ast, min_line) do + Macro.prewalk(ast, fn ast -> + Macro.update_meta(ast, fn meta -> + Keyword.update!(meta, :line, &(&1 - min_line + 1)) + end) + end) + end + + defp min_line(ast) do + {_ast, min} = + Macro.prewalk(ast, :infinity, fn + {_fun, meta, _args} = ast, min -> {ast, min(min, Keyword.get(meta, :line, 1))} + other, min -> {other, min} + end) + + min + end + + defp to_warning({:error, {type, reason, context}}) do + {Module.Types, error, _location} = Module.Types.error_to_warning(type, reason, context) + + error + |> Module.Types.format_warning() + |> List.to_string() + |> String.trim_trailing("\nConflict found at") + end test "format_type/1" do assert Types.format_type(:binary, false) == "binary()" @@ -52,4 +105,388 @@ defmodule Module.Types.TypesTest do assert Types.expr_to_string(quote(do: :erlang.element(1, a))) == "elem(a, 0)" assert Types.expr_to_string(quote(do: :erlang.element(:erlang.+(a, 1), b))) == "elem(b, a)" end + + describe "function head warnings" do + test "warns on literals" do + string = warning([var = 123, var = "abc"], var) + + assert string == """ + incompatible types: + + integer() !~ binary() + + in expression: + + # types_test.ex:1 + var = "abc" + + where "var" was given the type integer() in: + + # types_test.ex:1 + var = 123 + + where "var" was given the type binary() in: + + # types_test.ex:1 + var = "abc" + """ + end + + test "warns on binary patterns" do + string = warning([<<var::integer, var::binary>>], var) + + assert string == """ + incompatible types: + + integer() !~ binary() + + in expression: + + # types_test.ex:1 + <<..., var::binary()>> + + where "var" was given the type integer() in: + + # types_test.ex:1 + <<var::integer(), ...>> + + where "var" was given the type binary() in: + + # types_test.ex:1 + <<..., var::binary()>> + """ + end + + test "warns on recursive patterns" do + string = warning([{var} = var], var) + + assert string == """ + incompatible types: + + {var0} !~ var0 + + in expression: + + # types_test.ex:1 + {var} = var + + where "var" was given the type {var0} in: + + # types_test.ex:1 + {var} = var + """ + end + + test "warns on guards" do + string = warning([var], [is_integer(var) and is_binary(var)], var) + + assert string == """ + incompatible types: + + integer() !~ binary() + + in expression: + + # types_test.ex:1 + is_integer(var) and is_binary(var) + + where "var" was given the type integer() in: + + # types_test.ex:1 + is_integer(var) + + where "var" was given the type binary() in: + + # types_test.ex:1 + is_binary(var) + """ + end + + test "warns on guards with multiple variables" do + string = warning([x = y], [is_integer(x) and is_binary(y)], {x, y}) + + assert string == """ + incompatible types: + + integer() !~ binary() + + in expression: + + # types_test.ex:1 + is_integer(x) and is_binary(y) + + where "y" was given the same type as "x" in: + + # types_test.ex:1 + x = y + + where "y" was given the type binary() in: + + # types_test.ex:1 + is_binary(y) + + where "x" was given the type integer() in: + + # types_test.ex:1 + is_integer(x) + """ + end + + test "only show relevant traces in warning" do + string = warning([x = y, z], [is_integer(x) and is_binary(y) and is_boolean(z)], {x, y, z}) + + assert string == """ + incompatible types: + + integer() !~ binary() + + in expression: + + # types_test.ex:1 + is_integer(x) and is_binary(y) and is_boolean(z) + + where "y" was given the same type as "x" in: + + # types_test.ex:1 + x = y + + where "y" was given the type binary() in: + + # types_test.ex:1 + is_binary(y) + + where "x" was given the type integer() in: + + # types_test.ex:1 + is_integer(x) + """ + end + + test "check body" do + string = warning([x], [is_integer(x)], :foo = x) + + assert string == """ + incompatible types: + + integer() !~ :foo + + in expression: + + # types_test.ex:1 + :foo = x + + where "x" was given the type integer() in: + + # types_test.ex:1 + is_integer(x) + + where "x" was given the type :foo in: + + # types_test.ex:1 + :foo = x + """ + end + + test "check binary" do + string = warning([foo], [is_binary(foo)], <<foo>>) + + assert string == """ + incompatible types: + + binary() !~ integer() + + in expression: + + # types_test.ex:1 + <<foo>> + + where "foo" was given the type binary() in: + + # types_test.ex:1 + is_binary(foo) + + where "foo" was given the type integer() in: + + # types_test.ex:1 + <<foo>> + + HINT: all expressions given to binaries are assumed to be of type \ + integer() unless said otherwise. For example, <<expr>> assumes "expr" \ + is an integer. Pass a modifier, such as <<expr::float>> or <<expr::binary>>, \ + to change the default behaviour. + """ + + string = warning([foo], [is_binary(foo)], <<foo::integer>>) + + assert string == """ + incompatible types: + + binary() !~ integer() + + in expression: + + # types_test.ex:1 + <<foo::integer()>> + + where "foo" was given the type binary() in: + + # types_test.ex:1 + is_binary(foo) + + where "foo" was given the type integer() in: + + # types_test.ex:1 + <<foo::integer()>> + """ + end + end + + describe "map warnings" do + test "show map() when comparing against non-map" do + string = + warning( + [foo], + ( + foo.bar + :atom = foo + ) + ) + + assert string == """ + incompatible types: + + map() !~ :atom + + in expression: + + # types_test.ex:4 + :atom = foo + + where "foo" was given the type map() (due to calling var.field) in: + + # types_test.ex:3 + foo.bar + + where "foo" was given the type :atom in: + + # types_test.ex:4 + :atom = foo + + HINT: "var.field" (without parentheses) implies "var" is a map() while \ + "var.fun()" (with parentheses) implies "var" is an atom() + """ + end + + test "use module as map (without parentheses)" do + string = + warning( + [foo], + ( + %module{} = foo + module.__struct__ + ) + ) + + assert string == """ + incompatible types: + + map() !~ atom() + + in expression: + + # types_test.ex:4 + module.__struct__ + + where "module" was given the type atom() in: + + # types_test.ex:3 + %module{} + + where "module" was given the type map() (due to calling var.field) in: + + # types_test.ex:4 + module.__struct__ + + HINT: "var.field" (without parentheses) implies "var" is a map() while \ + "var.fun()" (with parentheses) implies "var" is an atom() + """ + end + + test "use map as module (with parentheses)" do + string = warning([foo], [is_map(foo)], foo.__struct__()) + + assert string == """ + incompatible types: + + map() !~ atom() + + in expression: + + # types_test.ex:1 + foo.__struct__() + + where "foo" was given the type map() in: + + # types_test.ex:1 + is_map(foo) + + where "foo" was given the type atom() (due to calling var.fun()) in: + + # types_test.ex:1 + foo.__struct__() + + HINT: "var.field" (without parentheses) implies "var" is a map() while \ + "var.fun()" (with parentheses) implies "var" is an atom() + """ + end + + test "non-existant map field warning" do + string = + warning( + ( + map = %{foo: 1} + map.bar + ) + ) + + assert string == """ + undefined field "bar" in expression: + + # types_test.ex:3 + map.bar + + expected one of the following fields: foo + + where "map" was given the type map() in: + + # types_test.ex:2 + map = %{foo: 1} + """ + end + + test "non-existant struct field warning" do + string = + warning( + [foo], + ( + %URI{} = foo + foo.bar + ) + ) + + assert string == """ + undefined field "bar" in expression: + + # types_test.ex:4 + foo.bar + + expected one of the following fields: __struct__, authority, fragment, host, path, port, query, scheme, userinfo + + where "foo" was given the type %URI{} in: + + # types_test.ex:3 + %URI{} = foo + """ + end + end end |