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

Normalizing multi-component modules and module aliases #44

Merged
merged 2 commits into from
Nov 13, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
77 changes: 48 additions & 29 deletions lib/representer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@ defmodule Representer do
alias Representer.Mapping

def process(file, code_output, mapping_output) do
{represented_ast, mapping} = represent(file)
{represented_ast, mapping} =
file
|> File.read!()
|> represent

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

def represent(file) do
def represent(code) do
{ast, mapping} =
file
|> File.read!()
code
|> Code.string_to_quoted!()
|> Macro.prewalk(&add_meta/1)
|> Macro.prewalk(Mapping.init(), &define_placeholders/2)
Expand Down Expand Up @@ -49,17 +51,23 @@ defmodule Representer do
do_define_placeholders(node, represented)
end

# module definition
defp do_define_placeholders(
{:defmodule, [line: x],
[{:__aliases__, [line: x], [module_name]} = module_alias | _] = args} = node,
{:defmodule, meta1, [{:__aliases__, meta2, module_name}, content]},
represented
) do
{:ok, represented, mapped_term} = Mapping.get_placeholder(represented, module_name, :module)

module_alias = module_alias |> Tuple.delete_at(2) |> Tuple.append([mapped_term])
args = [module_alias | args |> tl]
node = node |> Tuple.delete_at(2) |> Tuple.append(args)
{:ok, represented, names} = Mapping.get_placeholder(represented, module_name)
node = {:defmodule, meta1, [{:__aliases__, meta2, names}, content]}
{node, represented}
end

# module alias
defp do_define_placeholders(
{:alias, meta, [module, [as: {:__aliases__, meta2, module_alias}]]},
represented
) do
{:ok, represented, names} = Mapping.get_placeholder(represented, module_alias)
node = {:alias, meta, [module, [as: {:__aliases__, meta2, names}]]}
{node, represented}
end

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

{:ok, represented, mapped_name} = Representer.Mapping.get_placeholder(represented, name3)
{:ok, represented, mapped_name} = Mapping.get_placeholder(represented, name3)
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} = Representer.Mapping.get_placeholder(represented, name)
{:ok, represented, mapped_name} = Mapping.get_placeholder(represented, name)
meta2 = Keyword.put(meta2, :visited?, true)

{[{mapped_name, meta2, args2} | args_tail], represented}
Expand All @@ -92,10 +100,10 @@ defmodule Representer do
# 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__, :...]
@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
{:ok, represented, mapped_term} = Representer.Mapping.get_placeholder(represented, atom)
{:ok, represented, mapped_term} = Mapping.get_placeholder(represented, atom)

{{mapped_term, meta, context}, represented}
end
Expand All @@ -114,10 +122,23 @@ defmodule Representer 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)
)

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 = Representer.Mapping.get_existing_placeholder(represented, atom)
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
Expand All @@ -127,31 +148,30 @@ defmodule Representer do

# external function calls
defp do_use_existing_placeholders(
{{:., meta2, [{:__aliases__, meta3, [module_name]}, function_name]}, meta, context},
{{:., meta2, [{:__aliases__, _, module_name} = module, function_name]}, meta, context},
represented
)
when is_atom(module_name) and is_atom(function_name) do
placeholder_module_name =
Representer.Mapping.get_existing_placeholder(represented, module_name)
when is_list(module_name) and is_atom(function_name) do
{{_, _, new_module_name} = module, _} = do_use_existing_placeholders(module, represented)

module_name = placeholder_module_name || module_name
all_replaced? =
Enum.zip_with(module_name, new_module_name, &(&1 != &2))
|> Enum.all?()

placeholder_function_name =
if placeholder_module_name do
Representer.Mapping.get_existing_placeholder(represented, function_name)
if all_replaced? do
Mapping.get_existing_placeholder(represented, function_name)
else
# hack: assuming that if a module has no placeholder name, that means it's not being defined in this file
# 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
end

function_name = placeholder_function_name || function_name

meta2 = Keyword.put(meta2, :visited?, true)
meta3 = Keyword.put(meta3, :visited?, true)

{{{:., meta2, [{:__aliases__, meta3, [module_name]}, function_name]}, meta, context},
represented}
{{{:., meta2, [module, function_name]}, meta, context}, represented}
end

# external function calls via __MODULE__
Expand All @@ -160,8 +180,7 @@ defmodule Representer do
represented
)
when is_atom(function_name) do
placeholder_function_name =
Representer.Mapping.get_existing_placeholder(represented, function_name)
placeholder_function_name = Mapping.get_existing_placeholder(represented, function_name)

function_name = placeholder_function_name || function_name
meta2 = Keyword.put(meta2, :visited?, true)
Expand Down
13 changes: 12 additions & 1 deletion lib/representer/mapping.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,18 @@ defmodule Representer.Mapping do
%Mapping{}
end

def get_placeholder(%Mapping{} = map, term, _type \\ :term) do
def get_placeholder(%Mapping{} = map, terms) when is_list(terms) do
{map, names} =
Enum.reduce(terms, {map, []}, fn
name, {map, names} ->
{:ok, map, name} = Mapping.get_placeholder(map, name)
{map, [name | names]}
end)

{:ok, map, Enum.reverse(names)}
end

def get_placeholder(%Mapping{} = map, term) do
if map.mappings[term] do
{:ok, map, map.mappings[term]}
else
Expand Down
3 changes: 3 additions & 0 deletions test/representer_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ defmodule RepresenterTest do
test "parentheses_in_pipes" do
test_directory("parentheses_in_pipes")
end
test "modules" do
test_directory("modules")
end
end

defp test_directory(dir) do
Expand Down
16 changes: 16 additions & 0 deletions test_data/modules/expected_mapping.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"Placeholder_1": "One",
"Placeholder_10": "Ten",
"Placeholder_11": "Eleven",
"Placeholder_12": "Twelve",
"Placeholder_13": "Thirteen",
"Placeholder_2": "Two",
"Placeholder_5": "Five",
"Placeholder_6": "Six",
"Placeholder_7": "Seven",
"Placeholder_8": "Eight",
"placeholder_14": "fourteen",
"placeholder_3": "three",
"placeholder_4": "four",
"placeholder_9": "nine"
}
38 changes: 38 additions & 0 deletions test_data/modules/expected_representation.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
(
defmodule(Placeholder_1) do
alias(Placeholder_1, as: Placeholder_2)
def(placeholder_3) do
Placeholder_1.placeholder_4()
Placeholder_2.placeholder_4()
end
def(placeholder_4) do
:ok
end
end
defmodule(Placeholder_5.Placeholder_6.Placeholder_7) do
alias(Placeholder_5.Placeholder_6, as: Placeholder_8)
def(placeholder_9) do
Placeholder_1.placeholder_3()
Placeholder_2.placeholder_3()
Placeholder_5.Placeholder_6.Placeholder_7.placeholder_9()
Placeholder_8.Placeholder_7.placeholder_9()
__MODULE__.placeholder_9()
External.external()
Placeholder_1.External.external()
Placeholder_5.External.Placeholder_7.nine()
end
defmodule(Placeholder_10) do

end
defmodule(Placeholder_11) do
alias(Placeholder_5.Placeholder_6.Placeholder_7.{Placeholder_10, Placeholder_11})
alias(Placeholder_8.Placeholder_7.Placeholder_11, as: Placeholder_12)
alias(Placeholder_8.Placeholder_7, as: Placeholder_2)
alias(External, as: Placeholder_13)
def(placeholder_14) do
External.external()
Placeholder_13.external()
end
end
end
)
45 changes: 45 additions & 0 deletions test_data/modules/input.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
defmodule One do
alias One, as: Two

def three do
One.four()
Two.four()
end

def four, do: :ok
end

defmodule Five.Six.Seven do
alias Five.Six, as: Eight

def nine do
One.three()
Two.three()

Five.Six.Seven.nine()
Eight.Seven.nine()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So close to "why is six afraid of seven?" 😁

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh no! Is it too late to change the test? lol

__MODULE__.nine()
# not replaced
External.external()
# only :One is replaced
One.External.external()
# nine not replaced because of External
Five.External.Seven.nine()
end

defmodule Ten do
end

defmodule Eleven do
alias Five.Six.Seven.{Ten, Eleven}
alias Eight.Seven.Eleven, as: Twelve
alias Eight.Seven, as: Two

alias External, as: Thirteen

def fourteen do
External.external()
Thirteen.external()
end
end
end
2 changes: 1 addition & 1 deletion test_data/special_variables/expected_representation.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule(Placeholder_1) do
:math.pow(__DIR__)
placeholder_6.(__ENV__)
end
defmacro(placeholder_7()) do
defmacro(placeholder_7(_)) do
placeholder_2(__CALLER__, __STACKTRACE__)
placeholder_8 = 3
end
Expand Down
2 changes: 1 addition & 1 deletion test_data/special_variables/input.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule AnythingAndEverything do
some_value.(__ENV__)
end

defmacro foo() do
defmacro foo(_) do
some_function(__CALLER__, __STACKTRACE__)
_ignored = 3
end
Expand Down