defmodule Tyyppi.T do
@moduledoc """
Raw type wrapper. All the macros exported by that module are available in `Tyyppi`.
Require and use `Tyyppi` instead.
"""
use Boundary, deps: [Tyyppi]
alias Tyyppi.{Stats, T}
require Logger
@doc false
defguardp is_params(params) when is_list(params) or is_atom(params)
@typep kind :: :type | :remote_type | :user_type | :ann_type | :atom | :var
@typep ast_lead :: :->
@typep visibility :: :typep | :type | :opaque | :built_in
@typep simple ::
nil
| :a_function
| :a_set
| :abstract_expr
| :af_atom
| :af_clause
| :af_lit_atom
| :af_variable
| :any
| :atom
| :binary
| :boolean
| :byte
| :check_schedulers
| :deflated
| :des3_cbc
| :filename
| :filename_all
| :fun
| :idn
| :input
| :integer
| :iovec
| :iter
| :iterator
| :list
| :map
| :maybe_improper_list
| :module
| :non_neg_integer
| :nonempty_list
| :nonempty_string
| :orddict
| :pid
| :pos_integer
| :queue
| :range
| :receive
| :record
| :reference
| :relation
| :set
| :string
| :term
| :timeout
| :tree
| :tuple
| :union
@type ast :: Macro.t() | {module(), atom(), list() | nil | non_neg_integer()}
@type raw :: {kind() | ast_lead(), non_neg_integer() | keyword(), simple() | [ast()], [raw()]}
@typedoc """
The type information in a human-readable format.
For remote types, it’s gathered from
[`Code.Typespec`](https://github.com/elixir-lang/elixir/blob/master/lib/elixir/lib/code/typespec.ex#L1),
for built-in like `atom()` it’s simply constructed on the fly.
"""
@type t(wrapped) :: %__MODULE__{
type: visibility(),
module: module(),
name: atom(),
params: [atom()],
source: binary() | nil,
definition: raw() | nil,
quoted: wrapped
}
defstruct ~w|type module name params source definition quoted|a
@spec loaded?(type :: T.t(wrapped)) :: boolean() when wrapped: term()
@doc "Returns `true` if the type definition was loaded, `false` otherwise."
def loaded?(%T{definition: nil}), do: false
def loaded?(%T{}), do: true
@spec parse_quoted({:| | {:., keyword(), list()} | atom(), keyword(), list() | nil}) ::
Tyyppi.T.t(wrapped)
when wrapped: term()
@doc false
def parse_quoted({:|, _, [_, _]} = union) do
union
|> union()
|> parse_definition()
|> Stats.type()
end
def parse_quoted({{:., _, [{:__aliases__, _, aliases}, fun]}, _, params})
when is_params(params) do
params = params |> normalize_params() |> length()
Stats.type({Module.concat(aliases), fun, params})
end
def parse_quoted({{:., _, [module, fun]}, _, params}) when is_params(params) do
params = params |> normalize_params() |> length()
Stats.type({module, fun, params})
end
def parse_quoted({fun, _, params}) when is_atom(fun) and fun != :{} and is_params(params),
do: Stats.type({:type, 0, fun, param_names(params)})
def parse_quoted(any) do
Logger.debug("[🚰 T.parse_quoted/1]: " <> inspect(any))
any
|> parse_definition()
|> Stats.type()
end
@doc false
def parse_definition(atom) when is_atom(atom), do: {:atom, 0, atom}
def parse_definition(list) when is_list(list),
do: {:type, 0, :union, Enum.map(list, &parse_definition/1)}
def parse_definition(tuple) when is_tuple(tuple) do
case Macro.decompose_call(tuple) do
:error -> {:type, 0, :tuple, tuple |> decompose_tuple() |> Enum.map(&parse_definition/1)}
{:{}, list} when is_list(list) -> {:type, 0, :tuple, Enum.map(list, &parse_definition/1)}
_ -> parse_quoted(tuple).definition
end
end
defp decompose_tuple({:{}, _, list}) when is_list(list), do: list
defp decompose_tuple(tuple), do: Tuple.to_list(tuple)
@doc false
def union(ast, acc \\ [])
def union({:|, _, [t1, t2]}, acc), do: union(t2, [t1 | acc])
def union(t, acc), do: Enum.reverse([t | acc])
@doc false
@spec normalize_params([raw()] | any()) :: [raw()]
def normalize_params(params) when is_list(params), do: params
def normalize_params(_params), do: []
@doc false
@spec param_names([raw()] | any()) :: [atom()]
def param_names(params) when is_list(params) do
params
|> Enum.reduce([], fn kv, acc ->
case kv do
{k, _} -> [k | acc]
{k, _, _} -> [k | acc]
_ -> acc
end
end)
|> Enum.reverse()
end
def param_names(_), do: []
# FIXME
@doc false
def collectable?(%Tyyppi.T{
definition:
{:type, _, :map,
[{:type, _, :map_field_exact, [{:atom, _, :__struct__}, {:atom, _, _struct}]} | _]}
}),
do: false
def collectable?(any), do: not is_nil(Collectable.impl_for(any))
@doc false
def enumerable?(%Tyyppi.T{
definition:
{:type, _, :map,
[{:type, _, :map_field_exact, [{:atom, _, :__struct__}, {:atom, _, _struct}]} | _]}
}),
do: false
def enumerable?(any), do: not is_nil(Enumerable.impl_for(any))
defimpl String.Chars do
@moduledoc false
use Boundary, classify_to: Tyyppi.T
defp stringify([]), do: ~s|[]|
defp stringify({:atom, _, atom}) when atom in [nil, false, true], do: ~s|#{atom}|
defp stringify({:atom, _, atom}), do: ~s|:#{atom}|
defp stringify({:var, _, name}), do: ~s|_#{name}|
defp stringify({:type, _, type}), do: ~s|#{type}()|
defp stringify({:type, _, type, params}) do
params = Enum.map_join(params, ", ", &stringify/1)
~s|#{type}(#{params})|
end
defp stringify({:remote_type, _, type}) when is_list(type),
do: Enum.map_join(type, ", ", &stringify/1)
defp stringify({:remote_type, _, type, params}) do
params = Enum.map_join(params, ", ", &stringify/1)
~s|#{type}(#{params})|
end
defp stringify(any), do: inspect(any)
def to_string(%T{module: nil, name: nil, definition: {:type, _, _type, _params} = type}),
do: stringify(type)
def to_string(%T{module: module, name: name, params: params}),
do: stringify({:type, 0, "#{inspect(module)}.#{name}", params})
end
end