From 1310f05da6841736c07dff10640f8fd0b30d7f20 Mon Sep 17 00:00:00 2001 From: Jakub Melkowski <9402720+Blatts12@users.noreply.github.com> Date: Tue, 17 Oct 2023 14:13:37 +0200 Subject: [PATCH] Use `Macro.to_string/1` instead of parsing specs by "hand" (#34) --- README.md | 2 +- lib/contexted/delegator.ex | 30 +++++++++++++- lib/contexted/module_analyzer.ex | 71 +++++++++----------------------- mix.exs | 2 +- 4 files changed, 50 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 48b12b7..8f41fad 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Add the following to your `mix.exs` file: ```elixir defp deps do [ - {:contexted, "~> 0.1.10"} + {:contexted, "~> 0.1.11"} ] end ``` diff --git a/lib/contexted/delegator.ex b/lib/contexted/delegator.ex index 12ccca8..0efffba 100644 --- a/lib/contexted/delegator.ex +++ b/lib/contexted/delegator.ex @@ -65,7 +65,7 @@ defmodule Contexted.Delegator do |> Enum.map(fn {name, arity} -> args = ModuleAnalyzer.generate_random_function_arguments(arity) doc = ModuleAnalyzer.get_function_doc(functions_docs, name, arity) - spec = ModuleAnalyzer.get_function_spec(functions_specs, name, arity, module) + spec = ModuleAnalyzer.get_function_spec(functions_specs, name, arity) {name, arity, args, doc, spec} end) @@ -85,9 +85,37 @@ defmodule Contexted.Delegator do end end) + types = + case Code.Typespec.fetch_types(module) do + {:ok, types} -> + Enum.map(types, fn + {type_of_type, type} -> + Code.Typespec.type_to_quoted(type) + |> add_ast_for_type(type_of_type) + + _ -> + nil + end) + + _ -> + [] + end + # Combine the generated delegates into a single AST quote do + (unquote_splicing(types)) (unquote_splicing(delegates)) end end + + @spec add_ast_for_type(tuple(), atom()) :: tuple() + defp add_ast_for_type(ast, type_of_type) do + {:@, [context: Elixir, imports: [{1, Kernel}]], + [ + {type_of_type, [context: Elixir], + [ + ast + ]} + ]} + end end diff --git a/lib/contexted/module_analyzer.ex b/lib/contexted/module_analyzer.ex index 06b15d0..0591028 100644 --- a/lib/contexted/module_analyzer.ex +++ b/lib/contexted/module_analyzer.ex @@ -3,8 +3,6 @@ defmodule Contexted.ModuleAnalyzer do The `Contexted.ModuleAnalyzer` defines utils functions that analyze and extract information from other modules. """ - @module_prefix "Elixir." - @doc """ Fetches the `@doc` definitions for all functions within the given module. """ @@ -37,14 +35,14 @@ defmodule Contexted.ModuleAnalyzer do Finds and returns the `@spec` definition in string format for the specified function name and arity. Returns `nil` if the function is not found in the specs. """ - @spec get_function_spec([tuple()], atom(), non_neg_integer(), module()) :: String.t() | nil - def get_function_spec(specs, function_name, arity, module) do + @spec get_function_spec([tuple()], atom(), non_neg_integer()) :: String.t() | nil + def get_function_spec(specs, function_name, arity) do # Find the spec tuple in the specs spec = find_spec(specs, function_name, arity) # If spec is found, build the spec expression if spec do - build_spec(spec, module) + build_spec(spec) else nil end @@ -89,54 +87,23 @@ defmodule Contexted.ModuleAnalyzer do end) end - @spec build_spec(tuple(), module()) :: String.t() - defp build_spec({{function_name, _arity}, spec}, module) do - {:type, _, :fun, [arg_types, return_type]} = hd(spec) - arg_types_string = format_arg_types(arg_types, module) - return_type_string = format_type(return_type, module) - - function_with_args = "#{function_name}(#{arg_types_string})" - return_value = return_type_string - - "@spec #{function_with_args} :: #{return_value}" - end - - @spec format_arg_types(tuple(), module()) :: String.t() - defp format_arg_types({:type, _, :product, []}, _module), do: "" - - defp format_arg_types({:type, _, :product, arg_types}, module) do - Enum.map_join(arg_types, ",", &format_type(&1, module)) - end - - @spec format_type(tuple(), module()) :: String.t() - defp format_type({:type, _, :union, types}, module) do - Enum.map_join(types, " | ", &format_type(&1, module)) - end - - defp format_type({:type, _, type_name, _}, _module), do: "#{type_name}()" - - defp format_type({:user_type, _, atom, _}, module) do - "#{Atom.to_string(module)}.#{Atom.to_string(atom)}()" - end - - defp format_type({:atom, _, atom}, _module) do - stringed_atom = Atom.to_string(atom) - - if is_module(stringed_atom) do - stringed_atom - else - ":#{stringed_atom}" - end + @spec build_spec(tuple()) :: String.t() + defp build_spec({{function_name, _arity}, specs}) do + Enum.map_join(specs, "\n", fn spec -> + Code.Typespec.spec_to_quoted(function_name, spec) + |> add_spec_ast() + |> Macro.to_string() + end) end - defp format_type({:remote_type, _, [{:atom, _, module}, {:atom, _, type}, _list]}, _module) do - if module == :elixir do - "#{type}()" - else - "#{module}.#{type}()" - end + @spec add_spec_ast(tuple()) :: tuple() + defp add_spec_ast(ast) do + {:@, [context: Elixir, imports: [{1, Kernel}]], + [ + {:spec, [context: Elixir], + [ + ast + ]} + ]} end - - @spec is_module(String.t()) :: boolean() - defp is_module(stringed_atom), do: String.starts_with?(stringed_atom, @module_prefix) end diff --git a/mix.exs b/mix.exs index 91eb032..4d4ba4f 100644 --- a/mix.exs +++ b/mix.exs @@ -6,7 +6,7 @@ defmodule Contexted.MixProject do app: :contexted, description: "Contexted is an Elixir library designed to streamline the management of complex Phoenix contexts in your projects, offering tools for module separation, subcontext creation, and auto-generating CRUD operations for improved code maintainability.", - version: "0.1.10", + version: "0.1.11", elixir: "~> 1.14", start_permanent: Mix.env() == :prod, deps: deps(),