Skip to content

Commit

Permalink
Collapse structs for better pretty printing in different scenarios
Browse files Browse the repository at this point in the history
  • Loading branch information
josevalim committed Dec 19, 2024
1 parent ca6edfd commit c6597bc
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 53 deletions.
2 changes: 1 addition & 1 deletion lib/elixir/lib/module/types/apply.ex
Original file line number Diff line number Diff line change
Expand Up @@ -944,7 +944,7 @@ defmodule Module.Types.Apply do
defp type_comparison_to_string(fun, left, right) do
{Kernel, fun, [left, right], _} = :elixir_rewrite.erl_to_ex(:erlang, fun, [left, right])

{fun, [], [to_quoted(left), to_quoted(right)]}
{fun, [], [to_quoted(left, collapse_structs: true), to_quoted(right, collapse_structs: true)]}
|> Code.Formatter.to_algebra()
|> Inspect.Algebra.format(98)
|> IO.iodata_to_binary()
Expand Down
119 changes: 76 additions & 43 deletions lib/elixir/lib/module/types/descr.ex
Original file line number Diff line number Diff line change
Expand Up @@ -367,16 +367,21 @@ defmodule Module.Types.Descr do

@doc """
Converts a descr to its quoted representation.
## Options
* `:collapse_structs` - do not show struct fields that match
their default type
"""
def to_quoted(descr) do
def to_quoted(descr, opts \\ []) do
if term_type?(descr) do
{:term, [], []}
else
# Dynamic always come first for visibility
{dynamic, descr} =
case :maps.take(:dynamic, descr) do
:error -> {[], descr}
{dynamic, descr} -> {to_quoted(:dynamic, dynamic), descr}
{dynamic, descr} -> {to_quoted(:dynamic, dynamic, opts), descr}
end

# Merge empty list and list together if they both exist
Expand All @@ -385,7 +390,7 @@ defmodule Module.Types.Descr do
%{list: list, bitmap: bitmap} when (bitmap &&& @bit_empty_list) != 0 ->
descr = descr |> Map.delete(:list) |> Map.replace!(:bitmap, bitmap - @bit_empty_list)

case list_to_quoted(list, :list) do
case list_to_quoted(list, :list, opts) do
[] -> {[{:empty_list, [], []}], descr}
unions -> {unions, descr}
end
Expand All @@ -396,7 +401,9 @@ defmodule Module.Types.Descr do

unions =
dynamic ++
Enum.sort(extra ++ Enum.flat_map(descr, fn {key, value} -> to_quoted(key, value) end))
Enum.sort(
extra ++ Enum.flat_map(descr, fn {key, value} -> to_quoted(key, value, opts) end)
)

case unions do
[] -> {:none, [], []}
Expand All @@ -405,19 +412,19 @@ defmodule Module.Types.Descr do
end
end

defp to_quoted(:atom, val), do: atom_to_quoted(val)
defp to_quoted(:bitmap, val), do: bitmap_to_quoted(val)
defp to_quoted(:dynamic, descr), do: dynamic_to_quoted(descr)
defp to_quoted(:map, dnf), do: map_to_quoted(dnf)
defp to_quoted(:list, dnf), do: list_to_quoted(dnf, :non_empty_list)
defp to_quoted(:tuple, dnf), do: tuple_to_quoted(dnf)
defp to_quoted(:atom, val, _opts), do: atom_to_quoted(val)
defp to_quoted(:bitmap, val, _opts), do: bitmap_to_quoted(val)
defp to_quoted(:dynamic, descr, opts), do: dynamic_to_quoted(descr, opts)
defp to_quoted(:map, dnf, opts), do: map_to_quoted(dnf, opts)
defp to_quoted(:list, dnf, opts), do: list_to_quoted(dnf, :non_empty_list, opts)
defp to_quoted(:tuple, dnf, opts), do: tuple_to_quoted(dnf, opts)

@doc """
Converts a descr to its quoted string representation.
"""
def to_quoted_string(descr) do
def to_quoted_string(descr, opts \\ []) do
descr
|> to_quoted()
|> to_quoted(opts)
|> Code.Formatter.to_algebra()
|> Inspect.Algebra.format(98)
|> IO.iodata_to_binary()
Expand Down Expand Up @@ -1045,16 +1052,16 @@ defmodule Module.Types.Descr do
end
end

defp list_to_quoted(dnf, name) do
defp list_to_quoted(dnf, name, opts) do
dnf = list_normalize(dnf)

for {list_type, last_type, negs} <- dnf, reduce: [] do
acc ->
arguments =
if subtype?(last_type, @empty_list) do
[to_quoted(list_type)]
[to_quoted(list_type, opts)]
else
[to_quoted(list_type), to_quoted(last_type)]
[to_quoted(list_type, opts), to_quoted(last_type, opts)]
end

if negs == [] do
Expand All @@ -1064,9 +1071,9 @@ defmodule Module.Types.Descr do
|> Enum.map(fn {ty, lst} ->
args =
if subtype?(lst, @empty_list) do
[to_quoted(ty)]
[to_quoted(ty, opts)]
else
[to_quoted(ty), to_quoted(lst)]
[to_quoted(ty, opts), to_quoted(lst, opts)]
end

{name, [], args}
Expand Down Expand Up @@ -1176,7 +1183,7 @@ defmodule Module.Types.Descr do
end
end

defp dynamic_to_quoted(descr) do
defp dynamic_to_quoted(descr, opts) do
cond do
term_type?(descr) ->
[{:dynamic, [], []}]
Expand All @@ -1185,7 +1192,7 @@ defmodule Module.Types.Descr do
[single]

true ->
case to_quoted(descr) do
case to_quoted(descr, opts) do
{:none, _meta, []} = none -> [none]
descr -> [{:dynamic, [], [descr]}]
end
Expand Down Expand Up @@ -1786,51 +1793,77 @@ defmodule Module.Types.Descr do
end))
end

defp map_to_quoted(dnf) do
defp map_to_quoted(dnf, opts) do
dnf
|> map_normalize()
|> Enum.map(&map_each_to_quoted/1)
|> Enum.map(&map_each_to_quoted(&1, opts))
end

defp map_each_to_quoted({tag, positive_map, negative_maps}) do
defp map_each_to_quoted({tag, positive_map, negative_maps}, opts) do
case negative_maps do
[] ->
map_literal_to_quoted({tag, positive_map})
map_literal_to_quoted({tag, positive_map}, opts)

_ ->
negative_maps
|> Enum.map(&map_literal_to_quoted/1)
|> Enum.map(&map_literal_to_quoted(&1, opts))
|> Enum.reduce(&{:or, [], [&2, &1]})
|> Kernel.then(
&{:and, [], [map_literal_to_quoted({tag, positive_map}), {:not, [], [&1]}]}
&{:and, [], [map_literal_to_quoted({tag, positive_map}, opts), {:not, [], [&1]}]}
)
end
end

def map_literal_to_quoted({:closed, fields}) when map_size(fields) == 0 do
def map_literal_to_quoted({:closed, fields}, _opts) when map_size(fields) == 0 do
{:empty_map, [], []}
end

def map_literal_to_quoted({tag, fields}) do
def map_literal_to_quoted({tag, fields}, opts) do
case tag do
:closed ->
with %{__struct__: struct_descr} <- fields,
{_, [struct]} <- atom_fetch(struct_descr) do
fields = Map.delete(fields, :__struct__)

fields =
with true <- Keyword.get(opts, :collapse_structs, false),
[_ | _] = info <- maybe_struct(struct),
true <- Enum.all?(info, &is_map_key(fields, &1.field)) do
Enum.reduce(info, fields, fn %{field: field}, acc ->
# TODO: This should consider the struct default value
if Map.fetch!(acc, field) == term() do
Map.delete(acc, field)
else
acc
end
end)
else
_ -> fields
end

{:%, [],
[
literal_to_quoted(struct),
{:%{}, [], map_fields_to_quoted(tag, Map.delete(fields, :__struct__))}
{:%{}, [], map_fields_to_quoted(tag, fields, opts)}
]}
else
_ -> {:%{}, [], map_fields_to_quoted(tag, fields)}
_ -> {:%{}, [], map_fields_to_quoted(tag, fields, opts)}
end

:open ->
{:%{}, [], [{:..., [], nil} | map_fields_to_quoted(tag, fields)]}
{:%{}, [], [{:..., [], nil} | map_fields_to_quoted(tag, fields, opts)]}
end
end

defp maybe_struct(struct) do
try do
struct.__info__(:struct)
rescue
_ -> nil
end
end

defp map_fields_to_quoted(tag, map) do
defp map_fields_to_quoted(tag, map, opts) do
sorted = Enum.sort(Map.to_list(map))
keyword? = Inspect.List.keyword?(sorted)

Expand All @@ -1846,9 +1879,9 @@ defmodule Module.Types.Descr do
{optional?, type} = pop_optional_static(type)

cond do
not optional? -> {key, to_quoted(type)}
not optional? -> {key, to_quoted(type, opts)}
empty?(type) -> {key, {:not_set, [], []}}
true -> {key, {:if_set, [], [to_quoted(type)]}}
true -> {key, {:if_set, [], [to_quoted(type, opts)]}}
end
end
end
Expand Down Expand Up @@ -1969,11 +2002,11 @@ defmodule Module.Types.Descr do
# This is a cheap optimization that relies on structural equality.
defp tuple_union(left, right), do: left ++ (right -- left)

defp tuple_to_quoted(dnf) do
defp tuple_to_quoted(dnf, opts) do
dnf
|> tuple_simplify()
|> tuple_fusion()
|> Enum.map(&tuple_each_to_quoted/1)
|> Enum.map(&tuple_each_to_quoted(&1, opts))
end

# Given a dnf of tuples, fuses the tuple unions when possible,
Expand Down Expand Up @@ -2020,27 +2053,27 @@ defmodule Module.Types.Descr do
{tag, fused_elements, []}
end

defp tuple_each_to_quoted({tag, positive_tuple, negative_tuples}) do
defp tuple_each_to_quoted({tag, positive_tuple, negative_tuples}, opts) do
case negative_tuples do
[] ->
tuple_literal_to_quoted({tag, positive_tuple})
tuple_literal_to_quoted({tag, positive_tuple}, opts)

_ ->
negative_tuples
|> Enum.map(&tuple_literal_to_quoted/1)
|> Enum.map(&tuple_literal_to_quoted(&1, opts))
|> Enum.reduce(&{:or, [], [&2, &1]})
|> Kernel.then(
&{:and, [], [tuple_literal_to_quoted({tag, positive_tuple}), {:not, [], [&1]}]}
&{:and, [], [tuple_literal_to_quoted({tag, positive_tuple}, opts), {:not, [], [&1]}]}
)
end
end

defp tuple_literal_to_quoted({:closed, []}), do: {:{}, [], []}
defp tuple_literal_to_quoted({:closed, []}, _opts), do: {:{}, [], []}

defp tuple_literal_to_quoted({tag, elements}) do
defp tuple_literal_to_quoted({tag, elements}, opts) do
case tag do
:closed -> {:{}, [], Enum.map(elements, &to_quoted/1)}
:open -> {:{}, [], Enum.map(elements, &to_quoted/1) ++ [{:..., [], nil}]}
:closed -> {:{}, [], Enum.map(elements, &to_quoted(&1, opts))}
:open -> {:{}, [], Enum.map(elements, &to_quoted(&1, opts)) ++ [{:..., [], nil}]}
end
end

Expand Down
2 changes: 1 addition & 1 deletion lib/elixir/lib/module/types/helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ defmodule Module.Types.Helpers do
column: meta[:column],
hints: formatter_hints ++ expr_hints(expr),
formatted_expr: formatted_expr,
formatted_type: Module.Types.Descr.to_quoted_string(type)
formatted_type: Module.Types.Descr.to_quoted_string(type, collapse_structs: true)
}
end)
|> Enum.sort_by(&{&1.line, &1.column})
Expand Down
4 changes: 1 addition & 3 deletions lib/elixir/lib/module/types/of.ex
Original file line number Diff line number Diff line change
Expand Up @@ -495,13 +495,11 @@ defmodule Module.Types.Of do
unknown key .#{key} in expression:
#{expr_to_string(expr) |> indent(4)}
""",
empty_if(dot_var?(expr), """
the given type does not have the given key:
#{to_quoted_string(type) |> indent(4)}
"""),
""",
format_traces(traces)
])
}
Expand Down
12 changes: 8 additions & 4 deletions lib/elixir/test/elixir/module/types/expr_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -798,9 +798,9 @@ defmodule Module.Types.ExprTest do
x.foo_bar
where "x" was given the type:
the given type does not have the given key:
# type: dynamic(%URI{
dynamic(%URI{
authority: term(),
fragment: term(),
host: term(),
Expand All @@ -810,6 +810,10 @@ defmodule Module.Types.ExprTest do
scheme: term(),
userinfo: term()
})
where "x" was given the type:
# type: dynamic(%URI{})
# from: types_test.ex:LINE-4:43
x = %URI{}
"""
Expand Down Expand Up @@ -903,7 +907,7 @@ defmodule Module.Types.ExprTest do
given types:
dynamic(:foo) <= dynamic(%Point{x: term(), y: term(), z: term()})
dynamic(:foo) <= dynamic(%Point{})
where "mod" was given the type:
Expand All @@ -919,7 +923,7 @@ defmodule Module.Types.ExprTest do
where "y" was given the type:
# type: dynamic(%Point{x: term(), y: term(), z: term()})
# type: dynamic(%Point{})
# from: types_test.ex:LINE-2
y = %Point{}
Expand Down
6 changes: 5 additions & 1 deletion lib/elixir/test/elixir/module/types/pattern_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,13 @@ defmodule Module.Types.PatternTest do
x.foo_bar
the given type does not have the given key:
dynamic(%Point{x: term(), y: term(), z: term()})
where "x" was given the type:
# type: dynamic(%Point{x: term(), y: term(), z: term()})
# type: dynamic(%Point{})
# from: types_test.ex:LINE-1
x = %Point{}
"""
Expand Down

0 comments on commit c6597bc

Please sign in to comment.