Skip to content

Commit

Permalink
Version macros and fix injected functions
Browse files Browse the repository at this point in the history
  • Loading branch information
brunvez committed May 12, 2022
1 parent 649c91c commit c93d0a1
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 14 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
## master (unreleased)

### New features
- Version macro docs

### Bug fixes

- Fix multiple versions or errors for injected functions

## 0.1.1 (2022-05-09)

Expand Down
57 changes: 44 additions & 13 deletions lib/scapa/code.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,18 @@ defmodule Scapa.Code do
@doc version: "74559578"
def functions_with_doc({:module, module, module_source}) do
docs = function_docs(module)
ast = Code.string_to_quoted!(module_source, columns: true)
source_functions = functions_defined_in_source(module_source)

docs
|> Enum.filter(&has_doc?/1)
|> Enum.filter(fn function_doc ->
has_doc?(function_doc) && defined_in_source?(function_doc, source_functions)
end)
|> Enum.map(fn
{{:function, name, arity}, doc_start, [string_signature], _, metadata} ->
{{_, name, arity}, _, [string_signature], _, metadata} ->
%FunctionDefinition{
signature: {module, name, arity, string_signature},
version: metadata[:version],
position: function_position(doc_start, ast)
position: function_position!({name, arity}, source_functions)
}
end)
end
Expand Down Expand Up @@ -77,26 +79,55 @@ defmodule Scapa.Code do
|> Enum.map(&String.to_existing_atom/1)
end

defp function_position(doc_start, ast) do
ast
|> Macro.prewalk([], fn
{:def, [line: line, column: column], _} = t, acc when line >= doc_start ->
{t, [{line, column} | acc]}
defp functions_defined_in_source(module_source) do
update_known_functions = fn known_functions, func_name, args, metadata ->
arity = args |> List.wrap() |> Enum.count()
function = {func_name, arity}
position = {metadata[:line], metadata[:column]}

t, acc ->
{t, acc}
Map.update(known_functions, function, position, &min(&1, position))
end

module_source
|> Code.string_to_quoted!(columns: true)
|> Macro.prewalk(%{}, fn
# def with guard
{def_type, metadata, [{:when, _, [{func_name, _, args}, _]}, _inner]} = t, known_functions
when def_type in [:def, :defmacro] ->
{t, update_known_functions.(known_functions, func_name, args, metadata)}

# def without body (function head)
{def_type, metadata, [{func_name, _meta, args}]} = t, known_functions
when def_type in [:def, :defmacro] ->
{t, update_known_functions.(known_functions, func_name, args, metadata)}

# normal def
{def_type, metadata, [{func_name, _meta, args}, _inner]} = t, known_functions
when def_type in [:def, :defmacro] ->
{t, update_known_functions.(known_functions, func_name, args, metadata)}

t, known_functions ->
{t, known_functions}
end)
|> elem(1)
|> Enum.min()
end

defp function_position!({name, arity}, source_functions),
do: Map.fetch!(source_functions, {name, arity})

defp has_doc?({_, _, _, doc_content, _}) when doc_content in [:none, :hidden], do: false
defp has_doc?(_), do: true

defp defined_in_source?({{_, function_name, arity}, _, _, _, _}, source_functions),
do: Map.has_key?(source_functions, {function_name, arity})

defp function_docs(module) do
{:docs_v1, _, :elixir, _, _, _, docs} = Code.fetch_docs(module)

docs
Enum.filter(docs, fn
{{def_type, _, _}, _, _, _, _} when def_type in [:function, :macro] -> true
_ -> false
end)
end

defp doc_tag(version) do
Expand Down
22 changes: 22 additions & 0 deletions test/scapa/cli_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,28 @@ defmodule Scapa.CLITest do
def public_no_doc, do: nil
@doc "Multiple arities 1"
@doc version: "11060830"
def multiple_arities(_a), do: nil
@doc "Multiple arities 2"
@doc version: "76143507"
def multiple_arities(_a, _b), do: nil
@doc "Public with guard"
@doc version: "58040676"
def public_with_guard(a) when is_atom(a), do: nil
@doc "Simple macro"
@doc version: "40986300"
defmacro macro(_a, _b, _c), do: nil
@doc "Macro with guard"
@doc version: "84650413"
defmacro __using__(which) when is_atom(which) and not is_nil(which) do
apply(__MODULE__, which, [])
end
defp private_fun, do: nil
end
"""
Expand Down
61 changes: 61 additions & 0 deletions test/scapa/code_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,67 @@ defmodule Scapa.CodeTest do
)
end

test "returns functions with multiple arities", %{Scapa.ModuleWithDoc => module_source} do
assert [
%FunctionDefinition{
signature: {Scapa.ModuleWithDoc, :multiple_arities, 1, "multiple_arities(a)"},
position: {27, 3},
version: nil
},
%FunctionDefinition{
position: {30, 3},
signature: {Scapa.ModuleWithDoc, :multiple_arities, 2, "multiple_arities(a, b)"},
version: nil
}
] =
function_docs(
Code.functions_with_doc({:module, Scapa.ModuleWithDoc, module_source}),
:multiple_arities
)
end

test "returns functions with guards", %{Scapa.ModuleWithDoc => module_source} do
assert [
%FunctionDefinition{
position: {33, 3},
signature: {Scapa.ModuleWithDoc, :public_with_guard, 1, "public_with_guard(a)"},
version: nil
}
] =
function_docs(
Code.functions_with_doc({:module, Scapa.ModuleWithDoc, module_source}),
:public_with_guard
)
end

test "returns macros", %{Scapa.ModuleWithDoc => module_source} do
assert [
%FunctionDefinition{
position: {36, 3},
signature: {Scapa.ModuleWithDoc, :macro, 3, "macro(a, b, c)"},
version: nil
}
] =
function_docs(
Code.functions_with_doc({:module, Scapa.ModuleWithDoc, module_source}),
:macro
)
end

test "returns macros with guards", %{Scapa.ModuleWithDoc => module_source} do
assert [
%FunctionDefinition{
position: {39, 3},
signature: {Scapa.ModuleWithDoc, :__using__, 1, "__using__(which)"},
version: nil
}
] =
function_docs(
Code.functions_with_doc({:module, Scapa.ModuleWithDoc, module_source}),
:__using__
)
end

test "does not include functions without doc", %{Scapa.ModuleWithDoc => module_source} do
docs = Code.functions_with_doc({:module, Scapa.ModuleWithDoc, module_source})

Expand Down
17 changes: 17 additions & 0 deletions test/support/module_with_doc.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,22 @@ defmodule Scapa.ModuleWithDoc do

def public_no_doc, do: nil

@doc "Multiple arities 1"
def multiple_arities(_a), do: nil

@doc "Multiple arities 2"
def multiple_arities(_a, _b), do: nil

@doc "Public with guard"
def public_with_guard(a) when is_atom(a), do: nil

@doc "Simple macro"
defmacro macro(_a, _b, _c), do: nil

@doc "Macro with guard"
defmacro __using__(which) when is_atom(which) and not is_nil(which) do
apply(__MODULE__, which, [])
end

defp private_fun, do: nil
end

0 comments on commit c93d0a1

Please sign in to comment.