GithubHelp home page GithubHelp logo

qqwy / elixir-type_check Goto Github PK

View Code? Open in Web Editor NEW
514.0 11.0 23.0 1.47 MB

TypeCheck: Fast and flexible runtime type-checking for your Elixir projects.

License: MIT License

Elixir 100.00%
elixir-lang type-checking property-based-testing metaprogramming hacktoberfest

elixir-type_check's Introduction

TypeCheck: Fast and flexible runtime type-checking for your Elixir projects.

hex.pm version Documentation ci Coverage Status

Core ideas

  • Type- and function specifications are constructed using (essentially) the same syntax as built-in Elixir Typespecs.
  • When a value does not match a type check, the user is shown human-friendly error messages.
  • 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.

Prefer to watch a presentation instead of reading? See "TypeCheck: Effortless Runtime Type Checking" - Marten Wijnja - ElixirConf EU 2022.

Usage Example

We add use TypeCheck to a module and wherever we want to add runtime type-checks we replace the normal calls to @type and @spec with @type! and @spec! respectively.

defmodule User do
  use TypeCheck
  defstruct [:name, :age]

  @type! t :: %User{name: binary, age: integer}
end

defmodule AgeCheck do
  use TypeCheck

  @spec! user_older_than?(User.t, integer) :: boolean
  def user_older_than?(user, age) do
    user.age >= age
  end
end

Now we can try the following:

iex> AgeCheck.user_older_than?(%User{name: "Qqwy", age: 11}, 10)
true
iex> AgeCheck.user_older_than?(%User{name: "Qqwy", age: 9}, 10)
false

So far so good. Now let's see what happens when we pass values that are incorrect:

iex> AgeCheck.user_older_than?("foobar", 42)
** (TypeCheck.TypeError) At lib/type_check_example.ex:28:
The call to `user_older_than?/2` failed,
because parameter no. 1 does not adhere to the spec `%User{age: integer(), name: binary()}`.
Rather, its value is: `"foobar"`.
Details:
  The call `user_older_than?("foobar", 42)` 
  does not adhere to spec `user_older_than?(%User{age: integer(), name: binary()},  integer()) :: boolean()`. Reason:
    parameter no. 1:
      `"foobar"` does not check against `%User{age: integer(), name: binary()}`. Reason:
        `"foobar"` is not a map.
    (type_check_example 0.1.0) lib/type_check_example.ex:28: AgeCheck.user_older_than?/2
iex> AgeCheck.user_older_than?(%User{name: nil, age: 11}, 10)
** (TypeCheck.TypeError) At lib/type_check_example.ex:28:
The call to `user_older_than?/2` failed,
because parameter no. 1 does not adhere to the spec `%User{age: integer(), name: binary()}`.
Rather, its value is: `%User{age: 11, name: nil}`.
Details:
  The call `user_older_than?(%User{age: 11, name: nil}, 10)` 
  does not adhere to spec `user_older_than?(%User{age: integer(), name: binary()},  integer()) :: boolean()`. Reason:
    parameter no. 1:
      `%User{age: 11, name: nil}` does not check against `%User{age: integer(), name: binary()}`. Reason:
        under key `:name`:
          `nil` is not a binary.
    (type_check_example 0.1.0) lib/type_check_example.ex:28: AgeCheck.user_older_than?/2
iex> AgeCheck.user_older_than?(%User{name: "Aaron", age: nil}, 10) 
** (TypeCheck.TypeError) At lib/type_check_example.ex:28:
The call to `user_older_than?/2` failed,
because parameter no. 1 does not adhere to the spec `%User{age: integer(), name: binary()}`.
Rather, its value is: `%User{age: nil, name: "Aaron"}`.
Details:
  The call `user_older_than?(%User{age: nil, name: "Aaron"}, 10)` 
  does not adhere to spec `user_older_than?(%User{age: integer(), name: binary()},  integer()) :: boolean()`. Reason:
    parameter no. 1:
      `%User{age: nil, name: "Aaron"}` does not check against `%User{age: integer(), name: binary()}`. Reason:
        under key `:age`:
          `nil` is not an integer.
    (type_check_example 0.1.0) lib/type_check_example.ex:28: AgeCheck.user_older_than?/2
    
iex> AgeCheck.user_older_than?(%User{name: "José", age: 11}, 10.0) 
** (TypeCheck.TypeError) At lib/type_check_example.ex:28:
The call to `user_older_than?/2` failed,
because parameter no. 2 does not adhere to the spec `integer()`.
Rather, its value is: `10.0`.
Details:
  The call `user_older_than?(%User{age: 11, name: "José"}, 10.0)` 
  does not adhere to spec `user_older_than?(%User{age: integer(), name: binary()},  integer()) :: boolean()`. Reason:
    parameter no. 2:
      `10.0` is not an integer.
    (type_check_example 0.1.0) lib/type_check_example.ex:28: AgeCheck.user_older_than?/2

And if we were to introduce an error in the function definition:

defmodule AgeCheck do
  use TypeCheck

  @spec! user_older_than?(User.t, integer) :: boolean
  def user_older_than?(user, age) do
    user.age
  end
end

Then we get a nice error message explaining that problem as well:

** (TypeCheck.TypeError) The call to `user_older_than?/2` failed,
because the returned result does not adhere to the spec `boolean()`.
Rather, its value is: `26`.
Details:
  The result of calling `user_older_than?(%User{age: 26, name: "Marten"}, 10)` 
  does not adhere to spec `user_older_than?(%User{age: integer(), name: binary()},  integer()) :: boolean()`. Reason:
    Returned result:
      `26` is not a boolean.
    (type_check_example 0.1.0) lib/type_check_example.ex:28: AgeCheck.user_older_than?/2

Features & Roadmap

Implemented

  • Proof and implementation of the basic concept
  • Custom type definitions (type, typep, opaque)
    • Basic
    • Parameterized
  • Hide implementation of opaque from documentation
  • Spec argument types checking
  • Spec return type checking
  • Spec possibly named arguments
  • Implementation of Elixir's builtin types
    • Primitive types
    • More primitive types
    • Compound types
    • special forms like |, a..b etc.
    • Literal lists
    • Maps with keys => types
    • Structs with keys => types
    • More map/list-based structures.
    • Bitstring type syntax (<<>>, <<_ :: size>>, <<_ :: _ * unit>>, <<_ :: size, _ :: _ * unit>>)
  • A when to add guards to typedefs for more power.
  • Make errors raised when types do not match humanly readable
    • Improve readability of spec-errors by repeating spec and which parameter did not match.
  • Creating generators from types
  • Don't warn on zero-arity types used without parentheses.
  • Hide structure of opaque and typep from documentation
  • Make sure to handle recursive (and mutually recursive) types without hanging.
    • A compile-error is raised when a type is expanded more than a million times
    • A macro called lazy is introduced to allow to defer type expansion to runtime (to within the check).
  • the Elixir formatter likes the way types+specs are constructed
  • A type impl(ProtocolName) to work with 'any type implementing protocol Protocolname'.
    • Type checks.
    • StreamData generator.
  • High code-coverage to ensure stability of implementation.
  • Make sure we handle most (if not all) of Typespec's primitive types and syntax. (With the exception of functions and binary pattern matching)
  • Option to turn @type/@opaque/@typep-injection off for the cases in which it generates improper results.
  • Manually overriding generators for user-specified types if so desired.
  • Creating generators from specs
    • 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.
  • Option to turn the generation of runtime checks off for a given module in a particular environment (enable_runtime_checks).
  • Support for function-types (for typechecks as well as property-testing generators):
    • (-> result_type)
    • (...-> result_type)
    • (param_type, param2_type -> result_type)
  • Basic support for maps with a single required(type) or optional(type).
  • Overrides for builtin remote types (String.t,Enum.t, Range.t, MapSet.t etc.) (75% done) Details
  • Overrides for more builtin remote types
  • Support for maps with mixed required(type) and optional(type) syntaxes.
  • Configurable setting to turn checks on/off at compile-time, on a per-OTP-app basis (so you have control over your dependencies) as well as your individual modules.
  • Hide named types from opaque types.
  • A way to define structs and their field types at the same time.
  • Finalize formatter specification and make a generator for this so that people can easily test their own formatters.

Pre-stable

Longer-term future ideas

  • Per-module or even per-spec settings to turn on/off, configure formatter, etc.

Installation

TypeCheck is available in Hex. The package can be installed by adding type_check to your list of dependencies in mix.exs:

def deps do
  [
    {:type_check, "~> 0.13.3"},
    # To allow spectesting and property-testing data generators (optional):
    {:stream_data, "~> 0.5.0", only: :test}, 
  ]
end

The documentation can be found at https://hexdocs.pm/type_check.

Formatter

TypeCheck exports a couple of macros that you might want to use without parentheses. To make mix format respect this setting, add import_deps: [:type_check] to your .formatter.exs file.

Changelog

The full changelog can be found here

TypeCheck compared to other tools

TypeCheck is by no means the other solution out there to reduce the number of bugs in your code.

Elixir's builtin typespecs and Dialyzer

Elixir's builtin type-specifications use the same syntax as TypeCheck. They are however not used by the compiler or the runtime, and therefore mainly exist to improve your documentation.

Besides documentation, extra external tools like Dialyzer can be used to perform static analysis of the types used in your application.

Dialyzer is an opt-in static analysis tool. This means that it can point out some inconsistencies or bugs, but because of its opt-in nature, there are also many problems it cannot detect, and it requires your dependencies to have written all of their typespecs correctly.

Dialyzer is also (unfortunately) infamous for its at times difficult-to-understand error messages.

An advantage that Dialyzer has over TypeCheck is that its checking is done without having to execute your program code (thus not having any effect on the runtime behaviour or efficiency of your projects).

Because TypeCheck adds @type, @typep, @opaque and @spec-attributes based on the types that are defined, it is possible to use Dialyzer together with TypeCheck.

Norm

Norm is an Elixir library for specifying the structure of data that can be used for both validation and data-generation.

On a superficial level, Norm and TypeCheck seem similar. However, there are important differences in their design considerations.

Is it any good?

yes

elixir-type_check's People

Contributors

0urobor0s avatar assimelha avatar baldwindavid avatar dependabot[bot] avatar dvic avatar jameslavin avatar kgautreaux avatar ktec avatar marcandre avatar orsinium avatar patrikstenmark avatar paulswartz avatar qqwy avatar skwerlman avatar tomekowal avatar trarbr avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

elixir-type_check's Issues

Question about Compilation Time

Hello there!

I enjoyed your talk at the Amsterdam Elixir meetup, and decided to give TypeCheck a try on our codebase. We have a few structs with a relatively large number of fields, and their old @type t definitions fully specified each field. Upon changing these to @type! and implementing a few @spec!s on functions that used them, we quickly found that our compilation time rose to 3–4 minutes.

For reference, the rough setup is:

A.t() :: %A{...[16 fields]...}
B.t() :: %B{...[8 fields]...}
C.t() :: %B{a: A.t(), b: B.t()}

With six function specifications on module C that reference all three types in various combinations.

So, the question: is this expected? I can imagine that the sheer number of fields on each struct, especially when nested, creates an explosion of code generation. In the documentation you mention:

In the case of for instance a large collection or a deeply-nested 'tree of structs' [runtime checks] might still be too slow.

Could this be true for compile time as well?

More generally, do you have any recommendations on strategies for type-checking structs? Perhaps the full specification of each field would make sense on a constructor function, but the canonical t type should stop at %__MODULE__{} with no fields.

Thanks for your work! Excited for more people to see it at ElixirConf EU.

Support finding spec definitions & checking types based on behaviours

A common pattern in Elixir is to define a function spec in a behaviour via a @callback attribute, and then modules implementing that behaviour put @impl true on the implementing functions rather than reproducing the full spec.

It would be awesome if TypeCheck could work with these attributes to generate runtime type checks the same way it does for @spec! and @type!. E.g. replace the attributes with probably something like @behaviour! and @callback!, and generate runtime checks for functions with @impl!.

Ideally this feature would allow for handling behaviours defined without TypeCheck. I'm guessing that would happen in a similar way to the existing overrides functionality, but probably would need some tweaks to play nicely with the callback logic.

Hypothetical example:

defmodule MyBehaviour do
  use TypeCheck
  @callback! my_func(integer(), atom()) :: :ok | {:error, integer()}
end

defmodule MyModule do
  use TypeCheck
  @behaviour! MyBehaviour

  @impl! true
  # Gets a runtime-generated type check as if it had the spec from the callback, e.g.
  # @spec! my_func(integer(), atom()) :: :ok | {:error, integer()}
  def my_func(the_integer, the_atom) do
    :ok
  end
end

Manually overriding property-checking generators for a type

In some instances, users will want to manually override the way values will be generated from their type.

This is mainly important when 'type guards' have been used that would thus filter away too much values that would otherwise be generated.

The considerations to keep in mind are:

  • The type should still work when StreamData is not available (because (i) StreamData might only be a dev/test dependency and (ii) consider a type that is defined in a library and that library is included in another project)
  • The type should be able to be persisted as part of code that is compiled. In other words: It should be able to be Macro.escape'd. This rules out anonymous functions. We thus probably need people to provide a function in &Mod.fun/1 format.
  • We need to build this in a way that keeps adding PropEr-support later possible without backwards-incompatible changes.

Issues using programatically generated types with guards

First of all, thank you for the great job in this library!

I'm trying to generate some types that depend on n. The example below compiles and works as expected.

defmodule MyTypes do
  use TypeCheck

  @type! address :: String.t() when byte_size(address) == 42

  # generate signed and unsigned int types
  for n <- Range.new(8, 256, 8) do
    @type! unquote({String.to_atom("int#{n}"), [], __MODULE__}) ::
             unquote(Macro.escape(-Integer.pow(2, n - 1)..(Integer.pow(2, n - 1) - 1)))

    @type! unquote({String.to_atom("uint#{n}"), [], __MODULE__}) ::
             unquote(Macro.escape(0..(Integer.pow(2, n) - 1)))
  end

  # generate bytes types
  for n <- 1..32 do
    name = {String.to_atom("bytes#{n}"), [], __MODULE__}

    # when byte_size(unquote(name)) <= unquote(n)
    @type! unquote(name) :: binary()
  end
end
defmodule MyTypesUse do
  use TypeCheck
  alias MyTypes

  @spec! func_int(MyTypes.int32()) :: nil
  def func_int(x) do
    nil
  end

  @spec! func_bytes(MyTypes.bytes32()) :: nil
  def func_bytes(x) do
    nil
  end
end

However, the moment I add a guard to the bytes{n} types, it fails. Changing the bytes lines to

defmodule MyTypes do 
# ...
  # generate bytes types
  for n <- 1..32 do
    name = {String.to_atom("bytes#{n}"), [], __MODULE__}

    @type! unquote(name) :: binary() when byte_size(unquote(name)) <= unquote(n)
  end
end

Results in a compilation error:

== Compilation error in file lib/my_types.ex ==
** (CompileError) lib/type_check/spec.ex:23: undefined function bytes32/0
    (elixir 1.12.1) src/elixir_locals.erl:114: anonymous fn/3 in :elixir_locals.ensure_no_undefined_local/3
    (stdlib 3.15) erl_eval.erl:685: :erl_eval.do_apply/6
    (elixir 1.12.1) lib/kernel/parallel_compiler.ex:319: anonymous fn/4 in Kernel.ParallelCompiler.spawn_workers/7

Notice that MyTypes compiles correctly. The compilation error only happens when we try to use it in a spec. It happens not only with remote types, but in the same module the types were defined too. If you move func_bytes to MyTypes

defmodule MyTypes do 
# ...
  @spec! func_bytes(bytes32()) :: nil
  def func_bytes(x) do
    nil
  end
# ...
end

you get

== Compilation error in file lib/my_types.ex ==
** (CompileError) lib/my_types.ex:19: imported TypeCheck.Internals.UserTypes.MyTypes.bytes32/0 conflicts with local function
    (elixir 1.12.1) src/elixir_locals.erl:94: :elixir_locals."-ensure_no_import_conflict/3-lc$^0/1-0-"/2
    (elixir 1.12.1) src/elixir_locals.erl:95: anonymous fn/3 in :elixir_locals.ensure_no_import_conflict/3
    (stdlib 3.15) erl_eval.erl:685: :erl_eval.do_apply/6
    (elixir 1.12.1) lib/kernel/parallel_compiler.ex:319: anonymous fn/4 in Kernel.ParallelCompiler.spawn_workers/7

I'm not sure whether it's a but or an error in my assumptions. I'm willing to contribute to helping solve these if it's indeed a bug, just need to discuss it first as I'm not familiar with the internals.

Credo warns about missing spec when using @spec!

for this function head:

  @spec! distance(String.t(), String.t()) :: integer()
  def distance(a, b)

credo gives this warning:

  Code Readability
┃ 
┃ [R] → Functions should have a @spec type specification.lib/utils/damerau_levenshtein.ex:32:7 #(Utils.DamerauLevenshtein.distance)

Support binary pattern-matches containing size-references like `<<_ :: size>>`

I think the majority of the unsupported functionality is represented in issues other than binary pattern-matches containing size-references like <<_ :: size>> so I am adding here.

A practical example of where this would be useful is for specifying a UUID. I'm currently using the following custom type as a placeholder.

@type! uuid :: utf8_binary()

utf8_binary itself is a placeholder until something like String.t is introduced. If binary pattern-matches with size references were supported I believe this could be changed to...

@type! uuid :: <<_::128>>

This is based on the type from Ecto.UUID at https://hexdocs.pm/ecto/Ecto.UUID.html#t:raw/0

Can't compile dependency

mix deps.compile type_check                                                                                                                                                                                                 1 ↵ fuelen@arch
==> type_check
Compiling 39 files (.ex)

== Compilation error in file lib/type_check/type.ex ==
** (CompileError) lib/type_check/type.ex:18: cannot import TypeCheck.Builtin.|/2 because it conflicts with Elixir special forms
    (elixir 1.11.0-rc.0) src/elixir_import.erl:96: :elixir_import.calculate/6
    (elixir 1.11.0-rc.0) src/elixir_import.erl:24: :elixir_import.import/4
elixir 1.11.0-rc.0-otp-22
erlang 22.3.4.10

Support with multiple arity functions

I have trouble with functions with different arity. I added type checks to all of them and now I get this weird error. When I check only for %{} I don't get an error and everything works as expected.

  @doc false
  @spec! validate(%Ingest{} | %{}) :: %Ecto.Changeset{}
  def validate(%Ingest{} = ingest) do
    validate(ingest, %{})
  end

  def validate(attrs) do
    validate(%__MODULE__{}, attrs)
  end

  @spec! validate(%Ingest{} | %__MODULE__{}, %{}) :: %Ecto.Changeset{}
  def validate(%Ingest{} = ingest, attrs) do
    from_model(ingest) |> validate(attrs)
  end

  def validate(%__MODULE__{} = ingest, attrs) do
    {ingest, @types}
    |> cast(attrs, Map.keys(@types))
    |> validate_required(@required)
    |> validate_length(:name, min: 2, max: 100)
  end


  @spec! from_model(%Ingest{} | %__MODULE__{}) :: %__MODULE__{}
  def from_model(%__MODULE__{} = ingest), do: ingest

  def from_model(%Ingest{} = ingest) do
    m = Map.from_struct(ingest)
    struct(__MODULE__, m)
  end

It should be returning Changeset. In another part of my code I'm checking for Changesets and it works as expected.

I'm just testing it after I heard about it on the Thinking Elixir podcast. So far I'm happy how it works.

Dynamic @spec! via defmacro

I am attempting to write a macro that dynamically writes a function. Additionally, I would like the corresponding @spec! to be written above it. This is referenced in a thread in ElixirForum where you mention posting as an issue here.

This is a stripped down example where I have a macro that is dynamically named. The macro (with @spec! commented out) is the following:

defmodule MacroSpec do
  defmacro my_macro(name) do
    quote bind_quoted: [name: name] do
      # @spec! unquote(:"my_function_#{name}")(binary) :: binary
      def unquote(:"my_#{name}")(greeting) do
        IO.inspect(unquote(name))
        IO.inspect(greeting)
        greeting
      end
    end
  end
end

This module is then imported to MyModule and my_macro is called.

defmodule MyModule do
  import MacroSpec

  MacroSpec.my_macro("greeter")
end

The my_greeter function can then be called with a string.

MyModule.my_greeter("hello world!")

This all works fine, but breaks down when I uncomment the @spec!. The syntax that is there right now clearly won't work at all because it is trying to evaluate that line and trips over the ::, but I am posting this way to give you the best idea of what it is trying to achieve. I can use TypeCheck, but I don't believe I want it even caring about TypeCheck during compilation of the macro.

I've tried various things like using Macro.escape and quoting the entire line. I'm pretty sure this just comes down to me not understanding how to quote/unquote/escape the right parts of the spec rather than anything specific to TypeCheck, but posting here in case there is something unique.

Unused variable warning for types with guards

It seems that referencing a custom type that has a guard clause results in an unused variable compiler warning. A stripped down example:

@type! some_type :: value :: any() when is_binary(value)
@spec! some_fun(some_type()) :: binary()
def some_fun(value) do
  value
end

The resulting warning...

warning: variable "some_type" is unused (if the variable is not meant to be used, prefix it with an underscore)

This seems to have been introduced in 0.2.1 as I'm not seeing the warnings in 0.2.0.

Unable to reference __MODULE__ in type declarations

It appears that referencing __MODULE__ in a @type! declaration results in a specification that can never be met. For example, suppose t is declared as a type...

defmodule Typetest do
  use TypeCheck

  @type! t :: %__MODULE__{name: String.t()}
  defstruct [:name]

  @spec! hello(String.t()) :: t()
  def hello(name) do
    %__MODULE__{name: name}
  end
end

Calling Typetest.hello("Jane") results in the following error:

** (TypeCheck.TypeError) The call to `hello/1` failed,
   because the returned result does not adhere to the spec `Typetest.t`.
   Rather, its value is: `%Typetest{name: "Jane"}`.
   Details:
     The result of calling `hello("Jane")`
     does not adhere to spec `hello(String.t()) :: Typetest.t`. Reason:
       Returned result:
         `%Typetest{name: "Jane"}` does not match the definition of the named type `Typetest.t`
         which is: `Typetest.t
         ::
         %{__struct__: TypeCheck.Internals.UserTypes.Typetest, name: String.t()}`. Reason:
           `%Typetest{name: "Jane"}` does not check against `%{__struct__: TypeCheck.Internals.UserTypes.Typetest, name: String.t()}`. Reason:
             under key `:__struct__`:
               `Typetest` is not the same value as `TypeCheck.Internals.UserTypes.Typetest`.

It seems that TypeCheck has converted it to an internal module and is then comparing the module against that. Changing the type to @type! t :: %Typetest{name: String.t()} works, but referencing __MODULE__ I believe to be quite common.

Allow people to write 'type-level functions'

In certain cases, it can be very useful to generate types programmatically, potentially based on other pre-existing types.

It would be very nice if people were able to write their own type-level functions akin to the ones currently in TypeCheck.Builtin.

However, this is not a trivial tasks, as currently types are evaluated in a bit of a special context to make sure they can already be used at compile-time inside the module that is being compiled.

To do this we either (a) need access to the imports of the main module, or (b) need to be able to provide special/extra imports to the type-eval module itself. What the best way to do this might be is still up for debate.

Improve test coverage

The library already has quite a few tests, but before we reach a stable release, we at least need to make sure to cover also:

  • All builtin types
    • The return results of their successful/unsuccessful checks (therefore testing if the checking-code itself works).
    • Their StreamData generators.
    • That they can be represented correctly when inspected (which is paramount for the error formatting to be correct)
  • All paths of the default formatter.
  • All of the functions/macros in TypeCheck and TypeCheck.Type.

Using @spec! allows calling private functions

I was surprised to find that adding @spec! to a private function exposes that function publicly.

defmodule Typetest do
  use TypeCheck

  @spec! hello :: any()
  defp hello, do: "world"
end

# This works...
Typetest.hello()

I noticed it when changing a public function to private and seeing that my test of the function still somehow worked.

** (FunctionClauseError) no function clause matching in Inspect.Stream.inspect/2

defmodule Laboratory do
  use TypeCheck

  @spec! foo(%Stream{}) :: list
  def foo (random_sequence) do
    Enum.take(random_sequence, 100)
  end

  @spec! baz() :: %Stream{}
  def baz() do

    :rand.seed(:exrop, {1, 2, 3})

    random_sequence =
    Stream.repeatedly(&:rand.uniform/0)

    foo(random_sequence)
  end
end

Error:

iex(1)> Laboratory.baz
** (FunctionClauseError) no function clause matching in Inspect.Stream.inspect/2    
    
    The following arguments were given to Inspect.Stream.inspect/2:
    
        # 1
        %Inspect.Error{
          message: "got FunctionClauseError with message \"no function clause matching in Inspect.Stream.inspect/2\" while inspecting %{__struct__: Stream}"
        }
    
        # 2
        %Inspect.Opts{
          base: :decimal,
          binaries: :infer,
          char_lists: :infer,
          charlists: :infer,
          custom_options: [],
          inspect_fun: &TypeCheck.Protocols.Inspect.inspect/2,
          limit: 50,
          pretty: false,
          printable_limit: 4096,
          safe: true,
          structs: true,
          syntax_colors: [],
          width: 80
        }
    
    Attempted function clauses (showing 1 out of 1):
    
        def inspect(%{enum: enum, funs: funs}, opts)
    
    (elixir 1.10.3) lib/stream.ex:1637: Inspect.Stream.inspect/2
    (type_check 0.3.1) lib/type_check/protocols/inspect.ex:52: TypeCheck.Inspect.inspect/2
    (type_check 0.3.1) lib/type_check/protocols/inspect.ex:57: TypeCheck.Inspect.inspect_binary/2
    (type_check 0.3.1) lib/type_check/type_error/default_formatter.ex:212: TypeCheck.TypeError.DefaultFormatter.do_format/1
    (type_check 0.3.1) lib/type_check/type_error/default_formatter.ex:6: TypeCheck.TypeError.DefaultFormatter.format/2
    (type_check 0.3.1) lib/type_check/type_error.ex:55: TypeCheck.TypeError.exception/1
    (arbit 0.1.0) lib/Laboratory.ex:1: Laboratory.foo/1
    (arbit 0.1.0) lib/Laboratory.ex:9: Laboratory.baz/0

Performance / Disabling Per-environment

This has been discussed somewhat, but wondering how this might impact a large codebase. I believe there was also some discussion of allowing the checks to be disabled in production to avoid any extra cost. Perhaps an option like this would be a nice generalized way to specify whether checks could be disabled at runtime.

use TypeCheck, enable_runtime_checks: mix.env != :prod # default `true`

External types

Interesting and promising library.

I imagine there's a way to deal with types defined in dependencies?

** (UndefinedFunctionError) function Plug.Conn.t/0 is undefined or private

Is there a package for popular packages, e.g. phoenix?

Hiding named types when used within opaque types

When we have a type like opaque foo :: {x :: integer(), integer()} we don't want x to escape from this scope.
The reason for this being that it is an implementation detail of an opaque type.

ElixirLS Dialyzer (VS code) warning : The pattern 'false' can never match the type 'true'

Hi, first I want to thank you for this great library , I have some issue with Dialyzer warning but I'm not sure why , here's my settings:

Mix.exs:

      {:type_check, github: "Qqwy/elixir-type_check"}

Elixir vers:
1.12.2-otp-24

ElixirLS vs code:
0.8.1

Code:

defmodule Document.ExcelTest do
  use TypeCheck
  alias Document.ExcelTest

  defstruct [
    :name,
    :age,
    :phone
  ]

  @type! t() :: %ExcelTest{
           name: String.t(),
           age: integer(),
           phone: String.t()
         }
end
defmodule Document.Doc do

  use TypeCheck
  alias Document.ExcelTest

  @spec! parse_list(any) :: ExcelTest.t()
  def parse_list(list) do

    column = [
      :name,
      :age,
      :phone
    ]

    Enum.zip(column, list)
    |> Enum.into(%{})
    |> then( fn map -> struct(%ExcelTest{}, map) end)

  end
end

the code can be compiled and run , but there' warning from dialyzer ;

screenshot

Is there something wrong with my code ?

Rethink approach to fixed-list-syntax

TypeCheck allows for fixed-list types like [1, 2], which Elixir's builtin Typespecs do not allow.
On the other hand, syntaxes like [type, ...] are not supported at all, and [type] means something different in TypeCheck from the builtin Typespecs.

While this is a nice feature to have, it might also be confusing for some.

So this requires some thought.

Maybe we can warn on usage of literal lists, always preferring people to use list(...) or fixed_list(...) instead?

Inability to import shared types upgrading from 0.4 to 0.5

In moving from 0.4 to 0.5 (and up) it seems that I can no longer import shared types. Example:

defmodule Typetest.MyTypes do
  use TypeCheck

  @type! a_name :: binary()
end
defmodule Typetest do
  use TypeCheck
  import Typetest.MyTypes

  @spec! hello(a_name()) :: binary()
  def hello(name) do
    "Hello, #{name}"
  end
end

This works in 0.4, but the following error happens in 0.5 and up...

== Compilation error in file lib/typetest.ex ==                                                                                                                                                                  
  9 ** (CompileError) lib/typetest.ex:1: type a_name/0 undefined (no such type in Typetest)                                                                                                                          
  8     (elixir 1.12.2) lib/kernel/typespec.ex:925: Kernel.Typespec.compile_error/2                                                                                                                                  
  7     (stdlib 3.15.2) lists.erl:1358: :lists.mapfoldl/3                                                                                                                                                            
  6     (elixir 1.12.2) lib/kernel/typespec.ex:977: Kernel.Typespec.fn_args/5                                                                                                                                        
  5     (elixir 1.12.2) lib/kernel/typespec.ex:963: Kernel.Typespec.fn_args/6                                                                                                                                        
  4     (elixir 1.12.2) lib/kernel/typespec.ex:390: Kernel.Typespec.translate_spec/8                                                                                                                                 
  3     (stdlib 3.15.2) lists.erl:1358: :lists.mapfoldl/3                                                                                                                                                            
  2     (elixir 1.12.2) lib/kernel/typespec.ex:236: Kernel.Typespec.translate_typespecs_for_module/2

If I add that same @type directly within the file while still importing Types, the resulting error is clear that the shared one is imported...

defmodule Typetest do
  use TypeCheck
  import Typetest.MyTypes

  @type! a_name :: binary()

  @spec! hello(a_name()) :: binary()
  def hello(name) do
    "Hello, #{name}"
  end
end
  6 == Compilation error in file lib/typetest.ex ==                                                                                                                                                                  
  5 ** (CompileError) lib/typetest.ex:7: function a_name/0 imported from both TypeCheck.Internals.UserTypes.Typetest and Typetest.MyTypes, call is ambiguous                                                         
  4     (type_check 0.5.0) expanding macro: TypeCheck.Macros.__before_compile__/1                                                                                                                                    
  3     lib/typetest.ex:1: Typetest (module)                                                                                                                                                                         
  2     (elixir 1.12.2) lib/kernel/parallel_compiler.ex:319: anonymous fn/4 in Kernel.ParallelCompiler.spawn_workers/7  

Is there a different method in order to share types via imports now? I'd rather not alias some very commonly used types.

Dialyzer seems unhappy with certain parameter type usage

@Qqwy I found a case, where the bug (c.f. #85 / #93 ) seems to persist...

### this module works fine, because of `any` in spec: `@spec! from_db(any) :: t()`
defmodule Types.Ad do
  use TypeCheck
  defstruct id: nil, consumer_price: nil

  @type! t :: %Types.Ad{
           id: number(),
           consumer_price: number()
         }

  @spec! from_db(any) :: t()
  def from_db(row) do
    %Types.Ad{
      id: Map.get(row, "id")
    }
  end
end

### this fails, because of `map` in spec: `@spec! from_db(map) :: t()`
defmodule Types.Ad3 do
  use TypeCheck
  defstruct id: nil

  @type! t :: %Types.Ad3{
           id: number()
         }

  @spec! from_db(map) :: t()
  def from_db(row) do
    %Types.Ad3{}
  end
end

ERROR message:

lib/type_check/spec.ex:22:call_without_opaque
Function call without opaqueness type mismatch.

Call does not have expected term of type
  {{TypeCheck.Builtin.Map.t()
    | %{
        :__struct__ => atom(),
        :choices => [any()],
        :element_type => _,
        :element_types => [any()],
        :keypairs => [any()],
        :local => boolean(),
        :name => atom(),
        :range => map(),
        :type => _,
        :value => _
      }, atom(),
    %{
      :expected_length => non_neg_integer(),
      :expected_size => integer(),
      :index => integer(),
      :key => _,
      :keys => [any()],
      :problem => _,
      :problems => [any()]
    }, _}, nil | maybe_improper_list() | map()}
  | {TypeCheck.Builtin.Map.t()
     | %{
         :__struct__ => atom(),
         :choices => [any()],
         :element_type => _,
         :element_types => [any()],
         :keypairs => [any()],
         :local => boolean(),
         :name => atom(),
         :range => map(),
         :type => _,
         :value => _
       }, atom(),
     %{
       :expected_length => non_neg_integer(),
       :expected_size => integer(),
       :index => integer(),
       :key => _,
       :keys => [any()],
       :problem => _,
       :problems => [any()]
     }, _}
 (with opaque subterms) in the 1st position.

TypeCheck.TypeError.exception(
  {{%TypeCheck.Spec{
      :location => {<<_::464>>, 30},
      :name => :from_db,
      :param_types => [
        %TypeCheck.Builtin.Map{:key_type => map(), :value_type => map()},
        ...
      ],
      :return_type => %TypeCheck.Builtin.NamedType{
        :local => false,
        :name => <<_::88>>,
        :type => %TypeCheck.Builtin.FixedMap{:keypairs => [any(), ...]}
      }
    }, :param_error,
    %{
      :index => 0,
      :problem =>
        {%TypeCheck.Builtin.Map{
           :key_type => %TypeCheck.Builtin.Any{},
           :value_type => %TypeCheck.Builtin.Any{}
         }, :not_a_map, %{}, _}
    }, [any(), ...]}, [{:file, <<_::464>>} | {:line, 22}, ...]}
)

________________________________________________________________________________
done (warnings were emitted)
Halting VM with exit status 2

I guess there is still some fishiness going on. Hope this helps a bit to narrow down this issue.

Originally posted by @mindreframer in #93 (comment)

Various issues with bitstring types

Hi

First of all, thank you for all your hard work on this excellent library :)

I am experimenting with the use of TypeCheck for specifying types and specs for a library that does a lot of binary decoding and encoding (Blue Heron, a Bluetooth library). I would like to bring in TypeCheck for documentation, runtime checks and test data generation.

This is my first time really using TypeCheck, and I have run into a few issues. I am not sure if it's because I am doing it wrong, or because of bugs. Take the following code as an example:

defmodule Diacheck.Frame do
  use TypeCheck

  @type! status() :: byte()

  @spec! decode(<<_::8>>) :: status()
  def decode(<<status>>) do
    status
  end
end

The decode function extracts an integer in range 0..255 from the input, which must be a bitstring with a length 8 bits.

If I run Diacheck.Frame.decode(<<40>>), that works fine. But if I run Diacheck.Frame.decode(<<40, 1>>) I get the following error:

** (Protocol.UndefinedError) protocol String.Chars not implemented for {:doc_cons, {:doc_color, "8", :yellow}, {:doc_color, :doc_nil, :red}} of type Tuple. This protocol is implemented for the following type(s): Atom, BitString, Date, DateTime, Float, Integer, List, NaiveDateTime, Time, URI, Version, Version.Requirement
    (elixir 1.13.2) lib/string/chars.ex:3: String.Chars.impl_for!/1
    (elixir 1.13.2) lib/string/chars.ex:22: String.Chars.to_string/1
    (type_check 0.10.7) lib/type_check/builtin/sized_bitstring.ex:45: TypeCheck.Protocols.Inspect.TypeCheck.Builtin.SizedBitstring.inspect/2
    (type_check 0.10.7) lib/type_check/protocols/inspect.ex:101: TypeCheck.Inspect.inspect/2
    (type_check 0.10.7) lib/type_check/protocols/inspect.ex:118: TypeCheck.Inspect.inspect_binary/2
    (type_check 0.10.7) lib/type_check/type_error/default_formatter.ex:267: TypeCheck.TypeError.DefaultFormatter.do_format/1
    (type_check 0.10.7) lib/type_check/type_error/default_formatter.ex:6: TypeCheck.TypeError.DefaultFormatter.format/2
    (type_check 0.10.7) lib/type_check/type_error.ex:57: TypeCheck.TypeError.exception/1

Also, if I run mix dialyzer in the project, it complains like this:

Compiling 4 files (.ex)
Generated diacheck app
Finding suitable PLTs
Checking PLT...
[:compiler, :elixir, :iex, :kernel, :logger, :stdlib, :type_check]
Looking up modules in dialyxir_erlang-24.2_elixir-1.13.2_deps-dev.plt
Looking up modules in dialyxir_erlang-24.2_elixir-1.13.2.plt
Finding applications for dialyxir_erlang-24.2_elixir-1.13.2.plt
Finding modules for dialyxir_erlang-24.2_elixir-1.13.2.plt
Checking 449 modules in dialyxir_erlang-24.2_elixir-1.13.2.plt
Finding applications for dialyxir_erlang-24.2_elixir-1.13.2_deps-dev.plt
Finding modules for dialyxir_erlang-24.2_elixir-1.13.2_deps-dev.plt
Copying dialyxir_erlang-24.2_elixir-1.13.2.plt to dialyxir_erlang-24.2_elixir-1.13.2_deps-dev.plt
Looking up modules in dialyxir_erlang-24.2_elixir-1.13.2_deps-dev.plt
Checking 449 modules in dialyxir_erlang-24.2_elixir-1.13.2_deps-dev.plt
Adding 335 modules to dialyxir_erlang-24.2_elixir-1.13.2_deps-dev.plt
done in 0m54.34s
No :ignore_warnings opt specified in mix.exs and default does not exist.

Starting Dialyzer
[
  check_plt: false,
  init_plt: '/Users/trarbr/diacheck/_build/dev/dialyxir_erlang-24.2_elixir-1.13.2_deps-dev.plt',
  files: ['/Users/trarbr/diacheck/_build/dev/lib/diacheck/ebin/Elixir.Diacheck.Frame.beam',
   '/Users/trarbr/diacheck/_build/dev/lib/diacheck/ebin/Elixir.Diacheck.FrameOld.beam',
   '/Users/trarbr/diacheck/_build/dev/lib/diacheck/ebin/Elixir.Diacheck.Type.beam',
   '/Users/trarbr/diacheck/_build/dev/lib/diacheck/ebin/Elixir.Diacheck.beam',
   '/Users/trarbr/diacheck/_build/dev/lib/diacheck/ebin/Elixir.TypeCheck.Internals.UserTypes.Diacheck.Frame.beam',
   ...],
  warnings: [:unknown]
]
Total errors: 1, Skipped: 0, Unnecessary Skips: 0
done in 0m0.39s
lib/type_check/spec.ex:1:call_without_opaque
Function call without opaqueness type mismatch.

Call does not have expected term of type
  {{TypeCheck.Builtin.Map.t()
    | %{
        :__struct__ => atom(),
        :choices => [any()],
        :element_type => _,
        :element_types => [any()],
        :keypairs => [any()],
        :local => boolean(),
        :name => atom(),
        :range => map(),
        :type => _,
        :value => _
      }, atom(),
    %{
      :expected_length => non_neg_integer(),
      :expected_size => integer(),
      :index => integer(),
      :key => _,
      :keys => [any()],
      :problem => _,
      :problems => [any()]
    }, _}, nil | maybe_improper_list() | map()}
  | {TypeCheck.Builtin.Map.t()
     | %{
         :__struct__ => atom(),
         :choices => [any()],
         :element_type => _,
         :element_types => [any()],
         :keypairs => [any()],
         :local => boolean(),
         :name => atom(),
         :range => map(),
         :type => _,
         :value => _
       }, atom(),
     %{
       :expected_length => non_neg_integer(),
       :expected_size => integer(),
       :index => integer(),
       :key => _,
       :keys => [any()],
       :problem => _,
       :problems => [any()]
     }, _}
 (with opaque subterms) in the 1st position.

TypeCheck.TypeError.exception(
  {{%TypeCheck.Spec{
      :location => {<<_::528>>, 6},
      :name => :decode,
      :param_types => [
        %TypeCheck.Builtin.SizedBitstring{:prefix_size => 8, :unit_size => nil},
        ...
      ],
      :return_type => %TypeCheck.Builtin.NamedType{
        :local => false,
        :name => <<_::184>>,
        :type => %TypeCheck.Builtin.Range{:range => map()}
      }
    }, :param_error,
    %{
      :index => 0,
      :problem =>
        {%TypeCheck.Builtin.SizedBitstring{:prefix_size => 8, :unit_size => nil},
         :no_match | :wrong_size, %{}, _}
    }, [any(), ...]}, [{:file, <<_::528>>} | {:line, 1}, ...]}
)

________________________________________________________________________________
done (warnings were emitted)
Halting VM with exit status 2

If I remove the bang from spec!, mix dialyzer finds no errors.

I also noted one more thing. If I specify the binary literal in the wrong way (e.g. <<_::_>> instead of <<_::8>>), the error says that the size parameter specifies the byte-size of he bitstring, but I do believe it should be the bit-size of the bitstring.

== Compilation error in file lib/diacheck/frame.ex ==
** (TypeCheck.CompileError) TypeCheck does not support the bitstring literal `<<_::_>>`
Currently supported are:
- <<>> -> empty bitstring
- <<_ :: size >> -> a bitstring of exactly `size` bytes long <---- This should say bits long instead bytes long
- <<_ :: _ * unit >> -> a bitstring whose length is divisible by `unit`.
- <<_ :: size, _ * unit >> -> a bitstring whose (length - `size`) is divisible by `unit`.

    (type_check 0.10.7) lib/type_check/internals/pre_expander.ex:137: TypeCheck.Internals.PreExpander.rewrite/3
    (type_check 0.10.7) lib/type_check/type.ex:81: TypeCheck.Type.build_unescaped/4
    (elixir 1.13.2) lib/enum.ex:1593: Enum."-map/2-lists^map/1-0-"/2
    (type_check 0.10.7) lib/type_check/macros.ex:236: anonymous fn/3 in TypeCheck.Macros.create_spec_defs/3
    (elixir 1.13.2) lib/enum.ex:2396: Enum."-reduce/3-lists^foldl/2-0-"/3
    (type_check 0.10.7) lib/type_check/macros.ex:232: TypeCheck.Macros.create_spec_defs/3
    (type_check 0.10.7) expanding macro: TypeCheck.Macros.__before_compile__/1
    lib/diacheck/frame.ex:1: Diacheck.Frame (module)

Turn off (or alter other options of) TypeCheck in dependencies

When TypeCheck is in use in a dependency of your application, you might want to alter how it works.
Maybe there are extra overrides you want to provide, or maybe you want to enable debug mode, or maybe turn it off all-together in a particular environment.

One approach to do this might be to have TypeCheck.Options.new look at Application.compile_env(Application.get_application(caller), :type_check).

Think about if it might at all be possible to support `@type t :: %__MODULE__{}` even above a `defstruct`

With the older checking implementation, this 'sort of' worked but silently did something not fully according to Elixir's typespec rules.

With the new checking implementation of the last minor revision, struct types were broken (c.f. #78 ).

The fix (#82) uncovered a related issue however: It is very common to write something like

defmodule User do
  use TypeCheck

  @type! t :: %__MODULE__{name: String.t(), age: integer()}
  defstruct [:name, :age]
end

however this will currently fail with a compiler error.

What works, is:

defmodule User do
  use TypeCheck

  defstruct [:name, :age]
  @type! t :: %__MODULE__{name: String.t(), age: integer()}
end

but I believe most style guides prefer the former, so a lot of code in the wild will use the former.


I'm not sure what, if at all, we can change to accommodate this, but it needs some thought.

Error: misplaced operator ::/2

The module below does not compile but throws an error:
** (CompileError) .../account.ex:12: misplaced operator ::/2 (the @type!)

Same error for the @spec! when I comment the @type!.

defmodule App.Accounts.Account
  use Ecto.Schema

  import Ecto.Changeset

  alias App.Accounts.Account

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id
  @timestamps_opts [type: :utc_datetime_usec]

  @type! t :: %Account{
           title: binary()
         }

  schema "accounts" do
    field :title, :string

    timestamps()
  end

  @doc false
  @spec! changeset(account :: t(), attrs :: map()) :: %Ecto.Changeset{}
  def changeset(%Account{} = account, attrs) do
    account
    |> cast(attrs, [:title])
    |> validate_required([:title])
  end
end

Similar code has worked before, so what am I doing wrong here?

Make the generation of type-checks configurable

In some situations, people might want to turn off the type-checks to improve performance.

Therefore, it would be nice to b able to:

  • Configure TypeCheck to turn on/off all typechecks.
  • Be able to override this at the module-level
  • Maybe be able to override this at the spec-level.
  • Possibly be able to have an option to defer picking between yes/no until runtime (at slightly reduced performance), so that type-checks can be turned on/off at runtime without requiring recompilation?

`Date` and `DateTime` accept any map

It seems that a spec with Date or DateTime accept anything that is a map. As an example, all of the below assertions work.

defmodule TypetestTest do
  use ExUnit.Case
  use TypeCheck

  defmodule User do
    defstruct []
  end

  @spec! run(Date.t()) :: true
  def run(_), do: true

  test "check types" do
    assert run(Date.utc_today())
    assert run(DateTime.utc_now())
    assert run(%User{})
    assert run(%{})

    assert_raise TypeCheck.TypeError, fn -> run("some string") end
    assert_raise TypeCheck.TypeError, fn -> run(6) end
    assert_raise TypeCheck.TypeError, fn -> run([]) end
  end
end

Should keyword list be builtin?

I attempted to use keyword() in a spec and got a compilation error of undefined function keyword/0 as I thought it might be built in. I then tried to create a custom type of the same name and got the compilation error of type keyword/0 is a built-in type and it cannot be redefined. I am a little confused by these compilation errors.

I ended up making adding a type of a slightly different name:

type keyword_list :: list({atom(), any()})

That works fine, but wasn't sure if this was expected behavior.

Make error formatter configurable

The formatter itself has already been split into a separate module to allow this in the future.

However, we still need to provide a configuration setting. My current idea is to:

  • Have a global setting
  • Be able to override it per module
  • Be able to override it per spec
  • Be able to override it by passing in explicit options to conform and friends.

The other thing we'd want is to make new formatters test-able. For this, also see #3 . We have a Formatter.problem_tuple type that will (once #3 is done) contain all possible problems that might occur for all wrong typechecks, therefore being a generator of all potential inputs of the formatter.
This will make it easy to property-test custom formatters.

Crazy idea: Recompile bootstrapping modules to dogfood TypeCheck

TypeCheck already uses itself wherever possible (most importantly inside the TypeCheck.Builtin.* modules and the TypeCheck.TypeError.Formatter module) to ensure self-consistency of the library.

There are a couple of places where this is however not possible, as it would introduce circular dependencies into the library (such as TypeCheck.Builtin itself, TypeCheck.TypeError, of course TypeCheck.Macros and a couple more).

However, one thing we could potentially do, is to:

  • compile the bootstrapping modules
  • compile the rest of the library
  • re-read the source code of the bootstrapping modules, modifying their AST so that they include a use TypeCheck call as well as replacing @spec, @type etc. with @spec!, @type! etc.
  • Recompile the modules using Module.create.

This idea might be a little crazy and maybe there is a detail that makes this impossible, but it definitely is something interesting to try.

Don't hang on recursive types

This project is reaching relative maturity, so it makes sense to formalize some of the remaining outstanding problems as issues.

Currently, TypeCheck does not handle recursive types. A type that refers to itself (directly or indirectly) will cause the call to the ToCheck protocol (or actually before that in the TypeCheck.Type.build_unescaped call which should resolve the type to a struct) to hang in an infinite loop, as the type is expanded ad infinitum.

I see the following possible approaches:

  1. Stop type expansion after a certain large limit (like 255 calls deep) and after that just accept anything. This would be very simple to implement and always work. However, its results might be surprising (although deep recursive type definitions are not that common in Elixir) if the limit is ever reached.
  2. Make all types lazy always. This would resolve the problem, but would make it impossible to make the type-checking fast for all cases where it is not required. So let's not do that.
  3. Introduce a new type, lazy/1, which will lazily perform its internal check. This type will need to be added manually by the user.
  • We can however raise in e.g. build_unescaped after a certain depth is reached to prevent a user-unfriendly infinite loop and instead hint at the user what the problem may be so they can resolve it.
  • The implementation of this is however difficult. I've tried to implement something on the lazy branch. In essence, lazy would need to be a macro that would take its AST and only expand+call ToCheck on the inner type inside the call to lazy. I see two possibilities:
    1. Allow lazy to define a new named (but hidden) function in the module (if it did not exist yet), which is called inside ToCheck, making the calls to the internal checks live on the normal BEAM call stack. This should be reasonably fast, but does require to define extra functions, which is not possible when using the type dynamically.
    2. Call Code.eval_quoted inside lazy on the given AST. This 'works' but making sure that we're in the correct environment and are able to see all other functions that are defined is difficult. We can not resort to using __CALLER__ since lazy/1 is called in the module while it is being defined, meaning that we do not have access to types being defined later in the same module, so some kind of different method to do this is necessary. Hrm.

Warnings with very basic case

First: this package looks very promising and I saw how @baldwindavid is using it for his application. This motivated me to play with TypeCheck in my spare time.

Somehow I get warnings for the most basic example(s) and I am wondering what I'm doing wrong or missing out...
The same code with normal specs is accepted by Dialyzer, yet is rejected when used with @spec! / @type! macros...

Here is the repo with the failing code:

defmodule Input1 do
  use TypeCheck
  defstruct [:to, :from]

  @type! t :: %Input1{from: binary()}
  @spec! run(t()) :: boolean()
  def run(input) do
    IO.inspect(input.from)
    true
  end
end

Using latest released version vs. current master branch seems not to make any difference.

So the question would be: is this expected behaviour? The compiled code works OK, it is just that my editor is full with warnings and the message text is very verbose.

Inside quotes, `@` cannot be used

This was encountered as part of #36.
During my investigation I found that Elixir does not seem to use a custom implementation of @ when inside a quote inside a macro.
This might be something that needs to be fixed upstream (it might of course also be possible that I made a mistake). For this the following upstream issue was made: elixir-lang/elixir#10497

This issue exists to not forget about this problem and continue working on once the upstream issue is resolved (one way or another 🙃 ).

Making spec look close to @spec

I'm starting to try this out and wondering if what I'm seeing in terms of interaction with the formatter and compiler is the same for you. I have the following spec:

spec get_contact_request(uuid, list(function) | function) :: nil | ContactRequest.t()

def get_contact_request(id, opts \\ []) do

A few notes:

  1. The formatter respects spec without parens after I added locals_without_parens: [type: 1, typep: 1, opaque: 1, spec: 1] to .formatter.exs. Curiously, adding :type_check to :import_deps did not seem to work for me so needed to add them directly.
  2. One issue I run into is that the compiler still warns that there are missing parens on function and uuid (my own type) so perhaps I'll either need to add parens or live with the compiler warnings.
  3. The formatter always adds an empty line between spec and my function. Not sure there is any way around this, but it would feel like more of a regular @spec if this newline weren't necessary.
  4. The formatter always adds parens for .t (example ContactRequest.t()). Not a big deal, but it seems like your examples display without parens so wondering if there is any configuration you've done to allow this.

Possibility of making structs type-safe?

defstruct is in the end syntactic sugar around a definition of __struct__/0 (which creates an empty 'default' version of the struct map) and __struct__/1 (which merges the given enumerable into an existing struct. It is for instance called when someone calls %Foo{struct | a: 1, b: 2}).

This means that we could override it with a version that checks a struct's keys against their types every time someone uses %Foo{} or %Foo{foo | some: value}.

The best way to do this is probably not to override defstruct itself (because this, for instance, would make it impossible to use TypeCheck together with other libraries that customize or wrap defstruct like for instance Ecto) but to provide a separate compile-time macro type_check_struct/1 that accepts a type as (the single) parameter, which will use defoverridable to override __struct__/{0,1} with versions that perform type-checking before calling super.


This is a cool but advanced feature that is not planned to be added for some time (unless someone else wants to write a PR), but it's something I stumbled upon while thinking about other things, so I thought I would write it down to not forget.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.