Skip to content

Commit 35eef87

Browse files
committed
Allow file, line and context to be dynamically set on quote, closes #9721
1 parent fa091c6 commit 35eef87

File tree

6 files changed

+142
-98
lines changed

6 files changed

+142
-98
lines changed

lib/elixir/src/elixir.hrl

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,6 @@
1616
stacktrace=nil %% holds information about the stacktrace variable
1717
}).
1818

19-
-record(elixir_quote, {
20-
line=false,
21-
file=nil,
22-
context=nil,
23-
vars_hygiene=true,
24-
aliases_hygiene=true,
25-
imports_hygiene=true,
26-
unquote=true,
27-
generated=false
28-
}).
29-
3019
-record(elixir_tokenizer, {
3120
file=(<<"nofile">>),
3221
terminators=[],

lib/elixir/src/elixir_expand.erl

Lines changed: 10 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -156,31 +156,16 @@ expand({quote, Meta, [Opts, Do]}, E) when is_list(Do) ->
156156
ValidOpts = [context, location, line, file, unquote, bind_quoted, generated],
157157
{EOpts, ET} = expand_opts(Meta, quote, ValidOpts, Opts, E),
158158

159-
Context = case lists:keyfind(context, 1, EOpts) of
160-
{context, Ctx} when is_atom(Ctx) and (Ctx /= nil) ->
161-
Ctx;
162-
{context, Ctx} ->
163-
form_error(Meta, E, ?MODULE, {invalid_context_for_quote, Ctx});
164-
false ->
165-
case ?key(E, module) of
166-
nil -> 'Elixir';
167-
Mod -> Mod
168-
end
169-
end,
159+
Context = proplists:get_value(context, EOpts, case ?key(E, module) of
160+
nil -> 'Elixir';
161+
Mod -> Mod
162+
end),
170163

171164
{File, Line} = case lists:keyfind(location, 1, EOpts) of
172165
{location, keep} ->
173166
{elixir_utils:relative_to_cwd(?key(E, file)), false};
174167
false ->
175-
{ case lists:keyfind(file, 1, EOpts) of
176-
{file, F} -> F;
177-
false -> nil
178-
end,
179-
180-
case lists:keyfind(line, 1, EOpts) of
181-
{line, L} -> L;
182-
false -> false
183-
end }
168+
{proplists:get_value(file, EOpts, nil), proplists:get_value(line, EOpts, false)}
184169
end,
185170

186171
{Binding, DefaultUnquote} = case lists:keyfind(bind_quoted, 1, EOpts) of
@@ -191,20 +176,14 @@ expand({quote, Meta, [Opts, Do]}, E) when is_list(Do) ->
191176
false -> form_error(Meta, E, ?MODULE, {invalid_bind_quoted_for_quote, BQ})
192177
end;
193178
false ->
194-
{nil, true}
179+
{[], true}
195180
end,
196181

197-
Unquote = case lists:keyfind(unquote, 1, EOpts) of
198-
{unquote, U} when is_boolean(U) -> U;
199-
false -> DefaultUnquote
200-
end,
201-
202-
Generated = lists:keyfind(generated, 1, EOpts) == {generated, true},
203-
204-
Q = #elixir_quote{line=Line, file=File, unquote=Unquote,
205-
context=Context, generated=Generated},
182+
Unquote = proplists:get_value(unquote, EOpts, DefaultUnquote),
183+
Generated = proplists:get_value(generated, EOpts, false),
206184

207-
Quoted = elixir_quote:quote(Meta, Exprs, Binding, Q, ET),
185+
{Q, Prelude} = elixir_quote:build(Meta, Line, File, Context, Unquote, Generated),
186+
Quoted = elixir_quote:quote(Meta, Exprs, Binding, Q, Prelude, ET),
208187
expand(Quoted, ET);
209188

210189
expand({quote, Meta, [_, _]}, E) ->
@@ -1133,9 +1112,6 @@ format_error({expected_compile_time_module, Kind, GivenTerm}) ->
11331112
format_error({unquote_outside_quote, Unquote}) ->
11341113
%% Unquote can be "unquote" or "unquote_splicing".
11351114
io_lib:format("~p called outside quote", [Unquote]);
1136-
format_error({invalid_context_for_quote, Context}) ->
1137-
io_lib:format("invalid :context for quote, expected non-nil compile time atom or alias, got: ~ts",
1138-
['Elixir.Macro':to_string(Context)]);
11391115
format_error({invalid_bind_quoted_for_quote, BQ}) ->
11401116
io_lib:format("invalid :bind_quoted for quote, expected a keyword list of variable names, got: ~ts",
11411117
['Elixir.Macro':to_string(BQ)]);

lib/elixir/src/elixir_quote.erl

Lines changed: 96 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,71 @@
11
-module(elixir_quote).
2-
-export([escape/3, linify/3, linify_with_context_counter/3, quote/5, has_unquotes/1]).
3-
-export([dot/5, tail_list/3, list/2]). %% Quote callbacks
2+
-export([escape/3, linify/3, linify_with_context_counter/3, build/6, quote/6, has_unquotes/1]).
3+
-export([dot/5, tail_list/3, list/2, validate_runtime/2]). %% Quote callbacks
44

55
-include("elixir.hrl").
66
-define(defs(Kind), Kind == def; Kind == defp; Kind == defmacro; Kind == defmacrop; Kind == '@').
77
-define(lexical(Kind), Kind == import; Kind == alias; Kind == require).
88
-compile({inline, [keyfind/2, keystore/3, keydelete/2, keynew/3, do_tuple_linify/5]}).
99

10+
-record(elixir_quote, {
11+
line=false,
12+
file=nil,
13+
context=nil,
14+
vars_hygiene=true,
15+
aliases_hygiene=true,
16+
imports_hygiene=true,
17+
unquote=true,
18+
generated=false
19+
}).
20+
21+
build(Meta, Line, File, Context, Unquote, Generated) ->
22+
Acc0 = [],
23+
{ELine, Acc1} = validate_compile(Meta, line, Line, Acc0),
24+
{EFile, Acc2} = validate_compile(Meta, file, File, Acc1),
25+
{EContext, Acc3} = validate_compile(Meta, context, Context, Acc2),
26+
validate_runtime(unquote, Unquote),
27+
validate_runtime(generated, Generated),
28+
29+
Q = #elixir_quote{
30+
line=ELine,
31+
file=EFile,
32+
unquote=Unquote,
33+
context=EContext,
34+
generated=Generated
35+
},
36+
37+
{Q, Acc3}.
38+
39+
validate_compile(Meta, Key, Value, Acc) ->
40+
case is_valid(Key, Value) of
41+
true ->
42+
{Value, Acc};
43+
false ->
44+
Var = {Key, Meta, ?MODULE},
45+
Call = {{'.', Meta, [?MODULE, validate_runtime]}, Meta, [Key, Value]},
46+
{Var, [{'=', Meta, [Var, Call]} | Acc]}
47+
end.
48+
49+
validate_runtime(Key, Value) ->
50+
case is_valid(Key, Value) of
51+
true ->
52+
Value;
53+
54+
false ->
55+
erlang:error(
56+
'Elixir.ArgumentError':exception(
57+
<<"invalid value for option :", (erlang:atom_to_binary(Key, utf8))/binary,
58+
"in quote, got: ", ('Elixir.Kernel':inspect(Value))/binary>>
59+
)
60+
)
61+
end.
62+
63+
is_valid(line, Line) -> is_integer(Line) orelse is_boolean(Line);
64+
is_valid(file, File) -> is_binary(File) orelse (File == nil);
65+
is_valid(context, Context) -> is_atom(Context) andalso (Context /= nil);
66+
is_valid(generated, Generated) -> is_boolean(Generated);
67+
is_valid(unquote, Unquote) -> is_boolean(Unquote).
68+
1069
%% Apply the line from site call on quoted contents.
1170
%% Receives a Key to look for the default line as argument.
1271
linify(0, _Key, Exprs) ->
@@ -164,26 +223,31 @@ escape(Expr, Kind, Unquote) ->
164223

165224
%% Quotes an expression and return its quoted Elixir AST.
166225

167-
quote(_Meta, {unquote_splicing, _, [_]}, _Binding, #elixir_quote{unquote=true}, _) ->
226+
quote(_Meta, {unquote_splicing, _, [_]}, _Binding, #elixir_quote{unquote=true}, _, _) ->
168227
argument_error(<<"unquote_splicing only works inside arguments and block contexts, "
169228
"wrap it in parens if you want it to work with one-liners">>);
170229

171-
quote(_Meta, Expr, nil, Q, E) ->
172-
do_quote(Expr, Q, E);
173-
174-
quote(Meta, Expr, Binding, Q, E) ->
230+
quote(Meta, Expr, Binding, Q, Prelude, E) ->
175231
Context = Q#elixir_quote.context,
176-
VarMeta = [Pair || {K, _} = Pair <- Meta, K == counter],
177232

178-
Vars = [ {'{}', [],
179-
[ '=', [], [
180-
{'{}', [], [K, VarMeta, Context]},
233+
Vars = [{'{}', [],
234+
['=', [], [
235+
{'{}', [], [K, Meta, Context]},
181236
V
182-
] ]
237+
]]
183238
} || {K, V} <- Binding],
184239

185-
TExprs = do_quote(Expr, Q, E),
186-
{'{}', [], ['__block__', [], Vars ++ [TExprs]]}.
240+
Quoted = do_quote(Expr, Q, E),
241+
242+
WithVars = case Vars of
243+
[] -> Quoted;
244+
_ -> {'{}', [], ['__block__', [], Vars ++ [Quoted]]}
245+
end,
246+
247+
case Prelude of
248+
[] -> WithVars;
249+
_ -> {'__block__', [], Prelude ++ [WithVars]}
250+
end.
187251

188252
%% Actual quoting and helpers
189253

@@ -224,11 +288,12 @@ do_quote({'__aliases__', Meta, [H | T]} = Alias, #elixir_quote{aliases_hygiene=t
224288

225289
%% Vars
226290

227-
do_quote({Left, Meta, nil}, #elixir_quote{vars_hygiene=true, imports_hygiene=true} = Q, E) when is_atom(Left) ->
228-
do_quote_import(Left, Meta, Q#elixir_quote.context, Q, E);
291+
do_quote({Name, Meta, nil}, #elixir_quote{vars_hygiene=true, imports_hygiene=true} = Q, E) when is_atom(Name) ->
292+
ImportMeta = import_meta(Meta, Name, 0, Q, E),
293+
{'{}', [], [Name, meta(ImportMeta, Q), Q#elixir_quote.context]};
229294

230-
do_quote({Left, Meta, nil}, #elixir_quote{vars_hygiene=true} = Q, E) when is_atom(Left) ->
231-
do_quote_tuple(Left, Meta, Q#elixir_quote.context, Q, E);
295+
do_quote({Name, Meta, nil}, #elixir_quote{vars_hygiene=true} = Q, _E) when is_atom(Name) ->
296+
{'{}', [], [Name, meta(Meta, Q), Q#elixir_quote.context]};
232297

233298
%% Unquote
234299

@@ -244,8 +309,11 @@ do_quote({'&', Meta, [{'/', _, [{F, _, C}, A]}] = Args},
244309
#elixir_quote{imports_hygiene=true} = Q, E) when is_atom(F), is_integer(A), is_atom(C) ->
245310
do_quote_fa('&', Meta, Args, F, A, Q, E);
246311

247-
do_quote({Name, Meta, ArgsOrAtom}, #elixir_quote{imports_hygiene=true} = Q, E) when is_atom(Name) ->
248-
do_quote_import(Name, Meta, ArgsOrAtom, Q, E);
312+
do_quote({Name, Meta, Args}, #elixir_quote{imports_hygiene=true} = Q, E) when is_atom(Name), is_list(Args) ->
313+
do_quote_import(Meta, Name, length(Args), Args, Q, E);
314+
315+
do_quote({Name, Meta, Context}, #elixir_quote{imports_hygiene=true} = Q, E) when is_atom(Name), is_atom(Context) ->
316+
do_quote_import(Meta, Name, 0, Context, Q, E);
249317

250318
%% Two-element tuples
251319

@@ -341,15 +409,8 @@ bad_escape(Arg) ->
341409
"The supported values are: lists, tuples, maps, atoms, numbers, bitstrings, ",
342410
"PIDs and remote functions in the format &Mod.fun/arity">>).
343411

344-
%% do_quote_*
345-
346-
do_quote_import(Name, Meta, ArgsOrAtom, #elixir_quote{imports_hygiene=true} = Q, E) ->
347-
Arity = case is_atom(ArgsOrAtom) of
348-
true -> 0;
349-
false -> length(ArgsOrAtom)
350-
end,
351-
352-
NewMeta = case (keyfind(import, Meta) == false) andalso
412+
import_meta(Meta, Name, Arity, Q, E) ->
413+
case (keyfind(import, Meta) == false) andalso
353414
elixir_dispatch:find_import(Meta, Name, Arity, E) of
354415
false ->
355416
case (Arity == 1) andalso keyfind(ambiguous_op, Meta) of
@@ -358,9 +419,13 @@ do_quote_import(Name, Meta, ArgsOrAtom, #elixir_quote{imports_hygiene=true} = Q,
358419
end;
359420
Receiver ->
360421
keystore(import, keystore(context, Meta, Q#elixir_quote.context), Receiver)
361-
end,
422+
end.
423+
424+
%% do_quote_*
362425

363-
Annotated = annotate({Name, NewMeta, ArgsOrAtom}, Q#elixir_quote.context),
426+
do_quote_import(Meta, Name, Arity, ArgsOrAtom, Q, E) ->
427+
ImportMeta = import_meta(Meta, Name, Arity, Q, E),
428+
Annotated = annotate({Name, ImportMeta, ArgsOrAtom}, Q#elixir_quote.context),
364429
do_quote_tuple(Annotated, Q, E).
365430

366431
do_quote_call(Left, Meta, Expr, Args, Q, E) ->

lib/elixir/test/elixir/kernel/expansion_test.exs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -548,16 +548,6 @@ defmodule Kernel.ExpansionTest do
548548
assert expand(quote(do: quote(do: hello))) == {:{}, [], [:hello, [], __MODULE__]}
549549
end
550550

551-
test "raises if the :context option is invalid" do
552-
assert_raise CompileError, ~r"invalid :context for quote, .*, got: :erlang\.self\(\)", fn ->
553-
expand(quote(do: quote(context: self(), do: :ok)))
554-
end
555-
556-
assert_raise CompileError, ~r"invalid :context for quote, .*, got: nil", fn ->
557-
expand(quote(do: quote(context: nil, do: :ok)))
558-
end
559-
end
560-
561551
test "raises if the :bind_quoted option is invalid" do
562552
assert_raise CompileError, ~r"invalid :bind_quoted for quote", fn ->
563553
expand(quote(do: quote(bind_quoted: self(), do: :ok)))

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

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,39 @@ defmodule Kernel.QuoteTest do
1212
end
1313

1414
test "keep line" do
15-
# DO NOT MOVE THIS LINE
15+
line = __ENV__.line + 2
16+
1617
assert quote(location: :keep, do: bar(1, 2, 3)) ==
17-
{:bar, [keep: {Path.relative_to_cwd(__ENV__.file), 16}], [1, 2, 3]}
18+
{:bar, [keep: {Path.relative_to_cwd(__ENV__.file), line}], [1, 2, 3]}
1819
end
1920

2021
test "fixed line" do
2122
assert quote(line: 3, do: bar(1, 2, 3)) == {:bar, [line: 3], [1, 2, 3]}
2223
end
2324

2425
test "quote line var" do
25-
# DO NOT MOVE THIS LINE
2626
line = __ENV__.line
27-
assert quote(line: line, do: bar(1, 2, 3)) == {:bar, [line: 26], [1, 2, 3]}
27+
assert quote(line: line, do: bar(1, 2, 3)) == {:bar, [line: line], [1, 2, 3]}
28+
29+
assert_raise ArgumentError, fn ->
30+
line = "oops"
31+
quote(line: line, do: bar(1, 2, 3))
32+
end
33+
end
34+
35+
test "quote context var" do
36+
context = :dynamic
37+
assert quote(context: context, do: bar) == {:bar, [], :dynamic}
38+
39+
assert_raise ArgumentError, fn ->
40+
context = "oops"
41+
quote(context: context, do: bar)
42+
end
43+
44+
assert_raise ArgumentError, fn ->
45+
context = nil
46+
quote(context: context, do: bar)
47+
end
2848
end
2949

3050
test "operator precedence" do
@@ -185,7 +205,7 @@ defmodule Kernel.QuoteTest do
185205

186206
test "bind quoted" do
187207
args = [
188-
{:=, [], [{:foo, [], Kernel.QuoteTest}, 3]},
208+
{:=, [], [{:foo, [line: __ENV__.line + 4], Kernel.QuoteTest}, 3]},
189209
{:foo, [], Kernel.QuoteTest}
190210
]
191211

@@ -276,6 +296,8 @@ end
276296

277297
# DO NOT MOVE THIS LINE
278298
defmodule Kernel.QuoteTest.Errors do
299+
def line, do: __ENV__.line + 4
300+
279301
defmacro defraise do
280302
quote location: :keep do
281303
def will_raise(_a, _b), do: raise("oops")
@@ -294,27 +316,29 @@ defmodule Kernel.QuoteTest.ErrorsTest do
294316
# Defines the add function
295317
defraise()
296318

319+
@line line()
297320
test "inside function error" do
298321
try do
299322
will_raise(:a, :b)
300323
rescue
301324
RuntimeError ->
302325
mod = Kernel.QuoteTest.ErrorsTest
303326
file = __ENV__.file |> Path.relative_to_cwd() |> String.to_charlist()
304-
assert [{^mod, :will_raise, 2, [file: ^file, line: 281]} | _] = __STACKTRACE__
327+
assert [{^mod, :will_raise, 2, [file: ^file, line: @line]} | _] = __STACKTRACE__
305328
else
306329
_ -> flunk("expected failure")
307330
end
308331
end
309332

333+
@line __ENV__.line + 3
310334
test "outside function error" do
311335
try do
312336
will_raise()
313337
rescue
314338
RuntimeError ->
315339
mod = Kernel.QuoteTest.ErrorsTest
316340
file = __ENV__.file |> Path.relative_to_cwd() |> String.to_charlist()
317-
assert [{^mod, _, _, [file: ^file, line: 312]} | _] = __STACKTRACE__
341+
assert [{^mod, _, _, [file: ^file, line: @line]} | _] = __STACKTRACE__
318342
else
319343
_ -> flunk("expected failure")
320344
end

0 commit comments

Comments
 (0)