Skip to content

Commit

Permalink
Wip
Browse files Browse the repository at this point in the history
  • Loading branch information
Premwoik committed Jun 13, 2022
1 parent 55b323f commit d251b78
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 24 deletions.
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
elixir 1.12.3
elixir 1.13.3
erlang 24.1
23 changes: 17 additions & 6 deletions lib/gradient.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ defmodule Gradient do

with {:ok, forms} <- ElixirFileUtils.get_forms(file),
{:elixir, _} <- wrap_language_name(forms) do
forms = maybe_specify_forms(forms, opts)
forms = update_code_path(forms, opts)
{tokens, opts} = maybe_use_tokens(forms, opts)
forms = maybe_specify_forms(forms, tokens, opts)

case maybe_gradient_check(forms, opts) ++ maybe_gradualizer_check(forms, opts) do
[] ->
Expand All @@ -51,6 +53,17 @@ defmodule Gradient do
end
end

defp maybe_use_tokens(forms, opts) do
unless opts[:no_tokens] do
tokens = Gradient.ElixirFileUtils.load_tokens(forms)
env = %{macro_lines: Gradient.Tokens.find_macro_lines(tokens)}
opts = Keyword.put(opts, :env, env)
{tokens, opts}
else
{[], Keyword.put(opts, :env, %{macro_lines: []})}
end
end

defp maybe_gradualizer_check(forms, opts) do
unless opts[:no_gradualizer_check] do
try do
Expand All @@ -73,11 +86,9 @@ defmodule Gradient do
end
end

defp maybe_specify_forms(forms, opts) do
defp maybe_specify_forms(forms, tokens, opts) do
unless opts[:no_specify] do
forms
|> put_code_path(opts)
|> AstSpecifier.specify()
AstSpecifier.run_mappers(forms, tokens)
else
forms
end
Expand All @@ -91,7 +102,7 @@ defmodule Gradient do
end
end

defp put_code_path(forms, opts) do
defp update_code_path(forms, opts) do
case opts[:code_path] do
nil ->
case opts[:app_path] do
Expand Down
26 changes: 16 additions & 10 deletions lib/gradient/elixir_checker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ defmodule Gradient.ElixirChecker do
- {`ex_check`, boolean()}: whether to use checks specific only to Elixir.
"""

@spec check([:erl_parse.abstract_form()], keyword()) :: [{:file.filename(), any()}]
@type env :: %{macro_lines: [integer()]}

@type opts :: [env: env(), ex_check: boolean()]

@spec check([:erl_parse.abstract_form()], opts()) :: [{:file.filename(), any()}]
def check(forms, opts) do
if Keyword.get(opts, :ex_check, true) do
check_spec(forms)
check_spec(forms, opts[:env])
else
[]
end
Expand Down Expand Up @@ -46,10 +50,12 @@ defmodule Gradient.ElixirChecker do
end
```
"""
@spec check_spec([:erl_parse.abstract_form()]) :: [{:file.filename(), any()}]
def check_spec([{:attribute, _, :file, {file, _}} | forms]) do
@spec check_spec([:erl_parse.abstract_form()], map()) :: [{:file.filename(), any()}]
def check_spec([{:attribute, _, :file, {file, _}} | forms], env) do
%{macro_lines: macro_lines} = env

forms
|> Stream.filter(&is_fun_or_spec?/1)
|> Stream.filter(&is_fun_or_spec?(&1, macro_lines))
|> Stream.map(&simplify_form/1)
|> Stream.concat()
|> Stream.filter(&is_not_generated?/1)
Expand Down Expand Up @@ -81,12 +87,12 @@ defmodule Gradient.ElixirChecker do
not (String.starts_with?(name_str, "__") and String.ends_with?(name_str, "__"))
end

def is_fun_or_spec?({:attribute, _, :spec, _}), do: true
def is_fun_or_spec?({:function, _, _, _, _}), do: true
def is_fun_or_spec?(_), do: false
def is_fun_or_spec?({:attribute, anno, :spec, _}, ml), do: :erl_anno.line(anno) not in ml
def is_fun_or_spec?({:function, anno, _, _, _}, ml), do: :erl_anno.line(anno) not in ml
def is_fun_or_spec?(_, _), do: false

@spec simplify_form(:erl_parse.abstract_form()) ::
Enumerable.t({:spec | :fun, {atom(), integer()}, :erl_anno.anno()})
# Returned type Enumerable.t({:spec | :fun, {atom(), integer()}, :erl_anno.anno()})
@spec simplify_form(:erl_parse.abstract_form()) :: Enumerable.t()
def simplify_form({:attribute, _, :spec, {{name, arity}, types}}) do
Stream.map(types, &{:spec, {name, arity}, elem(&1, 1)})
end
Expand Down
14 changes: 14 additions & 0 deletions lib/gradient/tokens.ex
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,20 @@ defmodule Gradient.Tokens do
end
end

def find_macro_lines([
{:identifier, {l, _, _}, :use},
{:alias, {l, _, _}, _},
{:eol, {l, _, _}} | t
]) do
[l | find_macro_lines(t)]
end
def find_macro_lines([_ | t]) do
find_macro_lines(t)
end
def find_macro_lines([]) do
[]
end

@doc """
Drop tokens to the first tuple occurrence. Returns type of the encountered
list and the following tokens.
Expand Down
21 changes: 21 additions & 0 deletions test/examples/spec_in_macro.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
defmodule NewMod do
defmacro __using__(opts \\ []) do
quote do
@spec new(attrs :: map()) :: atom()
def new(_attrs), do: :ok

@spec a(attrs :: map()) :: atom()
def a(_attrs), do: :ok

@spec b(attrs :: map()) :: atom()
def b(_attrs), do: :ok
end
end
end

defmodule SpecInMacro do
use NewMod

@spec c(attrs :: map()) :: atom()
def c(_attrs), do: :ok
end
25 changes: 18 additions & 7 deletions test/gradient/elixir_checker_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,27 @@ defmodule Gradient.ElixirCheckerTest do
test "checker options" do
ast = load("Elixir.SpecWrongName.beam")

assert [] = ElixirChecker.check(ast, ex_check: false)
assert [] != ElixirChecker.check(ast, ex_check: true)
assert [] = ElixirChecker.check(ast, env([], ex_check: false))
assert [] != ElixirChecker.check(ast, env([], ex_check: true))
end

test "all specs are correct" do
ast = load("Elixir.CorrectSpec.beam")

assert [] = ElixirChecker.check(ast, ex_check: true)
assert [] = ElixirChecker.check(ast, env())
end

test "specs over default args are correct" do
ast = load("Elixir.SpecDefaultArgs.beam")

assert [] = ElixirChecker.check(ast, ex_check: true)
assert [] = ElixirChecker.check(ast, env())
end

test "spec arity doesn't match the function arity" do
ast = load("Elixir.SpecWrongArgsArity.beam")

assert [{_, {:spec_error, :wrong_spec_name, 2, :foo, 3}}] =
ElixirChecker.check(ast, ex_check: true)
ElixirChecker.check(ast, env())
end

test "spec name doesn't match the function name" do
Expand All @@ -38,7 +38,7 @@ defmodule Gradient.ElixirCheckerTest do
assert [
{_, {:spec_error, :wrong_spec_name, 5, :convert, 1}},
{_, {:spec_error, :wrong_spec_name, 11, :last_two, 1}}
] = ElixirChecker.check(ast, [])
] = ElixirChecker.check(ast, env())
end

test "mixing specs names is not allowed" do
Expand All @@ -47,6 +47,17 @@ defmodule Gradient.ElixirCheckerTest do
assert [
{_, {:spec_error, :mixed_specs, 3, :encode, 1}},
{_, {:spec_error, :wrong_spec_name, 3, :encode, 1}}
] = ElixirChecker.check(ast, [])
] = ElixirChecker.check(ast, env())
end

test "spec defined in a __using__ macro" do
{tokens, ast} = load("Elixir.SpecInMacro.beam", "spec_in_macro.ex")

assert [] = ElixirChecker.check(ast, env(tokens))
end

defp env(tokens \\ [], opts \\ []) do
env = %{macro_lines: Gradient.Tokens.find_macro_lines(tokens)}
[{:env, env} | opts]
end
end

0 comments on commit d251b78

Please sign in to comment.