From d251b781e8be100d053b9f530f4a4045e0800004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Mon, 13 Jun 2022 09:15:09 +0200 Subject: [PATCH] Wip --- .tool-versions | 2 +- lib/gradient.ex | 23 +++++++++++++++++------ lib/gradient/elixir_checker.ex | 26 ++++++++++++++++---------- lib/gradient/tokens.ex | 14 ++++++++++++++ test/examples/spec_in_macro.ex | 21 +++++++++++++++++++++ test/gradient/elixir_checker_test.exs | 25 ++++++++++++++++++------- 6 files changed, 87 insertions(+), 24 deletions(-) create mode 100644 test/examples/spec_in_macro.ex diff --git a/.tool-versions b/.tool-versions index 24efada7..df042d56 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -elixir 1.12.3 +elixir 1.13.3 erlang 24.1 diff --git a/lib/gradient.ex b/lib/gradient.ex index 2bc33c11..ba7e88f4 100644 --- a/lib/gradient.ex +++ b/lib/gradient.ex @@ -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 [] -> @@ -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 @@ -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 @@ -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 diff --git a/lib/gradient/elixir_checker.ex b/lib/gradient/elixir_checker.ex index 2b54b424..e3f54186 100644 --- a/lib/gradient/elixir_checker.ex +++ b/lib/gradient/elixir_checker.ex @@ -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 @@ -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) @@ -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 diff --git a/lib/gradient/tokens.ex b/lib/gradient/tokens.ex index 42f99ce5..da637c32 100644 --- a/lib/gradient/tokens.ex +++ b/lib/gradient/tokens.ex @@ -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. diff --git a/test/examples/spec_in_macro.ex b/test/examples/spec_in_macro.ex new file mode 100644 index 00000000..0a9ab4e8 --- /dev/null +++ b/test/examples/spec_in_macro.ex @@ -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 diff --git a/test/gradient/elixir_checker_test.exs b/test/gradient/elixir_checker_test.exs index e85d9665..fb7204f4 100644 --- a/test/gradient/elixir_checker_test.exs +++ b/test/gradient/elixir_checker_test.exs @@ -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 @@ -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 @@ -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