Skip to content

Commit bfa7e41

Browse files
authored
Normalizing multi-component modules and module aliases (#44)
1 parent a1f42f8 commit bfa7e41

File tree

8 files changed

+164
-32
lines changed

8 files changed

+164
-32
lines changed

lib/representer.ex

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,18 @@ defmodule Representer do
44
alias Representer.Mapping
55

66
def process(file, code_output, mapping_output) do
7-
{represented_ast, mapping} = represent(file)
7+
{represented_ast, mapping} =
8+
file
9+
|> File.read!()
10+
|> represent
811

912
File.write!(code_output, Macro.to_string(represented_ast) <> "\n")
1013
File.write!(mapping_output, to_string(mapping))
1114
end
1215

13-
def represent(file) do
16+
def represent(code) do
1417
{ast, mapping} =
15-
file
16-
|> File.read!()
18+
code
1719
|> Code.string_to_quoted!()
1820
|> Macro.prewalk(&add_meta/1)
1921
|> Macro.prewalk(Mapping.init(), &define_placeholders/2)
@@ -49,17 +51,23 @@ defmodule Representer do
4951
do_define_placeholders(node, represented)
5052
end
5153

54+
# module definition
5255
defp do_define_placeholders(
53-
{:defmodule, [line: x],
54-
[{:__aliases__, [line: x], [module_name]} = module_alias | _] = args} = node,
56+
{:defmodule, meta1, [{:__aliases__, meta2, module_name}, content]},
5557
represented
5658
) do
57-
{:ok, represented, mapped_term} = Mapping.get_placeholder(represented, module_name, :module)
58-
59-
module_alias = module_alias |> Tuple.delete_at(2) |> Tuple.append([mapped_term])
60-
args = [module_alias | args |> tl]
61-
node = node |> Tuple.delete_at(2) |> Tuple.append(args)
59+
{:ok, represented, names} = Mapping.get_placeholder(represented, module_name)
60+
node = {:defmodule, meta1, [{:__aliases__, meta2, names}, content]}
61+
{node, represented}
62+
end
6263

64+
# module alias
65+
defp do_define_placeholders(
66+
{:alias, meta, [module, [as: {:__aliases__, meta2, module_alias}]]},
67+
represented
68+
) do
69+
{:ok, represented, names} = Mapping.get_placeholder(represented, module_alias)
70+
node = {:alias, meta, [module, [as: {:__aliases__, meta2, names}]]}
6371
{node, represented}
6472
end
6573

@@ -73,13 +81,13 @@ defmodule Representer do
7381
# function/macro/guard definition with a guard
7482
[{name3, meta3, args3} | args2_tail] = args2
7583

76-
{:ok, represented, mapped_name} = Representer.Mapping.get_placeholder(represented, name3)
84+
{:ok, represented, mapped_name} = Mapping.get_placeholder(represented, name3)
7785
meta2 = Keyword.put(meta2, :visited?, true)
7886
meta3 = Keyword.put(meta3, :visited?, true)
7987

8088
{[{name, meta2, [{mapped_name, meta3, args3} | args2_tail]} | args_tail], represented}
8189
else
82-
{:ok, represented, mapped_name} = Representer.Mapping.get_placeholder(represented, name)
90+
{:ok, represented, mapped_name} = Mapping.get_placeholder(represented, name)
8391
meta2 = Keyword.put(meta2, :visited?, true)
8492

8593
{[{mapped_name, meta2, args2} | args_tail], represented}
@@ -92,10 +100,10 @@ defmodule Representer do
92100
# variables
93101
# https://elixir-lang.org/getting-started/meta/quote-and-unquote.html
94102
# "The third element is either a list of arguments for the function call or an atom. When this element is an atom, it means the tuple represents a variable."
95-
@special_var_names [:__CALLER__, :__DIR__, :__ENV__, :__MODULE__, :__STACKTRACE__, :...]
103+
@special_var_names [:__CALLER__, :__DIR__, :__ENV__, :__MODULE__, :__STACKTRACE__, :..., :_]
96104
defp do_define_placeholders({atom, meta, context}, represented)
97105
when is_atom(atom) and is_nil(context) and atom not in @special_var_names do
98-
{:ok, represented, mapped_term} = Representer.Mapping.get_placeholder(represented, atom)
106+
{:ok, represented, mapped_term} = Mapping.get_placeholder(represented, atom)
99107

100108
{{mapped_term, meta, context}, represented}
101109
end
@@ -114,10 +122,23 @@ defmodule Representer do
114122
do_use_existing_placeholders(node, represented)
115123
end
116124

125+
# module names
126+
defp do_use_existing_placeholders({:__aliases__, meta, module_name}, represented)
127+
when is_list(module_name) do
128+
module_name =
129+
Enum.map(
130+
module_name,
131+
&(Mapping.get_existing_placeholder(represented, &1) || &1)
132+
)
133+
134+
meta = Keyword.put(meta, :visited?, true)
135+
{{:__aliases__, meta, module_name}, represented}
136+
end
137+
117138
# local function calls
118139
defp do_use_existing_placeholders({atom, meta, context}, represented)
119140
when is_atom(atom) and is_list(context) do
120-
placeholder = Representer.Mapping.get_existing_placeholder(represented, atom)
141+
placeholder = Mapping.get_existing_placeholder(represented, atom)
121142

122143
# if there is no placeholder for this name, that means it's an imported or a standard library function/macro/special form
123144
atom = placeholder || atom
@@ -127,31 +148,30 @@ defmodule Representer do
127148

128149
# external function calls
129150
defp do_use_existing_placeholders(
130-
{{:., meta2, [{:__aliases__, meta3, [module_name]}, function_name]}, meta, context},
151+
{{:., meta2, [{:__aliases__, _, module_name} = module, function_name]}, meta, context},
131152
represented
132153
)
133-
when is_atom(module_name) and is_atom(function_name) do
134-
placeholder_module_name =
135-
Representer.Mapping.get_existing_placeholder(represented, module_name)
154+
when is_list(module_name) and is_atom(function_name) do
155+
{{_, _, new_module_name} = module, _} = do_use_existing_placeholders(module, represented)
136156

137-
module_name = placeholder_module_name || module_name
157+
all_replaced? =
158+
Enum.zip_with(module_name, new_module_name, &(&1 != &2))
159+
|> Enum.all?()
138160

139161
placeholder_function_name =
140-
if placeholder_module_name do
141-
Representer.Mapping.get_existing_placeholder(represented, function_name)
162+
if all_replaced? do
163+
Mapping.get_existing_placeholder(represented, function_name)
142164
else
143-
# hack: assuming that if a module has no placeholder name, that means it's not being defined in this file
165+
# hack: assuming that if a module has no complete placeholder name, that means it's not being defined in this file
144166
# TODO: fix when dealing with aliases
145167
nil
146168
end
147169

148170
function_name = placeholder_function_name || function_name
149171

150172
meta2 = Keyword.put(meta2, :visited?, true)
151-
meta3 = Keyword.put(meta3, :visited?, true)
152173

153-
{{{:., meta2, [{:__aliases__, meta3, [module_name]}, function_name]}, meta, context},
154-
represented}
174+
{{{:., meta2, [module, function_name]}, meta, context}, represented}
155175
end
156176

157177
# external function calls via __MODULE__
@@ -160,8 +180,7 @@ defmodule Representer do
160180
represented
161181
)
162182
when is_atom(function_name) do
163-
placeholder_function_name =
164-
Representer.Mapping.get_existing_placeholder(represented, function_name)
183+
placeholder_function_name = Mapping.get_existing_placeholder(represented, function_name)
165184

166185
function_name = placeholder_function_name || function_name
167186
meta2 = Keyword.put(meta2, :visited?, true)

lib/representer/mapping.ex

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,18 @@ defmodule Representer.Mapping do
2727
%Mapping{}
2828
end
2929

30-
def get_placeholder(%Mapping{} = map, term, _type \\ :term) do
30+
def get_placeholder(%Mapping{} = map, terms) when is_list(terms) do
31+
{map, names} =
32+
Enum.reduce(terms, {map, []}, fn
33+
name, {map, names} ->
34+
{:ok, map, name} = Mapping.get_placeholder(map, name)
35+
{map, [name | names]}
36+
end)
37+
38+
{:ok, map, Enum.reverse(names)}
39+
end
40+
41+
def get_placeholder(%Mapping{} = map, term) do
3142
if map.mappings[term] do
3243
{:ok, map, map.mappings[term]}
3344
else

test/representer_test.exs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ defmodule RepresenterTest do
2929
test "parentheses_in_pipes" do
3030
test_directory("parentheses_in_pipes")
3131
end
32+
test "modules" do
33+
test_directory("modules")
34+
end
3235
end
3336

3437
defp test_directory(dir) do
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"Placeholder_1": "One",
3+
"Placeholder_10": "Ten",
4+
"Placeholder_11": "Eleven",
5+
"Placeholder_12": "Twelve",
6+
"Placeholder_13": "Thirteen",
7+
"Placeholder_2": "Two",
8+
"Placeholder_5": "Five",
9+
"Placeholder_6": "Six",
10+
"Placeholder_7": "Seven",
11+
"Placeholder_8": "Eight",
12+
"placeholder_14": "fourteen",
13+
"placeholder_3": "three",
14+
"placeholder_4": "four",
15+
"placeholder_9": "nine"
16+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
(
2+
defmodule(Placeholder_1) do
3+
alias(Placeholder_1, as: Placeholder_2)
4+
def(placeholder_3) do
5+
Placeholder_1.placeholder_4()
6+
Placeholder_2.placeholder_4()
7+
end
8+
def(placeholder_4) do
9+
:ok
10+
end
11+
end
12+
defmodule(Placeholder_5.Placeholder_6.Placeholder_7) do
13+
alias(Placeholder_5.Placeholder_6, as: Placeholder_8)
14+
def(placeholder_9) do
15+
Placeholder_1.placeholder_3()
16+
Placeholder_2.placeholder_3()
17+
Placeholder_5.Placeholder_6.Placeholder_7.placeholder_9()
18+
Placeholder_8.Placeholder_7.placeholder_9()
19+
__MODULE__.placeholder_9()
20+
External.external()
21+
Placeholder_1.External.external()
22+
Placeholder_5.External.Placeholder_7.nine()
23+
end
24+
defmodule(Placeholder_10) do
25+
26+
end
27+
defmodule(Placeholder_11) do
28+
alias(Placeholder_5.Placeholder_6.Placeholder_7.{Placeholder_10, Placeholder_11})
29+
alias(Placeholder_8.Placeholder_7.Placeholder_11, as: Placeholder_12)
30+
alias(Placeholder_8.Placeholder_7, as: Placeholder_2)
31+
alias(External, as: Placeholder_13)
32+
def(placeholder_14) do
33+
External.external()
34+
Placeholder_13.external()
35+
end
36+
end
37+
end
38+
)

test_data/modules/input.ex

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
defmodule One do
2+
alias One, as: Two
3+
4+
def three do
5+
One.four()
6+
Two.four()
7+
end
8+
9+
def four, do: :ok
10+
end
11+
12+
defmodule Five.Six.Seven do
13+
alias Five.Six, as: Eight
14+
15+
def nine do
16+
One.three()
17+
Two.three()
18+
19+
Five.Six.Seven.nine()
20+
Eight.Seven.nine()
21+
__MODULE__.nine()
22+
# not replaced
23+
External.external()
24+
# only :One is replaced
25+
One.External.external()
26+
# nine not replaced because of External
27+
Five.External.Seven.nine()
28+
end
29+
30+
defmodule Ten do
31+
end
32+
33+
defmodule Eleven do
34+
alias Five.Six.Seven.{Ten, Eleven}
35+
alias Eight.Seven.Eleven, as: Twelve
36+
alias Eight.Seven, as: Two
37+
38+
alias External, as: Thirteen
39+
40+
def fourteen do
41+
External.external()
42+
Thirteen.external()
43+
end
44+
end
45+
end

test_data/special_variables/expected_representation.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ defmodule(Placeholder_1) do
55
:math.pow(__DIR__)
66
placeholder_6.(__ENV__)
77
end
8-
defmacro(placeholder_7()) do
8+
defmacro(placeholder_7(_)) do
99
placeholder_2(__CALLER__, __STACKTRACE__)
1010
placeholder_8 = 3
1111
end

test_data/special_variables/input.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ defmodule AnythingAndEverything do
66
some_value.(__ENV__)
77
end
88

9-
defmacro foo() do
9+
defmacro foo(_) do
1010
some_function(__CALLER__, __STACKTRACE__)
1111
_ignored = 3
1212
end

0 commit comments

Comments
 (0)