From 33cb6f10d373d0bb1018af0098475a7df88ec668 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Fri, 1 Oct 2021 19:24:26 +0200 Subject: [PATCH 01/15] Add mapper for function spec This commit aimed to specify a line in the undefined remote type error message. Unfortunately, the missing line was not the Elixir thing because Gradualizer cleans the location info on purpose. Well, but the mapper may be useful in the future, so why not push it. - Add spec mapper - Test spec mapper --- lib/gradient/specify_erl_ast.ex | 48 +++++++++++++++ lib/gradualizer_ex/type_annotation.ex | 20 +++++++ lib/mix/tasks/gradient.ex | 2 +- mix.exs | 2 +- mix.lock | 3 +- test/examples/Elixir.Typespec.beam | Bin 0 -> 2244 bytes test/examples/typespec.ex | 18 ++++++ test/gradient/specify_erl_ast_test.exs | 80 +++++++++++++++++++++++++ 8 files changed, 169 insertions(+), 4 deletions(-) create mode 100644 lib/gradualizer_ex/type_annotation.ex create mode 100644 test/examples/Elixir.Typespec.beam create mode 100644 test/examples/typespec.ex diff --git a/lib/gradient/specify_erl_ast.ex b/lib/gradient/specify_erl_ast.ex index 90628520..ea79ca6a 100644 --- a/lib/gradient/specify_erl_ast.ex +++ b/lib/gradient/specify_erl_ast.ex @@ -157,6 +157,14 @@ defmodule Gradient.SpecifyErlAst do @spec mapper(form(), [token()], options()) :: {form(), [token()]} defp mapper(form, tokens, opts) + defp mapper({:attribute, anno, :spec, {name_arity, specs}}, tokens, opts) do + # + new_specs = context_mapper_map(specs, [], opts, &spec_mapper/3) + + {:attribute, anno, :spec, {name_arity, new_specs}} + |> pass_tokens(tokens) + end + defp mapper({:function, _line, :__info__, _arity, _children} = form, tokens, _opts) do # skip analysis for __info__ functions pass_tokens(form, tokens) @@ -463,6 +471,46 @@ defmodule Gradient.SpecifyErlAst do pass_tokens(form, tokens) end + @doc """ + Adds missing line to the function specification. + """ + @spec spec_mapper(form(), tokens(), options()) :: {form(), tokens()} + def spec_mapper({:type, anno, type_name, args}, tokens, opts) do + {:ok, _line, anno, opts, _} = get_line(anno, opts) + new_args = context_mapper_map(args, tokens, opts, &spec_mapper/3) + + {:type, anno, type_name, new_args} + |> pass_tokens(tokens) + end + + def spec_mapper({:remote_type, anno, [mod, type, args]}, tokens, opts) do + {:ok, _line, anno, opts, _} = get_line(anno, opts) + {new_mod, _} = spec_mapper(mod, tokens, opts) + {new_type, _} = spec_mapper(type, tokens, opts) + new_args = context_mapper_map(args, tokens, opts, &spec_mapper/3) + + {:remote_type, anno, [new_mod, new_type, new_args]} + |> pass_tokens(tokens) + end + + def spec_mapper({:user_type, anno, name, args}, tokens, opts) do + new_args = context_mapper_map(args, tokens, opts, &spec_mapper/3) + + {:user_type, anno, name, new_args} + |> pass_tokens(tokens) + end + + def spec_mapper({:ann_type, anno, attrs}, tokens, opts) do + new_attrs = context_mapper_map(attrs, tokens, opts, &spec_mapper/3) + + {:ann_type, anno, new_attrs} + |> pass_tokens(tokens) + end + + def spec_mapper(type, tokens, opts) do + mapper(type, tokens, opts) + end + @doc """ Adds missing line to the module literal """ diff --git a/lib/gradualizer_ex/type_annotation.ex b/lib/gradualizer_ex/type_annotation.ex new file mode 100644 index 00000000..043232ce --- /dev/null +++ b/lib/gradualizer_ex/type_annotation.ex @@ -0,0 +1,20 @@ +defmodule GradualizerEx.TypeAnnotation do + defmacro annotate_type(expr, type) do + {:"::", [], [expr, Macro.to_string(type)]} + end + + defmacro assert_type(expr, type) do + {:":::", [], [expr, Macro.to_string(type)]} + end + + defmacro __using__(_) do + quote [] do + import GradualizerEx.TypeAnnotation + require GradualizerEx.TypeAnnotation + + @compile {:inline, "::": 2, ":::": 2} + def unquote(:"::")(expr, _type), do: expr + def unquote(:":::")(expr, _type), do: expr + end + end +end diff --git a/lib/mix/tasks/gradient.ex b/lib/mix/tasks/gradient.ex index 7ccf6a6d..850be6ee 100644 --- a/lib/mix/tasks/gradient.ex +++ b/lib/mix/tasks/gradient.ex @@ -7,7 +7,7 @@ defmodule Mix.Tasks.Gradient do Mix.Tasks.Compile.run([]) files = get_beams_paths() - #IO.puts("Found files:\n #{Enum.join(Enum.concat(Map.values(files)), "\n ")}") + # IO.puts("Found files:\n #{Enum.join(Enum.concat(Map.values(files)), "\n ")}") Application.ensure_all_started(:gradualizer) diff --git a/mix.exs b/mix.exs index 549e49e8..e60f214d 100644 --- a/mix.exs +++ b/mix.exs @@ -23,8 +23,8 @@ defmodule Gradient.MixProject do # Run "mix help deps" to learn about dependencies. def deps do [ - # {:gradualizer, path: "../Gradualizer", manager: :rebar3} {:gradualizer, github: "josefs/Gradualizer", ref: "master", manager: :rebar3}, + # {:gradualizer, path: "../Gradualizer/", manager: :rebar3}, {:dialyxir, "~> 1.0", only: [:dev], runtime: false} ] end diff --git a/mix.lock b/mix.lock index 4066e3c8..1055a1ce 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,5 @@ %{ - "astranaut": {:hex, :astranaut, "0.9.0", "76f94d98bb1dfadf3b53fbfa3e25d2738582aaf9607fb85a6d8b6e4b92adfbdc", [:rebar3], [], "hexpm", "cf88a1ae5702005bb005facd8702ab9fcca4645d1a06f5446171bf2bb9ca52bb"}, "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, - "gradualizer": {:git, "https://github.com/josefs/Gradualizer.git", "051040400924dfcfe59f7a8936beb34b43278191", [ref: "master"]}, + "gradualizer": {:git, "https://github.com/josefs/Gradualizer.git", "051040400924dfcfe59f7a8936beb34b43278191", [ref: "master"]} } diff --git a/test/examples/Elixir.Typespec.beam b/test/examples/Elixir.Typespec.beam new file mode 100644 index 0000000000000000000000000000000000000000..85aec1635f722de923c4c88fe10d8814d01fd1bf GIT binary patch literal 2244 zcmbVNdsq`!7N1E7gb-m!3YKEs06y5QB|JVAM7oJ2BJwZ-TD9P0OfE3wG2_exqO`yU zDMIxtSh1Umpgh8gk0RDuO4Zt0ZGEH^Y{g<(w9@+AU7&8)Zny58$bMya|7z#^esj+G z-E+^G`^`D`EM1WmgCOGfmT1)(YHn3Lf*_wj$OE)y(oQl_Ih$y}(!e0banhPk;kbn0 zIELI{;{fYppe!_L1`;En8DJndVDtlanqoK>w;1R9=G&|Wj-;%tw}mh;6f1qqA;c)=L7%(dvmf?+G&Z|Iw7+JX>bZpu z$}4!}lS}iW+!xcTBMGEdta#~ZO^!j&CcHI4bY6~CbwvZV7K$`qr;uzH8;jP z9p}cfBILSlq2@$GLUvhkFp_4Sw-!OtDFdI|3j$HY3WXxF(hT2dp}>d;i&?A3uwqCA zVRmS5{ZrE{X&UU#yuYrfC^^-d4?WL${Dr)~PcdLvSh*%dv@mKx)ItwO(Vk-F<9Hd7 z5G#eVG*M#3C%b^bX@)WYmNg>2`3z-&JOhkK0QBQExC&^3XZ#^U*fFJ67H`k*@7u{>M69 zs-8PpmWT#nGcV6whG)8Ob0x9+h65zMH?Wu^udjTVd1Cs=y^nLeYUkcsfBD?_SN5jk zn%~DPJ(5SLIxd%P2fO0@>o0CkQvUXyQ~z@6zJxhmHO<>%QtPCJ#>|Aezz1`l1ScZy zP9(@x>j%z{1Ps>KQtEr8`KzO{oo7v3uSl1gPut%Z4#AK7=!`k!jE`>$ThLB^UQMzF1X0>!T}v0kwS& z`tQ@?L(gGDf8IjRTI*IVi(huf0Cg>Af&NflXg&*GB zedu_(ZGnHQAXEBG*dqD5xA%5@`KRy&4N`OUl1RnICB-vxujkji8d2)hIVSE5-88-! zS7rNG%=>#{m~N$J)hU;4*E4OQyD#)!tigiLcGoT)YMXUFFQ4jqx!sZeRNuU9aDC4Xw;rp%c%zn)ZC& z;E2GyN2pug`Q7T#Z_tKM2i|#hpfjoDl5McZ`uPj(pX(b|2jtv#-aH)p!p8V-vO+q( z?LK)oB&@Z`tE5N!X64~6i;o9~wvGn-mZi+Of3*D(TCmviA%bWq0}Gqv6kmVnDSKm8 z{b&A;XsI?WN2k%A6f|%92_px$gbhh|OUnQ1st?@$kG`a9rKSDFK&4Q`K7aLo#Z0vM zi-TaAPep_4&@gLuYlf>9NESDLxf6Gv_x;1e+rMKjwJETcUAf@hUfp-JA-SpjOy&v^ z^X`EexBa@g>#h4{C4t1~pA6ydof}&08NyrNH$Lb~O$i%|euKztT`@WG_1WQ@rwPYpDM#RfRT8+>!g8awZr;ff~ zv;V)wC4v0%5BR_N`fGwrnaRKG5Jl;vm7m#>5dV4jnInQgWn;cFqW(1EH|56rH*bRq ABme*a literal 0 HcmV?d00001 diff --git a/test/examples/typespec.ex b/test/examples/typespec.ex new file mode 100644 index 00000000..d7aafd23 --- /dev/null +++ b/test/examples/typespec.ex @@ -0,0 +1,18 @@ +defmodule Typespec do + @type mylist(t) :: [t] + + @spec missing_type() :: Unknown.atom() + def missing_type, do: :ok + + @spec missing_type_arg() :: mylist(Unknown.atom()) + def missing_type_arg, do: [:ok] + + @spec named_type(name :: Unknown.atom()) :: atom() + def named_type(name), do: name + + @spec atoms_type(:ok | :error) :: :ok | :error + def atoms_type(name), do: name + + @spec atoms_type2(:ok | :error) :: Unknown.atom(:ok | :error) + def atoms_type2(name), do: name +end diff --git a/test/gradient/specify_erl_ast_test.exs b/test/gradient/specify_erl_ast_test.exs index f5867e92..16121615 100644 --- a/test/gradient/specify_erl_ast_test.exs +++ b/test/gradient/specify_erl_ast_test.exs @@ -1351,6 +1351,86 @@ defmodule Gradient.SpecifyErlAstTest do ]} = recv end + test "typespec" do + {tokens, ast} = load("/Elixir.Typespec.beam", "/typespec.ex") + + [atoms_type2, atoms_type, named_type, missing_type_arg, missing_type | _] = + SpecifyErlAst.add_missing_loc_literals(ast, tokens) + |> filter_specs() + |> Enum.reverse() + + assert {:attribute, 17, :spec, + {{:atoms_type2, 1}, + [ + {:type, 17, :fun, + [ + {:type, 17, :product, + [{:type, 17, :union, [{:atom, 17, :ok}, {:atom, 17, :error}]}]}, + {:remote_type, 17, + [ + {:atom, 17, Unknown}, + {:atom, 17, :atom}, + [{:type, 17, :union, [{:atom, 17, :ok}, {:atom, 17, :error}]}] + ]} + ]} + ]}} = atoms_type2 + + assert {:attribute, 14, :spec, + {{:atoms_type, 1}, + [ + {:type, 14, :fun, + [ + {:type, 14, :product, + [{:type, 14, :union, [{:atom, 14, :ok}, {:atom, 14, :error}]}]}, + {:type, 14, :union, [{:atom, 14, :ok}, {:atom, 14, :error}]} + ]} + ]}} = atoms_type + + assert {:attribute, 11, :spec, + {{:named_type, 1}, + [ + {:type, 11, :fun, + [ + {:type, 11, :product, + [ + {:ann_type, 11, + [ + {:var, 11, :name}, + {:remote_type, 11, [{:atom, 11, Unknown}, {:atom, 11, :atom}, []]} + ]} + ]}, + {:type, 11, :atom, []} + ]} + ]}} = named_type + + assert {:attribute, 8, :spec, + {{:missing_type_arg, 0}, + [ + {:type, 8, :fun, + [ + {:type, 8, :product, []}, + {:user_type, 8, :mylist, + [{:remote_type, 8, [{:atom, 8, Unknown}, {:atom, 8, :atom}, []]}]} + ]} + ]}} = missing_type_arg + + assert {:attribute, 5, :spec, + {{:missing_type, 0}, + [ + {:type, 5, :fun, + [ + {:type, 5, :product, []}, + {:remote_type, 5, [{:atom, 5, Unknown}, {:atom, 5, :atom}, []]} + ]} + ]}} = missing_type + end + + # Helpers + + def filter_specs(ast) do + Enum.filter(ast, &match?({:attribute, _, :spec, _}, &1)) + end + @spec load(String.t(), String.t()) :: {list(), list()} def load(beam_file, ex_file) do beam_file = String.to_charlist(@examples_path <> beam_file) From 07d6a2a413ce0a546c49e8c3f729a480dc8c8b2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Sat, 2 Oct 2021 01:27:31 +0200 Subject: [PATCH 02/15] Code refactoring, Add docs Included changes: - Make code cleaner and better organized. - Extract functions handling tokens to new Tokens module. - Extract types to new Types module. - Rename SpecifyErlAst to AstSpecifier module. - [test] Extract helper functions to new test helper module. - [test] Add test module for tokens and move tokens tests there. --- lib/gradient.ex | 8 +- lib/gradient/specify_erl_ast.ex | 317 +++--- lib/gradualizer_ex/ast_specifier.ex | 789 ++++++++++++++ lib/gradualizer_ex/tokens.ex | 198 ++++ lib/gradualizer_ex/types.ex | 11 + mix.exs | 5 + test/gradient/ast_specifier_test.exs | 1380 ++++++++++++++++++++++++ test/gradient/specify_erl_ast_test.exs | 195 +--- test/gradient/tokens_test.exs | 97 ++ test/support/helpers.ex | 80 ++ 10 files changed, 2747 insertions(+), 333 deletions(-) create mode 100644 lib/gradualizer_ex/ast_specifier.ex create mode 100644 lib/gradualizer_ex/tokens.ex create mode 100644 lib/gradualizer_ex/types.ex create mode 100644 test/gradient/ast_specifier_test.exs create mode 100644 test/gradient/tokens_test.exs create mode 100644 test/support/helpers.ex diff --git a/lib/gradient.ex b/lib/gradient.ex index 367dc619..ca0c2551 100644 --- a/lib/gradient.ex +++ b/lib/gradient.ex @@ -7,9 +7,9 @@ defmodule Gradient do - `code_path` - Path to a file with code (e.g. when beam was compiled without project). """ - alias Gradient.ElixirFileUtils - alias Gradient.ElixirFmt - alias Gradient.SpecifyErlAst + alias GradualizerEx.ElixirFileUtils + alias GradualizerEx.ElixirFmt + alias GradualizerEx.AstSpecifier require Logger @@ -23,7 +23,7 @@ defmodule Gradient do forms = forms |> put_code_path(opts) - |> SpecifyErlAst.specify() + |> AstSpecifier.specify() case :gradualizer.type_check_forms(forms, opts) do [] -> diff --git a/lib/gradient/specify_erl_ast.ex b/lib/gradient/specify_erl_ast.ex index ea79ca6a..58f82dfd 100644 --- a/lib/gradient/specify_erl_ast.ex +++ b/lib/gradient/specify_erl_ast.ex @@ -1,13 +1,19 @@ +<<<<<<< HEAD:lib/gradient/specify_erl_ast.ex defmodule Gradient.SpecifyErlAst do +======= +defmodule GradualizerEx.AstSpecifier do +>>>>>>> 9a63ccd (Code refactoring, Add docs):lib/gradualizer_ex/ast_specifier.ex @moduledoc """ - Module adds missing line information to the Erlang abstract code produced - from Elixir AST. + Module adds missing location information to the Erlang abstract code produced + from Elixir AST. Moreover it can be used to catch some ast pattern and replace + it to forms that cannot be produced from Elixir directly. FIXME Optimize tokens searching. Find out why some tokens are dropped NOTE Mapper implements: - function [x] - fun [x] + - fun @spec [x] - clause [x] - case [x] - block [X] @@ -42,30 +48,40 @@ defmodule Gradient.SpecifyErlAst do - guards [X] """ +<<<<<<< HEAD:lib/gradient/specify_erl_ast.ex import Gradient.Utils +======= + import GradualizerEx.Tokens +>>>>>>> 9a63ccd (Code refactoring, Add docs):lib/gradualizer_ex/ast_specifier.ex require Logger - @type token :: tuple() - @type tokens :: [tuple()] - @type form :: - :erl_parse.abstract_clause() - | :erl_parse.abstract_expr() - | :erl_parse.abstract_form() - | :erl_parse.abstract_type() - @type forms :: [form()] - @type options :: keyword() + alias GradualizerEx.Types - @doc """ + @type token :: Types.token() + @type tokens :: Types.tokens() + @type form :: Types.form() + @type forms :: Types.forms() + @type options :: Types.options() + + # Api + @doc """ + Read and tokenize code file. Than run mappers on the given AST (with obtained tokens) + to specify missing locations or replace some parts that match pattern. """ @spec specify(nonempty_list(:erl_parse.abstract_form())) :: [:erl_parse.abstract_form()] def specify(forms) do with {:attribute, line, :file, {path, _}} <- hd(forms), path <- to_string(path), {:ok, code} <- File.read(path), +<<<<<<< HEAD:lib/gradient/specify_erl_ast.ex {:ok, tokens} <- :elixir.string_to_tokens(String.to_charlist(code), line, line, path, []) do add_missing_loc_literals(forms, tokens) +======= + {:ok, tokens} <- :elixir.string_to_tokens(String.to_charlist(code), 1, 1, path, []) do + run_mappers(forms, tokens) +>>>>>>> 9a63ccd (Code refactoring, Add docs):lib/gradualizer_ex/ast_specifier.ex else error -> IO.puts("Error occured when specifying forms : #{inspect(error)}") @@ -74,14 +90,14 @@ defmodule Gradient.SpecifyErlAst do end @doc """ - Function takes forms and traverse them to add missing location for literals. - Firstly the parent location is set, then it is matched - with tokens to precise the literal line. + Function takes forms and traverse them in order to specify location or modify + forms matching the pattern. The tokens are required to obtain the missing location + as precise as possible. """ - @spec add_missing_loc_literals([:erl_parse.abstract_form()], tokens()) :: [ + @spec run_mappers([:erl_parse.abstract_form()], tokens()) :: [ :erl_parse.abstract_form() ] - def add_missing_loc_literals(forms, tokens) do + def run_mappers(forms, tokens) do opts = [end_line: -1] {forms, _} = @@ -92,6 +108,8 @@ defmodule Gradient.SpecifyErlAst do forms end + # Mappers + @doc """ Map over the forms using mapper and attach a context i.e. end line. """ @@ -106,7 +124,7 @@ defmodule Gradient.SpecifyErlAst do end @doc """ - Fold over the forms using mapper and attach a context i.e. end line. + Fold over the forms using mapper and attach a context i.e. end line. """ @spec context_mapper_fold(forms(), tokens(), options()) :: {forms(), tokens()} def context_mapper_fold(forms, tokens, opts, mapper \\ &mapper/3) @@ -119,58 +137,26 @@ defmodule Gradient.SpecifyErlAst do {[form | forms], res_tokens} end - def set_form_end_line(opts, form, forms) do - case Enum.find(forms, fn f -> - anno = elem(f, 1) - - # Maybe should try to go deeper when generated and try to obtain - # the line from the first child. It should work for sure for clauses, - # but it has to be in the right order (e.g. if clauses are reversed) - :erl_anno.line(anno) > 0 and not :erl_anno.generated(anno) - end) do - nil -> - opts - - next_form -> - current_line = :erl_anno.line(elem(form, 1)) - next_line = :erl_anno.line(elem(next_form, 1)) - - if current_line == next_line do - Keyword.put(opts, :end_line, next_line + 1) - else - Keyword.put(opts, :end_line, next_line) - end - end - end - - def prepare_forms_order(forms) do - forms - |> Enum.sort(fn l, r -> elem(l, 0) == elem(r, 0) and elem(l, 1) > elem(r, 1) end) - |> Enum.reverse() - end - - @spec pass_tokens(any(), tokens()) :: {any(), tokens()} - defp pass_tokens(form, tokens) do - {form, tokens} - end - + @doc """ + The main mapper function traverses AST and specifies missing locations + or replaces parts that match the pattern. + """ @spec mapper(form(), [token()], options()) :: {form(), [token()]} - defp mapper(form, tokens, opts) + def mapper(form, tokens, opts) - defp mapper({:attribute, anno, :spec, {name_arity, specs}}, tokens, opts) do - # + def mapper({:attribute, anno, :spec, {name_arity, specs}}, tokens, opts) do new_specs = context_mapper_map(specs, [], opts, &spec_mapper/3) {:attribute, anno, :spec, {name_arity, new_specs}} |> pass_tokens(tokens) end - defp mapper({:function, _line, :__info__, _arity, _children} = form, tokens, _opts) do + def mapper({:function, _line, :__info__, _arity, _children} = form, tokens, _opts) do # skip analysis for __info__ functions pass_tokens(form, tokens) end - defp mapper({:function, anno, name, arity, clauses}, tokens, opts) do + def mapper({:function, anno, name, arity, clauses}, tokens, opts) do # anno has line {clauses, tokens} = context_mapper_fold(clauses, tokens, opts) @@ -178,7 +164,7 @@ defmodule Gradient.SpecifyErlAst do |> pass_tokens(tokens) end - defp mapper({:fun, anno, {:clauses, clauses}}, tokens, opts) do + def mapper({:fun, anno, {:clauses, clauses}}, tokens, opts) do # anno has line {clauses, tokens} = context_mapper_fold(clauses, tokens, opts) @@ -186,7 +172,7 @@ defmodule Gradient.SpecifyErlAst do |> pass_tokens(tokens) end - defp mapper({:case, anno, condition, clauses}, tokens, opts) do + def mapper({:case, anno, condition, clauses}, tokens, opts) do # anno has line # NOTE In Elixir `if`, `case` and `cond` statements are represented # as a `case` in abstract code. @@ -213,7 +199,7 @@ defmodule Gradient.SpecifyErlAst do |> pass_tokens(tokens) end - defp mapper({:clause, anno, args, guards, children}, tokens, opts) do + def mapper({:clause, anno, args, guards, children}, tokens, opts) do # anno has line # FIXME Handle generated clauses. Right now the literals inherit lines # from the parents without checking them with tokens @@ -245,7 +231,7 @@ defmodule Gradient.SpecifyErlAst do end end - defp mapper({:block, anno, body}, tokens, opts) do + def mapper({:block, anno, body}, tokens, opts) do # TODO check if anno has line {:ok, _line, anno, opts, _} = get_line(anno, opts) @@ -255,7 +241,7 @@ defmodule Gradient.SpecifyErlAst do |> pass_tokens(tokens) end - defp mapper({:match, anno, left, right}, tokens, opts) do + def mapper({:match, anno, left, right}, tokens, opts) do {:ok, _, anno, opts, _} = get_line(anno, opts) {left, tokens} = mapper(left, tokens, opts) @@ -265,35 +251,35 @@ defmodule Gradient.SpecifyErlAst do |> pass_tokens(tokens) end - defp mapper({:map, anno, pairs}, tokens, opts) do + def mapper({:map, anno, pairs}, tokens, opts) do # anno has correct line {:ok, _, anno, opts, _} = get_line(anno, opts) - {pairs, tokens} = context_mapper_fold(pairs, tokens, opts, &map_element/3) + {pairs, tokens} = context_mapper_fold(pairs, tokens, opts, &map_element_mapper/3) {:map, anno, pairs} |> pass_tokens(tokens) end # update map pattern - defp mapper({:map, anno, map, pairs}, tokens, opts) do + def mapper({:map, anno, map, pairs}, tokens, opts) do # anno has correct line {:ok, _, anno, opts, _} = get_line(anno, opts) {map, tokens} = mapper(map, tokens, opts) - {pairs, tokens} = context_mapper_fold(pairs, tokens, opts, &map_element/3) + {pairs, tokens} = context_mapper_fold(pairs, tokens, opts, &map_element_mapper/3) {:map, anno, map, pairs} |> pass_tokens(tokens) end - defp mapper({:cons, anno, value, more} = cons, tokens, opts) do + def mapper({:cons, anno, value, more} = cons, tokens, opts) do # anno could be 0 {:ok, line, anno, opts, _} = get_line(anno, opts) tokens = drop_tokens_to_line(tokens, line) - case get_list_from_tokens(tokens, opts) do + case get_list(tokens, opts) do {:list, tokens} -> cons_mapper(cons, tokens, opts) @@ -311,13 +297,13 @@ defmodule Gradient.SpecifyErlAst do end end - defp mapper({:tuple, anno, elements}, tokens, opts) do + def mapper({:tuple, anno, elements}, tokens, opts) do # anno could be 0 {:ok, line, anno, opts, has_line?} = get_line(anno, opts) tokens |> drop_tokens_to_line(line) - |> get_tuple_from_tokens(opts) + |> get_tuple(opts) |> case do {:tuple, tokens} -> {anno, opts} = update_line_from_tokens(tokens, anno, opts, has_line?) @@ -335,7 +321,7 @@ defmodule Gradient.SpecifyErlAst do end end - defp mapper({:receive, anno, clauses}, tokens, opts) do + def mapper({:receive, anno, clauses}, tokens, opts) do # anno has correct line {:ok, _, anno, opts, _} = get_line(anno, opts) @@ -346,7 +332,7 @@ defmodule Gradient.SpecifyErlAst do end # receive with timeout - defp mapper({:receive, anno, clauses, after_val, after_block}, tokens, opts) do + def mapper({:receive, anno, clauses, after_val, after_block}, tokens, opts) do # anno has correct line {:ok, _, anno, opts, _} = get_line(anno, opts) @@ -358,7 +344,7 @@ defmodule Gradient.SpecifyErlAst do |> pass_tokens(tokens) end - defp mapper({:try, anno, body, else_block, catchers, after_block}, tokens, opts) do + def mapper({:try, anno, body, else_block, catchers, after_block}, tokens, opts) do # anno has correct line {:ok, _, anno, opts, _} = get_line(anno, opts) @@ -374,19 +360,19 @@ defmodule Gradient.SpecifyErlAst do |> pass_tokens(tokens) end - defp mapper( - {:call, anno, {:atom, _, name_atom} = name, - [expr, {:bin, _, [{:bin_element, _, {:string, _, _} = val, :default, :default}]}]}, - tokens, - _opts - ) - when name_atom in [:"::", :":::"] do + def mapper( + {:call, anno, {:atom, _, name_atom} = name, + [expr, {:bin, _, [{:bin_element, _, {:string, _, _} = val, :default, :default}]}]}, + tokens, + _opts + ) + when name_atom in [:"::", :":::"] do # unwrap string from binary for correct type annotation matching {:call, anno, name, [expr, val]} |> pass_tokens(tokens) end - defp mapper({:call, anno, name, args}, tokens, opts) do + def mapper({:call, anno, name, args}, tokens, opts) do # anno has correct line {:ok, _, anno, opts, _} = get_line(anno, opts) @@ -398,7 +384,7 @@ defmodule Gradient.SpecifyErlAst do |> pass_tokens(tokens) end - defp mapper({:op, anno, op, left, right}, tokens, opts) do + def mapper({:op, anno, op, left, right}, tokens, opts) do # anno has correct line {:ok, _, anno, opts, _} = get_line(anno, opts) @@ -409,7 +395,7 @@ defmodule Gradient.SpecifyErlAst do |> pass_tokens(tokens) end - defp mapper({:op, anno, op, right}, tokens, opts) do + def mapper({:op, anno, op, right}, tokens, opts) do # anno has correct line {:ok, _, anno, opts, _} = get_line(anno, opts) @@ -419,7 +405,7 @@ defmodule Gradient.SpecifyErlAst do |> pass_tokens(tokens) end - defp mapper({:bin, anno, elements}, tokens, opts) do + def mapper({:bin, anno, elements}, tokens, opts) do # anno could be 0 {:ok, line, anno, opts, _} = get_line(anno, opts) @@ -432,15 +418,15 @@ defmodule Gradient.SpecifyErlAst do _ -> {bin_tokens, other_tokens} = cut_tokens_to_bin(tokens, line) bin_tokens = flat_tokens(bin_tokens) - {elements, _} = context_mapper_fold(elements, bin_tokens, opts, &bin_element/3) + {elements, _} = context_mapper_fold(elements, bin_tokens, opts, &bin_element_mapper/3) {:bin, anno, elements} |> pass_tokens(other_tokens) end end - defp mapper({type, 0, value}, tokens, opts) - when type in [:atom, :char, :float, :integer, :string, :bin] do + def mapper({type, 0, value}, tokens, opts) + when type in [:atom, :char, :float, :integer, :string, :bin] do # TODO check what happend for :string {:ok, line} = Keyword.fetch(opts, :line) @@ -448,31 +434,31 @@ defmodule Gradient.SpecifyErlAst do |> specify_line(tokens, opts) end - defp mapper(skip, tokens, _opts) - when elem(skip, 0) in [ - :fun, - :attribute, - :var, - nil, - :atom, - :char, - :float, - :integer, - :string, - :bin - ] do + def mapper(skip, tokens, _opts) + when elem(skip, 0) in [ + :fun, + :attribute, + :var, + nil, + :atom, + :char, + :float, + :integer, + :string, + :bin + ] do # NOTE fun - I skipped here checking &name/arity or &module.name/arity # skip forms that don't need analysis and do not display warning pass_tokens(skip, tokens) end - defp mapper(form, tokens, _opts) do + def mapper(form, tokens, _opts) do Logger.warn("Not found mapper for #{inspect(form)}") pass_tokens(form, tokens) end @doc """ - Adds missing line to the function specification. + Adds missing location to the function specification. """ @spec spec_mapper(form(), tokens(), options()) :: {form(), tokens()} def spec_mapper({:type, anno, type_name, args}, tokens, opts) do @@ -512,7 +498,7 @@ defmodule Gradient.SpecifyErlAst do end @doc """ - Adds missing line to the module literal + Adds missing location to the module literal """ def remote_mapper({:remote, line, {:atom, 0, mod}, fun}) do {:remote, line, {:atom, line, mod}, fun} @@ -538,7 +524,11 @@ defmodule Gradient.SpecifyErlAst do end) end - def map_element({field, anno, key, value}, tokens, opts) + @doc """ + Run mapper on map value and key. + """ + @spec map_element_mapper(tuple(), tokens(), options()) :: {tuple(), tokens()} + def map_element_mapper({field, anno, key, value}, tokens, opts) when field in [:map_field_assoc, :map_field_exact] do line = :erl_anno.line(anno) opts = Keyword.put(opts, :line, line) @@ -550,7 +540,11 @@ defmodule Gradient.SpecifyErlAst do |> pass_tokens(tokens) end - def bin_element({:bin_element, anno, value, size, tsl}, tokens, opts) do + @doc """ + Run mapper on bin element value. + """ + @spec bin_element_mapper(tuple(), tokens(), options()) :: {tuple(), tokens()} + def bin_element_mapper({:bin_element, anno, value, size, tsl}, tokens, opts) do {:ok, _line, anno, opts, _} = get_line(anno, opts) {value, tokens} = mapper(value, tokens, opts) @@ -563,7 +557,6 @@ defmodule Gradient.SpecifyErlAst do Iterate over the list in abstract code format and runs mapper on each element """ @spec cons_mapper(form(), [token()], options()) :: {form(), tokens()} - def cons_mapper({:cons, anno, value, tail}, tokens, opts) do {:ok, _, anno, opts, has_line?} = get_line(anno, opts) @@ -580,77 +573,12 @@ defmodule Gradient.SpecifyErlAst do def cons_mapper(other, tokens, opts), do: mapper(other, tokens, opts) @doc """ - Drop tokens to the first conditional occurance. Returns type of the encountered conditional and following tokens. + Update form anno with location taken from the corresponding token, if found. + Otherwise return form unchanged. """ - @spec get_conditional([token()], integer(), options()) :: - {:case, [token()]} - | {:cond, [token()]} - | {:unless, [token()]} - | {:if, [token()]} - | {:with, [token()]} - | :undefined - def get_conditional(tokens, line, opts) do - conditionals = [:if, :unless, :cond, :case, :with] - {:ok, limit_line} = Keyword.fetch(opts, :end_line) - - drop_tokens_while(tokens, limit_line, fn - {:do_identifier, _, c} -> c not in conditionals - {:paren_identifier, _, c} -> c not in conditionals - {:identifier, _, c} -> c not in conditionals - _ -> true - end) - |> case do - [token | _] = tokens when elem(elem(token, 1), 0) == line -> {elem(token, 2), tokens} - _ -> :undefined - end - end - - @spec get_list_from_tokens([token()], options()) :: - {:list, [token()]} | {:keyword, [token()]} | {:charlist, [token()]} | :undefined - def get_list_from_tokens(tokens, opts) do - tokens = flat_tokens(tokens) - {:ok, limit_line} = Keyword.fetch(opts, :end_line) - - res = - drop_tokens_while(tokens, limit_line, fn - {:"[", _} -> false - {:list_string, _, _} -> false - {:kw_identifier, _, id} when id not in [:do] -> false - _ -> true - end) - - case res do - [{:"[", _} | _] = list -> {:list, list} - [{:list_string, _, _} | _] = list -> {:charlist, list} - [{:kw_identifier, _, _} | _] = list -> {:keyword, list} - _ -> :undefined - end - end - - @spec get_tuple_from_tokens(tokens, options()) :: - {:tuple, tokens()} | :undefined - def get_tuple_from_tokens(tokens, opts) do - {:ok, limit_line} = Keyword.fetch(opts, :end_line) - - res = - drop_tokens_while(tokens, limit_line, fn - {:"{", _} -> false - {:kw_identifier, _, _} -> false - _ -> true - end) - - case res do - [{:"{", _} | _] = tuple -> {:tuple, tuple} - [{:kw_identifier, _, _} | _] = tuple -> {:tuple, tuple} - _ -> :undefined - end - end - @spec specify_line(form(), [token()], options()) :: {form(), [token()]} - # def specify_line(form, []), do: raise("ehh -- #{inspect form}") def specify_line(form, tokens, opts) do if not :erl_anno.generated(elem(form, 1)) do - # Logger.debug("#{inspect(form)} --- #{inspect(tokens, limit: :infinity)}") {:ok, end_line} = Keyword.fetch(opts, :end_line) res = drop_tokens_while(tokens, end_line, &(!match_token_to_form(&1, form))) @@ -660,7 +588,6 @@ defmodule Gradient.SpecifyErlAst do {take_loc_from_token(token, form), tokens} [] -> - # Logger.info("Not found - #{inspect(form)}") {form, tokens} end else @@ -668,6 +595,8 @@ defmodule Gradient.SpecifyErlAst do end end + # Private Helpers + @spec match_token_to_form(token(), form()) :: boolean() defp match_token_to_form({:int, {l1, _, v1}, _}, {:integer, l2, v2}) do l2 = :erl_anno.line(l2) @@ -804,7 +733,7 @@ defmodule Gradient.SpecifyErlAst do {anno, opts} end - def get_line(anno, opts) do + defp get_line(anno, opts) do case :erl_anno.line(anno) do 0 -> case Keyword.fetch(opts, :line) do @@ -821,4 +750,40 @@ defmodule Gradient.SpecifyErlAst do {:ok, line, anno, opts, true} end end + + @spec prepare_forms_order(forms()) :: forms() + defp prepare_forms_order(forms) do + forms + |> Enum.sort(fn l, r -> elem(l, 0) == elem(r, 0) and elem(l, 1) > elem(r, 1) end) + |> Enum.reverse() + end + + defp set_form_end_line(opts, form, forms) do + case Enum.find(forms, fn f -> + anno = elem(f, 1) + + # Maybe should try to go deeper when generated and try to obtain + # the line from the first child. It should work for sure for clauses, + # but it has to be in the right order (e.g. if clauses are reversed) + :erl_anno.line(anno) > 0 and not :erl_anno.generated(anno) + end) do + nil -> + opts + + next_form -> + current_line = :erl_anno.line(elem(form, 1)) + next_line = :erl_anno.line(elem(next_form, 1)) + + if current_line == next_line do + Keyword.put(opts, :end_line, next_line + 1) + else + Keyword.put(opts, :end_line, next_line) + end + end + end + + @spec pass_tokens(any(), tokens()) :: {any(), tokens()} + defp pass_tokens(form, tokens) do + {form, tokens} + end end diff --git a/lib/gradualizer_ex/ast_specifier.ex b/lib/gradualizer_ex/ast_specifier.ex new file mode 100644 index 00000000..58f82dfd --- /dev/null +++ b/lib/gradualizer_ex/ast_specifier.ex @@ -0,0 +1,789 @@ +<<<<<<< HEAD:lib/gradient/specify_erl_ast.ex +defmodule Gradient.SpecifyErlAst do +======= +defmodule GradualizerEx.AstSpecifier do +>>>>>>> 9a63ccd (Code refactoring, Add docs):lib/gradualizer_ex/ast_specifier.ex + @moduledoc """ + Module adds missing location information to the Erlang abstract code produced + from Elixir AST. Moreover it can be used to catch some ast pattern and replace + it to forms that cannot be produced from Elixir directly. + + FIXME Optimize tokens searching. Find out why some tokens are dropped + + NOTE Mapper implements: + - function [x] + - fun [x] + - fun @spec [x] + - clause [x] + - case [x] + - block [X] + - pipe [x] + - call [x] (remote [X]) + - match [x] + - op [x] + - integer [x] + - float [x] + - string [x] + - charlist [x] + - tuple [X] + - var [X] + - list [X] + - keyword [X] + - binary [X] + - map [X] + - try [x] + - receive [X] + - record [X] elixir don't use it record_field, record_index, record_pattern, record + - named_fun [ ] is named_fun used by elixir? + + NOTE Elixir expressions to handle or test: + - list comprehension [X] + - binary [X] + - maps [X] + - struct [X] + - pipe [ ] TODO decide how to search for line in reversed form order + - range [X] + - receive [X] + - record [X] + - guards [X] + """ + +<<<<<<< HEAD:lib/gradient/specify_erl_ast.ex + import Gradient.Utils +======= + import GradualizerEx.Tokens +>>>>>>> 9a63ccd (Code refactoring, Add docs):lib/gradualizer_ex/ast_specifier.ex + + require Logger + + alias GradualizerEx.Types + + @type token :: Types.token() + @type tokens :: Types.tokens() + @type form :: Types.form() + @type forms :: Types.forms() + @type options :: Types.options() + + # Api + + @doc """ + Read and tokenize code file. Than run mappers on the given AST (with obtained tokens) + to specify missing locations or replace some parts that match pattern. + """ + @spec specify(nonempty_list(:erl_parse.abstract_form())) :: [:erl_parse.abstract_form()] + def specify(forms) do + with {:attribute, line, :file, {path, _}} <- hd(forms), + path <- to_string(path), + {:ok, code} <- File.read(path), +<<<<<<< HEAD:lib/gradient/specify_erl_ast.ex + {:ok, tokens} <- :elixir.string_to_tokens(String.to_charlist(code), line, line, path, []) do + add_missing_loc_literals(forms, tokens) +======= + {:ok, tokens} <- :elixir.string_to_tokens(String.to_charlist(code), 1, 1, path, []) do + run_mappers(forms, tokens) +>>>>>>> 9a63ccd (Code refactoring, Add docs):lib/gradualizer_ex/ast_specifier.ex + else + error -> + IO.puts("Error occured when specifying forms : #{inspect(error)}") + forms + end + end + + @doc """ + Function takes forms and traverse them in order to specify location or modify + forms matching the pattern. The tokens are required to obtain the missing location + as precise as possible. + """ + @spec run_mappers([:erl_parse.abstract_form()], tokens()) :: [ + :erl_parse.abstract_form() + ] + def run_mappers(forms, tokens) do + opts = [end_line: -1] + + {forms, _} = + forms + |> prepare_forms_order() + |> context_mapper_fold(tokens, opts) + + forms + end + + # Mappers + + @doc """ + Map over the forms using mapper and attach a context i.e. end line. + """ + @spec context_mapper_map(forms(), tokens(), options()) :: forms() + def context_mapper_map(forms, tokens, opts, mapper \\ &mapper/3) + def context_mapper_map([], _, _, _), do: [] + + def context_mapper_map([form | forms], tokens, opts, mapper) do + cur_opts = set_form_end_line(opts, form, forms) + {form, _} = mapper.(form, tokens, cur_opts) + [form | context_mapper_map(forms, tokens, opts, mapper)] + end + + @doc """ + Fold over the forms using mapper and attach a context i.e. end line. + """ + @spec context_mapper_fold(forms(), tokens(), options()) :: {forms(), tokens()} + def context_mapper_fold(forms, tokens, opts, mapper \\ &mapper/3) + def context_mapper_fold([], tokens, _, _), do: {[], tokens} + + def context_mapper_fold([form | forms], tokens, opts, mapper) do + cur_opts = set_form_end_line(opts, form, forms) + {form, new_tokens} = mapper.(form, tokens, cur_opts) + {forms, res_tokens} = context_mapper_fold(forms, new_tokens, opts, mapper) + {[form | forms], res_tokens} + end + + @doc """ + The main mapper function traverses AST and specifies missing locations + or replaces parts that match the pattern. + """ + @spec mapper(form(), [token()], options()) :: {form(), [token()]} + def mapper(form, tokens, opts) + + def mapper({:attribute, anno, :spec, {name_arity, specs}}, tokens, opts) do + new_specs = context_mapper_map(specs, [], opts, &spec_mapper/3) + + {:attribute, anno, :spec, {name_arity, new_specs}} + |> pass_tokens(tokens) + end + + def mapper({:function, _line, :__info__, _arity, _children} = form, tokens, _opts) do + # skip analysis for __info__ functions + pass_tokens(form, tokens) + end + + def mapper({:function, anno, name, arity, clauses}, tokens, opts) do + # anno has line + {clauses, tokens} = context_mapper_fold(clauses, tokens, opts) + + {:function, anno, name, arity, clauses} + |> pass_tokens(tokens) + end + + def mapper({:fun, anno, {:clauses, clauses}}, tokens, opts) do + # anno has line + {clauses, tokens} = context_mapper_fold(clauses, tokens, opts) + + {:fun, anno, {:clauses, clauses}} + |> pass_tokens(tokens) + end + + def mapper({:case, anno, condition, clauses}, tokens, opts) do + # anno has line + # NOTE In Elixir `if`, `case` and `cond` statements are represented + # as a `case` in abstract code. + {:ok, line, anno, opts, _} = get_line(anno, opts) + + opts = + case get_conditional(tokens, line, opts) do + {type, _} when type in [:case, :with] -> + Keyword.put(opts, :case_type, :case) + + {type, _} when type in [:cond, :if, :unless] -> + Keyword.put(opts, :case_type, :gen) + + :undefined -> + Keyword.put(opts, :case_type, :gen) + end + + {new_condition, tokens} = mapper(condition, tokens, opts) + + # NOTE use map because generated clauses can be in wrong order + clauses = context_mapper_map(clauses, tokens, opts) + + {:case, anno, new_condition, clauses} + |> pass_tokens(tokens) + end + + def mapper({:clause, anno, args, guards, children}, tokens, opts) do + # anno has line + # FIXME Handle generated clauses. Right now the literals inherit lines + # from the parents without checking them with tokens + + {:ok, line, anno, opts, _} = get_line(anno, opts) + case_type = Keyword.get(opts, :case_type, :case) + + tokens = drop_tokens_to_line(tokens, line) + + if case_type == :case do + {guards, tokens} = guards_mapper(guards, tokens, opts) + + {args, tokens} = + if not :erl_anno.generated(anno) do + context_mapper_fold(args, tokens, opts) + else + {args, tokens} + end + + {children, tokens} = children |> context_mapper_fold(tokens, opts) + + {:clause, anno, args, guards, children} + |> pass_tokens(tokens) + else + {children, tokens} = children |> context_mapper_fold(tokens, opts) + + {:clause, anno, args, guards, children} + |> pass_tokens(tokens) + end + end + + def mapper({:block, anno, body}, tokens, opts) do + # TODO check if anno has line + {:ok, _line, anno, opts, _} = get_line(anno, opts) + + {body, tokens} = context_mapper_fold(body, tokens, opts) + + {:block, anno, body} + |> pass_tokens(tokens) + end + + def mapper({:match, anno, left, right}, tokens, opts) do + {:ok, _, anno, opts, _} = get_line(anno, opts) + + {left, tokens} = mapper(left, tokens, opts) + {right, tokens} = mapper(right, tokens, opts) + + {:match, anno, left, right} + |> pass_tokens(tokens) + end + + def mapper({:map, anno, pairs}, tokens, opts) do + # anno has correct line + {:ok, _, anno, opts, _} = get_line(anno, opts) + + {pairs, tokens} = context_mapper_fold(pairs, tokens, opts, &map_element_mapper/3) + + {:map, anno, pairs} + |> pass_tokens(tokens) + end + + # update map pattern + def mapper({:map, anno, map, pairs}, tokens, opts) do + # anno has correct line + {:ok, _, anno, opts, _} = get_line(anno, opts) + + {map, tokens} = mapper(map, tokens, opts) + {pairs, tokens} = context_mapper_fold(pairs, tokens, opts, &map_element_mapper/3) + + {:map, anno, map, pairs} + |> pass_tokens(tokens) + end + + def mapper({:cons, anno, value, more} = cons, tokens, opts) do + # anno could be 0 + {:ok, line, anno, opts, _} = get_line(anno, opts) + + tokens = drop_tokens_to_line(tokens, line) + + case get_list(tokens, opts) do + {:list, tokens} -> + cons_mapper(cons, tokens, opts) + + {:keyword, tokens} -> + cons_mapper(cons, tokens, opts) + + {:charlist, tokens} -> + {:cons, anno, value, more} + |> specify_line(tokens, opts) + + :undefined -> + {form, _} = cons_mapper(cons, [], opts) + + pass_tokens(form, tokens) + end + end + + def mapper({:tuple, anno, elements}, tokens, opts) do + # anno could be 0 + {:ok, line, anno, opts, has_line?} = get_line(anno, opts) + + tokens + |> drop_tokens_to_line(line) + |> get_tuple(opts) + |> case do + {:tuple, tokens} -> + {anno, opts} = update_line_from_tokens(tokens, anno, opts, has_line?) + + {elements, tokens} = context_mapper_fold(elements, tokens, opts) + + {:tuple, anno, elements} + |> pass_tokens(tokens) + + :undefined -> + elements = context_mapper_map(elements, [], opts) + + {:tuple, anno, elements} + |> pass_tokens(tokens) + end + end + + def mapper({:receive, anno, clauses}, tokens, opts) do + # anno has correct line + {:ok, _, anno, opts, _} = get_line(anno, opts) + + {clauses, tokens} = context_mapper_fold(clauses, tokens, opts) + + {:receive, anno, clauses} + |> pass_tokens(tokens) + end + + # receive with timeout + def mapper({:receive, anno, clauses, after_val, after_block}, tokens, opts) do + # anno has correct line + {:ok, _, anno, opts, _} = get_line(anno, opts) + + {clauses, tokens} = context_mapper_fold(clauses, tokens, opts) + {after_val, tokens} = mapper(after_val, tokens, opts) + {after_block, tokens} = context_mapper_fold(after_block, tokens, opts) + + {:receive, anno, clauses, after_val, after_block} + |> pass_tokens(tokens) + end + + def mapper({:try, anno, body, else_block, catchers, after_block}, tokens, opts) do + # anno has correct line + {:ok, _, anno, opts, _} = get_line(anno, opts) + + {body, tokens} = context_mapper_fold(body, tokens, opts) + + {catchers, tokens} = context_mapper_fold(catchers, tokens, opts) + + {else_block, tokens} = context_mapper_fold(else_block, tokens, opts) + + {after_block, tokens} = context_mapper_fold(after_block, tokens, opts) + + {:try, anno, body, else_block, catchers, after_block} + |> pass_tokens(tokens) + end + + def mapper( + {:call, anno, {:atom, _, name_atom} = name, + [expr, {:bin, _, [{:bin_element, _, {:string, _, _} = val, :default, :default}]}]}, + tokens, + _opts + ) + when name_atom in [:"::", :":::"] do + # unwrap string from binary for correct type annotation matching + {:call, anno, name, [expr, val]} + |> pass_tokens(tokens) + end + + def mapper({:call, anno, name, args}, tokens, opts) do + # anno has correct line + {:ok, _, anno, opts, _} = get_line(anno, opts) + + name = remote_mapper(name) + + {args, tokens} = context_mapper_fold(args, tokens, opts) + + {:call, anno, name, args} + |> pass_tokens(tokens) + end + + def mapper({:op, anno, op, left, right}, tokens, opts) do + # anno has correct line + {:ok, _, anno, opts, _} = get_line(anno, opts) + + {left, tokens} = mapper(left, tokens, opts) + {right, tokens} = mapper(right, tokens, opts) + + {:op, anno, op, left, right} + |> pass_tokens(tokens) + end + + def mapper({:op, anno, op, right}, tokens, opts) do + # anno has correct line + {:ok, _, anno, opts, _} = get_line(anno, opts) + + {right, tokens} = mapper(right, tokens, opts) + + {:op, anno, op, right} + |> pass_tokens(tokens) + end + + def mapper({:bin, anno, elements}, tokens, opts) do + # anno could be 0 + {:ok, line, anno, opts, _} = get_line(anno, opts) + + # TODO find a way to merge this cases + case elements do + [{:bin_element, _, {:string, _, _}, :default, :default}] = e -> + {:bin, anno, e} + |> specify_line(tokens, opts) + + _ -> + {bin_tokens, other_tokens} = cut_tokens_to_bin(tokens, line) + bin_tokens = flat_tokens(bin_tokens) + {elements, _} = context_mapper_fold(elements, bin_tokens, opts, &bin_element_mapper/3) + + {:bin, anno, elements} + |> pass_tokens(other_tokens) + end + end + + def mapper({type, 0, value}, tokens, opts) + when type in [:atom, :char, :float, :integer, :string, :bin] do + # TODO check what happend for :string + {:ok, line} = Keyword.fetch(opts, :line) + + {type, line, value} + |> specify_line(tokens, opts) + end + + def mapper(skip, tokens, _opts) + when elem(skip, 0) in [ + :fun, + :attribute, + :var, + nil, + :atom, + :char, + :float, + :integer, + :string, + :bin + ] do + # NOTE fun - I skipped here checking &name/arity or &module.name/arity + # skip forms that don't need analysis and do not display warning + pass_tokens(skip, tokens) + end + + def mapper(form, tokens, _opts) do + Logger.warn("Not found mapper for #{inspect(form)}") + pass_tokens(form, tokens) + end + + @doc """ + Adds missing location to the function specification. + """ + @spec spec_mapper(form(), tokens(), options()) :: {form(), tokens()} + def spec_mapper({:type, anno, type_name, args}, tokens, opts) do + {:ok, _line, anno, opts, _} = get_line(anno, opts) + new_args = context_mapper_map(args, tokens, opts, &spec_mapper/3) + + {:type, anno, type_name, new_args} + |> pass_tokens(tokens) + end + + def spec_mapper({:remote_type, anno, [mod, type, args]}, tokens, opts) do + {:ok, _line, anno, opts, _} = get_line(anno, opts) + {new_mod, _} = spec_mapper(mod, tokens, opts) + {new_type, _} = spec_mapper(type, tokens, opts) + new_args = context_mapper_map(args, tokens, opts, &spec_mapper/3) + + {:remote_type, anno, [new_mod, new_type, new_args]} + |> pass_tokens(tokens) + end + + def spec_mapper({:user_type, anno, name, args}, tokens, opts) do + new_args = context_mapper_map(args, tokens, opts, &spec_mapper/3) + + {:user_type, anno, name, new_args} + |> pass_tokens(tokens) + end + + def spec_mapper({:ann_type, anno, attrs}, tokens, opts) do + new_attrs = context_mapper_map(attrs, tokens, opts, &spec_mapper/3) + + {:ann_type, anno, new_attrs} + |> pass_tokens(tokens) + end + + def spec_mapper(type, tokens, opts) do + mapper(type, tokens, opts) + end + + @doc """ + Adds missing location to the module literal + """ + def remote_mapper({:remote, line, {:atom, 0, mod}, fun}) do + {:remote, line, {:atom, line, mod}, fun} + end + + def remote_mapper(name), do: name + + @doc """ + Adds missing location to the literals in the guards + """ + @spec guards_mapper([form()], [token()], options()) :: {[form()], [token()]} + def guards_mapper([], tokens, _opts), do: {[], tokens} + + def guards_mapper(guards, tokens, opts) do + List.foldl(guards, {[], tokens}, fn + [guard], {gs, tokens} -> + {g, ts} = mapper(guard, tokens, opts) + {[[g] | gs], ts} + + gs, {ags, ts} -> + Logger.error("Unsupported guards format #{inspect(gs)}") + {gs ++ ags, ts} + end) + end + + @doc """ + Run mapper on map value and key. + """ + @spec map_element_mapper(tuple(), tokens(), options()) :: {tuple(), tokens()} + def map_element_mapper({field, anno, key, value}, tokens, opts) + when field in [:map_field_assoc, :map_field_exact] do + line = :erl_anno.line(anno) + opts = Keyword.put(opts, :line, line) + + {key, tokens} = mapper(key, tokens, opts) + {value, tokens} = mapper(value, tokens, opts) + + {field, anno, key, value} + |> pass_tokens(tokens) + end + + @doc """ + Run mapper on bin element value. + """ + @spec bin_element_mapper(tuple(), tokens(), options()) :: {tuple(), tokens()} + def bin_element_mapper({:bin_element, anno, value, size, tsl}, tokens, opts) do + {:ok, _line, anno, opts, _} = get_line(anno, opts) + + {value, tokens} = mapper(value, tokens, opts) + + {:bin_element, anno, value, size, tsl} + |> pass_tokens(tokens) + end + + @doc """ + Iterate over the list in abstract code format and runs mapper on each element + """ + @spec cons_mapper(form(), [token()], options()) :: {form(), tokens()} + def cons_mapper({:cons, anno, value, tail}, tokens, opts) do + {:ok, _, anno, opts, has_line?} = get_line(anno, opts) + + {anno, opts} = update_line_from_tokens(tokens, anno, opts, has_line?) + + {new_value, tokens} = mapper(value, tokens, opts) + + {tail, tokens} = cons_mapper(tail, tokens, opts) + + {:cons, anno, new_value, tail} + |> pass_tokens(tokens) + end + + def cons_mapper(other, tokens, opts), do: mapper(other, tokens, opts) + + @doc """ + Update form anno with location taken from the corresponding token, if found. + Otherwise return form unchanged. + """ + @spec specify_line(form(), [token()], options()) :: {form(), [token()]} + def specify_line(form, tokens, opts) do + if not :erl_anno.generated(elem(form, 1)) do + {:ok, end_line} = Keyword.fetch(opts, :end_line) + + res = drop_tokens_while(tokens, end_line, &(!match_token_to_form(&1, form))) + + case res do + [token | tokens] -> + {take_loc_from_token(token, form), tokens} + + [] -> + {form, tokens} + end + else + {form, tokens} + end + end + + # Private Helpers + + @spec match_token_to_form(token(), form()) :: boolean() + defp match_token_to_form({:int, {l1, _, v1}, _}, {:integer, l2, v2}) do + l2 = :erl_anno.line(l2) + l2 <= l1 && v1 == v2 + end + + defp match_token_to_form({:char, {l1, _, _}, v1}, {:integer, l2, v2}) do + l2 = :erl_anno.line(l2) + l2 <= l1 && v1 == v2 + end + + defp match_token_to_form({:flt, {l1, _, v1}, _}, {:float, l2, v2}) do + l2 = :erl_anno.line(l2) + l2 <= l1 && v1 == v2 + end + + defp match_token_to_form({:atom, {l1, _, _}, v1}, {:atom, l2, v2}) do + l2 = :erl_anno.line(l2) + l2 <= l1 && v1 == v2 + end + + defp match_token_to_form({:alias, {l1, _, _}, v1}, {:atom, l2, v2}) do + l2 = :erl_anno.line(l2) + l2 <= l1 && v1 == v2 + end + + defp match_token_to_form({:kw_identifier, {l1, _, _}, v1}, {:atom, l2, v2}) do + l2 = :erl_anno.line(l2) + l2 <= l1 && v1 == v2 + end + + defp match_token_to_form({:list_string, {l1, _, _}, [v1]}, {:cons, l2, _, _} = cons) do + v2 = cons_to_charlist(cons) + l2 <= l1 && to_charlist(v1) == v2 + end + + # BINARY + defp match_token_to_form( + {:bin_string, {l1, _, _}, [v1]}, + {:bin, l2, [{:bin_element, _, {:string, _, v2}, :default, :default}]} + ) do + # string + l2 <= l1 && :binary.bin_to_list(v1) == v2 + end + + defp match_token_to_form({:str, _, v}, {:string, _, v1}) do + to_charlist(v) == v1 + end + + defp match_token_to_form({true, {l1, _, _}}, {:atom, l2, true}) do + l2 <= l1 + end + + defp match_token_to_form({false, {l1, _, _}}, {:atom, l2, false}) do + l2 <= l1 + end + + defp match_token_to_form(_, _) do + false + end + + @spec take_loc_from_token(token(), form()) :: form() + defp take_loc_from_token({:int, {line, _, _}, _}, {:integer, _, value}) do + {:integer, line, value} + end + + defp take_loc_from_token({:char, {line, _, _}, _}, {:integer, _, value}) do + {:integer, line, value} + end + + defp take_loc_from_token({:flt, {line, _, _}, _}, {:float, _, value}) do + {:float, line, value} + end + + defp take_loc_from_token({:atom, {line, _, _}, _}, {:atom, _, value}) do + {:atom, line, value} + end + + defp take_loc_from_token({:alias, {line, _, _}, _}, {:atom, _, value}) do + {:atom, line, value} + end + + defp take_loc_from_token({:kw_identifier, {line, _, _}, _}, {:atom, _, value}) do + {:atom, line, value} + end + + defp take_loc_from_token({:list_string, {l1, _, _}, _}, {:cons, _, _, _} = charlist) do + charlist_set_loc(charlist, l1) + end + + defp take_loc_from_token( + {:bin_string, {l1, _, _}, _}, + {:bin, _, [{:bin_element, _, {:string, _, v2}, :default, :default}]} + ) do + {:bin, l1, [{:bin_element, l1, {:string, l1, v2}, :default, :default}]} + end + + defp take_loc_from_token({:str, _, _}, {:string, loc, v2}) do + {:string, loc, v2} + end + + defp take_loc_from_token({true, {line, _, _}}, {:atom, _, true}) do + {:atom, line, true} + end + + defp take_loc_from_token({false, {line, _, _}}, {:atom, _, false}) do + {:atom, line, false} + end + + defp take_loc_from_token(_, _), do: nil + + def cons_to_charlist({nil, _}), do: [] + + def cons_to_charlist({:cons, _, {:integer, _, value}, tail}) do + [value | cons_to_charlist(tail)] + end + + def charlist_set_loc({:cons, _, {:integer, _, value}, tail}, loc) do + {:cons, loc, {:integer, loc, value}, charlist_set_loc(tail, loc)} + end + + def charlist_set_loc({nil, loc}, _), do: {nil, loc} + + def put_line(anno, opts, line) do + {:erl_anno.set_line(line, anno), Keyword.put(opts, :line, line)} + end + + def update_line_from_tokens([token | _], anno, opts, false) do + line = get_line_from_token(token) + put_line(anno, opts, line) + end + + def update_line_from_tokens(_, anno, opts, _) do + {anno, opts} + end + + defp get_line(anno, opts) do + case :erl_anno.line(anno) do + 0 -> + case Keyword.fetch(opts, :line) do + {:ok, line} -> + anno = :erl_anno.set_line(line, anno) + {:ok, line, anno, opts, false} + + err -> + err + end + + line -> + opts = Keyword.put(opts, :line, line) + {:ok, line, anno, opts, true} + end + end + + @spec prepare_forms_order(forms()) :: forms() + defp prepare_forms_order(forms) do + forms + |> Enum.sort(fn l, r -> elem(l, 0) == elem(r, 0) and elem(l, 1) > elem(r, 1) end) + |> Enum.reverse() + end + + defp set_form_end_line(opts, form, forms) do + case Enum.find(forms, fn f -> + anno = elem(f, 1) + + # Maybe should try to go deeper when generated and try to obtain + # the line from the first child. It should work for sure for clauses, + # but it has to be in the right order (e.g. if clauses are reversed) + :erl_anno.line(anno) > 0 and not :erl_anno.generated(anno) + end) do + nil -> + opts + + next_form -> + current_line = :erl_anno.line(elem(form, 1)) + next_line = :erl_anno.line(elem(next_form, 1)) + + if current_line == next_line do + Keyword.put(opts, :end_line, next_line + 1) + else + Keyword.put(opts, :end_line, next_line) + end + end + end + + @spec pass_tokens(any(), tokens()) :: {any(), tokens()} + defp pass_tokens(form, tokens) do + {form, tokens} + end +end diff --git a/lib/gradualizer_ex/tokens.ex b/lib/gradualizer_ex/tokens.ex new file mode 100644 index 00000000..da15bb3a --- /dev/null +++ b/lib/gradualizer_ex/tokens.ex @@ -0,0 +1,198 @@ +defmodule GradualizerEx.Tokens do + @moduledoc """ + Group of functions helping with manage tokens. + """ + alias GradualizerEx.Types, as: T + + @doc """ + Drop tokens to the first conditional occurance. Returns type of the encountered + conditional and following tokens. + """ + @spec get_conditional(T.tokens(), integer(), T.options()) :: + {:case, T.tokens()} + | {:cond, T.tokens()} + | {:unless, T.tokens()} + | {:if, T.tokens()} + | {:with, T.tokens()} + | :undefined + def get_conditional(tokens, line, opts) do + conditionals = [:if, :unless, :cond, :case, :with] + {:ok, limit_line} = Keyword.fetch(opts, :end_line) + + drop_tokens_while(tokens, limit_line, fn + {:do_identifier, _, c} -> c not in conditionals + {:paren_identifier, _, c} -> c not in conditionals + {:identifier, _, c} -> c not in conditionals + _ -> true + end) + |> case do + [token | _] = tokens when elem(elem(token, 1), 0) == line -> {elem(token, 2), tokens} + _ -> :undefined + end + end + + @doc """ + Drop tokens to the first list occurance. Returns type of the encountered + list and following tokens. + """ + @spec get_list(T.tokens(), T.options()) :: + {:list, T.tokens()} | {:keyword, T.tokens()} | {:charlist, T.tokens()} | :undefined + def get_list(tokens, opts) do + tokens = flat_tokens(tokens) + {:ok, limit_line} = Keyword.fetch(opts, :end_line) + + res = + drop_tokens_while(tokens, limit_line, fn + {:"[", _} -> false + {:list_string, _, _} -> false + {:kw_identifier, _, id} when id not in [:do] -> false + _ -> true + end) + + case res do + [{:"[", _} | _] = list -> {:list, list} + [{:list_string, _, _} | _] = list -> {:charlist, list} + [{:kw_identifier, _, _} | _] = list -> {:keyword, list} + _ -> :undefined + end + end + + @doc """ + Drop tokens to the first tuple occurance. Returns type of the encountered + list and following tokens. + """ + @spec get_tuple(T.tokens(), T.options()) :: + {:tuple, T.tokens()} | :undefined + def get_tuple(tokens, opts) do + {:ok, limit_line} = Keyword.fetch(opts, :end_line) + + res = + drop_tokens_while(tokens, limit_line, fn + {:"{", _} -> false + {:kw_identifier, _, _} -> false + _ -> true + end) + + case res do + [{:"{", _} | _] = tuple -> {:tuple, tuple} + [{:kw_identifier, _, _} | _] = tuple -> {:tuple, tuple} + _ -> :undefined + end + end + + @doc """ + Drop tokens till the matcher returns false or the token's line exceeds the limit. + """ + @spec drop_tokens_while(T.tokens(), integer(), (T.token() -> boolean())) :: T.tokens() + def drop_tokens_while(tokens, limit_line \\ -1, matcher) + def drop_tokens_while([], _, _), do: [] + + def drop_tokens_while([token | tokens] = all, limit_line, matcher) do + line = get_line_from_token(token) + + limit_passed = limit_line < 0 or line < limit_line + + cond do + matcher.(token) and limit_passed -> + drop_tokens_while(tokens, limit_line, matcher) + + not limit_passed -> + [] + + true -> + all + end + end + + @doc """ + Drop tokens while the token's line is lower than given location. + """ + @spec drop_tokens_to_line(T.tokens(), integer()) :: T.tokens() + def drop_tokens_to_line(tokens, line) do + Enum.drop_while(tokens, fn t -> + elem(elem(t, 1), 0) < line + end) + end + + @doc """ + Get line from token. + """ + @spec get_line_from_token(T.token()) :: integer() + def get_line_from_token(token), do: elem(elem(token, 1), 0) + + def get_line_from_form(form) do + form + |> elem(1) + |> :erl_anno.line() + end + + @doc """ + Drop the tokens to binary occurrence and then collect all belonging tokens. + Return tuple where first element is a list of tokens making binary, and second + element is a list of tokens after binary. + """ + @spec cut_tokens_to_bin(T.tokens(), integer()) :: {T.tokens(), T.tokens()} + def cut_tokens_to_bin(tokens, line) do + tokens = drop_tokens_to_line(tokens, line) + + drop_tokens_while(tokens, fn + {:"<<", _} -> false + {:bin_string, _, _} -> false + _ -> true + end) + |> case do + [{:"<<", _} | _] = ts -> cut_bottom(ts, 0) + [{:bin_string, _, _} = t | ts] -> {[t], ts} + [] -> {[], tokens} + end + end + + @doc """ + Flat the tokens, mostly binaries or string interpolation. + """ + @spec flat_tokens(T.tokens()) :: T.tokens() + def flat_tokens(tokens) do + Enum.map(tokens, &flat_token/1) + |> Enum.concat() + end + + # Private + + defp flat_token(token) do + case token do + {:bin_string, _, [s]} = t when is_binary(s) -> + [t] + + {:bin_string, _, ts} -> + flat_tokens(ts) + + {{_, _, nil}, {_, _, nil}, ts} -> + flat_tokens(ts) + + str when is_binary(str) -> + [{:str, {0, 0, nil}, str}] + + _otherwise -> + [token] + end + end + + defp cut_bottom([{:"<<", _} = t | ts], deep) do + {ts, cut_ts} = cut_bottom(ts, deep + 1) + {[t | ts], cut_ts} + end + + defp cut_bottom([{:">>", _} = t | ts], deep) do + if deep - 1 > 0 do + {ts, cut_ts} = cut_bottom(ts, deep - 1) + {[t | ts], cut_ts} + else + {[t], ts} + end + end + + defp cut_bottom([t | ts], deep) do + {ts, cut_ts} = cut_bottom(ts, deep) + {[t | ts], cut_ts} + end +end diff --git a/lib/gradualizer_ex/types.ex b/lib/gradualizer_ex/types.ex new file mode 100644 index 00000000..6d36aac9 --- /dev/null +++ b/lib/gradualizer_ex/types.ex @@ -0,0 +1,11 @@ +defmodule GradualizerEx.Types do + @type token :: tuple() + @type tokens :: [tuple()] + @type form :: + :erl_parse.abstract_clause() + | :erl_parse.abstract_expr() + | :erl_parse.abstract_form() + | :erl_parse.abstract_type() + @type forms :: [form()] + @type options :: keyword() +end diff --git a/mix.exs b/mix.exs index e60f214d..875226c2 100644 --- a/mix.exs +++ b/mix.exs @@ -6,6 +6,7 @@ defmodule Gradient.MixProject do app: :gradient, version: "0.1.0", elixir: "~> 1.12", + elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, deps: deps(), aliases: aliases(), @@ -13,6 +14,10 @@ defmodule Gradient.MixProject do ] end + # Specifies which paths to compile per environment. + defp elixirc_paths(:test), do: ["lib", "test/support"] + defp elixirc_paths(_), do: ["lib"] + # Run "mix help compile.app" to learn about applications. def application do [ diff --git a/test/gradient/ast_specifier_test.exs b/test/gradient/ast_specifier_test.exs new file mode 100644 index 00000000..db021034 --- /dev/null +++ b/test/gradient/ast_specifier_test.exs @@ -0,0 +1,1380 @@ +<<<<<<< HEAD:test/gradient/specify_erl_ast_test.exs +defmodule Gradient.SpecifyErlAstTest do + use ExUnit.Case + doctest Gradient.SpecifyErlAst + + alias Gradient.SpecifyErlAst + + import Gradient.Utils + + @examples_path "test/examples" +======= +defmodule GradualizerEx.AstSpecifierTest do + use ExUnit.Case + doctest GradualizerEx.AstSpecifier + + alias GradualizerEx.AstSpecifier + + import GradualizerEx.TestHelpers +>>>>>>> 9a63ccd (Code refactoring, Add docs):test/gradient/ast_specifier_test.exs + + setup_all state do + {:ok, state} + end + + describe "run_mappers/2" do + test "messy test on simple_app" do + {tokens, ast} = example_data() + new_ast = AstSpecifier.run_mappers(ast, tokens) + + assert is_list(new_ast) + end + + test "integer" do + {tokens, ast} = load("/basic/Elixir.Basic.Int.beam", "/basic/int.ex") + + [block, inline | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() + + assert {:function, 2, :int, 0, [{:clause, 2, [], [], [{:integer, 2, 1}]}]} = inline + + assert {:function, 4, :int_block, 0, [{:clause, 4, [], [], [{:integer, 5, 2}]}]} = block + end + + test "float" do + {tokens, ast} = load("/basic/Elixir.Basic.Float.beam", "/basic/float.ex") + + [block, inline | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() + assert {:function, 2, :float, 0, [{:clause, 2, [], [], [{:float, 2, 0.12}]}]} = inline + + assert {:function, 4, :float_block, 0, [{:clause, 4, [], [], [{:float, 5, 0.12}]}]} = block + end + + test "atom" do + {tokens, ast} = load("/basic/Elixir.Basic.Atom.beam", "/basic/atom.ex") + + [block, inline | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() + + assert {:function, 2, :atom, 0, [{:clause, 2, [], [], [{:atom, 2, :ok}]}]} = inline + + assert {:function, 4, :atom_block, 0, [{:clause, 4, [], [], [{:atom, 5, :ok}]}]} = block + end + + test "char" do + {tokens, ast} = load("/basic/Elixir.Basic.Char.beam", "/basic/char.ex") + + [block, inline | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() + + assert {:function, 2, :char, 0, [{:clause, 2, [], [], [{:integer, 2, 99}]}]} = inline + + assert {:function, 4, :char_block, 0, [{:clause, 4, [], [], [{:integer, 5, 99}]}]} = block + end + + test "charlist" do + {tokens, ast} = load("/basic/Elixir.Basic.Charlist.beam", "/basic/charlist.ex") + + [block, inline | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() + + # TODO propagate location to each charlist element + assert {:function, 2, :charlist, 0, + [ + {:clause, 2, [], [], + [ + {:cons, 2, {:integer, 2, 97}, + {:cons, 2, {:integer, 2, 98}, {:cons, 2, {:integer, 2, 99}, {nil, 0}}}} + ]} + ]} = inline + + assert {:function, 4, :charlist_block, 0, + [ + {:clause, 4, [], [], + [ + {:cons, 5, {:integer, 5, 97}, + {:cons, 5, {:integer, 5, 98}, {:cons, 5, {:integer, 5, 99}, {nil, 0}}}} + ]} + ]} = block + end + + test "string" do + {tokens, ast} = load("/basic/Elixir.Basic.String.beam", "/basic/string.ex") + + [block, inline | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() + + assert {:function, 2, :string, 0, + [ + {:clause, 2, [], [], + [{:bin, 2, [{:bin_element, 2, {:string, 2, 'abc'}, :default, :default}]}]} + ]} = inline + + assert {:function, 4, :string_block, 0, + [ + {:clause, 4, [], [], + [{:bin, 5, [{:bin_element, 5, {:string, 5, 'abc'}, :default, :default}]}]} + ]} = block + end + + test "tuple" do + {tokens, ast} = load("/Elixir.Tuple.beam", "/tuple.ex") + + [tuple_in_str2, tuple_in_str, tuple_in_list, _list_in_tuple, tuple | _] = + AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() + + # FIXME + assert {:function, 18, :tuple_in_str2, 0, + [ + {:clause, 18, [], [], + [ + {:match, 19, {:var, 19, :_msg@1}, + {:bin, 20, + [ + {:bin_element, 20, {:string, 20, '\nElixir formatter not exist for '}, + :default, :default}, + {:bin_element, 20, + {:call, 20, {:remote, 20, {:atom, 20, Kernel}, {:atom, 20, :inspect}}, + [ + {:tuple, 20, []}, + {:cons, 20, {:tuple, 20, [{:atom, 20, :pretty}, {:atom, 20, true}]}, + {:cons, 20, + {:tuple, 20, [{:atom, 20, :limit}, {:atom, 20, :infinity}]}, + {nil, 0}}} + ]}, :default, [:binary]}, + {:bin_element, 20, {:string, 20, ' using default \n'}, :default, :default} + ]}}, + {:call, 22, {:remote, 22, {:atom, 22, String}, {:atom, 22, :to_charlist}}, + [ + {:bin, 22, + [ + {:bin_element, 22, + {:call, 22, + {:remote, 22, {:atom, 22, IO.ANSI}, {:atom, 22, :light_yellow}}, []}, + :default, [:binary]}, + {:bin_element, 22, {:var, 22, :_msg@1}, :default, [:binary]}, + {:bin_element, 22, + {:call, 22, {:remote, 22, {:atom, 22, IO.ANSI}, {:atom, 22, :reset}}, + []}, :default, [:binary]} + ]} + ]} + ]} + ]} = tuple_in_str2 + + assert {:function, 14, :tuple_in_str, 0, + [ + {:clause, 14, [], [], + [ + {:bin, 15, + [ + {:bin_element, 15, {:string, 15, 'abc '}, :default, :default}, + {:bin_element, 15, + {:call, 15, {:remote, 15, {:atom, 15, Kernel}, {:atom, 15, :inspect}}, + [ + {:atom, 15, :abc}, + {:cons, 15, {:tuple, 15, [{:atom, 15, :limit}, {:atom, 15, :infinity}]}, + {:cons, 15, + {:tuple, 15, + [ + {:atom, 15, :label}, + {:bin, 15, + [ + {:bin_element, 15, {:string, 15, 'abc '}, :default, :default}, + {:bin_element, 15, + {:case, [generated: true, location: 15], {:integer, 15, 13}, + [ + {:clause, [generated: true, location: 15], + [{:var, [generated: true, location: 15], :_@1}], + [ + [ + {:call, [generated: true, location: 15], + {:remote, [generated: true, location: 15], + {:atom, [generated: true, location: 15], :erlang}, + {:atom, [generated: true, location: 15], :is_binary}}, + [{:var, [generated: true, location: 15], :_@1}]} + ] + ], [{:var, [generated: true, location: 15], :_@1}]}, + {:clause, [generated: true, location: 15], + [{:var, [generated: true, location: 15], :_@1}], [], + [ + {:call, [generated: true, location: 15], + {:remote, [generated: true, location: 15], + {:atom, [generated: true, location: 15], String.Chars}, + {:atom, [generated: true, location: 15], :to_string}}, + [{:var, [generated: true, location: 15], :_@1}]} + ]} + ]}, :default, [:binary]} + ]} + ]}, {nil, 0}}} + ]}, :default, [:binary]}, + {:bin_element, 15, {:integer, 15, 12}, :default, [:integer]} + ]} + ]} + ]} = tuple_in_str + + assert {:function, 10, :tuple_in_list, 0, + [ + {:clause, 10, [], [], + [ + {:cons, 11, {:tuple, 11, [{:atom, 11, :a}, {:integer, 11, 12}]}, + {:cons, 11, {:tuple, 11, [{:atom, 11, :b}, {:atom, 11, :ok}]}, {nil, 0}}} + ]} + ]} = tuple_in_list + + assert {:function, 2, :tuple, 0, + [{:clause, 2, [], [], [{:tuple, 3, [{:atom, 3, :ok}, {:integer, 3, 12}]}]}]} = tuple + end + + test "binary" do + {tokens, ast} = load("/basic/Elixir.Basic.Binary.beam", "/basic/binary.ex") + + [complex2, complex, bin_block, bin | _] = + AstSpecifier.run_mappers(ast, tokens) + |> Enum.reverse() + + assert {:function, 13, :complex2, 0, + [ + {:clause, 13, [], [], + [ + {:bin, 14, + [ + {:bin_element, 14, {:string, 14, 'abc '}, :default, :default}, + {:bin_element, 14, + {:call, 14, {:remote, 14, {:atom, 14, Kernel}, {:atom, 14, :inspect}}, + [{:integer, 14, 12}]}, :default, [:binary]}, + {:bin_element, 14, {:string, 14, ' cba'}, :default, :default} + ]} + ]} + ]} = complex2 + + assert {:function, 8, :complex, 0, + [ + {:clause, 8, [], [], + [ + {:match, 9, {:var, 9, :_x@2}, + {:fun, 9, + {:clauses, + [ + {:clause, 9, [{:var, 9, :_x@1}], [], + [{:op, 9, :+, {:var, 9, :_x@1}, {:integer, 9, 1}}]} + ]}}}, + {:bin, 10, + [ + {:bin_element, 10, {:integer, 10, 49}, :default, [:integer]}, + {:bin_element, 10, {:integer, 10, 48}, :default, [:integer]}, + {:bin_element, 10, {:call, 10, {:var, 10, :_x@2}, [{:integer, 10, 50}]}, + :default, [:integer]} + ]} + ]} + ]} = complex + + assert {:function, 4, :bin_block, 0, + [ + {:clause, 4, [], [], + [ + {:bin, 5, + [ + {:bin_element, 5, {:integer, 5, 49}, :default, [:integer]}, + {:bin_element, 5, {:integer, 5, 48}, :default, [:integer]}, + {:bin_element, 5, {:integer, 5, 48}, :default, [:integer]} + ]} + ]} + ]} = bin_block + + assert {:function, 2, :bin, 0, + [ + {:clause, 2, [], [], + [ + {:bin, 2, + [ + {:bin_element, 2, {:integer, 2, 49}, :default, [:integer]}, + {:bin_element, 2, {:integer, 2, 48}, :default, [:integer]}, + {:bin_element, 2, {:integer, 2, 48}, :default, [:integer]} + ]} + ]} + ]} = bin + end + + test "case conditional" do + {tokens, ast} = load("/conditional/Elixir.Conditional.Case.beam", "/conditional/case.ex") + + [block, inline | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() + + assert {:function, 2, :case_, 0, + [ + {:clause, 2, [], [], + [ + {:case, 4, {:integer, 4, 5}, + [ + {:clause, 5, [{:integer, 5, 5}], [], [{:atom, 5, :ok}]}, + {:clause, 6, [{:var, 6, :_}], [], [{:atom, 6, :error}]} + ]} + ]} + ]} = inline + + assert {:function, 9, :case_block, 0, + [ + {:clause, 9, [], [], + [ + {:case, 10, {:integer, 10, 5}, + [ + {:clause, 11, [{:integer, 11, 5}], [], [{:atom, 11, :ok}]}, + {:clause, 12, [{:var, 12, :_}], [], [{:atom, 12, :error}]} + ]} + ]} + ]} = block + end + + test "if conditional" do + {tokens, ast} = load("/conditional/Elixir.Conditional.If.beam", "/conditional/if.ex") + + [block, inline, if_ | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() + + assert {:function, 12, :if_block, 0, + [ + {:clause, 12, [], [], + [ + {:case, 13, {:op, 13, :<, {:integer, 13, 1}, {:integer, 13, 5}}, + [ + {:clause, [generated: true, location: 13], [{:atom, 0, false}], [], + [{:atom, 16, :error}]}, + {:clause, [generated: true, location: 13], [{:atom, 0, true}], [], + [{:atom, 14, :ok}]} + ]} + ]} + ]} = block + + assert {:function, 10, :if_inline, 0, + [ + {:clause, 10, [], [], + [ + {:case, 10, {:op, 10, :<, {:integer, 10, 1}, {:integer, 10, 5}}, + [ + {:clause, [generated: true, location: 10], [{:atom, 0, false}], [], + [{:atom, 10, :error}]}, + {:clause, [generated: true, location: 10], [{:atom, 0, true}], [], + [{:atom, 10, :ok}]} + ]} + ]} + ]} = inline + + assert {:function, 2, :if_, 0, + [ + {:clause, 2, [], [], + [ + {:case, 4, {:op, 4, :<, {:integer, 4, 1}, {:integer, 4, 5}}, + [ + {:clause, [generated: true, location: 4], [{:atom, 0, false}], [], + [{:atom, 7, :error}]}, + {:clause, [generated: true, location: 4], [{:atom, 0, true}], [], + [{:atom, 5, :ok}]} + ]} + ]} + ]} = if_ + end + + test "unless conditional" do + {tokens, ast} = + load("/conditional/Elixir.Conditional.Unless.beam", "/conditional/unless.ex") + + [block | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() + + assert { + :function, + 2, + :unless_block, + 0, + [ + {:clause, 2, [], [], + [ + {:case, 3, {:atom, 3, false}, + [ + {:clause, [generated: true, location: 3], [{:atom, 0, false}], [], + [{:atom, 4, :ok}]}, + {:clause, [generated: true, location: 3], [{:atom, 0, true}], [], + [{:atom, 6, :error}]} + ]} + ]} + ] + } == block + end + + test "cond conditional" do + {tokens, ast} = load("/conditional/Elixir.Conditional.Cond.beam", "/conditional/cond.ex") + + [block, inline | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() + + assert {:function, 2, :cond_, 1, + [ + {:clause, 2, [{:var, 2, :_a@1}], [], + [ + {:case, 4, {:op, 5, :==, {:var, 5, :_a@1}, {:atom, 5, :ok}}, + [ + {:clause, 5, [{:atom, 0, true}], [], [{:atom, 5, :ok}]}, + {:clause, 6, [{:atom, 0, false}], [], + [ + {:case, 6, {:op, 6, :>, {:var, 6, :_a@1}, {:integer, 6, 5}}, + [ + {:clause, 6, [{:atom, 0, true}], [], [{:atom, 6, :ok}]}, + {:clause, 7, [{:atom, 0, false}], [], + [ + {:case, 7, {:atom, 7, true}, + [ + {:clause, 7, [{:atom, 0, true}], [], [{:atom, 7, :error}]}, + {:clause, [generated: true, location: 7], [{:atom, 0, false}], + [], + [ + {:call, 7, + {:remote, 7, {:atom, 7, :erlang}, {:atom, 7, :error}}, + [{:atom, 7, :cond_clause}]} + ]} + ]} + ]} + ]} + ]} + ]} + ]} + ]} = inline + + assert {:function, 10, :cond_block, 0, + [ + {:clause, 10, [], [], + [ + {:match, 11, {:var, 11, :_a@1}, {:integer, 11, 5}}, + {:case, 13, {:op, 14, :==, {:var, 14, :_a@1}, {:atom, 14, :ok}}, + [ + {:clause, 14, [{:atom, 0, true}], [], [{:atom, 14, :ok}]}, + {:clause, 15, [{:atom, 0, false}], [], + [ + {:case, 15, {:op, 15, :>, {:var, 15, :_a@1}, {:integer, 15, 5}}, + [ + {:clause, 15, [{:atom, 0, true}], [], [{:atom, 15, :ok}]}, + {:clause, 16, [{:atom, 0, false}], [], + [ + {:case, 16, {:atom, 16, true}, + [ + {:clause, 16, [{:atom, 0, true}], [], [{:atom, 16, :error}]}, + {:clause, [generated: true, location: 16], [{:atom, 0, false}], + [], + [ + {:call, 16, + {:remote, 16, {:atom, 16, :erlang}, {:atom, 16, :error}}, + [{:atom, 16, :cond_clause}]} + ]} + ]} + ]} + ]} + ]} + ]} + ]} + ]} = block + end + + test "with conditional" do + {tokens, ast} = load("/conditional/Elixir.Conditional.With.beam", "/conditional/with.ex") + + [block | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() + + assert {:function, 6, :test_with, 0, + [ + {:clause, 6, [], [], + [ + {:case, [generated: true, location: 7], {:call, 7, {:atom, 7, :ok_res}, []}, + [ + {:clause, 7, [{:tuple, 7, [{:atom, 7, :ok}, {:var, 7, :__a@1}]}], [], + [{:integer, 8, 12}]}, + {:clause, [generated: true, location: 7], [{:var, 10, :_}], [], + [ + {:block, 7, + [ + {:call, 11, {:remote, 11, {:atom, 11, IO}, {:atom, 11, :puts}}, + [ + {:bin, 11, + [{:bin_element, 11, {:string, 11, 'error'}, :default, :default}]} + ]}, + {:cons, 12, {:integer, 12, 49}, + {:cons, 12, {:integer, 12, 50}, {nil, 0}}} + ]} + ]} + ]} + ]} + ]} == block + end + + @tag :skip + test "basic function return" do + ex_file = "/basic.ex" + beam_file = "/Elixir.Basic.beam" + {tokens, ast} = load(beam_file, ex_file) + + specified_ast = AstSpecifier.run_mappers(ast, tokens) + IO.inspect(specified_ast) + assert is_list(specified_ast) + end + end + + test "specify_line/2" do + {tokens, _} = example_data() + opts = [end_line: -1] + + assert {{:integer, 21, 12}, tokens} = + AstSpecifier.specify_line({:integer, 21, 12}, tokens, opts) + + assert {{:integer, 22, 12}, _tokens} = + AstSpecifier.specify_line({:integer, 20, 12}, tokens, opts) + end + + test "cons_to_charlist/1" do + cons = + {:cons, 0, {:integer, 0, 49}, + {:cons, 0, {:integer, 0, 48}, {:cons, 0, {:integer, 0, 48}, {nil, 0}}}} + + assert '100' == AstSpecifier.cons_to_charlist(cons) + end + + describe "test that prints result" do + @tag :skip + test "specify/1" do + {_tokens, forms} = example_data() + + AstSpecifier.specify(forms) + |> IO.inspect() + end + + @tag :skip + test "display forms" do + {_, forms} = example_data() + IO.inspect(forms) + end + end + + test "function call" do + {tokens, ast} = load("/Elixir.Call.beam", "/call.ex") + + [call, _ | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() + + assert {:function, 5, :call, 0, + [ + {:clause, 5, [], [], + [ + {:call, 6, {:atom, 6, :get_x}, + [ + {:bin, 7, [{:bin_element, 7, {:string, 7, 'ala'}, :default, :default}]}, + {:cons, 8, {:integer, 8, 97}, + {:cons, 8, {:integer, 8, 108}, {:cons, 8, {:integer, 8, 97}, {nil, 0}}}}, + {:integer, 9, 12} + ]} + ]} + ]} = call + end + + test "pipe" do + {tokens, ast} = load("/Elixir.Pipe.beam", "/pipe_op.ex") + + [block | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() + + assert {:function, 2, :pipe, 0, + [ + {:clause, 2, [], [], + [ + {:call, 5, {:remote, 5, {:atom, 5, :erlang}, {:atom, 5, :length}}, + [ + {:call, 4, {:remote, 4, {:atom, 4, Enum}, {:atom, 4, :filter}}, + [ + {:cons, 4, {:integer, 4, 1}, + {:cons, 4, + { + :integer, + 4, + 2 + }, {:cons, 4, {:integer, 4, 3}, {nil, 0}}}}, + {:fun, 4, + {:clauses, + [ + {:clause, 4, [{:var, 4, :_x@1}], [], + [{:op, 4, :<, {:var, 4, :_x@1}, {:integer, 4, 3}}]} + ]}} + ]} + ]} + ]} + ]} = block + end + + test "guards" do + {tokens, ast} = load("/conditional/Elixir.Conditional.Guard.beam", "/conditional/guards.ex") + + [guarded_case, guarded_fun | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() + + assert {:function, 3, :guarded_fun, 1, + [ + {:clause, 3, [{:var, 3, :_x@1}], + [ + [ + {:call, 3, {:remote, 3, {:atom, 3, :erlang}, {:atom, 3, :is_integer}}, + [{:var, 3, :_x@1}]} + ], + [ + {:op, 3, :andalso, {:op, 3, :>, {:var, 3, :_x@1}, {:integer, 3, 3}}, + {:op, 3, :<, {:var, 3, :_x@1}, {:integer, 3, 6}}} + ] + ], [{:atom, 3, :ok}]} + ]} = guarded_fun + + assert {:function, 6, :guarded_case, 1, + [ + {:clause, 6, [{:var, 6, :_x@1}], [], + [ + {:case, 7, {:var, 7, :_x@1}, + [ + {:clause, 8, [{:integer, 8, 0}], [], + [{:tuple, 8, [{:atom, 8, :ok}, {:integer, 8, 1}]}]}, + {:clause, 9, [{:var, 9, :_i@1}], + [[{:op, 9, :>, {:var, 9, :_i@1}, {:integer, 9, 0}}]], + [ + {:tuple, 9, + [{:atom, 9, :ok}, {:op, 9, :+, {:var, 9, :_i@1}, {:integer, 9, 1}}]} + ]}, + {:clause, 10, [{:var, 10, :__otherwise@1}], [], [{:atom, 10, :error}]} + ]} + ]} + ]} = guarded_case + end + + test "range" do + {tokens, ast} = load("/Elixir.RangeEx.beam", "/range.ex") + + [to_list, match_range, rev_range_step, range_step, range | _] = + AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() + + assert {:function, 18, :to_list, 0, + [ + {:clause, 18, [], [], + [ + {:call, 19, {:remote, 19, {:atom, 19, Enum}, {:atom, 19, :to_list}}, + [ + {:map, 19, + [ + {:map_field_assoc, 19, {:atom, 19, :__struct__}, {:atom, 19, Range}}, + {:map_field_assoc, 19, {:atom, 19, :first}, {:integer, 19, 1}}, + {:map_field_assoc, 19, {:atom, 19, :last}, {:integer, 19, 100}}, + {:map_field_assoc, 19, {:atom, 19, :step}, {:integer, 19, 5}} + ]} + ]} + ]} + ]} = to_list + + assert {:function, 14, :match_range, 0, + [ + {:clause, 14, [], [], + [ + {:match, 15, + {:map, 15, + [ + {:map_field_exact, 15, {:atom, 15, :__struct__}, {:atom, 15, Range}}, + {:map_field_exact, 15, {:atom, 15, :first}, {:var, 15, :_first@1}}, + {:map_field_exact, 15, {:atom, 15, :last}, {:var, 15, :_last@1}}, + {:map_field_exact, 15, {:atom, 15, :step}, {:var, 15, :_step@1}} + ]}, {:call, 15, {:atom, 15, :range_step}, []}} + ]} + ]} = match_range + + assert {:function, 10, :rev_range_step, 0, + [ + {:clause, 10, [], [], + [ + {:map, 11, + [ + {:map_field_assoc, 11, {:atom, 11, :__struct__}, {:atom, 11, Range}}, + {:map_field_assoc, 11, {:atom, 11, :first}, {:integer, 11, 12}}, + {:map_field_assoc, 11, {:atom, 11, :last}, {:integer, 11, 1}}, + {:map_field_assoc, 11, {:atom, 11, :step}, {:integer, 11, -2}} + ]} + ]} + ]} = rev_range_step + + assert {:function, 6, :range_step, 0, + [ + {:clause, 6, [], [], + [ + {:map, 7, + [ + {:map_field_assoc, 7, {:atom, 7, :__struct__}, {:atom, 7, Range}}, + {:map_field_assoc, 7, {:atom, 7, :first}, {:integer, 7, 1}}, + {:map_field_assoc, 7, {:atom, 7, :last}, {:integer, 7, 12}}, + {:map_field_assoc, 7, {:atom, 7, :step}, {:integer, 7, 2}} + ]} + ]} + ]} = range_step + + assert {:function, 2, :range, 0, + [ + {:clause, 2, [], [], + [ + {:map, 3, + [ + {:map_field_assoc, 3, {:atom, 3, :__struct__}, {:atom, 3, Range}}, + {:map_field_assoc, 3, {:atom, 3, :first}, {:integer, 3, 1}}, + {:map_field_assoc, 3, {:atom, 3, :last}, {:integer, 3, 12}}, + {:map_field_assoc, 3, {:atom, 3, :step}, {:integer, 3, 1}} + ]} + ]} + ]} = range + end + + test "list comprehension" do + {tokens, ast} = load("/Elixir.ListComprehension.beam", "/list_comprehension.ex") + + [block | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() + + assert {:function, 2, :lc, 0, + [ + {:clause, 2, [], [], + [ + {:call, 3, {:remote, 3, {:atom, 3, :lists}, {:atom, 3, :reverse}}, + [ + {:call, 3, {:remote, 3, {:atom, 3, Enum}, {:atom, 3, :reduce}}, + [ + {:map, 3, + [ + {:map_field_assoc, 3, {:atom, 3, :__struct__}, {:atom, 3, Range}}, + {:map_field_assoc, 3, {:atom, 3, :first}, {:integer, 3, 0}}, + {:map_field_assoc, 3, {:atom, 3, :last}, {:integer, 3, 5}}, + {:map_field_assoc, 3, {:atom, 3, :step}, {:integer, 3, 1}} + ]}, + {nil, 3}, + {:fun, 3, + {:clauses, + [ + {:clause, 3, [{:var, 3, :_n@1}, {:var, 3, :_@1}], [], + [ + {:case, [generated: true, location: 3], + {:op, 3, :==, {:op, 3, :rem, {:var, 3, :_n@1}, {:integer, 3, 3}}, + {:integer, 3, 0}}, + [ + {:clause, [generated: true, location: 3], + [{:atom, [generated: true, location: 3], true}], [], + [ + {:cons, 3, {:op, 3, :*, {:var, 3, :_n@1}, {:var, 3, :_n@1}}, + {:var, 3, :_@1}} + ]}, + {:clause, [generated: true, location: 3], + [{:atom, [generated: true, location: 3], false}], [], + [{:var, 3, :_@1}]} + ]} + ]} + ]}} + ]} + ]} + ]} + ]} = block + end + + test "list" do + {tokens, ast} = load("/Elixir.ListEx.beam", "/list.ex") + + [ht2, ht, list, _wrap | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() + + assert {:function, 5, :list, 0, + [ + {:clause, 5, [], [], + [ + {:cons, 6, + {:cons, 6, {:integer, 6, 49}, {:cons, 6, {:integer, 6, 49}, {nil, 0}}}, + {:cons, 6, + {:bin, 6, [{:bin_element, 6, {:string, 6, '12'}, :default, :default}]}, + {:cons, 6, {:integer, 6, 1}, + {:cons, 6, {:integer, 6, 2}, + {:cons, 6, {:integer, 6, 3}, + {:cons, 6, {:call, 6, {:atom, 6, :wrap}, [{:integer, 6, 4}]}, {nil, 0}}}}}}} + ]} + ]} = list + + assert {:function, 9, :ht, 1, + [ + {:clause, 9, [{:cons, 9, {:var, 9, :_a@1}, {:var, 9, :_}}], [], + [ + {:cons, 10, {:var, 10, :_a@1}, + {:cons, 10, {:integer, 10, 1}, + {:cons, 10, {:integer, 10, 2}, {:cons, 10, {:integer, 10, 3}, {nil, 0}}}}} + ]} + ]} = ht + + assert {:function, 13, :ht2, 1, + [ + {:clause, 13, [{:cons, 13, {:var, 13, :_a@1}, {:var, 13, :_}}], [], + [ + {:cons, 14, {:var, 14, :_a@1}, + {:call, 14, {:atom, 14, :wrap}, [{:integer, 14, 1}]}} + ]} + ]} = ht2 + end + + test "try" do + {tokens, ast} = load("/Elixir.Try.beam", "/try.ex") + + [body_after, try_after, try_else, try_rescue | _] = + AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() + + assert {:function, 2, :try_rescue, 0, + [ + {:clause, 2, [], [], + [ + {:try, 3, + [ + {:case, 4, {:atom, 4, true}, + [ + {:clause, [generated: true, location: 4], [{:atom, 0, false}], [], + [ + {:call, 7, {:remote, 7, {:atom, 7, :erlang}, {:atom, 7, :error}}, + [ + {:call, 7, + {:remote, 7, {:atom, 7, RuntimeError}, {:atom, 7, :exception}}, + [ + {:bin, 7, + [ + {:bin_element, 7, {:string, 7, 'oops'}, :default, :default} + ]} + ]} + ]} + ]}, + {:clause, [generated: true, location: 4], [{:atom, 0, true}], [], + [ + {:call, 5, {:remote, 5, {:atom, 5, :erlang}, {:atom, 5, :throw}}, + [ + {:bin, 5, + [{:bin_element, 5, {:string, 5, 'good'}, :default, :default}]} + ]} + ]} + ]} + ], [], + [ + {:clause, 10, + [ + {:tuple, 10, + [ + {:atom, 10, :error}, + {:var, 10, :_@1}, + {:var, 10, :___STACKTRACE__@1} + ]} + ], + [ + [ + {:op, 10, :andalso, + {:op, 10, :==, + {:call, 10, {:remote, 10, {:atom, 10, :erlang}, {:atom, 10, :map_get}}, + [{:atom, 10, :__struct__}, {:var, 10, :_@1}]}, + {:atom, 10, RuntimeError}}, + {:call, 10, {:remote, 10, {:atom, 10, :erlang}, {:atom, 10, :map_get}}, + [{:atom, 10, :__exception__}, {:var, 10, :_@1}]}} + ] + ], + [ + {:match, 10, {:var, 10, :_e@1}, {:var, 10, :_@1}}, + {:integer, 11, 11}, + {:var, 12, :_e@1} + ]}, + {:clause, 14, + [ + {:tuple, 14, + [ + {:atom, 14, :throw}, + {:var, 14, :_val@1}, + {:var, 14, :___STACKTRACE__@1} + ]} + ], [], [{:integer, 15, 12}, {:var, 16, :_val@1}]} + ], []} + ]} + ]} = try_rescue + + assert {:function, 20, :try_else, 0, + [ + {:clause, 20, [], [], + [ + {:match, 21, {:var, 21, :_x@1}, {:integer, 21, 2}}, + {:try, 23, [{:op, 24, :/, {:integer, 24, 1}, {:var, 24, :_x@1}}], + [ + {:clause, 30, [{:var, 30, :_y@1}], + [ + [ + {:op, 30, :andalso, {:op, 30, :<, {:var, 30, :_y@1}, {:integer, 30, 1}}, + {:op, 30, :>, {:var, 30, :_y@1}, {:op, 30, :-, {:integer, 30, 1}}}} + ] + ], [{:integer, 31, 2}, {:atom, 32, :small}]}, + {:clause, 34, [{:var, 34, :_}], [], [{:integer, 35, 3}, {:atom, 36, :large}]} + ], + [ + {:clause, 26, + [ + {:tuple, 26, + [ + {:atom, 26, :error}, + {:var, 26, :_@1}, + {:var, 26, :___STACKTRACE__@1} + ]} + ], + [ + [{:op, 26, :==, {:var, 26, :_@1}, {:atom, 26, :badarith}}], + [ + {:op, 26, :andalso, + {:op, 26, :==, + {:call, 26, {:remote, 26, {:atom, 26, :erlang}, {:atom, 26, :map_get}}, + [{:atom, 26, :__struct__}, {:var, 26, :_@1}]}, + {:atom, 26, ArithmeticError}}, + {:call, 26, {:remote, 26, {:atom, 26, :erlang}, {:atom, 26, :map_get}}, + [{:atom, 26, :__exception__}, {:var, 26, :_@1}]}} + ] + ], [{:integer, 27, 1}, {:atom, 28, :infinity}]} + ], []} + ]} + ]} = try_else + + assert {:function, 40, :try_after, 0, + [ + {:clause, 40, [], [], + [ + {:match, 41, {:tuple, 41, [{:atom, 41, :ok}, {:var, 41, :_file@1}]}, + {:call, 41, {:remote, 41, {:atom, 41, File}, {:atom, 41, :open}}, + [ + {:bin, 41, + [{:bin_element, 41, {:string, 41, 'sample'}, :default, :default}]}, + {:cons, 41, {:atom, 41, :utf8}, {:cons, 41, {:atom, 41, :write}, {nil, 0}}} + ]}}, + {:try, 43, + [ + {:call, 44, {:remote, 44, {:atom, 44, IO}, {:atom, 44, :write}}, + [ + {:var, 44, :_file@1}, + {:bin, 44, + [ + {:bin_element, 44, {:string, 44, [111, 108, 195, 161]}, :default, + :default} + ]} + ]}, + {:call, 45, {:remote, 45, {:atom, 45, :erlang}, {:atom, 45, :error}}, + [ + {:call, 45, + {:remote, 45, {:atom, 45, RuntimeError}, {:atom, 45, :exception}}, + [ + {:bin, 45, + [ + {:bin_element, 45, {:string, 45, 'oops, something went wrong'}, + :default, :default} + ]} + ]} + ]} + ], [], [], + [ + {:call, 47, {:remote, 47, {:atom, 47, File}, {:atom, 47, :close}}, + [{:var, 47, :_file@1}]} + ]} + ]} + ]} = try_after + + assert {:function, 51, :body_after, 0, + [ + {:clause, 51, [], [], + [ + {:try, 51, + [ + {:call, 52, {:remote, 52, {:atom, 52, :erlang}, {:atom, 52, :error}}, + [ + {:call, 52, {:remote, 52, {:atom, 52, Kernel.Utils}, {:atom, 52, :raise}}, + [ + {:cons, 52, {:integer, 52, 49}, + {:cons, 52, {:integer, 52, 50}, {nil, 0}}} + ]} + ]}, + {:integer, 53, 1} + ], [], [], [{:op, 55, :-, {:integer, 55, 1}}]} + ]} + ]} = body_after + end + + test "map" do + {tokens, ast} = load("/Elixir.MapEx.beam", "/map.ex") + + [pattern_matching_str, pattern_matching, test_map_str, test_map, empty_map | _] = + AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() + + assert {:function, 2, :empty_map, 0, [{:clause, 2, [], [], [{:map, 3, []}]}]} = empty_map + + assert {:function, 6, :test_map, 0, + [ + {:clause, 6, [], [], + [ + {:map, 7, + [ + {:map_field_assoc, 7, {:atom, 7, :a}, {:integer, 7, 12}}, + {:map_field_assoc, 7, {:atom, 7, :b}, {:call, 7, {:atom, 7, :empty_map}, []}} + ]} + ]} + ]} = test_map + + assert {:function, 10, :test_map_str, 0, + [ + {:clause, 10, [], [], + [ + {:map, 11, + [ + {:map_field_assoc, 11, + {:bin, 11, [{:bin_element, 11, {:string, 11, 'a'}, :default, :default}]}, + {:integer, 11, 12}}, + {:map_field_assoc, 11, + {:bin, 11, [{:bin_element, 11, {:string, 11, 'b'}, :default, :default}]}, + {:integer, 11, 0}} + ]} + ]} + ]} = test_map_str + + assert {:function, 14, :pattern_matching, 0, + [ + {:clause, 14, [], [], + [ + {:match, 15, + {:map, 15, [{:map_field_exact, 15, {:atom, 15, :a}, {:var, 15, :_a@1}}]}, + {:call, 15, {:atom, 15, :test_map}, []}}, + {:match, 16, + {:map, 16, [{:map_field_exact, 16, {:atom, 16, :b}, {:var, 16, :_a@1}}]}, + {:call, 16, {:atom, 16, :test_map}, []}} + ]} + ]} = pattern_matching + + assert {:function, 19, :pattern_matching_str, 0, + [ + {:clause, 19, [], [], + [ + {:match, 20, + {:map, 20, + [ + {:map_field_exact, 20, + {:bin, 20, [{:bin_element, 20, {:string, 20, 'a'}, :default, :default}]}, + {:var, 20, :_a@1}} + ]}, {:call, 20, {:atom, 20, :test_map}, []}} + ]} + ]} = pattern_matching_str + end + + test "struct" do + {tokens, ast} = load("/struct/Elixir.StructEx.beam", "/struct/struct.ex") + + [get2, get, update, empty, struct | _] = + AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() + + assert {:function, 8, :update, 0, + [ + {:clause, 8, [], [], + [ + {:map, 9, {:call, 9, {:atom, 9, :empty}, []}, + [{:map_field_exact, 9, {:atom, 9, :x}, {:integer, 9, 13}}]} + ]} + ]} = update + + assert {:function, 16, :get2, 0, + [ + {:clause, 16, [], [], + [ + {:match, 17, {:var, 17, :_x@1}, + {:case, [generated: true, location: 17], {:call, 17, {:atom, 17, :update}, []}, + [ + {:clause, [generated: true, location: 17], + [ + {:map, 17, + [ + {:map_field_exact, 17, {:atom, 17, :x}, + {:var, [generated: true, location: 17], :_@1}} + ]} + ], [], [{:var, [generated: true, location: 17], :_@1}]}, + {:clause, [generated: true, location: 17], + [{:var, [generated: true, location: 17], :_@1}], + [ + [ + {:call, [generated: true, location: 17], + {:remote, [generated: true, location: 17], + {:atom, [generated: true, location: 17], :erlang}, + {:atom, [generated: true, location: 17], :is_map}}, + [{:var, [generated: true, location: 17], :_@1}]} + ] + ], + [ + {:call, 17, {:remote, 17, {:atom, 17, :erlang}, {:atom, 17, :error}}, + [ + {:tuple, 17, + [ + {:atom, 17, :badkey}, + {:atom, 17, :x}, + {:var, [generated: true, location: 17], :_@1} + ]} + ]} + ]}, + {:clause, [generated: true, location: 17], + [{:var, [generated: true, location: 17], :_@1}], [], + [ + {:call, [generated: true, location: 17], + {:remote, [generated: true, location: 17], + {:var, [generated: true, location: 17], :_@1}, {:atom, 17, :x}}, []} + ]} + ]}} + ]} + ]} = get2 + + assert {:function, 12, :get, 0, + [ + {:clause, 12, [], [], + [ + {:match, 13, + {:map, 13, + [ + {:map_field_exact, 13, {:atom, 13, :__struct__}, {:atom, 13, StructEx}}, + {:map_field_exact, 13, {:atom, 13, :x}, {:var, 13, :_x@1}} + ]}, {:call, 13, {:atom, 13, :update}, []}} + ]} + ]} = get + + assert {:function, 4, :empty, 0, + [ + {:clause, 4, [], [], + [ + {:map, 5, + [ + {:map_field_assoc, 5, {:atom, 5, :__struct__}, {:atom, 5, StructEx}}, + {:map_field_assoc, 5, {:atom, 5, :x}, {:integer, 5, 0}}, + {:map_field_assoc, 5, {:atom, 5, :y}, {:integer, 5, 0}} + ]} + ]} + ]} = empty + + assert {:function, 2, :__struct__, 1, + [ + {:clause, 2, [{:var, 2, :_@1}], [], + [ + {:call, 2, {:remote, 2, {:atom, 2, Enum}, {:atom, 2, :reduce}}, + [ + {:var, 2, :_@1}, + {:map, 2, + [ + {:map_field_assoc, 2, {:atom, 2, :__struct__}, {:atom, 2, StructEx}}, + {:map_field_assoc, 2, {:atom, 2, :x}, {:integer, 2, 0}}, + {:map_field_assoc, 2, {:atom, 2, :y}, {:integer, 2, 0}} + ]}, + {:fun, 2, + {:clauses, + [ + {:clause, 2, + [{:tuple, 2, [{:var, 2, :_@2}, {:var, 2, :_@3}]}, {:var, 2, :_@4}], [], + [ + {:call, 2, {:remote, 2, {:atom, 2, :maps}, {:atom, 2, :update}}, + [{:var, 2, :_@2}, {:var, 2, :_@3}, {:var, 2, :_@4}]} + ]} + ]}} + ]} + ]} + ]} = struct + end + + test "record" do + {tokens, ast} = load("/record/Elixir.RecordEx.beam", "/record/record.ex") + + [update, init, empty, macro3, macro2, macro1 | _] = + AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() + + assert {:function, 7, :empty, 0, + [ + {:clause, 7, [], [], + [{:tuple, 8, [{:atom, 8, :record_ex}, {:integer, 8, 0}, {:integer, 8, 0}]}]} + # FIXME Should be a tuple with line 8, not 12. The line is taken from a token that is in another scope. Related to the cutting out tokens at the bottom + ]} = empty + + assert {:function, 11, :init, 0, + [ + {:clause, 11, [], [], + [{:tuple, 12, [{:atom, 12, :record_ex}, {:integer, 12, 1}, {:integer, 12, 0}]}]} + ]} = init + + assert {:function, 5, :"MACRO-record_ex", 1, + [ + {:clause, 5, [{:var, 5, :_@CALLER}], [], + [ + {:match, 5, {:var, 5, :__CALLER__}, + {:call, 5, {:remote, 5, {:atom, 5, :elixir_env}, {:atom, 5, :linify}}, + [{:var, 5, :_@CALLER}]}}, + {:call, 5, {:atom, 5, :"MACRO-record_ex"}, [{:var, 5, :__CALLER__}, {nil, 0}]} + ]} + ]} = macro1 + + assert {:function, 5, :"MACRO-record_ex", 2, + [ + {:clause, 5, [{:var, 5, :_@CALLER}, {:var, 5, :_@1}], [], + [ + {:match, 5, {:var, 5, :__CALLER__}, + {:call, 5, {:remote, 5, {:atom, 5, :elixir_env}, {:atom, 5, :linify}}, + [{:var, 5, :_@CALLER}]}}, + {:call, 5, {:remote, 5, {:atom, 5, Record}, {:atom, 5, :__access__}}, + [ + {:atom, 5, :record_ex}, + {:cons, 5, {:tuple, 5, [{:atom, 5, :x}, {:integer, 5, 0}]}, + {:cons, 5, {:tuple, 5, [{:atom, 5, :y}, {:integer, 5, 0}]}, {nil, 0}}}, + {:var, 5, :_@1}, + {:var, 5, :__CALLER__} + ]} + ]} + ]} = macro2 + + assert {:function, 5, :"MACRO-record_ex", 3, + [ + {:clause, 5, [{:var, 5, :_@CALLER}, {:var, 5, :_@1}, {:var, 5, :_@2}], [], + [ + {:match, 5, {:var, 5, :__CALLER__}, + {:call, 5, {:remote, 5, {:atom, 5, :elixir_env}, {:atom, 5, :linify}}, + [{:var, 5, :_@CALLER}]}}, + {:call, 5, {:remote, 5, {:atom, 5, Record}, {:atom, 5, :__access__}}, + [ + {:atom, 5, :record_ex}, + {:cons, 5, {:tuple, 5, [{:atom, 5, :x}, {:integer, 5, 0}]}, + {:cons, 5, {:tuple, 5, [{:atom, 5, :y}, {:integer, 5, 0}]}, {nil, 0}}}, + {:var, 5, :_@1}, + {:var, 5, :_@2}, + {:var, 5, :__CALLER__} + ]} + ]} + ]} = macro3 + + assert {:function, 16, :update, 1, + [ + {:clause, 16, [{:var, 16, :_record@1}], [], + [ + {:call, 17, {:remote, 17, {:atom, 17, :erlang}, {:atom, 17, :setelement}}, + [ + {:integer, 17, 2}, + {:call, 17, {:remote, 17, {:atom, 17, :erlang}, {:atom, 17, :setelement}}, + [{:integer, 17, 3}, {:var, 17, :_record@1}, {:integer, 17, 3}]}, + {:integer, 17, 2} + ]} + ]} + ]} = update + end + + test "receive" do + {tokens, ast} = load("/Elixir.Receive.beam", "/receive.ex") + + [recv, recv2 | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() + + assert {:function, 2, :recv2, 0, + [ + {:clause, 2, [], [], + [ + {:call, 3, {:remote, 3, {:atom, 3, :erlang}, {:atom, 3, :send}}, + [ + {:call, 3, {:remote, 3, {:atom, 3, :erlang}, {:atom, 3, :self}}, []}, + {:tuple, 3, + [ + {:atom, 3, :hello}, + {:bin, 3, [{:bin_element, 3, {:string, 3, 'All'}, :default, :default}]} + ]} + ]}, + {:receive, 5, + [ + {:clause, 6, [{:tuple, 6, [{:atom, 6, :hello}, {:var, 6, :_to@1}]}], [], + [ + {:call, 7, {:remote, 7, {:atom, 7, IO}, {:atom, 7, :puts}}, + [ + {:bin, 7, + [ + {:bin_element, 7, {:string, 7, 'Hello, '}, :default, :default}, + {:bin_element, 7, {:var, 7, :_to@1}, :default, [:binary]} + ]} + ]} + ]}, + {:clause, 9, [{:atom, 9, :skip}], [], [{:atom, 10, :ok}]} + ], {:integer, 12, 1000}, + [ + {:call, 13, {:remote, 13, {:atom, 13, IO}, {:atom, 13, :puts}}, + [ + {:bin, 13, + [{:bin_element, 13, {:string, 13, 'Timeout'}, :default, :default}]} + ]} + ]} + ]} + ]} = recv2 + + assert {:function, 17, :recv, 0, + [ + {:clause, 17, [], [], + [{:receive, 18, [{:clause, 19, [{:atom, 19, :ok}], [], [{:atom, 19, :ok}]}]}]} + ]} = recv + end + + test "typespec" do + {tokens, ast} = load("/Elixir.Typespec.beam", "/typespec.ex") + + [atoms_type2, atoms_type, named_type, missing_type_arg, missing_type | _] = + AstSpecifier.run_mappers(ast, tokens) + |> filter_specs() + |> Enum.reverse() + + assert {:attribute, 17, :spec, + {{:atoms_type2, 1}, + [ + {:type, 17, :fun, + [ + {:type, 17, :product, + [{:type, 17, :union, [{:atom, 17, :ok}, {:atom, 17, :error}]}]}, + {:remote_type, 17, + [ + {:atom, 17, Unknown}, + {:atom, 17, :atom}, + [{:type, 17, :union, [{:atom, 17, :ok}, {:atom, 17, :error}]}] + ]} + ]} + ]}} = atoms_type2 + + assert {:attribute, 14, :spec, + {{:atoms_type, 1}, + [ + {:type, 14, :fun, + [ + {:type, 14, :product, + [{:type, 14, :union, [{:atom, 14, :ok}, {:atom, 14, :error}]}]}, + {:type, 14, :union, [{:atom, 14, :ok}, {:atom, 14, :error}]} + ]} + ]}} = atoms_type + + assert {:attribute, 11, :spec, + {{:named_type, 1}, + [ + {:type, 11, :fun, + [ + {:type, 11, :product, + [ + {:ann_type, 11, + [ + {:var, 11, :name}, + {:remote_type, 11, [{:atom, 11, Unknown}, {:atom, 11, :atom}, []]} + ]} + ]}, + {:type, 11, :atom, []} + ]} + ]}} = named_type + + assert {:attribute, 8, :spec, + {{:missing_type_arg, 0}, + [ + {:type, 8, :fun, + [ + {:type, 8, :product, []}, + {:user_type, 8, :mylist, + [{:remote_type, 8, [{:atom, 8, Unknown}, {:atom, 8, :atom}, []]}]} + ]} + ]}} = missing_type_arg + + assert {:attribute, 5, :spec, + {{:missing_type, 0}, + [ + {:type, 5, :fun, + [ + {:type, 5, :product, []}, + {:remote_type, 5, [{:atom, 5, Unknown}, {:atom, 5, :atom}, []]} + ]} + ]}} = missing_type + end + + # Helpers + + def filter_specs(ast) do + Enum.filter(ast, &match?({:attribute, _, :spec, _}, &1)) + end +end diff --git a/test/gradient/specify_erl_ast_test.exs b/test/gradient/specify_erl_ast_test.exs index 16121615..db021034 100644 --- a/test/gradient/specify_erl_ast_test.exs +++ b/test/gradient/specify_erl_ast_test.exs @@ -1,3 +1,4 @@ +<<<<<<< HEAD:test/gradient/specify_erl_ast_test.exs defmodule Gradient.SpecifyErlAstTest do use ExUnit.Case doctest Gradient.SpecifyErlAst @@ -7,65 +8,24 @@ defmodule Gradient.SpecifyErlAstTest do import Gradient.Utils @examples_path "test/examples" +======= +defmodule GradualizerEx.AstSpecifierTest do + use ExUnit.Case + doctest GradualizerEx.AstSpecifier - setup_all state do - {:ok, state} - end - - describe "get_conditional/1" do - test "case" do - {tokens, _ast} = load("/conditional/Elixir.Conditional.Case.beam", "/conditional/case.ex") - tokens = drop_tokens_to_line(tokens, 2) - opts = [end_line: -1] - assert {:case, _} = SpecifyErlAst.get_conditional(tokens, 4, opts) - - tokens = drop_tokens_to_line(tokens, 9) - assert {:case, _} = SpecifyErlAst.get_conditional(tokens, 10, opts) - end - - test "if" do - {tokens, _ast} = load("/conditional/Elixir.Conditional.If.beam", "/conditional/if.ex") - tokens = drop_tokens_to_line(tokens, 2) - opts = [end_line: -1] - assert {:if, _} = SpecifyErlAst.get_conditional(tokens, 4, opts) - - tokens = drop_tokens_to_line(tokens, 12) - assert {:if, _} = SpecifyErlAst.get_conditional(tokens, 13, opts) - end - - test "unless" do - {tokens, _ast} = - load("/conditional/Elixir.Conditional.Unless.beam", "/conditional/unless.ex") - - tokens = drop_tokens_to_line(tokens, 2) - opts = [end_line: -1] - assert {:unless, _} = SpecifyErlAst.get_conditional(tokens, 3, opts) - end - - test "cond" do - {tokens, _ast} = load("/conditional/Elixir.Conditional.Cond.beam", "/conditional/cond.ex") - - tokens = drop_tokens_to_line(tokens, 2) - opts = [end_line: -1] - assert {:cond, _} = SpecifyErlAst.get_conditional(tokens, 4, opts) + alias GradualizerEx.AstSpecifier - tokens = drop_tokens_to_line(tokens, 10) - assert {:cond, _} = SpecifyErlAst.get_conditional(tokens, 13, opts) - end + import GradualizerEx.TestHelpers +>>>>>>> 9a63ccd (Code refactoring, Add docs):test/gradient/ast_specifier_test.exs - test "with" do - {tokens, _ast} = load("/conditional/Elixir.Conditional.With.beam", "/conditional/with.ex") - - tokens = drop_tokens_to_line(tokens, 6) - opts = [end_line: -1] - assert {:with, _} = SpecifyErlAst.get_conditional(tokens, 7, opts) - end + setup_all state do + {:ok, state} end - describe "add_missing_loc_literals/2" do + describe "run_mappers/2" do test "messy test on simple_app" do {tokens, ast} = example_data() - new_ast = SpecifyErlAst.add_missing_loc_literals(ast, tokens) + new_ast = AstSpecifier.run_mappers(ast, tokens) assert is_list(new_ast) end @@ -73,7 +33,7 @@ defmodule Gradient.SpecifyErlAstTest do test "integer" do {tokens, ast} = load("/basic/Elixir.Basic.Int.beam", "/basic/int.ex") - [block, inline | _] = SpecifyErlAst.add_missing_loc_literals(ast, tokens) |> Enum.reverse() + [block, inline | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() assert {:function, 2, :int, 0, [{:clause, 2, [], [], [{:integer, 2, 1}]}]} = inline @@ -83,7 +43,7 @@ defmodule Gradient.SpecifyErlAstTest do test "float" do {tokens, ast} = load("/basic/Elixir.Basic.Float.beam", "/basic/float.ex") - [block, inline | _] = SpecifyErlAst.add_missing_loc_literals(ast, tokens) |> Enum.reverse() + [block, inline | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() assert {:function, 2, :float, 0, [{:clause, 2, [], [], [{:float, 2, 0.12}]}]} = inline assert {:function, 4, :float_block, 0, [{:clause, 4, [], [], [{:float, 5, 0.12}]}]} = block @@ -92,7 +52,7 @@ defmodule Gradient.SpecifyErlAstTest do test "atom" do {tokens, ast} = load("/basic/Elixir.Basic.Atom.beam", "/basic/atom.ex") - [block, inline | _] = SpecifyErlAst.add_missing_loc_literals(ast, tokens) |> Enum.reverse() + [block, inline | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() assert {:function, 2, :atom, 0, [{:clause, 2, [], [], [{:atom, 2, :ok}]}]} = inline @@ -102,7 +62,7 @@ defmodule Gradient.SpecifyErlAstTest do test "char" do {tokens, ast} = load("/basic/Elixir.Basic.Char.beam", "/basic/char.ex") - [block, inline | _] = SpecifyErlAst.add_missing_loc_literals(ast, tokens) |> Enum.reverse() + [block, inline | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() assert {:function, 2, :char, 0, [{:clause, 2, [], [], [{:integer, 2, 99}]}]} = inline @@ -112,7 +72,7 @@ defmodule Gradient.SpecifyErlAstTest do test "charlist" do {tokens, ast} = load("/basic/Elixir.Basic.Charlist.beam", "/basic/charlist.ex") - [block, inline | _] = SpecifyErlAst.add_missing_loc_literals(ast, tokens) |> Enum.reverse() + [block, inline | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() # TODO propagate location to each charlist element assert {:function, 2, :charlist, 0, @@ -137,7 +97,7 @@ defmodule Gradient.SpecifyErlAstTest do test "string" do {tokens, ast} = load("/basic/Elixir.Basic.String.beam", "/basic/string.ex") - [block, inline | _] = SpecifyErlAst.add_missing_loc_literals(ast, tokens) |> Enum.reverse() + [block, inline | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() assert {:function, 2, :string, 0, [ @@ -156,7 +116,7 @@ defmodule Gradient.SpecifyErlAstTest do {tokens, ast} = load("/Elixir.Tuple.beam", "/tuple.ex") [tuple_in_str2, tuple_in_str, tuple_in_list, _list_in_tuple, tuple | _] = - SpecifyErlAst.add_missing_loc_literals(ast, tokens) |> Enum.reverse() + AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() # FIXME assert {:function, 18, :tuple_in_str2, 0, @@ -264,7 +224,7 @@ defmodule Gradient.SpecifyErlAstTest do {tokens, ast} = load("/basic/Elixir.Basic.Binary.beam", "/basic/binary.ex") [complex2, complex, bin_block, bin | _] = - SpecifyErlAst.add_missing_loc_literals(ast, tokens) + AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() assert {:function, 13, :complex2, 0, @@ -333,7 +293,7 @@ defmodule Gradient.SpecifyErlAstTest do test "case conditional" do {tokens, ast} = load("/conditional/Elixir.Conditional.Case.beam", "/conditional/case.ex") - [block, inline | _] = SpecifyErlAst.add_missing_loc_literals(ast, tokens) |> Enum.reverse() + [block, inline | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() assert {:function, 2, :case_, 0, [ @@ -363,8 +323,7 @@ defmodule Gradient.SpecifyErlAstTest do test "if conditional" do {tokens, ast} = load("/conditional/Elixir.Conditional.If.beam", "/conditional/if.ex") - [block, inline, if_ | _] = - SpecifyErlAst.add_missing_loc_literals(ast, tokens) |> Enum.reverse() + [block, inline, if_ | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() assert {:function, 12, :if_block, 0, [ @@ -413,7 +372,7 @@ defmodule Gradient.SpecifyErlAstTest do {tokens, ast} = load("/conditional/Elixir.Conditional.Unless.beam", "/conditional/unless.ex") - [block | _] = SpecifyErlAst.add_missing_loc_literals(ast, tokens) |> Enum.reverse() + [block | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() assert { :function, @@ -438,7 +397,7 @@ defmodule Gradient.SpecifyErlAstTest do test "cond conditional" do {tokens, ast} = load("/conditional/Elixir.Conditional.Cond.beam", "/conditional/cond.ex") - [block, inline | _] = SpecifyErlAst.add_missing_loc_literals(ast, tokens) |> Enum.reverse() + [block, inline | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() assert {:function, 2, :cond_, 1, [ @@ -509,7 +468,7 @@ defmodule Gradient.SpecifyErlAstTest do test "with conditional" do {tokens, ast} = load("/conditional/Elixir.Conditional.With.beam", "/conditional/with.ex") - [block | _] = SpecifyErlAst.add_missing_loc_literals(ast, tokens) |> Enum.reverse() + [block | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() assert {:function, 6, :test_with, 0, [ @@ -543,7 +502,7 @@ defmodule Gradient.SpecifyErlAstTest do beam_file = "/Elixir.Basic.beam" {tokens, ast} = load(beam_file, ex_file) - specified_ast = SpecifyErlAst.add_missing_loc_literals(ast, tokens) + specified_ast = AstSpecifier.run_mappers(ast, tokens) IO.inspect(specified_ast) assert is_list(specified_ast) end @@ -554,10 +513,10 @@ defmodule Gradient.SpecifyErlAstTest do opts = [end_line: -1] assert {{:integer, 21, 12}, tokens} = - SpecifyErlAst.specify_line({:integer, 21, 12}, tokens, opts) + AstSpecifier.specify_line({:integer, 21, 12}, tokens, opts) assert {{:integer, 22, 12}, _tokens} = - SpecifyErlAst.specify_line({:integer, 20, 12}, tokens, opts) + AstSpecifier.specify_line({:integer, 20, 12}, tokens, opts) end test "cons_to_charlist/1" do @@ -565,17 +524,7 @@ defmodule Gradient.SpecifyErlAstTest do {:cons, 0, {:integer, 0, 49}, {:cons, 0, {:integer, 0, 48}, {:cons, 0, {:integer, 0, 48}, {nil, 0}}}} - assert '100' == SpecifyErlAst.cons_to_charlist(cons) - end - - test "get_list_from_tokens" do - tokens = example_string_tokens() - ts = drop_tokens_to_line(tokens, 4) - opts = [end_line: -1] - assert {:charlist, _} = SpecifyErlAst.get_list_from_tokens(ts, opts) - - ts = drop_tokens_to_line(ts, 6) - assert {:list, _} = SpecifyErlAst.get_list_from_tokens(ts, opts) + assert '100' == AstSpecifier.cons_to_charlist(cons) end describe "test that prints result" do @@ -583,7 +532,7 @@ defmodule Gradient.SpecifyErlAstTest do test "specify/1" do {_tokens, forms} = example_data() - SpecifyErlAst.specify(forms) + AstSpecifier.specify(forms) |> IO.inspect() end @@ -597,7 +546,7 @@ defmodule Gradient.SpecifyErlAstTest do test "function call" do {tokens, ast} = load("/Elixir.Call.beam", "/call.ex") - [call, _ | _] = SpecifyErlAst.add_missing_loc_literals(ast, tokens) |> Enum.reverse() + [call, _ | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() assert {:function, 5, :call, 0, [ @@ -617,7 +566,7 @@ defmodule Gradient.SpecifyErlAstTest do test "pipe" do {tokens, ast} = load("/Elixir.Pipe.beam", "/pipe_op.ex") - [block | _] = SpecifyErlAst.add_missing_loc_literals(ast, tokens) |> Enum.reverse() + [block | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() assert {:function, 2, :pipe, 0, [ @@ -649,8 +598,7 @@ defmodule Gradient.SpecifyErlAstTest do test "guards" do {tokens, ast} = load("/conditional/Elixir.Conditional.Guard.beam", "/conditional/guards.ex") - [guarded_case, guarded_fun | _] = - SpecifyErlAst.add_missing_loc_literals(ast, tokens) |> Enum.reverse() + [guarded_case, guarded_fun | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() assert {:function, 3, :guarded_fun, 1, [ @@ -691,7 +639,7 @@ defmodule Gradient.SpecifyErlAstTest do {tokens, ast} = load("/Elixir.RangeEx.beam", "/range.ex") [to_list, match_range, rev_range_step, range_step, range | _] = - SpecifyErlAst.add_missing_loc_literals(ast, tokens) |> Enum.reverse() + AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() assert {:function, 18, :to_list, 0, [ @@ -771,7 +719,7 @@ defmodule Gradient.SpecifyErlAstTest do test "list comprehension" do {tokens, ast} = load("/Elixir.ListComprehension.beam", "/list_comprehension.ex") - [block | _] = SpecifyErlAst.add_missing_loc_literals(ast, tokens) |> Enum.reverse() + [block | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() assert {:function, 2, :lc, 0, [ @@ -819,8 +767,7 @@ defmodule Gradient.SpecifyErlAstTest do test "list" do {tokens, ast} = load("/Elixir.ListEx.beam", "/list.ex") - [ht2, ht, list, _wrap | _] = - SpecifyErlAst.add_missing_loc_literals(ast, tokens) |> Enum.reverse() + [ht2, ht, list, _wrap | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() assert {:function, 5, :list, 0, [ @@ -861,7 +808,7 @@ defmodule Gradient.SpecifyErlAstTest do {tokens, ast} = load("/Elixir.Try.beam", "/try.ex") [body_after, try_after, try_else, try_rescue | _] = - SpecifyErlAst.add_missing_loc_literals(ast, tokens) |> Enum.reverse() + AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() assert {:function, 2, :try_rescue, 0, [ @@ -1042,7 +989,7 @@ defmodule Gradient.SpecifyErlAstTest do {tokens, ast} = load("/Elixir.MapEx.beam", "/map.ex") [pattern_matching_str, pattern_matching, test_map_str, test_map, empty_map | _] = - SpecifyErlAst.add_missing_loc_literals(ast, tokens) |> Enum.reverse() + AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() assert {:function, 2, :empty_map, 0, [{:clause, 2, [], [], [{:map, 3, []}]}]} = empty_map @@ -1106,7 +1053,7 @@ defmodule Gradient.SpecifyErlAstTest do {tokens, ast} = load("/struct/Elixir.StructEx.beam", "/struct/struct.ex") [get2, get, update, empty, struct | _] = - SpecifyErlAst.add_missing_loc_literals(ast, tokens) |> Enum.reverse() + AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() assert {:function, 8, :update, 0, [ @@ -1223,7 +1170,7 @@ defmodule Gradient.SpecifyErlAstTest do {tokens, ast} = load("/record/Elixir.RecordEx.beam", "/record/record.ex") [update, init, empty, macro3, macro2, macro1 | _] = - SpecifyErlAst.add_missing_loc_literals(ast, tokens) |> Enum.reverse() + AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() assert {:function, 7, :empty, 0, [ @@ -1304,7 +1251,7 @@ defmodule Gradient.SpecifyErlAstTest do test "receive" do {tokens, ast} = load("/Elixir.Receive.beam", "/receive.ex") - [recv, recv2 | _] = SpecifyErlAst.add_missing_loc_literals(ast, tokens) |> Enum.reverse() + [recv, recv2 | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() assert {:function, 2, :recv2, 0, [ @@ -1355,7 +1302,7 @@ defmodule Gradient.SpecifyErlAstTest do {tokens, ast} = load("/Elixir.Typespec.beam", "/typespec.ex") [atoms_type2, atoms_type, named_type, missing_type_arg, missing_type | _] = - SpecifyErlAst.add_missing_loc_literals(ast, tokens) + AstSpecifier.run_mappers(ast, tokens) |> filter_specs() |> Enum.reverse() @@ -1430,62 +1377,4 @@ defmodule Gradient.SpecifyErlAstTest do def filter_specs(ast) do Enum.filter(ast, &match?({:attribute, _, :spec, _}, &1)) end - - @spec load(String.t(), String.t()) :: {list(), list()} - def load(beam_file, ex_file) do - beam_file = String.to_charlist(@examples_path <> beam_file) - ex_file = @examples_path <> ex_file - - code = - File.read!(ex_file) - |> String.to_charlist() - - {:ok, tokens} = - code - |> :elixir.string_to_tokens(1, 1, ex_file, []) - - {:ok, {_, [abstract_code: {:raw_abstract_v1, ast}]}} = - :beam_lib.chunks(beam_file, [:abstract_code]) - - ast = replace_file_path(ast, ex_file) - {tokens, ast} - end - - def example_data() do - beam_path = (@examples_path <> "/Elixir.SimpleApp.beam") |> String.to_charlist() - file_path = @examples_path <> "/simple_app.ex" - - code = - File.read!(file_path) - |> String.to_charlist() - - {:ok, tokens} = - code - |> :elixir.string_to_tokens(1, 1, file_path, []) - - {:ok, {SimpleApp, [abstract_code: {:raw_abstract_v1, ast}]}} = - :beam_lib.chunks(beam_path, [:abstract_code]) - - ast = replace_file_path(ast, file_path) - {tokens, ast} - end - - def example_string_tokens() do - file_path = @examples_path <> "/string_example.ex" - - code = - File.read!(file_path) - |> String.to_charlist() - - {:ok, tokens} = - code - |> :elixir.string_to_tokens(1, 1, file_path, []) - - tokens - end - - def replace_file_path([_ | forms], path) do - path = String.to_charlist(path) - [{:attribute, 1, :file, {path, 1}} | forms] - end end diff --git a/test/gradient/tokens_test.exs b/test/gradient/tokens_test.exs new file mode 100644 index 00000000..abfe456c --- /dev/null +++ b/test/gradient/tokens_test.exs @@ -0,0 +1,97 @@ +defmodule GradualizerEx.TokensTest do + use ExUnit.Case + doctest GradualizerEx.Tokens + + alias GradualizerEx.Tokens + + import GradualizerEx.TestHelpers + + test "drop_tokens_while" do + tokens = example_tokens() + + matcher = fn + {:atom, _, :ok} -> false + _ -> true + end + + assert [] = + Tokens.drop_tokens_while( + tokens, + 5, + matcher + ) + + refute [] == + Tokens.drop_tokens_while( + tokens, + 6, + matcher + ) + + refute [] == + Tokens.drop_tokens_while( + tokens, + matcher + ) + end + + test "get_list_from_tokens" do + tokens = example_string_tokens() + ts = Tokens.drop_tokens_to_line(tokens, 4) + opts = [end_line: -1] + assert {:charlist, _} = Tokens.get_list(ts, opts) + + ts = Tokens.drop_tokens_to_line(ts, 6) + assert {:list, _} = Tokens.get_list(ts, opts) + end + + describe "get_conditional/1" do + test "case" do + {tokens, _ast} = load("/conditional/Elixir.Conditional.Case.beam", "/conditional/case.ex") + tokens = Tokens.drop_tokens_to_line(tokens, 2) + opts = [end_line: -1] + assert {:case, _} = Tokens.get_conditional(tokens, 4, opts) + + tokens = Tokens.drop_tokens_to_line(tokens, 9) + assert {:case, _} = Tokens.get_conditional(tokens, 10, opts) + end + + test "if" do + {tokens, _ast} = load("/conditional/Elixir.Conditional.If.beam", "/conditional/if.ex") + tokens = Tokens.drop_tokens_to_line(tokens, 2) + opts = [end_line: -1] + assert {:if, _} = Tokens.get_conditional(tokens, 4, opts) + + tokens = Tokens.drop_tokens_to_line(tokens, 12) + assert {:if, _} = Tokens.get_conditional(tokens, 13, opts) + end + + test "unless" do + {tokens, _ast} = + load("/conditional/Elixir.Conditional.Unless.beam", "/conditional/unless.ex") + + tokens = Tokens.drop_tokens_to_line(tokens, 2) + opts = [end_line: -1] + assert {:unless, _} = Tokens.get_conditional(tokens, 3, opts) + end + + test "cond" do + {tokens, _ast} = load("/conditional/Elixir.Conditional.Cond.beam", "/conditional/cond.ex") + + tokens = Tokens.drop_tokens_to_line(tokens, 2) + opts = [end_line: -1] + assert {:cond, _} = Tokens.get_conditional(tokens, 4, opts) + + tokens = Tokens.drop_tokens_to_line(tokens, 10) + assert {:cond, _} = Tokens.get_conditional(tokens, 13, opts) + end + + test "with" do + {tokens, _ast} = load("/conditional/Elixir.Conditional.With.beam", "/conditional/with.ex") + + tokens = Tokens.drop_tokens_to_line(tokens, 6) + opts = [end_line: -1] + assert {:with, _} = Tokens.get_conditional(tokens, 7, opts) + end + end +end diff --git a/test/support/helpers.ex b/test/support/helpers.ex new file mode 100644 index 00000000..ceecc776 --- /dev/null +++ b/test/support/helpers.ex @@ -0,0 +1,80 @@ +defmodule GradualizerEx.TestHelpers do + alias GradualizerEx.Types, as: T + + @examples_path "test/examples" + + @spec load(String.t(), String.t()) :: {T.tokens(), T.forms()} + def load(beam_file, ex_file) do + beam_file = String.to_charlist(@examples_path <> beam_file) + ex_file = @examples_path <> ex_file + + code = + File.read!(ex_file) + |> String.to_charlist() + + {:ok, tokens} = + code + |> :elixir.string_to_tokens(1, 1, ex_file, []) + + {:ok, {_, [abstract_code: {:raw_abstract_v1, ast}]}} = + :beam_lib.chunks(beam_file, [:abstract_code]) + + ast = replace_file_path(ast, ex_file) + {tokens, ast} + end + + @spec example_data() :: {T.tokens(), T.forms()} + def example_data() do + beam_path = (@examples_path <> "/Elixir.SimpleApp.beam") |> String.to_charlist() + file_path = @examples_path <> "/simple_app.ex" + + code = + File.read!(file_path) + |> String.to_charlist() + + {:ok, tokens} = + code + |> :elixir.string_to_tokens(1, 1, file_path, []) + + {:ok, {SimpleApp, [abstract_code: {:raw_abstract_v1, ast}]}} = + :beam_lib.chunks(beam_path, [:abstract_code]) + + ast = replace_file_path(ast, file_path) + {tokens, ast} + end + + @spec example_tokens() :: T.tokens() + def example_tokens() do + file_path = @examples_path <> "/conditional/cond.ex" + + code = + File.read!(file_path) + |> String.to_charlist() + + {:ok, tokens} = + code + |> :elixir.string_to_tokens(1, 1, file_path, []) + + tokens + end + + @spec example_string_tokens() :: T.tokens() + def example_string_tokens() do + file_path = @examples_path <> "/string_example.ex" + + code = + File.read!(file_path) + |> String.to_charlist() + + {:ok, tokens} = + code + |> :elixir.string_to_tokens(1, 1, file_path, []) + + tokens + end + + defp replace_file_path([_ | forms], path) do + path = String.to_charlist(path) + [{:attribute, 1, :file, {path, 1}} | forms] + end +end From 5c301601ac9e44004ba77652346e27c7fc14465f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Mon, 11 Oct 2021 22:16:40 +0200 Subject: [PATCH 03/15] Add missing cases to spec_mapper --- lib/gradualizer_ex/ast_specifier.ex | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/gradualizer_ex/ast_specifier.ex b/lib/gradualizer_ex/ast_specifier.ex index 58f82dfd..be366848 100644 --- a/lib/gradualizer_ex/ast_specifier.ex +++ b/lib/gradualizer_ex/ast_specifier.ex @@ -461,6 +461,16 @@ defmodule GradualizerEx.AstSpecifier do Adds missing location to the function specification. """ @spec spec_mapper(form(), tokens(), options()) :: {form(), tokens()} + def spec_mapper({:type, anno, :tuple, :any}, tokens, _opts) do + {:type, anno, :tuple, :any} + |> pass_tokens(tokens) + end + + def spec_mapper({:type, anno, :any}, tokens, _opts) do + {:type, anno, :any} + |> pass_tokens(tokens) + end + def spec_mapper({:type, anno, type_name, args}, tokens, opts) do {:ok, _line, anno, opts, _} = get_line(anno, opts) new_args = context_mapper_map(args, tokens, opts, &spec_mapper/3) From 46c185d5aa47d5c889d858f3f9e37d1c76a486ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Mon, 11 Oct 2021 22:19:39 +0200 Subject: [PATCH 04/15] Add Elixir type pretty printer module --- examples/simple_app/lib/simple_app.ex | 1 - examples/simple_app/lib/simple_app/box.ex | 1 - lib/gradient/elixir_fmt.ex | 8 +- lib/gradualizer_ex/elixir_type.ex | 131 ++++++++++++++++++++++ 4 files changed, 136 insertions(+), 5 deletions(-) create mode 100644 lib/gradualizer_ex/elixir_type.ex diff --git a/examples/simple_app/lib/simple_app.ex b/examples/simple_app/lib/simple_app.ex index 6c3603fc..a196bf11 100644 --- a/examples/simple_app/lib/simple_app.ex +++ b/examples/simple_app/lib/simple_app.ex @@ -35,5 +35,4 @@ defmodule SimpleApp do l = fn -> :ok end l.() end - end diff --git a/examples/simple_app/lib/simple_app/box.ex b/examples/simple_app/lib/simple_app/box.ex index a24a5831..728443e3 100644 --- a/examples/simple_app/lib/simple_app/box.ex +++ b/examples/simple_app/lib/simple_app/box.ex @@ -1,5 +1,4 @@ defmodule SimpleApp.Box do - @spec get_l() :: {:ok, integer()} | :error def get_l, do: {:ok, 5} diff --git a/lib/gradient/elixir_fmt.ex b/lib/gradient/elixir_fmt.ex index 8ef70625..3cbc9796 100644 --- a/lib/gradient/elixir_fmt.ex +++ b/lib/gradient/elixir_fmt.ex @@ -5,6 +5,7 @@ defmodule Gradient.ElixirFmt do @behaviour Gradient.Fmt alias :gradualizer_fmt, as: FmtLib + alias GradualizerEx.ElixirType def print_errors(errors, opts) do for {file, e} <- errors do @@ -16,6 +17,7 @@ defmodule Gradient.ElixirFmt do def print_error(error, opts) do file = Keyword.get(opts, :filename) fmt_loc = Keyword.get(opts, :fmt_location, :verbose) + opts = Keyword.put(opts, :fmt_type_fun, &ElixirType.pretty_print/1) case file do nil -> :ok @@ -68,9 +70,9 @@ defmodule Gradient.ElixirFmt do IO.ANSI.blue() <> "#{inspect(expression)}" <> IO.ANSI.reset() end - def pp_type(expression, _opts) do - pp = expression |> :typelib.pp_type() |> to_string() - IO.ANSI.yellow() <> pp <> IO.ANSI.reset() + def pp_type(type, _opts) do + pp = ElixirType.pretty_print(type) + IO.ANSI.cyan() <> pp <> IO.ANSI.reset() end def try_highlight_in_context(expression, opts) do diff --git a/lib/gradualizer_ex/elixir_type.ex b/lib/gradualizer_ex/elixir_type.ex new file mode 100644 index 00000000..fb4248f9 --- /dev/null +++ b/lib/gradualizer_ex/elixir_type.ex @@ -0,0 +1,131 @@ +defmodule GradualizerEx.ElixirType do + @moduledoc """ + Module to format types. + + TODO records + FIXME add tests + """ + + @doc """ + Take type and prepare pretty string represntation. + """ + @spec pretty_print(tuple()) :: String.t() + def pretty_print({:remote_type, _, [{:atom, _, mod}, {:atom, _, type}, args]}) do + args_str = Enum.map(args, &pretty_print(&1)) |> Enum.join(", ") + type_str = Atom.to_string(type) + mod_str = parse_module(mod) + mod_str <> type_str <> "(#{args_str})" + end + + def pretty_print({:user_type, _, type, args}) do + args_str = Enum.map(args, &pretty_print(&1)) |> Enum.join(", ") + type_str = Atom.to_string(type) + "#{type_str}(#{args_str})" + end + + def pretty_print({:ann_type, _, [var_name, var_type]}) do + pretty_print(var_name) <> pretty_print(var_type) + end + + def pretty_print({:type, _, :map, :any}) do + "map()" + end + + def pretty_print({:type, _, :map, assocs}) do + assocs_str = Enum.map(assocs, &association_type(&1)) |> Enum.join(", ") + "%{" <> assocs_str <> "}" + end + + def pretty_print({:op, _, op, type}) do + Atom.to_string(op) <> pretty_print(type) + end + + def pretty_print({:op, _, op, left_type, right_type}) do + operator = " " <> Atom.to_string(op) <> " " + pretty_print(left_type) <> operator <> pretty_print(right_type) + end + + def pretty_print({:type, _, :fun, [{:type, _, :product, arg_types}, res_type]}) do + args = Enum.map(arg_types, &pretty_print(&1)) |> Enum.join(", ") + res = pretty_print(res_type) + "(" <> args <> " -> " <> res <> ")" + end + + def pretty_print({:type, _, :fun, [{:type, _, :any}, res_type]}) do + res = pretty_print(res_type) + "(... -> " <> res <> ")" + end + + def pretty_print({:type, _, :tuple, :any}) do + "tuple()" + end + + def pretty_print({:type, _, :tuple, elements}) do + elements_str = Enum.map(elements, &pretty_print(&1)) |> Enum.join(", ") + "{" <> elements_str <> "}" + end + + def pretty_print({:atom, _, nil}) do + "nil" + end + + def pretty_print({:atom, _, val}) do + ":" <> Atom.to_string(val) + end + + def pretty_print({:integer, _, val}) do + Integer.to_string(val) + end + + def pretty_print({:type, _, :binary, _}) do + "binary()" + end + + def pretty_print({:type, _, nil, []}) do + "[]" + end + + def pretty_print({:type, _, :union, [{:atom, _, true}, {:atom, _, false}]}) do + "boolean()" + end + + def pretty_print({:type, _, :union, args}) do + args |> Enum.map(&pretty_print/1) |> Enum.join(" | ") + end + + def pretty_print({:type, _, type, args}) do + args_str = Enum.map(args, &pretty_print(&1)) |> Enum.join(", ") + Atom.to_string(type) <> "(#{args_str})" + end + + def pretty_print(type) do + "#{inspect(type)}" + end + + ###### + ### Private + ###### + + @spec parse_module(atom()) :: String.t() + defp parse_module(:elixir), do: "" + + defp parse_module(mod) do + case Atom.to_string(mod) do + "Elixir." <> mod_str -> mod_str <> "." + mod -> mod <> "." + end + end + + @spec association_type(tuple()) :: String.t() + defp association_type({:type, _, :map_field_assoc, [key, value]}) do + key_str = pretty_print(key) + value_str = pretty_print(value) + "optional(#{key_str}) => #{value_str}" + end + + defp association_type({:type, _, :map_field_exact, [key, value]}) do + key_str = pretty_print(key) + value_str = pretty_print(value) + "required(#{key_str}) => #{value_str}" + end +end From ca85bf2cbafb804231e05c4b8a9c710668cda391 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Mon, 11 Oct 2021 22:57:59 +0200 Subject: [PATCH 05/15] Add format_type_error cases for undef --- lib/gradient/elixir_fmt.ex | 73 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/lib/gradient/elixir_fmt.ex b/lib/gradient/elixir_fmt.ex index 3cbc9796..666a61fe 100644 --- a/lib/gradient/elixir_fmt.ex +++ b/lib/gradient/elixir_fmt.ex @@ -34,6 +34,79 @@ defmodule Gradient.ElixirFmt do format_expr_type_error(expression, actual_type, expected_type, opts) end + def format_type_error({:call_undef, anno, module, func, arity}, opts) do + :io_lib.format( + "~sCall to undefined function ~p:~p/~p~s~n", + [ + format_location(anno, :brief, opts), + module, + func, + arity, + format_location(anno, :verbose, opts) + ] + ) + end + + def format_type_error({:undef, :record, anno, {module, recName}}, opts) do + :io_lib.format( + "~sUndefined record ~p:~p~s~n", + [ + format_location(anno, :brief, opts), + module, + recName, + format_location(anno, :verbose, opts) + ] + ) + end + + def format_type_error({:undef, :record, anno, recName}, opts) do + :io_lib.format( + "~sUndefined record ~p~s~n", + [format_location(anno, :brief, opts), recName, format_location(anno, :verbose, opts)] + ) + end + + def format_type_error({:undef, :record_field, fieldName}, opts) do + :io_lib.format( + "~sUndefined record field ~s~s~n", + [ + format_location(fieldName, :brief, opts), + pp_expr(fieldName, opts), + format_location(fieldName, :verbose, opts) + ] + ) + end + + def format_type_error({:undef, :user_type, anno, {name, arity}}, opts) do + :io_lib.format( + "~sUndefined type ~p/~p~s~n", + [format_location(anno, :brief, opts), name, arity, format_location(anno, :verbose, opts)] + ) + end + + def format_type_error({:undef, type, anno, {module, name, arity}}, opts) + when type in [:user_type, :remote_type] do + type = + case type do + :user_type -> "type" + :remote_type -> "remote type" + end + + module = "#{inspect(module)}" + + :io_lib.format( + "~sUndefined ~s ~s:~p/~p~s~n", + [ + format_location(anno, :brief, opts), + type, + module, + name, + arity, + format_location(anno, :verbose, opts) + ] + ) + end + def format_type_error(error, opts) do :gradualizer_fmt.format_type_error(error, opts) ++ '\n' end From d19cf30ff9a76ee162c2ccda11d1b5ac752be87b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Tue, 12 Oct 2021 22:16:22 +0200 Subject: [PATCH 06/15] Add missing spec mapper for empty map --- lib/gradualizer_ex/ast_specifier.ex | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/gradualizer_ex/ast_specifier.ex b/lib/gradualizer_ex/ast_specifier.ex index be366848..593dfdfd 100644 --- a/lib/gradualizer_ex/ast_specifier.ex +++ b/lib/gradualizer_ex/ast_specifier.ex @@ -466,6 +466,11 @@ defmodule GradualizerEx.AstSpecifier do |> pass_tokens(tokens) end + def spec_mapper({:type, anno, :map, :any}, tokens, _opts) do + {:type, anno, :map, :any} + |> pass_tokens(tokens) + end + def spec_mapper({:type, anno, :any}, tokens, _opts) do {:type, anno, :any} |> pass_tokens(tokens) From a0db683a35120b2bd242c4dc048c66099b21bfb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Tue, 12 Oct 2021 22:19:26 +0200 Subject: [PATCH 07/15] Extract pp for boolean from atoms --- lib/gradualizer_ex/elixir_type.ex | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/gradualizer_ex/elixir_type.ex b/lib/gradualizer_ex/elixir_type.ex index fb4248f9..eaa66ad9 100644 --- a/lib/gradualizer_ex/elixir_type.ex +++ b/lib/gradualizer_ex/elixir_type.ex @@ -69,6 +69,10 @@ defmodule GradualizerEx.ElixirType do "nil" end + def pretty_print({:atom, _, val}) when val in [true, false] do + Atom.to_string(val) + end + def pretty_print({:atom, _, val}) do ":" <> Atom.to_string(val) end From 315ca2e6b101a976c130ad8796379d64801738ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Tue, 12 Oct 2021 22:47:52 +0200 Subject: [PATCH 08/15] Init tests --- lib/gradient/elixir_fmt.ex | 8 +++- test/examples/type/Elixir.WrongRet.beam | Bin 0 -> 4148 bytes test/examples/type/wrong_ret.ex | 49 ++++++++++++++++++++++++ test/gradient/elixir_fmt_test.exs | 24 ++++++++++++ 4 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 test/examples/type/Elixir.WrongRet.beam create mode 100644 test/examples/type/wrong_ret.ex diff --git a/lib/gradient/elixir_fmt.ex b/lib/gradient/elixir_fmt.ex index 666a61fe..088b0b10 100644 --- a/lib/gradient/elixir_fmt.ex +++ b/lib/gradient/elixir_fmt.ex @@ -17,7 +17,6 @@ defmodule Gradient.ElixirFmt do def print_error(error, opts) do file = Keyword.get(opts, :filename) fmt_loc = Keyword.get(opts, :fmt_location, :verbose) - opts = Keyword.put(opts, :fmt_type_fun, &ElixirType.pretty_print/1) case file do nil -> :ok @@ -25,7 +24,12 @@ defmodule Gradient.ElixirFmt do _ -> :io.format("~s: ", [file]) end - :io.put_chars(format_type_error(error, opts)) + :io.put_chars(format_error(error, opts)) + end + + def format_error(error, opts) do + opts = Keyword.put(opts, :fmt_type_fun, &ElixirType.pretty_print/1) + format_type_error(error, opts) end @impl Gradient.Fmt diff --git a/test/examples/type/Elixir.WrongRet.beam b/test/examples/type/Elixir.WrongRet.beam new file mode 100644 index 0000000000000000000000000000000000000000..472f47c988dae153606bca9bde48705589ff5a22 GIT binary patch literal 4148 zcmb`Kc~}$I7QiQAOITzGyR0Iz2?W82mI?zAf))f>lv*_jWPp%m@+Jucl@_F8L7rF^ zA%YSX6==n!w$HW*2vuu~$^$G^!BXHUbzdH!P@eA&#IID}KM#Aq@8q2Oo0Bv5p2?h? z8xj$<41!?ypy0spKw0cc2qHB?5M*x{EaWTrxaTGu6DLKXG76i`7bjwDwk}sD!};-Y z87kFDz(f+h5Y^?O5*$t7%21vjs*qr~Ov)DV7}P|$I6=n8#8Pb$Hvz|_8USjGFrHk9 zYNNQ2D^4;@LS^h(G85he2XjmgB$17S>nNbI1q4hZ*1_h=Fp=S(r{1s4eO{X{*Gl9H zrKr&#--^dDA<7k#F#+uj==~=0c@r(46Adg7O+xWGPHHkYlp6VH%5XXQrt92z0yI4h z!+8dOEKS6f%-NGnE)k-7b8-ejOpJ$u=T-rp8w`XP^+4Q#7?WTnM5Z~wBzr#%Q*#RJ zEf11Mq4BYJ@Gi7aUd}Le5-|d^R;3r2_7oy@A}o6_9aJJ@nl22H9wRI-FsH!ENC1o=62%D7 zCGJL{DLH@`17eKmnh?Z9DFMV35L1K&ZcL(>DKh~v1H^phE))ypegMn?utZpqgv&yu z3J?oGtPmE5AlAy0fLH=zqYiAVYz1T?Aa;nZ72#vA>;%LbkVObfLJ$Y#ARsn?ELIao z0a=2uG6^3iB?Sh^4j^ZQwVyyPL~3Ybn^qtNdw1%kUWD z!K@WY2yG373`7rz{%bZVSTW->3b-*4dmtnrOVu_|kSh>7!T`e=i0zC8K}&$PQQM1w zChkLwML-tLShL@=RNI8Fh1#|P+FWfD*Ui*6!A#XQp@}sSgV07Z)~tT@+cN}=5Mmer zSukVG?v_v?y0m<09cjWyO`1MF-1t z%*@*06>W>e`Aq9}g~~aqJ9=+t33K2Gpqb#Rme%&Tfu5H`PCqv8h!|^{_Np59iOjFDtjl!`u&T}b_G`^h?*l$g zPTpk8Eu3)ooREu7$Yv93EDX7B&xcEjzc1F>csucB7D>CW{0qx6OJ&)4nq>#)LT;G- zkLPokvg|Q3bhWDnfx@r^A_tm4pg>SzfpWTm>2T>DoOawTN@*qRtM_Ax0D&Z1j0U%tt3}f>}M6FLDf@@1LIi7$D zAQnAV3hqsp;M-AAx>U$bql5Cq7bnq^a4t{I74o;EI2%>a!T*Phjw-k!P|l=uS-J$J z&(?TQ_&iZX3RvZu_#{3AX{2XIf*;X86cxJm>1qdY)0<~SK?m3D)9~p2&$AEP_H`E*KCdvTr>#2Ga&1+!b%953AD$jyvufMQ z{E9n%y?rBDzGEM^2Yco8TD=&4c4)bNjn*S&aofFPh0e`_Gdllt949|qb1LSVN2%et z&Z2E5P9)iLuf~G#-afM}@6a7|S^L6 zWlOSov{p(!N>9m144MjS`qCsKE-V?NJ`goB826g?Fw#6l14ka@UfbsIEA677O<{1+ zxxGQTaee>$v~!V9jm!%)@k-?3i4cGI`>o`!^+KPs^NNc)cI^6^>6PYv)N=K2^&PpI zl5$9_@p!8Aqmc1qhF{nPpVVi%m6vLc(dtU{)^CdZd9}029TkT`V=WmkZaqX6ldC2N zrrT={_gAb#3aF!h<9~JQ0p?UgExEoTOOSN-;(jgarQ8^rFJ(wOBi{X+5#!O}Ah#>w zL47en(!)g+eXcaBDi}IG@uTWkN|Dhf<>9Rwh|dmnkY44KKa4%dl*u@x~?Q z%9iW8FSsw)oejMo*YxzVJGF1`Gy^{;;T32~qsZQd?^$Iu2bp zc_TR0Crg?Hc^vQ5Qw`Qu4GO&yr!%M0FJv{0ZLIBce(bc&EXnULTQE5+V;8xGzC8z$LCyM#}206QPo9UUjaJTvskz?0) zFoZ`A3>Jmr&O4Ng+)W$lKcz}PjCT3%C;Z4GK~4YCeA;p85$?mP_I{>MZS|_qO%ogI z?3T+7V<#fzC)N#Yt!nP=*%~tte0{)8zGt%;Hufd&q3ZU6l!q}&dR@UbvtJ7*$+2uz zis_ZWZzhGiH&@>EiOI-mxYp3)ytHRBrc!m+I3^?Nh=29b$>TRlJ6LruC=9z;N*f`oX=2q|Dey>huQ_@(}6;cvYc~jd>r0UXkW#~mc*tG#aSl7Mg zkkfDPx~$AC552rI^Ppj*&l)RB{;eCE4?A{bPRXC7N5>Yvj0kWnAl^YNK?;hH5AhCa z*(fSXx&`mGdzokt`s9>rB;BcSpzN|&ke-~Lu7pcJfAEvj;-jhm*%1+P<$mFXapq*z zqsvUi+Cu+q|DtCRPsc+1^tZqJo^SmRBu?Fnkd~2{<7MuyrBz~G`W1Y z@D(2M@)Xh3bpO4CmU;_mn7w)gibeM<&5V?(z^ zfzkb2q2G`BnJ*t3&}qGGFm!HY*{0FpX6?_;K6%lQ-aXit*8EG8NAkiZO6C=-&sUUb zpCEIdX>a=d+v2>ci4nocy!L*HZ)GcoM(lnukIqk9?KG*#D{Szj3ldYKCHZDYF?d!{+cjvn;P|M-ZnMr!`XcH^ydTJd?L(w z6K4*A|6dHyP=)cu;G78g19>%w9-rvjNi?d}GVZ76skYQbR7a}w6Do~rO0}T6QN5@h LkpA4x41)d(fqRF% literal 0 HcmV?d00001 diff --git a/test/examples/type/wrong_ret.ex b/test/examples/type/wrong_ret.ex new file mode 100644 index 00000000..6fd794d4 --- /dev/null +++ b/test/examples/type/wrong_ret.ex @@ -0,0 +1,49 @@ +defmodule WrongRet do + @spec ret_wrong_atom() :: atom() + def ret_wrong_atom, do: 1 + + @spec ret_wrong_atom2() :: atom() + def ret_wrong_atom2, do: {:ok, []} + + @spec ret_wrong_atom3() :: atom() + def ret_wrong_atom3, do: %{a: 1} + + @spec ret_wrong_atom4() :: atom() + def ret_wrong_atom4, do: false + + @spec ret_wrong_integer() :: integer() + def ret_wrong_integer, do: 1.0 + + @spec ret_wrong_integer2() :: integer() + def ret_wrong_integer2, do: :ok + + @spec ret_wrong_integer3() :: integer() + def ret_wrong_integer3, do: true + + @spec ret_wrong_integer4() :: integer() + def ret_wrong_integer4, do: [1, 2, 3] + + @spec ret_out_of_range_int() :: 1..10 + def ret_out_of_range_int, do: 12 + + @spec ret_wrong_boolean() :: boolean() + def ret_wrong_boolean, do: :ok + + @spec ret_wrong_boolean2() :: boolean() + def ret_wrong_boolean2, do: "1234" + + @spec ret_wrong_boolean3() :: boolean() + def ret_wrong_boolean3, do: 1 + + @spec ret_wrong_boolean4() :: boolean() + def ret_wrong_boolean4, do: [a: 1, b: 2] + + @spec ret_wrong_keyword() :: keyword() + def ret_wrong_keyword, do: [1, 2, 3] + + @spec ret_wrong_tuple() :: tuple() + def ret_wrong_tuple, do: %{a: 1, b: 2} + + @spec ret_wrong_map() :: map() + def ret_wrong_map, do: {:a, 1, 2} +end diff --git a/test/gradient/elixir_fmt_test.exs b/test/gradient/elixir_fmt_test.exs index be7e7afb..b0f2b6a5 100644 --- a/test/gradient/elixir_fmt_test.exs +++ b/test/gradient/elixir_fmt_test.exs @@ -3,6 +3,8 @@ defmodule Gradient.ElixirFmtTest do doctest Gradient.ElixirFmt alias Gradient.ElixirFmt + alias Gradient.TestHelpers + alias Gradient.AstSpecifier @example_module_path "test/examples/simple_app.ex" @@ -18,6 +20,28 @@ defmodule Gradient.ElixirFmtTest do assert res == expected end + describe "types format" do + test "wrong return type" do + {_tokens, ast} = load("/type/Elixir.WrongRet.beam", "/type/wrong_ret.ex") + opts = [] + errors = type_check_file(ast, opts) + for e <- errors do + :io.put_chars(e) + end + + end + end + + def type_check_file(ast, opts) do + forms = AstSpecifier.specify(ast) + opts = Keyword.put(opts, :return_errors, true) + opts = Keyword.put(opts, :forms, forms) + + forms + |> :gradualizer.type_check_forms(opts) + |> Enum.map(fn {_, err} -> ElixirFmt.format_error(err, opts) end) + end + @tag :skip test "format_expr_type_error/4" do opts = [forms: basic_erlang_forms()] From 3b650a15818b630cbcc92380bd7d5daa0971044a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Tue, 9 Nov 2021 21:05:57 +0100 Subject: [PATCH 09/15] Fix names after rebase to gradient --- lib/gradient.ex | 6 +- .../ast_specifier.ex | 19 +- lib/gradient/elixir_fmt.ex | 2 +- .../elixir_type.ex | 2 +- lib/gradient/specify_erl_ast.ex | 789 ---------- lib/{gradualizer_ex => gradient}/tokens.ex | 4 +- lib/{gradualizer_ex => gradient}/types.ex | 2 +- lib/gradient/utils.ex | 100 -- lib/gradualizer_ex/type_annotation.ex | 20 - test/gradient/ast_specifier_test.exs | 20 +- test/gradient/elixir_fmt_test.exs | 2 +- test/gradient/specify_erl_ast_test.exs | 1380 ----------------- test/gradient/tokens_test.exs | 8 +- test/gradient/utils_test.exs | 50 - test/support/helpers.ex | 4 +- 15 files changed, 22 insertions(+), 2386 deletions(-) rename lib/{gradualizer_ex => gradient}/ast_specifier.ex (96%) rename lib/{gradualizer_ex => gradient}/elixir_type.ex (98%) delete mode 100644 lib/gradient/specify_erl_ast.ex rename lib/{gradualizer_ex => gradient}/tokens.ex (98%) rename lib/{gradualizer_ex => gradient}/types.ex (89%) delete mode 100644 lib/gradient/utils.ex delete mode 100644 lib/gradualizer_ex/type_annotation.ex delete mode 100644 test/gradient/specify_erl_ast_test.exs delete mode 100644 test/gradient/utils_test.exs diff --git a/lib/gradient.ex b/lib/gradient.ex index ca0c2551..13008752 100644 --- a/lib/gradient.ex +++ b/lib/gradient.ex @@ -7,9 +7,9 @@ defmodule Gradient do - `code_path` - Path to a file with code (e.g. when beam was compiled without project). """ - alias GradualizerEx.ElixirFileUtils - alias GradualizerEx.ElixirFmt - alias GradualizerEx.AstSpecifier + alias Gradient.ElixirFileUtils + alias Gradient.ElixirFmt + alias Gradient.AstSpecifier require Logger diff --git a/lib/gradualizer_ex/ast_specifier.ex b/lib/gradient/ast_specifier.ex similarity index 96% rename from lib/gradualizer_ex/ast_specifier.ex rename to lib/gradient/ast_specifier.ex index 593dfdfd..ab52a689 100644 --- a/lib/gradualizer_ex/ast_specifier.ex +++ b/lib/gradient/ast_specifier.ex @@ -1,8 +1,4 @@ -<<<<<<< HEAD:lib/gradient/specify_erl_ast.ex -defmodule Gradient.SpecifyErlAst do -======= -defmodule GradualizerEx.AstSpecifier do ->>>>>>> 9a63ccd (Code refactoring, Add docs):lib/gradualizer_ex/ast_specifier.ex +defmodule Gradient.AstSpecifier do @moduledoc """ Module adds missing location information to the Erlang abstract code produced from Elixir AST. Moreover it can be used to catch some ast pattern and replace @@ -48,15 +44,11 @@ defmodule GradualizerEx.AstSpecifier do - guards [X] """ -<<<<<<< HEAD:lib/gradient/specify_erl_ast.ex - import Gradient.Utils -======= - import GradualizerEx.Tokens ->>>>>>> 9a63ccd (Code refactoring, Add docs):lib/gradualizer_ex/ast_specifier.ex + import Gradient.Tokens require Logger - alias GradualizerEx.Types + alias Gradient.Types @type token :: Types.token() @type tokens :: Types.tokens() @@ -75,13 +67,8 @@ defmodule GradualizerEx.AstSpecifier do with {:attribute, line, :file, {path, _}} <- hd(forms), path <- to_string(path), {:ok, code} <- File.read(path), -<<<<<<< HEAD:lib/gradient/specify_erl_ast.ex {:ok, tokens} <- :elixir.string_to_tokens(String.to_charlist(code), line, line, path, []) do - add_missing_loc_literals(forms, tokens) -======= - {:ok, tokens} <- :elixir.string_to_tokens(String.to_charlist(code), 1, 1, path, []) do run_mappers(forms, tokens) ->>>>>>> 9a63ccd (Code refactoring, Add docs):lib/gradualizer_ex/ast_specifier.ex else error -> IO.puts("Error occured when specifying forms : #{inspect(error)}") diff --git a/lib/gradient/elixir_fmt.ex b/lib/gradient/elixir_fmt.ex index 088b0b10..2616e6ea 100644 --- a/lib/gradient/elixir_fmt.ex +++ b/lib/gradient/elixir_fmt.ex @@ -5,7 +5,7 @@ defmodule Gradient.ElixirFmt do @behaviour Gradient.Fmt alias :gradualizer_fmt, as: FmtLib - alias GradualizerEx.ElixirType + alias Gradient.ElixirType def print_errors(errors, opts) do for {file, e} <- errors do diff --git a/lib/gradualizer_ex/elixir_type.ex b/lib/gradient/elixir_type.ex similarity index 98% rename from lib/gradualizer_ex/elixir_type.ex rename to lib/gradient/elixir_type.ex index eaa66ad9..93177ec0 100644 --- a/lib/gradualizer_ex/elixir_type.ex +++ b/lib/gradient/elixir_type.ex @@ -1,4 +1,4 @@ -defmodule GradualizerEx.ElixirType do +defmodule Gradient.ElixirType do @moduledoc """ Module to format types. diff --git a/lib/gradient/specify_erl_ast.ex b/lib/gradient/specify_erl_ast.ex deleted file mode 100644 index 58f82dfd..00000000 --- a/lib/gradient/specify_erl_ast.ex +++ /dev/null @@ -1,789 +0,0 @@ -<<<<<<< HEAD:lib/gradient/specify_erl_ast.ex -defmodule Gradient.SpecifyErlAst do -======= -defmodule GradualizerEx.AstSpecifier do ->>>>>>> 9a63ccd (Code refactoring, Add docs):lib/gradualizer_ex/ast_specifier.ex - @moduledoc """ - Module adds missing location information to the Erlang abstract code produced - from Elixir AST. Moreover it can be used to catch some ast pattern and replace - it to forms that cannot be produced from Elixir directly. - - FIXME Optimize tokens searching. Find out why some tokens are dropped - - NOTE Mapper implements: - - function [x] - - fun [x] - - fun @spec [x] - - clause [x] - - case [x] - - block [X] - - pipe [x] - - call [x] (remote [X]) - - match [x] - - op [x] - - integer [x] - - float [x] - - string [x] - - charlist [x] - - tuple [X] - - var [X] - - list [X] - - keyword [X] - - binary [X] - - map [X] - - try [x] - - receive [X] - - record [X] elixir don't use it record_field, record_index, record_pattern, record - - named_fun [ ] is named_fun used by elixir? - - NOTE Elixir expressions to handle or test: - - list comprehension [X] - - binary [X] - - maps [X] - - struct [X] - - pipe [ ] TODO decide how to search for line in reversed form order - - range [X] - - receive [X] - - record [X] - - guards [X] - """ - -<<<<<<< HEAD:lib/gradient/specify_erl_ast.ex - import Gradient.Utils -======= - import GradualizerEx.Tokens ->>>>>>> 9a63ccd (Code refactoring, Add docs):lib/gradualizer_ex/ast_specifier.ex - - require Logger - - alias GradualizerEx.Types - - @type token :: Types.token() - @type tokens :: Types.tokens() - @type form :: Types.form() - @type forms :: Types.forms() - @type options :: Types.options() - - # Api - - @doc """ - Read and tokenize code file. Than run mappers on the given AST (with obtained tokens) - to specify missing locations or replace some parts that match pattern. - """ - @spec specify(nonempty_list(:erl_parse.abstract_form())) :: [:erl_parse.abstract_form()] - def specify(forms) do - with {:attribute, line, :file, {path, _}} <- hd(forms), - path <- to_string(path), - {:ok, code} <- File.read(path), -<<<<<<< HEAD:lib/gradient/specify_erl_ast.ex - {:ok, tokens} <- :elixir.string_to_tokens(String.to_charlist(code), line, line, path, []) do - add_missing_loc_literals(forms, tokens) -======= - {:ok, tokens} <- :elixir.string_to_tokens(String.to_charlist(code), 1, 1, path, []) do - run_mappers(forms, tokens) ->>>>>>> 9a63ccd (Code refactoring, Add docs):lib/gradualizer_ex/ast_specifier.ex - else - error -> - IO.puts("Error occured when specifying forms : #{inspect(error)}") - forms - end - end - - @doc """ - Function takes forms and traverse them in order to specify location or modify - forms matching the pattern. The tokens are required to obtain the missing location - as precise as possible. - """ - @spec run_mappers([:erl_parse.abstract_form()], tokens()) :: [ - :erl_parse.abstract_form() - ] - def run_mappers(forms, tokens) do - opts = [end_line: -1] - - {forms, _} = - forms - |> prepare_forms_order() - |> context_mapper_fold(tokens, opts) - - forms - end - - # Mappers - - @doc """ - Map over the forms using mapper and attach a context i.e. end line. - """ - @spec context_mapper_map(forms(), tokens(), options()) :: forms() - def context_mapper_map(forms, tokens, opts, mapper \\ &mapper/3) - def context_mapper_map([], _, _, _), do: [] - - def context_mapper_map([form | forms], tokens, opts, mapper) do - cur_opts = set_form_end_line(opts, form, forms) - {form, _} = mapper.(form, tokens, cur_opts) - [form | context_mapper_map(forms, tokens, opts, mapper)] - end - - @doc """ - Fold over the forms using mapper and attach a context i.e. end line. - """ - @spec context_mapper_fold(forms(), tokens(), options()) :: {forms(), tokens()} - def context_mapper_fold(forms, tokens, opts, mapper \\ &mapper/3) - def context_mapper_fold([], tokens, _, _), do: {[], tokens} - - def context_mapper_fold([form | forms], tokens, opts, mapper) do - cur_opts = set_form_end_line(opts, form, forms) - {form, new_tokens} = mapper.(form, tokens, cur_opts) - {forms, res_tokens} = context_mapper_fold(forms, new_tokens, opts, mapper) - {[form | forms], res_tokens} - end - - @doc """ - The main mapper function traverses AST and specifies missing locations - or replaces parts that match the pattern. - """ - @spec mapper(form(), [token()], options()) :: {form(), [token()]} - def mapper(form, tokens, opts) - - def mapper({:attribute, anno, :spec, {name_arity, specs}}, tokens, opts) do - new_specs = context_mapper_map(specs, [], opts, &spec_mapper/3) - - {:attribute, anno, :spec, {name_arity, new_specs}} - |> pass_tokens(tokens) - end - - def mapper({:function, _line, :__info__, _arity, _children} = form, tokens, _opts) do - # skip analysis for __info__ functions - pass_tokens(form, tokens) - end - - def mapper({:function, anno, name, arity, clauses}, tokens, opts) do - # anno has line - {clauses, tokens} = context_mapper_fold(clauses, tokens, opts) - - {:function, anno, name, arity, clauses} - |> pass_tokens(tokens) - end - - def mapper({:fun, anno, {:clauses, clauses}}, tokens, opts) do - # anno has line - {clauses, tokens} = context_mapper_fold(clauses, tokens, opts) - - {:fun, anno, {:clauses, clauses}} - |> pass_tokens(tokens) - end - - def mapper({:case, anno, condition, clauses}, tokens, opts) do - # anno has line - # NOTE In Elixir `if`, `case` and `cond` statements are represented - # as a `case` in abstract code. - {:ok, line, anno, opts, _} = get_line(anno, opts) - - opts = - case get_conditional(tokens, line, opts) do - {type, _} when type in [:case, :with] -> - Keyword.put(opts, :case_type, :case) - - {type, _} when type in [:cond, :if, :unless] -> - Keyword.put(opts, :case_type, :gen) - - :undefined -> - Keyword.put(opts, :case_type, :gen) - end - - {new_condition, tokens} = mapper(condition, tokens, opts) - - # NOTE use map because generated clauses can be in wrong order - clauses = context_mapper_map(clauses, tokens, opts) - - {:case, anno, new_condition, clauses} - |> pass_tokens(tokens) - end - - def mapper({:clause, anno, args, guards, children}, tokens, opts) do - # anno has line - # FIXME Handle generated clauses. Right now the literals inherit lines - # from the parents without checking them with tokens - - {:ok, line, anno, opts, _} = get_line(anno, opts) - case_type = Keyword.get(opts, :case_type, :case) - - tokens = drop_tokens_to_line(tokens, line) - - if case_type == :case do - {guards, tokens} = guards_mapper(guards, tokens, opts) - - {args, tokens} = - if not :erl_anno.generated(anno) do - context_mapper_fold(args, tokens, opts) - else - {args, tokens} - end - - {children, tokens} = children |> context_mapper_fold(tokens, opts) - - {:clause, anno, args, guards, children} - |> pass_tokens(tokens) - else - {children, tokens} = children |> context_mapper_fold(tokens, opts) - - {:clause, anno, args, guards, children} - |> pass_tokens(tokens) - end - end - - def mapper({:block, anno, body}, tokens, opts) do - # TODO check if anno has line - {:ok, _line, anno, opts, _} = get_line(anno, opts) - - {body, tokens} = context_mapper_fold(body, tokens, opts) - - {:block, anno, body} - |> pass_tokens(tokens) - end - - def mapper({:match, anno, left, right}, tokens, opts) do - {:ok, _, anno, opts, _} = get_line(anno, opts) - - {left, tokens} = mapper(left, tokens, opts) - {right, tokens} = mapper(right, tokens, opts) - - {:match, anno, left, right} - |> pass_tokens(tokens) - end - - def mapper({:map, anno, pairs}, tokens, opts) do - # anno has correct line - {:ok, _, anno, opts, _} = get_line(anno, opts) - - {pairs, tokens} = context_mapper_fold(pairs, tokens, opts, &map_element_mapper/3) - - {:map, anno, pairs} - |> pass_tokens(tokens) - end - - # update map pattern - def mapper({:map, anno, map, pairs}, tokens, opts) do - # anno has correct line - {:ok, _, anno, opts, _} = get_line(anno, opts) - - {map, tokens} = mapper(map, tokens, opts) - {pairs, tokens} = context_mapper_fold(pairs, tokens, opts, &map_element_mapper/3) - - {:map, anno, map, pairs} - |> pass_tokens(tokens) - end - - def mapper({:cons, anno, value, more} = cons, tokens, opts) do - # anno could be 0 - {:ok, line, anno, opts, _} = get_line(anno, opts) - - tokens = drop_tokens_to_line(tokens, line) - - case get_list(tokens, opts) do - {:list, tokens} -> - cons_mapper(cons, tokens, opts) - - {:keyword, tokens} -> - cons_mapper(cons, tokens, opts) - - {:charlist, tokens} -> - {:cons, anno, value, more} - |> specify_line(tokens, opts) - - :undefined -> - {form, _} = cons_mapper(cons, [], opts) - - pass_tokens(form, tokens) - end - end - - def mapper({:tuple, anno, elements}, tokens, opts) do - # anno could be 0 - {:ok, line, anno, opts, has_line?} = get_line(anno, opts) - - tokens - |> drop_tokens_to_line(line) - |> get_tuple(opts) - |> case do - {:tuple, tokens} -> - {anno, opts} = update_line_from_tokens(tokens, anno, opts, has_line?) - - {elements, tokens} = context_mapper_fold(elements, tokens, opts) - - {:tuple, anno, elements} - |> pass_tokens(tokens) - - :undefined -> - elements = context_mapper_map(elements, [], opts) - - {:tuple, anno, elements} - |> pass_tokens(tokens) - end - end - - def mapper({:receive, anno, clauses}, tokens, opts) do - # anno has correct line - {:ok, _, anno, opts, _} = get_line(anno, opts) - - {clauses, tokens} = context_mapper_fold(clauses, tokens, opts) - - {:receive, anno, clauses} - |> pass_tokens(tokens) - end - - # receive with timeout - def mapper({:receive, anno, clauses, after_val, after_block}, tokens, opts) do - # anno has correct line - {:ok, _, anno, opts, _} = get_line(anno, opts) - - {clauses, tokens} = context_mapper_fold(clauses, tokens, opts) - {after_val, tokens} = mapper(after_val, tokens, opts) - {after_block, tokens} = context_mapper_fold(after_block, tokens, opts) - - {:receive, anno, clauses, after_val, after_block} - |> pass_tokens(tokens) - end - - def mapper({:try, anno, body, else_block, catchers, after_block}, tokens, opts) do - # anno has correct line - {:ok, _, anno, opts, _} = get_line(anno, opts) - - {body, tokens} = context_mapper_fold(body, tokens, opts) - - {catchers, tokens} = context_mapper_fold(catchers, tokens, opts) - - {else_block, tokens} = context_mapper_fold(else_block, tokens, opts) - - {after_block, tokens} = context_mapper_fold(after_block, tokens, opts) - - {:try, anno, body, else_block, catchers, after_block} - |> pass_tokens(tokens) - end - - def mapper( - {:call, anno, {:atom, _, name_atom} = name, - [expr, {:bin, _, [{:bin_element, _, {:string, _, _} = val, :default, :default}]}]}, - tokens, - _opts - ) - when name_atom in [:"::", :":::"] do - # unwrap string from binary for correct type annotation matching - {:call, anno, name, [expr, val]} - |> pass_tokens(tokens) - end - - def mapper({:call, anno, name, args}, tokens, opts) do - # anno has correct line - {:ok, _, anno, opts, _} = get_line(anno, opts) - - name = remote_mapper(name) - - {args, tokens} = context_mapper_fold(args, tokens, opts) - - {:call, anno, name, args} - |> pass_tokens(tokens) - end - - def mapper({:op, anno, op, left, right}, tokens, opts) do - # anno has correct line - {:ok, _, anno, opts, _} = get_line(anno, opts) - - {left, tokens} = mapper(left, tokens, opts) - {right, tokens} = mapper(right, tokens, opts) - - {:op, anno, op, left, right} - |> pass_tokens(tokens) - end - - def mapper({:op, anno, op, right}, tokens, opts) do - # anno has correct line - {:ok, _, anno, opts, _} = get_line(anno, opts) - - {right, tokens} = mapper(right, tokens, opts) - - {:op, anno, op, right} - |> pass_tokens(tokens) - end - - def mapper({:bin, anno, elements}, tokens, opts) do - # anno could be 0 - {:ok, line, anno, opts, _} = get_line(anno, opts) - - # TODO find a way to merge this cases - case elements do - [{:bin_element, _, {:string, _, _}, :default, :default}] = e -> - {:bin, anno, e} - |> specify_line(tokens, opts) - - _ -> - {bin_tokens, other_tokens} = cut_tokens_to_bin(tokens, line) - bin_tokens = flat_tokens(bin_tokens) - {elements, _} = context_mapper_fold(elements, bin_tokens, opts, &bin_element_mapper/3) - - {:bin, anno, elements} - |> pass_tokens(other_tokens) - end - end - - def mapper({type, 0, value}, tokens, opts) - when type in [:atom, :char, :float, :integer, :string, :bin] do - # TODO check what happend for :string - {:ok, line} = Keyword.fetch(opts, :line) - - {type, line, value} - |> specify_line(tokens, opts) - end - - def mapper(skip, tokens, _opts) - when elem(skip, 0) in [ - :fun, - :attribute, - :var, - nil, - :atom, - :char, - :float, - :integer, - :string, - :bin - ] do - # NOTE fun - I skipped here checking &name/arity or &module.name/arity - # skip forms that don't need analysis and do not display warning - pass_tokens(skip, tokens) - end - - def mapper(form, tokens, _opts) do - Logger.warn("Not found mapper for #{inspect(form)}") - pass_tokens(form, tokens) - end - - @doc """ - Adds missing location to the function specification. - """ - @spec spec_mapper(form(), tokens(), options()) :: {form(), tokens()} - def spec_mapper({:type, anno, type_name, args}, tokens, opts) do - {:ok, _line, anno, opts, _} = get_line(anno, opts) - new_args = context_mapper_map(args, tokens, opts, &spec_mapper/3) - - {:type, anno, type_name, new_args} - |> pass_tokens(tokens) - end - - def spec_mapper({:remote_type, anno, [mod, type, args]}, tokens, opts) do - {:ok, _line, anno, opts, _} = get_line(anno, opts) - {new_mod, _} = spec_mapper(mod, tokens, opts) - {new_type, _} = spec_mapper(type, tokens, opts) - new_args = context_mapper_map(args, tokens, opts, &spec_mapper/3) - - {:remote_type, anno, [new_mod, new_type, new_args]} - |> pass_tokens(tokens) - end - - def spec_mapper({:user_type, anno, name, args}, tokens, opts) do - new_args = context_mapper_map(args, tokens, opts, &spec_mapper/3) - - {:user_type, anno, name, new_args} - |> pass_tokens(tokens) - end - - def spec_mapper({:ann_type, anno, attrs}, tokens, opts) do - new_attrs = context_mapper_map(attrs, tokens, opts, &spec_mapper/3) - - {:ann_type, anno, new_attrs} - |> pass_tokens(tokens) - end - - def spec_mapper(type, tokens, opts) do - mapper(type, tokens, opts) - end - - @doc """ - Adds missing location to the module literal - """ - def remote_mapper({:remote, line, {:atom, 0, mod}, fun}) do - {:remote, line, {:atom, line, mod}, fun} - end - - def remote_mapper(name), do: name - - @doc """ - Adds missing location to the literals in the guards - """ - @spec guards_mapper([form()], [token()], options()) :: {[form()], [token()]} - def guards_mapper([], tokens, _opts), do: {[], tokens} - - def guards_mapper(guards, tokens, opts) do - List.foldl(guards, {[], tokens}, fn - [guard], {gs, tokens} -> - {g, ts} = mapper(guard, tokens, opts) - {[[g] | gs], ts} - - gs, {ags, ts} -> - Logger.error("Unsupported guards format #{inspect(gs)}") - {gs ++ ags, ts} - end) - end - - @doc """ - Run mapper on map value and key. - """ - @spec map_element_mapper(tuple(), tokens(), options()) :: {tuple(), tokens()} - def map_element_mapper({field, anno, key, value}, tokens, opts) - when field in [:map_field_assoc, :map_field_exact] do - line = :erl_anno.line(anno) - opts = Keyword.put(opts, :line, line) - - {key, tokens} = mapper(key, tokens, opts) - {value, tokens} = mapper(value, tokens, opts) - - {field, anno, key, value} - |> pass_tokens(tokens) - end - - @doc """ - Run mapper on bin element value. - """ - @spec bin_element_mapper(tuple(), tokens(), options()) :: {tuple(), tokens()} - def bin_element_mapper({:bin_element, anno, value, size, tsl}, tokens, opts) do - {:ok, _line, anno, opts, _} = get_line(anno, opts) - - {value, tokens} = mapper(value, tokens, opts) - - {:bin_element, anno, value, size, tsl} - |> pass_tokens(tokens) - end - - @doc """ - Iterate over the list in abstract code format and runs mapper on each element - """ - @spec cons_mapper(form(), [token()], options()) :: {form(), tokens()} - def cons_mapper({:cons, anno, value, tail}, tokens, opts) do - {:ok, _, anno, opts, has_line?} = get_line(anno, opts) - - {anno, opts} = update_line_from_tokens(tokens, anno, opts, has_line?) - - {new_value, tokens} = mapper(value, tokens, opts) - - {tail, tokens} = cons_mapper(tail, tokens, opts) - - {:cons, anno, new_value, tail} - |> pass_tokens(tokens) - end - - def cons_mapper(other, tokens, opts), do: mapper(other, tokens, opts) - - @doc """ - Update form anno with location taken from the corresponding token, if found. - Otherwise return form unchanged. - """ - @spec specify_line(form(), [token()], options()) :: {form(), [token()]} - def specify_line(form, tokens, opts) do - if not :erl_anno.generated(elem(form, 1)) do - {:ok, end_line} = Keyword.fetch(opts, :end_line) - - res = drop_tokens_while(tokens, end_line, &(!match_token_to_form(&1, form))) - - case res do - [token | tokens] -> - {take_loc_from_token(token, form), tokens} - - [] -> - {form, tokens} - end - else - {form, tokens} - end - end - - # Private Helpers - - @spec match_token_to_form(token(), form()) :: boolean() - defp match_token_to_form({:int, {l1, _, v1}, _}, {:integer, l2, v2}) do - l2 = :erl_anno.line(l2) - l2 <= l1 && v1 == v2 - end - - defp match_token_to_form({:char, {l1, _, _}, v1}, {:integer, l2, v2}) do - l2 = :erl_anno.line(l2) - l2 <= l1 && v1 == v2 - end - - defp match_token_to_form({:flt, {l1, _, v1}, _}, {:float, l2, v2}) do - l2 = :erl_anno.line(l2) - l2 <= l1 && v1 == v2 - end - - defp match_token_to_form({:atom, {l1, _, _}, v1}, {:atom, l2, v2}) do - l2 = :erl_anno.line(l2) - l2 <= l1 && v1 == v2 - end - - defp match_token_to_form({:alias, {l1, _, _}, v1}, {:atom, l2, v2}) do - l2 = :erl_anno.line(l2) - l2 <= l1 && v1 == v2 - end - - defp match_token_to_form({:kw_identifier, {l1, _, _}, v1}, {:atom, l2, v2}) do - l2 = :erl_anno.line(l2) - l2 <= l1 && v1 == v2 - end - - defp match_token_to_form({:list_string, {l1, _, _}, [v1]}, {:cons, l2, _, _} = cons) do - v2 = cons_to_charlist(cons) - l2 <= l1 && to_charlist(v1) == v2 - end - - # BINARY - defp match_token_to_form( - {:bin_string, {l1, _, _}, [v1]}, - {:bin, l2, [{:bin_element, _, {:string, _, v2}, :default, :default}]} - ) do - # string - l2 <= l1 && :binary.bin_to_list(v1) == v2 - end - - defp match_token_to_form({:str, _, v}, {:string, _, v1}) do - to_charlist(v) == v1 - end - - defp match_token_to_form({true, {l1, _, _}}, {:atom, l2, true}) do - l2 <= l1 - end - - defp match_token_to_form({false, {l1, _, _}}, {:atom, l2, false}) do - l2 <= l1 - end - - defp match_token_to_form(_, _) do - false - end - - @spec take_loc_from_token(token(), form()) :: form() - defp take_loc_from_token({:int, {line, _, _}, _}, {:integer, _, value}) do - {:integer, line, value} - end - - defp take_loc_from_token({:char, {line, _, _}, _}, {:integer, _, value}) do - {:integer, line, value} - end - - defp take_loc_from_token({:flt, {line, _, _}, _}, {:float, _, value}) do - {:float, line, value} - end - - defp take_loc_from_token({:atom, {line, _, _}, _}, {:atom, _, value}) do - {:atom, line, value} - end - - defp take_loc_from_token({:alias, {line, _, _}, _}, {:atom, _, value}) do - {:atom, line, value} - end - - defp take_loc_from_token({:kw_identifier, {line, _, _}, _}, {:atom, _, value}) do - {:atom, line, value} - end - - defp take_loc_from_token({:list_string, {l1, _, _}, _}, {:cons, _, _, _} = charlist) do - charlist_set_loc(charlist, l1) - end - - defp take_loc_from_token( - {:bin_string, {l1, _, _}, _}, - {:bin, _, [{:bin_element, _, {:string, _, v2}, :default, :default}]} - ) do - {:bin, l1, [{:bin_element, l1, {:string, l1, v2}, :default, :default}]} - end - - defp take_loc_from_token({:str, _, _}, {:string, loc, v2}) do - {:string, loc, v2} - end - - defp take_loc_from_token({true, {line, _, _}}, {:atom, _, true}) do - {:atom, line, true} - end - - defp take_loc_from_token({false, {line, _, _}}, {:atom, _, false}) do - {:atom, line, false} - end - - defp take_loc_from_token(_, _), do: nil - - def cons_to_charlist({nil, _}), do: [] - - def cons_to_charlist({:cons, _, {:integer, _, value}, tail}) do - [value | cons_to_charlist(tail)] - end - - def charlist_set_loc({:cons, _, {:integer, _, value}, tail}, loc) do - {:cons, loc, {:integer, loc, value}, charlist_set_loc(tail, loc)} - end - - def charlist_set_loc({nil, loc}, _), do: {nil, loc} - - def put_line(anno, opts, line) do - {:erl_anno.set_line(line, anno), Keyword.put(opts, :line, line)} - end - - def update_line_from_tokens([token | _], anno, opts, false) do - line = get_line_from_token(token) - put_line(anno, opts, line) - end - - def update_line_from_tokens(_, anno, opts, _) do - {anno, opts} - end - - defp get_line(anno, opts) do - case :erl_anno.line(anno) do - 0 -> - case Keyword.fetch(opts, :line) do - {:ok, line} -> - anno = :erl_anno.set_line(line, anno) - {:ok, line, anno, opts, false} - - err -> - err - end - - line -> - opts = Keyword.put(opts, :line, line) - {:ok, line, anno, opts, true} - end - end - - @spec prepare_forms_order(forms()) :: forms() - defp prepare_forms_order(forms) do - forms - |> Enum.sort(fn l, r -> elem(l, 0) == elem(r, 0) and elem(l, 1) > elem(r, 1) end) - |> Enum.reverse() - end - - defp set_form_end_line(opts, form, forms) do - case Enum.find(forms, fn f -> - anno = elem(f, 1) - - # Maybe should try to go deeper when generated and try to obtain - # the line from the first child. It should work for sure for clauses, - # but it has to be in the right order (e.g. if clauses are reversed) - :erl_anno.line(anno) > 0 and not :erl_anno.generated(anno) - end) do - nil -> - opts - - next_form -> - current_line = :erl_anno.line(elem(form, 1)) - next_line = :erl_anno.line(elem(next_form, 1)) - - if current_line == next_line do - Keyword.put(opts, :end_line, next_line + 1) - else - Keyword.put(opts, :end_line, next_line) - end - end - end - - @spec pass_tokens(any(), tokens()) :: {any(), tokens()} - defp pass_tokens(form, tokens) do - {form, tokens} - end -end diff --git a/lib/gradualizer_ex/tokens.ex b/lib/gradient/tokens.ex similarity index 98% rename from lib/gradualizer_ex/tokens.ex rename to lib/gradient/tokens.ex index da15bb3a..16de816f 100644 --- a/lib/gradualizer_ex/tokens.ex +++ b/lib/gradient/tokens.ex @@ -1,8 +1,8 @@ -defmodule GradualizerEx.Tokens do +defmodule Gradient.Tokens do @moduledoc """ Group of functions helping with manage tokens. """ - alias GradualizerEx.Types, as: T + alias Gradient.Types, as: T @doc """ Drop tokens to the first conditional occurance. Returns type of the encountered diff --git a/lib/gradualizer_ex/types.ex b/lib/gradient/types.ex similarity index 89% rename from lib/gradualizer_ex/types.ex rename to lib/gradient/types.ex index 6d36aac9..d37595da 100644 --- a/lib/gradualizer_ex/types.ex +++ b/lib/gradient/types.ex @@ -1,4 +1,4 @@ -defmodule GradualizerEx.Types do +defmodule Gradient.Types do @type token :: tuple() @type tokens :: [tuple()] @type form :: diff --git a/lib/gradient/utils.ex b/lib/gradient/utils.ex deleted file mode 100644 index a1e1761a..00000000 --- a/lib/gradient/utils.ex +++ /dev/null @@ -1,100 +0,0 @@ -defmodule Gradient.Utils do - @moduledoc """ - Utility functions that helps obtain info about location from data - """ - - @doc """ - Drop tokens till the matcher returns false or the token's line exceeds the limit. - """ - def drop_tokens_while(tokens, limit_line \\ -1, matcher) - def drop_tokens_while([], _, _), do: [] - - def drop_tokens_while([token | tokens] = all, limit_line, matcher) do - line = get_line_from_token(token) - - limit_passed = limit_line < 0 or line < limit_line - - cond do - matcher.(token) and limit_passed -> - drop_tokens_while(tokens, limit_line, matcher) - - not limit_passed -> - [] - - true -> - all - end - end - - def drop_tokens_to_line(tokens, line) do - Enum.drop_while(tokens, fn t -> - elem(elem(t, 1), 0) < line - end) - end - - def get_line_from_token(token), do: elem(elem(token, 1), 0) - - def get_line_from_form(form) do - form - |> elem(1) - |> :erl_anno.line() - end - - def cut_tokens_to_bin(tokens, line) do - tokens = drop_tokens_to_line(tokens, line) - - drop_tokens_while(tokens, fn - {:"<<", _} -> false - {:bin_string, _, _} -> false - _ -> true - end) - |> case do - [{:"<<", _} | _] = ts -> cut_bottom(ts, 0) - [{:bin_string, _, _} = t | ts] -> {[t], ts} - [] -> {[], tokens} - end - end - - defp cut_bottom([{:"<<", _} = t | ts], deep) do - {ts, cut_ts} = cut_bottom(ts, deep + 1) - {[t | ts], cut_ts} - end - - defp cut_bottom([{:">>", _} = t | ts], deep) do - if deep - 1 > 0 do - {ts, cut_ts} = cut_bottom(ts, deep - 1) - {[t | ts], cut_ts} - else - {[t], ts} - end - end - - defp cut_bottom([t | ts], deep) do - {ts, cut_ts} = cut_bottom(ts, deep) - {[t | ts], cut_ts} - end - - def flat_tokens(tokens) do - Enum.map(tokens, &flat_token/1) - |> Enum.concat() - end - - def flat_token(token) do - case token do - {:bin_string, _, [s]} = t when is_binary(s) -> - [t] - - {:bin_string, _, ts} -> - flat_tokens(ts) - - {{_, _, nil}, {_, _, nil}, ts} -> - flat_tokens(ts) - - str when is_binary(str) -> - [{:str, {0, 0, nil}, str}] - - _otherwise -> - [token] - end - end -end diff --git a/lib/gradualizer_ex/type_annotation.ex b/lib/gradualizer_ex/type_annotation.ex deleted file mode 100644 index 043232ce..00000000 --- a/lib/gradualizer_ex/type_annotation.ex +++ /dev/null @@ -1,20 +0,0 @@ -defmodule GradualizerEx.TypeAnnotation do - defmacro annotate_type(expr, type) do - {:"::", [], [expr, Macro.to_string(type)]} - end - - defmacro assert_type(expr, type) do - {:":::", [], [expr, Macro.to_string(type)]} - end - - defmacro __using__(_) do - quote [] do - import GradualizerEx.TypeAnnotation - require GradualizerEx.TypeAnnotation - - @compile {:inline, "::": 2, ":::": 2} - def unquote(:"::")(expr, _type), do: expr - def unquote(:":::")(expr, _type), do: expr - end - end -end diff --git a/test/gradient/ast_specifier_test.exs b/test/gradient/ast_specifier_test.exs index db021034..7a178b65 100644 --- a/test/gradient/ast_specifier_test.exs +++ b/test/gradient/ast_specifier_test.exs @@ -1,22 +1,10 @@ -<<<<<<< HEAD:test/gradient/specify_erl_ast_test.exs -defmodule Gradient.SpecifyErlAstTest do +defmodule Gradient.AstSpecifierTest do use ExUnit.Case - doctest Gradient.SpecifyErlAst + doctest Gradient.AstSpecifier - alias Gradient.SpecifyErlAst + alias Gradient.AstSpecifier - import Gradient.Utils - - @examples_path "test/examples" -======= -defmodule GradualizerEx.AstSpecifierTest do - use ExUnit.Case - doctest GradualizerEx.AstSpecifier - - alias GradualizerEx.AstSpecifier - - import GradualizerEx.TestHelpers ->>>>>>> 9a63ccd (Code refactoring, Add docs):test/gradient/ast_specifier_test.exs + import Gradient.TestHelpers setup_all state do {:ok, state} diff --git a/test/gradient/elixir_fmt_test.exs b/test/gradient/elixir_fmt_test.exs index b0f2b6a5..c607983a 100644 --- a/test/gradient/elixir_fmt_test.exs +++ b/test/gradient/elixir_fmt_test.exs @@ -3,7 +3,7 @@ defmodule Gradient.ElixirFmtTest do doctest Gradient.ElixirFmt alias Gradient.ElixirFmt - alias Gradient.TestHelpers + import Gradient.TestHelpers alias Gradient.AstSpecifier @example_module_path "test/examples/simple_app.ex" diff --git a/test/gradient/specify_erl_ast_test.exs b/test/gradient/specify_erl_ast_test.exs deleted file mode 100644 index db021034..00000000 --- a/test/gradient/specify_erl_ast_test.exs +++ /dev/null @@ -1,1380 +0,0 @@ -<<<<<<< HEAD:test/gradient/specify_erl_ast_test.exs -defmodule Gradient.SpecifyErlAstTest do - use ExUnit.Case - doctest Gradient.SpecifyErlAst - - alias Gradient.SpecifyErlAst - - import Gradient.Utils - - @examples_path "test/examples" -======= -defmodule GradualizerEx.AstSpecifierTest do - use ExUnit.Case - doctest GradualizerEx.AstSpecifier - - alias GradualizerEx.AstSpecifier - - import GradualizerEx.TestHelpers ->>>>>>> 9a63ccd (Code refactoring, Add docs):test/gradient/ast_specifier_test.exs - - setup_all state do - {:ok, state} - end - - describe "run_mappers/2" do - test "messy test on simple_app" do - {tokens, ast} = example_data() - new_ast = AstSpecifier.run_mappers(ast, tokens) - - assert is_list(new_ast) - end - - test "integer" do - {tokens, ast} = load("/basic/Elixir.Basic.Int.beam", "/basic/int.ex") - - [block, inline | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() - - assert {:function, 2, :int, 0, [{:clause, 2, [], [], [{:integer, 2, 1}]}]} = inline - - assert {:function, 4, :int_block, 0, [{:clause, 4, [], [], [{:integer, 5, 2}]}]} = block - end - - test "float" do - {tokens, ast} = load("/basic/Elixir.Basic.Float.beam", "/basic/float.ex") - - [block, inline | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() - assert {:function, 2, :float, 0, [{:clause, 2, [], [], [{:float, 2, 0.12}]}]} = inline - - assert {:function, 4, :float_block, 0, [{:clause, 4, [], [], [{:float, 5, 0.12}]}]} = block - end - - test "atom" do - {tokens, ast} = load("/basic/Elixir.Basic.Atom.beam", "/basic/atom.ex") - - [block, inline | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() - - assert {:function, 2, :atom, 0, [{:clause, 2, [], [], [{:atom, 2, :ok}]}]} = inline - - assert {:function, 4, :atom_block, 0, [{:clause, 4, [], [], [{:atom, 5, :ok}]}]} = block - end - - test "char" do - {tokens, ast} = load("/basic/Elixir.Basic.Char.beam", "/basic/char.ex") - - [block, inline | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() - - assert {:function, 2, :char, 0, [{:clause, 2, [], [], [{:integer, 2, 99}]}]} = inline - - assert {:function, 4, :char_block, 0, [{:clause, 4, [], [], [{:integer, 5, 99}]}]} = block - end - - test "charlist" do - {tokens, ast} = load("/basic/Elixir.Basic.Charlist.beam", "/basic/charlist.ex") - - [block, inline | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() - - # TODO propagate location to each charlist element - assert {:function, 2, :charlist, 0, - [ - {:clause, 2, [], [], - [ - {:cons, 2, {:integer, 2, 97}, - {:cons, 2, {:integer, 2, 98}, {:cons, 2, {:integer, 2, 99}, {nil, 0}}}} - ]} - ]} = inline - - assert {:function, 4, :charlist_block, 0, - [ - {:clause, 4, [], [], - [ - {:cons, 5, {:integer, 5, 97}, - {:cons, 5, {:integer, 5, 98}, {:cons, 5, {:integer, 5, 99}, {nil, 0}}}} - ]} - ]} = block - end - - test "string" do - {tokens, ast} = load("/basic/Elixir.Basic.String.beam", "/basic/string.ex") - - [block, inline | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() - - assert {:function, 2, :string, 0, - [ - {:clause, 2, [], [], - [{:bin, 2, [{:bin_element, 2, {:string, 2, 'abc'}, :default, :default}]}]} - ]} = inline - - assert {:function, 4, :string_block, 0, - [ - {:clause, 4, [], [], - [{:bin, 5, [{:bin_element, 5, {:string, 5, 'abc'}, :default, :default}]}]} - ]} = block - end - - test "tuple" do - {tokens, ast} = load("/Elixir.Tuple.beam", "/tuple.ex") - - [tuple_in_str2, tuple_in_str, tuple_in_list, _list_in_tuple, tuple | _] = - AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() - - # FIXME - assert {:function, 18, :tuple_in_str2, 0, - [ - {:clause, 18, [], [], - [ - {:match, 19, {:var, 19, :_msg@1}, - {:bin, 20, - [ - {:bin_element, 20, {:string, 20, '\nElixir formatter not exist for '}, - :default, :default}, - {:bin_element, 20, - {:call, 20, {:remote, 20, {:atom, 20, Kernel}, {:atom, 20, :inspect}}, - [ - {:tuple, 20, []}, - {:cons, 20, {:tuple, 20, [{:atom, 20, :pretty}, {:atom, 20, true}]}, - {:cons, 20, - {:tuple, 20, [{:atom, 20, :limit}, {:atom, 20, :infinity}]}, - {nil, 0}}} - ]}, :default, [:binary]}, - {:bin_element, 20, {:string, 20, ' using default \n'}, :default, :default} - ]}}, - {:call, 22, {:remote, 22, {:atom, 22, String}, {:atom, 22, :to_charlist}}, - [ - {:bin, 22, - [ - {:bin_element, 22, - {:call, 22, - {:remote, 22, {:atom, 22, IO.ANSI}, {:atom, 22, :light_yellow}}, []}, - :default, [:binary]}, - {:bin_element, 22, {:var, 22, :_msg@1}, :default, [:binary]}, - {:bin_element, 22, - {:call, 22, {:remote, 22, {:atom, 22, IO.ANSI}, {:atom, 22, :reset}}, - []}, :default, [:binary]} - ]} - ]} - ]} - ]} = tuple_in_str2 - - assert {:function, 14, :tuple_in_str, 0, - [ - {:clause, 14, [], [], - [ - {:bin, 15, - [ - {:bin_element, 15, {:string, 15, 'abc '}, :default, :default}, - {:bin_element, 15, - {:call, 15, {:remote, 15, {:atom, 15, Kernel}, {:atom, 15, :inspect}}, - [ - {:atom, 15, :abc}, - {:cons, 15, {:tuple, 15, [{:atom, 15, :limit}, {:atom, 15, :infinity}]}, - {:cons, 15, - {:tuple, 15, - [ - {:atom, 15, :label}, - {:bin, 15, - [ - {:bin_element, 15, {:string, 15, 'abc '}, :default, :default}, - {:bin_element, 15, - {:case, [generated: true, location: 15], {:integer, 15, 13}, - [ - {:clause, [generated: true, location: 15], - [{:var, [generated: true, location: 15], :_@1}], - [ - [ - {:call, [generated: true, location: 15], - {:remote, [generated: true, location: 15], - {:atom, [generated: true, location: 15], :erlang}, - {:atom, [generated: true, location: 15], :is_binary}}, - [{:var, [generated: true, location: 15], :_@1}]} - ] - ], [{:var, [generated: true, location: 15], :_@1}]}, - {:clause, [generated: true, location: 15], - [{:var, [generated: true, location: 15], :_@1}], [], - [ - {:call, [generated: true, location: 15], - {:remote, [generated: true, location: 15], - {:atom, [generated: true, location: 15], String.Chars}, - {:atom, [generated: true, location: 15], :to_string}}, - [{:var, [generated: true, location: 15], :_@1}]} - ]} - ]}, :default, [:binary]} - ]} - ]}, {nil, 0}}} - ]}, :default, [:binary]}, - {:bin_element, 15, {:integer, 15, 12}, :default, [:integer]} - ]} - ]} - ]} = tuple_in_str - - assert {:function, 10, :tuple_in_list, 0, - [ - {:clause, 10, [], [], - [ - {:cons, 11, {:tuple, 11, [{:atom, 11, :a}, {:integer, 11, 12}]}, - {:cons, 11, {:tuple, 11, [{:atom, 11, :b}, {:atom, 11, :ok}]}, {nil, 0}}} - ]} - ]} = tuple_in_list - - assert {:function, 2, :tuple, 0, - [{:clause, 2, [], [], [{:tuple, 3, [{:atom, 3, :ok}, {:integer, 3, 12}]}]}]} = tuple - end - - test "binary" do - {tokens, ast} = load("/basic/Elixir.Basic.Binary.beam", "/basic/binary.ex") - - [complex2, complex, bin_block, bin | _] = - AstSpecifier.run_mappers(ast, tokens) - |> Enum.reverse() - - assert {:function, 13, :complex2, 0, - [ - {:clause, 13, [], [], - [ - {:bin, 14, - [ - {:bin_element, 14, {:string, 14, 'abc '}, :default, :default}, - {:bin_element, 14, - {:call, 14, {:remote, 14, {:atom, 14, Kernel}, {:atom, 14, :inspect}}, - [{:integer, 14, 12}]}, :default, [:binary]}, - {:bin_element, 14, {:string, 14, ' cba'}, :default, :default} - ]} - ]} - ]} = complex2 - - assert {:function, 8, :complex, 0, - [ - {:clause, 8, [], [], - [ - {:match, 9, {:var, 9, :_x@2}, - {:fun, 9, - {:clauses, - [ - {:clause, 9, [{:var, 9, :_x@1}], [], - [{:op, 9, :+, {:var, 9, :_x@1}, {:integer, 9, 1}}]} - ]}}}, - {:bin, 10, - [ - {:bin_element, 10, {:integer, 10, 49}, :default, [:integer]}, - {:bin_element, 10, {:integer, 10, 48}, :default, [:integer]}, - {:bin_element, 10, {:call, 10, {:var, 10, :_x@2}, [{:integer, 10, 50}]}, - :default, [:integer]} - ]} - ]} - ]} = complex - - assert {:function, 4, :bin_block, 0, - [ - {:clause, 4, [], [], - [ - {:bin, 5, - [ - {:bin_element, 5, {:integer, 5, 49}, :default, [:integer]}, - {:bin_element, 5, {:integer, 5, 48}, :default, [:integer]}, - {:bin_element, 5, {:integer, 5, 48}, :default, [:integer]} - ]} - ]} - ]} = bin_block - - assert {:function, 2, :bin, 0, - [ - {:clause, 2, [], [], - [ - {:bin, 2, - [ - {:bin_element, 2, {:integer, 2, 49}, :default, [:integer]}, - {:bin_element, 2, {:integer, 2, 48}, :default, [:integer]}, - {:bin_element, 2, {:integer, 2, 48}, :default, [:integer]} - ]} - ]} - ]} = bin - end - - test "case conditional" do - {tokens, ast} = load("/conditional/Elixir.Conditional.Case.beam", "/conditional/case.ex") - - [block, inline | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() - - assert {:function, 2, :case_, 0, - [ - {:clause, 2, [], [], - [ - {:case, 4, {:integer, 4, 5}, - [ - {:clause, 5, [{:integer, 5, 5}], [], [{:atom, 5, :ok}]}, - {:clause, 6, [{:var, 6, :_}], [], [{:atom, 6, :error}]} - ]} - ]} - ]} = inline - - assert {:function, 9, :case_block, 0, - [ - {:clause, 9, [], [], - [ - {:case, 10, {:integer, 10, 5}, - [ - {:clause, 11, [{:integer, 11, 5}], [], [{:atom, 11, :ok}]}, - {:clause, 12, [{:var, 12, :_}], [], [{:atom, 12, :error}]} - ]} - ]} - ]} = block - end - - test "if conditional" do - {tokens, ast} = load("/conditional/Elixir.Conditional.If.beam", "/conditional/if.ex") - - [block, inline, if_ | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() - - assert {:function, 12, :if_block, 0, - [ - {:clause, 12, [], [], - [ - {:case, 13, {:op, 13, :<, {:integer, 13, 1}, {:integer, 13, 5}}, - [ - {:clause, [generated: true, location: 13], [{:atom, 0, false}], [], - [{:atom, 16, :error}]}, - {:clause, [generated: true, location: 13], [{:atom, 0, true}], [], - [{:atom, 14, :ok}]} - ]} - ]} - ]} = block - - assert {:function, 10, :if_inline, 0, - [ - {:clause, 10, [], [], - [ - {:case, 10, {:op, 10, :<, {:integer, 10, 1}, {:integer, 10, 5}}, - [ - {:clause, [generated: true, location: 10], [{:atom, 0, false}], [], - [{:atom, 10, :error}]}, - {:clause, [generated: true, location: 10], [{:atom, 0, true}], [], - [{:atom, 10, :ok}]} - ]} - ]} - ]} = inline - - assert {:function, 2, :if_, 0, - [ - {:clause, 2, [], [], - [ - {:case, 4, {:op, 4, :<, {:integer, 4, 1}, {:integer, 4, 5}}, - [ - {:clause, [generated: true, location: 4], [{:atom, 0, false}], [], - [{:atom, 7, :error}]}, - {:clause, [generated: true, location: 4], [{:atom, 0, true}], [], - [{:atom, 5, :ok}]} - ]} - ]} - ]} = if_ - end - - test "unless conditional" do - {tokens, ast} = - load("/conditional/Elixir.Conditional.Unless.beam", "/conditional/unless.ex") - - [block | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() - - assert { - :function, - 2, - :unless_block, - 0, - [ - {:clause, 2, [], [], - [ - {:case, 3, {:atom, 3, false}, - [ - {:clause, [generated: true, location: 3], [{:atom, 0, false}], [], - [{:atom, 4, :ok}]}, - {:clause, [generated: true, location: 3], [{:atom, 0, true}], [], - [{:atom, 6, :error}]} - ]} - ]} - ] - } == block - end - - test "cond conditional" do - {tokens, ast} = load("/conditional/Elixir.Conditional.Cond.beam", "/conditional/cond.ex") - - [block, inline | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() - - assert {:function, 2, :cond_, 1, - [ - {:clause, 2, [{:var, 2, :_a@1}], [], - [ - {:case, 4, {:op, 5, :==, {:var, 5, :_a@1}, {:atom, 5, :ok}}, - [ - {:clause, 5, [{:atom, 0, true}], [], [{:atom, 5, :ok}]}, - {:clause, 6, [{:atom, 0, false}], [], - [ - {:case, 6, {:op, 6, :>, {:var, 6, :_a@1}, {:integer, 6, 5}}, - [ - {:clause, 6, [{:atom, 0, true}], [], [{:atom, 6, :ok}]}, - {:clause, 7, [{:atom, 0, false}], [], - [ - {:case, 7, {:atom, 7, true}, - [ - {:clause, 7, [{:atom, 0, true}], [], [{:atom, 7, :error}]}, - {:clause, [generated: true, location: 7], [{:atom, 0, false}], - [], - [ - {:call, 7, - {:remote, 7, {:atom, 7, :erlang}, {:atom, 7, :error}}, - [{:atom, 7, :cond_clause}]} - ]} - ]} - ]} - ]} - ]} - ]} - ]} - ]} = inline - - assert {:function, 10, :cond_block, 0, - [ - {:clause, 10, [], [], - [ - {:match, 11, {:var, 11, :_a@1}, {:integer, 11, 5}}, - {:case, 13, {:op, 14, :==, {:var, 14, :_a@1}, {:atom, 14, :ok}}, - [ - {:clause, 14, [{:atom, 0, true}], [], [{:atom, 14, :ok}]}, - {:clause, 15, [{:atom, 0, false}], [], - [ - {:case, 15, {:op, 15, :>, {:var, 15, :_a@1}, {:integer, 15, 5}}, - [ - {:clause, 15, [{:atom, 0, true}], [], [{:atom, 15, :ok}]}, - {:clause, 16, [{:atom, 0, false}], [], - [ - {:case, 16, {:atom, 16, true}, - [ - {:clause, 16, [{:atom, 0, true}], [], [{:atom, 16, :error}]}, - {:clause, [generated: true, location: 16], [{:atom, 0, false}], - [], - [ - {:call, 16, - {:remote, 16, {:atom, 16, :erlang}, {:atom, 16, :error}}, - [{:atom, 16, :cond_clause}]} - ]} - ]} - ]} - ]} - ]} - ]} - ]} - ]} = block - end - - test "with conditional" do - {tokens, ast} = load("/conditional/Elixir.Conditional.With.beam", "/conditional/with.ex") - - [block | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() - - assert {:function, 6, :test_with, 0, - [ - {:clause, 6, [], [], - [ - {:case, [generated: true, location: 7], {:call, 7, {:atom, 7, :ok_res}, []}, - [ - {:clause, 7, [{:tuple, 7, [{:atom, 7, :ok}, {:var, 7, :__a@1}]}], [], - [{:integer, 8, 12}]}, - {:clause, [generated: true, location: 7], [{:var, 10, :_}], [], - [ - {:block, 7, - [ - {:call, 11, {:remote, 11, {:atom, 11, IO}, {:atom, 11, :puts}}, - [ - {:bin, 11, - [{:bin_element, 11, {:string, 11, 'error'}, :default, :default}]} - ]}, - {:cons, 12, {:integer, 12, 49}, - {:cons, 12, {:integer, 12, 50}, {nil, 0}}} - ]} - ]} - ]} - ]} - ]} == block - end - - @tag :skip - test "basic function return" do - ex_file = "/basic.ex" - beam_file = "/Elixir.Basic.beam" - {tokens, ast} = load(beam_file, ex_file) - - specified_ast = AstSpecifier.run_mappers(ast, tokens) - IO.inspect(specified_ast) - assert is_list(specified_ast) - end - end - - test "specify_line/2" do - {tokens, _} = example_data() - opts = [end_line: -1] - - assert {{:integer, 21, 12}, tokens} = - AstSpecifier.specify_line({:integer, 21, 12}, tokens, opts) - - assert {{:integer, 22, 12}, _tokens} = - AstSpecifier.specify_line({:integer, 20, 12}, tokens, opts) - end - - test "cons_to_charlist/1" do - cons = - {:cons, 0, {:integer, 0, 49}, - {:cons, 0, {:integer, 0, 48}, {:cons, 0, {:integer, 0, 48}, {nil, 0}}}} - - assert '100' == AstSpecifier.cons_to_charlist(cons) - end - - describe "test that prints result" do - @tag :skip - test "specify/1" do - {_tokens, forms} = example_data() - - AstSpecifier.specify(forms) - |> IO.inspect() - end - - @tag :skip - test "display forms" do - {_, forms} = example_data() - IO.inspect(forms) - end - end - - test "function call" do - {tokens, ast} = load("/Elixir.Call.beam", "/call.ex") - - [call, _ | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() - - assert {:function, 5, :call, 0, - [ - {:clause, 5, [], [], - [ - {:call, 6, {:atom, 6, :get_x}, - [ - {:bin, 7, [{:bin_element, 7, {:string, 7, 'ala'}, :default, :default}]}, - {:cons, 8, {:integer, 8, 97}, - {:cons, 8, {:integer, 8, 108}, {:cons, 8, {:integer, 8, 97}, {nil, 0}}}}, - {:integer, 9, 12} - ]} - ]} - ]} = call - end - - test "pipe" do - {tokens, ast} = load("/Elixir.Pipe.beam", "/pipe_op.ex") - - [block | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() - - assert {:function, 2, :pipe, 0, - [ - {:clause, 2, [], [], - [ - {:call, 5, {:remote, 5, {:atom, 5, :erlang}, {:atom, 5, :length}}, - [ - {:call, 4, {:remote, 4, {:atom, 4, Enum}, {:atom, 4, :filter}}, - [ - {:cons, 4, {:integer, 4, 1}, - {:cons, 4, - { - :integer, - 4, - 2 - }, {:cons, 4, {:integer, 4, 3}, {nil, 0}}}}, - {:fun, 4, - {:clauses, - [ - {:clause, 4, [{:var, 4, :_x@1}], [], - [{:op, 4, :<, {:var, 4, :_x@1}, {:integer, 4, 3}}]} - ]}} - ]} - ]} - ]} - ]} = block - end - - test "guards" do - {tokens, ast} = load("/conditional/Elixir.Conditional.Guard.beam", "/conditional/guards.ex") - - [guarded_case, guarded_fun | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() - - assert {:function, 3, :guarded_fun, 1, - [ - {:clause, 3, [{:var, 3, :_x@1}], - [ - [ - {:call, 3, {:remote, 3, {:atom, 3, :erlang}, {:atom, 3, :is_integer}}, - [{:var, 3, :_x@1}]} - ], - [ - {:op, 3, :andalso, {:op, 3, :>, {:var, 3, :_x@1}, {:integer, 3, 3}}, - {:op, 3, :<, {:var, 3, :_x@1}, {:integer, 3, 6}}} - ] - ], [{:atom, 3, :ok}]} - ]} = guarded_fun - - assert {:function, 6, :guarded_case, 1, - [ - {:clause, 6, [{:var, 6, :_x@1}], [], - [ - {:case, 7, {:var, 7, :_x@1}, - [ - {:clause, 8, [{:integer, 8, 0}], [], - [{:tuple, 8, [{:atom, 8, :ok}, {:integer, 8, 1}]}]}, - {:clause, 9, [{:var, 9, :_i@1}], - [[{:op, 9, :>, {:var, 9, :_i@1}, {:integer, 9, 0}}]], - [ - {:tuple, 9, - [{:atom, 9, :ok}, {:op, 9, :+, {:var, 9, :_i@1}, {:integer, 9, 1}}]} - ]}, - {:clause, 10, [{:var, 10, :__otherwise@1}], [], [{:atom, 10, :error}]} - ]} - ]} - ]} = guarded_case - end - - test "range" do - {tokens, ast} = load("/Elixir.RangeEx.beam", "/range.ex") - - [to_list, match_range, rev_range_step, range_step, range | _] = - AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() - - assert {:function, 18, :to_list, 0, - [ - {:clause, 18, [], [], - [ - {:call, 19, {:remote, 19, {:atom, 19, Enum}, {:atom, 19, :to_list}}, - [ - {:map, 19, - [ - {:map_field_assoc, 19, {:atom, 19, :__struct__}, {:atom, 19, Range}}, - {:map_field_assoc, 19, {:atom, 19, :first}, {:integer, 19, 1}}, - {:map_field_assoc, 19, {:atom, 19, :last}, {:integer, 19, 100}}, - {:map_field_assoc, 19, {:atom, 19, :step}, {:integer, 19, 5}} - ]} - ]} - ]} - ]} = to_list - - assert {:function, 14, :match_range, 0, - [ - {:clause, 14, [], [], - [ - {:match, 15, - {:map, 15, - [ - {:map_field_exact, 15, {:atom, 15, :__struct__}, {:atom, 15, Range}}, - {:map_field_exact, 15, {:atom, 15, :first}, {:var, 15, :_first@1}}, - {:map_field_exact, 15, {:atom, 15, :last}, {:var, 15, :_last@1}}, - {:map_field_exact, 15, {:atom, 15, :step}, {:var, 15, :_step@1}} - ]}, {:call, 15, {:atom, 15, :range_step}, []}} - ]} - ]} = match_range - - assert {:function, 10, :rev_range_step, 0, - [ - {:clause, 10, [], [], - [ - {:map, 11, - [ - {:map_field_assoc, 11, {:atom, 11, :__struct__}, {:atom, 11, Range}}, - {:map_field_assoc, 11, {:atom, 11, :first}, {:integer, 11, 12}}, - {:map_field_assoc, 11, {:atom, 11, :last}, {:integer, 11, 1}}, - {:map_field_assoc, 11, {:atom, 11, :step}, {:integer, 11, -2}} - ]} - ]} - ]} = rev_range_step - - assert {:function, 6, :range_step, 0, - [ - {:clause, 6, [], [], - [ - {:map, 7, - [ - {:map_field_assoc, 7, {:atom, 7, :__struct__}, {:atom, 7, Range}}, - {:map_field_assoc, 7, {:atom, 7, :first}, {:integer, 7, 1}}, - {:map_field_assoc, 7, {:atom, 7, :last}, {:integer, 7, 12}}, - {:map_field_assoc, 7, {:atom, 7, :step}, {:integer, 7, 2}} - ]} - ]} - ]} = range_step - - assert {:function, 2, :range, 0, - [ - {:clause, 2, [], [], - [ - {:map, 3, - [ - {:map_field_assoc, 3, {:atom, 3, :__struct__}, {:atom, 3, Range}}, - {:map_field_assoc, 3, {:atom, 3, :first}, {:integer, 3, 1}}, - {:map_field_assoc, 3, {:atom, 3, :last}, {:integer, 3, 12}}, - {:map_field_assoc, 3, {:atom, 3, :step}, {:integer, 3, 1}} - ]} - ]} - ]} = range - end - - test "list comprehension" do - {tokens, ast} = load("/Elixir.ListComprehension.beam", "/list_comprehension.ex") - - [block | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() - - assert {:function, 2, :lc, 0, - [ - {:clause, 2, [], [], - [ - {:call, 3, {:remote, 3, {:atom, 3, :lists}, {:atom, 3, :reverse}}, - [ - {:call, 3, {:remote, 3, {:atom, 3, Enum}, {:atom, 3, :reduce}}, - [ - {:map, 3, - [ - {:map_field_assoc, 3, {:atom, 3, :__struct__}, {:atom, 3, Range}}, - {:map_field_assoc, 3, {:atom, 3, :first}, {:integer, 3, 0}}, - {:map_field_assoc, 3, {:atom, 3, :last}, {:integer, 3, 5}}, - {:map_field_assoc, 3, {:atom, 3, :step}, {:integer, 3, 1}} - ]}, - {nil, 3}, - {:fun, 3, - {:clauses, - [ - {:clause, 3, [{:var, 3, :_n@1}, {:var, 3, :_@1}], [], - [ - {:case, [generated: true, location: 3], - {:op, 3, :==, {:op, 3, :rem, {:var, 3, :_n@1}, {:integer, 3, 3}}, - {:integer, 3, 0}}, - [ - {:clause, [generated: true, location: 3], - [{:atom, [generated: true, location: 3], true}], [], - [ - {:cons, 3, {:op, 3, :*, {:var, 3, :_n@1}, {:var, 3, :_n@1}}, - {:var, 3, :_@1}} - ]}, - {:clause, [generated: true, location: 3], - [{:atom, [generated: true, location: 3], false}], [], - [{:var, 3, :_@1}]} - ]} - ]} - ]}} - ]} - ]} - ]} - ]} = block - end - - test "list" do - {tokens, ast} = load("/Elixir.ListEx.beam", "/list.ex") - - [ht2, ht, list, _wrap | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() - - assert {:function, 5, :list, 0, - [ - {:clause, 5, [], [], - [ - {:cons, 6, - {:cons, 6, {:integer, 6, 49}, {:cons, 6, {:integer, 6, 49}, {nil, 0}}}, - {:cons, 6, - {:bin, 6, [{:bin_element, 6, {:string, 6, '12'}, :default, :default}]}, - {:cons, 6, {:integer, 6, 1}, - {:cons, 6, {:integer, 6, 2}, - {:cons, 6, {:integer, 6, 3}, - {:cons, 6, {:call, 6, {:atom, 6, :wrap}, [{:integer, 6, 4}]}, {nil, 0}}}}}}} - ]} - ]} = list - - assert {:function, 9, :ht, 1, - [ - {:clause, 9, [{:cons, 9, {:var, 9, :_a@1}, {:var, 9, :_}}], [], - [ - {:cons, 10, {:var, 10, :_a@1}, - {:cons, 10, {:integer, 10, 1}, - {:cons, 10, {:integer, 10, 2}, {:cons, 10, {:integer, 10, 3}, {nil, 0}}}}} - ]} - ]} = ht - - assert {:function, 13, :ht2, 1, - [ - {:clause, 13, [{:cons, 13, {:var, 13, :_a@1}, {:var, 13, :_}}], [], - [ - {:cons, 14, {:var, 14, :_a@1}, - {:call, 14, {:atom, 14, :wrap}, [{:integer, 14, 1}]}} - ]} - ]} = ht2 - end - - test "try" do - {tokens, ast} = load("/Elixir.Try.beam", "/try.ex") - - [body_after, try_after, try_else, try_rescue | _] = - AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() - - assert {:function, 2, :try_rescue, 0, - [ - {:clause, 2, [], [], - [ - {:try, 3, - [ - {:case, 4, {:atom, 4, true}, - [ - {:clause, [generated: true, location: 4], [{:atom, 0, false}], [], - [ - {:call, 7, {:remote, 7, {:atom, 7, :erlang}, {:atom, 7, :error}}, - [ - {:call, 7, - {:remote, 7, {:atom, 7, RuntimeError}, {:atom, 7, :exception}}, - [ - {:bin, 7, - [ - {:bin_element, 7, {:string, 7, 'oops'}, :default, :default} - ]} - ]} - ]} - ]}, - {:clause, [generated: true, location: 4], [{:atom, 0, true}], [], - [ - {:call, 5, {:remote, 5, {:atom, 5, :erlang}, {:atom, 5, :throw}}, - [ - {:bin, 5, - [{:bin_element, 5, {:string, 5, 'good'}, :default, :default}]} - ]} - ]} - ]} - ], [], - [ - {:clause, 10, - [ - {:tuple, 10, - [ - {:atom, 10, :error}, - {:var, 10, :_@1}, - {:var, 10, :___STACKTRACE__@1} - ]} - ], - [ - [ - {:op, 10, :andalso, - {:op, 10, :==, - {:call, 10, {:remote, 10, {:atom, 10, :erlang}, {:atom, 10, :map_get}}, - [{:atom, 10, :__struct__}, {:var, 10, :_@1}]}, - {:atom, 10, RuntimeError}}, - {:call, 10, {:remote, 10, {:atom, 10, :erlang}, {:atom, 10, :map_get}}, - [{:atom, 10, :__exception__}, {:var, 10, :_@1}]}} - ] - ], - [ - {:match, 10, {:var, 10, :_e@1}, {:var, 10, :_@1}}, - {:integer, 11, 11}, - {:var, 12, :_e@1} - ]}, - {:clause, 14, - [ - {:tuple, 14, - [ - {:atom, 14, :throw}, - {:var, 14, :_val@1}, - {:var, 14, :___STACKTRACE__@1} - ]} - ], [], [{:integer, 15, 12}, {:var, 16, :_val@1}]} - ], []} - ]} - ]} = try_rescue - - assert {:function, 20, :try_else, 0, - [ - {:clause, 20, [], [], - [ - {:match, 21, {:var, 21, :_x@1}, {:integer, 21, 2}}, - {:try, 23, [{:op, 24, :/, {:integer, 24, 1}, {:var, 24, :_x@1}}], - [ - {:clause, 30, [{:var, 30, :_y@1}], - [ - [ - {:op, 30, :andalso, {:op, 30, :<, {:var, 30, :_y@1}, {:integer, 30, 1}}, - {:op, 30, :>, {:var, 30, :_y@1}, {:op, 30, :-, {:integer, 30, 1}}}} - ] - ], [{:integer, 31, 2}, {:atom, 32, :small}]}, - {:clause, 34, [{:var, 34, :_}], [], [{:integer, 35, 3}, {:atom, 36, :large}]} - ], - [ - {:clause, 26, - [ - {:tuple, 26, - [ - {:atom, 26, :error}, - {:var, 26, :_@1}, - {:var, 26, :___STACKTRACE__@1} - ]} - ], - [ - [{:op, 26, :==, {:var, 26, :_@1}, {:atom, 26, :badarith}}], - [ - {:op, 26, :andalso, - {:op, 26, :==, - {:call, 26, {:remote, 26, {:atom, 26, :erlang}, {:atom, 26, :map_get}}, - [{:atom, 26, :__struct__}, {:var, 26, :_@1}]}, - {:atom, 26, ArithmeticError}}, - {:call, 26, {:remote, 26, {:atom, 26, :erlang}, {:atom, 26, :map_get}}, - [{:atom, 26, :__exception__}, {:var, 26, :_@1}]}} - ] - ], [{:integer, 27, 1}, {:atom, 28, :infinity}]} - ], []} - ]} - ]} = try_else - - assert {:function, 40, :try_after, 0, - [ - {:clause, 40, [], [], - [ - {:match, 41, {:tuple, 41, [{:atom, 41, :ok}, {:var, 41, :_file@1}]}, - {:call, 41, {:remote, 41, {:atom, 41, File}, {:atom, 41, :open}}, - [ - {:bin, 41, - [{:bin_element, 41, {:string, 41, 'sample'}, :default, :default}]}, - {:cons, 41, {:atom, 41, :utf8}, {:cons, 41, {:atom, 41, :write}, {nil, 0}}} - ]}}, - {:try, 43, - [ - {:call, 44, {:remote, 44, {:atom, 44, IO}, {:atom, 44, :write}}, - [ - {:var, 44, :_file@1}, - {:bin, 44, - [ - {:bin_element, 44, {:string, 44, [111, 108, 195, 161]}, :default, - :default} - ]} - ]}, - {:call, 45, {:remote, 45, {:atom, 45, :erlang}, {:atom, 45, :error}}, - [ - {:call, 45, - {:remote, 45, {:atom, 45, RuntimeError}, {:atom, 45, :exception}}, - [ - {:bin, 45, - [ - {:bin_element, 45, {:string, 45, 'oops, something went wrong'}, - :default, :default} - ]} - ]} - ]} - ], [], [], - [ - {:call, 47, {:remote, 47, {:atom, 47, File}, {:atom, 47, :close}}, - [{:var, 47, :_file@1}]} - ]} - ]} - ]} = try_after - - assert {:function, 51, :body_after, 0, - [ - {:clause, 51, [], [], - [ - {:try, 51, - [ - {:call, 52, {:remote, 52, {:atom, 52, :erlang}, {:atom, 52, :error}}, - [ - {:call, 52, {:remote, 52, {:atom, 52, Kernel.Utils}, {:atom, 52, :raise}}, - [ - {:cons, 52, {:integer, 52, 49}, - {:cons, 52, {:integer, 52, 50}, {nil, 0}}} - ]} - ]}, - {:integer, 53, 1} - ], [], [], [{:op, 55, :-, {:integer, 55, 1}}]} - ]} - ]} = body_after - end - - test "map" do - {tokens, ast} = load("/Elixir.MapEx.beam", "/map.ex") - - [pattern_matching_str, pattern_matching, test_map_str, test_map, empty_map | _] = - AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() - - assert {:function, 2, :empty_map, 0, [{:clause, 2, [], [], [{:map, 3, []}]}]} = empty_map - - assert {:function, 6, :test_map, 0, - [ - {:clause, 6, [], [], - [ - {:map, 7, - [ - {:map_field_assoc, 7, {:atom, 7, :a}, {:integer, 7, 12}}, - {:map_field_assoc, 7, {:atom, 7, :b}, {:call, 7, {:atom, 7, :empty_map}, []}} - ]} - ]} - ]} = test_map - - assert {:function, 10, :test_map_str, 0, - [ - {:clause, 10, [], [], - [ - {:map, 11, - [ - {:map_field_assoc, 11, - {:bin, 11, [{:bin_element, 11, {:string, 11, 'a'}, :default, :default}]}, - {:integer, 11, 12}}, - {:map_field_assoc, 11, - {:bin, 11, [{:bin_element, 11, {:string, 11, 'b'}, :default, :default}]}, - {:integer, 11, 0}} - ]} - ]} - ]} = test_map_str - - assert {:function, 14, :pattern_matching, 0, - [ - {:clause, 14, [], [], - [ - {:match, 15, - {:map, 15, [{:map_field_exact, 15, {:atom, 15, :a}, {:var, 15, :_a@1}}]}, - {:call, 15, {:atom, 15, :test_map}, []}}, - {:match, 16, - {:map, 16, [{:map_field_exact, 16, {:atom, 16, :b}, {:var, 16, :_a@1}}]}, - {:call, 16, {:atom, 16, :test_map}, []}} - ]} - ]} = pattern_matching - - assert {:function, 19, :pattern_matching_str, 0, - [ - {:clause, 19, [], [], - [ - {:match, 20, - {:map, 20, - [ - {:map_field_exact, 20, - {:bin, 20, [{:bin_element, 20, {:string, 20, 'a'}, :default, :default}]}, - {:var, 20, :_a@1}} - ]}, {:call, 20, {:atom, 20, :test_map}, []}} - ]} - ]} = pattern_matching_str - end - - test "struct" do - {tokens, ast} = load("/struct/Elixir.StructEx.beam", "/struct/struct.ex") - - [get2, get, update, empty, struct | _] = - AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() - - assert {:function, 8, :update, 0, - [ - {:clause, 8, [], [], - [ - {:map, 9, {:call, 9, {:atom, 9, :empty}, []}, - [{:map_field_exact, 9, {:atom, 9, :x}, {:integer, 9, 13}}]} - ]} - ]} = update - - assert {:function, 16, :get2, 0, - [ - {:clause, 16, [], [], - [ - {:match, 17, {:var, 17, :_x@1}, - {:case, [generated: true, location: 17], {:call, 17, {:atom, 17, :update}, []}, - [ - {:clause, [generated: true, location: 17], - [ - {:map, 17, - [ - {:map_field_exact, 17, {:atom, 17, :x}, - {:var, [generated: true, location: 17], :_@1}} - ]} - ], [], [{:var, [generated: true, location: 17], :_@1}]}, - {:clause, [generated: true, location: 17], - [{:var, [generated: true, location: 17], :_@1}], - [ - [ - {:call, [generated: true, location: 17], - {:remote, [generated: true, location: 17], - {:atom, [generated: true, location: 17], :erlang}, - {:atom, [generated: true, location: 17], :is_map}}, - [{:var, [generated: true, location: 17], :_@1}]} - ] - ], - [ - {:call, 17, {:remote, 17, {:atom, 17, :erlang}, {:atom, 17, :error}}, - [ - {:tuple, 17, - [ - {:atom, 17, :badkey}, - {:atom, 17, :x}, - {:var, [generated: true, location: 17], :_@1} - ]} - ]} - ]}, - {:clause, [generated: true, location: 17], - [{:var, [generated: true, location: 17], :_@1}], [], - [ - {:call, [generated: true, location: 17], - {:remote, [generated: true, location: 17], - {:var, [generated: true, location: 17], :_@1}, {:atom, 17, :x}}, []} - ]} - ]}} - ]} - ]} = get2 - - assert {:function, 12, :get, 0, - [ - {:clause, 12, [], [], - [ - {:match, 13, - {:map, 13, - [ - {:map_field_exact, 13, {:atom, 13, :__struct__}, {:atom, 13, StructEx}}, - {:map_field_exact, 13, {:atom, 13, :x}, {:var, 13, :_x@1}} - ]}, {:call, 13, {:atom, 13, :update}, []}} - ]} - ]} = get - - assert {:function, 4, :empty, 0, - [ - {:clause, 4, [], [], - [ - {:map, 5, - [ - {:map_field_assoc, 5, {:atom, 5, :__struct__}, {:atom, 5, StructEx}}, - {:map_field_assoc, 5, {:atom, 5, :x}, {:integer, 5, 0}}, - {:map_field_assoc, 5, {:atom, 5, :y}, {:integer, 5, 0}} - ]} - ]} - ]} = empty - - assert {:function, 2, :__struct__, 1, - [ - {:clause, 2, [{:var, 2, :_@1}], [], - [ - {:call, 2, {:remote, 2, {:atom, 2, Enum}, {:atom, 2, :reduce}}, - [ - {:var, 2, :_@1}, - {:map, 2, - [ - {:map_field_assoc, 2, {:atom, 2, :__struct__}, {:atom, 2, StructEx}}, - {:map_field_assoc, 2, {:atom, 2, :x}, {:integer, 2, 0}}, - {:map_field_assoc, 2, {:atom, 2, :y}, {:integer, 2, 0}} - ]}, - {:fun, 2, - {:clauses, - [ - {:clause, 2, - [{:tuple, 2, [{:var, 2, :_@2}, {:var, 2, :_@3}]}, {:var, 2, :_@4}], [], - [ - {:call, 2, {:remote, 2, {:atom, 2, :maps}, {:atom, 2, :update}}, - [{:var, 2, :_@2}, {:var, 2, :_@3}, {:var, 2, :_@4}]} - ]} - ]}} - ]} - ]} - ]} = struct - end - - test "record" do - {tokens, ast} = load("/record/Elixir.RecordEx.beam", "/record/record.ex") - - [update, init, empty, macro3, macro2, macro1 | _] = - AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() - - assert {:function, 7, :empty, 0, - [ - {:clause, 7, [], [], - [{:tuple, 8, [{:atom, 8, :record_ex}, {:integer, 8, 0}, {:integer, 8, 0}]}]} - # FIXME Should be a tuple with line 8, not 12. The line is taken from a token that is in another scope. Related to the cutting out tokens at the bottom - ]} = empty - - assert {:function, 11, :init, 0, - [ - {:clause, 11, [], [], - [{:tuple, 12, [{:atom, 12, :record_ex}, {:integer, 12, 1}, {:integer, 12, 0}]}]} - ]} = init - - assert {:function, 5, :"MACRO-record_ex", 1, - [ - {:clause, 5, [{:var, 5, :_@CALLER}], [], - [ - {:match, 5, {:var, 5, :__CALLER__}, - {:call, 5, {:remote, 5, {:atom, 5, :elixir_env}, {:atom, 5, :linify}}, - [{:var, 5, :_@CALLER}]}}, - {:call, 5, {:atom, 5, :"MACRO-record_ex"}, [{:var, 5, :__CALLER__}, {nil, 0}]} - ]} - ]} = macro1 - - assert {:function, 5, :"MACRO-record_ex", 2, - [ - {:clause, 5, [{:var, 5, :_@CALLER}, {:var, 5, :_@1}], [], - [ - {:match, 5, {:var, 5, :__CALLER__}, - {:call, 5, {:remote, 5, {:atom, 5, :elixir_env}, {:atom, 5, :linify}}, - [{:var, 5, :_@CALLER}]}}, - {:call, 5, {:remote, 5, {:atom, 5, Record}, {:atom, 5, :__access__}}, - [ - {:atom, 5, :record_ex}, - {:cons, 5, {:tuple, 5, [{:atom, 5, :x}, {:integer, 5, 0}]}, - {:cons, 5, {:tuple, 5, [{:atom, 5, :y}, {:integer, 5, 0}]}, {nil, 0}}}, - {:var, 5, :_@1}, - {:var, 5, :__CALLER__} - ]} - ]} - ]} = macro2 - - assert {:function, 5, :"MACRO-record_ex", 3, - [ - {:clause, 5, [{:var, 5, :_@CALLER}, {:var, 5, :_@1}, {:var, 5, :_@2}], [], - [ - {:match, 5, {:var, 5, :__CALLER__}, - {:call, 5, {:remote, 5, {:atom, 5, :elixir_env}, {:atom, 5, :linify}}, - [{:var, 5, :_@CALLER}]}}, - {:call, 5, {:remote, 5, {:atom, 5, Record}, {:atom, 5, :__access__}}, - [ - {:atom, 5, :record_ex}, - {:cons, 5, {:tuple, 5, [{:atom, 5, :x}, {:integer, 5, 0}]}, - {:cons, 5, {:tuple, 5, [{:atom, 5, :y}, {:integer, 5, 0}]}, {nil, 0}}}, - {:var, 5, :_@1}, - {:var, 5, :_@2}, - {:var, 5, :__CALLER__} - ]} - ]} - ]} = macro3 - - assert {:function, 16, :update, 1, - [ - {:clause, 16, [{:var, 16, :_record@1}], [], - [ - {:call, 17, {:remote, 17, {:atom, 17, :erlang}, {:atom, 17, :setelement}}, - [ - {:integer, 17, 2}, - {:call, 17, {:remote, 17, {:atom, 17, :erlang}, {:atom, 17, :setelement}}, - [{:integer, 17, 3}, {:var, 17, :_record@1}, {:integer, 17, 3}]}, - {:integer, 17, 2} - ]} - ]} - ]} = update - end - - test "receive" do - {tokens, ast} = load("/Elixir.Receive.beam", "/receive.ex") - - [recv, recv2 | _] = AstSpecifier.run_mappers(ast, tokens) |> Enum.reverse() - - assert {:function, 2, :recv2, 0, - [ - {:clause, 2, [], [], - [ - {:call, 3, {:remote, 3, {:atom, 3, :erlang}, {:atom, 3, :send}}, - [ - {:call, 3, {:remote, 3, {:atom, 3, :erlang}, {:atom, 3, :self}}, []}, - {:tuple, 3, - [ - {:atom, 3, :hello}, - {:bin, 3, [{:bin_element, 3, {:string, 3, 'All'}, :default, :default}]} - ]} - ]}, - {:receive, 5, - [ - {:clause, 6, [{:tuple, 6, [{:atom, 6, :hello}, {:var, 6, :_to@1}]}], [], - [ - {:call, 7, {:remote, 7, {:atom, 7, IO}, {:atom, 7, :puts}}, - [ - {:bin, 7, - [ - {:bin_element, 7, {:string, 7, 'Hello, '}, :default, :default}, - {:bin_element, 7, {:var, 7, :_to@1}, :default, [:binary]} - ]} - ]} - ]}, - {:clause, 9, [{:atom, 9, :skip}], [], [{:atom, 10, :ok}]} - ], {:integer, 12, 1000}, - [ - {:call, 13, {:remote, 13, {:atom, 13, IO}, {:atom, 13, :puts}}, - [ - {:bin, 13, - [{:bin_element, 13, {:string, 13, 'Timeout'}, :default, :default}]} - ]} - ]} - ]} - ]} = recv2 - - assert {:function, 17, :recv, 0, - [ - {:clause, 17, [], [], - [{:receive, 18, [{:clause, 19, [{:atom, 19, :ok}], [], [{:atom, 19, :ok}]}]}]} - ]} = recv - end - - test "typespec" do - {tokens, ast} = load("/Elixir.Typespec.beam", "/typespec.ex") - - [atoms_type2, atoms_type, named_type, missing_type_arg, missing_type | _] = - AstSpecifier.run_mappers(ast, tokens) - |> filter_specs() - |> Enum.reverse() - - assert {:attribute, 17, :spec, - {{:atoms_type2, 1}, - [ - {:type, 17, :fun, - [ - {:type, 17, :product, - [{:type, 17, :union, [{:atom, 17, :ok}, {:atom, 17, :error}]}]}, - {:remote_type, 17, - [ - {:atom, 17, Unknown}, - {:atom, 17, :atom}, - [{:type, 17, :union, [{:atom, 17, :ok}, {:atom, 17, :error}]}] - ]} - ]} - ]}} = atoms_type2 - - assert {:attribute, 14, :spec, - {{:atoms_type, 1}, - [ - {:type, 14, :fun, - [ - {:type, 14, :product, - [{:type, 14, :union, [{:atom, 14, :ok}, {:atom, 14, :error}]}]}, - {:type, 14, :union, [{:atom, 14, :ok}, {:atom, 14, :error}]} - ]} - ]}} = atoms_type - - assert {:attribute, 11, :spec, - {{:named_type, 1}, - [ - {:type, 11, :fun, - [ - {:type, 11, :product, - [ - {:ann_type, 11, - [ - {:var, 11, :name}, - {:remote_type, 11, [{:atom, 11, Unknown}, {:atom, 11, :atom}, []]} - ]} - ]}, - {:type, 11, :atom, []} - ]} - ]}} = named_type - - assert {:attribute, 8, :spec, - {{:missing_type_arg, 0}, - [ - {:type, 8, :fun, - [ - {:type, 8, :product, []}, - {:user_type, 8, :mylist, - [{:remote_type, 8, [{:atom, 8, Unknown}, {:atom, 8, :atom}, []]}]} - ]} - ]}} = missing_type_arg - - assert {:attribute, 5, :spec, - {{:missing_type, 0}, - [ - {:type, 5, :fun, - [ - {:type, 5, :product, []}, - {:remote_type, 5, [{:atom, 5, Unknown}, {:atom, 5, :atom}, []]} - ]} - ]}} = missing_type - end - - # Helpers - - def filter_specs(ast) do - Enum.filter(ast, &match?({:attribute, _, :spec, _}, &1)) - end -end diff --git a/test/gradient/tokens_test.exs b/test/gradient/tokens_test.exs index abfe456c..93ab5733 100644 --- a/test/gradient/tokens_test.exs +++ b/test/gradient/tokens_test.exs @@ -1,10 +1,10 @@ -defmodule GradualizerEx.TokensTest do +defmodule Gradient.TokensTest do use ExUnit.Case - doctest GradualizerEx.Tokens + doctest Gradient.Tokens - alias GradualizerEx.Tokens + alias Gradient.Tokens - import GradualizerEx.TestHelpers + import Gradient.TestHelpers test "drop_tokens_while" do tokens = example_tokens() diff --git a/test/gradient/utils_test.exs b/test/gradient/utils_test.exs deleted file mode 100644 index 2815f37a..00000000 --- a/test/gradient/utils_test.exs +++ /dev/null @@ -1,50 +0,0 @@ -defmodule Gradient.UtilsTest do - use ExUnit.Case - - alias Gradient.Utils - - @examples_path "test/examples" - - test "drop_tokens_while" do - tokens = example_tokens() - - matcher = fn - {:atom, _, :ok} -> false - _ -> true - end - - assert [] = - Utils.drop_tokens_while( - tokens, - 5, - matcher - ) - - refute [] == - Utils.drop_tokens_while( - tokens, - 6, - matcher - ) - - refute [] == - Utils.drop_tokens_while( - tokens, - matcher - ) - end - - def example_tokens() do - file_path = @examples_path <> "/conditional/cond.ex" - - code = - File.read!(file_path) - |> String.to_charlist() - - {:ok, tokens} = - code - |> :elixir.string_to_tokens(1, 1, file_path, []) - - tokens - end -end diff --git a/test/support/helpers.ex b/test/support/helpers.ex index ceecc776..b8763286 100644 --- a/test/support/helpers.ex +++ b/test/support/helpers.ex @@ -1,5 +1,5 @@ -defmodule GradualizerEx.TestHelpers do - alias GradualizerEx.Types, as: T +defmodule Gradient.TestHelpers do + alias Gradient.Types, as: T @examples_path "test/examples" From 77da1a25abed33eed1b7f40a7301a723b01b4d34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Tue, 9 Nov 2021 21:12:17 +0100 Subject: [PATCH 10/15] Formatting --- lib/gradient/type_annotation.ex | 26 +++++++++++----------- lib/gradient/typed_server.ex | 1 - lib/gradient/typed_server/compile_hooks.ex | 14 +++++++++--- test/gradient/elixir_fmt_test.exs | 2 +- 4 files changed, 25 insertions(+), 18 deletions(-) diff --git a/lib/gradient/type_annotation.ex b/lib/gradient/type_annotation.ex index bd8b4e85..9028adc8 100644 --- a/lib/gradient/type_annotation.ex +++ b/lib/gradient/type_annotation.ex @@ -1,41 +1,41 @@ defmodule Gradient.TypeAnnotation do - defmacro annotate_type(expr, type), - do: annotate(:'::', expr, type) + do: annotate(:"::", expr, type) defmacro assert_type(expr, type), - do: annotate(:':::', expr, type) + do: annotate(:":::", expr, type) defp annotate(type_op, expr, type) do erlang_type = elixir_type_to_erlang(type) - #IO.inspect(erlang_type, label: "erlang type") + # IO.inspect(erlang_type, label: "erlang type") {type_op, [], [expr, Macro.to_string(erlang_type)]} - #|> IO.inspect(label: "annotation node") + # |> IO.inspect(label: "annotation node") end defp elixir_type_to_erlang(type) do case type do {{:., _, [{:__aliases__, _, path}, name]}, _, [] = _args} -> - #unquote({:{}, [], [:string, 0, '\'Elixir.Fake\':t()']}) - #{:string, 0, Macro.escape("'Elixir.#{Enum.join(path, ".")}':#{name}()" |> to_charlist())} + # unquote({:{}, [], [:string, 0, '\'Elixir.Fake\':t()']}) + # {:string, 0, Macro.escape("'Elixir.#{Enum.join(path, ".")}':#{name}()" |> to_charlist())} "'Elixir.#{Enum.join(path, ".")}':#{name}()" + _ when is_atom(type) -> Atom.to_string(type) + other -> - #unquote({:{}, [], [:string, 0, '\'Elixir.Fake\':t()']}) + # unquote({:{}, [], [:string, 0, '\'Elixir.Fake\':t()']}) other end end - + defmacro __using__(_) do quote [] do import Gradient.TypeAnnotation require Gradient.TypeAnnotation - @compile {:inline, '::': 2, ':::': 2} - def unquote(:'::')(expr, _type), do: expr - def unquote(:':::')(expr, _type), do: expr - + @compile {:inline, "::": 2, ":::": 2} + def unquote(:"::")(expr, _type), do: expr + def unquote(:":::")(expr, _type), do: expr end end end diff --git a/lib/gradient/typed_server.ex b/lib/gradient/typed_server.ex index 31c32e44..055fe65e 100644 --- a/lib/gradient/typed_server.ex +++ b/lib/gradient/typed_server.ex @@ -1,5 +1,4 @@ defmodule Gradient.TypedServer do - ## TODO: docs, docs, docs! defmacro __using__(_) do diff --git a/lib/gradient/typed_server/compile_hooks.ex b/lib/gradient/typed_server/compile_hooks.ex index 2334629d..0cddbe1b 100644 --- a/lib/gradient/typed_server/compile_hooks.ex +++ b/lib/gradient/typed_server/compile_hooks.ex @@ -3,9 +3,10 @@ defmodule Gradient.TypedServer.CompileHooks do defmacro __before_compile__(env) do response_types = Module.get_attribute(env.module, :response_types) - #IO.inspect(response_types, label: "response types") + # IO.inspect(response_types, label: "response types") for {request_tag, response_type} <- response_types do - name = Macro.escape(:'call_#{request_tag}') + name = Macro.escape(:"call_#{request_tag}") + quote do @spec unquote(name)(t(), any) :: unquote(response_type) defp unquote(name)(pid, arg) do @@ -17,17 +18,21 @@ defmodule Gradient.TypedServer.CompileHooks do def __on_definition__(env, _kind, name, args, _guards, body) do if name == :handle do - #IO.inspect({name, env}, limit: :infinity) + # IO.inspect({name, env}, limit: :infinity) IO.inspect({env.module, Module.get_attribute(env.module, :spec)}) end + request_handler = Module.get_attribute(env.module, :request_handler, :handle) + case request_handler do ^name -> response_type = find_response_type(env, body) + if response_type != nil do {request_tag, _} = Enum.at(args, 0) Module.put_attribute(env.module, :response_types, {request_tag, response_type}) end + _ -> :ok end @@ -49,10 +54,13 @@ defmodule Gradient.TypedServer.CompileHooks do case Macro.expand(path, env) do Gradient.TypedServer -> get_response_type_from_typed_call(env, Macro.decompose_call(reply_call)) + _other -> :ok end + reply_call + not_a_call -> not_a_call end diff --git a/test/gradient/elixir_fmt_test.exs b/test/gradient/elixir_fmt_test.exs index c607983a..a690be71 100644 --- a/test/gradient/elixir_fmt_test.exs +++ b/test/gradient/elixir_fmt_test.exs @@ -25,10 +25,10 @@ defmodule Gradient.ElixirFmtTest do {_tokens, ast} = load("/type/Elixir.WrongRet.beam", "/type/wrong_ret.ex") opts = [] errors = type_check_file(ast, opts) + for e <- errors do :io.put_chars(e) end - end end From b92578650b1e7fa32b54ff98046f2698ad5ad392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Mon, 15 Nov 2021 13:43:42 +0100 Subject: [PATCH 11/15] Skip not ready tests --- test/gradient/elixir_fmt_test.exs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/test/gradient/elixir_fmt_test.exs b/test/gradient/elixir_fmt_test.exs index a690be71..889bae71 100644 --- a/test/gradient/elixir_fmt_test.exs +++ b/test/gradient/elixir_fmt_test.exs @@ -20,6 +20,7 @@ defmodule Gradient.ElixirFmtTest do assert res == expected end + @tag :skip describe "types format" do test "wrong return type" do {_tokens, ast} = load("/type/Elixir.WrongRet.beam", "/type/wrong_ret.ex") @@ -32,16 +33,6 @@ defmodule Gradient.ElixirFmtTest do end end - def type_check_file(ast, opts) do - forms = AstSpecifier.specify(ast) - opts = Keyword.put(opts, :return_errors, true) - opts = Keyword.put(opts, :forms, forms) - - forms - |> :gradualizer.type_check_forms(opts) - |> Enum.map(fn {_, err} -> ElixirFmt.format_error(err, opts) end) - end - @tag :skip test "format_expr_type_error/4" do opts = [forms: basic_erlang_forms()] @@ -56,4 +47,14 @@ defmodule Gradient.ElixirFmtTest do def basic_erlang_forms() do [{:attribute, 1, :file, {@example_module_path, 1}}] end + + def type_check_file(ast, opts) do + forms = AstSpecifier.specify(ast) + opts = Keyword.put(opts, :return_errors, true) + opts = Keyword.put(opts, :forms, forms) + + forms + |> :gradualizer.type_check_forms(opts) + |> Enum.map(fn {_, err} -> ElixirFmt.format_error(err, opts) end) + end end From 623667649ff029337ae5ddf55760b4b73a11a19b Mon Sep 17 00:00:00 2001 From: Premwoik Date: Tue, 16 Nov 2021 15:10:18 +0100 Subject: [PATCH 12/15] Apply doc suggestions from code review Co-authored-by: Radek Szymczyszyn --- lib/gradient/elixir_type.ex | 2 +- lib/gradient/tokens.ex | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/gradient/elixir_type.ex b/lib/gradient/elixir_type.ex index 93177ec0..96ad3fa0 100644 --- a/lib/gradient/elixir_type.ex +++ b/lib/gradient/elixir_type.ex @@ -7,7 +7,7 @@ defmodule Gradient.ElixirType do """ @doc """ - Take type and prepare pretty string represntation. + Take type and prepare a pretty string representation. """ @spec pretty_print(tuple()) :: String.t() def pretty_print({:remote_type, _, [{:atom, _, mod}, {:atom, _, type}, args]}) do diff --git a/lib/gradient/tokens.ex b/lib/gradient/tokens.ex index 16de816f..7a4d4754 100644 --- a/lib/gradient/tokens.ex +++ b/lib/gradient/tokens.ex @@ -1,12 +1,12 @@ defmodule Gradient.Tokens do @moduledoc """ - Group of functions helping with manage tokens. + Functions useful for token management. """ alias Gradient.Types, as: T @doc """ - Drop tokens to the first conditional occurance. Returns type of the encountered - conditional and following tokens. + Drop tokens to the first conditional occurrence. Returns type of the encountered + conditional and the following tokens. """ @spec get_conditional(T.tokens(), integer(), T.options()) :: {:case, T.tokens()} @@ -33,7 +33,7 @@ defmodule Gradient.Tokens do @doc """ Drop tokens to the first list occurance. Returns type of the encountered - list and following tokens. + list and the following tokens. """ @spec get_list(T.tokens(), T.options()) :: {:list, T.tokens()} | {:keyword, T.tokens()} | {:charlist, T.tokens()} | :undefined @@ -58,8 +58,8 @@ defmodule Gradient.Tokens do end @doc """ - Drop tokens to the first tuple occurance. Returns type of the encountered - list and following tokens. + Drop tokens to the first tuple occurrence. Returns type of the encountered + list and the following tokens. """ @spec get_tuple(T.tokens(), T.options()) :: {:tuple, T.tokens()} | :undefined @@ -105,7 +105,7 @@ defmodule Gradient.Tokens do end @doc """ - Drop tokens while the token's line is lower than given location. + Drop tokens while the token's line is lower than the given location. """ @spec drop_tokens_to_line(T.tokens(), integer()) :: T.tokens() def drop_tokens_to_line(tokens, line) do @@ -128,8 +128,8 @@ defmodule Gradient.Tokens do @doc """ Drop the tokens to binary occurrence and then collect all belonging tokens. - Return tuple where first element is a list of tokens making binary, and second - element is a list of tokens after binary. + Return tuple where the first element is a list of tokens making up the binary, and the second + element is a list of tokens after the binary. """ @spec cut_tokens_to_bin(T.tokens(), integer()) :: {T.tokens(), T.tokens()} def cut_tokens_to_bin(tokens, line) do From 174c81549c94eb7e3417e362bd191eb7ecf44a3c Mon Sep 17 00:00:00 2001 From: Radek Szymczyszyn Date: Tue, 16 Nov 2021 15:59:45 +0100 Subject: [PATCH 13/15] Fix typo I missed before --- lib/gradient/tokens.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gradient/tokens.ex b/lib/gradient/tokens.ex index 7a4d4754..a2a5befa 100644 --- a/lib/gradient/tokens.ex +++ b/lib/gradient/tokens.ex @@ -32,7 +32,7 @@ defmodule Gradient.Tokens do end @doc """ - Drop tokens to the first list occurance. Returns type of the encountered + Drop tokens to the first list occurrence. Returns type of the encountered list and the following tokens. """ @spec get_list(T.tokens(), T.options()) :: From 44b474d1f17a1495cf7003753bab6464b8477e0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Tue, 16 Nov 2021 16:15:16 +0100 Subject: [PATCH 14/15] Apply other review suggestions - Add top-level type for a get_conditional return. - Rename flat_tokens to flatten_tokens. --- lib/gradient/ast_specifier.ex | 2 +- lib/gradient/tokens.ex | 29 ++++++++++++++++------------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/lib/gradient/ast_specifier.ex b/lib/gradient/ast_specifier.ex index ab52a689..f79e648a 100644 --- a/lib/gradient/ast_specifier.ex +++ b/lib/gradient/ast_specifier.ex @@ -404,7 +404,7 @@ defmodule Gradient.AstSpecifier do _ -> {bin_tokens, other_tokens} = cut_tokens_to_bin(tokens, line) - bin_tokens = flat_tokens(bin_tokens) + bin_tokens = flatten_tokens(bin_tokens) {elements, _} = context_mapper_fold(elements, bin_tokens, opts, &bin_element_mapper/3) {:bin, anno, elements} diff --git a/lib/gradient/tokens.ex b/lib/gradient/tokens.ex index a2a5befa..42f99ce5 100644 --- a/lib/gradient/tokens.ex +++ b/lib/gradient/tokens.ex @@ -4,17 +4,20 @@ defmodule Gradient.Tokens do """ alias Gradient.Types, as: T - @doc """ - Drop tokens to the first conditional occurrence. Returns type of the encountered - conditional and the following tokens. - """ - @spec get_conditional(T.tokens(), integer(), T.options()) :: + @typedoc "Type of conditional with following tokens" + @type conditional_t() :: {:case, T.tokens()} | {:cond, T.tokens()} | {:unless, T.tokens()} | {:if, T.tokens()} | {:with, T.tokens()} | :undefined + + @doc """ + Drop tokens to the first conditional occurrence. Returns type of the encountered + conditional and the following tokens. + """ + @spec get_conditional(T.tokens(), integer(), T.options()) :: conditional_t() def get_conditional(tokens, line, opts) do conditionals = [:if, :unless, :cond, :case, :with] {:ok, limit_line} = Keyword.fetch(opts, :end_line) @@ -38,7 +41,7 @@ defmodule Gradient.Tokens do @spec get_list(T.tokens(), T.options()) :: {:list, T.tokens()} | {:keyword, T.tokens()} | {:charlist, T.tokens()} | :undefined def get_list(tokens, opts) do - tokens = flat_tokens(tokens) + tokens = flatten_tokens(tokens) {:ok, limit_line} = Keyword.fetch(opts, :end_line) res = @@ -148,26 +151,26 @@ defmodule Gradient.Tokens do end @doc """ - Flat the tokens, mostly binaries or string interpolation. + Flatten the tokens, mostly binaries or string interpolation. """ - @spec flat_tokens(T.tokens()) :: T.tokens() - def flat_tokens(tokens) do - Enum.map(tokens, &flat_token/1) + @spec flatten_tokens(T.tokens()) :: T.tokens() + def flatten_tokens(tokens) do + Enum.map(tokens, &flatten_token/1) |> Enum.concat() end # Private - defp flat_token(token) do + defp flatten_token(token) do case token do {:bin_string, _, [s]} = t when is_binary(s) -> [t] {:bin_string, _, ts} -> - flat_tokens(ts) + flatten_tokens(ts) {{_, _, nil}, {_, _, nil}, ts} -> - flat_tokens(ts) + flatten_tokens(ts) str when is_binary(str) -> [{:str, {0, 0, nil}, str}] From afa71aa916c9aa7647c0b89570c10c1cfff38a05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Wojtasik?= Date: Wed, 17 Nov 2021 18:00:14 +0100 Subject: [PATCH 15/15] Test pretty printing types in elixir --- test/examples/type/Elixir.WrongRet.beam | Bin 4148 -> 4252 bytes test/examples/type/wrong_ret.ex | 7 +- test/gradient/elixir_fmt_test.exs | 159 ++++++++++++++++++++++-- 3 files changed, 151 insertions(+), 15 deletions(-) diff --git a/test/examples/type/Elixir.WrongRet.beam b/test/examples/type/Elixir.WrongRet.beam index 472f47c988dae153606bca9bde48705589ff5a22..cf73ca4650e9d1546f558dc5187214ebb4150003 100644 GIT binary patch delta 2295 zcmY+Gdpwle8pq!;vt`C@Vi=4u?w4#b8VyN8 zXeV+>(v*#ykVNiLY#b#t-A+0=>y`7zem?K>d4B7=p7p-#UGG}!U3XRaYPfv|Jjf7) zE!bmEb)mAnEFcKco{wwKUv!m{j0s@{oQPpE!U96$7+f>NSu?!B-`Gwg15jgNWB@^S z%-|5vwgv(>0}k6w;D*3)+!;g}hekolBE)dKC=nS94nl~@QMSWCo|goG8~}I<%?&0B zygWeU0Z|NtKv{`b3d(p;CQxXBaE3Ck4iE)Eh!l(rOjLLRKokKXQ7}p{QRNK)LI6aK zLW@Gmau8zbycvL$0n*q+2r*6GDj-Bav?#PBxS2Lj3=5D7Kspp!9*lH(5FjK#^e8e& z9^{I}BE)w5gh1(o_xj0ILCUY2orjeHjQ^1Y| zHWdWvfJofZS|BQKX$=qwTUrf7#VrlrLt#t9?eJR~&XwQNaIV}>ExMV28oGA&;06F*`BiZ zNgA;xty9-7+O_@s{3t0hzBCJhoS8xJNWe4uZ=*$Jv0?zzO^t+Wj*A3C$oN>sW`Ti) z@B;6iV?PzEN&fVBNj&?p5)&f2Fs)tqwjb&G}Jg$CA?}U))vj%{lW&_SYz_lDfPB z8=3KL&l-HKaC_I}y?WC_O?aDAl4&jqD`K-mg@9ex%L#gGYH{io^&yU|!}Ciy>1=dfwnC*1S81#zBhV0GqXa8gH|RY9(dH6CSE0{hfy_IE z3>)$u(^s55gt$w?zRea#{;QhwV&-5@g56vUvi971dsV6Yf2^;ur7?%pqldovZ*S2T zxTt#mzVx{yCxY+6K`&|{9`ycJ)EJS`6S(KW2bJWB>$iEGd8}4HUk4Y*LhPYJx2q4S zjOG=cG^aAF9rM5lok6S>z8d z1ewL993J%vdD51((^z-RCr)Pc&bIvP%>CqquQxI_W{fSLt8n`Zj^w0;|1;h+B_bTM zmYiNIwdri1i8Km5ef~Z@IYqs>mi|tIeLYXQ%gVobknS-;YRg(;2)vAKbKX8LcB2d; z{~cOZm?jc**_IPK#UJmj5*F&~n7QA{KI0V-h#j9eE3bdl@{>VzyPtQ5niV!t#(|-A z4*l5e^@n5L-U}po-l%IeYiiHMO5U%}>n}SAaV|n!v6`F<)jlpZr`6ld+&xa#%iR1X z;mFWbN^5d8p{n)He!t45&U)pA-nb!OTHhy^dBZrV3q_pbhIsb~d}HW0rMK=DcpjrC zcW?Y_(?*pVtCFWHN`2ruV1LlfeQK=C@cm<{>i4Bf{n~M-a#~x&C#qV{RZmEX-`KX8XZCR~Gbk1;bSrp? z{-Vg{7ryXw>M+8I&X{Ce@$c=ow8x^TtH?jkzu!n}3I46wOp)9uoIYh7-c)qC5x;Cd z&E4iYZALU!Ft*}D{v4ipF@xi>|ieMQWV94G4VnI6aEi6Rq6az`wz9oHpt@i#8N zb;gJu>@>~S@)s|wa{>wFf;9zXIhleJZ1gay=lP$dTYgLxt za|Z>EVVuXVo@a-8IG)NCpDS$3doAU%rVhWG%<8_xs}t|X za@wSm8jLK)iGs}IoiEQQxxZCky)uXPcCc?(UuyYO^r=nn*qb9l^A${yx}iwmy805b zBC7^smu5XMZ+SCa=qd3)XPQtu`|^Q2>)cSyZSS)_8#S%IvtLjj$G+TVbci;7M?|ki zgU!*NZ5IJSsI*jg!`vPcNoUhz0)iT1#LW>6%Mt_xmo6=igpV9#iCx@f3GoJ5sS$*O zGo1lW7?3US{~P?j+uW07&;;_)e6$K$4XugRL7S|hWzceH1@sQI5!wKf_<4u{M;F5o GHuOIfvvBeN delta 2121 zcmXxl3p|ti9|!PfV`DB^YsPXLX+smGMMp7aCdb{B({Y*H8iuo}MrkflH0@;oX&3@|KIENeSV+U`}sZ3>v?{!=lT7_#%%Gu2mBCVozUT@NJP-rwzc)4%bpqK4#E7bjs$0N= zQ!z>+$cq3e2~R883kcvfVW75vQe;Il2W9jvK@bV_V3NMI&EROTmN$W3e=Qq>e&bqJ zT!&xFim|$DSr7C&Ygus~_isjiLv5j(a*)7-ox}vX0zq&zUmVV2O&nyg$Z?OuSS<7d zmW7VFyJb$l6=JgObM-yx|Ky*0r3EfXiH4C|oz%?HYLCNJQ%4G)nYY#?3Zht3IH>|+EXC2(B+b&~j~`Iz^Dbk(|Q(Z(mQ24A0J?x&UWo4QV(GD6FjI-2yApw;e$fNzL}`6=`iF-c`LpYFDku@ z!S}N6=Xekv-OZ*YWiG%m(Br`xD&&z6u9yX7#rz285QZC9e0frhfEWCZiHg+R`0t!z zVrYBgqrlldApv>PjA0ePi{$dG61gdnoMc{H*ikF+MYH3ht)jVM5r@O#*eQ|R&`7?p z8)+hdAeV#DYzR_H&h&wxb^qauvx+P|DZW;1f>u?|NT=$(Tx>tZezsD} z&OiU`CdawV<*btB(BGdwTH}RIx*&2Juc**!B8S0jXUO+otln=_35C%T;E-Wr^0E0bvN12uu2G z-JI_^Jc0}Q&dNlMan@rh%|=EcA8IU{up|JN@#x0ul(d~8kJ%2Xz<|rqYL-9)QS4sq~ ziQ3Wl+Hv9PlO9uKct9$%H!To@C(Q}a=C zKe2B-pHCpxpo*UCOplGe*?kUy>Jj8HF}A9c$kc=6+tVB7rkpMMJe^0F&b$jk@rXGF zC80x2OOGU%e0|5cYfyC!t>_;19-0@4lf{jm!IV&2DWfp?V!KWAt* zeDMvvA<*N^L4C^bAukhKO~SQR%U`zIb_6U{21ThH>8xGqt+;<>lzzk}ofi#RHVvpt zCTgoD;;f@q(^iu2r#CL_s~sV|CvG#!^OU0h`_OL|fBbYzyyj41d(R8T@IhFnp<4%< z6M>zRb&1}U$d5P$Z>4X(D)>2Xv=*A@-6B(1W zo2{N6;hpd|yZ4lP@m*}q*p@s@6YpZ!+bY=@&8D_`m)riOef35=4r>K0`5bO>9}li- zAC?C*$LUYTEe@YLfJ<1o9`RN(vo7W>Q)E@2AA);XunZ3fmBeWGISFnr$7LO;d}YH- z&2H>(l#{l|mzkB4R~wkAeit38>z12FOL`ggUsNeZ0iqw}?D9}`_vg4*oT}&hPBhOA zE?6`;$X?ec85A`O2VIMpmCukC9LXTkoTBbGpW(|bs~>WyApW`A%r2+g?cwpXGA$n) z7d<@t`RIWQroCw^hXo&!{R0ZVdOMotga6khA)E&mUmL}*h%n%bi-uyqmsw+x6E@l9 zO3|}o6V(g?ANJweY9+el^88ccrn)14AMj#pMAZn{479gIwrn%xT$T=KInHEYfxCt9AaHkmQi zE`#6MIb|>vyi7DSEV2K?$-A%EZ}0jIjge=AhPgE z>23Sx=l1SSU%wXSRxM4(Udfe> zCE8XFQT$qpb7Op`7^q8W9)>~bZ3{bp7iEYtMwy~W!Zfr#4EhIP Ci#S~X diff --git a/test/examples/type/wrong_ret.ex b/test/examples/type/wrong_ret.ex index 6fd794d4..9171431c 100644 --- a/test/examples/type/wrong_ret.ex +++ b/test/examples/type/wrong_ret.ex @@ -8,8 +8,8 @@ defmodule WrongRet do @spec ret_wrong_atom3() :: atom() def ret_wrong_atom3, do: %{a: 1} - @spec ret_wrong_atom4() :: atom() - def ret_wrong_atom4, do: false + # @spec ret_wrong_atom4() :: atom() + # def ret_wrong_atom4, do: false @spec ret_wrong_integer() :: integer() def ret_wrong_integer, do: 1.0 @@ -46,4 +46,7 @@ defmodule WrongRet do @spec ret_wrong_map() :: map() def ret_wrong_map, do: {:a, 1, 2} + + @spec ret_wrong_fun() :: (... -> atom()) + def ret_wrong_fun, do: fn -> 12 end end diff --git a/test/gradient/elixir_fmt_test.exs b/test/gradient/elixir_fmt_test.exs index 889bae71..16fcbb51 100644 --- a/test/gradient/elixir_fmt_test.exs +++ b/test/gradient/elixir_fmt_test.exs @@ -8,6 +8,10 @@ defmodule Gradient.ElixirFmtTest do @example_module_path "test/examples/simple_app.ex" + setup_all config do + load_wrong_ret_error_examples(config) + end + test "try_highlight_in_context/2" do opts = [forms: basic_erlang_forms()] expression = {:integer, 31, 12} @@ -20,16 +24,110 @@ defmodule Gradient.ElixirFmtTest do assert res == expected end - @tag :skip describe "types format" do - test "wrong return type" do - {_tokens, ast} = load("/type/Elixir.WrongRet.beam", "/type/wrong_ret.ex") - opts = [] - errors = type_check_file(ast, opts) + test "return integer() instead atom()", %{wrong_ret_errors: errors} do + msg = format_error_to_binary(errors.ret_wrong_atom) + + assert String.contains?(msg, "atom()") + assert String.contains?(msg, "1") + end + + test "return tuple() instead atom()", %{wrong_ret_errors: errors} do + msg = format_error_to_binary(errors.ret_wrong_atom2) + + assert String.contains?(msg, "atom()") + assert String.contains?(msg, "{:ok, []}") + end + + test "return map() instead atom()", %{wrong_ret_errors: errors} do + msg = format_error_to_binary(errors.ret_wrong_atom3) + + assert String.contains?(msg, "atom()") + assert String.contains?(msg, "%{required(:a) => 1}") + end + + test "return float() instead integer()", %{wrong_ret_errors: errors} do + msg = format_error_to_binary(errors.ret_wrong_integer) + + assert String.contains?(msg, "integer()") + assert String.contains?(msg, "1.0") + end + + test "return atom() instead integer()", %{wrong_ret_errors: errors} do + msg = format_error_to_binary(errors.ret_wrong_integer2) + + assert String.contains?(msg, "integer()") + assert String.contains?(msg, ":ok") + end + + test "return boolean() instead integer()", %{wrong_ret_errors: errors} do + msg = format_error_to_binary(errors.ret_wrong_integer3) + + assert String.contains?(msg, "integer()") + assert String.contains?(msg, "true") + end + + test "return list() instead integer()", %{wrong_ret_errors: errors} do + msg = format_error_to_binary(errors.ret_wrong_integer4) + + assert String.contains?(msg, "integer()") + assert String.contains?(msg, "nonempty_list()") + end + + test "return integer() out of the range()", %{wrong_ret_errors: errors} do + msg = format_error_to_binary(errors.ret_out_of_range_int) + + assert String.contains?(msg, "range(1, 10)") + assert String.contains?(msg, "12") + end + + test "return atom() instead boolean()", %{wrong_ret_errors: errors} do + msg = format_error_to_binary(errors.ret_wrong_boolean) + + assert String.contains?(msg, "boolean()") + assert String.contains?(msg, ":ok") + end + + test "return binary() instead boolean()", %{wrong_ret_errors: errors} do + msg = format_error_to_binary(errors.ret_wrong_boolean2) + + assert String.contains?(msg, "boolean()") + assert String.contains?(msg, "binary()") + end + + test "return integer() instead boolean()", %{wrong_ret_errors: errors} do + msg = format_error_to_binary(errors.ret_wrong_boolean3) + + assert String.contains?(msg, "boolean()") + assert String.contains?(msg, "1") + end + + test "return keyword() instead boolean()", %{wrong_ret_errors: errors} do + msg = format_error_to_binary(errors.ret_wrong_boolean4) + + assert String.contains?(msg, "boolean()") + assert String.contains?(msg, "nonempty_list()") + end + + test "return list() instead keyword()", %{wrong_ret_errors: errors} do + msg = format_error_to_binary(errors.ret_wrong_keyword) + + assert String.contains?(msg, "{atom(), any()}") + assert String.contains?(msg, "1") + end + + test "return tuple() instead map()", %{wrong_ret_errors: errors} do + msg = format_error_to_binary(errors.ret_wrong_map) - for e <- errors do - :io.put_chars(e) - end + assert String.contains?(msg, "map()") + assert String.contains?(msg, "{:a, 1, 2}") + end + + test "return lambda with wrong returned type", %{wrong_ret_errors: errors} do + msg = format_error_to_binary(errors.ret_wrong_fun) + + assert String.contains?(msg, "atom()") + assert String.contains?(msg, "12") end end @@ -44,17 +142,52 @@ defmodule Gradient.ElixirFmtTest do IO.puts(res) end - def basic_erlang_forms() do + # Helpers + + defp basic_erlang_forms() do [{:attribute, 1, :file, {@example_module_path, 1}}] end - def type_check_file(ast, opts) do + defp type_check_file(ast, opts) do forms = AstSpecifier.specify(ast) opts = Keyword.put(opts, :return_errors, true) opts = Keyword.put(opts, :forms, forms) - forms - |> :gradualizer.type_check_forms(opts) - |> Enum.map(fn {_, err} -> ElixirFmt.format_error(err, opts) end) + errors = + forms + |> :gradualizer.type_check_forms(opts) + |> Enum.map(&elem(&1, 1)) + + {errors, forms} + end + + defp format_error_to_binary(error, opts \\ []) do + error + |> ElixirFmt.format_error(opts) + |> :erlang.iolist_to_binary() + end + + @spec load_wrong_ret_error_examples(map()) :: map() + defp load_wrong_ret_error_examples(config) do + {_tokens, ast} = load("/type/Elixir.WrongRet.beam", "/type/wrong_ret.ex") + + {errors, forms} = type_check_file(ast, []) + names = get_function_names_from_ast(forms) + + errors_map = + Enum.zip(names, errors) + |> Map.new() + + Map.put(config, :wrong_ret_errors, errors_map) + end + + @spec get_function_names_from_ast([tuple()]) :: [atom()] + def get_function_names_from_ast(ast) do + ast + |> Enum.filter(fn + {:function, _, name, _, _} -> name != :__info__ + _ -> false + end) + |> Enum.map(&elem(&1, 2)) end end