Skip to content

Commit 28211b2

Browse files
author
José Valim
committed
Ensure aliases expansion also works for implicit aliases
1 parent 109c07e commit 28211b2

File tree

10 files changed

+108
-40
lines changed

10 files changed

+108
-40
lines changed

lib/elixir/include/elixir.hrl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@
2525
counter=[], %% a counter for the variables defined
2626
local=nil, %% the scope to evaluate local functions against
2727
scheduled=[], %% scheduled modules to be loaded
28-
file, %% the current scope filename
28+
macro_aliases=[], %% keep aliases defined inside a macro
2929
aliases, %% an orddict with aliases by new -> old names
30+
file, %% the current scope filename
3031
requires, %% a set with modules required
3132
macros, %% a list with macros imported by module
3233
functions}). %% a list with functions imported by module

lib/elixir/lib/kernel/special_forms.ex

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,7 @@ defmodule Kernel.SpecialForms do
452452
453453
### Hygiene in aliases
454454
455-
Aliases inside quote are expanded by default.
455+
Aliases inside quote are hygienic by default.
456456
Consider the following example:
457457
458458
defmodule Hygiene do
@@ -464,24 +464,43 @@ defmodule Kernel.SpecialForms do
464464
end
465465
466466
require Hygiene
467-
Hygiene.no_interference #=> HashDict[]
467+
Hygiene.no_interference #=> #HashDict<[]>
468468
469469
Notice that, even though the alias `D` is not available
470470
in the context the macro is expanded, the code above works
471-
because `D` was expanded when the quote was generated.
471+
because `D` still expands to `HashDict`.
472472
473-
There are two ways to disable this behaviour. By disabling
474-
hygiene for aliases or by using the `alias!` macro inside
475-
the quote:
473+
In some particular cases you may want to access an alias
474+
or a module defined in the caller. In such scenarios, you
475+
can access it by disabling hygiene with `hygiene: [aliases: false]`
476+
or by using the `alias!` macro inside the quote:
476477
477-
defmodule NoHygiene do
478+
defmodule Hygiene do
479+
# This will expand to Elixir.Nested.hello
480+
defmacro no_interference do
481+
quote do: Nested.hello
482+
end
483+
484+
# This will expand to Nested.hello for
485+
# whatever is Nested in the caller
478486
defmacro interference do
479-
quote do: alias!(D).new
487+
quote do: alias!(Nested).hello
480488
end
481489
end
482490
483-
require NoHygiene
484-
NoHygiene.interference #=> UndefinedFunctionError
491+
defmodule Parent do
492+
defmodule Nested do
493+
def hello, do: "world"
494+
end
495+
496+
require Hygiene
497+
Hygiene.no_interference
498+
#=> ** (UndefinedFunctionError) ...
499+
500+
Hygiene.interference
501+
#=> "world"
502+
end
503+
485504
486505
## Hygiene in imports
487506
@@ -564,7 +583,7 @@ defmodule Kernel.SpecialForms do
564583
particular, the macro `__FILE__` and exceptions happening inside
565584
the quote will always point to `GenServer.Behaviour` file.
566585
"""
567-
defmacro quote(opts, do: contents)
586+
defmacro quote(opts, block)
568587

569588
@doc """
570589
When used inside quoting, marks that the variable should

lib/elixir/lib/macro.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,7 @@ defmodule Macro do
443443
def expand(aliases, env)
444444

445445
def expand({ :__aliases__, _, _ } = original, env) do
446-
case :elixir_aliases.expand(original, env.aliases) do
446+
case :elixir_aliases.expand(original, env.aliases, []) do
447447
atom when is_atom(atom) -> atom
448448
aliases ->
449449
aliases = lc alias inlist aliases, do: expand(alias, env)

lib/elixir/src/elixir_aliases.erl

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,46 @@
11
-module(elixir_aliases).
22
-export([nesting_alias/2, last/1, concat/1, safe_concat/1,
3-
format_error/1, ensure_loaded/3, expand/2]).
3+
format_error/1, ensure_loaded/3, expand/3]).
44
-include("elixir.hrl").
55
-compile({parse_transform, elixir_transform}).
66

77
%% Expand an alias. It returns an atom (meaning that there
88
%% was an expansion) or a list of atoms.
99

10-
expand({ '__aliases__', Meta, _ } = Alias, Aliases) ->
10+
expand({ '__aliases__', Meta, _ } = Alias, Aliases, MacroAliases) ->
1111
case lists:keyfind(alias, 1, Meta) of
12+
{ alias, false } ->
13+
expand(Alias, MacroAliases);
1214
{ alias, Atom } when is_atom(Atom) ->
13-
Atom;
15+
case expand(Alias, MacroAliases) of
16+
OtherAtom when is_atom(OtherAtom) -> OtherAtom;
17+
OtherAliases when is_list(OtherAliases) -> Atom
18+
end;
1419
false ->
15-
do_expand(Alias, Aliases)
20+
expand(Alias, Aliases)
1621
end.
1722

18-
do_expand({ '__aliases__', _Meta, [H] }, Aliases) when H /= 'Elixir' ->
19-
case do_expand_one(H, Aliases) of
23+
expand({ '__aliases__', _Meta, [H] }, Aliases) when H /= 'Elixir' ->
24+
case expand_one(H, Aliases) of
2025
false -> [H];
2126
Atom -> Atom
2227
end;
2328

24-
do_expand({ '__aliases__', _Meta, [H|T] }, Aliases) when is_atom(H) ->
29+
expand({ '__aliases__', _Meta, [H|T] }, Aliases) when is_atom(H) ->
2530
case H of
2631
'Elixir' ->
2732
concat(T);
2833
_ ->
29-
case do_expand_one(H, Aliases) of
34+
case expand_one(H, Aliases) of
3035
false -> [H|T];
3136
Atom -> concat([Atom|T])
3237
end
3338
end;
3439

35-
do_expand({ '__aliases__', _Meta, List }, _Aliases) ->
40+
expand({ '__aliases__', _Meta, List }, _Aliases) ->
3641
List.
3742

38-
do_expand_one(H, Aliases) ->
43+
expand_one(H, Aliases) ->
3944
Lookup = list_to_atom("Elixir-" ++ atom_to_list(H)),
4045
case lookup(Lookup, Aliases) of
4146
Lookup -> false;

lib/elixir/src/elixir_dispatch.erl

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,9 +182,13 @@ expand_macro_named(Meta, Receiver, Name, Arity, Args, Module, Requires, SEnv) ->
182182
translate_expansion(Meta, Tree, S) ->
183183
{ TR, TS } = elixir_translator:translate_each(
184184
elixir_quote:linify(?line(Meta), Tree),
185-
S#elixir_scope{check_clauses=false}
185+
S#elixir_scope{
186+
check_clauses=false,
187+
macro_aliases=[] }
186188
),
187-
{ TR, TS#elixir_scope{check_clauses=S#elixir_scope.check_clauses} }.
189+
{ TR, TS#elixir_scope{
190+
check_clauses=S#elixir_scope.check_clauses,
191+
macro_aliases=S#elixir_scope.macro_aliases } }.
188192

189193
%% Helpers
190194

lib/elixir/src/elixir_macros.erl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,9 @@ translate({defmodule, Meta, [Ref, KV]}, S) ->
200200

201201
RS = case elixir_aliases:nesting_alias(S#elixir_scope.module, FullModule) of
202202
{ New, Old } ->
203-
S#elixir_scope{aliases=orddict:store(New, Old, S#elixir_scope.aliases)};
203+
S#elixir_scope{
204+
aliases=orddict:store(New, Old, S#elixir_scope.aliases),
205+
macro_aliases=orddict:store(New, Old, S#elixir_scope.macro_aliases)};
204206
false ->
205207
S
206208
end,
@@ -333,7 +335,7 @@ expand_module(Raw, Module, #elixir_scope{module=Nesting}) when is_atom(Raw); Nes
333335
Module;
334336

335337
expand_module({ '__aliases__', _, _ } = Alias, Module, S) ->
336-
case elixir_aliases:expand(Alias, S#elixir_scope.aliases) of
338+
case elixir_aliases:expand(Alias, S#elixir_scope.aliases, S#elixir_scope.macro_aliases) of
337339
Atom when is_atom(Atom) ->
338340
Module;
339341
Aliases when is_list(Aliases) ->

lib/elixir/src/elixir_quote.erl

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,18 +54,18 @@ do_quote({ unquote, _Meta, [Expr] }, #elixir_quote{unquote=true}, S) ->
5454
do_quote({ 'alias!', _Meta, [Expr] }, Q, S) ->
5555
do_quote(Expr, Q#elixir_quote{aliases_hygiene=false}, S);
5656

57-
do_quote({ '__aliases__', RawMeta, [H|T] } = Alias, #elixir_quote{aliases_hygiene=true} = Q, S) when is_atom(H) and (H /= 'Elixir') ->
58-
Meta = case elixir_aliases:expand(Alias, S#elixir_scope.aliases) of
59-
Atom when is_atom(Atom) -> [{alias,Atom}|RawMeta];
60-
Aliases when is_list(Aliases) -> RawMeta
57+
do_quote({ '__aliases__', Meta, [H|T] } = Alias, #elixir_quote{aliases_hygiene=true} = Q, S) when is_atom(H) and (H /= 'Elixir') ->
58+
Annotation = case elixir_aliases:expand(Alias, S#elixir_scope.aliases, S#elixir_scope.macro_aliases) of
59+
Atom when is_atom(Atom) -> Atom;
60+
Aliases when is_list(Aliases) -> false
6161
end,
6262

6363
Line = ?line(Meta),
6464
{ TAliases, SA } = do_quote([H|T], Q, S),
6565

6666
{ { tuple, Line, [
6767
{ atom, Line, '__aliases__' },
68-
meta(Meta, Q),
68+
meta([{alias,Annotation}|Meta], Q),
6969
TAliases
7070
] }, SA };
7171

lib/elixir/src/elixir_scope.erl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ filename(Other) -> element(3, Other).
9696
serialize(S) ->
9797
elixir_tree_helpers:abstract_syntax(
9898
{ S#elixir_scope.file, S#elixir_scope.functions, S#elixir_scope.check_clauses,
99-
S#elixir_scope.requires, S#elixir_scope.macros, S#elixir_scope.aliases, S#elixir_scope.scheduled }
99+
S#elixir_scope.requires, S#elixir_scope.macros, S#elixir_scope.aliases,
100+
S#elixir_scope.macro_aliases, S#elixir_scope.scheduled }
100101
).
101102

102103
serialize_with_vars(Line, S) when is_integer(Line) ->
@@ -114,14 +115,15 @@ serialize_with_vars(Line, S) when is_integer(Line) ->
114115

115116
deserialize(Tuple) -> deserialize_with_vars(Tuple, []).
116117

117-
deserialize_with_vars({ File, Functions, CheckClauses, Requires, Macros, Aliases, Scheduled }, Vars) ->
118+
deserialize_with_vars({ File, Functions, CheckClauses, Requires, Macros, Aliases, MacroAliases, Scheduled }, Vars) ->
118119
#elixir_scope{
119120
file=File,
120121
functions=Functions,
121122
check_clauses=CheckClauses,
122123
requires=Requires,
123124
macros=Macros,
124125
aliases=Aliases,
126+
macro_aliases=MacroAliases,
125127
scheduled=Scheduled,
126128
vars=orddict:from_list(Vars),
127129
counter=[{'',length(Vars)}]

lib/elixir/src/elixir_translator.erl

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ translate_each({ alias, Meta, [Ref, KV] }, S) ->
107107
{ as, false } ->
108108
{ Old, SR };
109109
{ as, Other } ->
110-
{ TOther, SA } = translate_each(Other, SR#elixir_scope{aliases=[]}),
110+
{ TOther, SA } = translate_each(no_alias_expansion(Other),
111+
SR#elixir_scope{aliases=[],macro_aliases=[]}),
111112
case TOther of
112113
{ atom, _, Atom } -> { Atom, SA };
113114
_ -> syntax_error(Meta, S#elixir_scope.file,
@@ -127,7 +128,8 @@ translate_each({ alias, Meta, [Ref, KV] }, S) ->
127128
end,
128129

129130
{ { nil, ?line(Meta) }, SF#elixir_scope{
130-
aliases=orddict:store(New, Old, S#elixir_scope.aliases)
131+
aliases=orddict:store(New, Old, S#elixir_scope.aliases),
132+
macro_aliases=orddict:store(New, Old, S#elixir_scope.macro_aliases)
131133
} }
132134
end;
133135
_ ->
@@ -213,7 +215,7 @@ translate_each({ '__CALLER__', Meta, Atom }, S) when is_atom(Atom) ->
213215
%% Aliases
214216

215217
translate_each({ '__aliases__', Meta, _ } = Alias, S) ->
216-
case elixir_aliases:expand(Alias, S#elixir_scope.aliases) of
218+
case elixir_aliases:expand(Alias, S#elixir_scope.aliases, S#elixir_scope.macro_aliases) of
217219
Atom when is_atom(Atom) -> { { atom, ?line(Meta), Atom }, S };
218220
Aliases ->
219221
{ TAliases, SA } = translate_args(Aliases, S),
@@ -555,6 +557,12 @@ expand_var_context(Meta, Alias, Msg, S) ->
555557
syntax_error(Meta, S#elixir_scope.file, "~ts, expected a compile time available alias or an atom", [Msg])
556558
end.
557559

560+
no_alias_expansion({ '__aliases__', Meta, [H|T] }) when (H /= 'Elixir') and is_atom(H) ->
561+
{ '__aliases__', Meta, ['Elixir',H|T] };
562+
563+
no_alias_expansion(Other) ->
564+
Other.
565+
558566
%% Translate args
559567

560568
%% Variables in arguments are not propagated from one

lib/elixir/test/elixir/kernel/quote_test.exs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,14 +159,41 @@ defmodule Kernel.QuoteTest.AliasHygieneTest do
159159

160160
alias Dict, as: SuperDict
161161

162-
test :expand_aliases do
163-
assert quote(do: Foo.Bar) == { :__aliases__, [], [:Foo, :Bar] }
164-
assert quote(do: Dict.Bar) == { :__aliases__, [], [:Dict, :Bar] }
162+
defmacro dict do
163+
quote do: Dict.Bar
164+
end
165+
166+
defmacro super_dict do
167+
quote do: SuperDict.Bar
168+
end
169+
170+
test :annotate_aliases do
171+
assert quote(do: Foo.Bar) == { :__aliases__, [alias: false], [:Foo, :Bar] }
172+
assert quote(do: Dict.Bar) == { :__aliases__, [alias: false], [:Dict, :Bar] }
165173
assert quote(do: SuperDict.Bar) == { :__aliases__, [alias: Dict.Bar], [:SuperDict, :Bar] }
166174
assert quote(do: alias!(SuperDict.Bar)) == { :__aliases__, [], [:SuperDict, :Bar] }
175+
end
167176

177+
test :expand_aliases do
168178
assert Code.eval_quoted(quote do: SuperDict.Bar) == { Elixir.Dict.Bar, [] }
169179
assert Code.eval_quoted(quote do: alias!(SuperDict.Bar)) == { Elixir.SuperDict.Bar, [] }
180+
181+
assert Code.eval_quoted(quote do
182+
alias HashDict, as: SuperDict
183+
SuperDict.Bar
184+
end) == { Elixir.HashDict.Bar, [] }
185+
186+
assert Code.eval_quoted(quote do
187+
alias HashDict, as: Dict
188+
require Kernel.QuoteTest.AliasHygieneTest
189+
Kernel.QuoteTest.AliasHygieneTest.dict
190+
end) == { Elixir.Dict.Bar, [] }
191+
192+
assert Code.eval_quoted(quote do
193+
alias HashDict, as: SuperDict
194+
require Kernel.QuoteTest.AliasHygieneTest
195+
Kernel.QuoteTest.AliasHygieneTest.super_dict
196+
end) == { Elixir.Dict.Bar, [] }
170197
end
171198
end
172199

0 commit comments

Comments
 (0)