Skip to content

Commit 83a70d7

Browse files
committed
Deprecate struct update syntax
The struct update syntax was added early in Elixir to help validate at compile-time that the update keys were valid. However, for a couple releases already, Elixir's static analysis can perform such validation more reliably and find more error if you pattern match on the struct when the variable is defined instead. This deprecation simplifies the language and pushes developers to better alternatives. Closes #13974.
1 parent 67b6f89 commit 83a70d7

File tree

10 files changed

+50
-196
lines changed

10 files changed

+50
-196
lines changed

lib/elixir/lib/exception.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1387,6 +1387,8 @@ defmodule BadFunctionError do
13871387
end
13881388

13891389
defmodule BadStructError do
1390+
@moduledoc deprecated:
1391+
"This exception is deprecated alongside the struct update syntax that raises it"
13901392
defexception [:struct, :term]
13911393

13921394
@impl true

lib/elixir/lib/kernel/special_forms.ex

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ defmodule Kernel.SpecialForms do
9898
9999
%User{}
100100
101-
Underneath a struct is just a map with a `:__struct__` key
102-
pointing to the `User` module:
101+
Underneath a struct is a map with a `:__struct__` key pointing
102+
to the `User` module, where the keys are validated at compile-time:
103103
104104
%User{} == %{__struct__: User, name: "john", age: 27}
105105
@@ -118,16 +118,7 @@ defmodule Kernel.SpecialForms do
118118
119119
%User{full_name: "john doe"}
120120
121-
An update operation specific for structs is also available:
122-
123-
%User{user | age: 28}
124-
125-
Once again, the syntax above will guarantee the given keys
126-
are valid at compilation time and it will guarantee at runtime
127-
the given argument is a struct, failing with `BadStructError`
128-
otherwise. The map update syntax can also be used for updating
129-
structs, and it is useful when you want to update any struct,
130-
regardless of their name, as long as they have matching fields:
121+
The map update syntax can also be used for updating structs:
131122
132123
%{user | age: 28}
133124

lib/elixir/lib/map_set.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -440,8 +440,8 @@ defmodule MapSet do
440440
defimpl Inspect do
441441
import Inspect.Algebra
442442

443-
def inspect(map_set, opts) do
444-
opts = %Inspect.Opts{opts | charlists: :as_lists}
443+
def inspect(map_set, %Inspect.Opts{} = opts) do
444+
opts = %{opts | charlists: :as_lists}
445445
concat(["MapSet.new(", Inspect.List.inspect(MapSet.to_list(map_set), opts), ")"])
446446
end
447447
end

lib/elixir/lib/module/types/expr.ex

Lines changed: 10 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -225,48 +225,23 @@ defmodule Module.Types.Expr do
225225
end
226226

227227
# %Struct{map | ...}
228+
# This syntax is deprecated, so we simply traverse.
228229
def of_expr(
229-
{:%, struct_meta, [module, {:%{}, _, [{:|, update_meta, [map, args]}]}]} = struct,
230-
expected,
230+
{:%, _, [_, {:%{}, _, [{:|, _, [map, args]}]}]} = struct,
231+
_expected,
231232
expr,
232233
stack,
233234
context
234235
) do
235-
if stack.mode == :traversal do
236-
{_, context} = of_expr(map, term(), struct, stack, context)
236+
{_, context} = of_expr(map, term(), struct, stack, context)
237237

238-
context =
239-
Enum.reduce(args, context, fn {key, value}, context when is_atom(key) ->
240-
{_, context} = of_expr(value, term(), expr, stack, context)
241-
context
242-
end)
238+
context =
239+
Enum.reduce(args, context, fn {key, value}, context when is_atom(key) ->
240+
{_, context} = of_expr(value, term(), expr, stack, context)
241+
context
242+
end)
243243

244-
{dynamic(), context}
245-
else
246-
{info, context} = Of.struct_info(module, struct_meta, stack, context)
247-
struct_type = Of.struct_type(module, info)
248-
{map_type, context} = of_expr(map, struct_type, struct, stack, context)
249-
250-
if compatible?(map_type, struct_type) do
251-
map_type = map_put!(map_type, :__struct__, atom([module]))
252-
253-
Enum.reduce(args, {map_type, context}, fn
254-
{key, value}, {map_type, context} when is_atom(key) ->
255-
# TODO: Once we support typed structs, we need to type check them here.
256-
expected_value_type =
257-
case map_fetch(expected, key) do
258-
{_, expected_value_type} -> expected_value_type
259-
_ -> term()
260-
end
261-
262-
{value_type, context} = of_expr(value, expected_value_type, expr, stack, context)
263-
{map_put!(map_type, key, value_type), context}
264-
end)
265-
else
266-
warning = {:badstruct, struct, struct_type, map_type, context}
267-
{error_type(), error(__MODULE__, warning, update_meta, stack, context)}
268-
end
269-
end
244+
{dynamic(), context}
270245
end
271246

272247
# %{...}
@@ -792,13 +767,6 @@ defmodule Module.Types.Expr do
792767
defp flatten_when({:when, _meta, [left, right]}), do: [left | flatten_when(right)]
793768
defp flatten_when(other), do: [other]
794769

795-
defp map_put!(map_type, key, value_type) do
796-
case map_put(map_type, key, value_type) do
797-
{:ok, descr} -> descr
798-
error -> raise "unexpected #{inspect(error)}"
799-
end
800-
end
801-
802770
defp repack_match(left_expr, {:=, meta, [new_left, new_right]}),
803771
do: repack_match({:=, meta, [left_expr, new_left]}, new_right)
804772

@@ -807,31 +775,6 @@ defmodule Module.Types.Expr do
807775

808776
## Warning formatting
809777

810-
def format_diagnostic({:badstruct, expr, expected_type, actual_type, context}) do
811-
traces = collect_traces(expr, context)
812-
813-
%{
814-
details: %{typing_traces: traces},
815-
message:
816-
IO.iodata_to_binary([
817-
"""
818-
incompatible types in struct update:
819-
820-
#{expr_to_string(expr) |> indent(4)}
821-
822-
expected type:
823-
824-
#{to_quoted_string(expected_type) |> indent(4)}
825-
826-
but got type:
827-
828-
#{to_quoted_string(actual_type) |> indent(4)}
829-
""",
830-
format_traces(traces)
831-
])
832-
}
833-
end
834-
835778
def format_diagnostic({:badmap, type, expr, context}) do
836779
traces = collect_traces(expr, context)
837780

lib/elixir/src/elixir_erl_pass.erl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,7 @@ translate_struct(Ann, Name, {'%{}', _, [{'|', _, [Update, Assocs]}]}, S) ->
477477
Map = {map, Ann, [{map_field_exact, Ann, {atom, Ann, '__struct__'}, {atom, Ann, Name}}]},
478478

479479
Match = {match, Ann, Var, Map},
480+
%% Once this is removed, we should remove badstruct handling from elixir_erl_try
480481
Error = {tuple, Ann, [{atom, Ann, badstruct}, {atom, Ann, Name}, Var]},
481482

482483
{TUpdate, TU} = translate(Update, Ann, VS),

lib/elixir/src/elixir_map.erl

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,29 @@ expand_map(Meta, Args, S, E) ->
1515
{{'%{}', Meta, EArgs}, SE, EE}.
1616

1717
expand_struct(Meta, Left, {'%{}', MapMeta, MapArgs}, S, #{context := Context} = E) ->
18-
CleanMapArgs = clean_struct_key_from_map_args(Meta, MapArgs, E),
18+
CleanMapArgs = delete_struct_key(Meta, MapArgs, E),
1919
{[ELeft, ERight], SE, EE} = elixir_expand:expand_args([Left, {'%{}', MapMeta, CleanMapArgs}], S, E),
2020

2121
case validate_struct(ELeft, Context) of
2222
true when is_atom(ELeft) ->
23-
case extract_struct_assocs(Meta, ERight, E) of
24-
{expand, MapMeta, Assocs} when Context /= match -> %% Expand
23+
case ERight of
24+
{'%{}', MapMeta, [{'|', _, [_, Assocs]}]} ->
25+
%% The update syntax for structs is deprecated,
26+
%% so we return only the update syntax downstream.
27+
%% TODO: Remove me on Elixir v2.0
28+
file_warn(MapMeta, ?key(E, file), ?MODULE, {deprecated_update, ELeft, ERight}),
29+
_ = load_struct_info(Meta, ELeft, Assocs, EE),
30+
{{'%', Meta, [ELeft, ERight]}, SE, EE};
31+
32+
{'%{}', MapMeta, Assocs} when Context /= match ->
2533
AssocKeys = [K || {K, _} <- Assocs],
2634
Struct = load_struct(Meta, ELeft, Assocs, EE),
2735
Keys = ['__struct__'] ++ AssocKeys,
2836
WithoutKeys = lists:sort(maps:to_list(maps:without(Keys, Struct))),
2937
StructAssocs = elixir_quote:escape(WithoutKeys, none, false),
3038
{{'%', Meta, [ELeft, {'%{}', MapMeta, StructAssocs ++ Assocs}]}, SE, EE};
3139

32-
{_, _, Assocs} -> %% Update or match
40+
{'%{}', MapMeta, Assocs} ->
3341
_ = load_struct_info(Meta, ELeft, Assocs, EE),
3442
{{'%', Meta, [ELeft, ERight]}, SE, EE}
3543
end;
@@ -46,12 +54,12 @@ expand_struct(Meta, Left, {'%{}', MapMeta, MapArgs}, S, #{context := Context} =
4654
expand_struct(Meta, _Left, Right, _S, E) ->
4755
file_error(Meta, E, ?MODULE, {non_map_after_struct, Right}).
4856

49-
clean_struct_key_from_map_args(Meta, [{'|', PipeMeta, [Left, MapAssocs]}], E) ->
50-
[{'|', PipeMeta, [Left, clean_struct_key_from_map_assocs(Meta, MapAssocs, E)]}];
51-
clean_struct_key_from_map_args(Meta, MapAssocs, E) ->
52-
clean_struct_key_from_map_assocs(Meta, MapAssocs, E).
57+
delete_struct_key(Meta, [{'|', PipeMeta, [Left, MapAssocs]}], E) ->
58+
[{'|', PipeMeta, [Left, delete_struct_key_assoc(Meta, MapAssocs, E)]}];
59+
delete_struct_key(Meta, MapAssocs, E) ->
60+
delete_struct_key_assoc(Meta, MapAssocs, E).
5361

54-
clean_struct_key_from_map_assocs(Meta, Assocs, E) ->
62+
delete_struct_key_assoc(Meta, Assocs, E) ->
5563
case lists:keytake('__struct__', 1, Assocs) of
5664
{value, _, CleanAssocs} ->
5765
file_warn(Meta, ?key(E, file), ?MODULE, ignored_struct_key_in_struct),
@@ -110,16 +118,6 @@ validate_kv(Meta, KV, Original, #{context := Context} = E) ->
110118
file_error(Meta, E, ?MODULE, {not_kv_pair, lists:nth(Index, Original)})
111119
end, {1, #{}}, KV).
112120

113-
extract_struct_assocs(_, {'%{}', Meta, [{'|', _, [_, Assocs]}]}, _) ->
114-
{update, Meta, delete_struct_key(Assocs)};
115-
extract_struct_assocs(_, {'%{}', Meta, Assocs}, _) ->
116-
{expand, Meta, delete_struct_key(Assocs)};
117-
extract_struct_assocs(Meta, Other, E) ->
118-
file_error(Meta, E, ?MODULE, {non_map_after_struct, Other}).
119-
120-
delete_struct_key(Assocs) ->
121-
lists:keydelete('__struct__', 1, Assocs).
122-
123121
validate_struct({'^', _, [{Var, _, Ctx}]}, match) when is_atom(Var), is_atom(Ctx) -> true;
124122
validate_struct({Var, _Meta, Ctx}, match) when is_atom(Var), is_atom(Ctx) -> true;
125123
validate_struct(Atom, _) when is_atom(Atom) -> true;
@@ -301,4 +299,9 @@ format_error({invalid_key_for_struct, Key}) ->
301299
io_lib:format("invalid key ~ts for struct, struct keys must be atoms, got: ",
302300
['Elixir.Macro':to_string(Key)]);
303301
format_error(ignored_struct_key_in_struct) ->
304-
"key :__struct__ is ignored when using structs".
302+
"key :__struct__ is ignored when using structs";
303+
format_error({deprecated_update, Struct, MapUpdate}) ->
304+
io_lib:format("the struct update syntax is deprecated:\n\n~ts\n\n"
305+
"Instead, prefer to pattern matching on structs when the variable is first defined and "
306+
"use the regular map update syntax instead:\n\n~ts\n",
307+
['Elixir.Macro':to_string({'%', [], [Struct, MapUpdate]}), 'Elixir.Macro':to_string(MapUpdate)]).

lib/elixir/test/elixir/calendar/datetime_test.exs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -483,8 +483,7 @@ defmodule DateTimeTest do
483483
assert DateTime.to_unix(gregorian_0) == -62_167_219_200
484484
assert DateTime.to_unix(Map.from_struct(gregorian_0)) == -62_167_219_200
485485

486-
min_datetime = %DateTime{gregorian_0 | year: -9999}
487-
486+
min_datetime = %{gregorian_0 | year: -9999}
488487
assert DateTime.to_unix(min_datetime) == -377_705_116_800
489488
end
490489

lib/elixir/test/elixir/map_test.exs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -283,12 +283,8 @@ defmodule MapTest do
283283

284284
test "structs" do
285285
assert %ExternalUser{} == %{__struct__: ExternalUser, name: "john", age: 27}
286-
287286
assert %ExternalUser{name: "meg"} == %{__struct__: ExternalUser, name: "meg", age: 27}
288287

289-
user = %ExternalUser{}
290-
assert %ExternalUser{user | name: "meg"} == %{__struct__: ExternalUser, name: "meg", age: 27}
291-
292288
%ExternalUser{name: name} = %ExternalUser{}
293289
assert name == "john"
294290
end

lib/elixir/test/elixir/module/types/expr_test.exs

Lines changed: 0 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -875,87 +875,6 @@ defmodule Module.Types.ExprTest do
875875
"""
876876
end
877877

878-
test "updating structs" do
879-
assert typecheck!([x], %Point{x | x: :zero}) ==
880-
dynamic(
881-
closed_map(__struct__: atom([Point]), x: atom([:zero]), y: term(), z: term())
882-
)
883-
884-
assert typecheck!([x], %Point{%Point{x | x: :zero} | y: :one}) ==
885-
dynamic(
886-
closed_map(
887-
__struct__: atom([Point]),
888-
x: atom([:zero]),
889-
y: atom([:one]),
890-
z: term()
891-
)
892-
)
893-
894-
assert typeerror!(
895-
(
896-
x = %{x: 0}
897-
%Point{x | x: :zero}
898-
)
899-
) ==
900-
~l"""
901-
incompatible types in struct update:
902-
903-
%Point{x | x: :zero}
904-
905-
expected type:
906-
907-
dynamic(%Point{x: term(), y: term(), z: term()})
908-
909-
but got type:
910-
911-
%{x: integer()}
912-
913-
where "x" was given the type:
914-
915-
# type: %{x: integer()}
916-
# from: types_test.ex:LINE-4
917-
x = %{x: 0}
918-
"""
919-
end
920-
921-
test "inference on struct update" do
922-
assert typecheck!(
923-
[x],
924-
(
925-
%Point{x | x: :zero}
926-
x
927-
)
928-
) ==
929-
dynamic(closed_map(__struct__: atom([Point]), x: term(), y: term(), z: term()))
930-
931-
assert typeerror!(
932-
[x],
933-
(
934-
x.w
935-
%Point{x | x: :zero}
936-
)
937-
) ==
938-
~l"""
939-
incompatible types in struct update:
940-
941-
%Point{x | x: :zero}
942-
943-
expected type:
944-
945-
dynamic(%Point{x: term(), y: term(), z: term()})
946-
947-
but got type:
948-
949-
dynamic(%{..., w: term()})
950-
951-
where "x" was given the type:
952-
953-
# type: dynamic(%{..., w: term()})
954-
# from: types_test.ex:LINE-4
955-
x.w
956-
"""
957-
end
958-
959878
test "nested map" do
960879
assert typecheck!([x = %{}], x.foo.bar) == dynamic()
961880
end

lib/ex_unit/lib/ex_unit/diff.ex

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -218,19 +218,19 @@ defmodule ExUnit.Diff do
218218
# Guards
219219

220220
defp diff_guard({:when, _, [expression, clause]}, right, env) do
221-
{diff_expression, post_env} = diff_quoted(expression, right, nil, env)
221+
{diff, post_env} = diff_quoted(expression, right, nil, env)
222222

223223
{guard_clause, guard_equivalent?} =
224-
if diff_expression.equivalent? do
224+
if diff.equivalent? do
225225
bindings = Map.merge(post_env.pins, post_env.current_vars)
226226
diff_guard_clause(clause, bindings)
227227
else
228228
{clause, false}
229229
end
230230

231-
diff = %__MODULE__{
232-
diff_expression
233-
| left: {:when, [], [diff_expression.left, guard_clause]},
231+
diff = %{
232+
diff
233+
| left: {:when, [], [diff.left, guard_clause]},
234234
equivalent?: guard_equivalent?
235235
}
236236

@@ -825,10 +825,10 @@ defmodule ExUnit.Diff do
825825
end
826826

827827
defp diff_string_concat(left, nil, indexes, _left_length, right, env) do
828-
{parsed_diff, parsed_post_env} = diff_string(left, right, ?", env)
829-
left_diff = rebuild_concat_string(parsed_diff.left, nil, indexes)
828+
{diff, parsed_post_env} = diff_string(left, right, ?", env)
829+
left_diff = rebuild_concat_string(diff.left, nil, indexes)
830830

831-
diff = %__MODULE__{parsed_diff | left: left_diff}
831+
diff = %{diff | left: left_diff}
832832
{diff, parsed_post_env}
833833
end
834834

0 commit comments

Comments
 (0)