Skip to content

Commit 146fb4e

Browse files
authored
Make has_unquote/1 quote-aware (#12836)
1 parent c4b5974 commit 146fb4e

File tree

2 files changed

+150
-11
lines changed

2 files changed

+150
-11
lines changed

lib/elixir/src/elixir_quote.erl

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,35 @@ fun_to_quoted(Function) ->
3030

3131
%% has_unquotes
3232

33-
has_unquotes({unquote, _, [_]}) -> true;
34-
has_unquotes({unquote_splicing, _, [_]}) -> true;
35-
has_unquotes({{'.', _, [_, unquote]}, _, [_]}) -> true;
36-
has_unquotes({Var, _, Ctx}) when is_atom(Var), is_atom(Ctx) -> false;
37-
has_unquotes({Name, _, Args}) when is_list(Args) ->
38-
has_unquotes(Name) orelse lists:any(fun has_unquotes/1, Args);
39-
has_unquotes({Left, Right}) ->
40-
has_unquotes(Left) orelse has_unquotes(Right);
41-
has_unquotes(List) when is_list(List) ->
42-
lists:any(fun has_unquotes/1, List);
43-
has_unquotes(_Other) -> false.
33+
has_unquotes(Ast) -> has_unquotes(Ast, 0).
34+
35+
has_unquotes({quote, _, [Child]}, QuoteLevel) ->
36+
has_unquotes(Child, QuoteLevel + 1);
37+
has_unquotes({quote, _, [QuoteOpts, Child]}, QuoteLevel) ->
38+
case disables_unquote(QuoteOpts) of
39+
true -> false;
40+
_ -> has_unquotes(Child, QuoteLevel + 1)
41+
end;
42+
has_unquotes({Unquote, _, [Child]}, QuoteLevel)
43+
when Unquote == unquote; Unquote == unquote_splicing ->
44+
case QuoteLevel of
45+
0 -> true;
46+
_ -> has_unquotes(Child, QuoteLevel - 1)
47+
end;
48+
has_unquotes({{'.', _, [_, unquote]}, _, [_]}, _) -> true;
49+
has_unquotes({Var, _, Ctx}, _) when is_atom(Var), is_atom(Ctx) -> false;
50+
has_unquotes({Name, _, Args}, QuoteLevel) when is_list(Args) ->
51+
has_unquotes(Name) orelse lists:any(fun(Child) -> has_unquotes(Child, QuoteLevel) end, Args);
52+
has_unquotes({Left, Right}, QuoteLevel) ->
53+
has_unquotes(Left, QuoteLevel) orelse has_unquotes(Right, QuoteLevel);
54+
has_unquotes(List, QuoteLevel) when is_list(List) ->
55+
lists:any(fun(Child) -> has_unquotes(Child, QuoteLevel) end, List);
56+
has_unquotes(_Other, _) -> false.
57+
58+
disables_unquote([{unquote, false} | _]) -> true;
59+
disables_unquote([{bind_quoted, _} | _]) -> true;
60+
disables_unquote([_H | T]) -> disables_unquote(T);
61+
disables_unquote(_) -> false.
4462

4563
%% Apply the line from site call on quoted contents.
4664
%% Receives a Key to look for the default line as argument.

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

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,3 +653,124 @@ defmodule Kernel.QuoteTest.ImportsHygieneTest do
653653
assert meta[:imports] == [{0, BinaryUtils}]
654654
end
655655
end
656+
657+
defmodule Kernel.QuoteTest.HasUnquoteTest do
658+
use ExUnit.Case, async: true
659+
660+
test "expression without unquote returns false" do
661+
ast =
662+
quote unquote: false do
663+
opts = [x: 5]
664+
x = Keyword.fetch!(opts, :x)
665+
x + 1
666+
end
667+
668+
refute :elixir_quote.has_unquotes(ast)
669+
end
670+
671+
test "expression with unquote returns true" do
672+
ast =
673+
quote unquote: false do
674+
[x: unquote(x)]
675+
end
676+
677+
assert :elixir_quote.has_unquotes(ast)
678+
679+
ast =
680+
quote unquote: false do
681+
unquote(module).fun(x)
682+
end
683+
684+
assert :elixir_quote.has_unquotes(ast)
685+
686+
ast =
687+
quote unquote: false do
688+
module.unquote(fun)(x)
689+
end
690+
691+
assert :elixir_quote.has_unquotes(ast)
692+
693+
ast =
694+
quote unquote: false do
695+
module.fun(unquote(x))
696+
end
697+
698+
assert :elixir_quote.has_unquotes(ast)
699+
700+
ast =
701+
quote unquote: false do
702+
module.fun(unquote_splicing(args))
703+
end
704+
705+
assert :elixir_quote.has_unquotes(ast)
706+
end
707+
708+
test "expression with unquote within quote returns false" do
709+
ast =
710+
quote unquote: false do
711+
quote do
712+
x + unquote(y)
713+
end
714+
end
715+
716+
refute :elixir_quote.has_unquotes(ast)
717+
718+
ast =
719+
quote unquote: false do
720+
quote do
721+
foo = bar(unquote_splicing(args))
722+
end
723+
end
724+
725+
refute :elixir_quote.has_unquotes(ast)
726+
end
727+
728+
test "expression with unquote within unquote within quote returns true" do
729+
ast =
730+
quote unquote: false do
731+
quote do
732+
x + unquote(unquote(y))
733+
end
734+
end
735+
736+
assert :elixir_quote.has_unquotes(ast)
737+
738+
ast =
739+
quote unquote: false do
740+
quote do
741+
foo = bar(unquote_splicing(unquote(args)))
742+
end
743+
end
744+
745+
assert :elixir_quote.has_unquotes(ast)
746+
747+
ast =
748+
quote unquote: false do
749+
quote do
750+
foo = bar(unquote(unquote_splicing(args)))
751+
end
752+
end
753+
754+
assert :elixir_quote.has_unquotes(ast)
755+
end
756+
757+
test "expression within quote disabling unquotes always returns false" do
758+
ast =
759+
quote unquote: false do
760+
quote unquote: false do
761+
x + unquote(unquote(y))
762+
end
763+
end
764+
765+
refute :elixir_quote.has_unquotes(ast)
766+
767+
ast =
768+
quote unquote: false do
769+
quote bind_quoted: [x: x] do
770+
x + unquote(unquote(y))
771+
end
772+
end
773+
774+
refute :elixir_quote.has_unquotes(ast)
775+
end
776+
end

0 commit comments

Comments
 (0)