-
-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Normalize types, typespecs and module attributes #52
Changes from 4 commits
a99d572
8061c25
376104b
3162079
4f2c7da
04788f5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,41 +19,174 @@ defmodule Representer do | |
File.write!(mapping_output, to_string(mapping)) | ||
end | ||
|
||
@spec represent(code :: String.t()) :: {Macro.t(), map()} | ||
def represent(code) do | ||
{ast, mapping} = | ||
code | ||
|> Code.string_to_quoted!() | ||
# protecting specific nodes | ||
|> Macro.prewalk(&add_meta/1) | ||
|> Macro.prewalk(Mapping.init(), &define_placeholders/2) | ||
# gathering type definitions | ||
|> Macro.prewalk(Mapping.init(), &define_type_placeholders/2) | ||
|
||
# replacing type definitions | ||
{ast, mapping} = | ||
Macro.prewalk(ast, mapping, &use_existing_placeholders/2) | ||
# protecting types from further change | ||
|> Macro.prewalk(&protect_types/1) | ||
|
||
# gathering function names and variables | ||
{ast, mapping} = Macro.prewalk(ast, mapping, &define_placeholders/2) | ||
|
||
ast | ||
# names in local function calls can only be exchanged after all names in function definitions were exchanged | ||
# replacing function names and variables | ||
|> Macro.prewalk(mapping, &use_existing_placeholders/2) | ||
# dropping docs | ||
|> Macro.prewalk(&drop_docstring/1) | ||
# removing added metadata | ||
|> Macro.prewalk(&drop_line_meta/1) | ||
# adding parentheses in pipes | ||
|> Macro.prewalk(&add_parentheses_in_pipes/1) | ||
end | ||
|
||
@doc """ | ||
""" | ||
def add_meta({:"::", _, [_, {:binary, meta, _} = bin] = args} = node) do | ||
# protect string interpolations | ||
defp add_meta({:"::", meta, [interpolate, {:binary, meta2, atom}]}) do | ||
meta2 = Keyword.put(meta2, :visited?, true) | ||
{:"::", meta, [interpolate, {:binary, meta2, atom}]} | ||
end | ||
|
||
defp add_meta(node), do: node | ||
|
||
defp define_type_placeholders({_, meta, _} = node, represented) do | ||
if meta[:visited?] do | ||
{node, represented} | ||
else | ||
do_define_type_placeholders(node, represented) | ||
end | ||
end | ||
|
||
defp define_type_placeholders(node, represented) do | ||
do_define_type_placeholders(node, represented) | ||
end | ||
|
||
@typecreation ~w(type typep opaque)a | ||
@typespecs ~w(spec callback macrocallback)a | ||
# type creation and type specifications without :when | ||
defp do_define_type_placeholders( | ||
{:@, meta, [{create, meta2, [{:"::", meta3, [{name, meta4, args}, definition]}]}]}, | ||
represented | ||
) | ||
when create in @typecreation or create in @typespecs do | ||
{:ok, represented, name} = Mapping.get_placeholder(represented, name) | ||
|
||
{args, represented} = | ||
cond do | ||
is_atom(args) -> | ||
{args, represented} | ||
|
||
args == [] -> | ||
{nil, represented} | ||
|
||
create in @typespecs -> | ||
args = Enum.map(args, &remove_type_parentheses/1) | ||
{args, represented} | ||
|
||
create in @typecreation -> | ||
args = Enum.map(args, &remove_type_parentheses/1) | ||
# when creating types, types may be passed as arguments to be used in the definitions | ||
vars = Enum.map(args, fn {var, _, nil} -> var end) | ||
{:ok, represented, _} = Mapping.get_placeholder(represented, vars) | ||
{args, represented} | ||
end | ||
|
||
definition = Macro.prewalk(definition, &remove_type_parentheses/1) | ||
meta = Keyword.put(meta, :visited?, true) | ||
meta2 = Keyword.put(meta2, :visited?, true) | ||
meta4 = Keyword.put(meta4, :visited?, true) | ||
|
||
{{:@, meta, [{create, meta2, [{:"::", meta3, [{name, meta4, args}, definition]}]}]}, | ||
represented} | ||
end | ||
|
||
# type specifications with :when | ||
defp do_define_type_placeholders( | ||
{:@, meta, | ||
[ | ||
{create, meta2, | ||
[{:when, meta_when, [{:"::", meta3, [{name, meta4, args}, definition]}, conditions]}]} | ||
]}, | ||
represented | ||
) | ||
when create in @typespecs do | ||
{:ok, represented, name} = Mapping.get_placeholder(represented, name) | ||
|
||
{args, represented} = | ||
if is_atom(args) do | ||
{args, represented} | ||
else | ||
args = Enum.map(args, &remove_type_parentheses/1) | ||
{args, represented} | ||
end | ||
|
||
conditions = Macro.prewalk(conditions, &remove_type_parentheses/1) | ||
# typespecs may receive variable types as arguments if they are constrained by :when | ||
vars = Enum.map(conditions, fn {var, _type} -> var end) | ||
{:ok, represented, _} = Mapping.get_placeholder(represented, vars) | ||
|
||
definition = Macro.prewalk(definition, &remove_type_parentheses/1) | ||
meta = Keyword.put(meta, :visited?, true) | ||
bin = Tuple.delete_at(bin, 1) |> Tuple.insert_at(1, meta) | ||
args = List.replace_at(args, 1, bin) | ||
_node = Tuple.delete_at(node, 2) |> Tuple.append(args) | ||
meta2 = Keyword.put(meta2, :visited?, true) | ||
meta4 = Keyword.put(meta4, :visited?, true) | ||
|
||
{{:@, meta, | ||
[ | ||
{create, meta2, | ||
[{:when, meta_when, [{:"::", meta3, [{name, meta4, args}, definition]}, conditions]}]} | ||
]}, represented} | ||
end | ||
|
||
defp do_define_type_placeholders(node, represented), do: {node, represented} | ||
|
||
defp remove_type_parentheses({:"::", meta, [var, type]}) do | ||
{:"::", meta, [var, remove_type_parentheses(type)]} | ||
end | ||
|
||
defp remove_type_parentheses({atom, type}) when is_atom(atom) do | ||
{atom, remove_type_parentheses(type)} | ||
end | ||
|
||
defp remove_type_parentheses({{:., meta, path}, meta2, args}) do | ||
meta2 = Keyword.put(meta2, :no_parens, true) | ||
{{:., meta, path}, meta2, args} | ||
end | ||
|
||
defp remove_type_parentheses({type, meta, args}) when args == [] or is_atom(args) do | ||
meta = Keyword.put(meta, :type?, true) | ||
{type, meta, nil} | ||
end | ||
|
||
defp remove_type_parentheses(node), do: node | ||
|
||
defp protect_types({name, meta, args} = node) do | ||
if meta[:type?] do | ||
meta = meta |> Keyword.drop([:type?]) |> Keyword.put(:visited?, true) | ||
{name, meta, args} | ||
else | ||
node | ||
end | ||
end | ||
|
||
def add_meta(node), do: node | ||
defp protect_types(node), do: node | ||
|
||
def define_placeholders({_, meta, _} = node, represented) do | ||
defp define_placeholders({_, meta, _} = node, represented) do | ||
if meta[:visited?] do | ||
{node, represented} | ||
else | ||
do_define_placeholders(node, represented) | ||
end | ||
end | ||
|
||
def define_placeholders(node, represented) do | ||
defp define_placeholders(node, represented) do | ||
do_define_placeholders(node, represented) | ||
end | ||
|
||
|
@@ -87,13 +220,25 @@ defmodule Representer do | |
# function/macro/guard definition with a guard | ||
[{name3, meta3, args3} | args2_tail] = args2 | ||
|
||
{:ok, represented, mapped_name} = Mapping.get_placeholder(represented, name3) | ||
{:ok, represented, mapped_name} = | ||
if meta3[:visited?] do | ||
{:ok, represented, name3} | ||
else | ||
Mapping.get_placeholder(represented, name3) | ||
end | ||
|
||
meta2 = Keyword.put(meta2, :visited?, true) | ||
meta3 = Keyword.put(meta3, :visited?, true) | ||
|
||
{[{name, meta2, [{mapped_name, meta3, args3} | args2_tail]} | args_tail], represented} | ||
else | ||
{:ok, represented, mapped_name} = Mapping.get_placeholder(represented, name) | ||
{:ok, represented, mapped_name} = | ||
if meta2[:visited?] do | ||
{:ok, represented, name} | ||
else | ||
Mapping.get_placeholder(represented, name) | ||
end | ||
|
||
meta2 = Keyword.put(meta2, :visited?, true) | ||
|
||
{[{mapped_name, meta2, args2} | args_tail], represented} | ||
|
@@ -103,53 +248,61 @@ defmodule Representer do | |
{node, represented} | ||
end | ||
|
||
# module attributes | ||
@reserved_attributes ~w(after_compile before_compile behaviour impl compile deprecated doc typedoc dialyzer file moduledoc on_definition vsn derive enforce_keys optional_callbacks)a | ||
jiegillet marked this conversation as resolved.
Show resolved
Hide resolved
|
||
defp do_define_placeholders({:@, meta, [{name, meta2, value}]}, represented) | ||
when name not in @reserved_attributes do | ||
{:ok, represented, name} = Mapping.get_placeholder(represented, name) | ||
node = {:@, meta, [{name, meta2, value}]} | ||
{node, represented} | ||
end | ||
|
||
# variables | ||
# https://elixir-lang.org/getting-started/meta/quote-and-unquote.html | ||
# "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." | ||
@special_var_names [:__CALLER__, :__DIR__, :__ENV__, :__MODULE__, :__STACKTRACE__, :..., :_] | ||
defp do_define_placeholders({atom, meta, context}, represented) | ||
when is_atom(atom) and is_nil(context) and atom not in @special_var_names do | ||
when is_atom(atom) and is_atom(context) and atom not in @special_var_names do | ||
{:ok, represented, mapped_term} = Mapping.get_placeholder(represented, atom) | ||
|
||
{{mapped_term, meta, context}, represented} | ||
end | ||
|
||
defp do_define_placeholders(node, represented), do: {node, represented} | ||
|
||
def use_existing_placeholders({_, meta, _} = node, represented) do | ||
defp use_existing_placeholders({_, meta, _} = node, represented) do | ||
if meta[:visited?] do | ||
{node, represented} | ||
else | ||
do_use_existing_placeholders(node, represented) | ||
end | ||
end | ||
|
||
def use_existing_placeholders(node, represented) do | ||
defp use_existing_placeholders(node, represented) do | ||
do_use_existing_placeholders(node, represented) | ||
end | ||
|
||
# module names | ||
defp do_use_existing_placeholders({:__aliases__, meta, module_name}, represented) | ||
when is_list(module_name) do | ||
module_name = | ||
Enum.map( | ||
module_name, | ||
&(Mapping.get_existing_placeholder(represented, &1) || &1) | ||
) | ||
Enum.map(module_name, &(Mapping.get_existing_placeholder(represented, &1) || &1)) | ||
|
||
meta = Keyword.put(meta, :visited?, true) | ||
{{:__aliases__, meta, module_name}, represented} | ||
end | ||
|
||
# local function calls | ||
defp do_use_existing_placeholders({atom, meta, context}, represented) | ||
when is_atom(atom) and is_list(context) do | ||
placeholder = Mapping.get_existing_placeholder(represented, atom) | ||
|
||
# if there is no placeholder for this name, that means it's an imported or a standard library function/macro/special form | ||
atom = placeholder || atom | ||
|
||
{{atom, meta, context}, represented} | ||
# variables or local function calls | ||
defp do_use_existing_placeholders({atom, meta, context} = node, represented) | ||
when is_atom(atom) do | ||
case Mapping.get_existing_placeholder(represented, atom) do | ||
nil -> | ||
# no representation yet, built-in type, imported, standard function/macro/special form... | ||
{node, represented} | ||
|
||
atom -> | ||
meta = Keyword.put(meta, :visited?, true) | ||
{{atom, meta, context}, represented} | ||
end | ||
end | ||
|
||
# external function calls | ||
|
@@ -169,14 +322,10 @@ defmodule Representer do | |
Mapping.get_existing_placeholder(represented, function_name) | ||
else | ||
# hack: assuming that if a module has no complete placeholder name, that means it's not being defined in this file | ||
# TODO: fix when dealing with aliases | ||
nil | ||
Comment on lines
319
to
320
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does removing the TODO comment mean we're giving up on fixing this, or is it no longer considered a hack but the final solution? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually I interpreted this line as "this doesn't work with aliases yet". I added alias support in my previous PR but forgot to remove the comment. So my intent was not to comment on the hack. I don't really have an issue with the "hack" or any plan to remove it or finalize it... It works, so I didn't touch it :) |
||
end | ||
|
||
function_name = placeholder_function_name || function_name | ||
|
||
meta2 = Keyword.put(meta2, :visited?, true) | ||
|
||
{{{:., meta2, [module, function_name]}, meta, context}, represented} | ||
end | ||
|
||
|
@@ -189,15 +338,18 @@ defmodule Representer do | |
placeholder_function_name = Mapping.get_existing_placeholder(represented, function_name) | ||
|
||
function_name = placeholder_function_name || function_name | ||
meta2 = Keyword.put(meta2, :visited?, true) | ||
meta3 = Keyword.put(meta3, :visited?, true) | ||
|
||
{{{:., meta2, [{:__MODULE__, meta3, args3}, function_name]}, meta, context}, represented} | ||
end | ||
|
||
# replace keys in key value pairs | ||
defp do_use_existing_placeholders({key, value}, represented) when is_atom(key) do | ||
key = Mapping.get_existing_placeholder(represented, key) || key | ||
{{key, value}, represented} | ||
end | ||
|
||
defp do_use_existing_placeholders(node, represented), do: {node, represented} | ||
|
||
def drop_docstring({:__block__, meta, children}) do | ||
defp drop_docstring({:__block__, meta, children}) do | ||
children = | ||
children | ||
|> Enum.reject(fn | ||
|
@@ -210,18 +362,18 @@ defmodule Representer do | |
{:__block__, meta, children} | ||
end | ||
|
||
def drop_docstring(node), do: node | ||
defp drop_docstring(node), do: node | ||
|
||
def drop_line_meta({marker, metadata, children}) do | ||
defp drop_line_meta({marker, metadata, children}) do | ||
metadata = Keyword.drop(metadata, [:line]) | ||
{marker, metadata, children} | ||
end | ||
|
||
def drop_line_meta(node), do: node | ||
defp drop_line_meta(node), do: node | ||
|
||
def add_parentheses_in_pipes({:|>, meta, [input, {name, meta2, atom}]}) when is_atom(atom) do | ||
defp add_parentheses_in_pipes({:|>, meta, [input, {name, meta2, atom}]}) when is_atom(atom) do | ||
{:|>, meta, [input, {name, meta2, []}]} | ||
end | ||
|
||
def add_parentheses_in_pipes(node), do: node | ||
defp add_parentheses_in_pipes(node), do: node | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
{ | ||
"Placeholder_18": "Eighteen", | ||
"Placeholder_26": "TwentySix", | ||
"Placeholder_27": "TwentySeven", | ||
"placeholder_1": "one", | ||
"placeholder_10": "ten", | ||
"placeholder_11": "eleven", | ||
"placeholder_12": "twelve", | ||
"placeholder_13": "thirteen", | ||
"placeholder_14": "fourteen", | ||
"placeholder_15": "fifteen", | ||
"placeholder_16": "sixteen", | ||
"placeholder_17": "seventeen", | ||
"placeholder_19": "nineteen", | ||
"placeholder_2": "two", | ||
"placeholder_20": "twenty", | ||
"placeholder_21": "twentyone", | ||
"placeholder_22": "twentytwo", | ||
"placeholder_23": "twentythree", | ||
"placeholder_24": "twentyfour", | ||
"placeholder_25": "twentyfive", | ||
"placeholder_28": "integer", | ||
"placeholder_29": "twentynine", | ||
"placeholder_3": "three", | ||
"placeholder_4": "four", | ||
"placeholder_5": "five", | ||
"placeholder_6": "six", | ||
"placeholder_7": "seven", | ||
"placeholder_8": "eight", | ||
"placeholder_9": "nine" | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure those comments are useful. They're almost the same as the function names.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, they were very helpful to me as I was restructuring the code, but they don't need to stay.