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

Added {:type_check, "~> 0.2.3"} to mix.exs, ran deps.get, deps.compile and . . .

iex(2)> Sandbox.foo("bing", "bang", "bow")
** (TypeCheck.TypeError) The call `foo("bing", "bang", "bow")` does not adhere to spec `foo(binary(),  binary(),  float()) :: float()`. Reason:
  parameter no. 3:
    `"bow"` is not a float.
    (arbit 0.1.0) lib/Sandbox.ex:1: Sandbox.foo/3

Success!! Thanks again! :slight_smile:

2 Likes

@Qqwy - I’ve been thinking about TypeCheck in regards to remote types and wondering about your thoughts. I use TypeCheck for the following:

  1. As a runtime checker for builtin elixir types. They are not all there, but most are.
  2. As a way to write tighter custom requirements than can be written via guards or regular specs.
  3. As a way to automatically generate traditional specs for compile-time.

I know it provides generators and manual compile time checking, but I just haven’t used them yet.

The one sticking point for me has probably been interoperability with libraries/modules that do not use TypeCheck. Referencing a remote type, of course, results in a compile-time error. This means that not only can the remote type not be type checked, but it can’t generate a traditional typespec. I can write a traditional typespec, but now I have a mix of @spec and @spec! in my codebase. I’d rather just write @spec! everywhere.

You mention thinking about some sort override configuration for remote types. It does make me wonder though if TypeCheck should/can just double down on runtime checking and ignore remote types altogether. Basically, if TypeCheck encounters something like Ecto.UUID.t() and doesn’t have a known way to handle it it is ignored, BUT is still included in the generated traditional typespecs. It handles the things it can and doesn’t harm the things it can’t.

Perhaps it is just not a concern of TypeCheck to deal with this and that is okay. TypeCheck would still be providing value in that case by generating the traditional spec for tools dedicated to compiled spec checking like Dialyzer and Selectrix.

The obvious caveat here is that people may assume TypeCheck is checking things that it is not and silently ignoring them. First, I just don’t know that this is that big of a deal assuming the documentation prominently mentions that remote types are not checked. Second, it is always possible for one to write custom TypeCheck-compatible types or even create TypeCheck support packages for common libraries. Lastly, I’m just thinking the value gained by ignoring checks (but generating traditional specs) outweighs the current situation where you can’t reference them at all.

I don’t know if this is doable or something you would want to consider, but throwing it out here in case it never seemed like an option.

1 Like

Hi @baldwindavid.

It is a valid concern. My opinion on the matter is to prefer explicit overrides over ‘implicitly doing nothing’, because the latter has great bug-hiding potential. That said, I definitely want to make it simple for people to opt-out by e.g. specifying that a particular remote type should be treated equivalent to any(). (But this should not be the default).

Now obviously the current situation in which remote types cannot properly be used at all is painful. I hope to resolve this as soon as possible (I have some time in the next few weeks to work on this, luckily).

I’m very interested in what this looks like! Please share an example! :grin:

1 Like

Fair enough. Just being able to reference remote types will be excellent. I guess I’m picturing a world where something like Selectrix provides an extra level of protection to avoid those hidden bugs, but I can appreciate wanting to flag those issues during run-time via a bit of additional configuration.

I just mean that under the hood TypeCheck is generating traditional typespecs that something like Dialyzer or Selectrix could theoretically use. Right? I don’t use Dialyzer, but that was my (perhaps mistaken) assumption.

1 Like

Ah! Yes, TypeCheck indeed generates traditional Dialyzer-compatible typespecs.

defmodule Sandbox do
    use TypeCheck

    @spec! foo(binary, binary | atom, float) :: float

    def foo(arg1, arg2, arg3) do
        #some process
  end
end
iex(4)> Sandbox.foo("bing", :ok, 5.712)    
** (TypeCheck.TypeError) The result of calling `foo("bing", :ok, 5.712)` does not adhere to spec `foo(binary(),  binary() | atom(),  float()) :: float()`. Reason:
  Returned result:
    `:ok` is not a float.
    lib/Sandbox.ex:1: Sandbox.foo/3

As you can see, arg2 can be either a string or an atom. What’s the proper TypeCheck syntax to get this to spec to pass?

That error is a mismatch on the function result rather than the arguments. The result is specified as float, but the function is returning :ok.

If the function can return either a string or atom, the spec could be changed to:

@spec! foo(binary, binary | atom, float) :: binary | atom
4 Likes

Got it, thanks!

Here’s a separate issue:

defmodule Sandbox do
  use TypeCheck

  @spec! main(list, binary) :: binary | list
  def main(arg1, arg2) when arg1 == [] do
    # some process
  end

  @spec! main(list, binary) :: list
  def main(arg1, arg2) when arg1 != [] do
    # some process
  end
end
iex(5)> recompile             
Compiling 1 file (.ex)
warning: this clause for __type_check_spec_for_main/2__/0 cannot match because a previous clause at line 1 always matches
  lib/type_check/spec.ex:1

How do I avoid this warning?

You only need a single @spec! declaration to cover multiple function heads of the same name and arity. You’re getting a warning there because the second declaration of @spec! main can’t possibly match anyway as any list would already have matched on the first @spec! main declaration.

2 Likes

Nice! Got it, thanks so much :slight_smile:

defmodule Print do
    use TypeCheck

    def utc_time, do: DateTime.utc_now()

    @spec! higlight(binary | number | list | atom) :: atom
    def highlight(message) do
        timestamp = @blue <> "#{utc_time()}-"
        text = @light_green <> "#{message}"

        IO.puts("#{timestamp} #{text} \n")
    end

end
iex(14)> recompile             
Compiling 1 file (.ex)

== Compilation error in file lib/Print.ex ==
** (ArgumentError) spec for undefined function higlight/1
(type_check 0.2.3) lib/type_check/macros.ex:94: anonymous fn/4 in TypeCheck.Macros.wrap_functions_with_specs/3
(elixir 1.10.3) lib/enum.ex:2111: Enum."-reduce/3-lists^foldl/2-0-"/3
(type_check 0.2.3) lib/type_check/macros.ex:92: TypeCheck.Macros.wrap_functions_with_specs/3
(type_check 0.2.3) expanding macro: TypeCheck.Macros.__before_compile__/1
lib/Print.ex:1: Print (module)
** (exit) shutdown: 1
(mix 1.10.3) lib/mix/tasks/compile.all.ex:62: Mix.Tasks.Compile.All.do_compile/4
(mix 1.10.3) lib/mix/tasks/compile.all.ex:27: anonymous fn/2 in Mix.Tasks.Compile.All.run/1
(mix 1.10.3) lib/mix/tasks/compile.all.ex:43: Mix.Tasks.Compile.All.with_logger_app/2
(mix 1.10.3) lib/mix/task.ex:330: Mix.Task.run_task/3
(mix 1.10.3) lib/mix/tasks/compile.ex:96: Mix.Tasks.Compile.run/1
(mix 1.10.3) lib/mix/task.ex:330: Mix.Task.run_task/3
(iex 1.10.3) lib/iex/helpers.ex:104: IEx.Helpers.recompile/1

Still getting my feet wet but, so far, I absolutely love TypeCheck. Ok, so, what’s going on with this error message and how do I keeping it from cropping up again in the future?

The error message notes that there is no function with that name. Check your spelling :slightly_smiling_face:

1 Like

lol, It’s always the little things . . .

Thanks so much, I appreciate your forbearance :slight_smile:

2 Likes

@Qqwy, my friend, let me first start off by saying how much I absolutely love TypeCheck! I’m an Elixir noob (started learning it on my own last year with ZERO programming background) and I don’t know how I’ve gotten as far I have without it. It’s absolutely amazing!!

Your focus on generating user friendly error messages is one of its very best features! With that in mind I’d like to offer up the following suggestion: It would be very helpful if the error message were restructured to something along the lines of-

** (TypeCheck.TypeError) The call `THE_FUNCTION_EVALUATED/arity' does not adhere to spec `THE_@spec!'. Reason: parameter no. PARAMETER_NUMBER (or result) is not a 'TYPE_SPECIFIED_IN_THE_@spec!'

Details: 'COPY_OF_THE_PARAMETER_DATA_THAT_TRIGGERED_THE TypeError'.

The advantage of such a format is that it provides the most actionable information right from the jump. (My project manipulates very dense data sets so whenever there’s a TypeError I have to wade through literally pages and pages of an error report to figure out exactly what went wrong.)

2 Likes

Interesting! Adding function/arity at the top (including its line number and filename if available) and specifying which parameter (or the result) failed before printing them might indeed be an improvement for when functions are called with arguments that are very large.
I’ll give the matter some thought :slight_smile:.

4 Likes

Version 0.3.0 has been released, which contains the improved error formatter output. (details can be found in this PR) as well as a small bugfix for types using tuple/1.

5 Likes

Is there anything unique about TypeCheck that might stop me from writing a macro that generates a function with a @spec!?

I’m experimenting with a few macros for functions I write all over the place and was hoping to include specs. This example (which I import) works fine except when I add the @spec!. Is it because it is a macro in a macro?

defmacro crud_list(resource) do
  quote bind_quoted: [resource: resource] do
    @spec! unquote(:"list_#{resource.plural_name}")(query_pipe()) ::
             list(unquote(resource.schema).t())
    def unquote(:"list_#{resource.plural_name}")(queries \\ & &1) do
      unquote(resource.schema)
      |> queries.()
      |> unquote(resource.repo).all()
    end
  end
end

There should not be.
While TypeCheck does need to jump through some tricky hoops to make all of the syntax and semantics work and be desugared at compile-time, wrapping specs in macros is a use case which, if at all possible, I’d like TypeCheck to be able to handle properly.
If you could open an issue on GitHub that explains the code you are trying to compile and the errors you encounter, then I’ll take a look at it ASAP :slight_smile:.

2 Likes

Would there be any way of integrating with https://hexdocs.pm/typed_struct/TypedStruct.html? Or something similar?

This would mean something like this is possible:

defmodule Example do
  use TypedStruct
  use TypeCheck

  typedstruct module: Options do
    field :name, String.t()
    field :age, integer()
  end

  @spec! init(Example.Options.t()) :: :ok
  def init(opts) do
    :ok
  end
end

over

defmodule Example do
  use TypeCheck

  defmodule Options do
    defstruct [:name, :age]

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

  @spec! init(Example.Options.t()) :: :ok
  def init(opts) do
    :ok
  end
end
3 Likes

This is definitely a feature that I’d like to add. C.f. GitHub issue #21