From 82cbc78043b3b2595b8f146ad152c03d8eb0cfbb Mon Sep 17 00:00:00 2001 From: sabiwara Date: Wed, 20 Aug 2025 21:33:57 +0900 Subject: [PATCH 1/3] Inspect ill-formed structs as maps --- lib/elixir/lib/inspect.ex | 60 +++++++++++-------- lib/elixir/lib/inspect/algebra.ex | 2 +- .../test/elixir/calendar/date_range_test.exs | 21 ------- lib/elixir/test/elixir/inspect_test.exs | 7 +++ lib/elixir/test/elixir/range_test.exs | 8 --- 5 files changed, 42 insertions(+), 56 deletions(-) diff --git a/lib/elixir/lib/inspect.ex b/lib/elixir/lib/inspect.ex index 7c20bcf8140..bc484bed9d0 100644 --- a/lib/elixir/lib/inspect.ex +++ b/lib/elixir/lib/inspect.ex @@ -475,6 +475,32 @@ defimpl Inspect, for: Map do map_container_doc(infos, name, opts, fun) end + def valid_struct?(struct) do + !!valid_struct_info(struct) + end + + def valid_struct_info(%module{} = struct) do + try do + module.__info__(:struct) + rescue + _ -> nil + else + info -> + if valid_struct?(info, struct, map_size(struct) - 1) do + info + end + end + end + + defp valid_struct?([%{field: field} | info], struct, count) when is_map_key(struct, field), + do: valid_struct?(info, struct, count - 1) + + defp valid_struct?([], _struct, 0), + do: true + + defp valid_struct?(_fields, _struct, _count), + do: false + defp to_assoc({key, value}, opts, sep) do {key_doc, opts} = to_doc_with_opts(key, opts) {value_doc, opts} = to_doc_with_opts(value, opts) @@ -662,36 +688,18 @@ end defimpl Inspect, for: Any do def inspect(%module{} = struct, opts) do - try do - module.__info__(:struct) - rescue - _ -> Inspect.Map.inspect_as_map(struct, opts) - else - info -> - if valid_struct?(info, struct) do - info = - for %{field: field} = map <- info, - field != :__exception__, - do: map + if info = Inspect.Map.valid_struct_info(struct) do + info = + for %{field: field} = map <- info, + field != :__exception__, + do: map - Inspect.Map.inspect_as_struct(struct, Macro.inspect_atom(:literal, module), info, opts) - else - Inspect.Map.inspect_as_map(struct, opts) - end + Inspect.Map.inspect_as_struct(struct, Macro.inspect_atom(:literal, module), info, opts) + else + Inspect.Map.inspect_as_map(struct, opts) end end - defp valid_struct?(info, struct), do: valid_struct?(info, struct, map_size(struct) - 1) - - defp valid_struct?([%{field: field} | info], struct, count) when is_map_key(struct, field), - do: valid_struct?(info, struct, count - 1) - - defp valid_struct?([], _struct, 0), - do: true - - defp valid_struct?(_fields, _struct, _count), - do: false - def inspect_as_struct(map, name, infos, opts) do open = color_doc("#" <> name <> "<", :map, opts) sep = color_doc(",", :map, opts) diff --git a/lib/elixir/lib/inspect/algebra.ex b/lib/elixir/lib/inspect/algebra.ex index 62e039c938c..44b7f94377a 100644 --- a/lib/elixir/lib/inspect/algebra.ex +++ b/lib/elixir/lib/inspect/algebra.ex @@ -396,7 +396,7 @@ defmodule Inspect.Algebra do def to_doc_with_opts(term, opts) def to_doc_with_opts(%_{} = struct, %Inspect.Opts{inspect_fun: fun} = opts) do - if opts.structs do + if opts.structs and Inspect.Map.valid_struct?(struct) do try do fun.(struct, opts) rescue diff --git a/lib/elixir/test/elixir/calendar/date_range_test.exs b/lib/elixir/test/elixir/calendar/date_range_test.exs index 10fbf53df37..39465421ca4 100644 --- a/lib/elixir/test/elixir/calendar/date_range_test.exs +++ b/lib/elixir/test/elixir/calendar/date_range_test.exs @@ -149,27 +149,6 @@ defmodule Date.RangeTest do end describe "old date ranges" do - test "inspect" do - asc = %{ - __struct__: Date.Range, - first: ~D[2021-07-14], - first_in_iso_days: 738_350, - last: ~D[2021-07-17], - last_in_iso_days: 738_353 - } - - desc = %{ - __struct__: Date.Range, - first: ~D[2021-07-17], - first_in_iso_days: 738_353, - last: ~D[2021-07-14], - last_in_iso_days: 738_350 - } - - assert inspect(asc) == "Date.range(~D[2021-07-14], ~D[2021-07-17])" - assert inspect(desc) == "Date.range(~D[2021-07-17], ~D[2021-07-14], -1)" - end - test "enumerable" do asc = %{ __struct__: Date.Range, diff --git a/lib/elixir/test/elixir/inspect_test.exs b/lib/elixir/test/elixir/inspect_test.exs index ddc5cc04bd2..be589836b33 100644 --- a/lib/elixir/test/elixir/inspect_test.exs +++ b/lib/elixir/test/elixir/inspect_test.exs @@ -445,6 +445,13 @@ defmodule Inspect.MapTest do "%{__struct__: Inspect.MapTest.Public, foo: :bar, key: 1}" end + test "public modified struct with defimpl" do + map_set = MapSet.new([1, 2]) + + assert inspect(Map.put(map_set, :foo, :bar), custom_options: [sort_maps: true]) == + "%{__struct__: MapSet, foo: :bar, map: %{1 => [], 2 => []}}" + end + test "private struct" do assert inspect(%{__struct__: Private, key: 1}, custom_options: [sort_maps: true]) == "%{__struct__: Inspect.MapTest.Private, key: 1}" diff --git a/lib/elixir/test/elixir/range_test.exs b/lib/elixir/test/elixir/range_test.exs index 4f8783f261a..d273c992019 100644 --- a/lib/elixir/test/elixir/range_test.exs +++ b/lib/elixir/test/elixir/range_test.exs @@ -200,14 +200,6 @@ defmodule RangeTest do end describe "old ranges" do - test "inspect" do - asc = %{__struct__: Range, first: 1, last: 3} - desc = %{__struct__: Range, first: 3, last: 1} - - assert inspect(asc) == "1..3" - assert inspect(desc) == "3..1//-1" - end - test "enum" do asc = %{__struct__: Range, first: 1, last: 3} desc = %{__struct__: Range, first: 3, last: 1} From cab02a13eaf24d340b4cb90a6c0964f969b733ef Mon Sep 17 00:00:00 2001 From: sabiwara Date: Wed, 20 Aug 2025 22:13:11 +0900 Subject: [PATCH 2/3] Simplify: remove downstream check --- lib/elixir/lib/inspect.ex | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/lib/elixir/lib/inspect.ex b/lib/elixir/lib/inspect.ex index bc484bed9d0..0c39ccc517d 100644 --- a/lib/elixir/lib/inspect.ex +++ b/lib/elixir/lib/inspect.ex @@ -475,20 +475,14 @@ defimpl Inspect, for: Map do map_container_doc(infos, name, opts, fun) end - def valid_struct?(struct) do - !!valid_struct_info(struct) - end - - def valid_struct_info(%module{} = struct) do + def valid_struct?(%module{} = struct) do try do module.__info__(:struct) rescue - _ -> nil + _ -> false else info -> - if valid_struct?(info, struct, map_size(struct) - 1) do - info - end + valid_struct?(info, struct, map_size(struct) - 1) end end @@ -688,16 +682,12 @@ end defimpl Inspect, for: Any do def inspect(%module{} = struct, opts) do - if info = Inspect.Map.valid_struct_info(struct) do - info = - for %{field: field} = map <- info, - field != :__exception__, - do: map + info = + for %{field: field} = map <- module.__info__(:struct), + field != :__exception__, + do: map - Inspect.Map.inspect_as_struct(struct, Macro.inspect_atom(:literal, module), info, opts) - else - Inspect.Map.inspect_as_map(struct, opts) - end + Inspect.Map.inspect_as_struct(struct, Macro.inspect_atom(:literal, module), info, opts) end def inspect_as_struct(map, name, infos, opts) do From 59d34430bdd042584c67358404a0c97f5c34e01c Mon Sep 17 00:00:00 2001 From: sabiwara Date: Thu, 21 Aug 2025 06:31:40 +0900 Subject: [PATCH 3/3] Move valid_struct? function to Inspect.Algebra --- lib/elixir/lib/inspect.ex | 20 -------------------- lib/elixir/lib/inspect/algebra.ex | 22 +++++++++++++++++++++- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/elixir/lib/inspect.ex b/lib/elixir/lib/inspect.ex index 0c39ccc517d..ff1c86d728f 100644 --- a/lib/elixir/lib/inspect.ex +++ b/lib/elixir/lib/inspect.ex @@ -475,26 +475,6 @@ defimpl Inspect, for: Map do map_container_doc(infos, name, opts, fun) end - def valid_struct?(%module{} = struct) do - try do - module.__info__(:struct) - rescue - _ -> false - else - info -> - valid_struct?(info, struct, map_size(struct) - 1) - end - end - - defp valid_struct?([%{field: field} | info], struct, count) when is_map_key(struct, field), - do: valid_struct?(info, struct, count - 1) - - defp valid_struct?([], _struct, 0), - do: true - - defp valid_struct?(_fields, _struct, _count), - do: false - defp to_assoc({key, value}, opts, sep) do {key_doc, opts} = to_doc_with_opts(key, opts) {value_doc, opts} = to_doc_with_opts(value, opts) diff --git a/lib/elixir/lib/inspect/algebra.ex b/lib/elixir/lib/inspect/algebra.ex index 44b7f94377a..99b8d5cce55 100644 --- a/lib/elixir/lib/inspect/algebra.ex +++ b/lib/elixir/lib/inspect/algebra.ex @@ -396,7 +396,7 @@ defmodule Inspect.Algebra do def to_doc_with_opts(term, opts) def to_doc_with_opts(%_{} = struct, %Inspect.Opts{inspect_fun: fun} = opts) do - if opts.structs and Inspect.Map.valid_struct?(struct) do + if opts.structs and valid_struct?(struct) do try do fun.(struct, opts) rescue @@ -453,6 +453,26 @@ defmodule Inspect.Algebra do fun.(arg, opts) |> pack_opts(opts) end + defp valid_struct?(%module{} = struct) do + try do + module.__info__(:struct) + rescue + _ -> false + else + info -> + valid_struct?(info, struct, map_size(struct) - 1) + end + end + + defp valid_struct?([%{field: field} | info], struct, count) when is_map_key(struct, field), + do: valid_struct?(info, struct, count - 1) + + defp valid_struct?([], _struct, 0), + do: true + + defp valid_struct?(_fields, _struct, _count), + do: false + defp pack_opts({_doc, %Inspect.Opts{}} = doc_opts, _opts), do: doc_opts defp pack_opts(doc, opts), do: {doc, opts}