Skip to content

Commit

Permalink
Merge pull request #50 from Qqwy/default_overrides
Browse files Browse the repository at this point in the history
WIP: Default overrides
  • Loading branch information
Qqwy committed Sep 25, 2021
2 parents d2cf08d + 2f2e099 commit 63cbb88
Show file tree
Hide file tree
Showing 55 changed files with 1,082 additions and 51 deletions.
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- Types and type-checks are generated at compiletime.
- This means **type-checking code is optimized** rigorously by the compiler.
- **Property-checking generators** can be extracted from type specifications without extra work.
- Automatically create a **spectest** which checks for each function if it adheres to its spec.
- Flexibility to add **custom checks**: Subparts of a type can be named, and 'type guards' can be specified to restrict what values are allowed to match that refer to these types.


Expand Down Expand Up @@ -182,10 +183,11 @@ Details:
- [x] Manually overriding generators for user-specified types if so desired.
- [x] Creating generators from specs
- [x] Wrap spec-generators so you have a single statement to call in the test suite which will prop-test your function against all allowed inputs/outputs.
- [ ] Overrides for builtin remote types (`String.t`,`Enum.t`, `Range.t`, `MapSet.t` etc.) **(75% done)**

### Pre-stable

- [ ] Overrides for builtin remote types (`String.t`,`Enum.t`, `Range.t`, `MapSet.t` etc.)
- [ ] Overrides for more builtin remote types
- [ ] Hide named types from opaque types.
- [ ] Configurable setting to turn on/off at compile-time, and maybe dynamically at run-time (with slight performance penalty).
- [ ] Finalize formatter specification and make a generator for this so that people can easily test their own formatters.
Expand All @@ -196,11 +198,20 @@ Details:

### Changelog

- 0.6.0 Addition of `spectest`:
- 0.6.0 Addition of `spectest` & 'default overrides' Elixir's standard library types:
- Adding `TypeCheck.ExUnit`, with the function `spectest` to test function-specifications.
- Possibility to use options `:except`, `:only`, `:initial_seed`.
- Possibility to pass custom options to StreamData.
- Adding `TypeCheck.DefaultOverrides` with many sub-modules containing checked typespecs for the types in Elixir's standard library (75% done).
- Ensure that these types are correct also on older Elixir versions (1.9, 1.10, 1.11)
- By default load these 'DefaultOverrides', but have the option to turn this behaviour off in `TypeCheck.Option`.
- Nice generators for `Enum.t`, `Collectable.t`, `String.t`.
- Support for the builtin types:
- `pid()`
- `nonempty_list()`, `nonempty_list(type)`.
- Allow `use TypeCheck` in IEx or other non-module contexts, to require `TypeCheck` and import `TypeCheck.Builtin` in the current scope (without importing/using the macros that only work at the module level.)
- The introspection function `__type_check__/1` is now added to any module that contains a `use TypeCheck`.
- Fixes the `Inspect` implementation of custom structs, by falling back to `Any`, which is more useful than attempting to use a customized implementation that would try to read the values in the struct and failing because the struct-type containing types in the fields.
- Fixes conditional compilation warnings when optional dependency `:stream_data` was not included in your project.
- 0.5.0 Stability improvements:
- Adding `Typecheck.Option` `debug: true`, which will (at compile-time) print the checks that TypeCheck is generating.
Expand Down
8 changes: 8 additions & 0 deletions coveralls.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"skip_files": [
"lib/type_check/options/default_overrides/"
],
"terminal_options": {
"file_column_width": 60
}
}
57 changes: 38 additions & 19 deletions lib/type_check.ex
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ defmodule TypeCheck do
end
end
Finally, you can test whether your functions correctly adhere to their specs,
by adding a `spectest` in your testing suite. See `TypeCheck.ExUnit.spectest/2` for details.
## Types and their syntax
Expand Down Expand Up @@ -111,9 +113,20 @@ defmodule TypeCheck do
"""

defmacro __using__(options) do
quote generated: true, location: :keep do
use TypeCheck.Macros, unquote(options)
import TypeCheck.Builtin
case __CALLER__.module do
nil ->
quote generated: true, location: :keep do
require TypeCheck
import TypeCheck.Builtin
:ok
end
_other ->
quote generated: true, location: :keep do
use TypeCheck.Macros, unquote(options)
require TypeCheck
import TypeCheck.Builtin
:ok
end
end
end

Expand All @@ -137,9 +150,11 @@ defmodule TypeCheck do
@type value :: any()
@spec conforms(value, TypeCheck.Type.expandable_type()) ::
{:ok, value} | {:error, TypeCheck.TypeError.t()}
defmacro conforms(value, type, options \\ TypeCheck.Options.new()) do
options = TypeCheck.Options.new(options)
type = TypeCheck.Type.build_unescaped(type, __CALLER__, options)
defmacro conforms(value, type, options \\ Macro.escape(TypeCheck.Options.new())) do
{evaluated_options, _} = Code.eval_quoted(options, [], __CALLER__)

evaluated_options = TypeCheck.Options.new(evaluated_options)
type = TypeCheck.Type.build_unescaped(type, __CALLER__, evaluated_options)
check = TypeCheck.Protocols.ToCheck.to_check(type, value)

res = quote generated: true, location: :keep do
Expand All @@ -149,7 +164,7 @@ defmodule TypeCheck do
end
end

if(options.debug) do
if(evaluated_options.debug) do
TypeCheck.Internals.Helper.prettyprint_spec("TypeCheck.conforms(#{inspect(value)}, #{inspect(type)}, #{inspect(options)})", res)
end
res
Expand All @@ -161,16 +176,18 @@ defmodule TypeCheck do
The same features and restrictions apply to this function as to `conforms/2`.
"""
@spec conforms?(value, TypeCheck.Type.expandable_type()) :: boolean()
defmacro conforms?(value, type, options \\ TypeCheck.Options.new()) do
options = TypeCheck.Options.new(options)
type = TypeCheck.Type.build_unescaped(type, __CALLER__, options)
defmacro conforms?(value, type, options \\ Macro.escape(TypeCheck.Options.new())) do
{evaluated_options, _} = Code.eval_quoted(options, [], __CALLER__)

evaluated_options = TypeCheck.Options.new(evaluated_options)
type = TypeCheck.Type.build_unescaped(type, __CALLER__, evaluated_options)
check = TypeCheck.Protocols.ToCheck.to_check(type, value)

res = quote generated: true, location: :keep do
match?({:ok, _}, unquote(check))
end

if(options.debug) do
if(evaluated_options.debug) do
TypeCheck.Internals.Helper.prettyprint_spec("TypeCheck.conforms?(#{inspect(value)}, #{inspect(type)}, #{inspect(options)})", res)
end

Expand All @@ -183,9 +200,11 @@ defmodule TypeCheck do
The same features and restrictions apply to this function as to `conforms/2`.
"""
@spec conforms!(value, TypeCheck.Type.expandable_type()) :: value | no_return()
defmacro conforms!(value, type, options \\ TypeCheck.Options.new()) do
options = TypeCheck.Options.new(options)
type = TypeCheck.Type.build_unescaped(type, __CALLER__, options)
defmacro conforms!(value, type, options \\ Macro.escape(TypeCheck.Options.new())) do
{evaluated_options, _} = Code.eval_quoted(options, [], __CALLER__)

evaluated_options = TypeCheck.Options.new(evaluated_options)
type = TypeCheck.Type.build_unescaped(type, __CALLER__, evaluated_options)
check = TypeCheck.Protocols.ToCheck.to_check(type, value)

res = quote generated: true, location: :keep do
Expand All @@ -195,7 +214,7 @@ defmodule TypeCheck do
end
end

if(options.debug) do
if(evaluated_options.debug) do
TypeCheck.Internals.Helper.prettyprint_spec("TypeCheck.conforms!(#{inspect(value)}, #{inspect(type)}, #{inspect(options)})", res)
end

Expand All @@ -222,16 +241,16 @@ defmodule TypeCheck do
{:ok, 42}
iex> {:error, type_error} = TypeCheck.dynamic_conforms(20, fourty_two)
iex> type_error.message
"At lib/type_check.ex:241:
"At lib/type_check.ex:260:
`20` is not the same value as `42`."
"""
@spec dynamic_conforms(value, TypeCheck.Type.t()) ::
{:ok, value} | {:error, TypeCheck.TypeError.t()}
def dynamic_conforms(value, type, options \\ TypeCheck.Options.new()) do
options = TypeCheck.Options.new(options)
evaluated_options = TypeCheck.Options.new(options)
check_code = TypeCheck.Protocols.ToCheck.to_check(type, Macro.var(:value, nil))

if(options.debug) do
if(evaluated_options.debug) do
TypeCheck.Internals.Helper.prettyprint_spec("TypeCheck.dynamic_conforms(#{inspect(value)}, #{inspect(type)}, #{inspect(options)})", check_code)
end

Expand Down Expand Up @@ -274,7 +293,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:241:
** (TypeCheck.TypeError) At lib/type_check.ex:260:
`20` is not the same value as `42`.
"""
@spec dynamic_conforms!(value, TypeCheck.Type.t()) :: value | no_return()
Expand Down
86 changes: 80 additions & 6 deletions lib/type_check/builtin.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,49 @@
defmodule TypeCheck.Builtin do
@moduledoc """
Contains TypeCheck specifications for all 'built-in' Elixir types.
These are all the types described on the ['Basic Types', 'Literals' and 'Builtin Types' sections of the Elixir 'Typespecs' documentation page.](https://hexdocs.pm/elixir/typespecs.html#basic-types)
See `TypeCheck.DefaultOverrides` for the 'Remote Types' supported by TypeCheck.
Usually you'd want to import this module when you're using TypeCheck.
This is done automatically when calling `use TypeCheck`.
If necessary, feel free to hide (using `import ... except: `)
the things you don't need.
### Ommissions
The following types are currently still missing from this module.
This will change in future versions of the library.
The hope is to at some point support all of them, or as close to it as feasible.
From the 'Basic Types':
- `port()`
- `reference()`
- `maybe_improper_list(content_type, termination_type)`
- `nonempty_improper_list(content_type, termination_type)`
- `nonempty_maybe_improper_list(content_type, termination_type)`
From the 'Literals':
- Special function syntax. Only `function()` is currently supported.
- Special bitstring-patterns. Only `binary()` and `bitstring()` are currently supported.
- The `optional(any()) => any()` syntax in maps. Currently `fixed_map` accepts maps with extra keys.
From the 'Builtin Types':
- `nonempty_charlist()`
- `iodata()`
- `identifier()`
- `iolist()`
- `node()`
- `timeout()`
"""


require TypeCheck.Internals.ToTypespec
# TypeCheck.Internals.ToTypespec.define_all()

Expand All @@ -7,12 +52,6 @@ defmodule TypeCheck.Builtin do
use TypeCheck
end

@moduledoc """
Usually you'd want to import this module when you're using TypeCheck.
Feel free to import only the things you need,
or hide (using `import ... except: `) the things you don't.
"""

@doc typekind: :builtin
@doc """
Expand Down Expand Up @@ -953,6 +992,41 @@ defmodule TypeCheck.Builtin do
"""
def no_return(), do: none()

@doc typekind: :builtin
@doc """
Matches any process-identifier.
Note that no checks are made to see whether the process is alive or not.
Also, the current property-generator will generate arbitrary PIDs, most of which
will not point to alive processes.
"""
def pid() do
build_struct(TypeCheck.Builtin.PID)
end

@doc typekind: :builtin
@doc """
A nonempty_list is any list with at least one element.
"""
def nonempty_list(type) do
guard =
quote do
length(unquote(Macro.var(:non_empty_list, nil))) > 0
end

guarded_by(named_type(:non_empty_list, list(type)), guard)
end

@doc typekind: :builtin
@doc """
Shorthand for nonempty_list(any()).
"""
def nonempty_list() do
nonempty_list(any())
end


@doc typekind: :extension
@doc """
Checks whether the given value implements the particular protocol.
Expand Down
1 change: 1 addition & 0 deletions lib/type_check/builtin/binary.ex
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ defmodule TypeCheck.Builtin.Binary do
defimpl TypeCheck.Protocols.ToStreamData do
def to_gen(_s) do
StreamData.binary()
# StreamData.string(:ascii)
end
end
end
Expand Down
41 changes: 41 additions & 0 deletions lib/type_check/builtin/pid.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
defmodule TypeCheck.Builtin.PID do
defstruct []

use TypeCheck
@type! t :: %__MODULE__{}
@type! problem_tuple :: {t(), :no_match, %{}, any()}

defimpl TypeCheck.Protocols.ToCheck do
def to_check(s, param) do
quote generated: true, location: :keep do
case unquote(param) do
x when is_pid(x) ->
{:ok, []}

_ ->
{:error, {unquote(Macro.escape(s)), :no_match, %{}, unquote(param)}}
end
end
end
end

defimpl TypeCheck.Protocols.Inspect do
def inspect(_, _opts) do
"pid()"
end
end

if Code.ensure_loaded?(StreamData) do
defimpl TypeCheck.Protocols.ToStreamData do
def to_gen(_s) do
partgen =
StreamData.integer()
|> StreamData.map(&abs/1)

{partgen, partgen, partgen}
|> StreamData.map(fn {a, b, c} -> IEx.Helpers.pid(a, b, c) end)

end
end
end
end
Loading

0 comments on commit 63cbb88

Please sign in to comment.