Merge pull request #3666 from lexmag/import-many-friends
Introduce support for Mod.{A, B, C} on imports, alias, require and use
This commit is contained in:
@@ -3613,17 +3613,31 @@ defmodule Kernel do
|
||||
please define it a module which will be imported accordingly.
|
||||
"""
|
||||
defmacro use(module, opts \\ []) do
|
||||
expanded = Macro.expand(module, __CALLER__)
|
||||
|
||||
case is_atom(expanded) do
|
||||
false ->
|
||||
raise ArgumentError, "invalid arguments for use, expected an atom or alias as argument"
|
||||
true ->
|
||||
calls = Enum.map(expand_aliases(module, __CALLER__), fn
|
||||
expanded when is_atom(expanded) ->
|
||||
quote do
|
||||
require unquote(expanded)
|
||||
unquote(expanded).__using__(unquote(opts))
|
||||
end
|
||||
end
|
||||
_otherwise ->
|
||||
raise ArgumentError, "invalid arguments for use, expected an atom or alias as argument"
|
||||
end)
|
||||
quote(do: (unquote_splicing calls))
|
||||
end
|
||||
|
||||
defp expand_aliases({{:., _, [base, :{}]}, _, refs}, env) do
|
||||
base = Macro.expand(base, env)
|
||||
Enum.map(refs, fn
|
||||
{:__aliases__, _, ref} ->
|
||||
Module.concat([base | ref])
|
||||
ref when is_atom(ref) ->
|
||||
Module.concat(base, ref)
|
||||
other -> other
|
||||
end)
|
||||
end
|
||||
|
||||
defp expand_aliases(module, env) do
|
||||
[Macro.expand(module, env)]
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -46,6 +46,20 @@ expand({'__aliases__', _, _} = Alias, E) ->
|
||||
|
||||
%% alias
|
||||
|
||||
expand({Kind, Meta, [{{'.', _, [Base, '{}']}, _, Refs} | Rest]}, E)
|
||||
when Kind == alias; Kind == require; Kind == import ->
|
||||
case Rest of
|
||||
[] ->
|
||||
expand_multi_alias_call(Kind, Meta, Base, Refs, [], E);
|
||||
[Opts] ->
|
||||
case lists:keymember(as, 1, Opts) of
|
||||
true ->
|
||||
compile_error(Meta, ?m(E, file),
|
||||
":as option is not supported by multi-alias call");
|
||||
false ->
|
||||
expand_multi_alias_call(Kind, Meta, Base, Refs, Opts, E)
|
||||
end
|
||||
end;
|
||||
expand({alias, Meta, [Ref]}, E) ->
|
||||
expand({alias, Meta, [Ref, []]}, E);
|
||||
expand({alias, Meta, [Ref, KV]}, E) ->
|
||||
@@ -362,6 +376,20 @@ expand(Other, E) ->
|
||||
|
||||
%% Helpers
|
||||
|
||||
expand_multi_alias_call(Kind, Meta, Base, Refs, Opts, E) ->
|
||||
{BaseRef, EB} = expand_without_aliases_report(Base, E),
|
||||
Fun = fun
|
||||
({'__aliases__', _, Ref}, ER) ->
|
||||
expand({Kind, Meta, [elixir_aliases:concat([BaseRef | Ref]), Opts]}, ER);
|
||||
(Ref, ER) when is_atom(Ref) ->
|
||||
expand({Kind, Meta, [elixir_aliases:concat([BaseRef, Ref]), Opts]}, ER);
|
||||
(Other, _ER) ->
|
||||
compile_error(Meta, ?m(E, file),
|
||||
"invalid argument for ~ts, expected a compile time atom or alias, got: ~ts",
|
||||
[atom_to_list(Kind), 'Elixir.Macro':to_string(Other)])
|
||||
end,
|
||||
lists:mapfoldl(Fun, EB, Refs).
|
||||
|
||||
expand_list([{'|', Meta, [_, _] = Args}], Fun, Acc, List) ->
|
||||
{EArgs, EAcc} = lists:mapfoldl(Fun, Acc, Args),
|
||||
expand_list([], Fun, EAcc, [{'|', Meta, EArgs}|List]);
|
||||
|
||||
@@ -21,7 +21,8 @@ Nonterminals
|
||||
call_args_no_parens_many_strict
|
||||
stab stab_eoe stab_expr stab_maybe_expr stab_parens_many
|
||||
kw_eol kw_base kw call_args_no_parens_kw_expr call_args_no_parens_kw
|
||||
dot_op dot_alias dot_identifier dot_op_identifier dot_do_identifier
|
||||
dot_op dot_alias dot_alias_container
|
||||
dot_identifier dot_op_identifier dot_do_identifier
|
||||
dot_paren_identifier dot_bracket_identifier
|
||||
do_block fn_eoe do_eoe end_eoe block_eoe block_item block_list
|
||||
.
|
||||
@@ -407,6 +408,10 @@ dot_identifier -> matched_expr dot_op identifier : build_dot('$2', '$1', '$3').
|
||||
|
||||
dot_alias -> aliases : {'__aliases__', meta_from_token('$1', 0), ?exprs('$1')}.
|
||||
dot_alias -> matched_expr dot_op aliases : build_dot_alias('$2', '$1', '$3').
|
||||
dot_alias -> matched_expr dot_op dot_alias_container : build_dot_container('$2', '$1', '$3').
|
||||
|
||||
dot_alias_container -> open_curly '}' : [].
|
||||
dot_alias_container -> open_curly container_args close_curly : '$2'.
|
||||
|
||||
dot_op_identifier -> op_identifier : '$1'.
|
||||
dot_op_identifier -> matched_expr dot_op op_identifier : build_dot('$2', '$1', '$3').
|
||||
@@ -634,6 +639,10 @@ build_dot_alias(_Dot, Atom, {'aliases', _, _} = Token) when is_atom(Atom) ->
|
||||
build_dot_alias(Dot, Other, {'aliases', _, Right}) ->
|
||||
{'__aliases__', meta_from_token(Dot), [Other|Right]}.
|
||||
|
||||
build_dot_container(Dot, Left, Right) ->
|
||||
Meta = meta_from_token(Dot),
|
||||
{{'.', Meta, [Left, '{}']}, Meta, Right}.
|
||||
|
||||
build_dot(Dot, Left, Right) ->
|
||||
{'.', meta_from_token(Dot), [Left, extract_identifier(Right)]}.
|
||||
|
||||
|
||||
@@ -44,6 +44,14 @@ defmodule Kernel.AliasTest do
|
||||
test "nested elixir alias" do
|
||||
assert Kernel.AliasTest.Elixir.sample == 1
|
||||
end
|
||||
|
||||
test "multi-call" do
|
||||
alias unquote(Inspect).{
|
||||
Opts, Algebra,
|
||||
}
|
||||
assert %Opts{} == %Inspect.Opts{}
|
||||
assert Algebra.empty == :doc_nil
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Kernel.AliasNestingGenerator do
|
||||
|
||||
@@ -838,6 +838,20 @@ defmodule Kernel.ErrorsTest do
|
||||
'''
|
||||
end
|
||||
|
||||
test "bad multi-call" do
|
||||
assert_compile_fail CompileError,
|
||||
"nofile:1: invalid argument for alias, expected a compile time atom or alias, got: 42",
|
||||
'alias IO.{ANSI, 42}'
|
||||
|
||||
assert_compile_fail CompileError,
|
||||
"nofile:1: :as option is not supported by multi-alias call",
|
||||
'alias Elixir.{Map}, as: Dict'
|
||||
|
||||
assert_compile_fail UndefinedFunctionError,
|
||||
"undefined function: List.{}/1",
|
||||
'[List.{Chars}, "one"]'
|
||||
end
|
||||
|
||||
test "macros error stacktrace" do
|
||||
assert [{:erlang, :+, [1, :foo], _},
|
||||
{Kernel.ErrorsTest.MacrosErrorStacktrace, :sample, 1, _}|_] =
|
||||
|
||||
@@ -12,6 +12,24 @@ defmodule Kernel.ImportTest do
|
||||
end
|
||||
end
|
||||
|
||||
test "multi-call" do
|
||||
import Elixir.{List, unquote(:String)}
|
||||
assert keymember?([a: 1], :a, 0)
|
||||
assert valid_character?("ø")
|
||||
end
|
||||
|
||||
test "blank multi-call" do
|
||||
import List.{}
|
||||
# Buggy local duplicate is untouched
|
||||
assert duplicate([1], 2) == [1]
|
||||
end
|
||||
|
||||
test "multi-call with options" do
|
||||
import Elixir.{List}, only: []
|
||||
# Buggy local duplicate is untouched
|
||||
assert duplicate([1], 2) == [1]
|
||||
end
|
||||
|
||||
test "import all" do
|
||||
import :lists
|
||||
assert flatten([1, [2], 3]) == [1, 2, 3]
|
||||
|
||||
@@ -647,4 +647,57 @@ defmodule KernelTest do
|
||||
defp a_list, do: [1, 2, 3]
|
||||
defp a_nil, do: nil
|
||||
end
|
||||
|
||||
defmodule UseMacro do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
import ExUnit.CaptureIO
|
||||
|
||||
defmodule SampleA do
|
||||
defmacro __using__(opts) do
|
||||
prefix = Keyword.get(opts, :prefix, "")
|
||||
IO.puts(prefix <> "A")
|
||||
end
|
||||
end
|
||||
|
||||
defmodule SampleB do
|
||||
defmacro __using__(_) do
|
||||
IO.puts("B")
|
||||
end
|
||||
end
|
||||
|
||||
test "invalid argument" do
|
||||
message = "invalid arguments for use, expected an atom or alias as argument"
|
||||
assert_raise ArgumentError, message, fn ->
|
||||
Code.eval_string("use 42")
|
||||
end
|
||||
end
|
||||
|
||||
test "multi-call" do
|
||||
assert capture_io(fn ->
|
||||
Code.eval_string("use UseMacro.{SampleA, SampleB,}", [], __ENV__)
|
||||
end) == "A\nB\n"
|
||||
end
|
||||
|
||||
test "multi-call with options" do
|
||||
assert capture_io(fn ->
|
||||
Code.eval_string(~S|use UseMacro.{SampleA}, prefix: "-"|, [], __ENV__)
|
||||
end) == "-A\n"
|
||||
end
|
||||
|
||||
test "multi-call with unquote" do
|
||||
assert capture_io(fn ->
|
||||
Code.eval_string("""
|
||||
defmodule TestMod do
|
||||
def main() do
|
||||
use UseMacro.{SampleB, unquote(:SampleA)}
|
||||
end
|
||||
end
|
||||
""", [], __ENV__)
|
||||
end) == "B\nA\n"
|
||||
after
|
||||
:code.purge(UseMacro.TestMod)
|
||||
:code.delete(UseMacro.TestMod)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user