From 66716b0f60e008be25a532b3df8bc14be5e8349a Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 6 Sep 2026 00:00:18 +0200 Subject: [PATCH] Fix inferred map requirements for mixed update keys Require the whole update key type to be an atom before inferring a required singleton field. Cover the inferred domain and warning-free calls with mixed atom and integer keys. Assisted-by: Codex:GPT-6 --- lib/elixir/lib/module/types/expr.ex | 9 ++++-- .../test/elixir/module/types/infer_test.exs | 29 +++++++++++++++++++ .../elixir/module/types/integration_test.exs | 17 +++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index c1e8f7d4023..9708d676f85 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -182,11 +182,14 @@ defmodule Module.Types.Expr do end) # The only information we can attach to the expected types is that - # certain keys are expected. + # certain keys are expected. The whole key type must be a singleton atom, + # as atom_fetch/1 may return only the atom subset of a gradual type. expected_pairs = Enum.flat_map(pairs_types, fn {key_type, _value_type} -> - case atom_fetch(key_type) do - {:finite, [key]} -> [{key, {term(), false}}] + with true <- subtype?(key_type, atom()), + {:finite, [key]} <- atom_fetch(key_type) do + [{key, {term(), false}}] + else _ -> [] end end) diff --git a/lib/elixir/test/elixir/module/types/infer_test.exs b/lib/elixir/test/elixir/module/types/infer_test.exs index 6e0960919ed..d3ccb154f41 100644 --- a/lib/elixir/test/elixir/module/types/infer_test.exs +++ b/lib/elixir/test/elixir/module/types/infer_test.exs @@ -292,6 +292,35 @@ defmodule Module.Types.InferTest do ) end + test "from map updates with mixed atom and non-atom keys", config do + types = + infer config do + def replace(map, flag) do + key = if flag, do: :a, else: 1 + %{map | key => :new} + end + end + + assert {:infer, _, [{[map, flag], _}]} = types[{:replace, 2}] + assert map == open_map() + assert equal?(flag, term()) + end + + test "from map updates with singleton atom keys", config do + types = + infer config do + def replace(map) do + key = key() + %{map | key => :new} + end + + defp key, do: :a + end + + assert {:infer, _, [{[map], _}]} = types[{:replace, 1}] + assert map == open_map(a: {term(), false}) + end + test "from captures", config do types = infer config do diff --git a/lib/elixir/test/elixir/module/types/integration_test.exs b/lib/elixir/test/elixir/module/types/integration_test.exs index a9cca324daa..4b2749c7c3f 100644 --- a/lib/elixir/test/elixir/module/types/integration_test.exs +++ b/lib/elixir/test/elixir/module/types/integration_test.exs @@ -209,6 +209,23 @@ defmodule Module.Types.IntegrationTest do end describe "type checking" do + test "map updates with mixed atom and non-atom keys" do + files = %{ + "a.ex" => """ + defmodule A do + def replace(map, flag) do + key = if flag, do: :a, else: 1 + %{map | key => :new} + end + + def run, do: replace(%{1 => :old}, false) + end + """ + } + + assert_no_warnings(files) + end + test "inferred remote calls" do files = %{ "a.ex" => """