Skip to content

Commit 6ef5801

Browse files
authored
Merge pull request #69 from esl/support-loc-specifying-in-pipe-op
Support location specifying in pipe op
2 parents e0c47ad + a76e935 commit 6ef5801

File tree

4 files changed

+239
-47
lines changed

4 files changed

+239
-47
lines changed

lib/gradient/ast_specifier.ex

Lines changed: 49 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -5,43 +5,6 @@ defmodule Gradient.AstSpecifier do
55
it to forms that cannot be produced from Elixir directly.
66
77
FIXME Optimize tokens searching. Find out why some tokens are dropped
8-
9-
NOTE Mapper implements:
10-
- function [x]
11-
- fun [x]
12-
- fun @spec [x]
13-
- clause [x]
14-
- case [x]
15-
- block [X]
16-
- pipe [x]
17-
- call [x] (remote [X])
18-
- match [x]
19-
- op [x]
20-
- integer [x]
21-
- float [x]
22-
- string [x]
23-
- charlist [x]
24-
- tuple [X]
25-
- var [X]
26-
- list [X]
27-
- keyword [X]
28-
- binary [X]
29-
- map [X]
30-
- try [x]
31-
- receive [X]
32-
- record [X] elixir don't use it record_field, record_index, record_pattern, record
33-
- named_fun [ ] is named_fun used by elixir?
34-
35-
NOTE Elixir expressions to handle or test:
36-
- list comprehension [X]
37-
- binary [X]
38-
- maps [X]
39-
- struct [X]
40-
- pipe [ ] TODO decide how to search for line in reversed form order
41-
- range [X]
42-
- receive [X]
43-
- record [X]
44-
- guards [X]
458
"""
469

4710
import Gradient.Tokens
@@ -55,6 +18,10 @@ defmodule Gradient.AstSpecifier do
5518
@type form :: Types.form()
5619
@type forms :: Types.forms()
5720
@type options :: Types.options()
21+
@type abstract_expr :: Types.abstract_expr()
22+
23+
# Expressions that could have missing location
24+
@lineless_forms [:atom, :char, :float, :integer, :string, :bin, :cons, :tuple]
5825

5926
# Api
6027

@@ -361,10 +328,9 @@ defmodule Gradient.AstSpecifier do
361328
def mapper({:call, anno, name, args}, tokens, opts) do
362329
# anno has correct line
363330
{:ok, _, anno, opts, _} = get_line(anno, opts)
364-
365331
name = remote_mapper(name)
366332

367-
{args, tokens} = context_mapper_fold(args, tokens, opts)
333+
{args, tokens} = call_args_mapper(args, tokens, name, opts)
368334

369335
{:call, anno, name, args}
370336
|> pass_tokens(tokens)
@@ -419,8 +385,7 @@ defmodule Gradient.AstSpecifier do
419385
end
420386

421387
def mapper({type, anno, value}, tokens, opts)
422-
when type in [:atom, :char, :float, :integer, :string, :bin] do
423-
# TODO check what happened for :string
388+
when type in @lineless_forms do
424389
{:ok, line} = Keyword.fetch(opts, :line)
425390
anno = :erl_anno.set_line(line, anno)
426391
anno = :erl_anno.set_generated(Keyword.get(opts, :generated, false), anno)
@@ -558,8 +523,7 @@ defmodule Gradient.AstSpecifier do
558523
@spec map_element_mapper(tuple(), tokens(), options()) :: {tuple(), tokens()}
559524
def map_element_mapper({field, anno, key, value}, tokens, opts)
560525
when field in [:map_field_assoc, :map_field_exact] do
561-
line = :erl_anno.line(anno)
562-
opts = Keyword.put(opts, :line, line)
526+
{:ok, _, anno, opts, _} = get_line(anno, opts)
563527

564528
{key, tokens} = mapper(key, tokens, opts)
565529
{value, tokens} = mapper(value, tokens, opts)
@@ -625,6 +589,29 @@ defmodule Gradient.AstSpecifier do
625589
end
626590
end
627591

592+
@doc """
593+
Update location in call args with the support to the pipe operator.
594+
"""
595+
@spec call_args_mapper([abstract_expr()], tokens(), abstract_expr(), options()) ::
596+
{options, [abstract_expr]}
597+
def call_args_mapper(args, tokens, name, opts) do
598+
# Check whether the call is after |> operator. If true, the parent location is set to 0
599+
# and the first arg location is cleared (if this arg is a lineless form).
600+
# NOTE If the call is to function from :erlang module then the first arg is swapped
601+
# with the second one because in Erlang the data is mostly in the second place.
602+
with true <- is_pipe_op?(tokens, opts),
603+
swapped? <- is_call_to_erlang?(name),
604+
[fst_arg | tail_args] <- maybe_swap_args(swapped?, args),
605+
true <- is_lineless?(fst_arg) do
606+
{arg, tokens} = mapper(clear_location(fst_arg), tokens, Keyword.put(opts, :line, 0))
607+
{args, tokens} = context_mapper_fold(tail_args, tokens, opts)
608+
{maybe_swap_args(swapped?, [arg | args]), tokens}
609+
else
610+
_ ->
611+
context_mapper_fold(args, tokens, opts)
612+
end
613+
end
614+
628615
# Private Helpers
629616

630617
@spec match_token_to_form(token(), form()) :: boolean()
@@ -820,4 +807,23 @@ defmodule Gradient.AstSpecifier do
820807
defp pass_tokens(form, tokens) do
821808
{form, tokens}
822809
end
810+
811+
defp is_pipe_op?(tokens, opts) do
812+
case List.first(drop_tokens_to_line(tokens, Keyword.fetch!(opts, :line))) do
813+
{:arrow_op, _, :|>} -> true
814+
_ -> false
815+
end
816+
end
817+
818+
defp maybe_swap_args(true, [fst, snd | t]), do: [snd, fst | t]
819+
defp maybe_swap_args(_, args), do: args
820+
821+
defp is_call_to_erlang?({:remote, _, {:atom, _, :erlang}, _}), do: true
822+
defp is_call_to_erlang?(_), do: false
823+
824+
defp is_lineless?(expr) do
825+
elem(expr, 0) in @lineless_forms
826+
end
827+
828+
defp clear_location(arg), do: :erl_parse.map_anno(&:erl_anno.set_line(0, &1), arg)
823829
end

test/gradient/ast_specifier_test.exs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,27 @@ defmodule Gradient.AstSpecifierTest do
33
doctest Gradient.AstSpecifier
44

55
alias Gradient.AstSpecifier
6+
alias Gradient.AstData
67

78
import Gradient.TestHelpers
89

910
setup_all state do
1011
{:ok, state}
1112
end
1213

14+
describe "specifying expression" do
15+
for {name, args, expected} <- AstData.ast_data() do
16+
test "#{name}" do
17+
{ast, tokens, opts} = unquote(Macro.escape(args))
18+
expected = AstData.normalize_expression(unquote(Macro.escape(expected)))
19+
20+
actual = AstData.normalize_expression(elem(AstSpecifier.mapper(ast, tokens, opts), 0))
21+
22+
assert expected == actual
23+
end
24+
end
25+
end
26+
1327
describe "run_mappers/2" do
1428
test "messy test on simple_app" do
1529
{tokens, ast} = example_data()
@@ -566,13 +580,13 @@ defmodule Gradient.AstSpecifierTest do
566580
[
567581
{:call, 4, {:remote, 4, {:atom, 4, Enum}, {:atom, 4, :filter}},
568582
[
569-
{:cons, 4, {:integer, 4, 1},
570-
{:cons, 4,
583+
{:cons, 3, {:integer, 3, 1},
584+
{:cons, 3,
571585
{
572586
:integer,
573-
4,
587+
3,
574588
2
575-
}, {:cons, 4, {:integer, 4, 3}, {nil, 4}}}},
589+
}, {:cons, 3, {:integer, 3, 3}, {nil, 3}}}},
576590
{:fun, 4,
577591
{:clauses,
578592
[

test/support/ast_data.ex

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
defmodule Gradient.AstData do
2+
@moduledoc """
3+
Stores the test cases data for expressions line specifying. To increase the flexibility
4+
the data need normalization before equality assertion. Thus we check only the line change,
5+
not the exact value and there is no need to update expected values when the file content
6+
changes.
7+
8+
This way of testing is useful only for more complex expressions in which we can observe
9+
some line change. For example, look at the pipe operator cases.
10+
"""
11+
12+
require Gradient.Debug
13+
import Gradient.Debug, only: [elixir_to_ast: 1]
14+
import Gradient.TestHelpers
15+
alias Gradient.Types
16+
17+
@tokens __ENV__.file |> load_tokens()
18+
19+
defp pipe do
20+
{__ENV__.function,
21+
{__ENV__.line,
22+
elixir_to_ast do
23+
1
24+
|> is_atom()
25+
26+
'1'
27+
|> is_atom()
28+
29+
:ok
30+
|> is_atom()
31+
32+
[1, 2, 3]
33+
|> is_atom()
34+
35+
{1, 2, 3}
36+
|> is_atom()
37+
38+
"a"
39+
|> is_atom()
40+
end, __ENV__.line},
41+
{:block, 22,
42+
[
43+
{:call, 24, {:remote, 24, {:atom, 24, :erlang}, {:atom, 24, :is_atom}},
44+
[{:integer, 23, 1}]},
45+
{:call, 27, {:remote, 27, {:atom, 27, :erlang}, {:atom, 27, :is_atom}},
46+
[{:cons, 26, {:integer, 26, 49}, {nil, 26}}]},
47+
{:call, 30, {:remote, 30, {:atom, 30, :erlang}, {:atom, 30, :is_atom}},
48+
[{:atom, 29, :ok}]},
49+
{:call, 33, {:remote, 33, {:atom, 33, :erlang}, {:atom, 33, :is_atom}},
50+
[
51+
{:cons, 32, {:integer, 32, 1},
52+
{:cons, 32, {:integer, 32, 2}, {:cons, 32, {:integer, 32, 3}, {nil, 32}}}}
53+
]},
54+
{:call, 36, {:remote, 36, {:atom, 36, :erlang}, {:atom, 36, :is_atom}},
55+
[{:tuple, 35, [{:integer, 35, 1}, {:integer, 35, 2}, {:integer, 35, 3}]}]},
56+
{:call, 39, {:remote, 39, {:atom, 39, :erlang}, {:atom, 39, :is_atom}},
57+
[{:bin, 38, [{:bin_element, 38, {:string, 38, 'a'}, :default, :default}]}]}
58+
]}}
59+
end
60+
61+
defp pipe_with_fun_converted_to_erl_equivalent do
62+
{__ENV__.function,
63+
{__ENV__.line,
64+
elixir_to_ast do
65+
:ok
66+
|> elem(0)
67+
end, __ENV__.line},
68+
{:call, 56, {:remote, 56, {:atom, 56, :erlang}, {:atom, 56, :element}},
69+
[{:integer, 56, 1}, {:atom, 55, :ok}]}}
70+
end
71+
72+
defp complex_list_pipe do
73+
{__ENV__.function,
74+
{__ENV__.line,
75+
elixir_to_ast do
76+
[
77+
{1, %{a: 1}},
78+
{2, %{a: 2}}
79+
]
80+
|> Enum.map(&elem(&1, 0))
81+
end, __ENV__.line},
82+
{:call, 80, {:remote, 80, {:atom, 80, Enum}, {:atom, 80, :map}},
83+
[
84+
{:cons, 76,
85+
{:tuple, 77,
86+
[
87+
{:integer, 77, 1},
88+
{:map, 77, [{:map_field_assoc, 77, {:atom, 77, :a}, {:integer, 77, 1}}]}
89+
]},
90+
{:cons, 77,
91+
{:tuple, 78,
92+
[
93+
{:integer, 78, 2},
94+
{:map, 78, [{:map_field_assoc, 78, {:atom, 78, :a}, {:integer, 78, 2}}]}
95+
]}, {nil, 77}}},
96+
{:fun, 80,
97+
{:clauses,
98+
[
99+
{:clause, 80, [{:var, 0, :_@1}], [],
100+
[
101+
{:call, 80, {:remote, 80, {:atom, 80, :erlang}, {:atom, 80, :element}},
102+
[{:integer, 80, 1}, {:var, 0, :_@1}]}
103+
]}
104+
]}}
105+
]}}
106+
end
107+
108+
defp complex_tuple_pipe do
109+
{__ENV__.function,
110+
{__ENV__.line,
111+
elixir_to_ast do
112+
{
113+
{1, %{a: 1}},
114+
{2, %{a: 2}}
115+
}
116+
|> Tuple.to_list()
117+
end, __ENV__.line},
118+
{:call, 119, {:remote, 119, {:atom, 119, :erlang}, {:atom, 119, :tuple_to_list}},
119+
[
120+
{:tuple, 115,
121+
[
122+
{:tuple, 116,
123+
[
124+
{:integer, 116, 1},
125+
{:map, 116, [{:map_field_assoc, 116, {:atom, 116, :a}, {:integer, 116, 1}}]}
126+
]},
127+
{:tuple, 117,
128+
[
129+
{:integer, 117, 2},
130+
{:map, 117, [{:map_field_assoc, 117, {:atom, 117, :a}, {:integer, 117, 2}}]}
131+
]}
132+
]}
133+
]}}
134+
end
135+
136+
@spec ast_data() :: [
137+
{atom(), {Types.abstract_expr(), Types.tokens(), Types.options()},
138+
Types.abstract_expr()}
139+
]
140+
def ast_data do
141+
[
142+
pipe(),
143+
pipe_with_fun_converted_to_erl_equivalent(),
144+
complex_list_pipe(),
145+
complex_tuple_pipe()
146+
]
147+
|> Enum.map(fn {{name, _}, {start_line, ast, end_line}, expected} ->
148+
tokens = Gradient.Tokens.drop_tokens_to_line(@tokens, start_line + 1)
149+
{name, {ast, tokens, [line: start_line + 1, end_line: end_line]}, expected}
150+
end)
151+
end
152+
153+
def normalize_expression(expression) do
154+
{expression, _} =
155+
:erl_parse.mapfold_anno(
156+
fn anno, acc ->
157+
{{:erl_anno.line(anno) - acc, :erl_anno.column(anno)}, acc}
158+
end,
159+
:erl_anno.line(elem(expression, 1)),
160+
expression
161+
)
162+
163+
expression
164+
end
165+
end

test/support/helpers.ex

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ defmodule Gradient.TestHelpers do
2929
{tokens, ast}
3030
end
3131

32+
def load_tokens(path) do
33+
with {:ok, code} <- File.read(path),
34+
{:ok, tokens} <- :elixir.string_to_tokens(String.to_charlist(code), 1, 1, path, []) do
35+
tokens
36+
end
37+
end
38+
3239
@spec example_data() :: {T.tokens(), T.forms()}
3340
def example_data() do
3441
beam_path = Path.join(@examples_build_path, "Elixir.SimpleApp.beam") |> String.to_charlist()

0 commit comments

Comments
 (0)