Skip to content

Commit 64b02a7

Browse files
Normalize types, typespecs and module attributes (#52)
* Normalize types, typespecs and module attributes * Fix formatting from 1.13 move * Remove parentheses for type definitions * Add Missing reserved attributes Co-authored-by: Angelika Tyborska <[email protected]> * Remove obvious comments Co-authored-by: Angelika Tyborska <[email protected]>
1 parent 89b6408 commit 64b02a7

File tree

7 files changed

+313
-52
lines changed

7 files changed

+313
-52
lines changed

lib/representer.ex

Lines changed: 190 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -19,41 +19,169 @@ defmodule Representer do
1919
File.write!(mapping_output, to_string(mapping))
2020
end
2121

22+
@spec represent(code :: String.t()) :: {Macro.t(), map()}
2223
def represent(code) do
2324
{ast, mapping} =
2425
code
2526
|> Code.string_to_quoted!()
2627
|> Macro.prewalk(&add_meta/1)
27-
|> Macro.prewalk(Mapping.init(), &define_placeholders/2)
28+
# gathering type definitions
29+
|> Macro.prewalk(Mapping.init(), &define_type_placeholders/2)
30+
31+
# replacing type definitions
32+
{ast, mapping} =
33+
Macro.prewalk(ast, mapping, &use_existing_placeholders/2)
34+
|> Macro.prewalk(&protect_types/1)
35+
36+
# gathering function names and variables
37+
{ast, mapping} = Macro.prewalk(ast, mapping, &define_placeholders/2)
2838

2939
ast
30-
# names in local function calls can only be exchanged after all names in function definitions were exchanged
40+
# replacing function names and variables
3141
|> Macro.prewalk(mapping, &use_existing_placeholders/2)
3242
|> Macro.prewalk(&drop_docstring/1)
3343
|> Macro.prewalk(&drop_line_meta/1)
3444
|> Macro.prewalk(&add_parentheses_in_pipes/1)
3545
end
3646

37-
@doc """
38-
"""
39-
def add_meta({:"::", _, [_, {:binary, meta, _} = bin] = args} = node) do
47+
# protect string interpolations
48+
defp add_meta({:"::", meta, [interpolate, {:binary, meta2, atom}]}) do
49+
meta2 = Keyword.put(meta2, :visited?, true)
50+
{:"::", meta, [interpolate, {:binary, meta2, atom}]}
51+
end
52+
53+
defp add_meta(node), do: node
54+
55+
defp define_type_placeholders({_, meta, _} = node, represented) do
56+
if meta[:visited?] do
57+
{node, represented}
58+
else
59+
do_define_type_placeholders(node, represented)
60+
end
61+
end
62+
63+
defp define_type_placeholders(node, represented) do
64+
do_define_type_placeholders(node, represented)
65+
end
66+
67+
@typecreation ~w(type typep opaque)a
68+
@typespecs ~w(spec callback macrocallback)a
69+
# type creation and type specifications without :when
70+
defp do_define_type_placeholders(
71+
{:@, meta, [{create, meta2, [{:"::", meta3, [{name, meta4, args}, definition]}]}]},
72+
represented
73+
)
74+
when create in @typecreation or create in @typespecs do
75+
{:ok, represented, name} = Mapping.get_placeholder(represented, name)
76+
77+
{args, represented} =
78+
cond do
79+
is_atom(args) ->
80+
{args, represented}
81+
82+
args == [] ->
83+
{nil, represented}
84+
85+
create in @typespecs ->
86+
args = Enum.map(args, &remove_type_parentheses/1)
87+
{args, represented}
88+
89+
create in @typecreation ->
90+
args = Enum.map(args, &remove_type_parentheses/1)
91+
# when creating types, types may be passed as arguments to be used in the definitions
92+
vars = Enum.map(args, fn {var, _, nil} -> var end)
93+
{:ok, represented, _} = Mapping.get_placeholder(represented, vars)
94+
{args, represented}
95+
end
96+
97+
definition = Macro.prewalk(definition, &remove_type_parentheses/1)
98+
meta = Keyword.put(meta, :visited?, true)
99+
meta2 = Keyword.put(meta2, :visited?, true)
100+
meta4 = Keyword.put(meta4, :visited?, true)
101+
102+
{{:@, meta, [{create, meta2, [{:"::", meta3, [{name, meta4, args}, definition]}]}]},
103+
represented}
104+
end
105+
106+
# type specifications with :when
107+
defp do_define_type_placeholders(
108+
{:@, meta,
109+
[
110+
{create, meta2,
111+
[{:when, meta_when, [{:"::", meta3, [{name, meta4, args}, definition]}, conditions]}]}
112+
]},
113+
represented
114+
)
115+
when create in @typespecs do
116+
{:ok, represented, name} = Mapping.get_placeholder(represented, name)
117+
118+
{args, represented} =
119+
if is_atom(args) do
120+
{args, represented}
121+
else
122+
args = Enum.map(args, &remove_type_parentheses/1)
123+
{args, represented}
124+
end
125+
126+
conditions = Macro.prewalk(conditions, &remove_type_parentheses/1)
127+
# typespecs may receive variable types as arguments if they are constrained by :when
128+
vars = Enum.map(conditions, fn {var, _type} -> var end)
129+
{:ok, represented, _} = Mapping.get_placeholder(represented, vars)
130+
131+
definition = Macro.prewalk(definition, &remove_type_parentheses/1)
40132
meta = Keyword.put(meta, :visited?, true)
41-
bin = Tuple.delete_at(bin, 1) |> Tuple.insert_at(1, meta)
42-
args = List.replace_at(args, 1, bin)
43-
_node = Tuple.delete_at(node, 2) |> Tuple.append(args)
133+
meta2 = Keyword.put(meta2, :visited?, true)
134+
meta4 = Keyword.put(meta4, :visited?, true)
135+
136+
{{:@, meta,
137+
[
138+
{create, meta2,
139+
[{:when, meta_when, [{:"::", meta3, [{name, meta4, args}, definition]}, conditions]}]}
140+
]}, represented}
141+
end
142+
143+
defp do_define_type_placeholders(node, represented), do: {node, represented}
144+
145+
defp remove_type_parentheses({:"::", meta, [var, type]}) do
146+
{:"::", meta, [var, remove_type_parentheses(type)]}
147+
end
148+
149+
defp remove_type_parentheses({atom, type}) when is_atom(atom) do
150+
{atom, remove_type_parentheses(type)}
151+
end
152+
153+
defp remove_type_parentheses({{:., meta, path}, meta2, args}) do
154+
meta2 = Keyword.put(meta2, :no_parens, true)
155+
{{:., meta, path}, meta2, args}
156+
end
157+
158+
defp remove_type_parentheses({type, meta, args}) when args == [] or is_atom(args) do
159+
meta = Keyword.put(meta, :type?, true)
160+
{type, meta, nil}
161+
end
162+
163+
defp remove_type_parentheses(node), do: node
164+
165+
defp protect_types({name, meta, args} = node) do
166+
if meta[:type?] do
167+
meta = meta |> Keyword.drop([:type?]) |> Keyword.put(:visited?, true)
168+
{name, meta, args}
169+
else
170+
node
171+
end
44172
end
45173

46-
def add_meta(node), do: node
174+
defp protect_types(node), do: node
47175

48-
def define_placeholders({_, meta, _} = node, represented) do
176+
defp define_placeholders({_, meta, _} = node, represented) do
49177
if meta[:visited?] do
50178
{node, represented}
51179
else
52180
do_define_placeholders(node, represented)
53181
end
54182
end
55183

56-
def define_placeholders(node, represented) do
184+
defp define_placeholders(node, represented) do
57185
do_define_placeholders(node, represented)
58186
end
59187

@@ -87,13 +215,25 @@ defmodule Representer do
87215
# function/macro/guard definition with a guard
88216
[{name3, meta3, args3} | args2_tail] = args2
89217

90-
{:ok, represented, mapped_name} = Mapping.get_placeholder(represented, name3)
218+
{:ok, represented, mapped_name} =
219+
if meta3[:visited?] do
220+
{:ok, represented, name3}
221+
else
222+
Mapping.get_placeholder(represented, name3)
223+
end
224+
91225
meta2 = Keyword.put(meta2, :visited?, true)
92226
meta3 = Keyword.put(meta3, :visited?, true)
93227

94228
{[{name, meta2, [{mapped_name, meta3, args3} | args2_tail]} | args_tail], represented}
95229
else
96-
{:ok, represented, mapped_name} = Mapping.get_placeholder(represented, name)
230+
{:ok, represented, mapped_name} =
231+
if meta2[:visited?] do
232+
{:ok, represented, name}
233+
else
234+
Mapping.get_placeholder(represented, name)
235+
end
236+
97237
meta2 = Keyword.put(meta2, :visited?, true)
98238

99239
{[{mapped_name, meta2, args2} | args_tail], represented}
@@ -103,53 +243,61 @@ defmodule Representer do
103243
{node, represented}
104244
end
105245

246+
# module attributes
247+
@reserved_attributes ~w(after_compile before_compile behaviour impl compile deprecated doc typedoc dialyzer external_resource file moduledoc on_definition on_load vsn derive enforce_keys optional_callbacks)a
248+
defp do_define_placeholders({:@, meta, [{name, meta2, value}]}, represented)
249+
when name not in @reserved_attributes do
250+
{:ok, represented, name} = Mapping.get_placeholder(represented, name)
251+
node = {:@, meta, [{name, meta2, value}]}
252+
{node, represented}
253+
end
254+
106255
# variables
107256
# https://elixir-lang.org/getting-started/meta/quote-and-unquote.html
108257
# "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."
109258
@special_var_names [:__CALLER__, :__DIR__, :__ENV__, :__MODULE__, :__STACKTRACE__, :..., :_]
110259
defp do_define_placeholders({atom, meta, context}, represented)
111-
when is_atom(atom) and is_nil(context) and atom not in @special_var_names do
260+
when is_atom(atom) and is_atom(context) and atom not in @special_var_names do
112261
{:ok, represented, mapped_term} = Mapping.get_placeholder(represented, atom)
113262

114263
{{mapped_term, meta, context}, represented}
115264
end
116265

117266
defp do_define_placeholders(node, represented), do: {node, represented}
118267

119-
def use_existing_placeholders({_, meta, _} = node, represented) do
268+
defp use_existing_placeholders({_, meta, _} = node, represented) do
120269
if meta[:visited?] do
121270
{node, represented}
122271
else
123272
do_use_existing_placeholders(node, represented)
124273
end
125274
end
126275

127-
def use_existing_placeholders(node, represented) do
276+
defp use_existing_placeholders(node, represented) do
128277
do_use_existing_placeholders(node, represented)
129278
end
130279

131280
# module names
132281
defp do_use_existing_placeholders({:__aliases__, meta, module_name}, represented)
133282
when is_list(module_name) do
134283
module_name =
135-
Enum.map(
136-
module_name,
137-
&(Mapping.get_existing_placeholder(represented, &1) || &1)
138-
)
284+
Enum.map(module_name, &(Mapping.get_existing_placeholder(represented, &1) || &1))
139285

140-
meta = Keyword.put(meta, :visited?, true)
141286
{{:__aliases__, meta, module_name}, represented}
142287
end
143288

144-
# local function calls
145-
defp do_use_existing_placeholders({atom, meta, context}, represented)
146-
when is_atom(atom) and is_list(context) do
147-
placeholder = Mapping.get_existing_placeholder(represented, atom)
148-
149-
# if there is no placeholder for this name, that means it's an imported or a standard library function/macro/special form
150-
atom = placeholder || atom
151-
152-
{{atom, meta, context}, represented}
289+
# variables or local function calls
290+
defp do_use_existing_placeholders({atom, meta, context} = node, represented)
291+
when is_atom(atom) do
292+
case Mapping.get_existing_placeholder(represented, atom) do
293+
nil ->
294+
# no representation yet, built-in type, imported, standard function/macro/special form...
295+
{node, represented}
296+
297+
atom ->
298+
meta = Keyword.put(meta, :visited?, true)
299+
{{atom, meta, context}, represented}
300+
end
153301
end
154302

155303
# external function calls
@@ -169,14 +317,10 @@ defmodule Representer do
169317
Mapping.get_existing_placeholder(represented, function_name)
170318
else
171319
# hack: assuming that if a module has no complete placeholder name, that means it's not being defined in this file
172-
# TODO: fix when dealing with aliases
173320
nil
174321
end
175322

176323
function_name = placeholder_function_name || function_name
177-
178-
meta2 = Keyword.put(meta2, :visited?, true)
179-
180324
{{{:., meta2, [module, function_name]}, meta, context}, represented}
181325
end
182326

@@ -189,15 +333,18 @@ defmodule Representer do
189333
placeholder_function_name = Mapping.get_existing_placeholder(represented, function_name)
190334

191335
function_name = placeholder_function_name || function_name
192-
meta2 = Keyword.put(meta2, :visited?, true)
193-
meta3 = Keyword.put(meta3, :visited?, true)
194-
195336
{{{:., meta2, [{:__MODULE__, meta3, args3}, function_name]}, meta, context}, represented}
196337
end
197338

339+
# replace keys in key value pairs
340+
defp do_use_existing_placeholders({key, value}, represented) when is_atom(key) do
341+
key = Mapping.get_existing_placeholder(represented, key) || key
342+
{{key, value}, represented}
343+
end
344+
198345
defp do_use_existing_placeholders(node, represented), do: {node, represented}
199346

200-
def drop_docstring({:__block__, meta, children}) do
347+
defp drop_docstring({:__block__, meta, children}) do
201348
children =
202349
children
203350
|> Enum.reject(fn
@@ -210,18 +357,18 @@ defmodule Representer do
210357
{:__block__, meta, children}
211358
end
212359

213-
def drop_docstring(node), do: node
360+
defp drop_docstring(node), do: node
214361

215-
def drop_line_meta({marker, metadata, children}) do
362+
defp drop_line_meta({marker, metadata, children}) do
216363
metadata = Keyword.drop(metadata, [:line])
217364
{marker, metadata, children}
218365
end
219366

220-
def drop_line_meta(node), do: node
367+
defp drop_line_meta(node), do: node
221368

222-
def add_parentheses_in_pipes({:|>, meta, [input, {name, meta2, atom}]}) when is_atom(atom) do
369+
defp add_parentheses_in_pipes({:|>, meta, [input, {name, meta2, atom}]}) when is_atom(atom) do
223370
{:|>, meta, [input, {name, meta2, []}]}
224371
end
225372

226-
def add_parentheses_in_pipes(node), do: node
373+
defp add_parentheses_in_pipes(node), do: node
227374
end

test/representer_test.exs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ defmodule RepresenterTest do
3333
test "modules" do
3434
test_directory("modules")
3535
end
36+
37+
test "module_attributes" do
38+
test_directory("module_attributes")
39+
end
3640
end
3741

3842
defp test_directory(dir) do
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"Placeholder_18": "Eighteen",
3+
"Placeholder_26": "TwentySix",
4+
"Placeholder_27": "TwentySeven",
5+
"placeholder_1": "one",
6+
"placeholder_10": "ten",
7+
"placeholder_11": "eleven",
8+
"placeholder_12": "twelve",
9+
"placeholder_13": "thirteen",
10+
"placeholder_14": "fourteen",
11+
"placeholder_15": "fifteen",
12+
"placeholder_16": "sixteen",
13+
"placeholder_17": "seventeen",
14+
"placeholder_19": "nineteen",
15+
"placeholder_2": "two",
16+
"placeholder_20": "twenty",
17+
"placeholder_21": "twentyone",
18+
"placeholder_22": "twentytwo",
19+
"placeholder_23": "twentythree",
20+
"placeholder_24": "twentyfour",
21+
"placeholder_25": "twentyfive",
22+
"placeholder_28": "integer",
23+
"placeholder_29": "twentynine",
24+
"placeholder_3": "three",
25+
"placeholder_4": "four",
26+
"placeholder_5": "five",
27+
"placeholder_6": "six",
28+
"placeholder_7": "seven",
29+
"placeholder_8": "eight",
30+
"placeholder_9": "nine"
31+
}

0 commit comments

Comments
 (0)