defmodule PropEx.TypespecClause do
defmacro typespec_clause(name, args \\ []) do
fun_args = Enum.map(args, fn name -> {:var!, [], [{name, [], __MODULE__}]} end)
call_args = Enum.map(args, fn name -> {:convert_typespec, [], [{:var!, [], [{name, [], __MODULE__}]}]} end)
call = {{:., [], [:proper_types, name]}, [], call_args}
quote do
def convert_typespec({unquote(name), _, unquote(fun_args)}), do: unquote(call)
end
end
end
defmodule PropEx do
@moduledoc """
A wrapper around PropEr
"""
alias Code.Typespec
alias :proper_types, as: PT
import __MODULE__.TypespecClause
def convert_typespec(typespec)
# straightforward types
typespec_clause :any
typespec_clause :term
typespec_clause :arity
typespec_clause :binary
typespec_clause :integer
typespec_clause :neg_integer
typespec_clause :non_neg_integer
typespec_clause :float
typespec_clause :list
typespec_clause :list, [:elem]
# type defined in another module
def convert_typespec({{:., _, [mod, type_name]}, _, _args} = ast) do
with {:ok, types} <- Typespec.fetch_types(mod),
types <- Enum.map(types, fn {:type, type} -> type end),
type <- List.keyfind(types, type_name, 0),
{:"::", _, [{^type_name, _, type_args}, type_definition]} <- Typespec.type_to_quoted(type) do
if length(type_args) != 0, do:
raise ArgumentError, "PropEx: Sorry, not implemented: type #{Macro.to_string(ast)} is generic"
convert_typespec(type_definition)
else
error -> raise ArgumentError, "PropEx: unable to access @type #{Macro.to_string(ast)}: #{error}"
end
end
# unimplemented types
def convert_typespec({type, _, []}) when type == :pid or type == :port or type == :reference, do:
raise ArgumentError, "PropEx: type #{type}() cannot be generated by PropEr"
def convert_typespec(type), do:
raise ArgumentError, "PropEx: Sorry, not implemented: type #{Macro.to_string(type)} is not supported\nRepresentation: #{inspect type}"
def convert_argument_spec({:"::", _, [{name, _, nil}, type]}), do:
{name, convert_typespec(type)}
def convert_argument_spec({:"::", _, [name, type]}) when is_atom(name), do:
{name, convert_typespec(type)}
def convert_argument_spec(_spec), do:
raise ArgumentError, """
PropEx: Your type specification seems to be missing variable names.
When using arguments_of, instead of:
@spec add(integer(), integer()) :: integer()
def add(a, b), do: a + b
You should write:
@spec add(a :: integer(), b :: integer()) :: integer()
def add(a, b), do: a + b
"""
def convert_argument_list(argument_list) do
argument_list
|> Enum.map(&convert_argument_spec/1)
|> Enum.into(%{})
|> Map.values
|> :erlang.list_to_tuple
end
def get_arguments_of({:/, _, [call, arity]} = ast, context) do
# parse the function reference
{orig_mod, mod, fun, arity} = case Macro.decompose_call(call) do
{mod, fun, []} -> {mod, Macro.expand(mod, context), fun, arity}
_ -> get_arguments_of(:yolo, context) # to raise an error
end
# fetch function specification and extract the arguments from it
args = with {:ok, specs} <- Typespec.fetch_specs(mod),
{{^fun, ^arity}, [spec]} <- List.keyfind(specs, {fun, arity}, 0),
{:"::", _, [{^fun, _, args}, _return_type]} <- Typespec.spec_to_quoted(fun, spec) do
args
else
error -> raise ArgumentError, "PropEx: unable to access the @spec for function #{Macro.to_string(ast)}: #{error}"
end
# generate the AST for `arg1, arg2, ...`
call_args = args
|> Enum.map(fn
{:"::", _, [{name, _, nil}, _type]} -> name
{:"::", _, [name, _type]} -> name
end)
|> Enum.map(fn var_name -> {:var!, [], [{var_name, [], __MODULE__}]} end)
# generate the AST for `result = mod.fun(args)`
result_fetcher = {:=, [], [{:var!, [], [{:result, [], __MODULE__}]}, {{:., [], [orig_mod, fun]}, [], call_args}]}
{args, result_fetcher}
end
def get_arguments_of(_, _), do: raise ArgumentError, """
PropEx: It looks like you passed something other than Mod.fun/arity to arguments_of. You should use it like this:
forall arguments_of MyModule.my_fun/2 do
# code
end
"""
defmacro __using__(_env) do
quote do
require PropEx
import PropEx, only: [forall: 2]
end
end
defmacro forall(argument_list, opts) do
# if argument_list is actually a request to fetch the arguments from the
# specs, do that
{argument_list, result_fetcher} = case argument_list do
{:arguments_of, _, [ast_fun_ref]} -> get_arguments_of(ast_fun_ref, __CALLER__)
_ -> {argument_list, nil}
end
# process argument list
destructurer = argument_list
|> Enum.map(&convert_argument_spec/1)
|> Enum.into(%{})
|> Map.keys
|> Enum.map(fn var_name -> {:var!, [], [{var_name, [], __MODULE__}]} end)
|> :erlang.list_to_tuple
quote do
assert :proper.forall(PropEx.convert_argument_list(unquote(Macro.escape(argument_list))), fn arguments ->
unquote(destructurer) = arguments
unquote(result_fetcher)
unquote(opts[:do])
end) |> :proper.quickcheck
end
end
end