%% SPDX-License-Identifier: Apache-2.0 %% SPDX-FileCopyrightText: 2021 The Elixir Team %% SPDX-FileCopyrightText: 2012 Plataformatec %% Helpers related to dispatching to imports and references. %% This module access the information stored on the scope %% by elixir_import and therefore assumes it is normalized (ordsets) -module(elixir_dispatch). -export([dispatch_import/6, dispatch_require/7, require_function/5, import_function/4, expand_import/7, expand_require/6, check_deprecated/6, default_functions/0, default_macros/0, default_requires/0, find_import/4, find_imports/3, format_error/1]). -include("elixir.hrl"). -import(ordsets, [is_element/2]). -define(kernel, 'Elixir.Kernel'). -define(application, 'Elixir.Application'). default_functions() -> [{?kernel, elixir_imported_functions()}]. default_macros() -> [{?kernel, elixir_imported_macros()}]. default_requires() -> ['Elixir.Application', 'Elixir.Kernel']. %% This is used by elixir_quote. Note we don't record the %% import locally because at that point there is no %% ambiguity. find_import(Meta, Name, Arity, E) -> Tuple = {Name, Arity}, case find_import_by_name_arity(Meta, Tuple, [], E) of {function, Receiver} -> elixir_env:trace({imported_function, Meta, Receiver, Name, Arity}, E), Receiver; {macro, Receiver} -> elixir_env:trace({imported_macro, Meta, Receiver, Name, Arity}, E), Receiver; {ambiguous, _} = Ambiguous -> elixir_errors:file_error(Meta, E, ?MODULE, {import, Ambiguous, Name, Arity}); _ -> false end. find_imports(Meta, Name, E) -> Funs = ?key(E, functions), Macs = ?key(E, macros), Acc0 = #{}, Acc1 = find_imports_by_name(Funs, Acc0, Name, Meta, E), Acc2 = find_imports_by_name(Macs, Acc1, Name, Meta, E), lists:sort(maps:to_list(Acc2)). %% Function retrieval import_function(Meta, Name, Arity, E) -> Tuple = {Name, Arity}, case find_import_by_name_arity(Meta, Tuple, [], E) of {function, Receiver} -> elixir_env:trace({imported_function, Meta, Receiver, Name, Arity}, E), elixir_import:record(Tuple, Receiver, ?key(E, module), ?key(E, function)), remote_function(Meta, Receiver, Name, Arity, E); {macro, _Receiver} -> false; {import, Receiver} -> require_function(Meta, Receiver, Name, Arity, E); {ambiguous, _} = Ambiguous -> elixir_errors:file_error(Meta, E, ?MODULE, {import, Ambiguous, Name, Arity}); false -> case elixir_import:special_form(Name, Arity) of true -> false; false -> Function = ?key(E, function), case (Function /= nil) andalso (Function /= Tuple) andalso elixir_def:local_for(Meta, Name, Arity, [defmacro, defmacrop], E) of false -> elixir_env:trace({local_function, Meta, Name, Arity}, E), {local, Name, Arity}; _ -> false end end end. require_function(Meta, Receiver, Name, Arity, E) -> Required = is_element(Receiver, ?key(E, requires)), case is_macro(Name, Arity, Receiver, Required) of true -> false; false -> elixir_env:trace({remote_function, Meta, Receiver, Name, Arity}, E), remote_function(Meta, Receiver, Name, Arity, E) end. remote_function(Meta, Receiver, Name, Arity, E) -> check_deprecated(function, Meta, Receiver, Name, Arity, E), case elixir_rewrite:inline(Receiver, Name, Arity) of {AR, AN} -> {remote, AR, AN, Arity}; false -> {remote, Receiver, Name, Arity} end. %% Dispatches dispatch_import(Meta, Name, Args, S, E, Callback) -> Arity = length(Args), AllowLocals = %% If we are inside a function, we support reading from locals. case E of #{function := {N, A}} when Name =/= N; Arity =/= A -> true; _ -> false end, case expand_import(Meta, Name, Arity, E, [], AllowLocals, true) of {macro, Receiver, Expander} -> check_deprecated(macro, Meta, Receiver, Name, Arity, E), Caller = {?line(Meta), S, E}, expand_quoted(Meta, Receiver, Name, Arity, Expander(Args, Caller), S, E); {function, Receiver, NewName} -> case elixir_rewrite:inline(Receiver, NewName, Arity) of {AR, AN} -> Callback({AR, AN}); false -> check_deprecated(function, Meta, Receiver, Name, Arity, E), Callback({Receiver, NewName}) end; not_found -> Callback(local); Error -> elixir_errors:file_error(Meta, E, ?MODULE, {import, Error, Name, Arity}) end. dispatch_require(Meta, Receiver, Name, Args, S, E, Callback) when is_atom(Receiver) -> Arity = length(Args), case elixir_rewrite:inline(Receiver, Name, Arity) of {AR, AN} -> elixir_env:trace({remote_function, Meta, Receiver, Name, Arity}, E), Callback(AR, AN); false -> case expand_require(Meta, Receiver, Name, Arity, E, true) of {macro, Receiver, Expander} -> check_deprecated(macro, Meta, Receiver, Name, Arity, E), Caller = {?line(Meta), S, E}, expand_quoted(Meta, Receiver, Name, Arity, Expander(Args, Caller), S, E); error -> check_deprecated(function, Meta, Receiver, Name, Arity, E), elixir_env:trace({remote_function, Meta, Receiver, Name, Arity}, E), Callback(Receiver, Name) end end; dispatch_require(_Meta, Receiver, Name, _Args, _S, _E, Callback) -> Callback(Receiver, Name). %% Macros expansion expand_import(Meta, Name, Arity, E, Extra, AllowLocals, Trace) -> Tuple = {Name, Arity}, Module = ?key(E, module), Dispatch = find_import_by_name_arity(Meta, Tuple, Extra, E), case Dispatch of {ambiguous, Ambiguous} -> {ambiguous, Ambiguous}; {import, _} -> do_expand_import(Dispatch, Meta, Name, Arity, Module, E, Trace); _ -> Local = AllowLocals andalso elixir_def:local_for(Meta, Name, Arity, [defmacro, defmacrop], E), case Dispatch of %% There is a local and an import. This is a conflict unless %% the receiver is the same as module (happens on bootstrap). {_, Receiver} when Local /= false, Receiver /= Module -> {conflict, Receiver}; %% There is no local. Dispatch the import. _ when Local == false -> do_expand_import(Dispatch, Meta, Name, Arity, Module, E, Trace); %% Dispatch to the local. _ -> Trace andalso elixir_env:trace({local_macro, Meta, Name, Arity}, E), {macro, Module, expander_macro_fun(Meta, Local, Module, Name, E)} end end. do_expand_import(Result, Meta, Name, Arity, Module, E, Trace) -> case Result of {function, Receiver} -> Trace andalso begin elixir_env:trace({imported_function, Meta, Receiver, Name, Arity}, E), elixir_import:record({Name, Arity}, Receiver, Module, ?key(E, function)) end, {function, Receiver, Name}; {macro, Receiver} -> Trace andalso begin elixir_env:trace({imported_macro, Meta, Receiver, Name, Arity}, E), elixir_import:record({Name, Arity}, Receiver, Module, ?key(E, function)) end, {macro, Receiver, expander_macro_named(Meta, Receiver, Name, Arity, E)}; {import, Receiver} -> case expand_require(true, Meta, Receiver, Name, Arity, E, Trace) of {macro, _, _} = Response -> Response; error -> {function, Receiver, Name} end; false when Module == ?kernel -> case elixir_rewrite:inline(Module, Name, Arity) of {AR, AN} -> {function, AR, AN}; false -> not_found end; false -> not_found end. expand_require(Meta, Receiver, Name, Arity, E, Trace) -> Required = (Receiver == ?key(E, module)) orelse (lists:keyfind(required, 1, Meta) == {required, true}) orelse is_element(Receiver, ?key(E, requires)), expand_require(Required, Meta, Receiver, Name, Arity, E, Trace). expand_require(Required, Meta, Receiver, Name, Arity, E, Trace) -> case is_macro(Name, Arity, Receiver, Required) of true -> Trace andalso elixir_env:trace({remote_macro, Meta, Receiver, Name, Arity}, E), {macro, Receiver, expander_macro_named(Meta, Receiver, Name, Arity, E)}; false -> error end. %% Expansion helpers expander_macro_fun(Meta, Fun, Receiver, Name, E) -> fun(Args, Caller) -> expand_macro_fun(Meta, Fun, Receiver, Name, Args, Caller, E) end. expander_macro_named(Meta, Receiver, Name, Arity, E) -> ProperName = elixir_utils:macro_name(Name), ProperArity = Arity + 1, Fun = fun Receiver:ProperName/ProperArity, fun(Args, Caller) -> expand_macro_fun(Meta, Fun, Receiver, Name, Args, Caller, E) end. expand_macro_fun(Meta, Fun, Receiver, Name, Args, Caller, E) -> try apply(Fun, [Caller | Args]) catch Kind:Reason:Stacktrace -> Arity = length(Args), MFA = {Receiver, elixir_utils:macro_name(Name), Arity+1}, Info = [{Receiver, Name, Arity, [{file, "expanding macro"}]}, caller(?line(Meta), E)], erlang:raise(Kind, Reason, prune_stacktrace(Stacktrace, MFA, Info, {ok, Caller})) end. expand_quoted(Meta, Receiver, Name, Arity, Quoted, S, E) -> Next = elixir_module:next_counter(?key(E, module)), try ToExpand = elixir_quote:linify_with_context_counter(Meta, {Receiver, Next}, Quoted), elixir_expand:expand(ToExpand, S, E) catch Kind:Reason:Stacktrace -> MFA = {Receiver, elixir_utils:macro_name(Name), Arity+1}, Info = [{Receiver, Name, Arity, [{file, "expanding macro"}]}, caller(?line(Meta), E)], erlang:raise(Kind, Reason, prune_stacktrace(Stacktrace, MFA, Info, error)) end. caller(Line, E) -> elixir_utils:caller(Line, ?key(E, file), ?key(E, module), ?key(E, function)). %% Helpers find_imports_by_name([{Mod, Imports} | ModImports], Acc, Name, Meta, E) -> NewAcc = find_imports_by_name(Name, Imports, Acc, Mod, Meta, E), find_imports_by_name(ModImports, NewAcc, Name, Meta, E); find_imports_by_name([], Acc, _Name, _Meta, _E) -> Acc. find_imports_by_name(Name, [{Name, Arity} | Imports], Acc, Mod, Meta, E) -> case Acc of #{Arity := OtherMod} -> Error = {import, {ambiguous, [Mod, OtherMod]}, Name, Arity}, elixir_errors:file_error(Meta, E, ?MODULE, Error); #{} -> find_imports_by_name(Name, Imports, Acc#{Arity => Mod}, Mod, Meta, E) end; find_imports_by_name(Name, [{ImportName, _} | Imports], Acc, Mod, Meta, E) when Name > ImportName -> find_imports_by_name(Name, Imports, Acc, Mod, Meta, E); find_imports_by_name(_Name, _Imports, Acc, _Mod, _Meta, _E) -> Acc. find_import_by_name_arity(Meta, {_Name, Arity} = Tuple, Extra, E) -> case is_import(Meta, Arity) of {import, _} = Import -> Import; false -> Funs = ?key(E, functions), Macs = Extra ++ ?key(E, macros), FunMatch = find_import_by_name_arity(Tuple, Funs), MacMatch = find_import_by_name_arity(Tuple, Macs), case {FunMatch, MacMatch} of {[], [Receiver]} -> {macro, Receiver}; {[Receiver], []} -> {function, Receiver}; {[], []} -> false; _ -> {ambiguous, FunMatch ++ MacMatch} end end. find_import_by_name_arity(Tuple, List) -> [Receiver || {Receiver, Set} <- List, is_element(Tuple, Set)]. is_import(Meta, Arity) -> case lists:keyfind(imports, 1, Meta) of {imports, [_ | _] = Imports} -> case lists:keyfind(context, 1, Meta) of {context, _} -> case lists:keyfind(Arity, 1, Imports) of {Arity, Receiver} -> {import, Receiver}; false -> false end; false -> false end; _ -> false end. % %% We've reached the macro wrapper fun, skip it with the rest prune_stacktrace([{_, _, [Caller | _], _} | _], _MFA, Info, {ok, Caller}) -> Info; %% We've reached the invoked macro, skip it prune_stacktrace([{M, F, A, _} | _], {M, F, A}, Info, _E) -> Info; %% We've reached the elixir_dispatch internals, skip it with the rest prune_stacktrace([{Mod, _, _, _} | _], _MFA, Info, _E) when Mod == elixir_dispatch; Mod == elixir_exp -> Info; prune_stacktrace([H | T], MFA, Info, E) -> [H | prune_stacktrace(T, MFA, Info, E)]; prune_stacktrace([], _MFA, Info, _E) -> Info. %% ERROR HANDLING format_error({import, {conflict, Receiver}, Name, Arity}) -> io_lib:format("call to local macro ~ts/~B conflicts with imported ~ts.~ts/~B, " "please rename the local macro or remove the conflicting import", [Name, Arity, elixir_aliases:inspect(Receiver), Name, Arity]); format_error({import, {ambiguous, [Mod1, Mod2 | _]}, Name, Arity}) -> io_lib:format("function ~ts/~B imported from both ~ts and ~ts, call is ambiguous", [Name, Arity, elixir_aliases:inspect(Mod1), elixir_aliases:inspect(Mod2)]); format_error({compile_env, Name, Arity}) -> io_lib:format("Application.~s/~B is discouraged in the module body, use Application.compile_env/3 instead", [Name, Arity]); format_error({deprecated, Mod, '__using__', 1, Message}) -> io_lib:format("use ~s is deprecated. ~s", [elixir_aliases:inspect(Mod), Message]); format_error({deprecated, Mod, Fun, Arity, Message}) -> io_lib:format("~s.~s/~B is deprecated. ~s",[elixir_aliases:inspect(Mod), Fun, Arity, Message]). %% INTROSPECTION is_macro(_Name, _Arity, _Module, false) -> false; is_macro(Name, Arity, Receiver, true) -> try Receiver:'__info__'(macros) of Macros -> is_element({Name, Arity}, Macros) catch error:_ -> false end. %% Deprecations checks only happen at the module body, %% so in there we can try to at least load the module. get_deprecations(Receiver) -> case code:ensure_loaded(Receiver) of {module, Receiver} -> get_info(Receiver, deprecated); _ -> [] end. get_info(Receiver, Key) -> case erlang:function_exported(Receiver, '__info__', 1) of true -> try Receiver:'__info__'(Key) catch error:_ -> [] end; false -> [] end. elixir_imported_functions() -> try ?kernel:'__info__'(functions) catch error:undef -> [] end. elixir_imported_macros() -> try ?kernel:'__info__'(macros) catch error:undef -> [] end. check_deprecated(_, _, erlang, _, _, _) -> ok; check_deprecated(_, _, elixir_def, _, _, _) -> ok; check_deprecated(_, _, elixir_module, _, _, _) -> ok; check_deprecated(_, _, ?kernel, _, _, _) -> ok; check_deprecated(Kind, Meta, ?application, Name, Arity, E) -> case E of #{module := Module, function := nil} when (Module /= nil) or (Kind == macro), (Name == get_env) orelse (Name == fetch_env) orelse (Name == 'fetch_env!') -> elixir_errors:file_warn(Meta, E, ?MODULE, {compile_env, Name, Arity}); _ -> ok end; check_deprecated(Kind, Meta, Receiver, Name, Arity, E) -> %% Any compile time behavior cannot be verified by the runtime group pass. case ((?key(E, function) == nil) or (Kind == macro)) andalso get_deprecations(Receiver) of [_ | _] = Deprecations -> case lists:keyfind({Name, Arity}, 1, Deprecations) of {_, Message} -> elixir_errors:file_warn(Meta, E, ?MODULE, {deprecated, Receiver, Name, Arity, Message}); false -> false end; _ -> ok end.