lib/type_check/options.ex

defmodule TypeCheck.Options do
  import TypeCheck.Internals.Bootstrap.Macros

  @moduledoc """
  Defines the options that TypeCheck supports on calls to `use TypeCheck`.

  Supported options:

  - `:overrides`: A list of overrides for remote types. (default: `[]`)
  - `:default_overrides`: A boolean. If false, will not include any of the overrides of the types of Elixir's standard library (c.f. `TypeCheck.DefaultOverrides.default_overrides/0`). (default: `true`)
  - `:enable_runtime_checks`: When true, functions that contain a `@spec!` will be wrapped with a runtime check which will check the input to and result returned from the function. (Default: `true`).
  - `:debug`: When true, will (at compile-time) print the generated TypeCheck-checking code. (Default: `false`)

  These options are usually specified as passed to `use TypeCheck`,
  although they may also be passed in direct calls to `TypeCheck.conforms/3` (and its variants).

  These options are module-specific and are read/used at compile-time.

  ## Setting different options for different dependencies

  Different default options can be set for each of the different dependencies,
  by supplying the desired options in the configuration of that dependency under the `:type_check` key:

  ```
  config :your_dependency_name, :type_check, [
    enable_runtime_checks: false
    # etc.
  ]
  ```

  Note that these values might (by design) be overridden by module-specific options
  that might be specified at the location where the dependency calls `use TypeCheck`.


  ## The supported options in detail

  ### Overrides:

  The `:overrides` field contains a list of remote types to be overridden by a replacement.
  This is useful to be able to specify TypeCheck-types for types that you do not have control over
  (because they are for instance defined in a library that is not itself using TypeCheck).

  For obvious reasons, using TypeCheck directly should be preferred over overriding types.


  Each of the elements in the `:overrides` list should be written as `{original_type, replacement_type}`.
  Both of these can take the shape of either`&Module.type/arity` or the longer form `{Module, :type, arity}`.

  An example:

  ```
  use TypeCheck, overrides: [
    {&Ecto.Schema.t/0, &MyProject.TypeCheckOverrides.Ecto.Schema.t/0}
  ]
  ```

  ### Enabling/Disabling runtime checks

  By default, runtime checks are enabled.

  In the case where the runtime checks turn out to be too slow (for instance, because of working with very large or deeply nested collections) in a particular module,
  they can be turned off completely.

  It is recommended to:

  - Only turn them off after benchmarking has shown that this will make a significant difference.
  - Only turn them off in e.g. the production environment, keeping them on in the development and test environments.

  An example:

  ```elixir
  use TypeCheck, enable_runtime_checks: Mix.env() != :prod
  ```

  ### Debugging

  Passing the option `debug: true` will at compile-time print the generated code
  for all added `@spec`s, as well as `TypeCheck.conforms/3`/`TypeCheck.conforms?/3`/`TypeCheck.conforms!/3` calls.
  """

  defstruct overrides: [], default_overrides: true, enable_runtime_checks: true, debug: false

  if_recompiling? do
    use TypeCheck

    @type! remote_type() :: mfa() | function

    @typedoc """
    An extra check is performed to ensure that the original type
    and the replacement type have the same arity.
    """
    @type! type_override :: {original :: remote_type(), replacement :: remote_type()}
    @type! type_overrides :: list(type_override())

    @type! t :: %TypeCheck.Options{
             overrides: type_overrides(),
             default_overrides: boolean(),
             enable_runtime_checks: boolean(),
             debug: boolean()
           }
  else
    @type remote_type() :: mfa | function
    @type type_override :: {remote_type(), remote_type()}

    @type t :: %TypeCheck.Options{
            overrides: list(type_override()),
            default_overrides: boolean(),
            enable_runtime_checks: boolean(),
            debug: boolean()
          }
  end

  def new() do
    %__MODULE__{overrides: default_overrides()}
  end

  def new(already_struct = %__MODULE__{}) do
    # IO.inspect(already_struct, label: "Inside first new clause")
    already_struct
  end

  if_recompiling? do
    @spec! new(enum :: any()) :: t()
  end

  def new(enum) do
    # IO.inspect(enum, label: "Inside second new clause")
    raw_overrides = Keyword.get(enum, :overrides, [])
    debug = Keyword.get(enum, :debug, false)
    enable_runtime_checks = Keyword.get(enum, :enable_runtime_checks, true)

    overrides = check_overrides!(raw_overrides)

    overrides =
      if Access.get(enum, :default_overrides, true) do
        overrides ++ default_overrides()
      else
        overrides
      end

    %__MODULE__{
      overrides: overrides,
      enable_runtime_checks: enable_runtime_checks,
      debug: debug
    }
  end

  if_recompiling? do
    @spec! check_overrides!(overrides :: type_overrides()) :: type_overrides()
  end

  def check_overrides!(overrides) do
    Enum.map(overrides, &check_override!/1)
  end

  defp default_overrides() do
    case Code.ensure_loaded(TypeCheck.DefaultOverrides) do
      {:error, _problem} -> []
      {:module, _} -> apply(TypeCheck.DefaultOverrides, :default_overrides, [])
    end
  end

  defp check_override!({original, override}) do
    {module_k, function_k, arity_k} = ensure_external_function!(original)
    {module_v, function_v, arity_v} = ensure_external_function!(override)

    if arity_k != arity_v do
      raise TypeCheck.CompileError,
            "Error while parsing TypeCheck overides: override #{inspect(override)} does not have same arity as original type #{inspect(original)}."
    else
      {
        {module_k, function_k, arity_k},
        {module_v, function_v, arity_v}
      }
    end
  end

  defp check_override!(other) do
    raise TypeCheck.CompileError,
          "`check_overrides!` expects a list of two-element tuples `{mfa, mfa}` where `mfa` is either `{Module, function, arity}` or `&Module.function/arity`. However, an element not adhering to the `{mfa, mfa}` format was found: `#{inspect(other)}`."
  end

  defp ensure_external_function!(fun) when is_function(fun) do
    case Function.info(fun, :type) do
      {:type, :external} ->
        info = Function.info(fun)
        {info[:module], info[:name], info[:arity]}

      _other ->
        raise TypeCheck.CompileError,
              "Error while parsing TypeCheck overides: #{inspect(fun)} is not an external function of the format `&Module.function/arity`!"
    end
  end

  defp ensure_external_function!({module, function, arity})
       when is_atom(module) and is_atom(function) and arity >= 0 do
    {module, function, arity}
  end

  defp ensure_external_function!(fun) do
    raise TypeCheck.CompileError,
          "Error while parsing TypeCheck overides: #{inspect(fun)} is not a function!"
  end
end