diff --git a/lib/representer.ex b/lib/representer.ex index 96d2a9d..af148bf 100644 --- a/lib/representer.ex +++ b/lib/representer.ex @@ -19,33 +19,161 @@ 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!() |> 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) + |> 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) |> Macro.prewalk(&drop_docstring/1) |> Macro.prewalk(&drop_line_meta/1) |> 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 @@ -53,7 +181,7 @@ defmodule Representer do end end - def define_placeholders(node, represented) do + defp define_placeholders(node, represented) do do_define_placeholders(node, represented) end @@ -87,13 +215,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,12 +243,21 @@ defmodule Representer do {node, represented} end + # module attributes + @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 + 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} @@ -116,7 +265,7 @@ defmodule Representer do 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 @@ -124,7 +273,7 @@ defmodule Representer do end end - def use_existing_placeholders(node, represented) do + defp use_existing_placeholders(node, represented) do do_use_existing_placeholders(node, represented) end @@ -132,24 +281,23 @@ defmodule Representer do 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 +317,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 end function_name = placeholder_function_name || function_name - - meta2 = Keyword.put(meta2, :visited?, true) - {{{:., meta2, [module, function_name]}, meta, context}, represented} end @@ -189,15 +333,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 +357,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 diff --git a/test/representer_test.exs b/test/representer_test.exs index 3fab2a6..d3c8c3b 100644 --- a/test/representer_test.exs +++ b/test/representer_test.exs @@ -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 diff --git a/test_data/module_attributes/expected_mapping.json b/test_data/module_attributes/expected_mapping.json new file mode 100644 index 0000000..45a6855 --- /dev/null +++ b/test_data/module_attributes/expected_mapping.json @@ -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" +} diff --git a/test_data/module_attributes/expected_representation.txt b/test_data/module_attributes/expected_representation.txt new file mode 100644 index 0000000..10a7e8e --- /dev/null +++ b/test_data/module_attributes/expected_representation.txt @@ -0,0 +1,39 @@ +defmodule Placeholder_18 do + @type placeholder_1 :: integer + @typep placeholder_2 :: integer | atom | placeholder_1 | placeholder_1 + @opaque placeholder_3(placeholder_4) :: [{:ok, placeholder_4} | {:error, placeholder_2}] + @spec placeholder_5(placeholder_6, placeholder_17 :: integer) :: keyword(placeholder_6) when placeholder_6: atom + @spec placeholder_7(placeholder_8, placeholder_9) :: {placeholder_8, placeholder_9} + when placeholder_8: atom, placeholder_9: integer + @spec placeholder_10(placeholder_11) :: [placeholder_11] when placeholder_11: var + @callback placeholder_12(placeholder_19 :: placeholder_3(placeholder_1), placeholder_20 :: any) :: placeholder_10(any) + @macrocallback placeholder_13(placeholder_21 :: String.t()) :: Macro.t() + @optional_callbacks placeholder_22: 0, placeholder_23: 1 + @impl true + def placeholder_22 do + :ok + end + + @impl Placeholder_18 + def placeholder_23 do + :error + end + + @behaviour Placeholder_18 + @compile {:inline, placeholder_22: 0} + @deprecated "This module is pretty complex" + @dialyzer {:nowarn_function, placeholder_23: 0} + @placeholder_24 Placeholder_18 + @placeholder_25 0 + defmodule Placeholder_26 do + @type placeholder_14 :: Placeholder_18.Placeholder_26.Placeholder_27.placeholder_15(integer) + defmodule Placeholder_27 do + @opaque placeholder_15(placeholder_16) :: [{atom, placeholder_16}] + end + + @spec placeholder_17(placeholder_28 :: integer) :: integer + def placeholder_29(placeholder_28) do + placeholder_28 + 1 + end + end +end diff --git a/test_data/module_attributes/input.ex b/test_data/module_attributes/input.ex new file mode 100644 index 0000000..973e77c --- /dev/null +++ b/test_data/module_attributes/input.ex @@ -0,0 +1,40 @@ +defmodule Eighteen do + # type specifications + @type one() :: integer + @typep two :: integer() | atom | one | one() + @opaque three(four) :: [{:ok, four} | {:error, two}] + @spec five(six, seventeen :: integer()) :: keyword(six) when six: atom() + @spec seven(eight, nine) :: {eight, nine} when eight: atom(), nine: integer() + @spec ten(eleven()) :: [eleven()] when eleven: var() + @callback twelve(nineteen :: three(one), twenty :: any()) :: ten(any()) + @macrocallback thirteen(twentyone :: String.t()) :: Macro.t() + @optional_callbacks twentytwo: 0, twentythree: 1 + @impl true + def twentytwo, do: :ok + @impl Eighteen + def twentythree, do: :error + + # other module attributes + @behaviour Eighteen + @compile {:inline, twentytwo: 0} + @deprecated "This module is pretty complex" + @doc """ + even more without documentation + """ + @dialyzer {:nowarn_function, twentythree: 0} + + # custom module attributes + @twentyfour Eighteen + @twentyfive 0 + + defmodule TwentySix do + @type fourteen() :: Eighteen.TwentySix.TwentySeven.fifteen(integer) + + defmodule TwentySeven do + @opaque fifteen(sixteen) :: [{atom, sixteen}] + end + + @spec seventeen(integer :: integer) :: integer + def twentynine(integer), do: integer + 1 + end +end diff --git a/test_data/two_modules/expected_mapping.json b/test_data/two_modules/expected_mapping.json index 44eb274..14380ce 100644 --- a/test_data/two_modules/expected_mapping.json +++ b/test_data/two_modules/expected_mapping.json @@ -1,12 +1,12 @@ { - "Placeholder_1": "HelloWorld", "Placeholder_12": "TestMultiple", + "Placeholder_2": "HelloWorld", + "placeholder_1": "add_then_subtract", "placeholder_10": "var", "placeholder_11": "check_expand_do", "placeholder_13": "test", - "placeholder_2": "hello", - "placeholder_3": "name", - "placeholder_4": "add_then_subtract", + "placeholder_3": "hello", + "placeholder_4": "name", "placeholder_5": "n", "placeholder_6": "a", "placeholder_7": "s", diff --git a/test_data/two_modules/expected_representation.txt b/test_data/two_modules/expected_representation.txt index 478a03d..4fb2391 100644 --- a/test_data/two_modules/expected_representation.txt +++ b/test_data/two_modules/expected_representation.txt @@ -1,10 +1,10 @@ -defmodule Placeholder_1 do - def placeholder_2(placeholder_3 \\ "world") do - "Hello, #{placeholder_3}" +defmodule Placeholder_2 do + def placeholder_3(placeholder_4 \\ "world") do + "Hello, #{placeholder_4}" end - @spec placeholder_4(integer(), integer(), integer()) :: integer() - def placeholder_4(placeholder_5, placeholder_6, placeholder_7) do + @spec placeholder_1(integer, integer, integer) :: integer + def placeholder_1(placeholder_5, placeholder_6, placeholder_7) do placeholder_8 = placeholder_5 + placeholder_6 placeholder_8 - placeholder_7 end