summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEric Meadows-Jönsson <eric.meadows.jonsson@gmail.com>2020-09-12 18:19:19 +0200
committerEric Meadows-Jönsson <eric.meadows.jonsson@gmail.com>2020-09-12 18:23:38 +0200
commit023db086f953e942be0c15c32fd26d5b773caa10 (patch)
tree9f625efb43d2d462698adf91e431803f2d2e2939
parent556bd254db79ae0c3287a0d6773dce3e96882691 (diff)
downloadelixir-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.ex2
-rw-r--r--lib/elixir/test/elixir/module/types/integration_test.exs539
-rw-r--r--lib/elixir/test/elixir/module/types/type_helper.exs6
-rw-r--r--lib/elixir/test/elixir/module/types/types_test.exs437
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