diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index a67893f418..c9785994f4 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -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 """ diff --git a/lib/elixir/src/elixir_exp.erl b/lib/elixir/src/elixir_exp.erl index 7fac487305..4776716a8d 100644 --- a/lib/elixir/src/elixir_exp.erl +++ b/lib/elixir/src/elixir_exp.erl @@ -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]); diff --git a/lib/elixir/src/elixir_parser.yrl b/lib/elixir/src/elixir_parser.yrl index 90e802f8dd..c59387ef40 100644 --- a/lib/elixir/src/elixir_parser.yrl +++ b/lib/elixir/src/elixir_parser.yrl @@ -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)]}. diff --git a/lib/elixir/test/elixir/kernel/alias_test.exs b/lib/elixir/test/elixir/kernel/alias_test.exs index c5190c5c0b..86b71e3b09 100644 --- a/lib/elixir/test/elixir/kernel/alias_test.exs +++ b/lib/elixir/test/elixir/kernel/alias_test.exs @@ -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 diff --git a/lib/elixir/test/elixir/kernel/errors_test.exs b/lib/elixir/test/elixir/kernel/errors_test.exs index beabf08697..549074c88a 100644 --- a/lib/elixir/test/elixir/kernel/errors_test.exs +++ b/lib/elixir/test/elixir/kernel/errors_test.exs @@ -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, _}|_] = diff --git a/lib/elixir/test/elixir/kernel/import_test.exs b/lib/elixir/test/elixir/kernel/import_test.exs index a42492d70b..97bbc18448 100644 --- a/lib/elixir/test/elixir/kernel/import_test.exs +++ b/lib/elixir/test/elixir/kernel/import_test.exs @@ -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] diff --git a/lib/elixir/test/elixir/kernel_test.exs b/lib/elixir/test/elixir/kernel_test.exs index f74649b8a5..889b5e154a 100644 --- a/lib/elixir/test/elixir/kernel_test.exs +++ b/lib/elixir/test/elixir/kernel_test.exs @@ -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