500 lines
15 KiB
Elixir
500 lines
15 KiB
Elixir
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
# SPDX-FileCopyrightText: 2012 Plataformatec
|
|
|
|
Code.require_file("../test_helper.exs", __DIR__)
|
|
|
|
defmodule Kernel.GuardTest do
|
|
use ExUnit.Case, async: true
|
|
|
|
import ExUnit.CaptureIO
|
|
|
|
describe "defguard(p) usage" do
|
|
defmodule GuardsInMacros do
|
|
defguard is_foo(atom) when atom == :foo
|
|
|
|
defmacro is_compile_time_foo(atom) when is_foo(atom) do
|
|
quote do: unquote(__MODULE__).is_foo(unquote(atom))
|
|
end
|
|
end
|
|
|
|
test "guards can be used in other macros in the same module" do
|
|
require GuardsInMacros
|
|
assert GuardsInMacros.is_foo(:foo)
|
|
refute GuardsInMacros.is_foo(:baz)
|
|
assert GuardsInMacros.is_compile_time_foo(:foo)
|
|
end
|
|
|
|
defmodule GuardsInFuns do
|
|
defguard is_foo(atom) when atom == :foo
|
|
defguard is_equal(foo, bar) when foo == bar
|
|
|
|
def foo?(atom) when is_foo(atom) do
|
|
is_foo(atom)
|
|
end
|
|
end
|
|
|
|
test "guards can be used in other funs in the same module" do
|
|
require GuardsInFuns
|
|
assert GuardsInFuns.is_foo(:foo)
|
|
refute GuardsInFuns.is_foo(:bar)
|
|
end
|
|
|
|
test "guards do not change code evaluation semantics" do
|
|
require GuardsInFuns
|
|
x = 1
|
|
assert GuardsInFuns.is_equal(x = 2, x) == false
|
|
assert x == 2
|
|
end
|
|
|
|
defmodule MacrosInGuards do
|
|
defmacro is_foo(atom) do
|
|
quote do
|
|
unquote(atom) == :foo
|
|
end
|
|
end
|
|
|
|
defguard is_foobar(atom) when is_foo(atom) or atom == :bar
|
|
end
|
|
|
|
test "macros can be used in other guards in the same module" do
|
|
require MacrosInGuards
|
|
assert MacrosInGuards.is_foobar(:foo)
|
|
assert MacrosInGuards.is_foobar(:bar)
|
|
refute MacrosInGuards.is_foobar(:baz)
|
|
end
|
|
|
|
defmodule UnquotedInGuardCall do
|
|
@value :foo
|
|
|
|
defguard unquote(String.to_atom("is_#{@value}"))(x) when x == unquote(@value)
|
|
end
|
|
|
|
test "guards names can be defined dynamically using unquote" do
|
|
require UnquotedInGuardCall
|
|
assert UnquotedInGuardCall.is_foo(:foo)
|
|
refute UnquotedInGuardCall.is_foo(:bar)
|
|
end
|
|
|
|
defmodule GuardsInGuards do
|
|
defguard is_foo(atom) when atom == :foo
|
|
defguard is_foobar(atom) when is_foo(atom) or atom == :bar
|
|
end
|
|
|
|
test "guards can be used in other guards in the same module" do
|
|
require GuardsInGuards
|
|
assert GuardsInGuards.is_foobar(:foo)
|
|
assert GuardsInGuards.is_foobar(:bar)
|
|
refute GuardsInGuards.is_foobar(:baz)
|
|
end
|
|
|
|
defmodule DefaultArgs do
|
|
defguard is_divisible(value, remainder \\ 2)
|
|
when is_integer(value) and rem(value, remainder) == 0
|
|
end
|
|
|
|
test "permits default values in args" do
|
|
require DefaultArgs
|
|
assert DefaultArgs.is_divisible(2)
|
|
refute DefaultArgs.is_divisible(1)
|
|
assert DefaultArgs.is_divisible(3, 3)
|
|
refute DefaultArgs.is_divisible(3, 4)
|
|
end
|
|
|
|
test "doesn't allow matching in args" do
|
|
assert_raise ArgumentError, ~r"invalid syntax in defguard", fn ->
|
|
defmodule Integer.Args do
|
|
defguard foo(value, 1) when is_integer(value)
|
|
end
|
|
end
|
|
|
|
assert_raise ArgumentError, ~r"invalid syntax in defguard", fn ->
|
|
defmodule String.Args do
|
|
defguard foo(value, "string") when is_integer(value)
|
|
end
|
|
end
|
|
|
|
assert_raise ArgumentError, ~r"invalid syntax in defguard", fn ->
|
|
defmodule Atom.Args do
|
|
defguard foo(value, :atom) when is_integer(value)
|
|
end
|
|
end
|
|
|
|
assert_raise ArgumentError, ~r"invalid syntax in defguard", fn ->
|
|
defmodule Tuple.Args do
|
|
defguard foo(value, {foo, bar}) when is_integer(value)
|
|
end
|
|
end
|
|
end
|
|
|
|
defmodule GuardFromMacro do
|
|
defmacro __using__(_) do
|
|
quote do
|
|
defguard is_even(value) when is_integer(value) and rem(value, 2) == 0
|
|
end
|
|
end
|
|
end
|
|
|
|
test "defguard defines a guard from inside another macro" do
|
|
defmodule UseGuardFromMacro do
|
|
use GuardFromMacro
|
|
|
|
def assert! do
|
|
assert is_even(0)
|
|
refute is_even(1)
|
|
end
|
|
end
|
|
|
|
UseGuardFromMacro.assert!()
|
|
end
|
|
|
|
defmodule IntegerPrivateGuards do
|
|
defguardp is_even(value) when is_integer(value) and rem(value, 2) == 0
|
|
|
|
def even_and_large?(value) when is_even(value) and value > 100, do: true
|
|
def even_and_large?(_), do: false
|
|
|
|
def even_and_small?(value) do
|
|
if is_even(value) and value <= 100, do: true, else: false
|
|
end
|
|
end
|
|
|
|
test "defguardp defines private guards that work inside and outside guard clauses" do
|
|
assert IntegerPrivateGuards.even_and_large?(102)
|
|
refute IntegerPrivateGuards.even_and_large?(98)
|
|
refute IntegerPrivateGuards.even_and_large?(99)
|
|
refute IntegerPrivateGuards.even_and_large?(103)
|
|
|
|
assert IntegerPrivateGuards.even_and_small?(98)
|
|
refute IntegerPrivateGuards.even_and_small?(99)
|
|
refute IntegerPrivateGuards.even_and_small?(102)
|
|
refute IntegerPrivateGuards.even_and_small?(103)
|
|
|
|
assert_compile_error(~r"cannot find or invoke local is_even/1", fn ->
|
|
defmodule IntegerPrivateGuardUtils do
|
|
import IntegerPrivateGuards
|
|
|
|
def even_and_large?(value) when is_even(value) and value > 100, do: true
|
|
def even_and_large?(_), do: false
|
|
end
|
|
end)
|
|
|
|
assert_compile_error(~r"undefined function is_even/1", fn ->
|
|
defmodule IntegerPrivateFunctionUtils do
|
|
import IntegerPrivateGuards
|
|
|
|
def even_and_small?(value) do
|
|
if is_even(value) and value <= 100, do: true, else: false
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
|
|
test "requires a proper macro name" do
|
|
assert_raise ArgumentError, ~r"invalid syntax in defguard", fn ->
|
|
defmodule(LiteralUsage, do: defguard("literal is bad"))
|
|
end
|
|
|
|
assert_raise ArgumentError, ~r"invalid syntax in defguard", fn ->
|
|
defmodule(RemoteUsage, do: defguard(Remote.call(is_bad)))
|
|
end
|
|
end
|
|
|
|
test "handles overriding appropriately" do
|
|
assert_compile_error(~r"defmacro (.*?) already defined as def", fn ->
|
|
defmodule OverriddenFunUsage do
|
|
def foo(bar), do: bar
|
|
defguard foo(bar) when bar
|
|
end
|
|
end)
|
|
|
|
assert_compile_error(~r"defmacro (.*?) already defined as defp", fn ->
|
|
defmodule OverriddenPrivateFunUsage do
|
|
defp foo(bar), do: bar
|
|
defguard foo(bar) when bar
|
|
end
|
|
end)
|
|
|
|
assert_compile_error(~r"defmacro (.*?) already defined as defmacrop", fn ->
|
|
defmodule OverriddenPrivateFunUsage do
|
|
defmacrop foo(bar), do: bar
|
|
defguard foo(bar) when bar
|
|
end
|
|
end)
|
|
|
|
assert_compile_error(~r"defmacrop (.*?) already defined as def", fn ->
|
|
defmodule OverriddenFunUsage do
|
|
def foo(bar), do: bar
|
|
defguardp foo(bar) when bar
|
|
end
|
|
end)
|
|
|
|
assert_compile_error(~r"defmacrop (.*?) already defined as defp", fn ->
|
|
defmodule OverriddenPrivateFunUsage do
|
|
defp foo(bar), do: bar
|
|
defguardp foo(bar) when bar
|
|
end
|
|
end)
|
|
|
|
assert_compile_error(~r"defmacrop (.*?) already defined as defmacro", fn ->
|
|
defmodule OverriddenPrivateFunUsage do
|
|
defmacro foo(bar), do: bar
|
|
defguardp foo(bar) when bar
|
|
end
|
|
end)
|
|
end
|
|
|
|
test "does not allow multiple guard clauses" do
|
|
assert_raise ArgumentError, ~r"invalid syntax in defguard", fn ->
|
|
defmodule MultiGuardUsage do
|
|
defguardp foo(bar, baz) when bar == 1 when baz == 2
|
|
end
|
|
end
|
|
end
|
|
|
|
test "does not accept a block" do
|
|
assert_compile_error(~r"undefined function defguard/2", fn ->
|
|
defmodule OnelinerBlockUsage do
|
|
defguard(foo(bar), do: one_liner)
|
|
end
|
|
end)
|
|
|
|
assert_compile_error(~r"undefined function defguard/2", fn ->
|
|
defmodule MultilineBlockUsage do
|
|
defguard foo(bar) do
|
|
multi
|
|
liner
|
|
end
|
|
end
|
|
end)
|
|
|
|
assert_compile_error(~r"undefined function defguard/2", fn ->
|
|
defmodule ImplAndBlockUsage do
|
|
defguard(foo(bar) when both_given, do: error)
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
|
|
describe "defguard(p) compilation" do
|
|
test "refuses to compile nonsensical code" do
|
|
assert_compile_error("cannot find or invoke local undefined/1", fn ->
|
|
defmodule UndefinedUsage do
|
|
defguard foo(function) when undefined(function)
|
|
end
|
|
end)
|
|
end
|
|
|
|
test "fails on expressions not allowed in guards" do
|
|
# Slightly unique errors
|
|
|
|
assert_raise ArgumentError, ~r{invalid right argument for operator "in"}, fn ->
|
|
defmodule RuntimeListUsage do
|
|
defguard foo(bar, baz) when bar in baz
|
|
end
|
|
end
|
|
|
|
assert_compile_error("cannot invoke remote function", fn ->
|
|
defmodule BadErlangFunctionUsage do
|
|
defguard foo(bar) when :erlang.binary_to_atom("foo")
|
|
end
|
|
end)
|
|
|
|
assert_compile_error("cannot invoke remote function", fn ->
|
|
defmodule SendUsage do
|
|
defguard foo(bar) when send(self(), :baz)
|
|
end
|
|
end)
|
|
|
|
# Consistent errors
|
|
|
|
assert_raise ArgumentError, ~r"invalid expression in guard, ! is not allowed", fn ->
|
|
defmodule SoftNegationLogicUsage do
|
|
defguard foo(logic) when !logic
|
|
end
|
|
end
|
|
|
|
assert_raise ArgumentError, ~r"invalid expression in guard, && is not allowed", fn ->
|
|
defmodule SoftAndLogicUsage do
|
|
defguard foo(soft, logic) when soft && logic
|
|
end
|
|
end
|
|
|
|
assert_raise ArgumentError, ~r"invalid expression in guard, || is not allowed", fn ->
|
|
defmodule SoftOrLogicUsage do
|
|
defguard foo(soft, logic) when soft || logic
|
|
end
|
|
end
|
|
|
|
assert_compile_error(
|
|
"cannot invoke remote function :erlang\.is_record/2 inside a guard",
|
|
fn ->
|
|
defmodule IsRecord2Usage do
|
|
defguard foo(rec) when :erlang.is_record(rec, :tag)
|
|
end
|
|
end
|
|
)
|
|
|
|
assert_compile_error(
|
|
"cannot invoke remote function :erlang\.is_record/3 inside a guard",
|
|
fn ->
|
|
defmodule IsRecord3Usage do
|
|
defguard foo(rec) when :erlang.is_record(rec, :tag, 7)
|
|
end
|
|
end
|
|
)
|
|
|
|
assert_compile_error(
|
|
~r"cannot invoke remote function :erlang\.\+\+/2 inside a guard",
|
|
fn ->
|
|
defmodule ListSubtractionUsage do
|
|
defguard foo(list) when list ++ []
|
|
end
|
|
end
|
|
)
|
|
|
|
assert_compile_error(
|
|
"cannot invoke remote function :erlang\.\-\-/2 inside a guard",
|
|
fn ->
|
|
defmodule ListSubtractionUsage do
|
|
defguard foo(list) when list -- []
|
|
end
|
|
end
|
|
)
|
|
|
|
assert_compile_error("invalid expression in guard", fn ->
|
|
defmodule LocalCallUsage do
|
|
defguard foo(local, call) when local.(call)
|
|
end
|
|
end)
|
|
|
|
assert_compile_error("invalid expression in guard", fn ->
|
|
defmodule ComprehensionUsage do
|
|
defguard foo(bar) when for(x <- [1, 2, 3], do: x * bar)
|
|
end
|
|
end)
|
|
|
|
assert_compile_error("invalid expression in guard", fn ->
|
|
defmodule AliasUsage do
|
|
defguard foo(bar) when alias(bar)
|
|
end
|
|
end)
|
|
|
|
assert_compile_error("invalid expression in guard", fn ->
|
|
defmodule ImportUsage do
|
|
defguard foo(bar) when import(bar)
|
|
end
|
|
end)
|
|
|
|
assert_compile_error("invalid expression in guard", fn ->
|
|
defmodule RequireUsage do
|
|
defguard foo(bar) when require(bar)
|
|
end
|
|
end)
|
|
|
|
assert_compile_error("invalid expression in guard", fn ->
|
|
defmodule SuperUsage do
|
|
defguard foo(bar) when super(bar)
|
|
end
|
|
end)
|
|
|
|
assert_compile_error("invalid expression in guard", fn ->
|
|
defmodule SpawnUsage do
|
|
defguard foo(bar) when spawn(& &1)
|
|
end
|
|
end)
|
|
|
|
assert_compile_error("invalid expression in guard", fn ->
|
|
defmodule ReceiveUsage do
|
|
defguard foo(bar) when receive(do: (baz -> baz))
|
|
end
|
|
end)
|
|
|
|
assert_compile_error("invalid expression in guard", fn ->
|
|
defmodule CaseUsage do
|
|
defguard foo(bar) when case(bar, do: (baz -> :baz))
|
|
end
|
|
end)
|
|
|
|
assert_compile_error("invalid expression in guard", fn ->
|
|
defmodule CondUsage do
|
|
defguard foo(bar) when cond(do: (bar -> :baz))
|
|
end
|
|
end)
|
|
|
|
assert_compile_error("invalid expression in guard", fn ->
|
|
defmodule TryUsage do
|
|
defguard foo(bar) when try(do: (baz -> baz))
|
|
end
|
|
end)
|
|
|
|
assert_compile_error("invalid expression in guard", fn ->
|
|
defmodule WithUsage do
|
|
defguard foo(bar) when with(do: (baz -> baz))
|
|
end
|
|
end)
|
|
|
|
assert_compile_error(
|
|
"cannot invoke remote function inside a guard. " <>
|
|
"If you want to do a map lookup instead, please remove parens from map.field()",
|
|
fn ->
|
|
defmodule MapDot do
|
|
def map_dot(map) when map.field(), do: true
|
|
end
|
|
end
|
|
)
|
|
|
|
assert_compile_error("cannot invoke remote function Module.fun/0 inside a guard", fn ->
|
|
defmodule MapDot do
|
|
def map_dot(map) when Module.fun(), do: true
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
|
|
describe "defguard(p) expansion" do
|
|
defguard with_unused_vars(foo, bar, _baz) when foo + bar
|
|
|
|
test "doesn't obscure unused variables" do
|
|
args = quote(do: [1 + 1, 2 + 2, 3 + 3])
|
|
|
|
assert expand_defguard_to_string(:with_unused_vars, args, :guard) == """
|
|
:erlang.+(1 + 1, 2 + 2)
|
|
"""
|
|
|
|
assert expand_defguard_to_string(:with_unused_vars, args, nil) == """
|
|
{arg1, arg2} = {1 + 1, 2 + 2}
|
|
:erlang.+(arg1, arg2)
|
|
"""
|
|
end
|
|
|
|
defguard with_reused_vars(foo, bar, baz) when foo + foo + bar + baz
|
|
|
|
test "handles re-used variables" do
|
|
args = quote(do: [1 + 1, 2 + 2, 3 + 3])
|
|
|
|
assert expand_defguard_to_string(:with_reused_vars, args, :guard) == """
|
|
:erlang.+(:erlang.+(:erlang.+(1 + 1, 1 + 1), 2 + 2), 3 + 3)
|
|
"""
|
|
|
|
assert expand_defguard_to_string(:with_reused_vars, args, nil) == """
|
|
{arg1, arg2, arg3} = {1 + 1, 2 + 2, 3 + 3}
|
|
:erlang.+(:erlang.+(:erlang.+(arg1, arg1), arg2), arg3)
|
|
"""
|
|
end
|
|
|
|
defp expand_defguard_to_string(fun, args, context) do
|
|
{{:., [], [__MODULE__, fun]}, [], args}
|
|
|> Macro.expand(%{__ENV__ | context: context})
|
|
|> Macro.to_string()
|
|
|> Kernel.<>("\n")
|
|
end
|
|
end
|
|
|
|
defp assert_compile_error(message, fun) do
|
|
assert capture_io(:stderr, fn ->
|
|
assert_raise CompileError, fun
|
|
end) =~ message
|
|
end
|
|
end
|