From 6bd613aee132467684eb2b882ad8aa85a0291827 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Gillet?= <jie.gillet@gmail.com>
Date: Sat, 6 Nov 2021 18:10:49 +0900
Subject: [PATCH 1/2] Add _ to special variables

---
 lib/representer.ex                                      | 4 ++--
 test_data/special_variables/expected_representation.txt | 2 +-
 test_data/special_variables/input.ex                    | 2 +-
 3 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/lib/representer.ex b/lib/representer.ex
index e478ade..f813a91 100644
--- a/lib/representer.ex
+++ b/lib/representer.ex
@@ -92,10 +92,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
diff --git a/test_data/special_variables/expected_representation.txt b/test_data/special_variables/expected_representation.txt
index 38f3525..2f2a03e 100644
--- a/test_data/special_variables/expected_representation.txt
+++ b/test_data/special_variables/expected_representation.txt
@@ -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
diff --git a/test_data/special_variables/input.ex b/test_data/special_variables/input.ex
index 5acff4a..36f6dc1 100644
--- a/test_data/special_variables/input.ex
+++ b/test_data/special_variables/input.ex
@@ -6,7 +6,7 @@ defmodule AnythingAndEverything do
     some_value.(__ENV__)
   end
 
-  defmacro foo() do
+  defmacro foo(_) do
     some_function(__CALLER__, __STACKTRACE__)
     _ignored = 3
   end

From b7e5ef13561511cd3c20abbba8a1fd075a91b186 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Gillet?= <jie.gillet@gmail.com>
Date: Sat, 6 Nov 2021 18:13:46 +0900
Subject: [PATCH 2/2] Mulit-component modules, module aliases

---
 lib/representer.ex                            | 73 ++++++++++++-------
 lib/representer/mapping.ex                    | 13 +++-
 test/representer_test.exs                     |  3 +
 test_data/modules/expected_mapping.json       | 16 ++++
 test_data/modules/expected_representation.txt | 38 ++++++++++
 test_data/modules/input.ex                    | 45 ++++++++++++
 6 files changed, 160 insertions(+), 28 deletions(-)
 create mode 100644 test_data/modules/expected_mapping.json
 create mode 100644 test_data/modules/expected_representation.txt
 create mode 100644 test_data/modules/input.ex

diff --git a/lib/representer.ex b/lib/representer.ex
index f813a91..28d369c 100644
--- a/lib/representer.ex
+++ b/lib/representer.ex
@@ -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)
@@ -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
 
@@ -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}
@@ -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
@@ -127,20 +148,21 @@ 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
@@ -148,10 +170,8 @@ defmodule Representer do
     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__
@@ -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)
diff --git a/lib/representer/mapping.ex b/lib/representer/mapping.ex
index 056c8ae..80f06ef 100644
--- a/lib/representer/mapping.ex
+++ b/lib/representer/mapping.ex
@@ -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
diff --git a/test/representer_test.exs b/test/representer_test.exs
index 85576b3..3fb11a5 100644
--- a/test/representer_test.exs
+++ b/test/representer_test.exs
@@ -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
diff --git a/test_data/modules/expected_mapping.json b/test_data/modules/expected_mapping.json
new file mode 100644
index 0000000..678a492
--- /dev/null
+++ b/test_data/modules/expected_mapping.json
@@ -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"
+}
diff --git a/test_data/modules/expected_representation.txt b/test_data/modules/expected_representation.txt
new file mode 100644
index 0000000..a9d4bf5
--- /dev/null
+++ b/test_data/modules/expected_representation.txt
@@ -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
+)
diff --git a/test_data/modules/input.ex b/test_data/modules/input.ex
new file mode 100644
index 0000000..3804af9
--- /dev/null
+++ b/test_data/modules/input.ex
@@ -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()
+    __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