Skip to content
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

Merged
merged 6 commits into from
Dec 12, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
238 changes: 195 additions & 43 deletions lib/representer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Member

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.

Copy link
Contributor Author

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.

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

Expand Down Expand Up @@ -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}
Expand All @@ -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
Expand All @@ -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
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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

Expand All @@ -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
Expand All @@ -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
4 changes: 4 additions & 0 deletions test/representer_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ defmodule RepresenterTest do
test "modules" do
test_directory("modules")
end

test "module_attributes" do
test_directory("module_attributes")
end
end

defp test_directory(dir) do
Expand Down
31 changes: 31 additions & 0 deletions test_data/module_attributes/expected_mapping.json
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"
}
Loading