Skip to content

Commit

Permalink
Merge branch 'master' of github.com:Qqwy/elixir-type_check
Browse files Browse the repository at this point in the history
  • Loading branch information
Qqwy committed Oct 13, 2021
2 parents 3204a51 + 1c4d534 commit 4103411
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 14 deletions.
16 changes: 14 additions & 2 deletions lib/type_check.ex
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,18 @@ defmodule TypeCheck do
`dynamic_conforms/2` and variants.
Because these variants have to evaluate the type-checking code at runtime,
these checks are not optimized by the compiler.
### Introspection
To allow checking what types and specs exist,
the introspection function `__type_check__/1` will be added to a module when `use TypeCheck` is used.
- `YourModule.__type_check__(:types)` returns a keyword list of all `{type_name, arity}`
pairs for all types defined in the module using `@type!`/`@typep!` or `@opaque!`.
- `YourModule.__type_check__(:specs)` returns a keyword list of all `{function_name, arity}`
pairs for all functions in the module wrapped with a spec using `@spec!`.
Note that these lists might also contain private types / private function names.
"""

defmacro __using__(options) do
Expand Down Expand Up @@ -247,7 +259,7 @@ defmodule TypeCheck do
{:ok, 42}
iex> {:error, type_error} = TypeCheck.dynamic_conforms(20, fourty_two)
iex> type_error.message
"At lib/type_check.ex:266:
"At lib/type_check.ex:278:
`20` is not the same value as `42`."
"""
@spec dynamic_conforms(value, TypeCheck.Type.t()) ::
Expand Down Expand Up @@ -299,7 +311,7 @@ defmodule TypeCheck do
iex> TypeCheck.dynamic_conforms!(42, fourty_two)
42
iex> TypeCheck.dynamic_conforms!(20, fourty_two)
** (TypeCheck.TypeError) At lib/type_check.ex:266:
** (TypeCheck.TypeError) At lib/type_check.ex:278:
`20` is not the same value as `42`.
"""
@spec dynamic_conforms!(value, TypeCheck.Type.t()) :: value | no_return()
Expand Down
3 changes: 2 additions & 1 deletion lib/type_check/macros.ex
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,8 @@ defmodule TypeCheck.Macros do
clean_params,
params_spec_code,
return_spec_code,
clean_specdef
clean_specdef,
caller
)

if typecheck_options.debug do
Expand Down
41 changes: 30 additions & 11 deletions lib/type_check/spec.ex
Original file line number Diff line number Diff line change
Expand Up @@ -112,23 +112,43 @@ defmodule TypeCheck.Spec do
end

@doc false
def wrap_function_with_spec(name, _location, arity, clean_params, params_spec_code, return_spec_code, typespec) do
def check_function_kind(module, function, arity) do
cond do
Module.defines?(module, {function, arity}, :def) ->
:def
Module.defines?(module, {function, arity}, :defp) ->
:defp
Module.defines?(module, {function, arity}, :defmacro) ->
:defmacro
Module.defines?(module, {function, arity}, :defmacrop) ->
:defmacrop
true ->
raise TypeCheck.CompileError, "cannot add spec to #{to_string(module)}.#{inspect(function)}/#{inspect(arity)} because it was not defined"
end
end

@doc false
def wrap_function_with_spec(name, _location, arity, clean_params, params_spec_code, return_spec_code, typespec, caller) do
# {file, line} = location

body = quote do
unquote(params_spec_code)
var!(super_result, nil) = super(unquote_splicing(clean_params))
unquote(return_spec_code)
var!(super_result, nil)
end

# Check if original function is public or private
function_kind = TypeCheck.Spec.check_function_kind(caller.module, name, arity)

quote generated: true, location: :keep do
if Module.get_attribute(__MODULE__, :autogen_typespec) do
@spec unquote(typespec)
end
defoverridable([{unquote(name), unquote(arity)}])

def unquote(name)(unquote_splicing(clean_params)) do
# import TypeCheck.Builtin
defoverridable([{unquote(name), unquote(arity)}])

unquote(params_spec_code)
var!(super_result, nil) = super(unquote_splicing(clean_params))
unquote(return_spec_code)
var!(super_result, nil)
end
unquote(function_kind)(unquote(name)(unquote_splicing(clean_params)), do: unquote(body))
end
end

Expand All @@ -144,9 +164,9 @@ defmodule TypeCheck.Spec do
defp params_check_code(_name, _arity = 0, _param_types, _clean_params, _caller, _location) do
# No check needed for arity-0 functions.
# Also gets rid of a compiler warning 'else will never match'
# {file, line} = location
quote generated: true, location: :keep do end
end

defp params_check_code(name, arity, param_types, clean_params, caller, location) do
paired_params =
param_types
Expand All @@ -156,7 +176,6 @@ defmodule TypeCheck.Spec do
param_check_code(param_type, clean_param, index, caller, location)
end)

# {file, line} = location
quote generated: true, location: :keep do
with unquote_splicing(paired_params) do
# Run actual code
Expand Down
51 changes: 51 additions & 0 deletions test/type_check/macros_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,55 @@ defmodule TypeCheck.MacrosTest do
end)
end
end

test "specs can be added to private functions" do
defmodule PrivateFunctionSpecExample do
use TypeCheck

@spec! secret(integer()) :: integer()
defp secret(_), do: 42

def public(val) do
secret(val)
end
end

assert TypeCheck.Spec.defined?(PrivateFunctionSpecExample, :secret, 1)
assert [secret: 1] == PrivateFunctionSpecExample.__type_check__(:specs)

# importantly, {secret: 1} is not in there:
assert ["__TypeCheck spec for 'secret/1'__": 0, __type_check__: 1, public: 1] = PrivateFunctionSpecExample.__info__(:functions)

assert PrivateFunctionSpecExample.public(10) == 42
assert_raise(TypeCheck.TypeError, fn ->
PrivateFunctionSpecExample.public("not an integer")
end)
end

test "specs can be added to macros" do
defmodule MacroSpecExample do
use TypeCheck

@spec! compile_time_atom_to_string(atom()) :: String.t()
defmacro compile_time_atom_to_string(atom) do
to_string(atom)
end
end

defmodule Example do
require MacroSpecExample
def example do
MacroSpecExample.compile_time_atom_to_string(:baz)
end
end

assert_raise(TypeCheck.TypeError, fn ->
defmodule Example2 do
require MacroSpecExample
def example do
MacroSpecExample.compile_time_atom_to_string(42)
end
end
end)
end
end

0 comments on commit 4103411

Please sign in to comment.