Files
n8n-openai-adapter/lib/elixir/src/elixir_map.erl
T
José Valim d74dff2bcf Transform the struct update syntax into a type assertion
This transforms the struct update into a type assertion,
requiring the type system to be sure the expression has
precisely the given struct type.

The struct update syntax may still be deprecated in the
future but this will provide a safer migration path and
allow us to engage in more conversations with the community.
2025-06-11 13:39:14 +02:00

305 lines
12 KiB
Erlang

%% SPDX-License-Identifier: Apache-2.0
%% SPDX-FileCopyrightText: 2021 The Elixir Team
%% SPDX-FileCopyrightText: 2012 Plataformatec
-module(elixir_map).
-export([expand_map/4, expand_struct/5, format_error/1, maybe_load_struct_info/5]).
-import(elixir_errors, [function_error/4, file_error/4, file_warn/4]).
-include("elixir.hrl").
expand_map(Meta, [{'|', UpdateMeta, [Left, Right]}], S, #{context := nil} = E) ->
{[ELeft | ERight], SE, EE} = elixir_expand:expand_args([Left | Right], S, E),
validate_kv(Meta, ERight, Right, E),
{{'%{}', Meta, [{'|', UpdateMeta, [ELeft, ERight]}]}, SE, EE};
expand_map(Meta, [{'|', _, [_, _]}] = Args, _S, #{context := Context, file := File}) ->
file_error(Meta, File, ?MODULE, {update_syntax_in_wrong_context, Context, {'%{}', Meta, Args}});
expand_map(Meta, Args, S, E) ->
{EArgs, SE, EE} = elixir_expand:expand_args(Args, S, E),
validate_kv(Meta, EArgs, Args, E),
{{'%{}', Meta, EArgs}, SE, EE}.
expand_struct(Meta, Left, {'%{}', MapMeta, MapArgs}, S, #{context := Context} = E) ->
CleanMapArgs = delete_struct_key(Meta, MapArgs, E),
{[ELeft, ERight], SE, EE} = elixir_expand:expand_args([Left, {'%{}', MapMeta, CleanMapArgs}], S, E),
case validate_struct(ELeft, Context) of
true when is_atom(ELeft) ->
case ERight of
{'%{}', MapMeta, [{'|', _, [_, Assocs]}]} ->
%% The update syntax for structs is deprecated,
%% so we return only the update syntax downstream.
%% TODO: Remove me on Elixir v2.0
_ = load_struct_info(Meta, ELeft, Assocs, EE),
{{'%', Meta, [ELeft, ERight]}, SE, EE};
{'%{}', MapMeta, Assocs} when Context /= match ->
AssocKeys = [K || {K, _} <- Assocs],
Struct = load_struct(Meta, ELeft, Assocs, EE),
Keys = ['__struct__'] ++ AssocKeys,
WithoutKeys = lists:sort(maps:to_list(maps:without(Keys, Struct))),
StructAssocs = elixir_quote:escape(WithoutKeys, none, false),
{{'%', Meta, [ELeft, {'%{}', MapMeta, StructAssocs ++ Assocs}]}, SE, EE};
{'%{}', MapMeta, Assocs} ->
_ = load_struct_info(Meta, ELeft, Assocs, EE),
{{'%', Meta, [ELeft, ERight]}, SE, EE}
end;
true ->
{{'%', Meta, [ELeft, ERight]}, SE, EE};
false when Context == match ->
file_error(Meta, E, ?MODULE, {invalid_struct_name_in_match, ELeft});
false ->
file_error(Meta, E, ?MODULE, {invalid_struct_name, ELeft})
end;
expand_struct(Meta, _Left, Right, _S, E) ->
file_error(Meta, E, ?MODULE, {non_map_after_struct, Right}).
delete_struct_key(Meta, [{'|', PipeMeta, [Left, MapAssocs]}], E) ->
[{'|', PipeMeta, [Left, delete_struct_key_assoc(Meta, MapAssocs, E)]}];
delete_struct_key(Meta, MapAssocs, E) ->
delete_struct_key_assoc(Meta, MapAssocs, E).
delete_struct_key_assoc(Meta, Assocs, E) ->
case lists:keytake('__struct__', 1, Assocs) of
{value, _, CleanAssocs} ->
file_warn(Meta, ?key(E, file), ?MODULE, ignored_struct_key_in_struct),
CleanAssocs;
false ->
Assocs
end.
validate_match_key(Meta, {Name, _, Context}, E) when is_atom(Name), is_atom(Context) ->
file_error(Meta, E, ?MODULE, {invalid_variable_in_map_key_match, Name});
validate_match_key(Meta, {'::', _, [Left, _]}, E) ->
validate_match_key(Meta, Left, E);
validate_match_key(_, {'^', _, [{Name, _, Context}]}, _) when is_atom(Name), is_atom(Context) ->
ok;
validate_match_key(_, {'%{}', _, [_ | _]}, _) ->
ok;
validate_match_key(Meta, {Left, _, Right}, E) ->
validate_match_key(Meta, Left, E),
validate_match_key(Meta, Right, E);
validate_match_key(Meta, {Left, Right}, E) ->
validate_match_key(Meta, Left, E),
validate_match_key(Meta, Right, E);
validate_match_key(Meta, List, E) when is_list(List) ->
[validate_match_key(Meta, Each, E) || Each <- List];
validate_match_key(_, _, _) ->
ok.
validate_not_repeated(Meta, Key, Used, E) ->
case is_literal(Key) andalso Used of
#{Key := true} ->
case E of
#{context := match} -> function_error(Meta, ?key(E, file), ?MODULE, {repeated_key, Key});
_ -> file_warn(Meta, ?key(E, file), ?MODULE, {repeated_key, Key})
end,
Used;
#{} ->
Used#{Key => true};
false ->
Used
end.
is_literal({_, _, _}) -> false;
is_literal({Left, Right}) -> is_literal(Left) andalso is_literal(Right);
is_literal([_ | _] = List) -> lists:all(fun is_literal/1, List);
is_literal(_) -> true.
validate_kv(Meta, KV, Original, #{context := Context} = E) ->
lists:foldl(fun
({K, _V}, {Index, Used}) ->
(Context == match) andalso validate_match_key(Meta, K, E),
NewUsed = validate_not_repeated(Meta, K, Used, E),
{Index + 1, NewUsed};
(_, {Index, _Used}) ->
file_error(Meta, E, ?MODULE, {not_kv_pair, lists:nth(Index, Original)})
end, {1, #{}}, KV).
validate_struct({'^', _, [{Var, _, Ctx}]}, match) when is_atom(Var), is_atom(Ctx) -> true;
validate_struct({Var, _Meta, Ctx}, match) when is_atom(Var), is_atom(Ctx) -> true;
validate_struct(Atom, _) when is_atom(Atom) -> true;
validate_struct(_, _) -> false.
load_struct_info(Meta, Name, Assocs, E) ->
assert_struct_assocs(Meta, Assocs, E),
case maybe_load_struct_info(Meta, Name, Assocs, true, E) of
{ok, Info} -> Info;
{error, Desc} -> file_error(Meta, E, ?MODULE, Desc)
end.
maybe_load_struct_info(Meta, Name, Assocs, Trace, E) ->
try
case is_open(Name, Meta, E) andalso lookup_struct_info_from_data_tables(Name) of
%% If I am accessing myself and there is no attribute,
%% don't invoke the fallback to avoid calling loaded code.
false when ?key(E, module) =:= Name -> nil;
false -> Name:'__info__'(struct);
InfoList -> InfoList
end
of
nil ->
{error, detail_undef(Name, E)};
Info ->
Keys = [begin
lists:any(fun(Field) -> ?key(Field, field) =:= Key end, Info) orelse
function_error(Meta, E, ?MODULE, {unknown_key_for_struct, Name, Key}),
Key
end || {Key, _} <- Assocs],
Trace andalso elixir_env:trace({struct_expansion, Meta, Name, Keys}, E),
{ok, Info}
catch
error:undef -> {error, detail_undef(Name, E)}
end.
lookup_struct_info_from_data_tables(Module) ->
try
{Set, _} = elixir_module:data_tables(Module),
ets:lookup_element(Set, {elixir, struct}, 2)
catch
_:_ -> false
end.
load_struct(Meta, Name, Assocs, E) ->
assert_struct_assocs(Meta, Assocs, E),
try
maybe_load_struct(Meta, Name, Assocs, E)
of
{ok, Struct} -> Struct;
{error, Desc} -> file_error(Meta, E, ?MODULE, Desc)
catch
Kind:Reason ->
Info = [{Name, '__struct__', 1, [{file, "expanding struct"}]},
elixir_utils:caller(?line(Meta), ?key(E, file), ?key(E, module), ?key(E, function))],
erlang:raise(Kind, Reason, Info)
end.
maybe_load_struct(Meta, Name, Assocs, E) ->
try
case is_open(Name, Meta, E) andalso elixir_def:external_for(Meta, Name, '__struct__', 1, [def]) of
%% If I am accessing myself and there is no __struct__ function,
%% don't invoke the fallback to avoid calling loaded code.
false when ?key(E, module) =:= Name ->
error(undef);
false ->
Name:'__struct__'(Assocs);
ExternalFun ->
%% There is an inherent race condition when using external_for.
%% By the time we got to execute the function, the ETS table
%% with temporary definitions for the given module may no longer
%% be available, so any function invocation happening inside the
%% local function will fail. In this case, we need to fall back to
%% the regular dispatching since the module will be available if
%% the table has not been deleted (unless compilation of that
%% module failed which should then cause this call to fail too).
try
ExternalFun(Assocs)
catch
error:undef -> Name:'__struct__'(Assocs)
end
end
of
#{'__struct__' := Name} = Struct ->
Keys = [begin
maps:is_key(Key, Struct) orelse
function_error(Meta, E, ?MODULE, {unknown_key_for_struct, Name, Key}),
Key
end || {Key, _} <- Assocs],
elixir_env:trace({struct_expansion, Meta, Name, Keys}, E),
{ok, Struct};
#{'__struct__' := StructName} when is_atom(StructName) ->
{error, {struct_name_mismatch, Name, StructName}};
Other ->
{error, {invalid_struct_return_value, Name, Other}}
catch
error:undef -> {error, detail_undef(Name, E)}
end.
assert_struct_assocs(Meta, Assocs, E) ->
[function_error(Meta, E, ?MODULE, {invalid_key_for_struct, K})
|| {K, _} <- Assocs, not is_atom(K)].
is_open(Name, Meta, E) ->
in_context(Name, E) orelse ((code:ensure_loaded(Name) /= {module, Name}) andalso wait_for_struct(Name, Meta, E)).
in_context(Name, E) ->
%% We also include the current module because it won't be present
%% in context module in case the module name is defined dynamically.
lists:member(Name, [?key(E, module) | ?key(E, context_modules)]).
wait_for_struct(Module, Meta, E) ->
(erlang:get(elixir_compiler_info) /= undefined) andalso
('Elixir.Kernel.ErrorHandler':ensure_compiled(Module, struct, hard, elixir_utils:get_line(Meta, E)) =:= found).
detail_undef(Name, E) ->
case in_context(Name, E) andalso (?key(E, function) == nil) of
true -> {inaccessible_struct, Name};
false -> {undefined_struct, Name}
end.
format_error({update_syntax_in_wrong_context, Context, Expr}) ->
io_lib:format("cannot use map/struct update syntax in ~ts, got: ~ts",
[Context, 'Elixir.Macro':to_string(Expr)]);
format_error({invalid_struct_name_in_match, Expr}) ->
Message =
"expected struct name in a match to be a compile time atom, alias or a "
"variable, got: ~ts",
io_lib:format(Message, ['Elixir.Macro':to_string(Expr)]);
format_error({invalid_struct_name, Expr}) ->
Message = "expected struct name to be a compile time atom or alias, got: ~ts",
io_lib:format(Message, ['Elixir.Macro':to_string(Expr)]);
format_error({invalid_variable_in_map_key_match, Name}) ->
Message =
"cannot use variable ~ts as map key inside a pattern. Map keys in patterns can only be literals "
"(such as atoms, strings, tuples, and the like) or an existing variable matched with the pin operator "
"(such as ^some_var)",
io_lib:format(Message, [Name]);
format_error({repeated_key, Key}) ->
io_lib:format("key ~ts will be overridden in map", ['Elixir.Macro':to_string(Key)]);
format_error({not_kv_pair, Expr}) ->
io_lib:format("expected key-value pairs in a map, got: ~ts",
['Elixir.Macro':to_string(Expr)]);
format_error({non_map_after_struct, Expr}) ->
io_lib:format("expected struct to be followed by a map, got: ~ts",
['Elixir.Macro':to_string(Expr)]);
format_error({struct_name_mismatch, Module, StructName}) ->
Name = elixir_aliases:inspect(Module),
Message = "expected struct name returned by ~ts.__struct__/1 to be ~ts, got: ~ts",
io_lib:format(Message, [Name, Name, elixir_aliases:inspect(StructName)]);
format_error({invalid_struct_return_value, Module, Value}) ->
Message =
"expected ~ts.__struct__/1 to return a map with a :__struct__ key that holds the "
"name of the struct (atom), got: ~ts",
io_lib:format(Message, [elixir_aliases:inspect(Module), 'Elixir.Kernel':inspect(Value)]);
format_error({inaccessible_struct, Module}) ->
Message =
"cannot access struct ~ts, the struct was not yet defined or the struct is "
"being accessed in the same context that defines it",
io_lib:format(Message, [elixir_aliases:inspect(Module)]);
format_error({undefined_struct, Module}) ->
Name = elixir_aliases:inspect(Module),
io_lib:format(
"~ts.__struct__/1 is undefined, cannot expand struct ~ts. "
"Make sure the struct name is correct. If the struct name exists and is correct "
"but it still cannot be found, you likely have cyclic module usage in your code",
[Name, Name]);
format_error({unknown_key_for_struct, Module, Key}) ->
io_lib:format("unknown key ~ts for struct ~ts",
['Elixir.Macro':to_string(Key), elixir_aliases:inspect(Module)]);
format_error({invalid_key_for_struct, Key}) ->
io_lib:format("invalid key ~ts for struct, struct keys must be atoms, got: ",
['Elixir.Macro':to_string(Key)]);
format_error(ignored_struct_key_in_struct) ->
"key :__struct__ is ignored when using structs".