From c6bcf8faa9ce856b3b9644a7f85a51dd782cee10 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 21 Apr 2024 10:46:39 +0200 Subject: [PATCH 001/235] squash --- lib/elixir_sense/core/compiler.ex | 3819 +++++++++++++++++ lib/elixir_sense/core/metadata_builder.ex | 90 +- lib/elixir_sense/core/state.ex | 101 +- test/elixir_sense/core/compiler_test.exs | 625 +++ .../core/metadata_builder/alias_test.exs | 1 + 5 files changed, 4592 insertions(+), 44 deletions(-) create mode 100644 lib/elixir_sense/core/compiler.ex create mode 100644 test/elixir_sense/core/compiler_test.exs diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex new file mode 100644 index 00000000..40174ac0 --- /dev/null +++ b/lib/elixir_sense/core/compiler.ex @@ -0,0 +1,3819 @@ +defmodule ElixirSense.Core.Compiler do + import ElixirSense.Core.State + require Logger + + @env :elixir_env.new() + def env, do: @env + + def expand(ast, state, env) do + try do + do_expand(ast, state, env) + catch + kind, payload -> + Logger.warning("Unable to expand ast node #{inspect(ast)}: #{Exception.format(kind, payload, __STACKTRACE__)}") + {ast, state, env} + end + end + + ## =/2 + + defp do_expand({:=, meta, [left, right]}, s, e) do + assert_no_guard_scope(e.context, "=/2") + {e_right, sr, er} = expand(right, s, e) + # dbg(e_right) + {e_left, sl, el} = __MODULE__.Clauses.match(&expand/3, left, sr, s, er) + # dbg(e_left) + # dbg(el.versioned_vars) + # dbg(sl.vars) + refute_parallel_bitstring_match(e_left, e_right, e, Map.get(e, :context) == :match) + # {{:=, meta, [e_left, e_right]}, sl, el |> Map.from_struct()} |> dbg(limit: :infinity) + # el = el |> :elixir_env.with_vars(sl.vars |> elem(0)) + # sl = sl |> add_current_env_to_line(Keyword.fetch!(meta, :line), el) + # dbg(sl) + {{:=, meta, [e_left, e_right]}, sl, el} + end + + # Literal operators + + defp do_expand({:{}, meta, args}, state, env) do + {args, state, env} = expand_args(args, state, env) + {{:{}, meta, args}, state, env} + end + + defp do_expand({:%{}, meta, args}, state, env) do + __MODULE__.Map.expand_map(meta, args, state, env) + end + + defp do_expand({:%, meta, [left, right]}, state, env) do + __MODULE__.Map.expand_struct(meta, left, right, state, env) + end + + defp do_expand({:<<>>, meta, args}, state, env) do + __MODULE__.Bitstring.expand(meta, args, state, env, false) + end + + defp do_expand({:->, _meta, [_, _]}, _s, _e), do: raise("unhandled_arrow_op") + + defp do_expand({:"::", _meta, [_, _]}, _s, _e), do: raise("unhandled_type_op") + + defp do_expand({:|, _meta, [_, _]}, _s, _e), do: raise("unhandled_cons_op") + + ## __block__ + + defp do_expand({:__block__, _meta, []}, s, e), do: {nil, s, e} + + defp do_expand({:__block__, _meta, [arg]}, s, e) do + # s = s |> add_current_env_to_line(Keyword.fetch!(meta, :line), e) + expand(arg, s, e) + end + + defp do_expand({:__block__, meta, args}, s, e) when is_list(args) do + {e_args, sa, ea} = expand_block(args, [], meta, s, e) + {{:__block__, meta, e_args}, sa, ea} + end + + ## __aliases__ + + defp do_expand({:__aliases__, meta, [head | tail] = list}, state, env) do + case Macro.Env.expand_alias(env, meta, list, trace: false) do + {:alias, alias} -> + # TODO? + # A compiler may want to emit a :local_function trace in here. + # Elixir also warns on easy to confuse aliases, such as True/False/Nil. + {alias, state, env} + + :error -> + {head, state, env} = expand(head, state, env) + + if is_atom(head) do + # TODO? + # A compiler may want to emit a :local_function trace in here. + {Module.concat([head | tail]), state, env} + else + raise "invalid_alias" + # {{:__aliases__, meta, [head | tail]}, state, env} + end + end + end + + ## require, alias, import + + defp do_expand({form, meta, [{{:., _, [base, :{}]}, _, refs} | rest]}, state, env) + when form in [:require, :alias, :import] do + case rest do + [] -> + expand_multi_alias_call(form, meta, base, refs, [], state, env) + + [opts] -> + if Keyword.has_key?(opts, :as) do + raise "as_in_multi_alias_call" + end + + expand_multi_alias_call(form, meta, base, refs, opts, state, env) + end + end + + defp do_expand({form, meta, [arg]}, state, env) when form in [:require, :alias, :import] do + expand({form, meta, [arg, []]}, state, env) + end + + defp do_expand({:alias, meta, [arg, opts]}, state, env) do + assert_no_match_or_guard_scope(env.context, "alias") + + state = + state + |> add_current_env_to_line(Keyword.fetch!(meta, :line), env) + + # no need to call expand_without_aliases_report - we never report + {arg, state, env} = expand(arg, state, env) + {opts, state, env} = expand_opts(meta, :alias, [:as, :warn], no_alias_opts(opts), state, env) + + if is_atom(arg) do + # TODO check difference with + # elixir_aliases:alias(Meta, Ref, IncludeByDefault, Opts, E, true) + # TODO PR to elixir with is_atom(module) check? + case Macro.Env.define_alias(env, meta, arg, [trace: false] ++ opts) do + {:ok, env} -> + {arg, state, env} + + {:error, _} -> + raise "elixir_aliases" + end + else + raise "expected_compile_time_module" + end + end + + defp do_expand({:require, meta, [arg, opts]}, state, env) do + assert_no_match_or_guard_scope(env.context, "require") + original_env = env + + state = + state + |> add_current_env_to_line(Keyword.fetch!(meta, :line), env) + + # no need to call expand_without_aliases_report - we never report + {arg, state, env} = expand(arg, state, env) + + {opts, state, env} = + expand_opts(meta, :require, [:as, :warn], no_alias_opts(opts), state, env) + + case Keyword.fetch(meta, :defined) do + {:ok, mod} when is_atom(mod) -> + env = %{env | context_modules: [mod | env.context_modules]} + + state = + case original_env do + %{function: nil} -> state + _ -> %{state | runtime_modules: [mod | state.runtime_modules]} + end + + # TODO how to test that case? + # TODO remove this case - in elixir this is a hack used only by special require call emitted by defmodule + # Macro.Env.define_alias is not fully equivalent - it calls alias with IncludeByDefault set to true + # we counter it with only calling it if :as option is set + # {arg, state, alias(meta, e_ref, false, e_opts, ea)} + if Keyword.has_key?(opts, :as) do + case Macro.Env.define_alias(env, meta, arg, [trace: false] ++ opts) do + {:ok, env} -> + {arg, state, env} + + {:error, _} -> + raise "elixir_aliases" + end + else + {arg, state, env} + end + + :error when is_atom(arg) -> + # TODO check differences + # TODO ensure loaded? + # ElixirAliases.ensure_loaded(meta, e_ref, et) + # re = ElixirAliases.require(meta, e_ref, e_opts, et, true) + # {e_ref, st, alias(meta, e_ref, false, e_opts, re)} + case Macro.Env.define_require(env, meta, arg, [trace: false] ++ opts) do + {:ok, env} -> + {arg, state, env} + + {:error, _} -> + raise "elixir_aliases" + end + + :error -> + raise "expected_compile_time_module" + end + end + + defp do_expand({:import, meta, [arg, opts]}, state, env) do + assert_no_match_or_guard_scope(env.context, "import") + + state = + state + |> add_current_env_to_line(Keyword.fetch!(meta, :line), env) + + # no need to call expand_without_aliases_report - we never report + {arg, state, env} = expand(arg, state, env) + {opts, state, env} = expand_opts(meta, :import, [:only, :except, :warn], opts, state, env) + + if is_atom(arg) do + # TODO check difference + # elixir_aliases:ensure_loaded(Meta, ERef, ET) + # elixir_import:import(Meta, ERef, EOpts, ET, true, true) + with true <- Code.ensure_loaded?(arg), + {:ok, env} <- Macro.Env.define_import(env, meta, arg, [trace: false] ++ opts) do + {arg, state, env} + else + _ -> + raise "elixir_import" + end + else + raise "expected_compile_time_module" + end + end + + # Compilation environment macros + + defp do_expand({:__MODULE__, meta, ctx}, state, env) when is_atom(ctx) do + line = Keyword.get(meta, :line, 0) + state = if line > 0, do: add_current_env_to_line(state, line, env), else: state + + {env.module, state, env} + end + + defp do_expand({:__DIR__, meta, ctx}, state, env) when is_atom(ctx) do + line = Keyword.get(meta, :line, 0) + state = if line > 0, do: add_current_env_to_line(state, line, env), else: state + + {Path.dirname(env.file), state, env} + end + + defp do_expand({:__CALLER__, meta, ctx} = caller, s, e) when is_atom(ctx) do + assert_no_match_scope(e.context, "__CALLER__") + # unless s.caller do + # function_error(meta, e, __MODULE__, :caller_not_allowed) + # end + line = Keyword.get(meta, :line, 0) + s = if line > 0, do: add_current_env_to_line(s, line, e), else: s + + {caller, s, e} + end + + defp do_expand({:__STACKTRACE__, meta, ctx} = stacktrace, s, e) when is_atom(ctx) do + assert_no_match_scope(e.context, "__STACKTRACE__") + # unless s.stacktrace do + # function_error(meta, e, __MODULE__, :stacktrace_not_allowed) + # end + line = Keyword.get(meta, :line, 0) + s = if line > 0, do: add_current_env_to_line(s, line, e), else: s + + {stacktrace, s, e} + end + + defp do_expand({:__ENV__, meta, ctx}, s, e) when is_atom(ctx) do + assert_no_match_scope(e.context, "__ENV__") + + line = Keyword.get(meta, :line, 0) + s = if line > 0, do: add_current_env_to_line(s, line, e), else: s + + {escape_map(escape_env_entries(meta, s, e)), s, e} + end + + defp do_expand({{:., dot_meta, [{:__ENV__, meta, atom}, field]}, call_meta, []}, s, e) + when is_atom(atom) and is_atom(field) do + assert_no_match_scope(e.context, "__ENV__") + + line = Keyword.get(call_meta, :line, 0) + s = if line > 0, do: add_current_env_to_line(s, line, e), else: s + + env = escape_env_entries(meta, s, e) + + case Map.fetch(env, field) do + {:ok, value} -> {value, s, e} + :error -> {{{:., dot_meta, [escape_map(env), field]}, call_meta, []}, s, e} + end + end + + # Quote + + defp do_expand({unquote_call, _meta, [_]}, _s, _e) + when unquote_call in [:unquote, :unquote_splicing], + do: raise("unquote_outside_quote") + + defp do_expand({:quote, meta, [opts]}, s, e) when is_list(opts) do + case Keyword.fetch(opts, :do) do + {:ok, do_block} -> + new_opts = Keyword.delete(opts, :do) + expand({:quote, meta, [new_opts, [{:do, do_block}]]}, s, e) + + :error -> + raise "missing_option" + end + end + + defp do_expand({:quote, _meta, [_]}, _s, _e), do: raise("invalid_args") + + defp do_expand({:quote, meta, [opts, do_block]}, s, e) when is_list(do_block) do + exprs = + case Keyword.fetch(do_block, :do) do + {:ok, expr} -> expr + :error -> raise "missing_option" + end + + valid_opts = [:context, :location, :line, :file, :unquote, :bind_quoted, :generated] + {e_opts, st, et} = expand_opts(meta, :quote, valid_opts, opts, s, e) + + context = Keyword.get(e_opts, :context, e.module || :"Elixir") + + {file, line} = + case Keyword.fetch(e_opts, :location) do + {:ok, :keep} -> {e.file, false} + :error -> {Keyword.get(e_opts, :file, nil), Keyword.get(e_opts, :line, false)} + end + + {binding, default_unquote} = + case Keyword.fetch(e_opts, :bind_quoted) do + {:ok, bq} -> + if is_list(bq) and Enum.all?(bq, &match?({key, _} when is_atom(key), &1)) do + {bq, false} + else + raise "invalid_bind_quoted_for_quote" + end + + :error -> + {[], true} + end + + unquote_opt = Keyword.get(e_opts, :unquote, default_unquote) + generated = Keyword.get(e_opts, :generated, false) + + # TODO this is a stub only + # res = expand_quote(exprs, st, et) + # res |> elem(0) |> IO.inspect + # res + {q, prelude} = + __MODULE__.Quote.build(meta, line, file, context, unquote_opt, generated) |> dbg + + quoted = __MODULE__.Quote.quote(meta, exprs |> dbg, binding, q, prelude, et) |> dbg + expand(quoted, st, et) + end + + defp do_expand({:quote, _meta, [_, _]}, _s, _e), do: raise("invalid_args") + + # Functions + + defp do_expand({:&, meta, [{:super, super_meta, args} = expr]}, s, e) when is_list(args) do + assert_no_match_or_guard_scope(e.context, "&") + + case resolve_super(meta, length(args), e) do + {kind, name, _} when kind in [:def, :defp] -> + expand_fn_capture(meta, {name, super_meta, args}, s, e) + + _ -> + expand_fn_capture(meta, expr, s, e) + end + end + + defp do_expand({:&, meta, [{:/, arity_meta, [{:super, super_meta, context}, arity]} = expr]}, s, e) + when is_atom(context) and is_integer(arity) do + assert_no_match_or_guard_scope(e.context, "&") + + case resolve_super(meta, arity, e) do + {kind, name, _} when kind in [:def, :defp] -> + {{:&, meta, [{:/, arity_meta, [{name, super_meta, context}, arity]}]}, s, e} + + _ -> + expand_fn_capture(meta, expr, s, e) + end + end + + defp do_expand({:&, meta, [arg]}, s, e) do + assert_no_match_or_guard_scope(e.context, "&") + expand_fn_capture(meta, arg, s, e) + end + + defp do_expand({:fn, meta, pairs}, s, e) do + assert_no_match_or_guard_scope(e.context, "fn") + __MODULE__.Fn.expand(meta, pairs, s, e) + end + + # case/cond/try/receive + + defp do_expand({:cond, meta, [opts]}, s, e) do + assert_no_match_or_guard_scope(e.context, "cond") + assert_no_underscore_clause_in_cond(opts, e) + {e_clauses, sc, ec} = __MODULE__.Clauses.cond(meta, opts, s, e) + {{:cond, meta, [e_clauses]}, sc, ec} + end + + defp do_expand({:case, meta, [expr, options]}, s, e) do + assert_no_match_or_guard_scope(e.context, "case") + expand_case(meta, expr, options, s, e) + end + + defp do_expand({:receive, meta, [opts]}, s, e) do + assert_no_match_or_guard_scope(e.context, "receive") + {e_clauses, sc, ec} = __MODULE__.Clauses.receive(meta, opts, s, e) + {{:receive, meta, [e_clauses]}, sc, ec} + end + + defp do_expand({:try, meta, [opts]}, s, e) do + assert_no_match_or_guard_scope(e.context, "try") + {e_clauses, sc, ec} = __MODULE__.Clauses.try(meta, opts, s, e) + {{:try, meta, [e_clauses]}, sc, ec} + end + + defp do_expand({:for, _, [_ | _]} = expr, s, e), do: expand_for(expr, s, e, true) + + defp do_expand({:with, meta, [_ | _] = args}, s, e) do + assert_no_match_or_guard_scope(e.context, "with") + __MODULE__.Clauses.with(meta, args, s, e) + end + + # Super + + defp do_expand({:super, meta, args}, s, e) when is_list(args) do + assert_no_match_or_guard_scope(e.context, "super") + {kind, name, _} = resolve_super(meta, length(args), e) + {e_args, sa, ea} = expand_args(args, s, e) + {{:super, [{:super, {kind, name}} | meta], e_args}, sa, ea} + end + + # Vars + + ## Pin operator + # It only appears inside match and it disables the match behaviour. + + defp do_expand({:^, meta, [arg]}, %{prematch: {prematch, _, _}, vars: {_, write}} = s, e) do + no_match_s = %{s | prematch: :pin, vars: {prematch, write}} + + case expand(arg, no_match_s, %{e | context: nil}) do + {{name, _, kind} = var, %{unused: unused}, _} when is_atom(name) and is_atom(kind) -> + {{:^, meta, [var]}, %{s | unused: unused}, e} + + _ -> + # function_error(meta, e, __MODULE__, {:invalid_arg_for_pin, arg}) + {{:^, meta, [arg]}, s, e} + end + end + + defp do_expand({:^, meta, [arg]}, s, e) do + # function_error(meta, e, __MODULE__, {:pin_outside_of_match, arg}) + {{:^, meta, [arg]}, s, e} + end + + defp do_expand({:_, _meta, kind} = var, s, %{context: _context} = e) when is_atom(kind) do + # if context != :match, do: function_error(meta, e, __MODULE__, :unbound_underscore) + {var, s, e} + end + + defp do_expand({:_, _meta, kind} = var, s, %{context: :match} = e) when is_atom(kind) do + {var, s, e} + end + + defp do_expand({name, meta, kind}, s, %{context: :match} = e) + when is_atom(name) and is_atom(kind) do + %{ + prematch: {_, prematch_version, _}, + unused: {unused, version}, + vars: {read, write} + } = s + + pair = {name, var_context(meta, kind)} + + case read do + # Variable was already overridden + %{^pair => var_version} when var_version >= prematch_version -> + # maybe_warn_underscored_var_repeat(meta, name, kind, e) + new_unused = var_used(meta, pair, var_version, unused) + var = {name, [{:version, var_version} | meta], kind} + {var, %{s | unused: {new_unused, version}}, e} + + # Variable is being overridden now + %{^pair => _} -> + new_unused = var_unused(pair, meta, version, unused, true) + new_read = Map.put(read, pair, version) + new_write = if write != false, do: Map.put(write, pair, version), else: write + var = {name, [{:version, version} | meta], kind} + {var, %{s | vars: {new_read, new_write}, unused: {new_unused, version + 1}}, e} + + # Variable defined for the first time + _ -> + new_unused = var_unused(pair, meta, version, unused, false) + new_read = Map.put(read, pair, version) + new_write = if write != false, do: Map.put(write, pair, version), else: write + var = {name, [{:version, version} | meta], kind} + {var, %{s | vars: {new_read, new_write}, unused: {new_unused, version + 1}}, e} + end + end + + defp do_expand({name, meta, kind}, s, e) when is_atom(name) and is_atom(kind) do + %{vars: {read, _write}, unused: {unused, version}, prematch: prematch} = s + pair = {name, var_context(meta, kind)} + + result = + case read do + %{^pair => current_version} -> + case prematch do + {pre, _counter, {_bitsize, original}} -> + cond do + Map.get(pre, pair) != current_version -> + {:ok, current_version} + + Map.has_key?(pre, pair) -> + # TODO: Enable this warning on Elixir v1.19 + # TODO: Remove me on Elixir 2.0 + # warn about unpinned bitsize var + {:ok, current_version} + + not Map.has_key?(original, pair) -> + {:ok, current_version} + + true -> + :raise + end + + _ -> + {:ok, current_version} + end + + _ -> + prematch + end + + case result do + {:ok, pair_version} -> + # maybe_warn_underscored_var_access(meta, name, kind, e) + var = {name, [{:version, pair_version} | meta], kind} + {var, %{s | unused: {var_used(meta, pair, pair_version, unused), version}}, e} + + error -> + case Keyword.fetch(meta, :if_undefined) do + {:ok, :apply} -> + expand({name, meta, []}, s, e) + + # TODO: Remove this clause on v2.0 as we will raise by default + {:ok, :raise} -> + # function_error(meta, e, __MODULE__, {:undefined_var, name, kind}) + {{name, meta, kind}, s, e} + + # TODO: Remove this clause on v2.0 as we will no longer support warn + _ when error == :warn -> + # Warn about undefined var to call + # elixir_errors:file_warn(Meta, E, ?MODULE, {undefined_var_to_call, Name}), + expand({name, [{:if_undefined, :warn} | meta], []}, s, e) + + _ when error == :pin -> + # function_error(meta, e, __MODULE__, {:undefined_var_pin, name, kind}) + {{name, meta, kind}, s, e} + + _ -> + span_meta = __MODULE__.Env.calculate_span(meta, name) + # function_error(span_meta, e, __MODULE__, {:undefined_var, name, kind}) + {{name, span_meta, kind}, s, e} + end + end + end + + # Local calls + + defp do_expand({fun, meta, args}, state, env) + when is_atom(fun) and is_list(meta) and is_list(args) do + assert_no_ambiguous_op(fun, meta, args, state, env) + arity = length(args) + + # TODO check if it works in our case + # If we are inside a function, we support reading from locals. + allow_locals = match?({n, a} when fun != n or arity != a, env.function) + + case Macro.Env.expand_import(env, meta, fun, arity, + trace: false, + allow_locals: allow_locals, + check_deprecations: false + ) do + {:macro, module, callback} -> + # TODO there is a subtle difference - callback will call expander with state derrived from env via + # :elixir_env.env_to_ex(env) possibly losing some details + expand_macro(meta, module, fun, args, callback, state, env) + + {:function, module, fun} -> + # Transform to remote call - we may need to do rewrites + expand({{:., meta, [module, fun]}, meta, args}, state, env) + + :error -> + expand_local(meta, fun, args, state, env) + end + end + + ## Remote call + + defp do_expand({{:., dot_meta, [module, fun]}, meta, args}, state, env) + when (is_tuple(module) or is_atom(module)) and is_atom(fun) and is_list(meta) and + is_list(args) do + {module, state_l, env} = expand(module, __MODULE__.Env.prepare_write(state), env) + arity = length(args) + + if is_atom(module) do + case :elixir_rewrite.inline(module, fun, arity) do + {ar, an} -> + expand_remote(ar, dot_meta, an, meta, args, state, state_l, env) + + false -> + case Macro.Env.expand_require(env, meta, module, fun, arity, + trace: false, + check_deprecations: false + ) do + {:macro, module, callback} -> + # TODO there is a subtle difference - callback will call expander with state derived from env via + # :elixir_env.env_to_ex(env) possibly losing some details + expand_macro(meta, module, fun, args, callback, state, env) + + :error -> + # expand_remote(meta, module, fun, args, state, env) + expand_remote(module, dot_meta, fun, meta, args, state, state_l, env) + end + end + else + expand_remote(module, dot_meta, fun, meta, args, state, state_l, env) + end + end + + # Anonymous calls + + defp do_expand({{:., dot_meta, [expr]}, meta, args}, s, e) when is_list(args) do + assert_no_match_or_guard_scope(e.context, "anonymous call") + {[e_expr | e_args], sa, ea} = expand_args([expr | args], s, e) + + # if is_atom(e_expr) do + # function_error(meta, e, __MODULE__, {:invalid_function_call, e_expr}) + # end + + {{{:., dot_meta, [e_expr]}, meta, e_args}, sa, ea} + end + + # Invalid calls + + defp do_expand({_, meta, args} = _invalid, _s, _e) when is_list(meta) and is_list(args) do + raise "invalid_call" + end + + # Literals + + defp do_expand({left, right}, state, env) do + {[e_left, e_right], state, env} = expand_args([left, right], state, env) + {{e_left, e_right}, state, env} + end + + defp do_expand(list, s, %{context: :match} = e) when is_list(list) do + expand_list(list, &expand/3, s, e, []) + end + + defp do_expand(list, s, e) when is_list(list) do + {e_args, {se, _}, ee} = + expand_list(list, &expand_arg/3, {__MODULE__.Env.prepare_write(s), s}, e, []) + + {e_args, __MODULE__.Env.close_write(se, s), ee} + end + + defp do_expand(function, s, e) when is_function(function) do + type_info = :erlang.fun_info(function, :type) + env_info = :erlang.fun_info(function, :env) + + case {type_info, env_info} do + {{:type, :external}, {:env, []}} -> + {__MODULE__.Quote.fun_to_quoted(function), s, e} + + _ -> + raise "invalid_quoted_expr" + end + end + + defp do_expand(pid, s, e) when is_pid(pid) do + case e.function do + nil -> + {pid, s, e} + + _function -> + # TODO: Make me an error on v2.0 + # ElixirErrors.file_warn([], e, __MODULE__, {:invalid_pid_in_function, pid, function}) + {pid, s, e} + end + end + + # defp do_expand(0.0 = zero, s, %{context: :match} = e) when is_float(zero) do + # # ElixirErrors.file_warn([], e, __MODULE__, :invalid_match_on_zero_float) + # {zero, s, e} + # end + + defp do_expand(other, s, e) when is_number(other) or is_atom(other) or is_binary(other) do + {other, s, e} + end + + defp do_expand(other, _s, _e) do + raise "invalid_quoted_expr #{inspect(other)}" + end + + ## Macro handling + + defp expand_macro( + meta, + Kernel, + :defmodule, + [alias, [do: block]] = _args, + _callback, + state, + env + ) do + %{vars: vars, unused: unused} = state + original_env = env + assert_no_match_or_guard_scope(env.context, "defmodule/2") + + {expanded, _state, _env} = expand(alias, state, env) + + # {expanded, with_alias} = + # case is_atom(expanded) do + # true -> + # {full, old, opts} = alias_defmodule(alias, expanded, env) + # # Expand the module considering the current environment/nesting + # meta = [defined: full] ++ alias_meta(alias) + # {full, {:require, meta, [old, opts]}} + + # false -> + # {expanded, nil} + # end + + # The env inside the block is discarded + {_result, state, env} = + if is_atom(expanded) do + {full, env} = alias_defmodule(alias, expanded, env) + + # in elixir context_modules and runtime_modules are handled via special require expansion + # with :defined key set in meta + env = %{env | context_modules: [full | env.context_modules]} + + state = + case original_env do + %{function: nil} -> + state + + _ -> + # TODO how to test that? quote do defmodule? + %{state | runtime_modules: [full | state.runtime_modules]} + end + + {position, end_position} = extract_range(meta) + + state = + state + |> add_module_to_index(full, position, end_position, []) + + {result, state, _env} = expand(block, state, %{env | module: full}) + {result, state, env} + else + raise "unable to expand module alias" + # alias |> dbg + # keys = state |> Map.from_struct() |> Map.take([:vars, :unused]) + # keys |> dbg(limit: :infinity) + # block |> dbg + # # If we don't know the module name, do we still want to expand it here? + # # Perhaps it would be useful for dealing with local functions anyway? + # # But note that __MODULE__ will return nil. + + # # TODO + # expand(block, state, %{env | module: nil}) + end + + # restore vars from outer scope + state = %{state | vars: vars, unused: unused} + + # TODO hardcode expansion? + # to result of require (a module atom) and :elixir_module.compile dot call in block + + {{:__block__, [], []}, state, env} + end + + defp expand_macro(meta, Kernel, def_kind, [call, expr], _callback, state, env) + when def_kind in [:def, :defp, :defmacro, :defmacrop] do + dbg(call) + dbg(expr) + assert_no_match_or_guard_scope(env.context, :"{def_kind}/2") + module = assert_module_scope(env, def_kind, 2) + + %{vars: vars, unused: unused} = state + + # unquoted_call = :elixir_quote.has_unquotes(call) + # unquoted_expr = :elixir_quote.has_unquotes(expr) + # TODO expand the call and expression. + # TODO store mod_fun_to_pos + line = Keyword.fetch!(meta, :line) + + state = + state + |> add_current_env_to_line(line, env) + + state = %{state | vars: {%{}, false}, unused: {%{}, 0}} + + {name_and_args, guards} = __MODULE__.Utils.extract_guards(call) + + {name, _meta_1, args} = + case name_and_args do + {n, m, a} when is_atom(n) and is_atom(a) -> {n, m, []} + {n, m, a} when is_atom(n) and is_list(a) -> {n, m, a} + _ -> raise "invalid_def" + end + + {_e_args, state, a_env} = + expand_args(args, %{state | prematch: {%{}, 0, :none}}, %{env | context: :match}) + + {_e_guard, state, g_env} = + __MODULE__.Clauses.guard( + guards, + %{state | prematch: :raise}, + Map.put(a_env, :context, :guard) + ) + + # The env inside the block is discarded. + # TODO name_arity from call + # TODO expand call + # TODO what should it be for macros? + # TODO how to handle guards? + # {call, e_call, state, env} = case call do + # {:when, meta_2, [call, guard]} -> + # {name, meta_1, args} = call + # {e_args, state, env} = expand_args(args, %{state | prematch: {%{}, 0, :none}}, %{env | context: :match}) + # {e_guard, state, env} = expand(guard, state, %{env | context: :guard}) + # {{name, meta_1, e_args}, {:when, meta_2, [{name, meta_1, e_args}, e_guard]}, state, env} + # call -> + # {name, meta_1, args} = call + # {e_args, state, _env} = expand_args(args, %{state | prematch: {%{}, 0, :none}}, %{env | context: :match}) + # {{name, meta_1, e_args}, {name, meta_1, e_args}, state, env} + # end + + # {name, _meta_1, args} = call + arity = length(args) + + {position, end_position} = extract_range(meta) + + state = + state + |> add_mod_fun_to_position( + {module, name, arity}, + position, + end_position, + args, + def_kind, + "", + # doc, + %{} + # meta + ) + + # expand_macro_callback(meta, Kernel, def_kind, [call, expr], callback, state, env) + # %{state | prematch: :warn} + {_e_body, state, _env} = + expand(expr, state, %{g_env | context: nil, function: {name, arity}}) + + # restore vars from outer scope + state = %{state | vars: vars, unused: unused} + + # result of def expansion is fa tuple + {{name, arity}, state, env} + end + + defp expand_macro(meta, module, fun, args, callback, state, env) do + expand_macro_callback(meta, module, fun, args, callback, state, env) + end + + defp expand_macro_callback(meta, module, fun, args, callback, state, env) do + try do + callback.(meta, args) + catch + # TODO raise? + # For language servers, if expanding the macro fails, we just give up. + _kind, payload -> + IO.inspect(payload, label: inspect(fun)) + {{{:., meta, [module, fun]}, meta, args}, state, env} + else + ast -> expand(ast, state, env) + end + end + + defp extract_range(meta) do + line = Keyword.get(meta, :line, 0) + + if line == 0 do + {nil, nil} + else + position = { + Keyword.get(meta, :line, 0), + Keyword.get(meta, :column, 1) + } + + end_position = + case meta[:end] do + nil -> + case meta[:end_of_expression] do + nil -> + nil + + end_of_expression_meta -> + { + Keyword.fetch!(end_of_expression_meta, :line), + Keyword.fetch!(end_of_expression_meta, :column) + } + end + + end_meta -> + { + Keyword.fetch!(end_meta, :line), + Keyword.fetch!(end_meta, :column) + 3 + } + end + + {position, end_position} + end + end + + ## defmodule helpers + # defmodule automatically defines aliases, we need to mirror this feature here. + + # defmodule Elixir.Alias + defp alias_defmodule({:__aliases__, _, [:"Elixir", _ | _]}, module, env), + do: {module, env} + + # defmodule Alias in root + defp alias_defmodule({:__aliases__, _, _}, module, %{module: nil} = env), + do: {module, env} + + # defmodule Alias nested + defp alias_defmodule({:__aliases__, meta, [h | t]}, _module, env) when is_atom(h) do + module = Module.concat([env.module, h]) + alias = String.to_atom("Elixir." <> Atom.to_string(h)) + {:ok, env} = Macro.Env.define_alias(env, meta, module, as: alias, trace: false) + + case t do + [] -> {module, env} + _ -> {String.to_atom(Enum.join([module | t], ".")), env} + end + end + + # defmodule _ + defp alias_defmodule(_raw, module, env) do + {module, env} + end + + ## Helpers + + defp expand_remote(receiver, dot_meta, right, meta, args, s, sl, %{context: context} = e) + when is_atom(receiver) or is_tuple(receiver) do + assert_no_clauses(right, meta, args, e) + + line = Keyword.fetch!(meta, :line) + # TODO register call + sl = + sl + |> add_current_env_to_line(line, e) + + if context == :guard and is_tuple(receiver) do + if Keyword.get(meta, :no_parens) != true do + raise "parens_map_lookup" + end + + {{{:., dot_meta, [receiver, right]}, meta, []}, sl, e} + else + attached_meta = attach_runtime_module(receiver, meta, s, e) + {e_args, {sa, _}, ea} = map_fold(&expand_arg/3, {sl, s}, e, args) + + case rewrite(context, receiver, dot_meta, right, attached_meta, e_args, s) do + {:ok, rewritten} -> + {rewritten, __MODULE__.Env.close_write(sa, s), ea} + + {:error, _error} -> + raise "elixir_rewrite" + end + end + end + + defp expand_remote(_receiver, _dot_meta, _right, _meta, _args, _, _, _e), + do: raise("invalid_call") + + defp attach_runtime_module(receiver, meta, s, _e) do + if receiver in s.runtime_modules do + [{:runtime_module, true} | meta] + else + meta + end + end + + defp rewrite(_, :erlang, _, :+, _, [arg], _s) when is_number(arg), do: {:ok, arg} + + defp rewrite(_, :erlang, _, :-, _, [arg], _s) when is_number(arg), do: {:ok, -arg} + + defp rewrite(:match, receiver, dot_meta, right, meta, e_args, _s) do + :elixir_rewrite.match_rewrite(receiver, dot_meta, right, meta, e_args) + end + + defp rewrite(:guard, receiver, dot_meta, right, meta, e_args, s) do + :elixir_rewrite.guard_rewrite(receiver, dot_meta, right, meta, e_args, guard_context(s)) + end + + defp rewrite(_, receiver, dot_meta, right, meta, e_args, _s) do + {:ok, :elixir_rewrite.rewrite(receiver, dot_meta, right, meta, e_args)} + end + + defp expand_local(meta, fun, args, state, env = %{function: function}) when function != nil do + assert_no_clauses(fun, meta, args, env) + + if env.context in [:match, :guard] do + raise "invalid_local_invocation" + end + + # A compiler may want to emit a :local_function trace in here. + # TODO register call + line = Keyword.fetch!(meta, :line) + + state = + state + |> add_current_env_to_line(line, env) + + # state = update_in(state.locals, &[{fun, length(args)} | &1]) + {args, state, env} = expand_args(args, state, env) + {{fun, meta, args}, state, env} + end + + defp expand_local(_meta, _fun, _args, _state, _env) do + raise "undefined_function" + end + + defp expand_opts(meta, kind, allowed, opts, s, e) do + {e_opts, se, ee} = expand(opts, s, e) + validate_opts(meta, kind, allowed, e_opts, ee) + {e_opts, se, ee} + end + + defp no_alias_opts(opts) when is_list(opts) do + case Keyword.fetch(opts, :as) do + {:ok, as} -> Keyword.put(opts, :as, no_alias_expansion(as)) + :error -> opts + end + end + + defp no_alias_opts(opts), do: opts + + defp no_alias_expansion({:__aliases__, _, [h | t]} = _aliases) when is_atom(h) do + Module.concat([h | t]) + end + + defp no_alias_expansion(other), do: other + + defp expand_list([{:|, meta, args} = _head], fun, s, e, list) do + {e_args, s_acc, e_acc} = map_fold(fun, s, e, args) + expand_list([], fun, s_acc, e_acc, [{:|, meta, e_args} | list]) + end + + defp expand_list([h | t], fun, s, e, list) do + {e_arg, s_acc, e_acc} = fun.(h, s, e) + expand_list(t, fun, s_acc, e_acc, [e_arg | list]) + end + + defp expand_list([], _fun, s, e, list) do + {Enum.reverse(list), s, e} + end + + defp expand_block([], acc, _meta, s, e), do: {Enum.reverse(acc), s, e} + + defp expand_block([h], acc, meta, s, e) do + # s = s |> add_current_env_to_line(Keyword.fetch!(meta, :line), e) + {eh, se, ee} = expand(h, s, e) + expand_block([], [eh | acc], meta, se, ee) + end + + defp expand_block([{:for, _, [_ | _]} = h | t], acc, meta, s, e) do + {eh, se, ee} = expand_for(h, s, e, false) + expand_block(t, [eh | acc], meta, se, ee) + end + + defp expand_block([{:=, _, [{:_, _, ctx}, {:for, _, [_ | _]} = h]} | t], acc, meta, s, e) + when is_atom(ctx) do + {eh, se, ee} = expand_for(h, s, e, false) + expand_block(t, [eh | acc], meta, se, ee) + end + + defp expand_block([h | t], acc, meta, s, e) do + # s = s |> add_current_env_to_line(Keyword.fetch!(meta, :line), e) + {eh, se, ee} = expand(h, s, e) + expand_block(t, [eh | acc], meta, se, ee) + end + + defp expand_quote(ast, state, env) do + {_, {state, env}} = + Macro.prewalk(ast, {state, env}, fn + # We need to traverse inside unquotes + {unquote, _, [expr]}, {state, env} when unquote in [:unquote, :unquote_splicing] -> + {_expr, state, env} = expand(expr, state, env) + {:ok, {state, env}} + + # If we find a quote inside a quote, we stop traversing it + {:quote, _, [_]}, acc -> + {:ok, acc} + + {:quote, _, [_, _]}, acc -> + {:ok, acc} + + # Otherwise we go on + node, acc -> + {node, acc} + end) + + {ast, state, env} + end + + defp expand_multi_alias_call(kind, meta, base, refs, opts, state, env) do + {base_ref, state, env} = expand(base, state, env) + + fun = fn + {:__aliases__, _, ref}, state, env -> + expand({kind, meta, [Module.concat([base_ref | ref]), opts]}, state, env) + + ref, state, env when is_atom(ref) -> + expand({kind, meta, [Module.concat([base_ref, ref]), opts]}, state, env) + + _other, _s, _e -> + raise "expected_compile_time_module" + end + + map_fold(fun, state, env, refs) + end + + defp resolve_super(_meta, arity, e) do + _module = assert_module_scope(e) + function = assert_function_scope(e) + + case function do + {name, ^arity} -> + {:def, name, []} + + # {kind, name, super_meta} = ElixirOverridable.super(meta, module, function, e) + + # TODO actual resolve + # {{{def, {Name, Arity}}, Kind, Meta, File, _Check, {Defaults, _HasBody, _LastDefaults}}, Clauses} = Def, + # {FinalKind, FinalName, FinalArity, FinalClauses} = + # case Hidden of + # false -> + # {Kind, Name, Arity, Clauses}; + # true when Kind == defmacro; Kind == defmacrop -> + # {defmacrop, name(Name, Count), Arity, Clauses}; + # true -> + # {defp, name(Name, Count), Arity, Clauses} + # end, + # name(Name, Count) when is_integer(Count) -> + # list_to_atom(atom_to_list(Name) ++ " (overridable " ++ integer_to_list(Count) ++ ")"). + + # {kind, name, super_meta} = ElixirOverridable.super(meta, module, function, e) + # maybe_warn_deprecated_super_in_gen_server_callback(meta, function, super_meta, e) + # {kind, name, super_meta} + _ -> + raise "wrong_number_of_args_for_super" + end + end + + defp expand_fn_capture(meta, arg, s, e) do + case __MODULE__.Fn.capture(meta, arg, s, e) |> dbg do + {{:remote, remote, fun, arity}, require_meta, dot_meta, se, ee} -> + # if is_atom(remote) do + # ElixirEnv.trace({:remote_function, require_meta, remote, fun, arity}, e) + # end + attached_meta = attach_runtime_module(remote, require_meta, s, e) + + {{:&, meta, [{:/, [], [{{:., dot_meta, [remote, fun]}, attached_meta, []}, arity]}]}, se, + ee} + + {{:local, _fun, _arity}, _, _, _se, %{function: nil}} -> + raise "undefined_local_capture" + + {{:local, fun, arity}, local_meta, _, se, ee} -> + {{:&, meta, [{:/, [], [{fun, local_meta, nil}, arity]}]}, se, ee} + + {:expand, expr, se, ee} -> + expand(expr, se, ee) + end + end + + defp expand_for({:for, meta, [_ | _] = args}, s, e, return) do + assert_no_match_or_guard_scope(e.context, "for") + {cases, block} = __MODULE__.Utils.split_opts(args) + validate_opts(meta, :for, [:do, :into, :uniq, :reduce], block, e) + + {expr, opts} = + case Keyword.pop(block, :do) do + {do_expr, do_opts} -> {do_expr, do_opts} + nil -> raise "missing_option" + end + + {e_opts, so, eo} = expand(opts, __MODULE__.Env.reset_unused_vars(s), e) + {e_cases, sc, ec} = map_fold(&expand_for_generator/3, so, eo, cases) + assert_generator_start(meta, e_cases, e) + + {{e_expr, se, ee}, normalized_opts} = + case validate_for_options(e_opts, false, false, false, return, meta, e, []) do + {:ok, maybe_reduce, nopts} -> + {expand_for_do_block(meta, expr, sc, ec, maybe_reduce), nopts} + + {:error, _error} -> + # {file_error(meta, e, __MODULE__, error), e_opts} + raise "invalid_option" + end + + {{:for, meta, e_cases ++ [[{:do, e_expr} | normalized_opts]]}, + __MODULE__.Env.merge_and_check_unused_vars(se, s, ee), e} + end + + defp expand_for_do_block(Meta, [{:->, _, _} | _], _S, E, false), + do: raise("for_without_reduce_bad_block") + + defp expand_for_do_block(_meta, expr, s, e, false), do: expand(expr, s, e) + + defp expand_for_do_block(meta, [{:->, _, _} | _] = clauses, s, e, {:reduce, _}) do + transformer = fn + {_, _, [[_], _]} = clause, sa -> + s_reset = __MODULE__.Env.reset_unused_vars(sa) + + {e_clause, s_acc, e_acc} = + __MODULE__.Clauses.clause(meta, :fn, &__MODULE__.Clauses.head/3, clause, s_reset, e) + + {e_clause, __MODULE__.Env.merge_and_check_unused_vars(s_acc, sa, e_acc)} + + _, _ -> + raise "for_with_reduce_bad_block" + end + + {do_expr, sa} = Enum.map_reduce(clauses, s, transformer) + {do_expr, sa, e} + end + + defp expand_for_do_block(_meta, _expr, _s, _e, {:reduce, _}), + do: raise("for_with_reduce_bad_block") + + defp expand_for_generator({:<-, meta, [left, right]}, s, e) do + {e_right, sr, er} = expand(right, s, e) + sm = __MODULE__.Env.reset_read(sr, s) + {[e_left], sl, el} = __MODULE__.Clauses.head([left], sm, er) + {{:<-, meta, [e_left, e_right]}, sl, el} + end + + defp expand_for_generator({:<<>>, meta, args} = x, s, e) when is_list(args) do + case __MODULE__.Utils.split_last(args) do + {left_start, {:<-, op_meta, [left_end, right]}} -> + {e_right, sr, er} = expand(right, s, e) + sm = __MODULE__.Env.reset_read(sr, s) + + {e_left, sl, el} = + __MODULE__.Clauses.match( + fn barg, bs, be -> + __MODULE__.Bitstring.expand(meta, barg, bs, be, true) + end, + left_start ++ [left_end], + sm, + sm, + er + ) + + {{:<<>>, meta, [{:<-, op_meta, [e_left, e_right]}]}, sl, el} + + _ -> + expand(x, s, e) + end + end + + defp expand_for_generator(x, s, e), do: expand(x, s, e) + + defp assert_generator_start(_, [{:<-, _, [_, _]} | _], _), do: :ok + defp assert_generator_start(_, [{:<<>>, _, [{:<-, _, [_, _]}]} | _], _), do: :ok + defp assert_generator_start(_meta, _, _e), do: raise("for_generator_start") + + defp validate_for_options([{:into, _} = pair | opts], _into, uniq, reduce, return, meta, e, acc) do + validate_for_options(opts, pair, uniq, reduce, return, meta, e, [pair | acc]) + end + + defp validate_for_options( + [{:uniq, boolean} = pair | opts], + into, + _uniq, + reduce, + return, + meta, + e, + acc + ) + when is_boolean(boolean) do + validate_for_options(opts, into, pair, reduce, return, meta, e, [pair | acc]) + end + + defp validate_for_options([{:uniq, value} | _], _, _, _, _, _, _, _) do + {:error, {:for_invalid_uniq, value}} + end + + defp validate_for_options( + [{:reduce, _} = pair | opts], + into, + uniq, + _reduce, + return, + meta, + e, + acc + ) do + validate_for_options(opts, into, uniq, pair, return, meta, e, [pair | acc]) + end + + defp validate_for_options([], into, uniq, {:reduce, _}, _return, _meta, _e, _acc) + when into != false or uniq != false do + {:error, :for_conflicting_reduce_into_uniq} + end + + defp validate_for_options([], false, uniq, false, true, meta, e, acc) do + pair = {:into, []} + validate_for_options([pair], pair, uniq, false, true, meta, e, acc) + end + + defp validate_for_options([], false, {:uniq, true}, false, false, meta, e, acc) do + # file_warn(meta, e, __MODULE__, :for_with_unused_uniq) + acc_without_uniq = Keyword.delete(acc, :uniq) + validate_for_options([], false, false, false, false, meta, e, acc_without_uniq) + end + + defp validate_for_options([], _into, _uniq, reduce, _return, _meta, _e, acc) do + {:ok, reduce, Enum.reverse(acc)} + end + + defp validate_opts(_meta, _kind, allowed, opts, _e) when is_list(opts) do + for {key, _} <- opts, not Enum.member?(allowed, key), do: raise("unsupported_option") + end + + defp validate_opts(_meta, _kind, _allowed, _opts, _e) do + raise "options_are_not_keyword" + end + + defp escape_env_entries(meta, %{vars: {read, _}}, env) do + env = + case env.function do + nil -> env + _ -> %{env | lexical_tracker: nil, tracers: []} + end + + %{env | versioned_vars: escape_map(read), line: __MODULE__.Utils.get_line(meta)} + end + + defp escape_map(map) do + {:%{}, [], Enum.sort(Map.to_list(map))} + end + + defp map_fold(fun, s, e, list), do: map_fold(fun, s, e, list, []) + + defp map_fold(fun, s, e, [h | t], acc) do + {rh, rs, re} = fun.(h, s, e) + map_fold(fun, rs, re, t, [rh | acc]) + end + + defp map_fold(_fun, s, e, [], acc), do: {Enum.reverse(acc), s, e} + + defp assert_no_clauses(_name, _meta, [], _e), do: :ok + + defp assert_no_clauses(name, meta, args, e) do + assert_arg_with_no_clauses(name, meta, List.last(args), e) + end + + defp assert_arg_with_no_clauses(name, meta, [{key, value} | rest], e) when is_atom(key) do + case value do + [{:->, _, _} | _] -> + raise "invalid_clauses" + + _ -> + assert_arg_with_no_clauses(name, meta, rest, e) + end + end + + defp assert_arg_with_no_clauses(_name, _meta, _arg, _e), do: :ok + + defp assert_module_scope(env, fun, arity) do + case env.module do + nil -> raise ArgumentError, "cannot invoke #{fun}/#{arity} outside module" + mod -> mod + end + end + + defp assert_module_scope(%{module: nil}), do: raise("invalid_expr_in_scope") + defp assert_module_scope(%{module: module}), do: module + defp assert_function_scope(%{function: nil}), do: raise("invalid_expr_in_scope") + defp assert_function_scope(%{function: function}), do: function + + defp assert_no_match_scope(context, _exp) do + case context do + :match -> + raise "invalid_pattern_in_match" + + _ -> + :ok + end + end + + defp assert_no_guard_scope(context, exp) do + case context do + :guard -> + raise ArgumentError, + "invalid expression in guard, #{exp} is not allowed in guards. " <> + "To learn more about guards, visit: https://hexdocs.pm/elixir/patterns-and-guards.html" + + _ -> + :ok + end + end + + defp assert_no_match_or_guard_scope(context, exp) do + case context do + :match -> + invalid_match!(exp) + + :guard -> + raise ArgumentError, + "invalid expression in guard, #{exp} is not allowed in guards. " <> + "To learn more about guards, visit: https://hexdocs.pm/elixir/patterns-and-guards.html" + + _ -> + :ok + end + end + + defp invalid_match!(exp) do + raise ArgumentError, + "invalid expression in match, #{exp} is not allowed in patterns " <> + "such as function clauses, case clauses or on the left side of the = operator" + end + + defp assert_no_underscore_clause_in_cond([{:do, clauses}], _e) when is_list(clauses) do + case List.last(clauses) do + {:->, _meta, [[{:_, _, atom}], _]} when is_atom(atom) -> + raise ArgumentError, "underscore_in_cond" + + _other -> + :ok + end + end + + defp assert_no_underscore_clause_in_cond(_other, _e), do: :ok + + defp assert_no_ambiguous_op(name, meta, [_arg], s, _e) do + case Keyword.fetch(meta, :ambiguous_op) do + {:ok, kind} -> + pair = {name, kind} + + case Map.get(s.vars, pair) do + nil -> + :ok + + _ -> + raise "op_ambiguity" + end + + _ -> + :ok + end + end + + defp assert_no_ambiguous_op(_atom, _meta, _args, _s, _e), do: :ok + + defp refute_parallel_bitstring_match({:<<>>, _, _}, {:<<>>, _meta, _} = _arg, _e, true) do + # file_error(meta, e, __MODULE__, {:parallel_bitstring_match, arg}) + raise ArgumentError, "parallel_bitstring_match" + end + + defp refute_parallel_bitstring_match(left, {:=, _meta, [match_left, match_right]}, e, parallel) do + refute_parallel_bitstring_match(left, match_left, e, true) + refute_parallel_bitstring_match(left, match_right, e, parallel) + end + + defp refute_parallel_bitstring_match(left = [_ | _], right = [_ | _], e, parallel) do + refute_parallel_bitstring_match_each(left, right, e, parallel) + end + + defp refute_parallel_bitstring_match({left1, left2}, {right1, right2}, e, parallel) do + refute_parallel_bitstring_match_each([left1, left2], [right1, right2], e, parallel) + end + + defp refute_parallel_bitstring_match({:tuple, _, args1}, {:tuple, _, args2}, e, parallel) do + refute_parallel_bitstring_match_each(args1, args2, e, parallel) + end + + defp refute_parallel_bitstring_match({:%{}, _, args1}, {:%{}, _, args2}, e, parallel) do + refute_parallel_bitstring_match_map_field(Enum.sort(args1), Enum.sort(args2), e, parallel) + end + + defp refute_parallel_bitstring_match({:%, _, [_, args]}, right, e, parallel) do + refute_parallel_bitstring_match(args, right, e, parallel) + end + + defp refute_parallel_bitstring_match(left, {:%, _, [_, args]}, e, parallel) do + refute_parallel_bitstring_match(left, args, e, parallel) + end + + defp refute_parallel_bitstring_match(_left, _right, _e, _parallel), do: :ok + + defp refute_parallel_bitstring_match_each([arg1 | rest1], [arg2 | rest2], e, parallel) do + refute_parallel_bitstring_match(arg1, arg2, e, parallel) + refute_parallel_bitstring_match_each(rest1, rest2, e, parallel) + end + + defp refute_parallel_bitstring_match_each(_list1, _list2, _e, _parallel), do: :ok + + defp refute_parallel_bitstring_match_map_field( + [{key, val1} | rest1], + [{key, val2} | rest2], + e, + parallel + ) do + refute_parallel_bitstring_match(val1, val2, e, parallel) + refute_parallel_bitstring_match_map_field(rest1, rest2, e, parallel) + end + + defp refute_parallel_bitstring_match_map_field( + [field1 | rest1] = args1, + [field2 | rest2] = args2, + e, + parallel + ) do + cond do + field1 > field2 -> refute_parallel_bitstring_match_map_field(args1, rest2, e, parallel) + true -> refute_parallel_bitstring_match_map_field(rest1, args2, e, parallel) + end + end + + defp refute_parallel_bitstring_match_map_field(_args1, _args2, _e, _parallel), do: :ok + + defp var_unused({_, kind} = pair, meta, version, unused, override) do + if kind == nil and should_warn(meta) do + Map.put(unused, {pair, version}, {meta, override}) + else + unused + end + end + + defp var_used(meta, {_, kind} = pair, version, unused) do + keep_unused = Keyword.has_key?(meta, :keep_unused) + + if keep_unused do + unused + else + if is_atom(kind) do + Map.put(unused, {pair, version}, false) + else + unused + end + end + end + + defp should_warn(meta) do + Keyword.get(meta, :generated) != true + end + + defp var_context(meta, kind) do + case Keyword.fetch(meta, :counter) do + {:ok, counter} -> counter + :error -> kind + end + end + + # TODO probable we can remove it/hardcode, used only for generating error message + defp guard_context(%{prematch: {_, _, {:bitsize, _}}}), do: "bitstring size specifier" + defp guard_context(_), do: "guard" + + defp expand_case(meta, expr, opts, s, e) do + {e_expr, se, ee} = expand(expr, s, e) + + r_opts = opts + # if proplists.get_value(:optimize_boolean, meta, false) do + # if ElixirUtils.returns_boolean(e_expr) do + # rewrite_case_clauses(opts) + # else + # generated_case_clauses(opts) + # end + # else + # opts + # end + + {e_opts, so, eo} = __MODULE__.Clauses.case(meta, r_opts, se, ee) + {{:case, meta, [e_expr, e_opts]}, so, eo} + end + + def expand_arg(arg, acc, e) + when is_number(arg) or is_atom(arg) or is_binary(arg) or is_pid(arg) do + {arg, acc, e} + end + + def expand_arg(arg, {acc, s}, e) do + {e_arg, s_acc, e_acc} = expand(arg, __MODULE__.Env.reset_read(acc, s), e) + {e_arg, {s_acc, s}, e_acc} + end + + def expand_args([arg], s, e) do + {e_arg, se, ee} = expand(arg, s, e) + {[e_arg], se, ee} + end + + def expand_args(args, s, %{context: :match} = e) do + map_fold(&expand/3, s, e, args) + end + + def expand_args(args, s, e) do + {e_args, {sa, _}, ea} = map_fold(&expand_arg/3, {__MODULE__.Env.prepare_write(s), s}, e, args) + {e_args, __MODULE__.Env.close_write(sa, s), ea} + end + + defmodule Env do + alias ElixirSense.Core.Compiler.Utils, as: ElixirUtils + + def reset_unused_vars(%{unused: {_unused, version}} = s) do + %{s | unused: {%{}, version}} + end + + def reset_read(%{vars: {_, write}} = s, %{vars: {read, _}}) do + %{s | vars: {read, write}} + end + + def prepare_write(%{vars: {read, _}} = s) do + %{s | vars: {read, read}} + end + + def close_write(%{vars: {_read, write}} = s, %{vars: {_, false}}) do + %{s | vars: {write, false}} + end + + def close_write(%{vars: {_read, write}} = s, %{vars: {_, upper_write}}) do + %{s | vars: {write, merge_vars(upper_write, write)}} + end + + defp merge_vars(v, v), do: v + + defp merge_vars(v1, v2) do + :maps.fold( + fn k, m2, acc -> + case Map.fetch(acc, k) do + {:ok, m1} when m1 >= m2 -> acc + _ -> Map.put(acc, k, m2) + end + end, + v1, + v2 + ) + end + + def merge_and_check_unused_vars(s, %{vars: {read, write}, unused: {unused, _version}}, e) do + %{unused: {clause_unused, version}} = s + new_unused = merge_and_check_unused_vars(read, unused, clause_unused, e) + %{s | unused: {new_unused, version}, vars: {read, write}} + end + + def merge_and_check_unused_vars(current, unused, clause_unused, _e) do + :maps.fold( + fn + {var, count} = key, false, acc -> + case Map.fetch(current, var) do + {:ok, current_count} when count <= current_count -> + Map.put(acc, key, false) + + _ -> + acc + end + + {{_name, _kind}, _count}, {_meta, _overridden}, acc -> + # if kind == nil and is_unused_var(name) do + # warn = {:unused_var, name, overridden} + # file_warn(meta, e, __MODULE__, warn) + # end + acc + end, + unused, + clause_unused + ) + end + + def calculate_span(meta, name) do + case Keyword.fetch(meta, :column) do + {:ok, column} -> + span = {ElixirUtils.get_line(meta), column + String.length(Atom.to_string(name))} + [{:span, span} | meta] + + _ -> + meta + end + end + end + + defmodule Utils do + def split_last([]), do: {[], []} + + def split_last(list), do: split_last(list, []) + + defp split_last([h], acc), do: {Enum.reverse(acc), h} + + defp split_last([h | t], acc), do: split_last(t, [h | acc]) + + def split_opts(args) do + case split_last(args) do + {outer_cases, outer_opts} when is_list(outer_opts) -> + case split_last(outer_cases) do + {inner_cases, inner_opts} when is_list(inner_opts) -> + {inner_cases, inner_opts ++ outer_opts} + + _ -> + {outer_cases, outer_opts} + end + + _ -> + {args, []} + end + end + + def get_line(opts) when is_list(opts) do + case Keyword.fetch(opts, :line) do + {:ok, line} when is_integer(line) -> line + _ -> 0 + end + end + + def extract_guards({:when, _, [left, right]}), do: {left, extract_or_guards(right)} + def extract_guards(term), do: {term, []} + + def extract_or_guards({:when, _, [left, right]}), do: [left | extract_or_guards(right)] + def extract_or_guards(term), do: [term] + end + + defmodule Clauses do + alias ElixirSense.Core.Compiler, as: ElixirExpand + alias ElixirSense.Core.Compiler.Env, as: ElixirEnv + alias ElixirSense.Core.Compiler.Utils, as: ElixirUtils + + def match(fun, expr, after_s, _before_s, %{context: :match} = e) do + fun.(expr, after_s, e) + end + + def match(fun, expr, after_s, before_s, e) do + %{vars: current, unused: {_counter, unused} = unused_tuple} = after_s + %{vars: {read, _write}, prematch: prematch} = before_s + + call_s = %{before_s | prematch: {read, unused, :none}, unused: unused_tuple, vars: current} + + call_e = Map.put(e, :context, :match) + {e_expr, %{vars: new_current, unused: new_unused}, ee} = fun.(expr, call_s, call_e) + + end_s = %{after_s | prematch: prematch, unused: new_unused, vars: new_current} + + end_e = Map.put(ee, :context, Map.get(e, :context)) + {e_expr, end_s, end_e} + end + + def clause(meta, kind, fun, {:->, clause_meta, [_, _]} = clause, s, e) + when is_function(fun, 4) do + clause(meta, kind, fn x, sa, ea -> fun.(clause_meta, x, sa, ea) end, clause, s, e) + end + + def clause(_meta, _kind, fun, {:->, meta, [left, right]}, s, e) do + {e_left, sl, el} = fun.(left, s, e) + {e_right, sr, er} = ElixirExpand.expand(right, sl, el) + {{:->, meta, [e_left, e_right]}, sr, er} + end + + def clause(_meta, _kind, _fun, _, _, _e) do + raise ArgumentError, "bad_or_missing_clauses" + end + + def head([{:when, meta, [_ | _] = all}], s, e) do + {args, guard} = ElixirUtils.split_last(all) + prematch = s.prematch + + {{e_args, e_guard}, sg, eg} = + match( + fn _ok, sm, em -> + {e_args, sa, ea} = ElixirExpand.expand_args(args, sm, em) + + {e_guard, sg, eg} = + guard(guard, %{sa | prematch: prematch}, Map.put(ea, :context, :guard)) + + {{e_args, e_guard}, sg, eg} + end, + :ok, + s, + s, + e + ) + + {[{:when, meta, e_args ++ [e_guard]}], sg, eg} + end + + def head(args, s, e) do + match(&ElixirExpand.expand_args/3, args, s, s, e) + end + + def guard({:when, meta, [left, right]}, s, e) do + {e_left, sl, el} = guard(left, s, e) + {e_right, sr, er} = guard(right, sl, el) + {{:when, meta, [e_left, e_right]}, sr, er} + end + + def guard(guard, s, e) do + {e_guard, sg, eg} = ElixirExpand.expand(guard, s, e) + # warn_zero_length_guard(e_guard, eg) + {e_guard, sg, eg} + end + + # case + + def case(_meta, [], _s, _e) do + raise ArgumentError, "missing_option" + end + + def case(_meta, opts, _s, _e) when not is_list(opts) do + raise ArgumentError, "invalid_args" + end + + def case(meta, opts, s, e) do + :ok = + assert_at_most_once(:do, opts, 0, fn _key -> + raise ArgumentError, "duplicated_clauses" + end) + + {case_clauses, sa} = + Enum.map_reduce(opts, s, fn x, sa -> + expand_case(meta, x, sa, e) + end) + + {case_clauses, sa, e} + end + + defp expand_case(meta, {:do, _} = do_clause, s, e) do + fun = expand_head(meta, :case, :do) + expand_clauses(meta, :case, fun, do_clause, s, e) + end + + defp expand_case(_meta, {_key, _}, _s, _e) do + raise ArgumentError, "unexpected_option" + end + + # cond + + def cond(_meta, [], _s, _e) do + raise ArgumentError, "missing_option" + end + + def cond(_meta, opts, _s, _e) when not is_list(opts) do + raise ArgumentError, "invalid_args" + end + + def cond(meta, opts, s, e) do + :ok = + assert_at_most_once(:do, opts, 0, fn _key -> + raise ArgumentError, "duplicated_clauses" + end) + + {cond_clauses, sa} = + Enum.map_reduce(opts, s, fn x, sa -> + expand_cond(meta, x, sa, e) + end) + + {cond_clauses, sa, e} + end + + defp expand_cond(meta, {:do, _} = do_clause, s, e) do + fun = expand_one(meta, :cond, :do, &ElixirExpand.expand_args/3) + expand_clauses(meta, :cond, fun, do_clause, s, e) + end + + defp expand_cond(_meta, {_key, _}, _s, _e) do + raise ArgumentError, "unexpected_option" + end + + # receive + + def receive(_meta, [], _s, _e) do + raise ArgumentError, "missing_option" + end + + def receive(_meta, opts, _s, _e) when not is_list(opts) do + raise ArgumentError, "invalid_args" + end + + def receive(meta, opts, s, e) do + raise_error = fn _key -> + raise ArgumentError, "duplicated_clauses" + end + + :ok = assert_at_most_once(:do, opts, 0, raise_error) + :ok = assert_at_most_once(:after, opts, 0, raise_error) + + {receive_clauses, sa} = + Enum.map_reduce(opts, s, fn x, sa -> + expand_receive(meta, x, sa, e) + end) + + {receive_clauses, sa, e} + end + + defp expand_receive(_meta, {:do, {:__block__, _, []}} = do_block, s, _e) do + {do_block, s} + end + + defp expand_receive(meta, {:do, _} = do_clause, s, e) do + fun = expand_head(meta, :receive, :do) + expand_clauses(meta, :receive, fun, do_clause, s, e) + end + + defp expand_receive(meta, {:after, [_]} = after_clause, s, e) do + fun = expand_one(meta, :receive, :after, &ElixirExpand.expand_args/3) + expand_clauses(meta, :receive, fun, after_clause, s, e) + end + + defp expand_receive(_meta, {:after, _}, _s, _e) do + raise ArgumentError, "multiple_after_clauses_in_receive" + end + + defp expand_receive(_meta, {_key, _}, _s, _e) do + raise ArgumentError, "unexpected_option" + end + + # with + + def with(meta, args, s, e) do + {exprs, opts0} = ElixirUtils.split_opts(args) + s0 = ElixirEnv.reset_unused_vars(s) + {e_exprs, {s1, e1, has_match}} = Enum.map_reduce(exprs, {s0, e, false}, &expand_with/2) + {e_do, opts1, s2} = expand_with_do(meta, opts0, s, s1, e1) + {e_opts, opts2, s3} = expand_with_else(meta, opts1, s2, e, has_match) + + case opts2 do + [{_key, _} | _] -> + raise "unexpected_option" + + [] -> + :ok + end + + {{:with, meta, e_exprs ++ [[{:do, e_do} | e_opts]]}, s3, e} + end + + defp expand_with({:<-, meta, [left, right]}, {s, e, has_match}) do + {e_right, sr, er} = ElixirExpand.expand(right, s, e) + sm = ElixirEnv.reset_read(sr, s) + {[e_left], sl, el} = head([left], sm, er) + + new_has_match = + case e_left do + {var, _, ctx} when is_atom(var) and is_atom(ctx) -> has_match + _ -> true + end + + {{:<-, meta, [e_left, e_right]}, {sl, el, new_has_match}} + end + + defp expand_with(expr, {s, e, has_match}) do + {e_expr, se, ee} = ElixirExpand.expand(expr, s, e) + {e_expr, {se, ee, has_match}} + end + + defp expand_with_do(_meta, opts, s, acc, e) do + case Keyword.pop(opts, :do) do + {nil, _} -> + raise "missing_option" + + {expr, rest_opts} -> + {e_expr, s_acc, e_acc} = ElixirExpand.expand(expr, acc, e) + {e_expr, rest_opts, ElixirEnv.merge_and_check_unused_vars(s_acc, s, e_acc)} + end + end + + defp expand_with_else(meta, opts, s, e, _has_match) do + case Keyword.pop(opts, :else) do + {nil, _} -> + {[], opts, s} + + {expr, rest_opts} -> + pair = {:else, expr} + fun = expand_head(meta, :with, :else) + {e_pair, se} = expand_clauses(meta, :with, fun, pair, s, e) + {[e_pair], rest_opts, se} + end + end + + # try + + def try(_meta, [], _s, _e), do: raise("missing_option") + def try(_meta, [{:do, _}], _s, _e), do: raise("missing_option") + def try(_meta, opts, _s, _e) when not is_list(opts), do: raise("invalid_args") + + def try(meta, opts, s, e) do + # TODO: Make this an error on v2.0 + # case opts do + # [{:do, _}, {:else, _}] -> + # file_warn(meta, Map.get(e, :file), __MODULE__, {:try_with_only_else_clause, origin(meta, :try)}) + # _ -> + # :ok + # end + + raise_error = fn _key -> + raise "duplicated_clauses" + end + + :ok = assert_at_most_once(:do, opts, 0, raise_error) + :ok = assert_at_most_once(:rescue, opts, 0, raise_error) + :ok = assert_at_most_once(:catch, opts, 0, raise_error) + :ok = assert_at_most_once(:else, opts, 0, raise_error) + :ok = assert_at_most_once(:after, opts, 0, raise_error) + # :ok = warn_catch_before_rescue(opts, meta, e, false) + + {try_clauses, sa} = + Enum.map_reduce(opts, s, fn x, sa -> + expand_try(meta, x, sa, e) + end) + + {try_clauses, sa, e} + end + + defp expand_try(_meta, {:do, expr}, s, e) do + {e_expr, se, ee} = ElixirExpand.expand(expr, ElixirEnv.reset_unused_vars(s), e) + {{:do, e_expr}, ElixirEnv.merge_and_check_unused_vars(se, s, ee)} + end + + defp expand_try(_meta, {:after, expr}, s, e) do + {e_expr, se, ee} = ElixirExpand.expand(expr, ElixirEnv.reset_unused_vars(s), e) + {{:after, e_expr}, ElixirEnv.merge_and_check_unused_vars(se, s, ee)} + end + + defp expand_try(meta, {:else, _} = else_clause, s, e) do + fun = expand_head(meta, :try, :else) + expand_clauses(meta, :try, fun, else_clause, s, e) + end + + defp expand_try(meta, {:catch, _} = catch_clause, s, e) do + expand_clauses_with_stacktrace(meta, &expand_catch/4, catch_clause, s, e) + end + + defp expand_try(meta, {:rescue, _} = rescue_clause, s, e) do + expand_clauses_with_stacktrace(meta, &expand_rescue/4, rescue_clause, s, e) + end + + defp expand_try(_meta, {_key, _}, _s, _e) do + raise ArgumentError, "unexpected_option" + end + + defp expand_clauses_with_stacktrace(meta, fun, clauses, s, e) do + old_stacktrace = s.stacktrace + ss = %{s | stacktrace: true} + {ret, se} = expand_clauses(meta, :try, fun, clauses, ss, e) + {ret, %{se | stacktrace: old_stacktrace}} + end + + defp expand_catch(_meta, args = [_], s, e) do + head(args, s, e) + end + + defp expand_catch(_meta, args = [_, _], s, e) do + head(args, s, e) + end + + defp expand_catch(_meta, _, _, _e) do + raise ArgumentError, "wrong_number_of_args_for_clause" + end + + defp expand_rescue(_meta, [arg], s, e) do + case expand_rescue(arg, s, e) do + {e_arg, sa, ea} -> + {[e_arg], sa, ea} + + false -> + raise ArgumentError, "invalid_rescue_clause" + end + end + + defp expand_rescue(_meta, _, _, _e) do + raise ArgumentError, "wrong_number_of_args_for_clause" + end + + # rescue var + defp expand_rescue({name, _, atom} = var, s, e) when is_atom(name) and is_atom(atom) do + match(&ElixirExpand.expand/3, var, s, s, e) + end + + # rescue Alias => _ in [Alias] + defp expand_rescue({:__aliases__, _, [_ | _]} = alias, s, e) do + expand_rescue({:in, [], [{:_, [], Map.get(e, :module)}, alias]}, s, e) + end + + # rescue var in _ + defp expand_rescue( + {:in, _, [{name, _, var_context} = var, {:_, _, underscore_context}]}, + s, + e + ) + when is_atom(name) and is_atom(var_context) and is_atom(underscore_context) do + match(&ElixirExpand.expand/3, var, s, s, e) + end + + # rescue var in (list() or atom()) + defp expand_rescue({:in, meta, [left, right]}, s, e) do + {e_left, sl, el} = match(&ElixirExpand.expand/3, left, s, s, e) + {e_right, sr, er} = ElixirExpand.expand(right, sl, el) + + case e_left do + {name, _, atom} when is_atom(name) and is_atom(atom) -> + case normalize_rescue(e_right) do + false -> false + other -> {{:in, meta, [e_left, other]}, sr, er} + end + + _ -> + false + end + end + + # rescue expr() => rescue expanded_expr() + defp expand_rescue({_meta, meta, _} = arg, s, e) do + # TODO wut? + case Macro.expand_once(arg, Map.put(e, :line, line(meta))) do + ^arg -> false + new_arg -> expand_rescue(new_arg, s, e) + end + end + + # rescue list() or atom() => _ in (list() or atom()) + defp expand_rescue(arg, s, e) do + expand_rescue({:in, [], [{:_, [], Map.get(e, :module)}, arg]}, s, e) + end + + defp normalize_rescue(atom) when is_atom(atom) do + [atom] + end + + defp normalize_rescue(other) do + if is_list(other) and Enum.all?(other, &is_atom/1), do: other, else: false + end + + defp expand_head(_meta, _kind, _key) do + fn + [{:when, _, [_, _, _ | _]}], _, _e -> + raise ArgumentError, "wrong_number_of_args_for_clause" + + [_] = args, s, e -> + head(args, s, e) + + _, _, _e -> + raise ArgumentError, "wrong_number_of_args_for_clause" + end + end + + defp expand_one(_meta, _kind, _key, fun) do + fn + [_] = args, s, e -> + fun.(args, s, e) + + _, _, _e -> + raise ArgumentError, "wrong_number_of_args_for_clause" + end + end + + defp expand_clauses(meta, kind, fun, clauses, s, e) do + new_kind = origin(meta, kind) + expand_clauses_origin(meta, new_kind, fun, clauses, s, e) + end + + defp expand_clauses_origin(meta, kind, fun, {key, [_ | _] = clauses}, s, e) do + transformer = fn clause, sa -> + {e_clause, s_acc, e_acc} = + clause(meta, {kind, key}, fun, clause, ElixirEnv.reset_unused_vars(sa), e) + + {e_clause, ElixirEnv.merge_and_check_unused_vars(s_acc, sa, e_acc)} + end + + {values, se} = Enum.map_reduce(clauses, s, transformer) + {{key, values}, se} + end + + defp expand_clauses_origin(_meta, _kind, _fun, {_key, _}, _, _e) do + raise ArgumentError, "bad_or_missing_clauses" + end + + # helpers + + defp assert_at_most_once(_kind, [], _count, _fun), do: :ok + + defp assert_at_most_once(kind, [{kind, _} | _], 1, error_fun) do + error_fun.(kind) + end + + defp assert_at_most_once(kind, [{kind, _} | rest], count, fun) do + assert_at_most_once(kind, rest, count + 1, fun) + end + + defp assert_at_most_once(kind, [_ | rest], count, fun) do + assert_at_most_once(kind, rest, count, fun) + end + + defp origin(meta, default) do + Keyword.get(meta, :origin, default) + end + + defp line(opts) when is_list(opts) do + case Keyword.fetch(opts, :line) do + {:ok, line} when is_integer(line) -> line + _ -> 0 + end + end + end + + defmodule Bitstring do + alias ElixirSense.Core.Compiler, as: ElixirExpand + alias ElixirSense.Core.Compiler.Env, as: ElixirEnv + alias ElixirSense.Core.Compiler.Utils, as: ElixirUtils + + defp expand_match(expr, {s, original_s}, e) do + {e_expr, se, ee} = ElixirExpand.expand(expr, s, e) + {e_expr, {se, original_s}, ee} + end + + def expand(meta, args, s, e, require_size) do + case Map.get(e, :context) do + :match -> + {e_args, alignment, {sa, _}, ea} = + expand(meta, &expand_match/3, args, [], {s, s}, e, 0, require_size) + + case find_match(e_args) do + false -> + :ok + + _match -> + raise "nested_match" + end + + {{:<<>>, [{:alignment, alignment} | meta], e_args}, sa, ea} + + _ -> + pair_s = {ElixirEnv.prepare_write(s), s} + + {e_args, alignment, {sa, _}, ea} = + expand(meta, &ElixirExpand.expand_arg/3, args, [], pair_s, e, 0, require_size) + + {{:<<>>, [{:alignment, alignment} | meta], e_args}, ElixirEnv.close_write(sa, s), ea} + end + end + + def expand(_bitstr_meta, _fun, [], acc, s, e, alignment, _require_size) do + {Enum.reverse(acc), alignment, s, e} + end + + def expand( + bitstr_meta, + fun, + [{:"::", meta, [left, right]} | t], + acc, + s, + e, + alignment, + require_size + ) do + {e_left, {sl, original_s}, el} = expand_expr(meta, left, fun, s, e) + + match_or_require_size = require_size or is_match_size(t, el) + e_type = expr_type(e_left) + + expect_size = + case e_left do + _ when not match_or_require_size -> :optional + {:^, _, [{_, _, _}]} -> {:infer, e_left} + _ -> :required + end + + {e_right, e_alignment, ss, es} = + expand_specs(e_type, meta, right, sl, original_s, el, expect_size) + + e_acc = concat_or_prepend_bitstring(meta, e_left, e_right, acc, es, match_or_require_size) + + expand( + bitstr_meta, + fun, + t, + e_acc, + {ss, original_s}, + es, + alignment(alignment, e_alignment), + require_size + ) + end + + def expand(bitstr_meta, fun, [h | t], acc, s, e, alignment, require_size) do + meta = extract_meta(h, bitstr_meta) + {e_left, {ss, original_s}, es} = expand_expr(meta, h, fun, s, e) + + match_or_require_size = require_size or is_match_size(t, es) + e_type = expr_type(e_left) + e_right = infer_spec(e_type, meta) + + inferred_meta = [{:inferred_bitstring_spec, true} | meta] + + e_acc = + concat_or_prepend_bitstring( + inferred_meta, + e_left, + e_right, + acc, + es, + match_or_require_size + ) + + expand(meta, fun, t, e_acc, {ss, original_s}, es, alignment, require_size) + end + + defp expand_expr( + _meta, + {{:., _, [mod, :to_string]}, _, [arg]} = ast, + fun, + s, + %{context: context} = e + ) + when context != nil and (mod == Kernel or mod == String.Chars) do + case fun.(arg, s, e) do + {ebin, se, ee} when is_binary(ebin) -> {ebin, se, ee} + _ -> fun.(ast, s, e) + end + end + + defp expand_expr(_meta, component, fun, s, e) do + case fun.(component, s, e) do + {e_component, _, _error_e} when is_list(e_component) or is_atom(e_component) -> + raise "invalid_literal" + + expanded -> + expanded + end + end + + defp expand_specs(expr_type, meta, info, s, original_s, e, expect_size) do + default = + %{size: :default, unit: :default, sign: :default, type: :default, endianness: :default} + + {specs, ss, es} = + expand_each_spec(meta, unpack_specs(info, []), default, s, original_s, e) + + merged_type = type(meta, expr_type, specs.type, e) + validate_size_required(meta, expect_size, expr_type, merged_type, specs.size, es) + size_and_unit = size_and_unit(meta, expr_type, specs.size, specs.unit, es) + alignment = compute_alignment(merged_type, specs.size, specs.unit) + + maybe_inferred_size = + case {expect_size, merged_type, size_and_unit} do + {{:infer, pinned_var}, :binary, []} -> + [{:size, meta, [{{:., meta, [:erlang, :byte_size]}, meta, [pinned_var]}]}] + + {{:infer, pinned_var}, :bitstring, []} -> + [{:size, meta, [{{:., meta, [:erlang, :bit_size]}, meta, [pinned_var]}]}] + + _ -> + size_and_unit + end + + [h | t] = + build_spec( + meta, + specs.size, + specs.unit, + merged_type, + specs.endianness, + specs.sign, + maybe_inferred_size, + es + ) + + {Enum.reduce(t, h, fn i, acc -> {:-, meta, [acc, i]} end), alignment, ss, es} + end + + defp type(_, :default, :default, _), do: :integer + defp type(_, expr_type, :default, _), do: expr_type + + defp type(_, :binary, type, _) when type in [:binary, :bitstring, :utf8, :utf16, :utf32], + do: type + + defp type(_, :bitstring, type, _) when type in [:binary, :bitstring], do: type + + defp type(_, :integer, type, _) when type in [:integer, :float, :utf8, :utf16, :utf32], + do: type + + defp type(_, :float, :float, _), do: :float + defp type(_, :default, type, _), do: type + + defp type(_meta, _other, type, _e) do + # function_error(meta, e, __MODULE__, {:bittype_mismatch, type, other, :type}) + type + end + + defp expand_each_spec(meta, [{expr, meta_e, args} = h | t], map, s, original_s, e) + when is_atom(expr) do + case validate_spec(expr, args) do + {key, arg} -> + # if args != [], do: :ok, else: file_warn(meta, e, __MODULE__, {:parens_bittype, expr}) + + {value, se, ee} = expand_spec_arg(arg, s, original_s, e) + validate_spec_arg(meta, key, value, se, original_s, ee) + + case Map.get(map, key, :default) do + :default -> + :ok + + ^value -> + :ok + + _other -> + # function_error(meta, e, __MODULE__, {:bittype_mismatch, value, _other, key}) + :ok + end + + expand_each_spec(meta, t, Map.put(map, key, value), se, original_s, ee) + + :none -> + ha = + if args == nil do + # file_warn(meta, e, __MODULE__, {:unknown_bittype, expr}) + {expr, meta_e, []} + else + h + end + + # TODO not call it here + case Macro.expand(ha, Map.put(e, :line, ElixirUtils.get_line(meta))) do + ^ha -> + # function_error(meta, e, __MODULE__, {:undefined_bittype, h}) + expand_each_spec(meta, t, map, s, original_s, e) + + new_types -> + expand_each_spec(meta, unpack_specs(new_types, []) ++ t, map, s, original_s, e) + end + end + end + + defp expand_each_spec(meta, [_expr | tail], map, s, original_s, e) do + # function_error(meta, e, __MODULE__, {:undefined_bittype, expr}) + expand_each_spec(meta, tail, map, s, original_s, e) + end + + defp expand_each_spec(_meta, [], map, s, _original_s, e), do: {map, s, e} + + defp compute_alignment(_, size, unit) when is_integer(size) and is_integer(unit), + do: rem(size * unit, 8) + + defp compute_alignment(:default, size, unit), do: compute_alignment(:integer, size, unit) + defp compute_alignment(:integer, :default, unit), do: compute_alignment(:integer, 8, unit) + defp compute_alignment(:integer, size, :default), do: compute_alignment(:integer, size, 1) + defp compute_alignment(:bitstring, size, :default), do: compute_alignment(:bitstring, size, 1) + defp compute_alignment(:binary, size, :default), do: compute_alignment(:binary, size, 8) + defp compute_alignment(:binary, _, _), do: 0 + defp compute_alignment(:float, _, _), do: 0 + defp compute_alignment(:utf32, _, _), do: 0 + defp compute_alignment(:utf16, _, _), do: 0 + defp compute_alignment(:utf8, _, _), do: 0 + defp compute_alignment(_, _, _), do: :unknown + + defp alignment(left, right) when is_integer(left) and is_integer(right) do + rem(left + right, 8) + end + + defp alignment(_, _), do: :unknown + + defp extract_meta({_, meta, _}, _), do: meta + defp extract_meta(_, meta), do: meta + + defp infer_spec(:bitstring, meta), do: {:bitstring, meta, nil} + defp infer_spec(:binary, meta), do: {:binary, meta, nil} + defp infer_spec(:float, meta), do: {:float, meta, nil} + defp infer_spec(:integer, meta), do: {:integer, meta, nil} + defp infer_spec(:default, meta), do: {:integer, meta, nil} + + defp expr_type(integer) when is_integer(integer), do: :integer + defp expr_type(float) when is_float(float), do: :float + defp expr_type(binary) when is_binary(binary), do: :binary + defp expr_type({:<<>>, _, _}), do: :bitstring + defp expr_type(_), do: :default + + defp concat_or_prepend_bitstring(_meta, {:<<>>, _, []}, _e_right, acc, _e, _require_size), + do: acc + + defp concat_or_prepend_bitstring( + meta, + {:<<>>, parts_meta, parts} = e_left, + e_right, + acc, + e, + require_size + ) do + case e do + %{context: :match} when require_size -> + case List.last(parts) do + {:"::", _spec_meta, [bin, {:binary, _, nil}]} when not is_binary(bin) -> + # function_error(spec_meta, e, __MODULE__, :unsized_binary) + :ok + + {:"::", _spec_meta, [_, {:bitstring, _, nil}]} -> + # function_error(spec_meta, e, __MODULE__, :unsized_binary) + :ok + + _ -> + :ok + end + + _ -> + :ok + end + + case e_right do + {:binary, _, nil} -> + {alignment, alignment} = Keyword.fetch!(parts_meta, :alignment) + + if is_integer(alignment) and alignment != 0 do + # function_error(meta, e, __MODULE__, {:unaligned_binary, e_left}) + Enum.reverse(parts, acc) + else + [{:"::", meta, [e_left, e_right]} | acc] + end + + {:bitstring, _, nil} -> + Enum.reverse(parts, acc) + end + end + + defp concat_or_prepend_bitstring(meta, e_left, e_right, acc, _e, _require_size) do + [{:"::", meta, [e_left, e_right]} | acc] + end + + defp unpack_specs({:-, _, [h, t]}, acc), do: unpack_specs(h, unpack_specs(t, acc)) + + defp unpack_specs({:*, _, [{:_, _, atom}, unit]}, acc) when is_atom(atom), + do: [{:unit, [], [unit]} | acc] + + defp unpack_specs({:*, _, [size, unit]}, acc), + do: [{:size, [], [size]}, {:unit, [], [unit]} | acc] + + defp unpack_specs(size, acc) when is_integer(size), do: [{:size, [], [size]} | acc] + + defp unpack_specs({expr, meta, args}, acc) when is_atom(expr) do + list_args = + cond do + is_atom(args) -> nil + is_list(args) -> args + true -> args + end + + [{expr, meta, list_args} | acc] + end + + defp unpack_specs(other, acc), do: [other | acc] + + defp validate_spec(spec, []), do: validate_spec(spec, nil) + defp validate_spec(:big, nil), do: {:endianness, :big} + defp validate_spec(:little, nil), do: {:endianness, :little} + defp validate_spec(:native, nil), do: {:endianness, :native} + defp validate_spec(:size, [size]), do: {:size, size} + defp validate_spec(:unit, [unit]), do: {:unit, unit} + defp validate_spec(:integer, nil), do: {:type, :integer} + defp validate_spec(:float, nil), do: {:type, :float} + defp validate_spec(:binary, nil), do: {:type, :binary} + defp validate_spec(:bytes, nil), do: {:type, :binary} + defp validate_spec(:bitstring, nil), do: {:type, :bitstring} + defp validate_spec(:bits, nil), do: {:type, :bitstring} + defp validate_spec(:utf8, nil), do: {:type, :utf8} + defp validate_spec(:utf16, nil), do: {:type, :utf16} + defp validate_spec(:utf32, nil), do: {:type, :utf32} + defp validate_spec(:signed, nil), do: {:sign, :signed} + defp validate_spec(:unsigned, nil), do: {:sign, :unsigned} + defp validate_spec(_, _), do: :none + + defp expand_spec_arg(expr, s, _original_s, e) when is_atom(expr) or is_integer(expr) do + {expr, s, e} + end + + defp expand_spec_arg(expr, s, original_s, %{context: :match} = e) do + %{prematch: {pre_read, pre_counter, _} = old_pre} = s + %{vars: {original_read, _}} = original_s + new_pre = {pre_read, pre_counter, {:bitsize, original_read}} + + {e_expr, se, ee} = + ElixirExpand.expand(expr, %{s | prematch: new_pre}, %{e | context: :guard}) + + {e_expr, %{se | prematch: old_pre}, %{ee | context: :match}} + end + + defp expand_spec_arg(expr, s, original_s, e) do + ElixirExpand.expand(expr, ElixirEnv.reset_read(s, original_s), e) + end + + defp validate_spec_arg(_meta, :unit, value, _s, _original_s, _e) when not is_integer(value) do + # function_error(meta, e, __MODULE__, {:bad_unit_argument, value}) + :ok + end + + defp validate_spec_arg(_meta, _key, _value, _s, _original_s, _e), do: :ok + + defp validate_size_required(_meta, :required, :default, type, :default, _e) + when type in [:binary, :bitstring] do + # function_error(meta, e, __MODULE__, :unsized_binary) + :ok + end + + defp validate_size_required(_, _, _, _, _, _), do: :ok + + defp size_and_unit(_meta, :bitstring, size, unit, _e) + when size != :default or unit != :default do + # function_error(meta, e, __MODULE__, :bittype_literal_bitstring) + [] + end + + defp size_and_unit(_meta, :binary, size, unit, _e) + when size != :default or unit != :default do + # function_error(meta, e, __MODULE__, :bittype_literal_string) + [] + end + + defp size_and_unit(_meta, _expr_type, size, unit, _e) do + add_arg(:unit, unit, add_arg(:size, size, [])) + end + + defp build_spec(_meta, size, unit, type, endianness, sign, spec, _e) + when type in [:utf8, :utf16, :utf32] do + cond do + size != :default or unit != :default -> + # function_error(meta, e, __MODULE__, :bittype_utf) + :ok + + sign != :default -> + # function_error(meta, e, __MODULE__, :bittype_signed) + :ok + + true -> + :ok + end + + add_spec(type, add_spec(endianness, spec)) + end + + defp build_spec(_meta, _size, unit, type, _endianness, sign, spec, _e) + when type in [:binary, :bitstring] do + cond do + type == :bitstring and unit != :default and unit != 1 -> + # function_error(meta, e, __MODULE__, {:bittype_mismatch, unit, 1, :unit}) + :ok + + sign != :default -> + # function_error(meta, e, __MODULE__, :bittype_signed) + :ok + + true -> + :ok + end + + add_spec(type, spec) + end + + defp build_spec(_meta, size, unit, type, endianness, sign, spec, _e) + when type in [:integer, :float] do + number_size = number_size(size, unit) + + cond do + type == :float and is_integer(number_size) -> + if valid_float_size(number_size) do + add_spec(type, add_spec(endianness, add_spec(sign, spec))) + else + # function_error(meta, e, __MODULE__, {:bittype_float_size, number_size}) + [] + end + + size == :default and unit != :default -> + # function_error(meta, e, __MODULE__, :bittype_unit) + [] + + true -> + add_spec(type, add_spec(endianness, add_spec(sign, spec))) + end + end + + defp add_spec(:default, spec), do: spec + defp add_spec(key, spec), do: [{key, [], nil} | spec] + + defp number_size(size, :default) when is_integer(size), do: size + defp number_size(size, unit) when is_integer(size), do: size * unit + defp number_size(size, _), do: size + + defp valid_float_size(16), do: true + defp valid_float_size(32), do: true + defp valid_float_size(64), do: true + defp valid_float_size(_), do: false + + defp add_arg(_key, :default, spec), do: spec + defp add_arg(key, arg, spec), do: [{key, [], [arg]} | spec] + + defp is_match_size([_ | _], %{context: :match}), do: true + defp is_match_size(_, _), do: false + + defp find_match([{:=, _, [_left, _right]} = expr | _rest]), do: expr + + defp find_match([{_, _, args} | rest]) when is_list(args) do + case find_match(args) do + false -> find_match(rest) + match -> match + end + end + + defp find_match([_arg | rest]), do: find_match(rest) + + defp find_match([]), do: false + end + + defmodule Fn do + alias ElixirSense.Core.Compiler, as: ElixirExpand + alias ElixirSense.Core.Compiler.Env, as: ElixirEnv + alias ElixirSense.Core.Compiler.Clauses, as: ElixirClauses + alias ElixirSense.Core.Compiler.Dispatch, as: ElixirDispatch + + def expand(meta, clauses, s, e) when is_list(clauses) do + transformer = fn + {:->, _, [left, _right]} = clause, sa -> + if Enum.any?(left, &is_invalid_arg/1) do + raise "defaults_in_args" + else + s_reset = ElixirEnv.reset_unused_vars(sa) + + {e_clause, s_acc, e_acc} = + ElixirClauses.clause(meta, :fn, &ElixirClauses.head/3, clause, s_reset, e) + + {e_clause, ElixirEnv.merge_and_check_unused_vars(s_acc, sa, e_acc)} + end + end + + {e_clauses, se} = Enum.map_reduce(clauses, s, transformer) + e_arities = Enum.map(e_clauses, fn {:->, _, [args, _]} -> fn_arity(args) end) + + case Enum.uniq(e_arities) do + [_] -> + {{:fn, meta, e_clauses}, se, e} + + _ -> + raise "clauses_with_different_arities" + end + end + + defp is_invalid_arg({:"\\\\", _, _}), do: true + defp is_invalid_arg(_), do: false + + defp fn_arity([{:when, _, args}]), do: length(args) - 1 + defp fn_arity(args), do: length(args) + + # Capture + + def capture(meta, {:/, _, [{{:., _, [_m, f]} = dot, require_meta, []}, a]}, s, e) + when is_atom(f) and is_integer(a) do + args = args_from_arity(meta, a, e) + # handle_capture_possible_warning(meta, require_meta, m, f, a, e) + capture_require({dot, require_meta, args}, s, e, true) + end + + def capture(meta, {:/, _, [{f, import_meta, c}, a]}, s, e) + when is_atom(f) and is_integer(a) and is_atom(c) do + args = args_from_arity(meta, a, e) + capture_import({f, import_meta, args}, s, e, true) + end + + def capture(_meta, {{:., _, [_, fun]}, _, args} = expr, s, e) + when is_atom(fun) and is_list(args) do + capture_require(expr, s, e, is_sequential_and_not_empty(args)) + end + + def capture(meta, {{:., _, [_]}, _, args} = expr, s, e) when is_list(args) do + capture_expr(meta, expr, s, e, false) + end + + def capture(meta, {:__block__, _, [expr]}, s, e) do + capture(meta, expr, s, e) + end + + def capture(_meta, {:__block__, _, _} = _expr, _s, _e) do + raise "block_expr_in_capture" + end + + def capture(_meta, {atom, _, args} = expr, s, e) when is_atom(atom) and is_list(args) do + capture_import(expr, s, e, is_sequential_and_not_empty(args)) + end + + def capture(meta, {left, right}, s, e) do + capture(meta, {:{}, meta, [left, right]}, s, e) + end + + def capture(meta, list, s, e) when is_list(list) do + capture_expr(meta, list, s, e, is_sequential_and_not_empty(list)) + end + + def capture(_meta, integer, _s, _e) when is_integer(integer) do + raise "capture_arg_outside_of_capture" + end + + def capture(_meta, _arg, _s, _e) do + raise "invalid_args_for_capture" + end + + defp capture_import({atom, import_meta, args} = expr, s, e, sequential) do + # TODO check similarity to macro expand_import + res = sequential && ElixirDispatch.import_function(import_meta, atom, length(args), e) + handle_capture(res, import_meta, import_meta, expr, s, e, sequential) + end + + defp capture_require({{:., dot_meta, [left, right]}, require_meta, args}, s, e, sequential) do + case escape(left, e, []) do + {esc_left, []} -> + {e_left, se, ee} = ElixirExpand.expand(esc_left, s, e) + + res = + sequential && + case e_left do + {name, _, context} when is_atom(name) and is_atom(context) -> + {:remote, e_left, right, length(args)} + + _ when is_atom(e_left) -> + # TODO check similarity to macro expand_require + ElixirDispatch.require_function(require_meta, e_left, right, length(args), ee) + + _ -> + false + end + + dot = {{:., dot_meta, [e_left, right]}, require_meta, args} + handle_capture(res, require_meta, dot_meta, dot, se, ee, sequential) + + {esc_left, escaped} -> + dot = {{:., dot_meta, [esc_left, right]}, require_meta, args} + capture_expr(require_meta, dot, s, e, escaped, sequential) + end + end + + defp handle_capture(false, meta, _dot_meta, expr, s, e, sequential) do + capture_expr(meta, expr, s, e, sequential) + end + + defp handle_capture(local_or_remote, meta, dot_meta, _expr, s, e, _sequential) do + {local_or_remote, meta, dot_meta, s, e} + end + + defp capture_expr(meta, expr, s, e, sequential) do + capture_expr(meta, expr, s, e, [], sequential) + end + + defp capture_expr(meta, expr, s, e, escaped, sequential) do + case escape(expr, e, escaped) do + {_, []} when not sequential -> + raise "invalid_args_for_capture" + + {e_expr, e_dict} -> + e_vars = validate(meta, e_dict, 1, e) + fn_expr = {:fn, meta, [{:->, meta, [e_vars, e_expr]}]} + {:expand, fn_expr, s, e} + end + end + + defp validate(meta, [{pos, var} | t], pos, e) do + [var | validate(meta, t, pos + 1, e)] + end + + defp validate(_meta, [{_pos, _} | _], _expected, _e) do + raise "capture_arg_without_predecessor" + end + + defp validate(_meta, [], _pos, _e), do: [] + + defp escape({:&, _, [pos]}, _e, dict) when is_integer(pos) and pos > 0 do + # Using a nil context here to emit warnings when variable is unused. + # This might pollute user space but is unlikely because variables + # named :"&1" are not valid syntax. + var = {:"&#{pos}", [], nil} + {var, :orddict.store(pos, var, dict)} + end + + defp escape({:&, _meta, [pos]}, _e, _dict) when is_integer(pos) do + raise "invalid_arity_for_capture" + end + + defp escape({:&, _meta, _} = _arg, _e, _dict) do + raise "nested_capture" + end + + defp escape({left, meta, right}, e, dict0) do + {t_left, dict1} = escape(left, e, dict0) + {t_right, dict2} = escape(right, e, dict1) + {{t_left, meta, t_right}, dict2} + end + + defp escape({left, right}, e, dict0) do + {t_left, dict1} = escape(left, e, dict0) + {t_right, dict2} = escape(right, e, dict1) + {{t_left, t_right}, dict2} + end + + defp escape(list, e, dict) when is_list(list) do + Enum.map_reduce(list, dict, fn x, acc -> escape(x, e, acc) end) + end + + defp escape(other, _e, dict) do + {other, dict} + end + + defp args_from_arity(_meta, a, _e) when is_integer(a) and a >= 0 and a <= 255 do + Enum.map(1..a, fn x -> {:&, [], [x]} end) + end + + defp args_from_arity(_meta, _a, _e) do + raise "invalid_arity_for_capture" + end + + defp is_sequential_and_not_empty([]), do: false + defp is_sequential_and_not_empty(list), do: is_sequential(list, 1) + + defp is_sequential([{:&, _, [int]} | t], int), do: is_sequential(t, int + 1) + defp is_sequential([], _int), do: true + defp is_sequential(_, _int), do: false + end + + defmodule Quote do + alias ElixirSense.Core.Compiler.Dispatch, as: ElixirDispatch + + defstruct line: false, + file: nil, + context: nil, + vars_hygiene: true, + aliases_hygiene: true, + imports_hygiene: true, + unquote: true, + generated: false + + def fun_to_quoted(function) do + {:module, module} = :erlang.fun_info(function, :module) + {:name, name} = :erlang.fun_info(function, :name) + {:arity, arity} = :erlang.fun_info(function, :arity) + + {:&, [], [{:/, [], [{{:., [], [module, name]}, [{:no_parens, true}], []}, arity]}]} + end + + def build(meta, line, file, context, unquote, generated) do + acc0 = [] + + {e_line, acc1} = validate_compile(meta, :line, line, acc0) + {e_file, acc2} = validate_compile(meta, :file, file, acc1) + {e_context, acc3} = validate_compile(meta, :context, context, acc2) + + validate_runtime(:unquote, unquote) + validate_runtime(:generated, generated) + + q = %__MODULE__{ + line: e_line, + file: e_file, + unquote: unquote, + context: e_context, + generated: generated + } + + {q, acc3} + end + + def validate_compile(_meta, :line, value, acc) when is_boolean(value) do + {value, acc} + end + + def validate_compile(_meta, :file, nil, acc) do + {nil, acc} + end + + def validate_compile(meta, key, value, acc) do + case is_valid(key, value) do + true -> + {value, acc} + + false -> + var = {key, meta, __MODULE__} + call = {{:., meta, [__MODULE__, :validate_runtime]}, meta, [key, value]} + {var, [{:=, meta, [var, call]} | acc]} + end + end + + def validate_runtime(key, value) do + case is_valid(key, value) do + true -> + value + + false -> + raise ArgumentError, + "invalid runtime value for option :#{Atom.to_string(key)} in quote, got: #{inspect(value)}" + end + end + + def is_valid(:line, line), do: is_integer(line) + def is_valid(:file, file), do: is_binary(file) + def is_valid(:context, context), do: is_atom(context) and context != nil + def is_valid(:generated, generated), do: is_boolean(generated) + def is_valid(:unquote, unquote), do: is_boolean(unquote) + + def quote(_meta, {:unquote_splicing, _, [_]}, _binding, %__MODULE__{unquote: true}, _, _), + do: raise("unquote_splicing only works inside arguments and block contexts") + + def quote(meta, expr, binding, q, prelude, e) do + context = q.context + + vars = + Enum.map(binding, fn {k, v} -> + {:{}, [], {:=, [], {:{}, [], [k, meta, context]}, v}} + end) + + quoted = do_quote(expr, q, e) |> dbg + + with_vars = + case vars do + [] -> quoted + _ -> {:{}, [], [:__block__, [], vars ++ [quoted]]} + end + + case prelude do + [] -> with_vars + _ -> {:__block__, [], prelude ++ [with_vars]} + end + end + + # quote/unquote + + defp do_quote({:quote, meta, [arg]}, q, e) do + t_arg = do_quote(arg, %__MODULE__{q | unquote: false}, e) + + new_meta = + case q do + %__MODULE__{vars_hygiene: true, context: context} -> + keystore(:context, meta, context) + + _ -> + meta + end + + {:{}, [], [:quote, meta(new_meta, q), [t_arg]]} + end + + defp do_quote({:quote, meta, [opts, arg]}, q, e) do + t_opts = do_quote(opts, q, e) + t_arg = do_quote(arg, %__MODULE__{q | unquote: false}, e) + + new_meta = + case q do + %__MODULE__{vars_hygiene: true, context: context} -> + keystore(:context, meta, context) + + _ -> + meta + end + + {:{}, [], [:quote, meta(new_meta, q), [t_opts, t_arg]]} + end + + defp do_quote({:unquote, _meta, [expr]}, %__MODULE__{unquote: true}, _), do: expr + + # Aliases + + defp do_quote({:__aliases__, meta, [h | t] = list}, %__MODULE__{aliases_hygiene: true} = q, e) + when is_atom(h) and h != :"Elixir" do + annotation = + case Macro.Env.expand_alias(e, meta, list, trace: false) do + {:alias, atom} -> atom + :error -> false + end + + alias_meta = keystore(:alias, Keyword.delete(meta, :counter), annotation) + do_quote_tuple(:__aliases__, alias_meta, [h | t], q, e) + end + + # Vars + + defp do_quote({name, meta, nil}, %__MODULE__{vars_hygiene: true} = q, e) + when is_atom(name) and is_list(meta) do + import_meta = + if q.imports_hygiene do + import_meta(meta, name, 0, q, e) + else + meta + end + + {:{}, [], [name, meta(import_meta, q), q.context]} + end + + # Unquote + + defp do_quote( + {{{:., meta, [left, :unquote]}, _, [expr]}, _, args}, + %__MODULE__{unquote: true} = q, + e + ) do + do_quote_call(left, meta, expr, args, q, e) + end + + defp do_quote({{:., meta, [left, :unquote]}, _, [expr]}, %__MODULE__{unquote: true} = q, e) do + do_quote_call(left, meta, expr, nil, q, e) + end + + # Imports + + defp do_quote( + {:&, meta, [{:/, _, [{f, _, c}, a]}] = args}, + %__MODULE__{imports_hygiene: true} = q, + e + ) + when is_atom(f) and is_integer(a) and is_atom(c) do + new_meta = + case ElixirDispatch.find_import(meta, f, a, e) do + false -> + meta + + receiver -> + keystore(:context, keystore(:imports, meta, [{a, receiver}]), q.context) + end + + do_quote_tuple(:&, new_meta, args, q, e) + end + + defp do_quote({name, meta, args_or_context}, %__MODULE__{imports_hygiene: true} = q, e) + when is_atom(name) and is_list(meta) and + (is_list(args_or_context) or is_atom(args_or_context)) do + arity = + case args_or_context do + args when is_list(args) -> length(args) + _context when is_atom(args_or_context) -> 0 + end + + import_meta = import_meta(meta, name, arity, q, e) + annotated = annotate({name, import_meta, args_or_context}, q.context) + do_quote_tuple(annotated, q, e) + end + + # Two-element tuples + + defp do_quote({left, right}, %__MODULE__{unquote: true} = q, e) + when is_tuple(left) and elem(left, 0) == :unquote_splicing and + is_tuple(right) and elem(right, 0) == :unquote_splicing do + do_quote({:{}, [], [left, right]}, q, e) + end + + defp do_quote({left, right}, q, e) do + t_left = do_quote(left, q, e) + t_right = do_quote(right, q, e) + {t_left, t_right} + end + + # Everything else + + defp do_quote(other, q, e) when is_atom(e) do + do_escape(other, q, e) + end + + defp do_quote({_, _, _} = tuple, q, e) do + annotated = annotate(tuple, q.context) + do_quote_tuple(annotated, q, e) + end + + defp do_quote([], _, _), do: [] + + defp do_quote([h | t], %__MODULE__{unquote: false} = q, e) do + head_quoted = do_quote(h, q, e) + do_quote_simple_list(t, head_quoted, q, e) + end + + defp do_quote([h | t], q, e) do + do_quote_tail(:lists.reverse(t, [h]), q, e) + end + + defp do_quote(other, _, _), do: other + + defp import_meta(meta, name, arity, q, e) do + case Keyword.get(meta, :import, false) == false && + ElixirDispatch.find_imports(meta, name, e) do + [] -> + case arity == 1 && Keyword.fetch(meta, :ambiguous_op) do + {:ok, nil} -> + keystore(:ambiguous_op, meta, q.context) + + _ -> + meta + end + + imports -> + keystore(:imports, keystore(:context, meta, q.context), imports) + end + end + + defp do_quote_call(left, meta, expr, args, q, e) do + all = [left, {:unquote, meta, [expr]}, args, q.context] + tall = Enum.map(all, fn x -> do_quote(x, q, e) end) + {{:., meta, [:elixir_quote, :dot]}, meta, [meta(meta, q) | tall]} + end + + defp do_quote_tuple({left, meta, right}, q, e) do + do_quote_tuple(left, meta, right, q, e) + end + + defp do_quote_tuple(left, meta, right, q, e) do + t_left = do_quote(left, q, e) + t_right = do_quote(right, q, e) + {:{}, [], [t_left, meta(meta, q), t_right]} + end + + defp do_quote_simple_list([], prev, _, _), do: [prev] + + defp do_quote_simple_list([h | t], prev, q, e) do + [prev | do_quote_simple_list(t, do_quote(h, q, e), q, e)] + end + + defp do_quote_simple_list(other, prev, q, e) do + [{:|, [], [prev, do_quote(other, q, e)]}] + end + + defp do_quote_tail( + [{:|, meta, [{:unquote_splicing, _, [left]}, right]} | t], + %__MODULE__{unquote: true} = q, + e + ) do + tt = do_quote_splice(t, q, e, [], []) + tr = do_quote(right, q, e) + do_runtime_list(meta, :tail_list, [left, tr, tt]) + end + + defp do_quote_tail(list, q, e) do + do_quote_splice(list, q, e, [], []) + end + + defp do_quote_splice( + [{:unquote_splicing, meta, [expr]} | t], + %__MODULE__{unquote: true} = q, + e, + buffer, + acc + ) do + runtime = do_runtime_list(meta, :list, [expr, do_list_concat(buffer, acc)]) + do_quote_splice(t, q, e, [], runtime) + end + + defp do_quote_splice([h | t], q, e, buffer, acc) do + th = do_quote(h, q, e) + do_quote_splice(t, q, e, [th | buffer], acc) + end + + defp do_quote_splice([], _q, _e, buffer, acc) do + do_list_concat(buffer, acc) + end + + defp do_list_concat(left, []), do: left + defp do_list_concat([], right), do: right + + defp do_list_concat(left, right) do + {{:., [], [:erlang, :++]}, [], [left, right]} + end + + defp do_runtime_list(meta, fun, args) do + {{:., meta, [:elixir_quote, fun]}, meta, args} + end + + defp meta(meta, q) do + generated(keep(Keyword.delete(meta, :column), q), q) + end + + defp generated(meta, %__MODULE__{generated: true}), do: [{:generated, true} | meta] + defp generated(meta, %__MODULE__{generated: false}), do: meta + + defp keep(meta, %__MODULE__{file: nil, line: line}) do + line(meta, line) + end + + defp keep(meta, %__MODULE__{file: file}) do + case Keyword.pop(meta, :line) do + {nil, _} -> + [{:keep, {file, 0}} | meta] + + {line, meta_no_line} -> + [{:keep, {file, line}} | meta_no_line] + end + end + + defp line(meta, true), do: meta + + defp line(meta, false) do + Keyword.delete(meta, :line) + end + + defp line(meta, line) do + keystore(:line, meta, line) + end + + defguardp defs(kind) when kind in [:def, :defp, :defmacro, :defmacrop, :@] + defguardp lexical(kind) when kind in [:import, :alias, :require] + + defp annotate({def, meta, [h | t]}, context) when defs(def) do + {def, meta, [annotate_def(h, context) | t]} + end + + defp annotate({{:., _, [_, def]} = target, meta, [h | t]}, context) when defs(def) do + {target, meta, [annotate_def(h, context) | t]} + end + + defp annotate({lexical, meta, [_ | _] = args}, context) when lexical(lexical) do + new_meta = keystore(:context, Keyword.delete(meta, :counter), context) + {lexical, new_meta, args} + end + + defp annotate(tree, _context), do: tree + + defp annotate_def({:when, meta, [left, right]}, context) do + {:when, meta, [annotate_def(left, context), right]} + end + + defp annotate_def({fun, meta, args}, context) do + {fun, keystore(:context, meta, context), args} + end + + defp annotate_def(other, _context), do: other + + defp do_escape({left, meta, right}, q, e = :prune_metadata) do + tm = for {k, v} <- meta, k == :no_parens or k == :line, do: {k, v} + tl = do_quote(left, q, e) + tr = do_quote(right, q, e) + {:{}, [], [tl, tm, tr]} + end + + defp do_escape(tuple, q, e) when is_tuple(tuple) do + tt = do_quote(Tuple.to_list(tuple), q, e) + {:{}, [], tt} + end + + defp do_escape(bitstring, _, _) when is_bitstring(bitstring) do + case Bitwise.band(bit_size(bitstring), 7) do + 0 -> + bitstring + + size -> + <> = bitstring + + {:<<>>, [], + [{:"::", [], [bits, {size, [], [size]}]}, {:"::", [], [bytes, {:binary, [], nil}]}]} + end + end + + defp do_escape(map, q, e) when is_map(map) do + tt = do_quote(Enum.sort(Map.to_list(map)), q, e) + {:%{}, [], tt} + end + + defp do_escape([], _, _), do: [] + + defp do_escape([h | t], %__MODULE__{unquote: false} = q, e) do + do_quote_simple_list(t, do_quote(h, q, e), q, e) + end + + defp do_escape([h | t], q, e) do + # The improper case is inefficient, but improper lists are rare. + try do + l = Enum.reverse(t, [h]) + do_quote_tail(l, q, e) + catch + _ -> + {l, r} = reverse_improper(t, [h]) + tl = do_quote_splice(l, q, e, [], []) + tr = do_quote(r, q, e) + update_last(tl, fn x -> {:|, [], [x, tr]} end) + end + end + + defp do_escape(other, _, _) when is_number(other) or is_pid(other) or is_atom(other), + do: other + + defp do_escape(fun, _, _) when is_function(fun) do + case {Function.info(fun, :env), Function.info(fun, :type)} do + {{:env, []}, {:type, :external}} -> + fun_to_quoted(fun) + + _ -> + raise ArgumentError + end + end + + defp do_escape(_other, _, _), do: raise(ArgumentError) + + defp reverse_improper([h | t], acc), do: reverse_improper(t, [h | acc]) + defp reverse_improper([], acc), do: acc + defp reverse_improper(t, acc), do: {acc, t} + defp update_last([], _), do: [] + defp update_last([h], f), do: [f.(h)] + defp update_last([h | t], f), do: [h | update_last(t, f)] + + defp keystore(_key, meta, value) when value == nil do + meta + end + + defp keystore(key, meta, value) do + :lists.keystore(key, 1, meta, {key, value}) + end + end + + defmodule Dispatch do + import :ordsets, only: [is_element: 2] + + def find_import(meta, name, arity, e) do + tuple = {name, arity} + + case find_import_by_name_arity(meta, tuple, [], e) do + {:function, receiver} -> + # ElixirEnv.trace({:imported_function, meta, receiver, name, arity}, e) + receiver + + {:macro, receiver} -> + # ElixirEnv.trace({:imported_macro, meta, receiver, name, arity}, e) + receiver + + _ -> + false + end + end + + def find_imports(meta, name, e) do + funs = e.functions + macs = e.macros + + acc0 = %{} + acc1 = find_imports_by_name(funs, acc0, name, meta, e) + acc2 = find_imports_by_name(macs, acc1, name, meta, e) + + imports = acc2 |> Map.to_list() |> Enum.sort() + # trace_import_quoted(imports, meta, name, e) + imports + end + + def import_function(meta, name, arity, e) do + tuple = {name, arity} + + case find_import_by_name_arity(meta, tuple, [], e) do + {:function, receiver} -> + # ElixirEnv.trace({:imported_function, meta, receiver, name, arity}, e) + # ElixirLocals.record_import(tuple, receiver, e.module, e.function) + remote_function(meta, receiver, name, arity, e) + + {:macro, _receiver} -> + false + + {:import, receiver} -> + require_function(meta, receiver, name, arity, e) + + false -> + if Macro.special_form?(name, arity) do + false + else + function = e.function + + # TODO the condition has this at the end + # and not ElixirDef.local_for(meta, name, arity, [:defmacro, :defmacrop], e) + if function != nil and function != tuple do + # ElixirEnv.trace({:local_function, meta, name, arity}, e) + # ElixirLocals.record_local(tuple, e.module, function, meta, false) + # TODO we may want to record + {:local, name, arity} + else + false + end + end + end + end + + def require_function(meta, receiver, name, arity, e) do + required = receiver in e.requires + + case is_macro(name, arity, receiver, required) do + true -> + false + + false -> + # ElixirEnv.trace({:remote_function, meta, receiver, name, arity}, e) + remote_function(meta, receiver, name, arity, e) + end + end + + defp remote_function(_meta, receiver, name, arity, _e) do + # check_deprecated(:function, meta, receiver, name, arity, e) + + # TODO rewrite is safe to use as it does not emit traces and does not have side effects + # but we may need to translate it anyway + case :elixir_rewrite.inline(receiver, name, arity) do + {ar, an} -> {:remote, ar, an, arity} + false -> {:remote, receiver, name, arity} + end + end + + def find_imports_by_name([{mod, imports} | mod_imports], acc, name, meta, e) do + new_acc = find_imports_by_name(name, imports, acc, mod, meta, e) + find_imports_by_name(mod_imports, new_acc, name, meta, e) + end + + def find_imports_by_name([], acc, _name, _meta, _e), do: acc + + def find_imports_by_name(name, [{name, arity} | imports], acc, mod, meta, e) do + case Map.get(acc, arity) do + nil -> + find_imports_by_name(name, imports, Map.put(acc, arity, mod), mod, meta, e) + + _other_mod -> + raise "ambiguous_call" + end + end + + def find_imports_by_name(name, [{import_name, _} | imports], acc, mod, meta, e) + when name > import_name do + find_imports_by_name(name, imports, acc, mod, meta, e) + end + + def find_imports_by_name(_name, _imports, acc, _mod, _meta, _e), do: acc + + defp find_import_by_name_arity(meta, {_name, arity} = tuple, extra, e) do + case is_import(meta, arity) do + {:import, _} = import_res -> + import_res + + false -> + funs = e.functions + macs = extra ++ e.macros + fun_match = find_import_by_name_arity(tuple, funs) + mac_match = find_import_by_name_arity(tuple, macs) + + case {fun_match, mac_match} do + {[], [receiver]} -> + {:macro, receiver} + + {[receiver], []} -> + {:function, receiver} + + {[], []} -> + false + + _ -> + raise "ambiguous_call" + end + end + end + + defp find_import_by_name_arity(tuple, list) do + for {receiver, set} <- list, is_element(tuple, set), do: receiver + end + + defp is_import(meta, arity) do + with {:ok, imports} <- Keyword.fetch(meta, :imports), + {:ok, _} <- Keyword.fetch(meta, :context), + {:ok, receiver} <- Keyword.fetch(imports, arity) do + {:import, receiver} + else + _ -> false + end + end + + defp is_macro(_name, _arity, _module, false), do: false + + defp is_macro(name, arity, receiver, true) do + try do + # TODO is it OK for local requires? + macros = receiver.__info__(:macros) + {name, arity} in macros + rescue + _error -> false + end + end + end + + defmodule Map do + alias ElixirSense.Core.Compiler, as: ElixirExpand + + def expand_struct(meta, left, {:%{}, map_meta, map_args}, s, %{context: context} = e) do + clean_map_args = clean_struct_key_from_map_args(meta, map_args, e) + + {[e_left, e_right], se, ee} = + ElixirExpand.expand_args([left, {:%{}, map_meta, clean_map_args}], s, e) + + case validate_struct(e_left, context) do + true when is_atom(e_left) -> + case extract_struct_assocs(meta, e_right, e) do + {:expand, map_meta, assocs} when context != :match -> + assoc_keys = Enum.map(assocs, fn {k, _} -> k end) + struct = load_struct(meta, e_left, [assocs], assoc_keys, ee) + keys = [:__struct__ | assoc_keys] + without_keys = Elixir.Map.drop(struct, keys) + struct_assocs = Macro.escape(Enum.sort(Elixir.Map.to_list(without_keys))) + {{:%, meta, [e_left, {:%{}, map_meta, struct_assocs ++ assocs}]}, se, ee} + + {_, _, assocs} -> + _ = load_struct(meta, e_left, [], Enum.map(assocs, fn {k, _} -> k end), ee) + {{:%, meta, [e_left, e_right]}, se, ee} + end + + true -> + {{:%, meta, [e_left, e_right]}, se, ee} + + false when context == :match -> + raise "invalid_struct_name_in_match" + + false -> + raise "invalid_struct_name" + end + end + + def expand_struct(_meta, _left, _right, _s, _e), do: raise("non_map_after_struct") + + def expand_map(meta, [{:|, update_meta, [left, right]}], s, %{context: nil} = e) do + {[e_left | e_right], se, ee} = ElixirExpand.expand_args([left | right], s, e) + validate_kv(meta, e_right, right, e) + {{:%{}, meta, [{:|, update_meta, [e_left, e_right]}]}, se, ee} + end + + def expand_map(_meta, [{:|, _, [_, _]}] = _args, _s, _e) do + raise "update_syntax_in_wrong_context" + end + + def expand_map(meta, args, s, e) do + {e_args, se, ee} = ElixirExpand.expand_args(args, s, e) + validate_kv(meta, e_args, args, e) + {{:%{}, meta, e_args}, se, ee} + end + + defp clean_struct_key_from_map_args(meta, [{:|, pipe_meta, [left, map_assocs]}], e) do + [{:|, pipe_meta, [left, clean_struct_key_from_map_assocs(meta, map_assocs, e)]}] + end + + defp clean_struct_key_from_map_args(meta, map_assocs, e) do + clean_struct_key_from_map_assocs(meta, map_assocs, e) + end + + defp clean_struct_key_from_map_assocs(_meta, assocs, _e) do + case Keyword.pop(assocs, :__struct__) do + {nil, cleaned_assocs} -> + cleaned_assocs + + {_struct_value, cleaned_assocs} -> + # file_warn(meta, Map.get(e, :file), __MODULE__, :ignored_struct_key_in_struct) + cleaned_assocs + end + end + + defp validate_kv(meta, kv, _original, %{context: context} = e) do + Enum.reduce(kv, {1, %{}}, fn + {k, _v}, {index, used} -> + if context == :match do + validate_match_key(meta, k, e) + end + + new_used = validate_not_repeated(meta, k, used, e) + {index + 1, new_used} + + _, {_index, _used} -> + raise "not_kv_pair" + end) + end + + defp validate_not_repeated(_meta, key, used, e) do + if is_literal(key) and Elixir.Map.has_key?(used, key) do + case e do + %{context: :match} -> + # raise "repeated_key" + # function_error(meta, Map.get(e, :file), __MODULE__, {:repeated_key, key}) + :ok + + _ -> + # file_warn(meta, Map.get(e, :file), __MODULE__, {:repeated_key, key}) + :ok + end + + used + else + Elixir.Map.put(used, key, true) + end + end + + defp validate_match_key(_meta, {name, _, context}, _e) + when is_atom(name) and is_atom(context) do + raise "invalid_variable_in_map_key_match" + end + + defp validate_match_key(meta, {:"::", _, [left, _]}, e) do + validate_match_key(meta, left, e) + end + + defp validate_match_key(_, {:^, _, [{name, _, context}]}, _) + when is_atom(name) and is_atom(context), + do: :ok + + defp validate_match_key(_, {:%{}, _, [_ | _]}, _), do: :ok + + defp validate_match_key(meta, {left, _, right}, e) do + validate_match_key(meta, left, e) + validate_match_key(meta, right, e) + end + + defp validate_match_key(meta, {left, right}, e) do + validate_match_key(meta, left, e) + validate_match_key(meta, right, e) + end + + defp validate_match_key(meta, list, e) when is_list(list) do + for each <- list do + validate_match_key(meta, each, e) + end + end + + defp validate_match_key(_, _, _), do: :ok + + defp is_literal({_, _, _}), do: false + + defp is_literal({left, right}), do: is_literal(left) and is_literal(right) + + defp is_literal(list) when is_list(list), do: Enum.all?(list, &is_literal/1) + + defp is_literal(_), do: true + + defp validate_struct({:^, _, [{var, _, ctx}]}, :match) when is_atom(var) and is_atom(ctx), + do: true + + defp validate_struct({var, _meta, ctx}, :match) when is_atom(var) and is_atom(ctx), do: true + defp validate_struct(atom, _) when is_atom(atom), do: true + defp validate_struct(_, _), do: false + + defp extract_struct_assocs(_, {:%{}, meta, [{:|, _, [_, assocs]}]}, _) do + {:update, meta, delete_struct_key(assocs)} + end + + defp extract_struct_assocs(_, {:%{}, meta, assocs}, _) do + {:expand, meta, delete_struct_key(assocs)} + end + + defp extract_struct_assocs(_meta, _other, _e) do + raise "non_map_after_struct" + end + + defp delete_struct_key(assocs) do + Keyword.delete(assocs, :__struct__) + end + + defp load_struct(meta, name, args, keys, e) do + module = e.module + in_context = name in [module | e.context_modules] + + _arity = length(args) + # TODO + # or (not ensure_loaded(name) and wait_for_struct(name)) + external = in_context + + try do + # TODO the condition includes + # and ElixirDef.external_for(meta, name, :__struct__, arity, [:def]) + case external do + false when module == name -> + raise UndefinedFunctionError + + false -> + apply(name, :__struct__, args) + + external_fun -> + try do + apply(external_fun, args) + rescue + UndefinedFunctionError -> apply(name, :__struct__, args) + end + end + rescue + UndefinedFunctionError -> + cond do + in_context and e.function == nil -> + raise "inaccessible_struct" + + true -> + raise "undefined_struct" + end + else + %{:__struct__ => struct_name} = struct when is_atom(struct_name) -> + assert_struct_keys(meta, name, struct, keys, e) + # ElixirEnv.trace({:struct_expansion, meta, name, keys}, e) + struct + + %{:__struct__ => struct_name} when is_atom(struct_name) -> + raise "struct_name_mismatch" + + _other -> + raise "invalid_struct_return_value" + end + end + + defp assert_struct_keys(_meta, _name, struct, keys, _e) do + for key <- keys, not Elixir.Map.has_key?(struct, key) do + raise "unknown_key_for_struct" + end + end + end +end diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index 4a685d58..1aa134d5 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -13,6 +13,7 @@ defmodule ElixirSense.Core.MetadataBuilder do alias ElixirSense.Core.State.VarInfo alias ElixirSense.Core.TypeInfo alias ElixirSense.Core.Guard + alias ElixirSense.Core.Compiler @scope_keywords [:for, :fn, :with] @block_keywords [:do, :else, :rescue, :catch, :after] @@ -35,52 +36,57 @@ defmodule ElixirSense.Core.MetadataBuilder do """ @spec build(Macro.t()) :: State.t() def build(ast) do - # dbg(ast) - {_ast, [state]} = - Macro.traverse(ast, [%State{}], &safe_call_pre/2, &safe_call_post/2) - - try do + if Version.match?(System.version(), ">= 1.17.0-dev") do + {_ast, state, _env} = Compiler.expand(ast, %State{}, Compiler.env()) state - |> remove_attributes_scope - |> remove_lexical_scope - |> remove_vars_scope - |> remove_module - |> remove_protocol_implementation - rescue - exception -> - warn( - Exception.format( - :error, - "#{inspect(exception.__struct__)} during metadata build scope closing:\n" <> - "#{Exception.message(exception)}\n" <> - "ast node: #{inspect(ast, limit: :infinity)}", - __STACKTRACE__ + else + # dbg(ast) + {_ast, [state]} = + Macro.traverse(ast, [%State{}], &safe_call_pre/2, &safe_call_post/2) + + try do + state + |> remove_attributes_scope + |> remove_lexical_scope + |> remove_vars_scope + |> remove_module + |> remove_protocol_implementation + rescue + exception -> + warn( + Exception.format( + :error, + "#{inspect(exception.__struct__)} during metadata build scope closing:\n" <> + "#{Exception.message(exception)}\n" <> + "ast node: #{inspect(ast, limit: :infinity)}", + __STACKTRACE__ + ) ) - ) - vars_info_per_scope_id = - try do - update_vars_info_per_scope_id(state) - rescue - _ -> - state.vars_info_per_scope_id - end + vars_info_per_scope_id = + try do + update_vars_info_per_scope_id(state) + rescue + _ -> + state.vars_info_per_scope_id + end - %{ - state - | attributes: [], - scope_attributes: [], - aliases: [], - imports: [], - requires: [], - scope_ids: [], - vars: [], - scope_vars: [], - vars_info_per_scope_id: vars_info_per_scope_id, - module: [], - scopes: [], - protocols: [] - } + %{ + state + | attributes: [], + scope_attributes: [], + aliases: [], + imports: [], + requires: [], + scope_ids: [], + vars: [], + scope_vars: [], + vars_info_per_scope_id: vars_info_per_scope_id, + module: [], + scopes: [], + protocols: [] + } + end end end diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index e5c0b4f5..dd15e80b 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -60,7 +60,8 @@ defmodule ElixirSense.Core.State do typedoc_context: list(), optional_callbacks_context: list(), # TODO better type - binding_context: list + binding_context: list, + macro_env: list(Macro.Env.t()) } @auto_imported_functions :elixir_env.new().functions @@ -85,6 +86,12 @@ defmodule ElixirSense.Core.State do specs: %{}, vars_info: [[]], scope_vars_info: [[]], + vars: {%{}, false}, + unused: {%{}, 0}, + prematch: :raise, + stacktrace: false, + caller: false, + runtime_modules: [], scope_id_count: 0, scope_ids: [0], vars_info_per_scope_id: %{}, @@ -100,7 +107,8 @@ defmodule ElixirSense.Core.State do doc_context: [[]], typedoc_context: [[]], optional_callbacks_context: [[]], - moduledoc_positions: %{} + moduledoc_positions: %{}, + macro_env: [:elixir_env.new()] defmodule Env do @moduledoc """ @@ -360,6 +368,31 @@ defmodule ElixirSense.Core.State do } end + def get_current_env(%__MODULE__{} = state, macro_env) do + # current_vars = state |> get_current_vars() + current_vars = state.vars |> elem(0) + current_attributes = state |> get_current_attributes() + current_behaviours = hd(state.behaviours) + + current_scope_id = hd(state.scope_ids) + current_scope_protocol = hd(state.protocols) + + %Env{ + functions: macro_env.functions, + macros: macro_env.macros, + requires: macro_env.requires, + aliases: macro_env.aliases, + module: macro_env.module, + function: macro_env.function, + vars: current_vars, + attributes: current_attributes, + behaviours: current_behaviours, + typespec: nil, + scope_id: current_scope_id, + protocol: current_scope_protocol + } + end + def get_current_module(%__MODULE__{} = state) do state.module |> hd end @@ -372,6 +405,16 @@ defmodule ElixirSense.Core.State do %__MODULE__{state | lines_to_env: Map.put(state.lines_to_env, line, env)} end + def add_current_env_to_line(%__MODULE__{} = state, line, macro_env) when is_integer(line) do + _previous_env = state.lines_to_env[line] + current_env = get_current_env(state, macro_env) + + # TODO + # env = merge_env_vars(current_env, previous_env) + env = current_env + %__MODULE__{state | lines_to_env: Map.put(state.lines_to_env, line, env)} + end + defp merge_env_vars(%Env{vars: current_vars} = current_env, previous_env) do case previous_env do nil -> @@ -1833,5 +1876,59 @@ defmodule ElixirSense.Core.State do generated: true ) end) + def macro_env(%__MODULE__{} = state, meta \\ []) do + function = + case hd(hd(state.scopes)) do + {function, arity} -> {function, arity} + _ -> nil + end + + context_modules = + state.mods_funs_to_positions + |> Enum.filter(&match?({{_module, nil, nil}, _}, &1)) + |> Enum.map(&(elem(&1, 0) |> elem(0))) + + %Macro.Env{ + aliases: current_aliases(state), + requires: current_requires(state), + module: get_current_module(state), + line: Keyword.get(meta, :line, 0), + function: function, + # TODO context :guard, :match + context: nil, + context_modules: context_modules, + functions: state.functions |> hd, + macros: state.macros |> hd + # TODO macro_aliases + # TODO versioned_vars + } + end + + def macro_env(%__MODULE__{} = state, %__MODULE__.Env{} = env, line) do + function = + case env.scope do + {function, arity} -> {function, arity} + _ -> nil + end + + context_modules = + state.mods_funs_to_positions + |> Enum.filter(&match?({{_module, nil, nil}, _}, &1)) + |> Enum.map(&(elem(&1, 0) |> elem(0))) + + %Macro.Env{ + aliases: env.aliases, + requires: env.requires, + module: env.module, + line: line, + function: function, + # TODO context :guard, :match + context: nil, + context_modules: context_modules, + functions: env.functions, + macros: env.macros + # TODO macro_aliases + # TODO versioned_vars + } end end diff --git a/test/elixir_sense/core/compiler_test.exs b/test/elixir_sense/core/compiler_test.exs new file mode 100644 index 00000000..8019a774 --- /dev/null +++ b/test/elixir_sense/core/compiler_test.exs @@ -0,0 +1,625 @@ +if Version.match?(System.version(), ">= 1.17.0-dev") do + defmodule ElixirSense.Core.CompilerTest do + use ExUnit.Case, async: true + alias ElixirSense.Core.Compiler + alias ElixirSense.Core.State + require Record + + defp to_quoted!(string_or_ast, ast \\ false) + defp to_quoted!(ast, true), do: ast + + defp to_quoted!(string, false), + do: Code.string_to_quoted!(string, columns: true, token_metadata: true) + + Record.defrecordp(:elixir_ex, + caller: false, + prematch: :raise, + stacktrace: false, + unused: {%{}, 0}, + runtime_modules: [], + vars: {%{}, false} + ) + + defp elixir_ex_to_map( + elixir_ex( + caller: caller, + prematch: prematch, + stacktrace: stacktrace, + unused: unused, + runtime_modules: runtime_modules, + vars: vars + ) + ) do + %{ + caller: caller, + prematch: prematch, + stacktrace: stacktrace, + unused: unused, + runtime_modules: runtime_modules, + vars: vars + } + end + + defp state_to_map(%State{} = state) do + Map.take(state, [:caller, :prematch, :stacktrace, :unused, :runtime_modules, :vars]) + end + + defp expand(ast) do + Compiler.expand(ast, %State{}, Compiler.env()) + end + + defp elixir_expand(ast) do + env = :elixir_env.new() + :elixir_expand.expand(ast, :elixir_env.env_to_ex(env), env) + end + + defmacrop assert_expansion(code, ast \\ false) do + quote do + ast = to_quoted!(unquote(code), unquote(ast)) + {elixir_expanded, elixir_state, elixir_env} = elixir_expand(ast) + dbg(elixir_expanded) + {expanded, state, env} = expand(ast) + dbg(expanded) + + assert expanded == elixir_expanded + assert env == elixir_env + assert state_to_map(state) == elixir_ex_to_map(elixir_state) + end + end + + defmacrop assert_expansion_env(code, ast \\ false) do + quote do + ast = to_quoted!(unquote(code), unquote(ast)) + {elixir_expanded, elixir_state, elixir_env} = elixir_expand(ast) + dbg(elixir_expanded) + dbg(elixir_ex_to_map(elixir_state)) + {expanded, state, env} = expand(ast) + dbg(expanded) + dbg(state_to_map(state)) + + assert env == elixir_env + assert state_to_map(state) == elixir_ex_to_map(elixir_state) + end + end + + test "initial" do + elixir_env = :elixir_env.new() + assert Compiler.env() == elixir_env + assert state_to_map(%State{}) == elixir_ex_to_map(:elixir_env.env_to_ex(elixir_env)) + end + + describe "special forms" do + test "expands =" do + assert_expansion("1 = 1") + end + + test "expands {}" do + assert_expansion("{}") + assert_expansion("{1, 2, 3}") + assert_expansion("{a, b} = {:ok, 1}") + end + + test "expands %{}" do + assert_expansion("%{1 => 2}") + assert_expansion("%{a: 3}") + assert_expansion("%{a: a} = %{}") + assert_expansion("%{1 => a} = %{}") + assert_expansion("%{%{a: 1} | a: 2}") + end + + test "expands %" do + assert_expansion("%Date{year: 2024, month: 2, day: 18}") + assert_expansion("%Date{calendar: Calendar.ISO, year: 2024, month: 2, day: 18}") + assert_expansion("%{year: x} = %Date{year: 2024, month: 2, day: 18}") + assert_expansion("%Date{%Date{year: 2024, month: 2, day: 18} | day: 1}") + assert_expansion("%{%Date{year: 2024, month: 2, day: 18} | day: 1}") + end + + test "expands <<>>" do + assert_expansion("<<>>") + assert_expansion("<<1>>") + assert_expansion("<> = \"\"") + end + + test "expands __block__" do + assert_expansion({:__block__, [], []}, true) + assert_expansion({:__block__, [], [1]}, true) + assert_expansion({:__block__, [], [1, 2]}, true) + end + + test "expands __aliases__" do + assert_expansion({:__aliases__, [], [:Asd, :Foo]}, true) + assert_expansion({:__block__, [], [:Asd]}, true) + assert_expansion({:__block__, [], [Elixir, :Asd]}, true) + end + + test "expands alias" do + assert_expansion("alias Foo") + assert_expansion("alias Foo.Bar") + assert_expansion("alias Foo.Bar, as: Baz") + end + + test "expands require" do + assert_expansion("require Code") + assert_expansion("require Code.Fragment") + assert_expansion("require Code.Fragment, as: Baz") + end + + test "expands import" do + assert_expansion("import Code") + assert_expansion("import Code.Fragment") + assert_expansion("import Code.Fragment, only: :functions") + end + + test "expands multi alias" do + assert_expansion("alias Foo.{Bar, Some.Other}") + end + + test "expands __MODULE__" do + ast = {:__MODULE__, [], nil} + {expanded, state, env} = Compiler.expand(ast, %State{}, %{Compiler.env() | module: Foo}) + elixir_env = %{:elixir_env.new() | module: Foo} + + {elixir_expanded, elixir_state, elixir_env} = + :elixir_expand.expand(ast, :elixir_env.env_to_ex(elixir_env), elixir_env) + + assert expanded == elixir_expanded + assert env == elixir_env + assert state_to_map(state) == elixir_ex_to_map(elixir_state) + end + + test "expands __DIR__" do + ast = {:__DIR__, [], nil} + + {expanded, state, env} = + Compiler.expand(ast, %State{}, %{Compiler.env() | file: __ENV__.file}) + + elixir_env = %{:elixir_env.new() | file: __ENV__.file} + + {elixir_expanded, elixir_state, elixir_env} = + :elixir_expand.expand(ast, :elixir_env.env_to_ex(elixir_env), elixir_env) + + assert expanded == elixir_expanded + assert env == elixir_env + assert state_to_map(state) == elixir_ex_to_map(elixir_state) + end + + test "expands __CALLER__" do + ast = {:__CALLER__, [], nil} + {expanded, state, env} = Compiler.expand(ast, %State{caller: true}, Compiler.env()) + elixir_env = :elixir_env.new() + + {elixir_expanded, elixir_state, elixir_env} = + :elixir_expand.expand( + ast, + elixir_ex(:elixir_env.env_to_ex(elixir_env), caller: true), + elixir_env + ) + + assert expanded == elixir_expanded + assert env == elixir_env + assert state_to_map(state) == elixir_ex_to_map(elixir_state) + end + + test "expands __STACKTRACE__" do + ast = {:__STACKTRACE__, [], nil} + {expanded, state, env} = Compiler.expand(ast, %State{stacktrace: true}, Compiler.env()) + elixir_env = :elixir_env.new() + + {elixir_expanded, elixir_state, elixir_env} = + :elixir_expand.expand( + ast, + elixir_ex(:elixir_env.env_to_ex(elixir_env), stacktrace: true), + elixir_env + ) + + assert expanded == elixir_expanded + assert env == elixir_env + assert state_to_map(state) == elixir_ex_to_map(elixir_state) + end + + test "expands __ENV__" do + ast = {:__ENV__, [], nil} + {expanded, state, env} = Compiler.expand(ast, %State{}, Compiler.env()) + elixir_env = :elixir_env.new() + + {elixir_expanded, elixir_state, elixir_env} = + :elixir_expand.expand(ast, :elixir_env.env_to_ex(elixir_env), elixir_env) + + assert expanded == elixir_expanded + assert env == elixir_env + assert state_to_map(state) == elixir_ex_to_map(elixir_state) + end + + test "expands __ENV__.property" do + assert_expansion("__ENV__.requires") + assert_expansion("__ENV__.foo") + end + + test "expands quote literal" do + assert_expansion("quote do: 2") + assert_expansion("quote do: :foo") + assert_expansion("quote do: \"asd\"") + assert_expansion("quote do: []") + assert_expansion("quote do: [12]") + assert_expansion("quote do: [12, 34]") + assert_expansion("quote do: [12 | 34]") + assert_expansion("quote do: [12 | [34]]") + assert_expansion("quote do: {12}") + assert_expansion("quote do: {12, 34}") + assert_expansion("quote do: %{a: 12}") + end + + test "expands quote variable" do + assert_expansion("quote do: abc") + end + + test "expands quote quote" do + assert_expansion(""" + quote do: (quote do: 1) + """) + end + + test "expands quote block" do + assert_expansion(""" + quote do: () + """) + end + + test "expands quote unquote" do + assert_expansion(""" + a = 1 + quote do: unquote(a) + """) + end + + test "expands quote unquote block" do + assert_expansion(""" + a = 1 + quote do: (unquote(a)) + """) + end + + test "expands quote unquote_splicing tuple" do + assert_expansion(""" + quote do: {unquote_splicing([1, 2]), unquote_splicing([2])} + """) + end + + test "expands quote unquote_splicing" do + assert_expansion(""" + a = [1, 2, 3] + quote do: (unquote_splicing(a)) + """) + end + + test "expands quote alias" do + assert_expansion("quote do: Date") + assert_expansion("quote do: Elixir.Date") + assert_expansion("quote do: String.Chars") + assert_expansion("alias String.Chars; quote do: Chars") + assert_expansion("alias String.Chars; quote do: Chars.foo().A") + end + + test "expands quote import" do + assert_expansion("quote do: inspect(1)") + assert_expansion("quote do: &inspect/1") + end + + test "expands &super" do + assert_expansion_env(""" + defmodule Abc do + use ElixirSense.Core.CompilerTest.Overridable + + def foo(a) do + &super/1 + end + end + """) + + assert_expansion_env(""" + defmodule Abc do + use ElixirSense.Core.CompilerTest.Overridable + + def foo(a) do + &super(&1) + end + end + """) + end + + test "expands &" do + assert_expansion("& &1") + assert_expansion("&Enum.take(&1, 5)") + assert_expansion("&{&1, &2}") + assert_expansion("&[&1 | &2]") + assert_expansion("&inspect/1") + assert_expansion("&Enum.count/1") + end + + test "expands fn" do + assert_expansion("fn -> 1 end") + assert_expansion("fn a, b -> {a, b} end") + + assert_expansion(""" + fn + 1 -> 1 + a -> a + end + """) + end + + test "expands cond" do + assert_expansion(""" + cond do + nil -> 0 + true -> 1 + end + """) + end + + test "expands case" do + assert_expansion(""" + case 1 do + 0 -> 0 + 1 -> 1 + end + """) + end + + test "expands try" do + assert_expansion(""" + try do + inspect(1) + rescue + e in ArgumentError -> + e + catch + {k, e} -> + {k, e} + else + _ -> :ok + after + IO.puts("") + end + """) + end + + test "expands receive" do + assert_expansion(""" + receive do + x -> x + after + 100 -> IO.puts("") + end + """) + end + + test "expands for" do + assert_expansion(""" + for i <- [1, 2, 3] do + i + end + """) + + assert_expansion(""" + for i <- [1, 2, 3], j <- [1, 2], true, into: %{}, do: {i, j} + """) + end + + test "expands with" do + assert_expansion(""" + with i <- :ok do + i + end + """) + + assert_expansion(""" + with :ok <- :ok, j = 5 do + j + else + a -> a + end + """) + end + + defmodule Overridable do + defmacro __using__(args) do + quote do + def foo(a) do + a + end + + defmacro bar(ast) do + ast + end + + defoverridable foo: 1, bar: 1 + end + end + end + + test "expands super" do + assert_expansion_env(""" + defmodule Abc do + use ElixirSense.Core.CompilerTest.Overridable + + def foo(a) do + super(a + 1) + end + + defmacro bar(a) do + quote do + unquote(super(b)) - 1 + end + end + end + """) + end + + test "expands var" do + assert_expansion("_ = 5") + assert_expansion("a = 5") + assert_expansion("a = 5; a") + assert_expansion("a = 5; ^a = 6") + end + + test "expands local call" do + assert_expansion("get_in(%{}, [:bar])") + assert_expansion("length([])") + end + + test "expands local call macro" do + # TODO + # assert_expansion("if true, do: :ok") + assert_expansion("1 |> IO.inspect") + end + + test "expands remote call" do + assert_expansion("Kernel.get_in(%{}, [:bar])") + assert_expansion("Kernel.length([])") + assert_expansion("Some.fun().other()") + end + + test "expands remote call macro" do + assert_expansion("Kernel.|>(1, IO.inspect)") + end + + test "expands anonymous call" do + assert_expansion("foo = fn a -> a end; foo.(1)") + end + + test "expands 2-tuple" do + assert_expansion("{1, 2}") + assert_expansion("{a, b} = {1, 2}") + end + + test "expands list" do + assert_expansion("[]") + assert_expansion("[1, 2]") + assert_expansion("[1 | [2]]") + assert_expansion("[a | b] = [1, 2, 3]") + end + + test "expands function" do + assert_expansion(&inspect/1, true) + end + + test "expands pid" do + assert_expansion(self(), true) + end + + test "expands number" do + assert_expansion(1, true) + assert_expansion(1.5, true) + end + + test "expands atom" do + assert_expansion(true, true) + assert_expansion(:foo, true) + assert_expansion(Kernel, true) + end + + test "expands binary" do + assert_expansion("abc", true) + end + end + + describe "Kernel macros" do + test "defmodule" do + assert_expansion_env("defmodule Abc, do: :ok") + + assert_expansion_env(""" + defmodule Abc do + foo = 1 + end + """) + + assert_expansion_env(""" + defmodule Abc.Some do + foo = 1 + end + """) + + assert_expansion_env(""" + defmodule Elixir.Abc.Some do + foo = 1 + end + """) + + assert_expansion_env(""" + defmodule Abc.Some do + defmodule Child do + foo = 1 + end + end + """) + + assert_expansion_env(""" + defmodule Abc.Some do + defmodule Elixir.Child do + foo = 1 + end + end + """) + end + + test "context local macro" do + # TODO this does not expand the macro + assert_expansion_env(""" + defmodule Abc do + defmacro foo(x) do + quote do + unquote(x) + 1 + end + end + + def go(z) do + foo(z) + end + end + """) + end + + test "context remote macro" do + # TODO this does not expand the macro + assert_expansion_env(""" + defmodule Abc do + defmacro foo(x) do + quote do + unquote(x) + 1 + end + end + end + + defmodule Cde do + require Abc + def go(z) do + Abc.foo(z) + end + end + """) + end + + test "def" do + ast = + Code.string_to_quoted( + """ + defmodule Abc do + def abc, do: :ok + end + """, + columns: true, + token_metadata: true + ) + + {expanded, state, env} = Compiler.expand(ast, %State{}, %{Compiler.env() | module: Foo}) + # elixir_env = %{:elixir_env.new() | module: Foo} + # {elixir_expanded, _elixir_state, elixir_env} = :elixir_expand.expand(ast, :elixir_env.env_to_ex(elixir_env), elixir_env) + + # assert expanded == elixir_expanded + # assert env == elixir_env + end + end + end +end diff --git a/test/elixir_sense/core/metadata_builder/alias_test.exs b/test/elixir_sense/core/metadata_builder/alias_test.exs index fa813b94..8c5f012f 100644 --- a/test/elixir_sense/core/metadata_builder/alias_test.exs +++ b/test/elixir_sense/core/metadata_builder/alias_test.exs @@ -58,6 +58,7 @@ defmodule ElixirSense.Core.MetadataBuilder.AliasTest do assert metadata_env = state.lines_to_env[env.line] assert metadata_env.aliases == env.aliases + # assert State.macro_env(state, metadata_env, env.line) == env end end From 3c391bc29e6a81839b5d2ac2833568f37915747b Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 23 Apr 2024 22:02:06 +0200 Subject: [PATCH 002/235] wip --- lib/elixir_sense/core/compiler.ex | 30 +++++++++++++++++------ lib/elixir_sense/core/metadata_builder.ex | 6 +++++ lib/elixir_sense/core/state.ex | 25 +++++++++++++++++++ 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 40174ac0..0546da8f 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1,5 +1,5 @@ defmodule ElixirSense.Core.Compiler do - import ElixirSense.Core.State + import ElixirSense.Core.State, except: [expand: 2, expand: 3, no_alias_expansion: 1] require Logger @env :elixir_env.new() @@ -10,7 +10,7 @@ defmodule ElixirSense.Core.Compiler do do_expand(ast, state, env) catch kind, payload -> - Logger.warning("Unable to expand ast node #{inspect(ast)}: #{Exception.format(kind, payload, __STACKTRACE__)}") + # Logger.warning("Unable to expand ast node #{inspect(ast)}: #{Exception.format(kind, payload, __STACKTRACE__)}") {ast, state, env} end end @@ -762,9 +762,15 @@ defmodule ElixirSense.Core.Compiler do {position, end_position} = extract_range(meta) + line = Keyword.fetch!(meta, :line) + state = state |> add_module_to_index(full, position, end_position, []) + |> add_current_env_to_line(line, %{env | module: full}) + |> add_module_functions(%{env | module: full}, [], position, end_position) + + dbg(state) {result, state, _env} = expand(block, state, %{env | module: full}) {result, state, env} @@ -791,8 +797,14 @@ defmodule ElixirSense.Core.Compiler do {{:__block__, [], []}, state, env} end + defp expand_macro(meta, Kernel, def_kind, [call], _callback, state, env) + when def_kind in [:defguard, :defguardp] do + # transform guard to def with empty body + expand_macro(meta, Kernel, def_kind, [call, {:__block__, [], []}], _callback, state, env) + end + defp expand_macro(meta, Kernel, def_kind, [call, expr], _callback, state, env) - when def_kind in [:def, :defp, :defmacro, :defmacrop] do + when def_kind in [:def, :defp, :defmacro, :defmacrop, :defguard, :defguardp] do dbg(call) dbg(expr) assert_no_match_or_guard_scope(env.context, :"{def_kind}/2") @@ -806,9 +818,9 @@ defmodule ElixirSense.Core.Compiler do # TODO store mod_fun_to_pos line = Keyword.fetch!(meta, :line) - state = - state - |> add_current_env_to_line(line, env) + # state = + # state + # |> add_current_env_to_line(line, env) state = %{state | vars: {%{}, false}, unused: {%{}, 0}} @@ -855,6 +867,7 @@ defmodule ElixirSense.Core.Compiler do state = state + |> add_current_env_to_line(line, %{g_env | context: nil, function: {name, arity}}) |> add_mod_fun_to_position( {module, name, arity}, position, @@ -884,13 +897,14 @@ defmodule ElixirSense.Core.Compiler do end defp expand_macro_callback(meta, module, fun, args, callback, state, env) do + dbg({module, fun, args}) try do callback.(meta, args) catch # TODO raise? # For language servers, if expanding the macro fails, we just give up. - _kind, payload -> - IO.inspect(payload, label: inspect(fun)) + kind, payload -> + # IO.inspect(payload, label: inspect(fun)) {{{:., meta, [module, fun]}, meta, args}, state, env} else ast -> expand(ast, state, env) diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index 1aa134d5..d335c728 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -39,6 +39,12 @@ defmodule ElixirSense.Core.MetadataBuilder do if Version.match?(System.version(), ">= 1.17.0-dev") do {_ast, state, _env} = Compiler.expand(ast, %State{}, Compiler.env()) state + |> remove_attributes_scope + |> remove_behaviours_scope + |> remove_lexical_scope + |> remove_vars_scope + |> remove_module + |> remove_protocol_implementation else # dbg(ast) {_ast, [state]} = diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index dd15e80b..7e974811 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -1931,4 +1931,29 @@ defmodule ElixirSense.Core.State do # TODO versioned_vars } end + + @module_functions [ + {:__info__, [:atom], :def}, + {:module_info, [], :def}, + {:module_info, [:atom], :def} + ] + + def add_module_functions(state, env, functions, position, end_position) do + {line, column} = position + (functions ++ @module_functions) + |> Enum.reduce(state, fn {name, args, kind}, acc -> + mapped_args = for arg <- args, do: {arg, [line: line, column: column], nil} + + acc + |> add_func_to_index( + env, + name, + mapped_args, + position, + end_position, + kind, + generated: true + ) + end) + end end From 24abad62340b7d2b7b6a73f9356ea9d84535f6dd Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 24 Apr 2024 19:28:33 +0200 Subject: [PATCH 003/235] behaviours --- lib/elixir_sense/core/compiler.ex | 21 +++++++++++++++- lib/elixir_sense/core/metadata_builder.ex | 1 - lib/elixir_sense/core/state.ex | 29 +++-------------------- 3 files changed, 23 insertions(+), 28 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 0546da8f..c546906d 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -10,7 +10,7 @@ defmodule ElixirSense.Core.Compiler do do_expand(ast, state, env) catch kind, payload -> - # Logger.warning("Unable to expand ast node #{inspect(ast)}: #{Exception.format(kind, payload, __STACKTRACE__)}") + Logger.warning("Unable to expand ast node #{inspect(ast)}: #{Exception.format(kind, payload, __STACKTRACE__)}") {ast, state, env} end end @@ -714,6 +714,25 @@ defmodule ElixirSense.Core.Compiler do ## Macro handling + defp expand_macro( + meta, + Kernel, + :@, + [{:behaviour, _meta, [arg]}], + _callback, + state, + env + ) do + line = Keyword.fetch!(meta, :line) + + state = + state + |> add_current_env_to_line(line) + + {arg, state, env} = expand(arg, state, env) + add_behaviour(arg, state, env) + end + defp expand_macro( meta, Kernel, diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index d335c728..0766fe5a 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -40,7 +40,6 @@ defmodule ElixirSense.Core.MetadataBuilder do {_ast, state, _env} = Compiler.expand(ast, %State{}, Compiler.env()) state |> remove_attributes_scope - |> remove_behaviours_scope |> remove_lexical_scope |> remove_vars_scope |> remove_module diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 7e974811..75b9b043 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -372,7 +372,7 @@ defmodule ElixirSense.Core.State do # current_vars = state |> get_current_vars() current_vars = state.vars |> elem(0) current_attributes = state |> get_current_attributes() - current_behaviours = hd(state.behaviours) + current_behaviours = state.behaviours |> Map.get(macro_env.module, []) current_scope_id = hd(state.scope_ids) current_scope_protocol = hd(state.protocols) @@ -1876,6 +1876,8 @@ defmodule ElixirSense.Core.State do generated: true ) end) + end + def macro_env(%__MODULE__{} = state, meta \\ []) do function = case hd(hd(state.scopes)) do @@ -1931,29 +1933,4 @@ defmodule ElixirSense.Core.State do # TODO versioned_vars } end - - @module_functions [ - {:__info__, [:atom], :def}, - {:module_info, [], :def}, - {:module_info, [:atom], :def} - ] - - def add_module_functions(state, env, functions, position, end_position) do - {line, column} = position - (functions ++ @module_functions) - |> Enum.reduce(state, fn {name, args, kind}, acc -> - mapped_args = for arg <- args, do: {arg, [line: line, column: column], nil} - - acc - |> add_func_to_index( - env, - name, - mapped_args, - position, - end_position, - kind, - generated: true - ) - end) - end end From 8db10bad35b141eff03b5234325905ced785bfe9 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 25 Apr 2024 15:41:49 +0200 Subject: [PATCH 004/235] defoverridable --- lib/elixir_sense/core/compiler.ex | 33 +++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index c546906d..422110c8 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1,6 +1,7 @@ defmodule ElixirSense.Core.Compiler do import ElixirSense.Core.State, except: [expand: 2, expand: 3, no_alias_expansion: 1] require Logger + alias ElixirSense.Core.Introspection @env :elixir_env.new() def env, do: @env @@ -733,6 +734,38 @@ defmodule ElixirSense.Core.Compiler do add_behaviour(arg, state, env) end + defp expand_macro( + meta, + Kernel, + :defoverridable, + [arg], + _callback, + state, + env + ) do + {arg, state, env} = expand(arg, state, env) + + case arg do + keyword when is_list(keyword) -> + {nil, make_overridable(state, env, keyword, meta[:context]), env} + + behaviour_module when is_atom(behaviour_module) -> + if Code.ensure_loaded?(behaviour_module) and + function_exported?(behaviour_module, :behaviour_info, 1) do + keyword = + behaviour_module.behaviour_info(:callbacks) + |> Enum.map(&Introspection.drop_macro_prefix/1) + + {nil, make_overridable(state, env, keyword, meta[:context]), env} + else + {nil, state, env} + end + + _ -> + {nil, state, env} + end + end + defp expand_macro( meta, Kernel, From 48a925f1b6ad0b12a6a39ac82a7f7c485cc42f94 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 25 Apr 2024 18:49:11 +0200 Subject: [PATCH 005/235] do not raise on unknown local --- lib/elixir_sense/core/compiler.ex | 14 ++++++++++++-- lib/elixir_sense/core/state.ex | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 422110c8..6e83f324 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1106,8 +1106,18 @@ defmodule ElixirSense.Core.Compiler do {{fun, meta, args}, state, env} end - defp expand_local(_meta, _fun, _args, _state, _env) do - raise "undefined_function" + defp expand_local(meta, fun, args, state, env) do + # elixir compiler raises here + # raise "undefined_function" + line = Keyword.fetch!(meta, :line) + + state = + state + |> add_current_env_to_line(line, env) + + {args, state, env} = expand_args(args, state, env) + + {{fun, meta, args}, state, env} end defp expand_opts(meta, kind, allowed, opts, s, e) do diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 75b9b043..1b9c1566 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -384,7 +384,7 @@ defmodule ElixirSense.Core.State do aliases: macro_env.aliases, module: macro_env.module, function: macro_env.function, - vars: current_vars, + versioned_vars: current_vars, attributes: current_attributes, behaviours: current_behaviours, typespec: nil, From fd4a4f3dee48745d527b89341a5970a39473d3db Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 26 Apr 2024 07:47:41 +0200 Subject: [PATCH 006/235] variables --- lib/elixir_sense/core/compiler.ex | 95 ++++++++++++++++++++++++------- lib/elixir_sense/core/state.ex | 11 +++- 2 files changed, 84 insertions(+), 22 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 6e83f324..34f19d26 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1,5 +1,6 @@ defmodule ElixirSense.Core.Compiler do import ElixirSense.Core.State, except: [expand: 2, expand: 3, no_alias_expansion: 1] + alias ElixirSense.Core.State require Logger alias ElixirSense.Core.Introspection @@ -16,13 +17,15 @@ defmodule ElixirSense.Core.Compiler do end end - ## =/2 + # =/2 defp do_expand({:=, meta, [left, right]}, s, e) do assert_no_guard_scope(e.context, "=/2") {e_right, sr, er} = expand(right, s, e) + # dbg(sr) # dbg(e_right) {e_left, sl, el} = __MODULE__.Clauses.match(&expand/3, left, sr, s, er) + # IO.inspect(sl.scope_vars_info, label: "left") # dbg(e_left) # dbg(el.versioned_vars) # dbg(sl.vars) @@ -59,7 +62,7 @@ defmodule ElixirSense.Core.Compiler do defp do_expand({:|, _meta, [_, _]}, _s, _e), do: raise("unhandled_cons_op") - ## __block__ + # __block__ defp do_expand({:__block__, _meta, []}, s, e), do: {nil, s, e} @@ -73,7 +76,7 @@ defmodule ElixirSense.Core.Compiler do {{:__block__, meta, e_args}, sa, ea} end - ## __aliases__ + # __aliases__ defp do_expand({:__aliases__, meta, [head | tail] = list}, state, env) do case Macro.Env.expand_alias(env, meta, list, trace: false) do @@ -97,7 +100,7 @@ defmodule ElixirSense.Core.Compiler do end end - ## require, alias, import + # require, alias, import defp do_expand({form, meta, [{{:., _, [base, :{}]}, _, refs} | rest]}, state, env) when form in [:require, :alias, :import] do @@ -441,14 +444,17 @@ defmodule ElixirSense.Core.Compiler do # Vars - ## Pin operator + # Pin operator # It only appears inside match and it disables the match behaviour. defp do_expand({:^, meta, [arg]}, %{prematch: {prematch, _, _}, vars: {_, write}} = s, e) do no_match_s = %{s | prematch: :pin, vars: {prematch, write}} case expand(arg, no_match_s, %{e | context: nil}) do - {{name, _, kind} = var, %{unused: unused}, _} when is_atom(name) and is_atom(kind) -> + {{name, var_meta, kind} = var, %{unused: unused}, _} when is_atom(name) and is_atom(kind) -> + line = var_meta[:line] + column = var_meta[:column] + s = if kind == nil, do: add_var(s, %State.VarInfo{name: name, is_definition: false, positions: [{line, column}]}, false), else: s {{:^, meta, [var]}, %{s | unused: unused}, e} _ -> @@ -481,12 +487,16 @@ defmodule ElixirSense.Core.Compiler do pair = {name, var_context(meta, kind)} - case read do + case read |> dbg do # Variable was already overridden %{^pair => var_version} when var_version >= prematch_version -> + IO.puts "Variable was already overridden" # maybe_warn_underscored_var_repeat(meta, name, kind, e) new_unused = var_used(meta, pair, var_version, unused) var = {name, [{:version, var_version} | meta], kind} + line = meta[:line] + column = meta[:column] + s = if kind == nil, do: add_var(s, %State.VarInfo{name: name, is_definition: true, positions: [{line, column}]}, true), else: s {var, %{s | unused: {new_unused, version}}, e} # Variable is being overridden now @@ -495,6 +505,9 @@ defmodule ElixirSense.Core.Compiler do new_read = Map.put(read, pair, version) new_write = if write != false, do: Map.put(write, pair, version), else: write var = {name, [{:version, version} | meta], kind} + line = meta[:line] + column = meta[:column] + s = if kind == nil, do: add_var(s, %State.VarInfo{name: name, is_definition: true, positions: [{line, column}]}, true), else: s {var, %{s | vars: {new_read, new_write}, unused: {new_unused, version + 1}}, e} # Variable defined for the first time @@ -503,6 +516,9 @@ defmodule ElixirSense.Core.Compiler do new_read = Map.put(read, pair, version) new_write = if write != false, do: Map.put(write, pair, version), else: write var = {name, [{:version, version} | meta], kind} + line = meta[:line] + column = meta[:column] + s = if kind == nil, do: add_var(s, %State.VarInfo{name: name, is_definition: true, positions: [{line, column}]}, true), else: s {var, %{s | vars: {new_read, new_write}, unused: {new_unused, version + 1}}, e} end end @@ -541,33 +557,41 @@ defmodule ElixirSense.Core.Compiler do prematch end - case result do + case result |> dbg do {:ok, pair_version} -> # maybe_warn_underscored_var_access(meta, name, kind, e) var = {name, [{:version, pair_version} | meta], kind} + line = meta[:line] + column = meta[:column] + s = if kind == nil, do: add_var(s, %State.VarInfo{name: name, is_definition: false, positions: [{line, column}]}, false), else: s {var, %{s | unused: {var_used(meta, pair, pair_version, unused), version}}, e} error -> case Keyword.fetch(meta, :if_undefined) do {:ok, :apply} -> + # TODO check if this can happen expand({name, meta, []}, s, e) # TODO: Remove this clause on v2.0 as we will raise by default {:ok, :raise} -> + # TODO is it worth registering var access # function_error(meta, e, __MODULE__, {:undefined_var, name, kind}) {{name, meta, kind}, s, e} # TODO: Remove this clause on v2.0 as we will no longer support warn _ when error == :warn -> + # TODO is it worth registering var access # Warn about undefined var to call # elixir_errors:file_warn(Meta, E, ?MODULE, {undefined_var_to_call, Name}), expand({name, [{:if_undefined, :warn} | meta], []}, s, e) _ when error == :pin -> + # TODO is it worth registering var access # function_error(meta, e, __MODULE__, {:undefined_var_pin, name, kind}) {{name, meta, kind}, s, e} _ -> + # TODO is it worth registering var access span_meta = __MODULE__.Env.calculate_span(meta, name) # function_error(span_meta, e, __MODULE__, {:undefined_var, name, kind}) {{name, span_meta, kind}, s, e} @@ -605,11 +629,12 @@ defmodule ElixirSense.Core.Compiler do end end - ## Remote call + # Remote call defp do_expand({{:., dot_meta, [module, fun]}, meta, args}, state, env) when (is_tuple(module) or is_atom(module)) and is_atom(fun) and is_list(meta) and is_list(args) do + dbg({module, fun, args}) {module, state_l, env} = expand(module, __MODULE__.Env.prepare_write(state), env) arity = length(args) @@ -713,7 +738,7 @@ defmodule ElixirSense.Core.Compiler do raise "invalid_quoted_expr #{inspect(other)}" end - ## Macro handling + # Macro handling defp expand_macro( meta, @@ -821,6 +846,8 @@ defmodule ElixirSense.Core.Compiler do |> add_module_to_index(full, position, end_position, []) |> add_current_env_to_line(line, %{env | module: full}) |> add_module_functions(%{env | module: full}, [], position, end_position) + |> new_vars_scope + # TODO magic with ElixirEnv instead of new_vars_scope? dbg(state) @@ -842,6 +869,7 @@ defmodule ElixirSense.Core.Compiler do # restore vars from outer scope state = %{state | vars: vars, unused: unused} + |> remove_vars_scope # TODO hardcode expansion? # to result of require (a module atom) and :elixir_module.compile dot call in block @@ -875,6 +903,7 @@ defmodule ElixirSense.Core.Compiler do # |> add_current_env_to_line(line, env) state = %{state | vars: {%{}, false}, unused: {%{}, 0}} + |> new_func_vars_scope {name_and_args, guards} = __MODULE__.Utils.extract_guards(call) @@ -939,6 +968,7 @@ defmodule ElixirSense.Core.Compiler do # restore vars from outer scope state = %{state | vars: vars, unused: unused} + |> remove_func_vars_scope # result of def expansion is fa tuple {{name, arity}, state, env} @@ -999,7 +1029,7 @@ defmodule ElixirSense.Core.Compiler do end end - ## defmodule helpers + # defmodule helpers # defmodule automatically defines aliases, we need to mirror this feature here. # defmodule Elixir.Alias @@ -1027,7 +1057,7 @@ defmodule ElixirSense.Core.Compiler do {module, env} end - ## Helpers + # Helpers defp expand_remote(receiver, dot_meta, right, meta, args, s, sl, %{context: context} = e) when is_atom(receiver) or is_tuple(receiver) do @@ -1051,7 +1081,9 @@ defmodule ElixirSense.Core.Compiler do case rewrite(context, receiver, dot_meta, right, attached_meta, e_args, s) do {:ok, rewritten} -> - {rewritten, __MODULE__.Env.close_write(sa, s), ea} + s = __MODULE__.Env.close_write(sa, s) + |> add_current_env_to_line(line, e) + {rewritten, s, ea} {:error, _error} -> raise "elixir_rewrite" @@ -1707,7 +1739,7 @@ defmodule ElixirSense.Core.Compiler do alias ElixirSense.Core.Compiler.Utils, as: ElixirUtils def reset_unused_vars(%{unused: {_unused, version}} = s) do - %{s | unused: {%{}, version}} + %{s | unused: {%{}, version}} |> new_vars_scope end def reset_read(%{vars: {_, write}} = s, %{vars: {read, _}}) do @@ -1718,11 +1750,11 @@ defmodule ElixirSense.Core.Compiler do %{s | vars: {read, read}} end - def close_write(%{vars: {_read, write}} = s, %{vars: {_, false}}) do + def close_write(%{vars: {_read, write}} = s, %{vars: {_, false}} = s1) do %{s | vars: {write, false}} end - def close_write(%{vars: {_read, write}} = s, %{vars: {_, upper_write}}) do + def close_write(%{vars: {_read, write}} = s, %{vars: {_, upper_write}} = s1) do %{s | vars: {write, merge_vars(upper_write, write)}} end @@ -1741,10 +1773,10 @@ defmodule ElixirSense.Core.Compiler do ) end - def merge_and_check_unused_vars(s, %{vars: {read, write}, unused: {unused, _version}}, e) do + def merge_and_check_unused_vars(s, s1 = %{vars: {read, write}, unused: {unused, _version}}, e) do %{unused: {clause_unused, version}} = s new_unused = merge_and_check_unused_vars(read, unused, clause_unused, e) - %{s | unused: {new_unused, version}, vars: {read, write}} + %{s | unused: {new_unused, version}, vars: {read, write}} |> remove_vars_scope end def merge_and_check_unused_vars(current, unused, clause_unused, _e) do @@ -1838,9 +1870,29 @@ defmodule ElixirSense.Core.Compiler do call_s = %{before_s | prematch: {read, unused, :none}, unused: unused_tuple, vars: current} call_e = Map.put(e, :context, :match) - {e_expr, %{vars: new_current, unused: new_unused}, ee} = fun.(expr, call_s, call_e) + {e_expr, %{vars: new_current, unused: new_unused} = s_expr, ee} = fun.(expr, call_s, call_e) + + # TODO elixir does it like that, is it a bug? we lose state + # end_s = %{after_s | prematch: prematch, unused: new_unused, vars: new_current} + end_s = %{s_expr | prematch: prematch, unused: new_unused, vars: new_current} + + # TODO I'm not sure this is correct + merged_vars = (hd(end_s.scope_vars_info) ++ hd(after_s.scope_vars_info)) + |> merge_same_name_vars() + # |> dbg + + end_s = %{end_s + | scope_vars_info: [merged_vars | tl(end_s.scope_vars_info)], + lines_to_env: Map.merge(after_s.lines_to_env, end_s.lines_to_env) + } - end_s = %{after_s | prematch: prematch, unused: new_unused, vars: new_current} + dbg(Map.keys(end_s.lines_to_env)) + dbg(Map.keys(after_s.lines_to_env)) + dbg(Map.keys(before_s.lines_to_env)) + dbg(Map.keys(before_s.lines_to_env)) + # dbg(before_s.scope_vars_info) + # dbg(after_s.scope_vars_info) + # dbg(end_s.scope_vars_info) end_e = Map.put(ee, :context, Map.get(e, :context)) {e_expr, end_s, end_e} @@ -1852,8 +1904,11 @@ defmodule ElixirSense.Core.Compiler do end def clause(_meta, _kind, fun, {:->, meta, [left, right]}, s, e) do + # TODO do it in ElixirEnv? + # s = s |> new_vars_scope {e_left, sl, el} = fun.(left, s, e) {e_right, sr, er} = ElixirExpand.expand(right, sl, el) + # sr = sr |> remove_vars_scope {{:->, meta, [e_left, e_right]}, sr, er} end diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 1b9c1566..6c037329 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -369,7 +369,6 @@ defmodule ElixirSense.Core.State do end def get_current_env(%__MODULE__{} = state, macro_env) do - # current_vars = state |> get_current_vars() current_vars = state.vars |> elem(0) current_attributes = state |> get_current_attributes() current_behaviours = state.behaviours |> Map.get(macro_env.module, []) @@ -377,6 +376,13 @@ defmodule ElixirSense.Core.State do current_scope_id = hd(state.scope_ids) current_scope_protocol = hd(state.protocols) + # Macro.Env versioned_vars is not updated + # versioned_vars: macro_env.versioned_vars, + versioned_vars = + state.vars + |> elem(0) + |> Map.new() + %Env{ functions: macro_env.functions, macros: macro_env.macros, @@ -384,7 +390,8 @@ defmodule ElixirSense.Core.State do aliases: macro_env.aliases, module: macro_env.module, function: macro_env.function, - versioned_vars: current_vars, + vars: state |> get_current_vars(), + versioned_vars: versioned_vars, attributes: current_attributes, behaviours: current_behaviours, typespec: nil, From 2e460730c0a87ecc6530f769dda6f61415d10fb9 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 26 Apr 2024 16:41:56 +0200 Subject: [PATCH 007/235] variables --- lib/elixir_sense/core/compiler.ex | 97 +++++++++++++++++------- lib/elixir_sense/core/state.ex | 15 +++- test/elixir_sense/core/compiler_test.exs | 12 +++ 3 files changed, 95 insertions(+), 29 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 34f19d26..6468cbdd 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -485,12 +485,11 @@ defmodule ElixirSense.Core.Compiler do vars: {read, write} } = s - pair = {name, var_context(meta, kind)} + pair = {name, var_context(meta, kind)} |> dbg case read |> dbg do # Variable was already overridden %{^pair => var_version} when var_version >= prematch_version -> - IO.puts "Variable was already overridden" # maybe_warn_underscored_var_repeat(meta, name, kind, e) new_unused = var_used(meta, pair, var_version, unused) var = {name, [{:version, var_version} | meta], kind} @@ -634,7 +633,7 @@ defmodule ElixirSense.Core.Compiler do defp do_expand({{:., dot_meta, [module, fun]}, meta, args}, state, env) when (is_tuple(module) or is_atom(module)) and is_atom(fun) and is_list(meta) and is_list(args) do - dbg({module, fun, args}) + # dbg({module, fun, args}) {module, state_l, env} = expand(module, __MODULE__.Env.prepare_write(state), env) arity = length(args) @@ -849,8 +848,6 @@ defmodule ElixirSense.Core.Compiler do |> new_vars_scope # TODO magic with ElixirEnv instead of new_vars_scope? - dbg(state) - {result, state, _env} = expand(block, state, %{env | module: full}) {result, state, env} else @@ -869,6 +866,7 @@ defmodule ElixirSense.Core.Compiler do # restore vars from outer scope state = %{state | vars: vars, unused: unused} + |> maybe_move_vars_to_outer_scope |> remove_vars_scope # TODO hardcode expansion? @@ -963,11 +961,15 @@ defmodule ElixirSense.Core.Compiler do # expand_macro_callback(meta, Kernel, def_kind, [call, expr], callback, state, env) # %{state | prematch: :warn} + # TODO not sure vars scope is needed + state = state |> new_vars_scope {_e_body, state, _env} = expand(expr, state, %{g_env | context: nil, function: {name, arity}}) # restore vars from outer scope + # TODO maybe_move_vars_to_outer_scope? state = %{state | vars: vars, unused: unused} + |> remove_vars_scope |> remove_func_vars_scope # result of def expansion is fa tuple @@ -989,7 +991,9 @@ defmodule ElixirSense.Core.Compiler do # IO.inspect(payload, label: inspect(fun)) {{{:., meta, [module, fun]}, meta, args}, state, env} else - ast -> expand(ast, state, env) + ast -> + {ast, state, env} = expand(ast, state, env) + {ast, state, env} end end @@ -1063,11 +1067,14 @@ defmodule ElixirSense.Core.Compiler do when is_atom(receiver) or is_tuple(receiver) do assert_no_clauses(right, meta, args, e) - line = Keyword.fetch!(meta, :line) + line = Keyword.get(meta, :line, 0) # TODO register call - sl = + sl = if line > 0 do sl |> add_current_env_to_line(line, e) + else + sl + end if context == :guard and is_tuple(receiver) do if Keyword.get(meta, :no_parens) != true do @@ -1197,7 +1204,10 @@ defmodule ElixirSense.Core.Compiler do defp expand_block([{:for, _, [_ | _]} = h | t], acc, meta, s, e) do {eh, se, ee} = expand_for(h, s, e, false) - expand_block(t, [eh | acc], meta, se, ee) + dbg(se.scope_vars_info) + dbg(se.vars_info_per_scope_id) + {eh, se, ee} = expand_block(t, [eh | acc], meta, se, ee) + {eh, se, ee} end defp expand_block([{:=, _, [{:_, _, ctx}, {:for, _, [_ | _]} = h]} | t], acc, meta, s, e) @@ -1285,7 +1295,7 @@ defmodule ElixirSense.Core.Compiler do end defp expand_fn_capture(meta, arg, s, e) do - case __MODULE__.Fn.capture(meta, arg, s, e) |> dbg do + case __MODULE__.Fn.capture(meta, arg, s, e) do {{:remote, remote, fun, arity}, require_meta, dot_meta, se, ee} -> # if is_atom(remote) do # ElixirEnv.trace({:remote_function, require_meta, remote, fun, arity}, e) @@ -1324,7 +1334,13 @@ defmodule ElixirSense.Core.Compiler do {{e_expr, se, ee}, normalized_opts} = case validate_for_options(e_opts, false, false, false, return, meta, e, []) do {:ok, maybe_reduce, nopts} -> - {expand_for_do_block(meta, expr, sc, ec, maybe_reduce), nopts} + # TODO not sure new vars scope is actually needed + sc = sc |> new_vars_scope + {ed, sd, envd} = expand_for_do_block(meta, expr, sc, ec, maybe_reduce) + sd = sd + |> maybe_move_vars_to_outer_scope + |> remove_vars_scope + {{ed, sd, envd}, nopts} {:error, _error} -> # {file_error(meta, e, __MODULE__, error), e_opts} @@ -1335,7 +1351,7 @@ defmodule ElixirSense.Core.Compiler do __MODULE__.Env.merge_and_check_unused_vars(se, s, ee), e} end - defp expand_for_do_block(Meta, [{:->, _, _} | _], _S, E, false), + defp expand_for_do_block(_meta, [{:->, _, _} | _], _s, _e, false), do: raise("for_without_reduce_bad_block") defp expand_for_do_block(_meta, expr, s, e, false), do: expand(expr, s, e) @@ -1392,7 +1408,13 @@ defmodule ElixirSense.Core.Compiler do end end - defp expand_for_generator(x, s, e), do: expand(x, s, e) + defp expand_for_generator(x, s, e) do + dbg(s.scope_vars_info) + dbg(x) + {x, s, e} = expand(x, s, e) + dbg(s.scope_vars_info) + {x, s, e} + end defp assert_generator_start(_, [{:<-, _, [_, _]} | _], _), do: :ok defp assert_generator_start(_, [{:<<>>, _, [{:<-, _, [_, _]}]} | _], _), do: :ok @@ -1776,7 +1798,15 @@ defmodule ElixirSense.Core.Compiler do def merge_and_check_unused_vars(s, s1 = %{vars: {read, write}, unused: {unused, _version}}, e) do %{unused: {clause_unused, version}} = s new_unused = merge_and_check_unused_vars(read, unused, clause_unused, e) - %{s | unused: {new_unused, version}, vars: {read, write}} |> remove_vars_scope + # dbg(s.scope_vars_info) + # dbg({read, write}) + s = %{s | unused: {new_unused, version}, vars: {read, write}} + |> maybe_move_vars_to_outer_scope + |> remove_vars_scope + + # dbg(s.scope_vars_info) + # dbg(s.vars_info_per_scope_id) + s end def merge_and_check_unused_vars(current, unused, clause_unused, _e) do @@ -1876,20 +1906,29 @@ defmodule ElixirSense.Core.Compiler do # end_s = %{after_s | prematch: prematch, unused: new_unused, vars: new_current} end_s = %{s_expr | prematch: prematch, unused: new_unused, vars: new_current} + dbg(hd(before_s.scope_vars_info)) + dbg(hd(after_s.scope_vars_info)) + dbg(hd(end_s.scope_vars_info)) + + dbg(current) + dbg(read) + dbg(new_current) + # TODO I'm not sure this is correct - merged_vars = (hd(end_s.scope_vars_info) ++ hd(after_s.scope_vars_info)) + merged_vars = (hd(end_s.scope_vars_info) -- hd(after_s.scope_vars_info)) |> merge_same_name_vars() - # |> dbg + + merged_vars = merged_vars ++ hd(after_s.scope_vars_info) - end_s = %{end_s - | scope_vars_info: [merged_vars | tl(end_s.scope_vars_info)], + end_s = %{end_s | + scope_vars_info: [merged_vars | tl(end_s.scope_vars_info)], lines_to_env: Map.merge(after_s.lines_to_env, end_s.lines_to_env) } - dbg(Map.keys(end_s.lines_to_env)) - dbg(Map.keys(after_s.lines_to_env)) - dbg(Map.keys(before_s.lines_to_env)) - dbg(Map.keys(before_s.lines_to_env)) + # dbg(Map.keys(end_s.lines_to_env)) + # dbg(Map.keys(after_s.lines_to_env)) + # dbg(Map.keys(before_s.lines_to_env)) + # dbg(Map.keys(before_s.lines_to_env)) # dbg(before_s.scope_vars_info) # dbg(after_s.scope_vars_info) # dbg(end_s.scope_vars_info) @@ -1904,11 +1943,8 @@ defmodule ElixirSense.Core.Compiler do end def clause(_meta, _kind, fun, {:->, meta, [left, right]}, s, e) do - # TODO do it in ElixirEnv? - # s = s |> new_vars_scope {e_left, sl, el} = fun.(left, s, e) {e_right, sr, er} = ElixirExpand.expand(right, sl, el) - # sr = sr |> remove_vars_scope {{:->, meta, [e_left, e_right]}, sr, er} end @@ -2114,7 +2150,12 @@ defmodule ElixirSense.Core.Compiler do raise "missing_option" {expr, rest_opts} -> + # TODO not sure new vars scope is needed + acc = acc |> new_vars_scope {e_expr, s_acc, e_acc} = ElixirExpand.expand(expr, acc, e) + s_acc = s_acc + |> maybe_move_vars_to_outer_scope + |> remove_vars_scope {e_expr, rest_opts, ElixirEnv.merge_and_check_unused_vars(s_acc, s, e_acc)} end end @@ -3019,11 +3060,11 @@ defmodule ElixirSense.Core.Compiler do defp validate(_meta, [], _pos, _e), do: [] - defp escape({:&, _, [pos]}, _e, dict) when is_integer(pos) and pos > 0 do + defp escape({:&, meta, [pos]}, _e, dict) when is_integer(pos) and pos > 0 do # Using a nil context here to emit warnings when variable is unused. # This might pollute user space but is unlikely because variables # named :"&1" are not valid syntax. - var = {:"&#{pos}", [], nil} + var = {:"&#{pos}", meta, nil} {var, :orddict.store(pos, var, dict)} end @@ -3387,7 +3428,7 @@ defmodule ElixirSense.Core.Compiler do buffer, acc ) do - runtime = do_runtime_list(meta, :list, [expr, do_list_concat(buffer, acc)]) + runtime = do_runtime_list(meta, :list, [expr, do_list_concat(buffer, acc)]) |> dbg do_quote_splice(t, q, e, [], runtime) end diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 6c037329..0af240bf 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -383,6 +383,16 @@ defmodule ElixirSense.Core.State do |> elem(0) |> Map.new() + # TODO this is a hack that hides a problem somewhere + vars = state + |> get_current_vars() + |> Enum.filter(& Map.has_key?(elem(state.vars, 0), {&1.name, nil})) + + dbg(vars) + dbg(state.vars) + dbg(state.scope_vars_info) + + %Env{ functions: macro_env.functions, macros: macro_env.macros, @@ -390,7 +400,7 @@ defmodule ElixirSense.Core.State do aliases: macro_env.aliases, module: macro_env.module, function: macro_env.function, - vars: state |> get_current_vars(), + vars: vars, versioned_vars: versioned_vars, attributes: current_attributes, behaviours: current_behaviours, @@ -945,6 +955,7 @@ defmodule ElixirSense.Core.State do |> List.flatten() |> reduce_vars(current_scope_reduced_vars, false) |> Enum.flat_map(fn {_var, scopes} -> scopes end) + # |> dbg Map.put(state.vars_info_per_scope_id, scope_id, vars_info) end @@ -1253,6 +1264,7 @@ defmodule ElixirSense.Core.State do %VarInfo{name: var_name} = var_info, is_definition ) do + dbg(var_info) scope = hd(state.scopes) [vars_from_scope | other_vars] = state.vars_info is_var_defined = is_variable_defined(state, var_name) @@ -1282,6 +1294,7 @@ defmodule ElixirSense.Core.State do _ -> vars_from_scope end + |> dbg %__MODULE__{ state diff --git a/test/elixir_sense/core/compiler_test.exs b/test/elixir_sense/core/compiler_test.exs index 8019a774..13205d5a 100644 --- a/test/elixir_sense/core/compiler_test.exs +++ b/test/elixir_sense/core/compiler_test.exs @@ -293,6 +293,18 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do """) end + test "expands quote unquote_splicing in list" do + assert_expansion(""" + a = [1, 2, 3] + quote do: [unquote_splicing(a) | [1]] + """) + + assert_expansion(""" + a = [1, 2, 3] + quote do: [1 | unquote_splicing(a)] + """) + end + test "expands quote alias" do assert_expansion("quote do: Date") assert_expansion("quote do: Elixir.Date") From 7460210d7931512184d303d31ca2ac0e357305ea Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 26 Apr 2024 23:16:52 +0200 Subject: [PATCH 008/235] more tests super resolve --- lib/elixir_sense/core/compiler.ex | 65 ++++++++++--------- test/elixir_sense/core/compiler_test.exs | 37 +++++++++++ .../core/metadata_builder_test.exs | 24 ++++++- 3 files changed, 91 insertions(+), 35 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 6468cbdd..70fb493c 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -368,7 +368,7 @@ defmodule ElixirSense.Core.Compiler do defp do_expand({:&, meta, [{:super, super_meta, args} = expr]}, s, e) when is_list(args) do assert_no_match_or_guard_scope(e.context, "&") - case resolve_super(meta, length(args), e) do + case resolve_super(meta, length(args), s, e) do {kind, name, _} when kind in [:def, :defp] -> expand_fn_capture(meta, {name, super_meta, args}, s, e) @@ -381,7 +381,7 @@ defmodule ElixirSense.Core.Compiler do when is_atom(context) and is_integer(arity) do assert_no_match_or_guard_scope(e.context, "&") - case resolve_super(meta, arity, e) do + case resolve_super(meta, arity, s, e) do {kind, name, _} when kind in [:def, :defp] -> {{:&, meta, [{:/, arity_meta, [{name, super_meta, context}, arity]}]}, s, e} @@ -437,7 +437,7 @@ defmodule ElixirSense.Core.Compiler do defp do_expand({:super, meta, args}, s, e) when is_list(args) do assert_no_match_or_guard_scope(e.context, "super") - {kind, name, _} = resolve_super(meta, length(args), e) + {kind, name, _} = resolve_super(meta, length(args), s, e) {e_args, sa, ea} = expand_args(args, s, e) {{:super, [{:super, {kind, name}} | meta], e_args}, sa, ea} end @@ -1204,8 +1204,6 @@ defmodule ElixirSense.Core.Compiler do defp expand_block([{:for, _, [_ | _]} = h | t], acc, meta, s, e) do {eh, se, ee} = expand_for(h, s, e, false) - dbg(se.scope_vars_info) - dbg(se.vars_info_per_scope_id) {eh, se, ee} = expand_block(t, [eh | acc], meta, se, ee) {eh, se, ee} end @@ -1262,33 +1260,39 @@ defmodule ElixirSense.Core.Compiler do map_fold(fun, state, env, refs) end - defp resolve_super(_meta, arity, e) do - _module = assert_module_scope(e) + defp overridable_name(name, count) when is_integer(count), do: :"#{name} (overridable #{count})" + + defp resolve_super(_meta, arity, state, e) do + module = assert_module_scope(e) function = assert_function_scope(e) case function do {name, ^arity} -> - {:def, name, []} - - # {kind, name, super_meta} = ElixirOverridable.super(meta, module, function, e) - - # TODO actual resolve - # {{{def, {Name, Arity}}, Kind, Meta, File, _Check, {Defaults, _HasBody, _LastDefaults}}, Clauses} = Def, - # {FinalKind, FinalName, FinalArity, FinalClauses} = - # case Hidden of - # false -> - # {Kind, Name, Arity, Clauses}; - # true when Kind == defmacro; Kind == defmacrop -> - # {defmacrop, name(Name, Count), Arity, Clauses}; - # true -> - # {defp, name(Name, Count), Arity, Clauses} - # end, - # name(Name, Count) when is_integer(Count) -> - # list_to_atom(atom_to_list(Name) ++ " (overridable " ++ integer_to_list(Count) ++ ")"). - - # {kind, name, super_meta} = ElixirOverridable.super(meta, module, function, e) - # maybe_warn_deprecated_super_in_gen_server_callback(meta, function, super_meta, e) - # {kind, name, super_meta} + state.mods_funs_to_positions |> dbg + + case state.mods_funs_to_positions[{module, name, arity} |> dbg] do + %State.ModFunInfo{overridable: {true, _}} = info -> + kind = case info.type do + :defdelegate -> :def + :defguard -> :defmacro + :defguardp -> :defmacrop + other -> other + end + hidden = Map.get(info.meta |> dbg, :hidden, false) + # def meta is not used anyway so let's pass empty + meta = [] + # TODO count 1 hardcoded but that's probably OK + count = 1 + case hidden do + false -> + {kind, name, meta} + true when kind in [:defmacro, :defmacrop] -> + {:defmacrop, overridable_name(name, count), meta} + true -> + {:defp, overridable_name(name, count), meta} + end + nil -> raise "no_super" + end _ -> raise "wrong_number_of_args_for_super" end @@ -1409,10 +1413,7 @@ defmodule ElixirSense.Core.Compiler do end defp expand_for_generator(x, s, e) do - dbg(s.scope_vars_info) - dbg(x) {x, s, e} = expand(x, s, e) - dbg(s.scope_vars_info) {x, s, e} end @@ -1711,7 +1712,7 @@ defmodule ElixirSense.Core.Compiler do end end - # TODO probable we can remove it/hardcode, used only for generating error message + # TODO probably we can remove it/hardcode, used only for generating error message defp guard_context(%{prematch: {_, _, {:bitsize, _}}}), do: "bitstring size specifier" defp guard_context(_), do: "guard" diff --git a/test/elixir_sense/core/compiler_test.exs b/test/elixir_sense/core/compiler_test.exs index 13205d5a..1cad90ef 100644 --- a/test/elixir_sense/core/compiler_test.exs +++ b/test/elixir_sense/core/compiler_test.exs @@ -121,6 +121,11 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do assert_expansion("<> = \"\"") end + test "expands <<>> with modifier" do + assert_expansion("x = 1; y = 1; <>") + assert_expansion("x = 1; y = 1; <> = <<>>") + end + test "expands __block__" do assert_expansion({:__block__, [], []}, true) assert_expansion({:__block__, [], [1]}, true) @@ -419,6 +424,38 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do """) end + test "expands for with bitstring generator" do + assert_expansion(""" + for <> do + :ok + end + """) + end + + test "expands for with reduce" do + assert_expansion(""" + for <>, x in ?a..?z, reduce: %{} do + acc -> acc + end + """) + end + + test "expands for in block" do + assert_expansion(""" + for i <- [1, 2, 3] do + i + end + :ok + """) + + assert_expansion(""" + _ = for i <- [1, 2, 3] do + i + end + :ok + """) + end + test "expands with" do assert_expansion(""" with i <- :ok do diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 98d6af65..f42276b0 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -216,6 +216,25 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] end + test "in bitstring modifier" do + state = + """ + y = 1 + <<1::size(y)>> + <<1::size(y)>> = <<>> + record_env() + """ + |> string_to_state + + assert Map.keys(state.lines_to_env[4].versioned_vars) == [ + {:y, nil} + ] + + assert [ + %VarInfo{name: :y, positions: [{1, 1}, {2, 11}, {3, 11}]} + ] = state |> get_line_vars(4) + end + test "undefined usage" do state = """ @@ -781,11 +800,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert Map.keys(state.lines_to_env[5].versioned_vars) == [{:y, nil}, {:z, nil}] # TODO sort? - # |> Enum.sort_by(& &1.name) assert [ %VarInfo{name: :y, positions: [{4, 3}]}, %VarInfo{name: :z, positions: [{4, 6}, {4, 24}]} - ] = state |> get_line_vars(5) + ] = state |> get_line_vars(5) |> Enum.sort_by(& &1.name) assert Map.keys(state.lines_to_env[7].versioned_vars) == [{:a, nil}] @@ -818,7 +836,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] = state |> get_line_vars(5) end - test "fn usage inclosure" do + test "fn usage in closure" do state = """ abc = 5 From 6e50e3df0bd04efa4d6e48adead9e973c9a119c4 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 28 Apr 2024 10:47:24 +0200 Subject: [PATCH 009/235] module attribute --- lib/elixir_sense/core/compiler.ex | 43 ++++++++++++ lib/elixir_sense/core/metadata_builder.ex | 13 ++-- .../core/metadata_builder_test.exs | 66 +++++++++++++++++-- 3 files changed, 107 insertions(+), 15 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 70fb493c..09b2f33c 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -748,6 +748,8 @@ defmodule ElixirSense.Core.Compiler do state, env ) do + assert_module_scope(env, :@, 1) + unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") line = Keyword.fetch!(meta, :line) state = @@ -758,6 +760,44 @@ defmodule ElixirSense.Core.Compiler do add_behaviour(arg, state, env) end + defp expand_macro( + meta, + Kernel, + :@, + [{name, _meta, args}], + _callback, + state, + env + ) when is_atom(name) do + assert_module_scope(env, :@, 1) + unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") + line = Keyword.fetch!(meta, :line) + column = Keyword.get(meta, :column, 1) + + {is_definition, {e_args, state, env}} = case args do + arg when is_atom(arg) -> + # @attribute + {false, {nil, state, env}} + [] -> + # deprecated @attribute() + {false, {nil, state, env}} + [_] -> + # @attribute(arg) + if env.function, do: raise "cannot set attribute @#{name} inside function/macro" + if name == :behavior, do: raise "@behavior attribute is not supported" + {true, expand_args(args, state, env)} + _ -> raise "invalid @ call" + end + + state = + state + |> add_attribute(name, nil, is_definition, {line, column}) + |> add_current_env_to_line(line) + + + {e_args, state, env} + end + defp expand_macro( meta, Kernel, @@ -767,6 +807,7 @@ defmodule ElixirSense.Core.Compiler do state, env ) do + assert_module_scope(env, :defoverridable, 1) {arg, state, env} = expand(arg, state, env) case arg do @@ -846,6 +887,7 @@ defmodule ElixirSense.Core.Compiler do |> add_current_env_to_line(line, %{env | module: full}) |> add_module_functions(%{env | module: full}, [], position, end_position) |> new_vars_scope + |> new_attributes_scope # TODO magic with ElixirEnv instead of new_vars_scope? {result, state, _env} = expand(block, state, %{env | module: full}) @@ -868,6 +910,7 @@ defmodule ElixirSense.Core.Compiler do state = %{state | vars: vars, unused: unused} |> maybe_move_vars_to_outer_scope |> remove_vars_scope + |> remove_attributes_scope # TODO hardcode expansion? # to result of require (a module atom) and :elixir_module.compile dot call in block diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index 0766fe5a..ec51c3ea 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -440,13 +440,6 @@ defmodule ElixirSense.Core.MetadataBuilder do |> result(ast) end - defp pre_module_attribute(ast, state, {line, _} = position, name, type, is_definition) do - state - |> add_attribute(name, type, is_definition, position) - |> add_current_env_to_line(line) - |> result(ast) - end - defp pre_type(ast, state, meta, type_name, type_args, spec, kind) do spec = TypeInfo.typespec_to_string(kind, spec) @@ -949,7 +942,11 @@ defmodule ElixirSense.Core.MetadataBuilder do ) new_ast = {:@, meta_attr, [{name, add_no_call(meta), params}]} - pre_module_attribute(new_ast, state, {line, column}, name, type, is_definition) + + state + |> add_attribute(name, type, is_definition, {line, column}) + |> add_current_env_to_line(line) + |> result(new_ast) _ -> {[], state} diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index f42276b0..b0af4a3a 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -7,7 +7,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do alias ElixirSense.Core.State.{VarInfo, CallInfo, StructInfo, ModFunInfo, AttributeInfo} @moduledoc_support Version.match?(System.version(), "< 1.17.0-dev") - @attribute_support Version.match?(System.version(), "< 1.17.0-dev") + @attribute_support true or Version.match?(System.version(), "< 1.17.0-dev") @binding_support Version.match?(System.version(), "< 1.17.0-dev") @protocol_support Version.match?(System.version(), "< 1.17.0-dev") @defdelegate_support Version.match?(System.version(), "< 1.17.0-dev") @@ -1289,7 +1289,61 @@ defmodule ElixirSense.Core.MetadataBuilderTest do end end - if @attribute_support do + test "module attributes" do + state = + """ + defmodule MyModule do + @myattribute String + IO.puts @myattribute + defmodule InnerModule do + @inner_attr %{abc: nil} + @inner_attr_1 __MODULE__ + IO.puts @inner_attr + end + IO.puts "" + @otherattribute Application.get_env(:elixir_sense, :some_attribute, InnerModule) + end + """ + |> string_to_state + + assert get_line_attributes(state, 10) == [ + %ElixirSense.Core.State.AttributeInfo{ + name: :myattribute, + positions: [{2, 3}, {3, 11}] + }, + %AttributeInfo{ + name: :otherattribute, + positions: [{10, 3}] + } + ] + + assert get_line_attributes(state, 3) == [ + %AttributeInfo{ + name: :myattribute, + positions: [{2, 3}, {3, 11}] + } + ] + + assert get_line_attributes(state, 7) == [ + %AttributeInfo{ + name: :inner_attr, + positions: [{5, 5}, {7, 13}] + }, + %AttributeInfo{ + name: :inner_attr_1, + positions: [{6, 5}] + } + ] + + assert get_line_attributes(state, 9) == [ + %AttributeInfo{ + name: :myattribute, + positions: [{2, 3}, {3, 11}] + } + ] + end + + if @attribute_binding_support do test "module attributes" do state = """ @@ -5335,11 +5389,9 @@ defmodule ElixirSense.Core.MetadataBuilderTest do {ProtocolEmbedded, InheritMod.ProtocolEmbedded} ]) - if @attribute_support do - assert get_line_attributes(state, 4) == [ - %AttributeInfo{name: :my_attribute, positions: [{2, 3}]} - ] - end + assert [ + %AttributeInfo{name: :my_attribute, positions: [{2, _}]} + ] = get_line_attributes(state, 4) if @protocol_support do assert %{ From eceb36fc5894b6c61b079ebffb5035ae09c85ffc Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 28 Apr 2024 20:58:01 +0200 Subject: [PATCH 010/235] defdelegate --- lib/elixir_sense/core/compiler.ex | 42 +++++++++++++++++++ .../core/metadata_builder_test.exs | 5 +-- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 09b2f33c..8b2203e2 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -739,6 +739,48 @@ defmodule ElixirSense.Core.Compiler do # Macro handling + defp expand_macro( + meta, + Kernel, + :defdelegate, + [funs, opts], + callback, + state, + env + ) do + assert_no_match_or_guard_scope(env.context, :"def/2") + module = assert_module_scope(env, :def, 2) + + {position, end_position} = extract_range(meta) + {line, _} = position + + {opts, state, env} = expand(opts, state, env) + target = Kernel.Utils.defdelegate_all(funs, opts, env) + # TODO: Remove List.wrap when multiple funs are no longer supported + state = funs + |> List.wrap + |> Enum.reduce(state, fn fun, state -> + # TODO expand args? + {name, args, as, as_args} = Kernel.Utils.defdelegate_each(fun, opts) + arity = length(args) + state + |> add_current_env_to_line(line, %{env | context: nil, function: {name, arity}}) + |> add_mod_fun_to_position( + {module, name, arity}, + position, + end_position, + args, + :defdelegate, + "", + # doc, + %{delegate_to: {target, as, length(as_args)}}, + # meta + [target: {target, as}] + ) + end) + {[], state, env} + end + defp expand_macro( meta, Kernel, diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index b0af4a3a..d91951f9 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -7,10 +7,9 @@ defmodule ElixirSense.Core.MetadataBuilderTest do alias ElixirSense.Core.State.{VarInfo, CallInfo, StructInfo, ModFunInfo, AttributeInfo} @moduledoc_support Version.match?(System.version(), "< 1.17.0-dev") - @attribute_support true or Version.match?(System.version(), "< 1.17.0-dev") + @attribute_binding_support Version.match?(System.version(), "< 1.17.0-dev") @binding_support Version.match?(System.version(), "< 1.17.0-dev") @protocol_support Version.match?(System.version(), "< 1.17.0-dev") - @defdelegate_support Version.match?(System.version(), "< 1.17.0-dev") @first_alias_positions Version.match?(System.version(), "< 1.17.0-dev") @struct_support Version.match?(System.version(), "< 1.17.0-dev") @calls_support Version.match?(System.version(), "< 1.17.0-dev") @@ -5221,7 +5220,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } = state.mods_funs_to_positions end - if @defdelegate_support do test "registers delegated func" do state = """ @@ -5262,7 +5260,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } } = state.mods_funs_to_positions end - end if @protocol_support do test "registers mods and func for protocols" do From 42aeb0d82ca6c05b628a5178cc2f80187fedd0c3 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 29 Apr 2024 07:21:11 +0200 Subject: [PATCH 011/235] calls --- lib/elixir_sense/core/compiler.ex | 60 +++++++++++++++---- lib/elixir_sense/core/state.ex | 19 +++++- test/elixir_sense/core/compiler_test.exs | 9 +++ .../core/metadata_builder_test.exs | 15 +++-- 4 files changed, 86 insertions(+), 17 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 8b2203e2..9587af5d 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -617,6 +617,11 @@ defmodule ElixirSense.Core.Compiler do {:macro, module, callback} -> # TODO there is a subtle difference - callback will call expander with state derrived from env via # :elixir_env.env_to_ex(env) possibly losing some details + line = Keyword.get(meta, :line, 0) + column = Keyword.get(meta, :column, nil) + # state = state + # |> add_call_to_line({module, fun, length(args)}, {line, column}) + # |> add_current_env_to_line(line, env) expand_macro(meta, module, fun, args, callback, state, env) {:function, module, fun} -> @@ -668,9 +673,24 @@ defmodule ElixirSense.Core.Compiler do assert_no_match_or_guard_scope(e.context, "anonymous call") {[e_expr | e_args], sa, ea} = expand_args([expr | args], s, e) - # if is_atom(e_expr) do - # function_error(meta, e, __MODULE__, {:invalid_function_call, e_expr}) - # end + sa = if is_atom(e_expr |> dbg) do + # function_error(meta, e, __MODULE__, {:invalid_function_call, e_expr}) + sa + else + line = Keyword.get(dot_meta, :line, 0) + column = Keyword.get(dot_meta, :column, nil) + column = if column do + # for remote calls we emit position of right side of . + # to make it consistent we shift dot position here + column + 1 + else + column + end + + sa + |> add_call_to_line({nil, e_expr, length(e_args)}, {line, column}) + |> add_current_env_to_line(line, e) + end {{{:., dot_meta, [e_expr]}, meta, e_args}, sa, ea} end @@ -806,7 +826,7 @@ defmodule ElixirSense.Core.Compiler do meta, Kernel, :@, - [{name, _meta, args}], + [{name, name_meta, args}], _callback, state, env @@ -837,7 +857,7 @@ defmodule ElixirSense.Core.Compiler do |> add_current_env_to_line(line) - {e_args, state, env} + {{:@, meta, [{name, name_meta, e_args}]}, state, env} end defp expand_macro( @@ -1153,7 +1173,8 @@ defmodule ElixirSense.Core.Compiler do assert_no_clauses(right, meta, args, e) line = Keyword.get(meta, :line, 0) - # TODO register call + column = Keyword.get(meta, :column, nil) + sl = if line > 0 do sl |> add_current_env_to_line(line, e) @@ -1174,6 +1195,7 @@ defmodule ElixirSense.Core.Compiler do case rewrite(context, receiver, dot_meta, right, attached_meta, e_args, s) do {:ok, rewritten} -> s = __MODULE__.Env.close_write(sa, s) + |> add_call_to_line({receiver, right, length(e_args)}, {line, column}) |> add_current_env_to_line(line, e) {rewritten, s, ea} @@ -1217,12 +1239,12 @@ defmodule ElixirSense.Core.Compiler do raise "invalid_local_invocation" end - # A compiler may want to emit a :local_function trace in here. - # TODO register call - line = Keyword.fetch!(meta, :line) + line = Keyword.get(meta, :line, 0) + column = Keyword.get(meta, :column, nil) state = state + |> add_call_to_line({nil, fun, length(args)}, {line, column}) |> add_current_env_to_line(line, env) # state = update_in(state.locals, &[{fun, length(args)} | &1]) @@ -1233,10 +1255,12 @@ defmodule ElixirSense.Core.Compiler do defp expand_local(meta, fun, args, state, env) do # elixir compiler raises here # raise "undefined_function" - line = Keyword.fetch!(meta, :line) + line = Keyword.get(meta, :line, 0) + column = Keyword.get(meta, :column, nil) state = state + |> add_call_to_line({nil, fun, length(args)}, {line, column}) |> add_current_env_to_line(line, env) {args, state, env} = expand_args(args, state, env) @@ -1391,13 +1415,29 @@ defmodule ElixirSense.Core.Compiler do # end attached_meta = attach_runtime_module(remote, require_meta, s, e) + line = Keyword.get(attached_meta, :line, 0) + column = Keyword.get(attached_meta, :column, nil) + + se = se + |> add_call_to_line({remote, fun, arity}, {line, column}) + |> add_current_env_to_line(line, ee) + {{:&, meta, [{:/, [], [{{:., dot_meta, [remote, fun]}, attached_meta, []}, arity]}]}, se, ee} {{:local, _fun, _arity}, _, _, _se, %{function: nil}} -> + # TODO register call? raise "undefined_local_capture" {{:local, fun, arity}, local_meta, _, se, ee} -> + + line = Keyword.get(local_meta, :line, 0) + column = Keyword.get(local_meta, :column, nil) + + se = se + |> add_call_to_line({nil, fun, arity}, {line, column}) + |> add_current_env_to_line(line, ee) + {{:&, meta, [{:/, [], [{fun, local_meta, nil}, arity]}]}, se, ee} {:expand, expr, se, ee} -> diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 0af240bf..07d155ae 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -388,9 +388,9 @@ defmodule ElixirSense.Core.State do |> get_current_vars() |> Enum.filter(& Map.has_key?(elem(state.vars, 0), {&1.name, nil})) - dbg(vars) - dbg(state.vars) - dbg(state.scope_vars_info) + # dbg(vars) + # dbg(state.vars) + # dbg(state.scope_vars_info) %Env{ @@ -511,8 +511,21 @@ defmodule ElixirSense.Core.State do end end + # TODO remove this def add_call_to_line(%__MODULE__{} = state, {nil, :__block__, _}, _position), do: state + def add_call_to_line(%__MODULE__{} = state, {{:@, _meta, [{name, _name_meta, _args}]}, func, arity}, {_line, _column} = position) when is_atom(name) do + add_call_to_line(state, {{:attribute, name}, func, arity}, position) + end + def add_call_to_line(%__MODULE__{} = state, {{name, _name_meta, args}, func, arity}, {_line, _column} = position) when is_atom(args) do + add_call_to_line(state, {{:variable, name}, func, arity}, position) + end + def add_call_to_line(%__MODULE__{} = state, {nil, {:@, _meta, [{name, _name_meta, _args}]}, arity}, {_line, _column} = position) when is_atom(name) do + add_call_to_line(state, {nil, {:attribute, name}, arity}, position) + end + def add_call_to_line(%__MODULE__{} = state, {nil, {name, _name_meta, args}, arity}, {_line, _column} = position) when is_atom(args) do + add_call_to_line(state, {nil, {:variable, name}, arity}, position) + end def add_call_to_line(%__MODULE__{} = state, {mod, func, arity}, {line, _column} = position) do call = %CallInfo{mod: mod, func: func, arity: arity, position: position} diff --git a/test/elixir_sense/core/compiler_test.exs b/test/elixir_sense/core/compiler_test.exs index 1cad90ef..7a6d7d72 100644 --- a/test/elixir_sense/core/compiler_test.exs +++ b/test/elixir_sense/core/compiler_test.exs @@ -575,6 +575,15 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do end describe "Kernel macros" do + test "@" do + assert_expansion_env(""" + defmodule Abc do + @foo 1 + @foo + end + """) + end + test "defmodule" do assert_expansion_env("defmodule Abc, do: :ok") diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index d91951f9..91b87b2f 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -12,7 +12,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do @protocol_support Version.match?(System.version(), "< 1.17.0-dev") @first_alias_positions Version.match?(System.version(), "< 1.17.0-dev") @struct_support Version.match?(System.version(), "< 1.17.0-dev") - @calls_support Version.match?(System.version(), "< 1.17.0-dev") + @calls_support true or Version.match?(System.version(), "< 1.17.0-dev") @typespec_support Version.match?(System.version(), "< 1.17.0-dev") @record_support Version.match?(System.version(), "< 1.17.0-dev") @doc_support Version.match?(System.version(), "< 1.17.0-dev") @@ -5959,6 +5959,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do end end + if @typespec_support do test "registers typespec no parens calls" do state = """ @@ -5975,11 +5976,13 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] } end + end test "registers calls local no arg" do state = """ defmodule NyModule do + def func_1, do: :ok def func do func_1() end @@ -5988,7 +5991,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert state.calls == %{ - 3 => [%CallInfo{arity: 0, func: :func_1, position: {3, 5}, mod: nil}] + 4 => [%CallInfo{arity: 0, func: :func_1, position: {4, 5}, mod: nil}] } end @@ -6266,7 +6269,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ + assert state.calls |> sort_calls == %{ 3 => [ %CallInfo{arity: 1, position: {3, 21}, func: :func, mod: MyMod}, %CallInfo{arity: 1, position: {3, 37}, func: :other, mod: Other} @@ -6285,7 +6288,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ + assert state.calls |> sort_calls == %{ 3 => [ %CallInfo{arity: 1, position: {3, 15}, func: :func_1, mod: nil}, %CallInfo{arity: 1, position: {3, 32}, func: :other, mod: Some} @@ -6425,6 +6428,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } end end + + defp sort_calls(calls) do + calls |> Enum.map(fn {k, v} -> {k, Enum.sort(v)} end) |> Map.new + end end if @typespec_support do From 6e99c463a31d3a8a922016581f54b9c420128d7a Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 29 Apr 2024 17:39:23 +0200 Subject: [PATCH 012/235] fix bug in bind_quoted --- lib/elixir_sense/core/compiler.ex | 6 +++--- test/elixir_sense/core/compiler_test.exs | 11 +++++++++++ test/elixir_sense/core/metadata_builder_test.exs | 14 +++++++------- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 9587af5d..3f0d0f19 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -355,7 +355,7 @@ defmodule ElixirSense.Core.Compiler do # res |> elem(0) |> IO.inspect # res {q, prelude} = - __MODULE__.Quote.build(meta, line, file, context, unquote_opt, generated) |> dbg + __MODULE__.Quote.build(meta, line, file, context, unquote_opt, generated) quoted = __MODULE__.Quote.quote(meta, exprs |> dbg, binding, q, prelude, et) |> dbg expand(quoted, st, et) @@ -3324,10 +3324,10 @@ defmodule ElixirSense.Core.Compiler do vars = Enum.map(binding, fn {k, v} -> - {:{}, [], {:=, [], {:{}, [], [k, meta, context]}, v}} + {:{}, [], [:=, [], [{:{}, [], [k, meta, context]}, v]]} end) - quoted = do_quote(expr, q, e) |> dbg + quoted = do_quote(expr, q, e) with_vars = case vars do diff --git a/test/elixir_sense/core/compiler_test.exs b/test/elixir_sense/core/compiler_test.exs index 7a6d7d72..888ff03f 100644 --- a/test/elixir_sense/core/compiler_test.exs +++ b/test/elixir_sense/core/compiler_test.exs @@ -323,6 +323,17 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do assert_expansion("quote do: &inspect/1") end + test "expands quote bind_quoted" do + assert_expansion(""" + kv = [a: 1] + quote bind_quoted: [kv: kv] do + Enum.each(kv, fn {k, v} -> + def unquote(k)(), do: unquote(v) + end) + end + """) + end + test "expands &super" do assert_expansion_env(""" defmodule Abc do diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 91b87b2f..0ba4ab94 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -12,7 +12,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do @protocol_support Version.match?(System.version(), "< 1.17.0-dev") @first_alias_positions Version.match?(System.version(), "< 1.17.0-dev") @struct_support Version.match?(System.version(), "< 1.17.0-dev") - @calls_support true or Version.match?(System.version(), "< 1.17.0-dev") + @macro_calls_support Version.match?(System.version(), "< 1.17.0-dev") @typespec_support Version.match?(System.version(), "< 1.17.0-dev") @record_support Version.match?(System.version(), "< 1.17.0-dev") @doc_support Version.match?(System.version(), "< 1.17.0-dev") @@ -5840,8 +5840,11 @@ defmodule ElixirSense.Core.MetadataBuilderTest do end end - if @calls_support do describe "calls" do + defp sort_calls(calls) do + calls |> Enum.map(fn {k, v} -> {k, Enum.sort(v)} end) |> Map.new + end + test "registers calls with __MODULE__" do state = """ @@ -6397,6 +6400,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } end + if @macro_calls_support do test "registers calls on ex_unit DSL" do state = """ @@ -6427,13 +6431,9 @@ defmodule ElixirSense.Core.MetadataBuilderTest do 12 => [%CallInfo{arity: 0, position: {12, 3}, func: :test, mod: nil}] } end + end end - defp sort_calls(calls) do - calls |> Enum.map(fn {k, v} -> {k, Enum.sort(v)} end) |> Map.new - end - end - if @typespec_support do describe "typespec" do test "registers types" do From ae9f28cebb728cf8e7672bb1595f10f341a02e2b Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 1 May 2024 06:00:18 +0200 Subject: [PATCH 013/235] some support for unquote fragments --- lib/elixir_sense/core/compiler.ex | 68 +++++++++++++++++++ test/elixir_sense/core/compiler_test.exs | 34 +++++++++- .../core/metadata_builder_test.exs | 48 +++++++++++++ 3 files changed, 149 insertions(+), 1 deletion(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 3f0d0f19..8ae16aab 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1001,6 +1001,26 @@ defmodule ElixirSense.Core.Compiler do # TODO store mod_fun_to_pos line = Keyword.fetch!(meta, :line) + unquoted_call = __MODULE__.Quote.has_unquotes(call) + unquoted_expr = __MODULE__.Quote.has_unquotes(expr) + + {call, expr} = + if unquoted_expr or unquoted_call do + {call, expr} = __MODULE__.Quote.escape({call, expr}, :none, true) + + try do + # TODO binding? + {{call, expr}, _} = Code.eval_quoted({call, expr}, [], env) + {call, expr} + rescue + _ -> raise "unable to eval" + end + else + {call, expr} + end + dbg(call) + dbg(expr) + # state = # state # |> add_current_env_to_line(line, env) @@ -1009,6 +1029,7 @@ defmodule ElixirSense.Core.Compiler do |> new_func_vars_scope {name_and_args, guards} = __MODULE__.Utils.extract_guards(call) + dbg(name_and_args) {name, _meta_1, args} = case name_and_args do @@ -3258,6 +3279,42 @@ defmodule ElixirSense.Core.Compiler do {:&, [], [{:/, [], [{{:., [], [module, name]}, [{:no_parens, true}], []}, arity]}]} end + def has_unquotes(ast), do: has_unquotes(ast, 0) + + def has_unquotes({:quote, _, [child]}, quote_level) do + has_unquotes(child, quote_level + 1) + end + def has_unquotes({:quote, _, [quote_opts, child]}, quote_level) do + case disables_unquote(quote_opts) do + true -> false + _ -> has_unquotes(child, quote_level + 1) + end + end + def has_unquotes({unquote, _, [child]}, quote_level) + when unquote in [:unquote, :unquote_splicing] do + case quote_level do + 0 -> true + _ -> has_unquotes(child, quote_level - 1) + end + end + def has_unquotes({{:., _, [_, :unquote]}, _, [_]}, _), do: true + def has_unquotes({var, _, ctx}, _) when is_atom(var) and is_atom(ctx), do: false + def has_unquotes({name, _, args}, quote_level) when is_list(args) do + has_unquotes(name) or Enum.any?(args, fn child -> has_unquotes(child, quote_level) end) + end + def has_unquotes({left, right}, quote_level) do + has_unquotes(left, quote_level) or has_unquotes(right, quote_level) + end + def has_unquotes(list, quote_level) when is_list(list) do + Enum.any?(list, fn child -> has_unquotes(child, quote_level) end) + end + def has_unquotes(_other, _), do: false + + defp disables_unquote([{:unquote, false} | _]), do: true + defp disables_unquote([{:bind_quoted, _} | _]), do: true + defp disables_unquote([_h | t]), do: disables_unquote(t) + defp disables_unquote(_), do: false + def build(meta, line, file, context, unquote, generated) do acc0 = [] @@ -3316,6 +3373,17 @@ defmodule ElixirSense.Core.Compiler do def is_valid(:generated, generated), do: is_boolean(generated) def is_valid(:unquote, unquote), do: is_boolean(unquote) + def escape(expr, kind, unquote) do + do_quote(expr, %__MODULE__{ + line: true, + file: nil, + vars_hygiene: false, + aliases_hygiene: false, + imports_hygiene: false, + unquote: unquote + }, kind) + end + def quote(_meta, {:unquote_splicing, _, [_]}, _binding, %__MODULE__{unquote: true}, _, _), do: raise("unquote_splicing only works inside arguments and block contexts") diff --git a/test/elixir_sense/core/compiler_test.exs b/test/elixir_sense/core/compiler_test.exs index 888ff03f..5773fc65 100644 --- a/test/elixir_sense/core/compiler_test.exs +++ b/test/elixir_sense/core/compiler_test.exs @@ -323,7 +323,7 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do assert_expansion("quote do: &inspect/1") end - test "expands quote bind_quoted" do + test "expands quote with bind_quoted" do assert_expansion(""" kv = [a: 1] quote bind_quoted: [kv: kv] do @@ -334,6 +334,38 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do """) end + test "expands quote with unquote false" do + assert_expansion(""" + quote unquote: false do + unquote("hello") + end + """) + end + + test "expands quote with file" do + assert_expansion(""" + quote file: "some.ex", do: bar(1, 2, 3) + """) + end + + test "expands quote with line" do + assert_expansion(""" + quote line: 123, do: bar(1, 2, 3) + """) + end + + test "expands quote with location: keep" do + assert_expansion(""" + quote location: :keep, do: bar(1, 2, 3) + """) + end + + test "expands quote with context" do + assert_expansion(""" + quote context: Foo, do: abc = 3 + """) + end + test "expands &super" do assert_expansion_env(""" defmodule Abc do diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 0ba4ab94..41bfd14d 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -5261,6 +5261,54 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } = state.mods_funs_to_positions end + test "registers defs with unquote fragments" do + state = + """ + defmodule MyModuleWithFuns do + def unquote(:foo)(), do: :ok + def bar(), do: unquote(:ok) + def baz(unquote(:abc)), do: unquote(:abc) + end + """ + |> string_to_state + + assert %{ + {MyModuleWithFuns, :foo, 0} => %ModFunInfo{ + params: [[]] + }, + {MyModuleWithFuns, :bar, 0} => %ModFunInfo{ + params: [[]] + }, + {MyModuleWithFuns, :baz, 1} => %ModFunInfo{ + params: [[:abc]] + } + } = state.mods_funs_to_positions + end + + if @expand_eval do + test "registers defs with unquote fragments with binding" do + state = + """ + defmodule MyModuleWithFuns do + kv = [foo: 1, bar: 2] |> IO.inspect + Enum.each(kv, fn {k, v} -> + def unquote(k)(), do: unquote(v) + end) + end + """ + |> string_to_state + + assert %{ + {MyModuleWithFuns, :foo, 0} => %ModFunInfo{ + params: [[]] + }, + {MyModuleWithFuns, :bar, 0} => %ModFunInfo{ + params: [[]] + } + } = state.mods_funs_to_positions + end + end + if @protocol_support do test "registers mods and func for protocols" do state = From d3c9ad32ffa7fd11cc5a56a355962c3de995a8e4 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 1 May 2024 10:01:52 +0200 Subject: [PATCH 014/235] typespecs --- lib/elixir_sense/core/compiler.ex | 129 +++++++++++++++++- lib/elixir_sense/core/metadata_builder.ex | 39 +++--- lib/elixir_sense/core/state.ex | 20 ++- .../core/metadata_builder_test.exs | 47 +++---- 4 files changed, 179 insertions(+), 56 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 8ae16aab..f0c929de 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -3,6 +3,7 @@ defmodule ElixirSense.Core.Compiler do alias ElixirSense.Core.State require Logger alias ElixirSense.Core.Introspection + alias ElixirSense.Core.TypeInfo @env :elixir_env.new() def env, do: @env @@ -816,12 +817,100 @@ defmodule ElixirSense.Core.Compiler do state = state - |> add_current_env_to_line(line) + |> add_current_env_to_line(line, env) {arg, state, env} = expand(arg, state, env) add_behaviour(arg, state, env) end + defp expand_macro( + attr_meta, + Kernel, + :@, + [{kind, kind_meta, [expr | _]}], + _callback, + state, + env + ) when kind in [:type, :typep, :opaque] do + assert_module_scope(env, :@, 1) + unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") + + {expr, state, env} = __MODULE__.Typespec.expand(expr, state, env) + + case __MODULE__.Typespec.type_to_signature(expr) do + {name, [type_arg]} when name in [:required, :optional] -> + raise "type #{name}/#{1} is a reserved type and it cannot be defined" + + {name, type_args} -> + # TODO elixir does Macro.escape with unquote: true + + spec = TypeInfo.typespec_to_string(kind, expr) + + {position = {line, _column}, end_position} = extract_range(attr_meta) + + state = state + |> add_type(env, name, type_args, spec, kind, position, end_position) + |> with_typespec({name, length(type_args)}) + |> add_current_env_to_line(line, env) + |> with_typespec(nil) + + {{:@, attr_meta, [{kind, kind_meta, [expr]}]}, state, env} + :error -> + {{:@, attr_meta, [{kind, kind_meta, [expr]}]}, state, env} + end + end + + defp expand_macro( + attr_meta, + Kernel, + :@, + [{kind, kind_meta, [expr | _]}], + _callback, + state, + env + ) when kind in [:callback, :macrocallback, :spec] do + assert_module_scope(env, :@, 1) + unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") + + {expr, state, env} = __MODULE__.Typespec.expand(expr, state, env) + + case __MODULE__.Typespec.spec_to_signature(expr) do + {name, type_args} -> + spec = TypeInfo.typespec_to_string(kind, expr) + + {position = {line, _column}, end_position} = extract_range(attr_meta) + + state = + if kind in [:callback, :macrocallback] do + state + |> add_func_to_index( + env, + :behaviour_info, + [{:atom, attr_meta, nil}], + position, + end_position, + :def, + generated: true + ) + else + state + end + + state = state + |> add_spec(env, name, type_args, spec, kind, position, end_position, + generated: state.generated + ) + |> with_typespec({name, length(type_args)}) + |> add_current_env_to_line(line, env) + |> with_typespec(nil) + + {{:@, attr_meta, [{kind, kind_meta, [expr]}]}, state, env} + + :error -> + {{:@, attr_meta, [{kind, kind_meta, [expr]}]}, state, env} + end + end + defp expand_macro( meta, Kernel, @@ -854,7 +943,7 @@ defmodule ElixirSense.Core.Compiler do state = state |> add_attribute(name, nil, is_definition, {line, column}) - |> add_current_env_to_line(line) + |> add_current_env_to_line(line, env) {{:@, meta, [{name, name_meta, e_args}]}, state, env} @@ -1130,7 +1219,7 @@ defmodule ElixirSense.Core.Compiler do {nil, nil} else position = { - Keyword.get(meta, :line, 0), + line, Keyword.get(meta, :column, 1) } @@ -4182,4 +4271,38 @@ defmodule ElixirSense.Core.Compiler do end end end + + defmodule Typespec do + alias ElixirSense.Core.Compiler, as: ElixirExpand + def spec_to_signature({:when, _, [spec, _]}), do: type_to_signature(spec) + def spec_to_signature(other), do: type_to_signature(other) + + def type_to_signature({:"::", _, [{name, _, context}, _]}) + when is_atom(name) and name != :"::" and is_atom(context), + do: {name, []} + + def type_to_signature({:"::", _, [{name, _, args}, _]}) when is_atom(name) and name != :"::", + do: {name, args} + + def type_to_signature(_), do: :error + + def expand(ast, state, env) do + {ast, {state, env}} = + # TODO this should handle remote calls, attributes unquotes? + {ast, {state, env}} = Macro.prewalk(ast, {state, env}, fn + {:__aliases__, meta, list} = node, {state, env} when is_list(list) -> + {node, state, env} = ElixirExpand.expand(node, state, env) + {node, {state, env}} + + {:__MODULE__, meta, ctx} = node, {state, env} when is_atom(ctx) -> + {node, state, env} = ElixirExpand.expand(node, state, env) + {node, {state, env}} + + other, acc -> + {other, acc} + end) + + {ast, state, env} + end + end end diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index ec51c3ea..22a753e1 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -440,21 +440,6 @@ defmodule ElixirSense.Core.MetadataBuilder do |> result(ast) end - defp pre_type(ast, state, meta, type_name, type_args, spec, kind) do - spec = TypeInfo.typespec_to_string(kind, spec) - - {position = {line, _column}, end_position} = extract_range(meta) - env = get_current_env(state) - - state - |> add_type(env, type_name, type_args, spec, kind, position, end_position, - generated: state.generated - ) - |> add_typespec_namespace(type_name, length(type_args)) - |> add_current_env_to_line(line) - |> result(ast) - end - defp pre_spec(ast, state, meta, type_name, type_args, spec, kind) do spec = TypeInfo.typespec_to_string(kind, spec) @@ -835,15 +820,23 @@ defmodule ElixirSense.Core.MetadataBuilder do ) when kind in [:type, :typep, :opaque] and is_atom(name) and (is_nil(type_args) or is_list(type_args)) do - pre_type( - {:@, meta_attr, [{kind, add_no_call(kind_meta), kind_args}]}, - state, - meta_attr, - name, - List.wrap(type_args), - expand_aliases_in_ast(state, spec), - kind + + ast = {:@, meta_attr, [{kind, add_no_call(kind_meta), kind_args}]} + spec = expand_aliases_in_ast(state, spec) + type_args = List.wrap(type_args) + + spec = TypeInfo.typespec_to_string(kind, spec) + + {position = {line, _column}, end_position} = extract_range(meta_attr) + env = get_current_env(state) + + state + |> add_type(env, name, type_args, spec, kind, position, end_position, + generated: state.generated ) + |> add_typespec_namespace(name, length(type_args)) + |> add_current_env_to_line(line) + |> result(ast) end defp pre( diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 07d155ae..df625f88 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -61,7 +61,8 @@ defmodule ElixirSense.Core.State do optional_callbacks_context: list(), # TODO better type binding_context: list, - macro_env: list(Macro.Env.t()) + macro_env: list(Macro.Env.t()), + typespec: nil | {atom, arity} } @auto_imported_functions :elixir_env.new().functions @@ -108,7 +109,8 @@ defmodule ElixirSense.Core.State do typedoc_context: [[]], optional_callbacks_context: [[]], moduledoc_positions: %{}, - macro_env: [:elixir_env.new()] + macro_env: [:elixir_env.new()], + typespec: nil defmodule Env do @moduledoc """ @@ -404,7 +406,7 @@ defmodule ElixirSense.Core.State do versioned_vars: versioned_vars, attributes: current_attributes, behaviours: current_behaviours, - typespec: nil, + typespec: state.typespec, scope_id: current_scope_id, protocol: current_scope_protocol } @@ -426,6 +428,10 @@ defmodule ElixirSense.Core.State do _previous_env = state.lines_to_env[line] current_env = get_current_env(state, macro_env) + if current_env.typespec do + dbg({line, current_env.typespec}) + end + # TODO # env = merge_env_vars(current_env, previous_env) env = current_env @@ -1145,7 +1151,7 @@ defmodule ElixirSense.Core.State do def add_type( %__MODULE__{} = state, - %__MODULE__.Env{} = env, + env, type_name, type_args, spec, @@ -1219,7 +1225,7 @@ defmodule ElixirSense.Core.State do def add_spec( %__MODULE__{} = state, - %__MODULE__.Env{} = env, + env, type_name, type_args, spec, @@ -1966,4 +1972,8 @@ defmodule ElixirSense.Core.State do # TODO versioned_vars } end + + def with_typespec(%__MODULE__{} = state, typespec) do + %{state | typespec: typespec} + end end diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 41bfd14d..fa262be9 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -8,12 +8,13 @@ defmodule ElixirSense.Core.MetadataBuilderTest do @moduledoc_support Version.match?(System.version(), "< 1.17.0-dev") @attribute_binding_support Version.match?(System.version(), "< 1.17.0-dev") + @expand_eval false @binding_support Version.match?(System.version(), "< 1.17.0-dev") @protocol_support Version.match?(System.version(), "< 1.17.0-dev") @first_alias_positions Version.match?(System.version(), "< 1.17.0-dev") @struct_support Version.match?(System.version(), "< 1.17.0-dev") @macro_calls_support Version.match?(System.version(), "< 1.17.0-dev") - @typespec_support Version.match?(System.version(), "< 1.17.0-dev") + @typespec_calls_support Version.match?(System.version(), "< 1.17.0-dev") @record_support Version.match?(System.version(), "< 1.17.0-dev") @doc_support Version.match?(System.version(), "< 1.17.0-dev") @meta_support Version.match?(System.version(), "< 1.17.0-dev") @@ -5056,11 +5057,9 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert nil == get_line_function(state, 15) assert MyModule == get_line_module(state, 15) - if @defdelegate_support do - assert nil == get_line_typespec(state, 16) - assert {:func_delegated, 1} == get_line_function(state, 16) - assert MyModule == get_line_module(state, 16) - end + assert nil == get_line_typespec(state, 16) + assert {:func_delegated, 1} == get_line_function(state, 16) + assert MyModule == get_line_module(state, 16) assert nil == get_line_typespec(state, 18) assert {:is_even, 1} == get_line_function(state, 18) @@ -6010,7 +6009,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do end end - if @typespec_support do + if @typespec_calls_support do test "registers typespec no parens calls" do state = """ @@ -6482,7 +6481,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do end end - if @typespec_support do describe "typespec" do test "registers types" do state = @@ -6498,7 +6496,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.types == %{ + assert %{ {My, :no_arg_no_parens, 0} => %ElixirSense.Core.State.TypeInfo{ args: [[]], kind: :type, @@ -6530,7 +6528,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do kind: :type, name: :overloaded, positions: [{6, 3}], - end_positions: [nil], + end_positions: [_], generated: [false], args: [["a"]], specs: ["@type overloaded(a) :: {a}"] @@ -6545,9 +6543,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do meta: %{opaque: true}, specs: ["@opaque with_args(a, b) :: {a, b}"] } - } + } = state.types end + if @protocol_support do test "protocol exports type t" do state = """ @@ -6569,6 +6568,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } } end + end test "specs and callbacks" do state = @@ -6585,7 +6585,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do # if there are callbacks behaviour_info/1 is defined assert state.mods_funs_to_positions[{Proto, :behaviour_info, 1}] != nil - assert state.specs == %{ + assert %{ {Proto, :abc, 0} => %ElixirSense.Core.State.SpecInfo{ args: [[], []], kind: :spec, @@ -6609,11 +6609,11 @@ defmodule ElixirSense.Core.MetadataBuilderTest do name: :other, args: [["x"]], positions: [{5, 3}], - end_positions: [nil], + end_positions: [_], generated: [false], specs: ["@macrocallback other(x) :: Macro.t() when x: integer"] } - } + } = state.specs end test "specs and types expand aliases" do @@ -6645,7 +6645,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } = state.types end end - end if @record_support do test "defrecord defines record macros" do @@ -6934,6 +6933,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state + # dbg(state.lines_to_env |> Enum.map(fn {k, v} -> {k, %{module: v.module, function: v.function, typespec: v.typespec}} end)) + assert nil == get_line_typespec(state, 1) assert nil == get_line_function(state, 1) assert nil == get_line_module(state, 1) @@ -6942,11 +6943,9 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert nil == get_line_function(state, 2) assert My == get_line_module(state, 2) - if @typespec_support do - assert {:a, 0} == get_line_typespec(state, 5) - assert nil == get_line_function(state, 5) - assert My == get_line_module(state, 5) - end + assert {:a, 0} == get_line_typespec(state, 5) + assert nil == get_line_function(state, 5) + assert My == get_line_module(state, 5) assert nil == get_line_typespec(state, 7) assert nil == get_line_function(state, 7) @@ -6960,11 +6959,9 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert nil == get_line_function(state, 13) assert My == get_line_module(state, 13) - if @typespec_support do - assert {:test, 2} == get_line_typespec(state, 15) - assert nil == get_line_function(state, 15) - assert My == get_line_module(state, 15) - end + assert {:test, 2} == get_line_typespec(state, 15) + assert nil == get_line_function(state, 15) + assert My == get_line_module(state, 15) assert nil == get_line_typespec(state, 16) assert {:test, 2} == get_line_function(state, 16) From 4fd93059158d663ad793738e9350d73eee4e3196 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 2 May 2024 05:45:56 +0200 Subject: [PATCH 015/235] structs --- lib/elixir_sense/core/compiler.ex | 42 +++++++++++ lib/elixir_sense/core/metadata_builder.ex | 70 +------------------ lib/elixir_sense/core/state.ex | 67 +++++++++++++++++- .../core/metadata_builder_test.exs | 7 +- 4 files changed, 113 insertions(+), 73 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index f0c929de..3039370a 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -982,6 +982,48 @@ defmodule ElixirSense.Core.Compiler do end end + defp expand_macro( + meta, + Kernel, + type, + [fields], + _callback, + state, + env + ) when type in [:defstruct, :defexception] do + module = assert_module_scope(env, type, 1) + if Map.has_key?(state.structs, module) do + raise ArgumentError, + "defstruct has already been called for " <> + "#{inspect(module)}, defstruct can only be called once per module" + end + case fields do + fs when is_list(fs) -> + :ok + + other -> + raise ArgumentError, "struct fields definition must be list, got: #{inspect(other)}" + end + + {position, end_position} = extract_range(meta) + + fields = fields + |> Enum.filter(fn + field when is_atom(field) -> true + {field, _} when is_atom(field) -> true + _ -> false + end) + |> Enum.map(fn + field when is_atom(field) -> {field, nil} + {field, value} when is_atom(field) -> {field, value} + end) + + state = state + |> add_struct_or_exception(env, type, fields, position, end_position) + + {{type, meta, [fields]}, state, env} + end + defp expand_macro( meta, Kernel, diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index 22a753e1..f59773b4 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -1053,8 +1053,10 @@ defmodule ElixirSense.Core.MetadataBuilder do [] end + env = get_current_env(state) + state - |> add_struct_or_exception(type, fields, position, end_position) + |> add_struct_or_exception(env, type, fields, position, end_position) |> result(ast) end @@ -1948,72 +1950,6 @@ defmodule ElixirSense.Core.MetadataBuilder do defp maybe_add_protocol_behaviour(_, state, env), do: {state, env} - defp add_struct_or_exception(state, type, fields, {line, column} = position, end_position) do - fields = - fields ++ - if type == :defexception do - [__exception__: true] - else - [] - end - - options = [generated: true] - env = get_current_env(state) - - state = - if type == :defexception do - {_, state, env} = add_behaviour(Exception, state, env) - - if Keyword.has_key?(fields, :message) do - state - |> add_func_to_index( - env, - :exception, - [{:msg, [line: line, column: column], nil}], - position, - end_position, - :def, - options - ) - |> add_func_to_index( - env, - :message, - [{:exception, [line: line, column: column], nil}], - position, - end_position, - :def, - options - ) - else - state - end - |> add_func_to_index( - env, - :exception, - [{:args, [line: line, column: column], nil}], - position, - end_position, - :def, - options - ) - else - state - end - |> add_func_to_index(env, :__struct__, [], position, end_position, :def, options) - |> add_func_to_index( - env, - :__struct__, - [{:kv, [line: line, column: column], nil}], - position, - end_position, - :def, - options - ) - - state - |> add_struct(env, type, fields) - end - defp expand_aliases_in_ast(state, ast) do # TODO shouldn't that handle more cases? Macro.prewalk(ast, fn diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index df625f88..f2fcd09b 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -563,7 +563,7 @@ defmodule ElixirSense.Core.State do end) end - def add_struct(%__MODULE__{} = state, %__MODULE__.Env{} = env, type, fields) do + def add_struct(%__MODULE__{} = state, env, type, fields) do structs = state.structs |> Map.put(env.module, %StructInfo{type: type, fields: fields ++ [__struct__: env.module]}) @@ -1976,4 +1976,69 @@ defmodule ElixirSense.Core.State do def with_typespec(%__MODULE__{} = state, typespec) do %{state | typespec: typespec} end + + def add_struct_or_exception(state, env, type, fields, {line, column} = position, end_position) do + fields = + fields ++ + if type == :defexception do + [__exception__: true] + else + [] + end + + options = [generated: true] + + state = + if type == :defexception do + {_, state, env} = add_behaviour(Exception, state, env) + + if Keyword.has_key?(fields, :message) do + state + |> add_func_to_index( + env, + :exception, + [{:msg, [line: line, column: column], nil}], + position, + end_position, + :def, + options + ) + |> add_func_to_index( + env, + :message, + [{:exception, [line: line, column: column], nil}], + position, + end_position, + :def, + options + ) + else + state + end + |> add_func_to_index( + env, + :exception, + [{:args, [line: line, column: column], nil}], + position, + end_position, + :def, + options + ) + else + state + end + |> add_func_to_index(env, :__struct__, [], position, end_position, :def, options) + |> add_func_to_index( + env, + :__struct__, + [{:kv, [line: line, column: column], nil}], + position, + end_position, + :def, + options + ) + + state + |> add_struct(env, type, fields) + end end diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index fa262be9..ca45c3ac 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -12,7 +12,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do @binding_support Version.match?(System.version(), "< 1.17.0-dev") @protocol_support Version.match?(System.version(), "< 1.17.0-dev") @first_alias_positions Version.match?(System.version(), "< 1.17.0-dev") - @struct_support Version.match?(System.version(), "< 1.17.0-dev") @macro_calls_support Version.match?(System.version(), "< 1.17.0-dev") @typespec_calls_support Version.match?(System.version(), "< 1.17.0-dev") @record_support Version.match?(System.version(), "< 1.17.0-dev") @@ -5618,7 +5617,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do end end - if @struct_support do test "use defining struct" do state = """ @@ -5667,7 +5665,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do {MyError, :exception, 1} => %State.ModFunInfo{} } = state.mods_funs_to_positions end - end test "use multi notation" do state = @@ -5742,7 +5739,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do end end - if @struct_support do describe "defstruct" do test "find struct" do state = @@ -5781,6 +5777,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } = state.mods_funs_to_positions end + if @expand_eval do test "find struct fields from expression" do state = """ @@ -5796,6 +5793,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do MyStruct => %StructInfo{type: :defstruct, fields: [__struct__: MyStruct]} } end + end test "find exception" do state = @@ -5885,7 +5883,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } = state.mods_funs_to_positions end end - end describe "calls" do defp sort_calls(calls) do From 1cf683e003fb79650396cc9c4f3f8a82af3c158f Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 2 May 2024 06:57:49 +0200 Subject: [PATCH 016/235] doc and meta --- lib/elixir_sense/core/compiler.ex | 143 +++++++++++++++++- lib/elixir_sense/core/metadata_builder.ex | 13 +- lib/elixir_sense/core/state.ex | 88 ++++------- .../core/metadata_builder_test.exs | 13 +- 4 files changed, 171 insertions(+), 86 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 3039370a..e420cc0f 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -127,6 +127,7 @@ defmodule ElixirSense.Core.Compiler do state = state + |> add_first_alias_positions(env, meta) |> add_current_env_to_line(Keyword.fetch!(meta, :line), env) # no need to call expand_without_aliases_report - we never report @@ -823,6 +824,131 @@ defmodule ElixirSense.Core.Compiler do add_behaviour(arg, state, env) end + defp expand_macro( + meta, + Kernel, + :@, + [{:moduledoc, doc_meta, [arg]}], + _callback, + state, + env + ) do + assert_module_scope(env, :@, 1) + unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") + line = Keyword.fetch!(meta, :line) + + state = + state + |> add_current_env_to_line(line, env) + + {arg, state, env} = expand(arg, state, env) + + state = state + |> add_moduledoc_positions( + env, + meta + ) + |> register_doc(env, :moduledoc, arg) + {{:@, meta, [{:moduledoc, doc_meta, [arg]}]}, state, env} + end + + defp expand_macro( + meta, + Kernel, + :@, + [{doc, doc_meta, [arg]}], + _callback, + state, + env + ) when doc in [:doc, :typedoc] do + assert_module_scope(env, :@, 1) + unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") + line = Keyword.fetch!(meta, :line) + + state = + state + |> add_current_env_to_line(line, env) + + {arg, state, env} = expand(arg, state, env) + + state = state + |> register_doc(env, doc, arg) + {{:@, meta, [{doc, doc_meta, [arg]}]}, state, env} + end + + defp expand_macro( + meta, + Kernel, + :@, + [{:impl, doc_meta, [arg]}], + _callback, + state, + env + ) do + assert_module_scope(env, :@, 1) + unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") + line = Keyword.fetch!(meta, :line) + + state = + state + |> add_current_env_to_line(line, env) + + {arg, state, env} = expand(arg, state, env) + + # impl adds sets :hidden by default + state = state + |> register_doc(env, :doc, :impl) + {{:@, meta, [{:impl, doc_meta, [arg]}]}, state, env} + end + + defp expand_macro( + meta, + Kernel, + :@, + [{:optional_callbacks, doc_meta, [arg]}], + _callback, + state, + env + ) do + assert_module_scope(env, :@, 1) + unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") + line = Keyword.fetch!(meta, :line) + + state = + state + |> add_current_env_to_line(line, env) + + {arg, state, env} = expand(arg, state, env) + + state = state + |> register_optional_callbacks(arg) + {{:@, meta, [{:optional_callbacks, doc_meta, [arg]}]}, state, env} + end + + defp expand_macro( + meta, + Kernel, + :@, + [{:deprecated, doc_meta, [arg]}], + _callback, + state, + env + ) do + assert_module_scope(env, :@, 1) + unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") + line = Keyword.fetch!(meta, :line) + + state = + state + |> add_current_env_to_line(line, env) + + {arg, state, env} = expand(arg, state, env) + + state = state + |> register_doc(env, :doc, deprecated: arg) + {{:@, meta, [{:deprecated, doc_meta, [arg]}]}, state, env} + end + defp expand_macro( attr_meta, Kernel, @@ -1084,6 +1210,10 @@ defmodule ElixirSense.Core.Compiler do # TODO magic with ElixirEnv instead of new_vars_scope? {result, state, _env} = expand(block, state, %{env | module: full}) + + state = state + |> apply_optional_callbacks(%{env | module: full}) + {result, state, env} else raise "unable to expand module alias" @@ -1204,16 +1334,13 @@ defmodule ElixirSense.Core.Compiler do state = state |> add_current_env_to_line(line, %{g_env | context: nil, function: {name, arity}}) - |> add_mod_fun_to_position( - {module, name, arity}, + |> add_func_to_index( + env, + name, + args, position, end_position, - args, - def_kind, - "", - # doc, - %{} - # meta + def_kind ) # expand_macro_callback(meta, Kernel, def_kind, [call, expr], callback, state, env) diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index f59773b4..61507c82 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -698,9 +698,8 @@ defmodule ElixirSense.Core.MetadataBuilder do state |> add_moduledoc_positions( - [line: line, column: column], - [{:moduledoc, meta, [doc_arg]}], - line + env, + meta_attr ) |> register_doc(env, :moduledoc, doc_arg) |> result(new_ast) @@ -926,14 +925,6 @@ defmodule ElixirSense.Core.MetadataBuilder do case binding do {type, is_definition} -> - state = - add_moduledoc_positions( - state, - [line: line, column: column], - [{name, meta, params}], - line - ) - new_ast = {:@, meta_attr, [{name, add_no_call(meta), params}]} state diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index f2fcd09b..47202858 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -461,61 +461,39 @@ defmodule ElixirSense.Core.State do def add_moduledoc_positions( %__MODULE__{} = state, - [line: line, column: column], - [{:moduledoc, _meta, [here_doc]}], - line - ) - when is_integer(line) and is_binary(here_doc) do - module_name = get_current_module(state) - - new_line_count = here_doc |> String.split("\n") |> Enum.count() - line_to_insert_alias = new_line_count + line + 1 - - %__MODULE__{ - state - | moduledoc_positions: - Map.put(state.moduledoc_positions, module_name, {line_to_insert_alias, column}) - } - end - - def add_moduledoc_positions( - %__MODULE__{} = state, - [line: line, column: column], - [{:moduledoc, _meta, [params]}], - line - ) - when is_integer(line) and is_boolean(params) do - module_name = get_current_module(state) - - line_to_insert_alias = line + 1 + env, + meta + ) do + module = env.module + case Keyword.get(meta, :end_of_expression) do + nil -> state + end_of_expression -> + line_to_insert_alias = Keyword.fetch!(end_of_expression, :line) + 1 + column = Keyword.get(meta, :column, 1) - %__MODULE__{ - state - | moduledoc_positions: - Map.put(state.moduledoc_positions, module_name, {line_to_insert_alias, column}) - } + %__MODULE__{ + state + | moduledoc_positions: + Map.put(state.moduledoc_positions, module, {line_to_insert_alias, column}) + } + end end - def add_moduledoc_positions(state, _, _, _), do: state - - def add_first_alias_positions(%__MODULE__{} = state, line, column) - when is_integer(line) and is_integer(column) do - current_scope = hd(state.scopes) - - is_module? = is_atom(current_scope) and current_scope != nil - - if is_module? do - module_name = get_current_module(state) - + def add_first_alias_positions(%__MODULE__{} = state, env = %{module: module, function: nil}, meta) do + # TODO shouldn't that look for end_of_expression + line = Keyword.get(meta, :line, 0) + if line > 0 do + column = Keyword.get(meta, :column, 1) %__MODULE__{ state | first_alias_positions: - Map.put_new(state.first_alias_positions, module_name, {line, column}) + Map.put_new(state.first_alias_positions, module, {line, column}) } else state end end + def add_first_alias_positions(%__MODULE__{} = state, _env, _meta), do: state # TODO remove this def add_call_to_line(%__MODULE__{} = state, {nil, :__block__, _}, _position), do: state @@ -713,7 +691,7 @@ defmodule ElixirSense.Core.State do %{state | optional_callbacks_context: [list | rest]} end - def apply_optional_callbacks(%__MODULE__{} = state, %__MODULE__.Env{} = env) do + def apply_optional_callbacks(%__MODULE__{} = state, env) do [list | _rest] = state.optional_callbacks_context module = env.module @@ -1386,7 +1364,7 @@ defmodule ElixirSense.Core.State do def add_behaviour(_module, %__MODULE__{} = state, env), do: {nil, state, env} - def register_doc(%__MODULE__{} = state, %__MODULE__.Env{} = env, :moduledoc, doc_arg) do + def register_doc(%__MODULE__{} = state, env, :moduledoc, doc_arg) do current_module = env.module doc_arg_formatted = format_doc_arg(doc_arg) @@ -1405,13 +1383,13 @@ defmodule ElixirSense.Core.State do %{state | mods_funs_to_positions: mods_funs_to_positions} end - def register_doc(%__MODULE__{} = state, %__MODULE__.Env{}, :doc, doc_arg) do + def register_doc(%__MODULE__{} = state, _env, :doc, doc_arg) do [doc_context | doc_context_rest] = state.doc_context %{state | doc_context: [[doc_arg | doc_context] | doc_context_rest]} end - def register_doc(%__MODULE__{} = state, %__MODULE__.Env{}, :typedoc, doc_arg) do + def register_doc(%__MODULE__{} = state, _env, :typedoc, doc_arg) do [doc_context | doc_context_rest] = state.typedoc_context %{state | typedoc_context: [[doc_arg | doc_context] | doc_context_rest]} @@ -1640,7 +1618,7 @@ defmodule ElixirSense.Core.State do expand({form, meta, [arg, []]}, state, env) end - def expand(module, %__MODULE__{} = state, %__MODULE__.Env{} = env) when is_atom(module) do + def expand(module, %__MODULE__{} = state, env) when is_atom(module) do {module, state, env} end @@ -1652,7 +1630,7 @@ defmodule ElixirSense.Core.State do # options = expand(no_alias_opts(arg), state, env, env) if is_atom(arg) do - state = add_first_alias_positions(state, line, column) + state = add_first_alias_positions(state, env, meta) alias_tuple = case Keyword.get(opts, :as) do @@ -1754,29 +1732,29 @@ defmodule ElixirSense.Core.State do def expand( {:__aliases__, _, [Elixir | _] = module}, %__MODULE__{} = state, - %__MODULE__.Env{} = env + env ) do {Module.concat(module), state, env} end - def expand({:__MODULE__, _, nil}, %__MODULE__{} = state, %__MODULE__.Env{} = env) do + def expand({:__MODULE__, _, nil}, %__MODULE__{} = state, env) do {env.module, state, env} end def expand( {:__aliases__, _, [{:__MODULE__, _, nil} | rest]}, %__MODULE__{} = state, - %__MODULE__.Env{} = env + env ) do {Module.concat([env.module | rest]), state, env} end - def expand({:__aliases__, _, module}, %__MODULE__{} = state, %__MODULE__.Env{} = env) + def expand({:__aliases__, _, module}, %__MODULE__{} = state, env) when is_list(module) do {Introspection.expand_alias(Module.concat(module), env.aliases), state, env} end - def expand(ast, %__MODULE__{} = state, %__MODULE__.Env{} = env) do + def expand(ast, %__MODULE__{} = state, env) do {ast, state, env} end diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index ca45c3ac..4f57751a 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -6,17 +6,13 @@ defmodule ElixirSense.Core.MetadataBuilderTest do alias ElixirSense.Core.State alias ElixirSense.Core.State.{VarInfo, CallInfo, StructInfo, ModFunInfo, AttributeInfo} - @moduledoc_support Version.match?(System.version(), "< 1.17.0-dev") @attribute_binding_support Version.match?(System.version(), "< 1.17.0-dev") @expand_eval false @binding_support Version.match?(System.version(), "< 1.17.0-dev") @protocol_support Version.match?(System.version(), "< 1.17.0-dev") - @first_alias_positions Version.match?(System.version(), "< 1.17.0-dev") @macro_calls_support Version.match?(System.version(), "< 1.17.0-dev") @typespec_calls_support Version.match?(System.version(), "< 1.17.0-dev") @record_support Version.match?(System.version(), "< 1.17.0-dev") - @doc_support Version.match?(System.version(), "< 1.17.0-dev") - @meta_support Version.match?(System.version(), "< 1.17.0-dev") describe "versioned_vars" do test "in block" do @@ -1253,7 +1249,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert state.scope_ids == [] end - if @moduledoc_support do describe "moduledoc positions" do test "moduledoc heredoc version" do state = @@ -1286,7 +1281,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert %{Outer => {3, 3}} = state.moduledoc_positions end end - end test "module attributes" do state = @@ -5360,7 +5354,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do end end - if @first_alias_positions do test "first_alias_positions" do state = """ @@ -5376,7 +5369,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert %{OuterMod => {2, 3}, OuterMod.InnerMod => {5, 5}} = state.first_alias_positions end - end describe "use" do test "use" do @@ -6986,7 +6978,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state end - if @doc_support do + describe "doc" do test "moduledoc is applied to current module" do state = @@ -7332,9 +7324,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do refute match?(%{hidden: true}, meta) end end - end - if @meta_support do describe "meta" do test "guard" do state = @@ -7399,7 +7389,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert %{meta: %{overridable: true}} = state.mods_funs_to_positions[{Some, :test, 2}] end end - end defp string_to_state(string) do string From 4b5fb8d11ad2fdc205dac9153b88faa945363303 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 3 May 2024 07:26:59 +0200 Subject: [PATCH 017/235] records --- lib/elixir_sense/core/compiler.ex | 57 +++++++++++++-- lib/elixir_sense/core/metadata_builder.ex | 5 +- .../core/metadata_builder_test.exs | 71 +++++++++++++------ 3 files changed, 105 insertions(+), 28 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index e420cc0f..f068feb6 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1150,6 +1150,53 @@ defmodule ElixirSense.Core.Compiler do {{type, meta, [fields]}, state, env} end + defp expand_macro( + meta, + Record, + call, + [name, _] = args, + _callback, + state, + env + ) when call in [:defrecord, :defrecordp] do + assert_no_match_or_guard_scope(env.context, :"{call}/2") + module = assert_module_scope(env, call, 2) + + {position = {line, column}, end_position} = extract_range(meta) + + type = + case call do + :defrecord -> :defmacro + :defrecordp -> :defmacrop + end + + options = [generated: true] + + state = state + |> add_func_to_index( + env, + name, + [{:\\, [], [{:args, [], nil}, []]}], + position, + end_position, + type, + options + ) + |> add_func_to_index( + env, + name, + [{:record, [], nil}, {:args, [], nil}], + position, + end_position, + type, + options + ) + |> add_call_to_line({module, call, length(args)}, {line, column}) + |> add_current_env_to_line(line) + + {{{:., meta, [Record, call]}, meta, args}, state, env} + end + defp expand_macro( meta, Kernel, @@ -1241,10 +1288,10 @@ defmodule ElixirSense.Core.Compiler do {{:__block__, [], []}, state, env} end - defp expand_macro(meta, Kernel, def_kind, [call], _callback, state, env) + defp expand_macro(meta, Kernel, def_kind, [call], callback, state, env) when def_kind in [:defguard, :defguardp] do # transform guard to def with empty body - expand_macro(meta, Kernel, def_kind, [call, {:__block__, [], []}], _callback, state, env) + expand_macro(meta, Kernel, def_kind, [call, {:__block__, [], []}], callback, state, env) end defp expand_macro(meta, Kernel, def_kind, [call, expr], _callback, state, env) @@ -1360,9 +1407,9 @@ defmodule ElixirSense.Core.Compiler do {{name, arity}, state, env} end - defp expand_macro(meta, module, fun, args, callback, state, env) do - expand_macro_callback(meta, module, fun, args, callback, state, env) - end + # defp expand_macro(meta, module, fun, args, callback, state, env) do + # expand_macro_callback(meta, module, fun, args, callback, state, env) + # end defp expand_macro_callback(meta, module, fun, args, callback, state, env) do dbg({module, fun, args}) diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index e4e3d163..0365a32b 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -1109,8 +1109,8 @@ defmodule ElixirSense.Core.MetadataBuilder do is_atom(name) do {position = {line, column}, end_position} = extract_range(meta1) - # TODO pass env - {module, state, _env} = expand(module_expression, state) + env = get_current_env(state) + {module, state, env} = expand(module_expression, state, env) type = case call do @@ -1121,7 +1121,6 @@ defmodule ElixirSense.Core.MetadataBuilder do options = [generated: true] shift = if state.generated, do: 0, else: 1 - env = get_current_env(state) state |> new_named_func(name, 1) diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 934c75b4..0179c871 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -12,7 +12,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do @protocol_support Version.match?(System.version(), "< 1.17.0-dev") @macro_calls_support Version.match?(System.version(), "< 1.17.0-dev") @typespec_calls_support Version.match?(System.version(), "< 1.17.0-dev") - @record_support Version.match?(System.version(), "< 1.17.0-dev") describe "versioned_vars" do test "in block" do @@ -6647,7 +6646,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do end end - if @record_support do + describe "defrecord" do test "defrecord defines record macros" do state = """ @@ -6664,26 +6663,58 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert %{ - {MyRecords, :user, 1} => %ModFunInfo{ - params: [[{:\\, [], [{:args, [], nil}, []]}]], - positions: [{3, 9}], - type: :defmacro - }, - {MyRecords, :user, 2} => %ModFunInfo{ - params: [[{:record, [], nil}, {:args, [], nil}]], - positions: [{3, 9}], - type: :defmacro - }, - {MyRecords, :userp, 1} => %ModFunInfo{type: :defmacrop}, - {MyRecords, :my_rec, 1} => %ModFunInfo{type: :defmacro} - } = state.mods_funs_to_positions + {MyRecords, :user, 1} => %ModFunInfo{ + params: [[{:\\, [], [{:args, [], nil}, []]}]], + positions: [{3, 10}], + type: :defmacro + }, + {MyRecords, :user, 2} => %ModFunInfo{ + params: [[{:record, [], nil}, {:args, [], nil}]], + positions: [{3, 10}], + type: :defmacro + }, + {MyRecords, :userp, 1} => %ModFunInfo{type: :defmacrop}, + {MyRecords, :my_rec, 1} => %ModFunInfo{type: :defmacro} + } = state.mods_funs_to_positions assert %{ - {MyRecords, :user, 0} => %State.TypeInfo{ - name: :user, - specs: ["@type user :: record(:user, name: String.t(), age: integer)"] - } - } = state.types + {MyRecords, :user, 0} => %State.TypeInfo{ + name: :user, + specs: ["@type user :: record(:user, name: String.t(), age: integer)"] + } + } = state.types + end + + test "defrecord imported defines record macros" do + state = + """ + defmodule MyRecords do + import Record + defrecord(:user, name: "meg", age: "25") + @type user :: record(:user, name: String.t(), age: integer) + end + """ + |> string_to_state + + assert %{ + {MyRecords, :user, 1} => %ModFunInfo{ + params: [[{:\\, [], [{:args, [], nil}, []]}]], + positions: [{3, 3}], + type: :defmacro + }, + {MyRecords, :user, 2} => %ModFunInfo{ + params: [[{:record, [], nil}, {:args, [], nil}]], + positions: [{3, 3}], + type: :defmacro + } + } = state.mods_funs_to_positions + + assert %{ + {MyRecords, :user, 0} => %State.TypeInfo{ + name: :user, + specs: ["@type user :: record(:user, name: String.t(), age: integer)"] + } + } = state.types end end From 05f8a6c3e4fc4b03a69a3f9a938a033db5577755 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 3 May 2024 18:36:19 +0200 Subject: [PATCH 018/235] protocols --- lib/elixir_sense/core/compiler.ex | 218 ++++- lib/elixir_sense/core/metadata_builder.ex | 77 +- lib/elixir_sense/core/state.ex | 98 +- .../core/metadata_builder_test.exs | 848 ++++++++++-------- 4 files changed, 759 insertions(+), 482 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index f068feb6..982cd85e 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -949,6 +949,64 @@ defmodule ElixirSense.Core.Compiler do {{:@, meta, [{:deprecated, doc_meta, [arg]}]}, state, env} end + defp expand_macro( + meta, + Kernel, + :@, + [{:derive, doc_meta, [derived_protos]}], + _callback, + state, + env + ) do + current_module = assert_module_scope(env, :@, 1) + unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") + + line = Keyword.fetch!(meta, :line) + column = Keyword.fetch!(meta, :column) + + state = List.wrap(derived_protos) + |> Enum.map(fn + {proto, _opts} -> proto + proto -> proto + end) + |> Enum.reduce(state, fn proto, acc -> + case expand(proto, acc, env) do + {proto_module, acc, _env} when is_atom(proto_module) -> + # protocol implementation module for Any + mod_any = Module.concat(proto_module, Any) + + # protocol implementation module built by @derive + mod = Module.concat(proto_module, current_module) + + case acc.mods_funs_to_positions[{mod_any, nil, nil}] do + nil -> + # implementation for: Any not detected (is in other file etc.) + acc + |> add_module_to_index(mod, {line, column}, nil, generated: true) + + _any_mods_funs -> + # copy implementation for: Any + copied_mods_funs_to_positions = + for {{module, fun, arity}, val} <- acc.mods_funs_to_positions, + module == mod_any, + into: %{}, + do: {{mod, fun, arity}, val} + + %{ + acc + | mods_funs_to_positions: + acc.mods_funs_to_positions |> Map.merge(copied_mods_funs_to_positions) + } + end + + :error -> + acc + end + end) + + {{:@, meta, [{:derive, doc_meta, [derived_protos]}]}, state, env} + end + defp expand_macro( attr_meta, Kernel, @@ -1197,6 +1255,109 @@ defmodule ElixirSense.Core.Compiler do {{{:., meta, [Record, call]}, meta, args}, state, env} end + defp expand_macro( + meta, + Kernel, + :defprotocol, + [alias, [do: block]] = args, + callback, + state, + env + ) do + {position, end_position} = extract_range(meta) + original_env = env + # expand the macro normally + {ast, state, env} = expand_macro_callback!(meta, Kernel, :defprotocol, args, callback, state, env) + + [module] = env.context_modules -- original_env.context_modules + # add behaviour_info builtin + # generate callbacks as macro expansion currently fails + state = state + |> add_func_to_index( + %{env | module: module}, + :behaviour_info, + [:atom], + position, + end_position, + :def, + [generated: true] + ) + |> generate_protocol_callbacks(%{env | module: module}) + {ast, state, env} + end + + defp expand_macro( + meta, + Kernel, + :defimpl, + [name, do_block], + callback, + state, + env + ) do + expand_macro( + meta, + Kernel, + :defimpl, + [name, [], do_block], + callback, + state, + env + ) + end + + defp expand_macro( + meta, + Kernel, + :defimpl, + [name, opts, do_block], + callback, + state, + env + ) do + opts = Keyword.merge(opts, do_block) + + {for, opts} = + Keyword.pop_lazy(opts, :for, fn -> + env.module || + raise ArgumentError, "defimpl/3 expects a :for option when declared outside a module" + end) + + # TODO elixir uses expand_literals here + {for, state, _env} = expand(for, state, %{env | module: env.module || Elixir, function: {:__impl__, 1}}) + {protocol, state, _env} = expand(name, state, env) + + impl = fn protocol, for, block, state, env -> + name = Module.concat(protocol, for) + expand_macro( + meta, + Kernel, + :defmodule, + [name, [do: block]], + callback, + state, + env + ) + end + + block = case opts do + [] -> raise ArgumentError, "defimpl expects a do-end block" + [do: block] -> block + _ -> raise ArgumentError, "unknown options given to defimpl, got: #{Macro.to_string(opts)}" + end + + for_wrapped = for + |> List.wrap + + {ast, state, env} = for_wrapped + |> Enum.reduce({[], state, env}, fn for, {acc, state, env} -> + {ast, state, env} = impl.(protocol, for, block, %{state | protocol: {protocol, for_wrapped}}, env) + {[ast | acc], state, env} + end) + + {Enum.reverse(ast), %{state | protocol: nil}, env} + end + defp expand_macro( meta, Kernel, @@ -1247,15 +1408,22 @@ defmodule ElixirSense.Core.Compiler do line = Keyword.fetch!(meta, :line) + module_functions = case state.protocol do + nil -> [] + _ -> [{:__impl__, [:atom], :def}] + end + state = state |> add_module_to_index(full, position, end_position, []) |> add_current_env_to_line(line, %{env | module: full}) - |> add_module_functions(%{env | module: full}, [], position, end_position) + |> add_module_functions(%{env | module: full}, module_functions, position, end_position) |> new_vars_scope |> new_attributes_scope # TODO magic with ElixirEnv instead of new_vars_scope? + {state, _env} = maybe_add_protocol_behaviour(state, %{env | module: full}) + {result, state, _env} = expand(block, state, %{env | module: full}) state = state @@ -1263,7 +1431,7 @@ defmodule ElixirSense.Core.Compiler do {result, state, env} else - raise "unable to expand module alias" + raise "unable to expand module alias #{inspect(expanded)}" # alias |> dbg # keys = state |> Map.from_struct() |> Map.take([:vars, :unused]) # keys |> dbg(limit: :infinity) @@ -1288,8 +1456,14 @@ defmodule ElixirSense.Core.Compiler do {{:__block__, [], []}, state, env} end + defp expand_macro(meta, Protocol, :def, [{name, _, args = [_ | _]} = call], callback, state, env) when is_atom(name) do + # transform protocol def to def with empty body + {ast, state, env} = expand_macro(meta, Kernel, :def, [call, {:__block__, [], []}], callback, state, env) + {ast, state, env} + end + defp expand_macro(meta, Kernel, def_kind, [call], callback, state, env) - when def_kind in [:defguard, :defguardp] do + when def_kind in [:def, :defp, :defmacro, :defmacrop, :defguard, :defguardp] do # transform guard to def with empty body expand_macro(meta, Kernel, def_kind, [call, {:__block__, [], []}], callback, state, env) end @@ -1326,8 +1500,8 @@ defmodule ElixirSense.Core.Compiler do else {call, expr} end - dbg(call) - dbg(expr) + # dbg(call) + # dbg(expr) # state = # state @@ -1407,9 +1581,9 @@ defmodule ElixirSense.Core.Compiler do {{name, arity}, state, env} end - # defp expand_macro(meta, module, fun, args, callback, state, env) do - # expand_macro_callback(meta, module, fun, args, callback, state, env) - # end + defp expand_macro(meta, module, fun, args, callback, state, env) do + expand_macro_callback(meta, module, fun, args, callback, state, env) + end defp expand_macro_callback(meta, module, fun, args, callback, state, env) do dbg({module, fun, args}) @@ -1428,6 +1602,13 @@ defmodule ElixirSense.Core.Compiler do end end + defp expand_macro_callback!(meta, module, fun, args, callback, state, env) do + dbg({module, fun, args}) + ast = callback.(meta, args) + {ast, state, env} = expand(ast, state, env) + {ast, state, env} + end + defp extract_range(meta) do line = Keyword.get(meta, :line, 0) @@ -2356,15 +2537,22 @@ defmodule ElixirSense.Core.Compiler do # TODO elixir does it like that, is it a bug? we lose state # end_s = %{after_s | prematch: prematch, unused: new_unused, vars: new_current} - end_s = %{s_expr | prematch: prematch, unused: new_unused, vars: new_current} + end_s = %{s_expr | + prematch: prematch, unused: new_unused, vars: new_current, + mods_funs_to_positions: after_s.mods_funs_to_positions, + types: after_s.types, + specs: after_s.specs, + structs: after_s.structs, + calls: after_s.calls + } - dbg(hd(before_s.scope_vars_info)) - dbg(hd(after_s.scope_vars_info)) - dbg(hd(end_s.scope_vars_info)) + # dbg(hd(before_s.scope_vars_info)) + # dbg(hd(after_s.scope_vars_info)) + # dbg(hd(end_s.scope_vars_info)) - dbg(current) - dbg(read) - dbg(new_current) + # dbg(current) + # dbg(read) + # dbg(new_current) # TODO I'm not sure this is correct merged_vars = (hd(end_s.scope_vars_info) -- hd(after_s.scope_vars_info)) diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index 0365a32b..1041fe28 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -209,79 +209,6 @@ defmodule ElixirSense.Core.MetadataBuilder do pre_module(ast, state, meta, module, @protocol_types, @protocol_functions) end - def post_protocol(ast, state) do - # turn specs into callbacks or create dummy callbacks - builtins = BuiltinFunctions.all() |> Keyword.keys() - - current_module = get_current_module(state) - - keys = - state.mods_funs_to_positions - |> Map.keys() - |> Enum.filter(fn - {^current_module, name, _arity} when not is_nil(name) -> - name not in builtins - - _ -> - false - end) - - new_specs = - for key = {_mod, name, _arity} <- keys, - into: %{}, - do: - ( - new_spec = - case state.specs[key] do - nil -> - %State.ModFunInfo{positions: positions, params: params} = - state.mods_funs_to_positions[key] - - args = - for param_variant <- params do - param_variant - |> Enum.map(&Macro.to_string/1) - end - - specs = - for arg <- args do - joined = Enum.join(arg, ", ") - "@callback #{name}(#{joined}) :: term" - end - - %State.SpecInfo{ - name: name, - args: args, - specs: specs, - kind: :callback, - positions: positions, - end_positions: Enum.map(positions, fn _ -> nil end), - generated: Enum.map(positions, fn _ -> true end) - } - - spec = %State.SpecInfo{specs: specs} -> - %State.SpecInfo{ - spec - | # TODO :spec will get replaced here, refactor into array - kind: :callback, - specs: - specs - |> Enum.map(fn s -> - String.replace_prefix(s, "@spec", "@callback") - end) - |> Kernel.++(specs) - } - end - - {key, new_spec} - ) - - specs = Map.merge(state.specs, new_specs) - - state = %{state | specs: specs} - post_module(ast, state) - end - defp pre_func({type, meta, ast_args}, state, meta, name, params, options \\ []) when is_atom(name) do vars = @@ -1330,7 +1257,9 @@ defmodule ElixirSense.Core.MetadataBuilder do {:defprotocol, _meta, [_protocol, _]} = ast, state ) do - post_protocol(ast, state) + env = get_current_env(state) + state = generate_protocol_callbacks(state, env) + post_module(ast, state) end defp post( diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index ac3f6932..f2b2c9d1 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -4,6 +4,7 @@ defmodule ElixirSense.Core.State do """ alias ElixirSense.Core.Introspection + alias ElixirSense.Core.BuiltinFunctions require Logger @type fun_arity :: {atom, non_neg_integer} @@ -62,7 +63,8 @@ defmodule ElixirSense.Core.State do # TODO better type binding_context: list, macro_env: list(Macro.Env.t()), - typespec: nil | {atom, arity} + typespec: nil | {atom, arity}, + protocol: nil | {atom, [atom]} } @auto_imported_functions :elixir_env.new().functions @@ -110,7 +112,8 @@ defmodule ElixirSense.Core.State do optional_callbacks_context: [[]], moduledoc_positions: %{}, macro_env: [:elixir_env.new()], - typespec: nil + typespec: nil, + protocol: nil defmodule Env do @moduledoc """ @@ -376,7 +379,6 @@ defmodule ElixirSense.Core.State do current_behaviours = state.behaviours |> Map.get(macro_env.module, []) current_scope_id = hd(state.scope_ids) - current_scope_protocol = hd(state.protocols) # Macro.Env versioned_vars is not updated # versioned_vars: macro_env.versioned_vars, @@ -393,6 +395,15 @@ defmodule ElixirSense.Core.State do # dbg(vars) # dbg(state.vars) # dbg(state.scope_vars_info) + + current_protocol = case state.protocol do + nil -> nil + {protocol, for_list} -> + # check wether we are in implementation or implementation child module + if Enum.any?(for_list, fn for -> macro_env.module == Module.concat(protocol, for) end) do + {protocol, for_list} + end + end %Env{ @@ -408,7 +419,7 @@ defmodule ElixirSense.Core.State do behaviours: current_behaviours, typespec: state.typespec, scope_id: current_scope_id, - protocol: current_scope_protocol + protocol: current_protocol } end @@ -2051,4 +2062,83 @@ defmodule ElixirSense.Core.State do state |> add_struct(env, type, fields) end + + def generate_protocol_callbacks(state, env) do + # turn specs into callbacks or create dummy callbacks + builtins = BuiltinFunctions.all() |> Keyword.keys() + + current_module = env.module + + keys = + state.mods_funs_to_positions + |> Map.keys() + |> Enum.filter(fn + {^current_module, name, _arity} when not is_nil(name) -> + name not in builtins + + _ -> + false + end) + + new_specs = + for key = {_mod, name, _arity} <- keys, + into: %{}, + do: + ( + new_spec = + case state.specs[key] do + nil -> + %ModFunInfo{positions: positions, params: params} = + state.mods_funs_to_positions[key] + + args = + for param_variant <- params do + param_variant + |> Enum.map(&Macro.to_string/1) + end + + specs = + for arg <- args do + joined = Enum.join(arg, ", ") + "@callback #{name}(#{joined}) :: term" + end + + %SpecInfo{ + name: name, + args: args, + specs: specs, + kind: :callback, + positions: positions, + end_positions: Enum.map(positions, fn _ -> nil end), + generated: Enum.map(positions, fn _ -> true end) + } + + spec = %SpecInfo{specs: specs} -> + %SpecInfo{ + spec + | # TODO :spec will get replaced here, refactor into array + kind: :callback, + specs: + specs + |> Enum.map(fn s -> + String.replace_prefix(s, "@spec", "@callback") + end) + |> Kernel.++(specs) + } + end + + {key, new_spec} + ) + + specs = Map.merge(state.specs, new_specs) + + %{state | specs: specs} + end + + def maybe_add_protocol_behaviour(%{protocol: {protocol, _}} = state, env) do + {_, state, env} = add_behaviour(protocol, state, env) + {state, env} + end + + def maybe_add_protocol_behaviour(state, env), do: {state, env} end diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 0179c871..6e7c0f36 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -9,7 +9,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do @attribute_binding_support Version.match?(System.version(), "< 1.17.0-dev") @expand_eval false @binding_support Version.match?(System.version(), "< 1.17.0-dev") - @protocol_support Version.match?(System.version(), "< 1.17.0-dev") @macro_calls_support Version.match?(System.version(), "< 1.17.0-dev") @typespec_calls_support Version.match?(System.version(), "< 1.17.0-dev") @@ -4063,6 +4062,24 @@ defmodule ElixirSense.Core.MetadataBuilderTest do {functions, _} = get_line_imports(state, 4) assert Keyword.has_key?(functions, Enum) end + + test "imports inside protocol" do + state = + """ + defprotocol OuterModule do + IO.puts "" + end + """ + |> string_to_state + + {_functions, macros} = get_line_imports(state, 2) + assert Keyword.keys(macros) == [Protocol, Kernel] + kernel_macros = Keyword.fetch!(macros, Kernel) + assert {:def, 1} not in kernel_macros + assert {:defmacro, 1} not in kernel_macros + assert {:defdelegate, 2} not in kernel_macros + assert {:def, 1} in Keyword.fetch!(macros, Protocol) + end end describe "require" do @@ -4467,316 +4484,351 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert get_line_protocol(state, 16) == nil end - if @protocol_support do - test "current module and protocol implementation" do - state = - """ - defprotocol My.Reversible do - def reverse(term) - IO.puts "" - end - - defimpl My.Reversible, for: String do - def reverse(term), do: String.reverse(term) - IO.puts "" - end - - defimpl My.Reversible, for: [Map, My.List] do - def reverse(term), do: Enum.reverse(term) - IO.puts "" - - defmodule OuterModule do - IO.puts "" - end - - defprotocol Other do - def other(term) - IO.puts "" - end - - defimpl Other, for: [Map, My.Map] do - def other(term), do: nil - IO.inspect(__ENV__.module) - end - end - """ - |> string_to_state - - # protocol and implementations create modules - assert get_line_module(state, 3) == My.Reversible - assert get_line_protocol(state, 3) == nil - assert get_line_module(state, 8) == My.Reversible.String - assert get_line_protocol(state, 8) == {My.Reversible, [String]} - assert get_line_module(state, 13) == My.Reversible.Map - assert get_line_protocol(state, 13) == {My.Reversible, [Map, My.List]} + test "current module and protocol" do + state = + """ + defprotocol My.Reversible do + def reverse(term) + IO.puts "" + end + """ + |> string_to_state - # implementation has behaviour - assert get_line_behaviours(state, 8) == [My.Reversible] + # protocol and implementations create modules + assert get_line_module(state, 3) == My.Reversible + assert get_line_protocol(state, 3) == nil + end - # multiple implementations create multiple modules - assert get_line_module(state, 16) == My.Reversible.Map.OuterModule + test "current module and protocol implementation - simple case" do + state = """ + defimpl Inspect, for: Atom do + IO.puts("") + end + """ + |> string_to_state - assert get_line_protocol(state, 16) == nil + assert get_line_module(state, 2) == Inspect.Atom + assert get_line_protocol(state, 2) == {Inspect, [Atom]} + end - # protocol and implementations inside protocol implementation creates a cross product - assert get_line_module(state, 21) == My.Reversible.Map.Other - assert get_line_protocol(state, 21) == nil + test "current module and protocol implementation" do + state = + """ + defprotocol My.Reversible do + def reverse(term) + IO.puts "" + end - assert get_line_module(state, 26) == My.Reversible.Map.Other.Map + defimpl My.Reversible, for: String do + def reverse(term), do: String.reverse(term) + IO.puts "" + end - assert get_line_protocol(state, 26) == {My.Reversible.Map.Other, [Map, My.Map]} - end - end - end + defimpl My.Reversible, for: [Map, My.List] do + def reverse(term), do: Enum.reverse(term) + IO.puts "" - if @protocol_support do - describe "protocol implementation" do - test "protocol implementation for atom modules" do - state = - """ - defprotocol :my_reversible do - def reverse(term) + defmodule OuterModule do IO.puts "" end - defimpl :my_reversible, for: [String, :my_str, :"Elixir.MyStr"] do - def reverse(term), do: String.reverse(term) + defprotocol Other do + def other(term) IO.puts "" end - defprotocol :"Elixir.My.Reversible" do - def reverse(term) - IO.puts "" + defimpl Other, for: [Map, My.Map] do + def other(term), do: nil + IO.inspect(__ENV__.module) end + end + """ + |> string_to_state - defimpl :"Elixir.My.Reversible", for: [String, :my_str, :"Elixir.MyStr"] do - def reverse(term), do: String.reverse(term) - IO.puts "" - end - """ - |> string_to_state + # protocol and implementations create modules + assert get_line_module(state, 3) == My.Reversible + assert get_line_protocol(state, 3) == nil + assert get_line_module(state, 8) == My.Reversible.String + assert get_line_protocol(state, 8) == {My.Reversible, [String]} + assert get_line_module(state, 13) == My.Reversible.My.List + assert get_line_protocol(state, 13) == {My.Reversible, [Map, My.List]} - assert get_line_module(state, 3) == :my_reversible - assert get_line_protocol(state, 3) == nil + # implementation has behaviour + assert get_line_behaviours(state, 8) == [My.Reversible] - assert get_line_module(state, 8) == :"Elixir.my_reversible.String" + # multiple implementations create multiple modules + assert get_line_module(state, 16) == My.Reversible.My.List.OuterModule - assert get_line_protocol(state, 8) == {:my_reversible, [String, :my_str, MyStr]} + assert get_line_protocol(state, 16) == nil - assert get_line_module(state, 13) == My.Reversible - assert get_line_protocol(state, 13) == nil + # protocol and implementations inside protocol implementation creates a cross product + assert get_line_module(state, 21) == My.Reversible.Map.Other + assert get_line_protocol(state, 21) == nil - assert get_line_module(state, 18) == My.Reversible.String + assert get_line_module(state, 26) == My.Reversible.My.List.Other.My.Map - assert get_line_protocol(state, 18) == {My.Reversible, [String, :my_str, MyStr]} - end + assert get_line_protocol(state, 26) == {My.Reversible.My.List.Other, [Map, My.Map]} + end + end - test "protocol implementation module naming rules" do - state = - """ - defprotocol NiceProto do - def reverse(term) - end + describe "protocol implementation" do + test "protocol implementation for atom modules" do + state = + """ + defprotocol :my_reversible do + def reverse(term) + IO.puts "" + end - defmodule NiceProtoImplementations do - defimpl NiceProto, for: String do - def reverse(term), do: String.reverse(term) - def a3, do: IO.puts "OuterModule " <> inspect(__ENV__.aliases) - end - def a3, do: IO.puts "OuterModule " <> inspect(__ENV__.aliases) + defimpl :my_reversible, for: [String, :my_str, :"Elixir.MyStr"] do + def reverse(term), do: String.reverse(term) + IO.puts "" + end - defmodule Some do - defstruct [a: nil] - end + defprotocol :"Elixir.My.Reversible" do + def reverse(term) + IO.puts "" + end - defimpl NiceProto, for: Some do - def reverse(term), do: String.reverse(term) - IO.inspect(__ENV__.module) - end + defimpl :"Elixir.My.Reversible", for: [String, :my_str, :"Elixir.MyStr"] do + def reverse(term), do: String.reverse(term) + IO.puts "" + end + """ + |> string_to_state - alias Enumerable.Date.Range, as: R - alias NiceProto, as: N + assert get_line_module(state, 3) == :my_reversible + assert get_line_protocol(state, 3) == nil - defimpl N, for: R do - def reverse(term), do: String.reverse(term) - IO.puts "" - end - end - """ - |> string_to_state + assert get_line_module(state, 8) == :"Elixir.my_reversible.MyStr" - # protocol implementation module name does not inherit enclosing module, only protocol - assert get_line_module(state, 8) == NiceProto.String - assert get_line_protocol(state, 8) == {NiceProto, [String]} - assert get_line_aliases(state, 8) == [] - assert get_line_aliases(state, 10) == [] + assert get_line_protocol(state, 8) == {:my_reversible, [String, :my_str, MyStr]} - # properly gets implementation name inherited from enclosing module - assert get_line_module(state, 18) == NiceProto.NiceProtoImplementations.Some - assert get_line_protocol(state, 18) == {NiceProto, [NiceProtoImplementations.Some]} + assert get_line_module(state, 13) == My.Reversible + assert get_line_protocol(state, 13) == nil - # aliases are expanded on protocol and implementation - assert get_line_module(state, 24) == NiceProto.Enumerable.Date.Range - assert get_line_protocol(state, 24) == {NiceProto, [Enumerable.Date.Range]} - end + assert get_line_module(state, 18) == My.Reversible.MyStr - test "protocol implementation using __MODULE__" do - state = - """ - defprotocol NiceProto do - def reverse(term) + assert get_line_protocol(state, 18) == {My.Reversible, [String, :my_str, MyStr]} + end + + test "protocol implementation module naming rules" do + state = + """ + defprotocol NiceProto do + def reverse(term) + end + + defmodule NiceProtoImplementations do + defimpl NiceProto, for: String do + def reverse(term), do: String.reverse(term) + def a3, do: IO.puts "OuterModule " <> inspect(__ENV__.aliases) end + def a3, do: IO.puts "OuterModule " <> inspect(__ENV__.aliases) - defmodule MyStruct do + defmodule Some do defstruct [a: nil] - - defimpl NiceProto, for: __MODULE__ do - def reverse(term), do: String.reverse(term) - end end - """ - |> string_to_state - # protocol implementation module name does not inherit enclosing module, only protocol - assert get_line_module(state, 8) == NiceProto.MyStruct - assert get_line_protocol(state, 8) == {NiceProto, [MyStruct]} - end + defimpl NiceProto, for: Some do + def reverse(term), do: String.reverse(term) + IO.inspect(__ENV__.module) + end - test "protocol implementation using __MODULE__ 2" do - state = - """ - defmodule Nice do - defprotocol Proto do - def reverse(term) - end + alias Enumerable.Date.Range, as: R + alias NiceProto, as: N - defimpl __MODULE__.Proto, for: String do - def reverse(term), do: String.reverse(term) - end + defimpl N, for: R do + def reverse(term), do: String.reverse(term) + IO.puts "" end - """ - |> string_to_state + end + """ + |> string_to_state - assert get_line_module(state, 7) == Nice.Proto.String - assert get_line_protocol(state, 7) == {Nice.Proto, [String]} - end + # protocol implementation module name does not inherit enclosing module, only protocol + assert get_line_module(state, 8) == NiceProto.String + assert get_line_protocol(state, 8) == {NiceProto, [String]} + assert get_line_aliases(state, 8) == [] + assert get_line_aliases(state, 10) == [] - test "protocol implementation for structs does not require for" do - state = - """ - defprotocol Proto do - def reverse(term) - end + # properly gets implementation name inherited from enclosing module + assert get_line_module(state, 18) == NiceProto.NiceProtoImplementations.Some + assert get_line_protocol(state, 18) == {NiceProto, [NiceProtoImplementations.Some]} - defmodule MyStruct do - defstruct [:field] + # aliases are expanded on protocol and implementation + assert get_line_module(state, 24) == NiceProto.Enumerable.Date.Range + assert get_line_protocol(state, 24) == {NiceProto, [Enumerable.Date.Range]} + end - defimpl Proto do - def reverse(term), do: String.reverse(term) - end + test "protocol implementation using __MODULE__" do + state = + """ + defprotocol NiceProto do + def reverse(term) + end + + defmodule MyStruct do + defstruct [a: nil] + + defimpl NiceProto, for: __MODULE__ do + def reverse(term), do: String.reverse(term) end - """ - |> string_to_state + end + """ + |> string_to_state - assert get_line_module(state, 9) == Proto.MyStruct - assert get_line_protocol(state, 9) == {Proto, [MyStruct]} - end + # protocol implementation module name does not inherit enclosing module, only protocol + assert get_line_module(state, 8) == NiceProto.MyStruct + assert get_line_protocol(state, 8) == {NiceProto, [MyStruct]} + end - test "protocol implementation by deriving" do - state = - """ + test "protocol implementation using __MODULE__ 2" do + state = + """ + defmodule Nice do defprotocol Proto do def reverse(term) end - defimpl Proto, for: Any do - def reverse(term), do: term + defimpl __MODULE__.Proto, for: String do + def reverse(term), do: String.reverse(term) end + end + """ + |> string_to_state - defmodule MyStruct do - @derive Proto - defstruct [:field] - IO.puts "" - end - IO.puts "" + assert get_line_module(state, 7) == Nice.Proto.String + assert get_line_protocol(state, 7) == {Nice.Proto, [String]} + end + + test "protocol implementation for structs does not require for" do + state = + """ + defprotocol Proto do + def reverse(term) + end + + defmodule MyStruct do + defstruct [:field] - defmodule MyOtherStruct do - @derive [{Proto, opt: 1}, Enumerable] - defstruct [:field] + defimpl Proto do + def reverse(term), do: String.reverse(term) end - """ - |> string_to_state + end + """ + |> string_to_state - assert %{ - {Enumerable.MyOtherStruct, nil, nil} => %ModFunInfo{ - params: [nil], - type: :defmodule - }, - {Proto.Any, nil, nil} => %ModFunInfo{ - params: [nil], - type: :defmodule - }, - {Proto.MyOtherStruct, nil, nil} => %ModFunInfo{ - params: [nil], - type: :defmodule - }, - {Proto.MyOtherStruct, :reverse, 1} => %ModFunInfo{ - params: [[{:term, [line: 6, column: 15], nil}]], - type: :def - }, - {Proto.MyStruct, nil, nil} => %ModFunInfo{ - params: [nil], - type: :defmodule - }, - {Proto.MyStruct, :reverse, 1} => %ModFunInfo{ - params: [[{:term, [line: 6, column: 15], nil}]], - type: :def - } - } = state.mods_funs_to_positions - end + assert get_line_module(state, 9) == Proto.MyStruct + assert get_line_protocol(state, 9) == {Proto, [MyStruct]} end - test "protocol registers callbacks from specs or generate dummy callbacks" do + test "protocol implementation by deriving" do state = """ defprotocol Proto do - @spec with_spec(t, integer) :: String.t - @spec with_spec(t, boolean) :: number - def with_spec(t, integer) + def reverse(term) + end - def without_spec(t, integer) + defimpl Proto, for: Any do + def reverse(term), do: term + end + + defmodule MyStruct do + @derive Proto + defstruct [:field] + IO.puts "" + end + IO.puts "" + + defmodule MyOtherStruct do + @derive [{Proto, opt: 1}, Enumerable] + defstruct [:field] end """ |> string_to_state - assert state.specs == %{ - {Proto, :with_spec, 2} => %ElixirSense.Core.State.SpecInfo{ - args: [["t", "boolean"], ["t", "integer"]], - kind: :callback, - name: :with_spec, - positions: [{3, 3}, {2, 3}], - end_positions: [{3, 40}, {2, 42}], - generated: [false, false], - specs: [ - "@callback with_spec(t, boolean) :: number", - "@callback with_spec(t, integer) :: String.t()", - "@spec with_spec(t, boolean) :: number", - "@spec with_spec(t, integer) :: String.t()" - ] - }, - {Proto, :without_spec, 2} => %ElixirSense.Core.State.SpecInfo{ - args: [["t", "integer"]], - kind: :callback, - name: :without_spec, - positions: [{6, 3}], - end_positions: [nil], - generated: [true], - specs: ["@callback without_spec(t, integer) :: term"] - } - } + assert %{ + {Enumerable.MyOtherStruct, nil, nil} => %ModFunInfo{ + params: [nil], + type: :defmodule + }, + {Proto.Any, nil, nil} => %ModFunInfo{ + params: [nil], + type: :defmodule + }, + {Proto.MyOtherStruct, nil, nil} => %ModFunInfo{ + params: [nil], + type: :defmodule + }, + {Proto.MyOtherStruct, :reverse, 1} => %ModFunInfo{ + params: [[{:term, [line: 6, column: 15], nil}]], + type: :def + }, + {Proto.MyStruct, nil, nil} => %ModFunInfo{ + params: [nil], + type: :defmodule + }, + {Proto.MyStruct, :reverse, 1} => %ModFunInfo{ + params: [[{:term, [line: 6, column: 15], nil}]], + type: :def + } + } = state.mods_funs_to_positions end end + test "protocol registers callbacks from specs or generate dummy callbacks" do + state = + """ + defprotocol Proto do + @spec with_spec(t, integer) :: String.t + @spec with_spec(t, boolean) :: number + def with_spec(t, integer) + + def without_spec(t, integer) + end + """ + |> string_to_state + + assert %{ + {Proto, :with_spec, 2} => %ElixirSense.Core.State.SpecInfo{ + args: [["t", "boolean"], ["t", "integer"]], + kind: :callback, + name: :with_spec, + positions: [{3, 3}, {2, 3}], + end_positions: [{3, 40}, {2, 42}], + generated: [false, false], + specs: [ + "@callback with_spec(t, boolean) :: number", + "@callback with_spec(t, integer) :: String.t()", + "@spec with_spec(t, boolean) :: number", + "@spec with_spec(t, integer) :: String.t()" + ] + }, + {Proto, :without_spec, 2} => %ElixirSense.Core.State.SpecInfo{ + args: [["t", "integer"]], + kind: :callback, + name: :without_spec, + positions: [{6, 3}], + end_positions: [nil], + generated: [true], + specs: ["@callback without_spec(t, integer) :: term"] + }, + {Proto, :__protocol__, 1} => %ElixirSense.Core.State.SpecInfo{ + kind: :spec, + specs: ["@spec __protocol__(:impls) :: :not_consolidated | {:consolidated, [module]}", "@spec __protocol__(:consolidated?) :: boolean", "@spec __protocol__(:functions) :: unquote(Protocol.__functions_spec__(@__functions__))", "@spec __protocol__(:module) :: Proto"] + }, + {Proto, :impl_for, 1} => %ElixirSense.Core.State.SpecInfo{ + kind: :spec, + specs: ["@spec impl_for(term) :: atom | nil"] + }, + {Proto, :impl_for!, 1} => %ElixirSense.Core.State.SpecInfo{ + kind: :spec, + specs: ["@spec impl_for!(term) :: atom"] + } + } = state.specs + end + test "registers positions" do state = """ @@ -4813,54 +4865,86 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } = state.mods_funs_to_positions end - if @protocol_support do - test "registers positions in protocol implementation" do - state = - """ - defprotocol Reversible do - def reverse(term) - IO.puts "" - end + test "registers def positions in protocol" do + state = + """ + defprotocol Reversible do + def reverse(term) + IO.puts "" + end + """ + |> string_to_state - defimpl Reversible, for: String do - def reverse(term), do: String.reverse(term) + assert %{ + {Reversible, :reverse, 1} => %ModFunInfo{ + params: [[{:term, [line: 2, column: 15], nil}]], + positions: [{2, 3}], + type: :def + } + } = state.mods_funs_to_positions + end + + test "registers def positions in protocol implementation" do + state = + """ + defprotocol Reversible do + def reverse(term) + IO.puts "" + end + + defimpl Reversible, for: String do + def reverse(term), do: String.reverse(term) + IO.puts "" + end + + defmodule Impls do + alias Reversible, as: R + alias My.List, as: Ml + defimpl R, for: [Map, Ml] do + def reverse(term), do: Enum.reverse(term) IO.puts "" end + end + """ + |> string_to_state - defmodule Impls do - alias Reversible, as: R - alias My.List, as: Ml - defimpl R, for: [Map, Ml] do - def reverse(term), do: Enum.reverse(term) - IO.puts "" - end - end - """ - |> string_to_state + assert %{ + {Impls, nil, nil} => %ModFunInfo{ + params: [nil], + positions: [{11, 1}], + type: :defmodule + }, + {Reversible.String, :__impl__, 1} => %ElixirSense.Core.State.ModFunInfo{ + params: [[{:atom, [line: 6, column: 1], nil}]], + positions: [{6, 1}], + type: :def + } + } = state.mods_funs_to_positions + end - assert %{ - {Impls, nil, nil} => %ModFunInfo{ - params: [nil], - positions: [{11, 1}], - type: :defmodule - }, - {Reversible, :reverse, 1} => %ModFunInfo{ - params: [[{:term, [line: 2, column: 15], nil}]], - positions: [{2, 3}], - type: :def - }, - {Reversible.String, :__impl__, 1} => %ElixirSense.Core.State.ModFunInfo{ - params: [[{:atom, [line: 6, column: 1], nil}]], - positions: [{6, 1}], - type: :def - }, - {Reversible, :behaviour_info, 1} => %ElixirSense.Core.State.ModFunInfo{ - params: [[{:atom, [line: 1, column: 1], nil}]], - positions: [{1, 1}], - type: :def - } - } = state.mods_funs_to_positions - end + test "functions head" do + state = + """ + defmodule OuterModule do + def abc(a \\\\ nil) + def abc(1), do: :ok + def abc(nil), do: :error + IO.puts "" + end + """ + |> string_to_state + + assert %{ + {OuterModule, :abc, 1} => %ModFunInfo{ + params: [ + [nil], + [1], + [ + {:\\, [line: 2, column: 13], [{:a, [line: 2, column: 11], nil}, nil]}, + ] + ] + } + } = state.mods_funs_to_positions end test "functions with default args" do @@ -5081,15 +5165,13 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert nil == get_line_function(state, 31) assert Reversible == get_line_module(state, 31) - if @protocol_support do - assert nil == get_line_typespec(state, 35) - assert nil == get_line_function(state, 35) - assert Reversible.Map == get_line_module(state, 35) + assert nil == get_line_typespec(state, 35) + assert nil == get_line_function(state, 35) + assert Reversible.My.List == get_line_module(state, 35) - assert nil == get_line_typespec(state, 37) - assert {:reverse, 1} == get_line_function(state, 37) - assert Reversible.Map == get_line_module(state, 37) - end + assert nil == get_line_typespec(state, 37) + assert {:reverse, 1} == get_line_function(state, 37) + assert Reversible.My.List == get_line_module(state, 37) end test "finds positions for guards" do @@ -5312,57 +5394,74 @@ defmodule ElixirSense.Core.MetadataBuilderTest do end end - if @protocol_support do - test "registers mods and func for protocols" do - state = - """ - defmodule MyModuleWithoutFuns do + test "registers builtin functions for protocols" do + state = + """ + defprotocol Reversible do + def reverse(term) + IO.puts "" + end + """ + |> string_to_state + + assert Map.has_key?(state.mods_funs_to_positions, {Reversible, :__protocol__, 1}) + assert Map.has_key?(state.mods_funs_to_positions, {Reversible, :impl_for, 1}) + assert Map.has_key?(state.mods_funs_to_positions, {Reversible, :impl_for!, 1}) + assert Map.has_key?(state.mods_funs_to_positions, {Reversible, :impl_for!, 1}) + assert Map.has_key?(state.mods_funs_to_positions, {Reversible, :__info__, 1}) + assert Map.has_key?(state.mods_funs_to_positions, {Reversible, :module_info, 0}) + assert Map.has_key?(state.mods_funs_to_positions, {Reversible, :module_info, 1}) + assert Map.has_key?(state.mods_funs_to_positions, {Reversible, :behaviour_info, 1}) + end + + test "registers builtin functions for protocol implementations" do + state = + """ + defmodule MyModuleWithoutFuns do + end + defmodule MyModuleWithFuns do + def func do + IO.puts "" end - defmodule MyModuleWithFuns do - def func do - IO.puts "" - end - defp funcp do - IO.puts "" - end - defmacro macro1(ast) do - IO.puts "" - end - defmacrop macro1p(ast) do - IO.puts "" - end - defguard is_even(value) when is_integer(value) and rem(value, 2) == 0 - defguardp is_evenp(value) when is_integer(value) and rem(value, 2) == 0 - defdelegate func_delegated(par), to: OtherModule - defmodule Nested do - end + defp funcp do + IO.puts "" end - - defprotocol Reversible do - def reverse(term) + defmacro macro1(ast) do IO.puts "" end - - defimpl Reversible, for: String do - def reverse(term), do: String.reverse(term) + defmacrop macro1p(ast) do IO.puts "" end + defguard is_even(value) when is_integer(value) and rem(value, 2) == 0 + defguardp is_evenp(value) when is_integer(value) and rem(value, 2) == 0 + defdelegate func_delegated(par), to: OtherModule + defmodule Nested do + end + end - defmodule Impls do - alias Reversible, as: R - alias My.List, as: Ml - defimpl R, for: [Ml, Map] do - def reverse(term), do: Enum.reverse(term) - IO.puts "" - end + defprotocol Reversible do + def reverse(term) + IO.puts "" + end + + defimpl Reversible, for: String do + def reverse(term), do: String.reverse(term) + IO.puts "" + end + + defmodule Impls do + alias Reversible, as: R + alias My.List, as: Ml + defimpl R, for: [Ml, Map] do + def reverse(term), do: Enum.reverse(term) + IO.puts "" end - """ - |> string_to_state + end + """ + |> string_to_state - assert Map.has_key?(state.mods_funs_to_positions, {Impls, :__info__, 1}) - assert Map.has_key?(state.mods_funs_to_positions, {Reversible, :__protocol__, 1}) - assert Map.has_key?(state.mods_funs_to_positions, {Reversible.My.List, :__impl__, 1}) - end + assert Map.has_key?(state.mods_funs_to_positions, {Impls, :__info__, 1}) + assert Map.has_key?(state.mods_funs_to_positions, {Reversible.My.List, :__impl__, 1}) end test "first_alias_positions" do @@ -5439,7 +5538,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do %AttributeInfo{name: :my_attribute, positions: [{2, _}]} ] = get_line_attributes(state, 4) - if @protocol_support do assert %{ {InheritMod, :handle_call, 3} => %ModFunInfo{ params: [ @@ -5449,17 +5547,13 @@ defmodule ElixirSense.Core.MetadataBuilderTest do {:state, _, _} ] ], - positions: [{2, 3}], type: :def }, {InheritMod, nil, nil} => %ModFunInfo{ - params: [nil], - positions: [{1, 1}], type: :defmodule }, {InheritMod, :private_func, 0} => %ModFunInfo{ params: [[]], - positions: [{2, 3}], type: :defp }, {InheritMod, :private_func_arg, 1} => %ModFunInfo{ @@ -5467,12 +5561,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do [{:a, _, _}], [{:\\, _, [{:a, _, _}, nil]}] ], - positions: [{2, 3}, {2, 3}], type: :defp }, {InheritMod, :private_guard, 0} => %ModFunInfo{ params: [[]], - positions: [{2, 3}], type: :defguardp }, {InheritMod, :private_guard_arg, 1} => %ModFunInfo{ @@ -5481,12 +5573,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do {:a, _, _} ] ], - positions: [{2, 3}], type: :defguardp }, {InheritMod, :private_macro, 0} => %ModFunInfo{ params: [[]], - positions: [{2, 3}], type: :defmacrop }, {InheritMod, :private_macro_arg, 1} => %ModFunInfo{ @@ -5495,12 +5585,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do {:a, _, _} ] ], - positions: [{2, 3}], type: :defmacrop }, {InheritMod, :public_func, 0} => %ModFunInfo{ params: [[]], - positions: [{2, 3}], type: :def, overridable: {true, ElixirSenseExample.ExampleBehaviour} }, @@ -5515,12 +5603,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ]} ] ], - positions: [{2, 3}], type: :def }, {InheritMod, :public_guard, 0} => %ModFunInfo{ params: [[]], - positions: [{2, 3}], type: :defguard }, {InheritMod, :public_guard_arg, 1} => %ModFunInfo{ @@ -5529,12 +5615,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do {:a, _, _} ] ], - positions: [{2, 3}], type: :defguard }, {InheritMod, :public_macro, 0} => %ModFunInfo{ params: [[]], - positions: [{2, 3}], type: :defmacro }, {InheritMod, :public_macro_arg, 1} => %ModFunInfo{ @@ -5543,28 +5627,19 @@ defmodule ElixirSense.Core.MetadataBuilderTest do {:a, _, _} ] ], - positions: [{2, 3}], type: :defmacro }, {InheritMod.Deeply.Nested, nil, nil} => %ModFunInfo{ - params: [nil], - positions: [{2, 3}], type: :defmodule }, {InheritMod.Nested, nil, nil} => %ModFunInfo{ - params: [nil], - positions: [{2, 3}], type: :defmodule }, {InheritMod.ProtocolEmbedded, nil, nil} => %ModFunInfo{ - params: [nil], - positions: [{2, 3}], type: :defmodule }, {InheritMod, :behaviour_info, 1} => %ModFunInfo{ - params: [[{:atom, [line: 2, column: 3], nil}]], - positions: [{2, 3}], - target: nil, + params: [[{:atom, _, nil}]], type: :def }, {InheritMod.ProtocolEmbedded, :module_info, 1} => %ModFunInfo{} @@ -5575,28 +5650,28 @@ defmodule ElixirSense.Core.MetadataBuilderTest do args: [[]], kind: :opaque, name: :my_opaque_type, - positions: [{2, 3}], + # positions: [{2, 3}], specs: ["@opaque my_opaque_type :: any"] }, {InheritMod, :my_priv_type, 0} => %State.TypeInfo{ args: [[]], kind: :typep, name: :my_priv_type, - positions: [{2, 3}], + # positions: [{2, 3}], specs: ["@typep my_priv_type :: any"] }, {InheritMod, :my_pub_type, 0} => %State.TypeInfo{ args: [[]], kind: :type, name: :my_pub_type, - positions: [{2, 3}], + # positions: [{2, 3}], specs: ["@type my_pub_type :: any"] }, {InheritMod, :my_pub_type_arg, 2} => %State.TypeInfo{ args: [["a", "b"]], kind: :type, name: :my_pub_type_arg, - positions: [{2, 3}], + # positions: [{2, 3}], specs: ["@type my_pub_type_arg(a, b) :: {b, a}"] } } = state.types @@ -5606,18 +5681,17 @@ defmodule ElixirSense.Core.MetadataBuilderTest do args: [[]], kind: :spec, name: :private_func, - positions: [{2, 3}], + # positions: [{2, 3}], specs: ["@spec private_func() :: String.t()"] }, {InheritMod, :some_callback, 1} => %State.SpecInfo{ args: [["abc"]], kind: :callback, name: :some_callback, - positions: [{2, 3}], + # positions: [{2, 3}], specs: ["@callback some_callback(abc) :: :ok when abc: integer"] } } = state.specs - end end test "use defining struct" do @@ -6546,28 +6620,24 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } = state.types end - if @protocol_support do - test "protocol exports type t" do - state = - """ - defprotocol Proto do - def reverse(term) - end - """ - |> string_to_state + test "protocol exports type t" do + state = + """ + defprotocol Proto do + def reverse(term) + end + """ + |> string_to_state - assert state.types == %{ - {Proto, :t, 0} => %ElixirSense.Core.State.TypeInfo{ - args: [[]], - kind: :type, - name: :t, - positions: [{1, 1}], - end_positions: [{3, 4}], - generated: [true], - specs: ["@type t :: term"] - } - } - end + assert %{ + {Proto, :t, 0} => %ElixirSense.Core.State.TypeInfo{ + args: [[]], + kind: :type, + name: :t, + specs: ["@type t :: term"], + doc: "All the types that implement this protocol" <> _ + } + } = state.types end test "specs and callbacks" do From d9103a48a37361eb63843a417d5acb503ac49e2e Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 4 May 2024 06:17:33 +0200 Subject: [PATCH 019/235] reduce debug amount --- lib/elixir_sense/core/compiler.ex | 28 +++++++++++++------------- lib/elixir_sense/core/state.ex | 7 +------ test/elixir_sense/core/parser_test.exs | 8 ++++---- 3 files changed, 19 insertions(+), 24 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 982cd85e..5f249e00 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -359,7 +359,7 @@ defmodule ElixirSense.Core.Compiler do {q, prelude} = __MODULE__.Quote.build(meta, line, file, context, unquote_opt, generated) - quoted = __MODULE__.Quote.quote(meta, exprs |> dbg, binding, q, prelude, et) |> dbg + quoted = __MODULE__.Quote.quote(meta, exprs, binding, q, prelude, et) expand(quoted, st, et) end @@ -487,9 +487,9 @@ defmodule ElixirSense.Core.Compiler do vars: {read, write} } = s - pair = {name, var_context(meta, kind)} |> dbg + pair = {name, var_context(meta, kind)} - case read |> dbg do + case read do # Variable was already overridden %{^pair => var_version} when var_version >= prematch_version -> # maybe_warn_underscored_var_repeat(meta, name, kind, e) @@ -558,7 +558,7 @@ defmodule ElixirSense.Core.Compiler do prematch end - case result |> dbg do + case result do {:ok, pair_version} -> # maybe_warn_underscored_var_access(meta, name, kind, e) var = {name, [{:version, pair_version} | meta], kind} @@ -675,7 +675,7 @@ defmodule ElixirSense.Core.Compiler do assert_no_match_or_guard_scope(e.context, "anonymous call") {[e_expr | e_args], sa, ea} = expand_args([expr | args], s, e) - sa = if is_atom(e_expr |> dbg) do + sa = if is_atom(e_expr) do # function_error(meta, e, __MODULE__, {:invalid_function_call, e_expr}) sa else @@ -1470,8 +1470,8 @@ defmodule ElixirSense.Core.Compiler do defp expand_macro(meta, Kernel, def_kind, [call, expr], _callback, state, env) when def_kind in [:def, :defp, :defmacro, :defmacrop, :defguard, :defguardp] do - dbg(call) - dbg(expr) + # dbg(call) + # dbg(expr) assert_no_match_or_guard_scope(env.context, :"{def_kind}/2") module = assert_module_scope(env, def_kind, 2) @@ -1511,7 +1511,7 @@ defmodule ElixirSense.Core.Compiler do |> new_func_vars_scope {name_and_args, guards} = __MODULE__.Utils.extract_guards(call) - dbg(name_and_args) + # dbg(name_and_args) {name, _meta_1, args} = case name_and_args do @@ -1586,7 +1586,7 @@ defmodule ElixirSense.Core.Compiler do end defp expand_macro_callback(meta, module, fun, args, callback, state, env) do - dbg({module, fun, args}) + # dbg({module, fun, args}) try do callback.(meta, args) catch @@ -1603,7 +1603,7 @@ defmodule ElixirSense.Core.Compiler do end defp expand_macro_callback!(meta, module, fun, args, callback, state, env) do - dbg({module, fun, args}) + # dbg({module, fun, args}) ast = callback.(meta, args) {ast, state, env} = expand(ast, state, env) {ast, state, env} @@ -1884,9 +1884,9 @@ defmodule ElixirSense.Core.Compiler do case function do {name, ^arity} -> - state.mods_funs_to_positions |> dbg + state.mods_funs_to_positions - case state.mods_funs_to_positions[{module, name, arity} |> dbg] do + case state.mods_funs_to_positions[{module, name, arity}] do %State.ModFunInfo{overridable: {true, _}} = info -> kind = case info.type do :defdelegate -> :def @@ -1894,7 +1894,7 @@ defmodule ElixirSense.Core.Compiler do :defguardp -> :defmacrop other -> other end - hidden = Map.get(info.meta |> dbg, :hidden, false) + hidden = Map.get(info.meta, :hidden, false) # def meta is not used anyway so let's pass empty meta = [] # TODO count 1 hardcoded but that's probably OK @@ -4115,7 +4115,7 @@ defmodule ElixirSense.Core.Compiler do buffer, acc ) do - runtime = do_runtime_list(meta, :list, [expr, do_list_concat(buffer, acc)]) |> dbg + runtime = do_runtime_list(meta, :list, [expr, do_list_concat(buffer, acc)]) do_quote_splice(t, q, e, [], runtime) end diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index f2b2c9d1..20600b22 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -439,10 +439,6 @@ defmodule ElixirSense.Core.State do _previous_env = state.lines_to_env[line] current_env = get_current_env(state, macro_env) - if current_env.typespec do - dbg({line, current_env.typespec}) - end - # TODO # env = merge_env_vars(current_env, previous_env) env = current_env @@ -1304,7 +1300,7 @@ defmodule ElixirSense.Core.State do %VarInfo{name: var_name} = var_info, is_definition ) do - dbg(var_info) + # dbg(var_info) scope = hd(state.scopes) [vars_from_scope | other_vars] = state.vars_info is_var_defined = is_variable_defined(state, var_name) @@ -1334,7 +1330,6 @@ defmodule ElixirSense.Core.State do _ -> vars_from_scope end - |> dbg %__MODULE__{ state diff --git a/test/elixir_sense/core/parser_test.exs b/test/elixir_sense/core/parser_test.exs index 632931e1..6fb1e947 100644 --- a/test/elixir_sense/core/parser_test.exs +++ b/test/elixir_sense/core/parser_test.exs @@ -253,23 +253,23 @@ defmodule ElixirSense.Core.ParserTest do # assert_received {:result, result} - assert %Metadata{ + assert (%Metadata{ error: {:error, :parse_error}, lines_to_env: %{ 1 => %Env{ module: MyModule, - scope_id: 1 + scope_id: scope_id_1 }, 3 => %Env{ module: MyModule, requires: _, - scope_id: 4, + scope_id: scope_id_2, vars: [ %VarInfo{name: :x} ] } } - } = result + } when scope_id_2 > scope_id_1) = result end test "parse_string with missing terminator \"end\" attempts to insert `end` at correct indentation" do From 895cdca57315e05d83443d7ec7c1497b485ed79b Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 6 May 2024 06:43:33 +0200 Subject: [PATCH 020/235] attribute type inference --- lib/elixir_sense/core/compiler.ex | 46 ++-- lib/elixir_sense/core/guard.ex | 4 +- lib/elixir_sense/core/metadata_builder.ex | 176 +------------ lib/elixir_sense/core/type_inference.ex | 175 +++++++++++++ .../core/metadata_builder_test.exs | 241 +++++++++--------- 5 files changed, 328 insertions(+), 314 deletions(-) create mode 100644 lib/elixir_sense/core/type_inference.ex diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 5f249e00..230f2518 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -4,6 +4,7 @@ defmodule ElixirSense.Core.Compiler do require Logger alias ElixirSense.Core.Introspection alias ElixirSense.Core.TypeInfo + alias ElixirSense.Core.TypeInference @env :elixir_env.new() def env, do: @env @@ -1124,9 +1125,14 @@ defmodule ElixirSense.Core.Compiler do _ -> raise "invalid @ call" end + inferred_type = case e_args do + nil -> nil + [arg] -> TypeInference.get_binding_type(state, arg) + end + state = state - |> add_attribute(name, nil, is_definition, {line, column}) + |> add_attribute(name, inferred_type, is_definition, {line, column}) |> add_current_env_to_line(line, env) @@ -1836,28 +1842,28 @@ defmodule ElixirSense.Core.Compiler do expand_block(t, [eh | acc], meta, se, ee) end - defp expand_quote(ast, state, env) do - {_, {state, env}} = - Macro.prewalk(ast, {state, env}, fn - # We need to traverse inside unquotes - {unquote, _, [expr]}, {state, env} when unquote in [:unquote, :unquote_splicing] -> - {_expr, state, env} = expand(expr, state, env) - {:ok, {state, env}} + # defp expand_quote(ast, state, env) do + # {_, {state, env}} = + # Macro.prewalk(ast, {state, env}, fn + # # We need to traverse inside unquotes + # {unquote, _, [expr]}, {state, env} when unquote in [:unquote, :unquote_splicing] -> + # {_expr, state, env} = expand(expr, state, env) + # {:ok, {state, env}} - # If we find a quote inside a quote, we stop traversing it - {:quote, _, [_]}, acc -> - {:ok, acc} + # # If we find a quote inside a quote, we stop traversing it + # {:quote, _, [_]}, acc -> + # {:ok, acc} - {:quote, _, [_, _]}, acc -> - {:ok, acc} + # {:quote, _, [_, _]}, acc -> + # {:ok, acc} - # Otherwise we go on - node, acc -> - {node, acc} - end) + # # Otherwise we go on + # node, acc -> + # {node, acc} + # end) - {ast, state, env} - end + # {ast, state, env} + # end defp expand_multi_alias_call(kind, meta, base, refs, opts, state, env) do {base_ref, state, env} = expand(base, state, env) @@ -4693,7 +4699,7 @@ defmodule ElixirSense.Core.Compiler do def expand(ast, state, env) do {ast, {state, env}} = # TODO this should handle remote calls, attributes unquotes? - {ast, {state, env}} = Macro.prewalk(ast, {state, env}, fn + {ast, {_state, _env}} = Macro.prewalk(ast, {state, env}, fn {:__aliases__, meta, list} = node, {state, env} when is_list(list) -> {node, state, env} = ElixirExpand.expand(node, state, env) {node, {state, env}} diff --git a/lib/elixir_sense/core/guard.ex b/lib/elixir_sense/core/guard.ex index 77897dae..2500d7c7 100644 --- a/lib/elixir_sense/core/guard.ex +++ b/lib/elixir_sense/core/guard.ex @@ -5,7 +5,7 @@ defmodule ElixirSense.Core.Guard do import ElixirSense.Core.State - alias ElixirSense.Core.MetadataBuilder + alias ElixirSense.Core.TypeInference # A guard expression can be in either these form: # :and :or @@ -116,7 +116,7 @@ defmodule ElixirSense.Core.Guard do defp guard_predicate_type(:is_map_key, [var, key], state) do type = - case MetadataBuilder.get_binding_type(state, key) do + case TypeInference.get_binding_type(state, key) do {:atom, key} -> {:map, [{key, nil}], nil} nil when is_binary(key) -> {:map, [{key, nil}], nil} _ -> {:map, [], nil} diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index 1041fe28..f62e9dad 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -5,9 +5,8 @@ defmodule ElixirSense.Core.MetadataBuilder do import ElixirSense.Core.State import ElixirSense.Log + import ElixirSense.Core.TypeInference - alias ElixirSense.Core.BuiltinFunctions - alias ElixirSense.Core.Introspection alias ElixirSense.Core.Source alias ElixirSense.Core.State alias ElixirSense.Core.State.VarInfo @@ -1656,179 +1655,6 @@ defmodule ElixirSense.Core.MetadataBuilder do end) end - # struct or struct update - def get_binding_type( - state, - {:%, _meta, - [ - struct_ast, - {:%{}, _, _} = ast - ]} - ) do - {fields, updated_struct} = - case get_binding_type(state, ast) do - {:map, fields, updated_map} -> {fields, updated_map} - {:struct, fields, _, updated_struct} -> {fields, updated_struct} - _ -> {[], nil} - end - - # expand struct type - only compile type atoms or attributes are supported - type = - case get_binding_type(state, struct_ast) do - {:atom, atom} -> {:atom, atom} - {:attribute, attribute} -> {:attribute, attribute} - _ -> nil - end - - {:struct, fields, type, updated_struct} - end - - # pipe - def get_binding_type(state, {:|>, _, [params_1, {call, meta, params_rest}]}) do - params = [params_1 | params_rest || []] - get_binding_type(state, {call, meta, params}) - end - - # remote call - def get_binding_type(state, {{:., _, [target, fun]}, _, args}) - when is_atom(fun) and is_list(args) do - target = get_binding_type(state, target) - {:call, target, fun, Enum.map(args, &get_binding_type(state, &1))} - end - - # current module - def get_binding_type(state, {:__MODULE__, _, nil} = module) do - {module, _state, _env} = expand(module, state) - {:atom, module} - end - - # elixir module - def get_binding_type(state, {:__aliases__, _, list} = module) when is_list(list) do - try do - {module, _state, _env} = expand(module, state) - {:atom, module} - rescue - _ -> nil - end - end - - # variable or local no parens call - def get_binding_type(_state, {var, _, nil}) when is_atom(var) do - {:variable, var} - end - - # attribute - def get_binding_type(_state, {:@, _, [{attribute, _, nil}]}) - when is_atom(attribute) do - {:attribute, attribute} - end - - # erlang module or atom - def get_binding_type(_state, atom) when is_atom(atom) do - {:atom, atom} - end - - # map update - def get_binding_type( - state, - {:%{}, _meta, - [ - {:|, _meta1, - [ - updated_map, - fields - ]} - ]} - ) - when is_list(fields) do - {:map, get_fields_binding_type(state, fields), get_binding_type(state, updated_map)} - end - - # map - def get_binding_type(state, {:%{}, _meta, fields}) when is_list(fields) do - {:map, get_fields_binding_type(state, fields), nil} - end - - # match - def get_binding_type(state, {:=, _, [_, ast]}) do - get_binding_type(state, ast) - end - - # stepped range struct - def get_binding_type(_state, {:"..//", _, [_, _, _]}) do - {:struct, [], {:atom, Range}} - end - - # range struct - def get_binding_type(_state, {:.., _, [_, _]}) do - {:struct, [], {:atom, Range}} - end - - @builtin_sigils %{ - sigil_D: Date, - sigil_T: Time, - sigil_U: DateTime, - sigil_N: NaiveDateTime, - sigil_R: Regex, - sigil_r: Regex - } - - # builtin sigil struct - def get_binding_type(_state, {sigil, _, _}) when is_map_key(@builtin_sigils, sigil) do - # TODO support custom sigils - {:struct, [], {:atom, @builtin_sigils |> Map.fetch!(sigil)}} - end - - # tuple - # regular tuples use {:{}, [], [field_1, field_2]} ast - # two element use {field_1, field_2} ast (probably as an optimization) - # detect and convert to regular - def get_binding_type(state, ast) when is_tuple(ast) and tuple_size(ast) == 2 do - get_binding_type(state, {:{}, [], Tuple.to_list(ast)}) - end - - def get_binding_type(state, {:{}, _, list}) do - {:tuple, length(list), list |> Enum.map(&get_binding_type(state, &1))} - end - - def get_binding_type(state, list) when is_list(list) do - type = - case list do - [] -> :empty - [{:|, _, [head, _tail]}] -> get_binding_type(state, head) - [head | _] -> get_binding_type(state, head) - end - - {:list, type} - end - - def get_binding_type(state, list) when is_list(list) do - {:list, list |> Enum.map(&get_binding_type(state, &1))} - end - - # pinned variable - def get_binding_type(state, {:^, _, [pinned]}), do: get_binding_type(state, pinned) - - # local call - def get_binding_type(state, {var, _, args}) when is_atom(var) and is_list(args) do - {:local_call, var, Enum.map(args, &get_binding_type(state, &1))} - end - - # integer - def get_binding_type(_state, integer) when is_integer(integer) do - {:integer, integer} - end - - # other - def get_binding_type(_state, _), do: nil - - defp get_fields_binding_type(state, fields) do - for {field, value} <- fields, - is_atom(field) do - {field, get_binding_type(state, value)} - end - end - defp add_no_call(meta) do [{:no_call, true} | meta] end diff --git a/lib/elixir_sense/core/type_inference.ex b/lib/elixir_sense/core/type_inference.ex new file mode 100644 index 00000000..7ba5bf08 --- /dev/null +++ b/lib/elixir_sense/core/type_inference.ex @@ -0,0 +1,175 @@ +defmodule ElixirSense.Core.TypeInference do + # TODO remove state arg + # struct or struct update + def get_binding_type( + state, + {:%, _meta, + [ + struct_ast, + {:%{}, _, _} = ast + ]} + ) do + {fields, updated_struct} = + case get_binding_type(state, ast) do + {:map, fields, updated_map} -> {fields, updated_map} + {:struct, fields, _, updated_struct} -> {fields, updated_struct} + _ -> {[], nil} + end + + # expand struct type - only compile type atoms or attributes are supported + type = + case get_binding_type(state, struct_ast) do + {:atom, atom} -> {:atom, atom} + {:attribute, attribute} -> {:attribute, attribute} + _ -> nil + end + + {:struct, fields, type, updated_struct} + end + + # pipe + def get_binding_type(state, {:|>, _, [params_1, {call, meta, params_rest}]}) do + params = [params_1 | params_rest || []] + get_binding_type(state, {call, meta, params}) + end + + # remote call + def get_binding_type(state, {{:., _, [target, fun]}, _, args}) + when is_atom(fun) and is_list(args) do + target = get_binding_type(state, target) + {:call, target, fun, Enum.map(args, &get_binding_type(state, &1))} + end + + # # current module + # def get_binding_type(state, {:__MODULE__, _, nil} = module) do + # {module, _state, _env} = expand(module, state) + # {:atom, module} + # end + + # # elixir module + # def get_binding_type(state, {:__aliases__, _, list} = module) when is_list(list) do + # try do + # {module, _state, _env} = expand(module, state) + # {:atom, module} + # rescue + # _ -> nil + # end + # end + + # variable or local no parens call + def get_binding_type(_state, {var, _, nil}) when is_atom(var) do + {:variable, var} + end + + # attribute + def get_binding_type(_state, {:@, _, [{attribute, _, nil}]}) + when is_atom(attribute) do + {:attribute, attribute} + end + + # erlang module or atom + def get_binding_type(_state, atom) when is_atom(atom) do + {:atom, atom} + end + + # map update + def get_binding_type( + state, + {:%{}, _meta, + [ + {:|, _meta1, + [ + updated_map, + fields + ]} + ]} + ) + when is_list(fields) do + {:map, get_fields_binding_type(state, fields), get_binding_type(state, updated_map)} + end + + # map + def get_binding_type(state, {:%{}, _meta, fields}) when is_list(fields) do + {:map, get_fields_binding_type(state, fields), nil} + end + + # match + def get_binding_type(state, {:=, _, [_, ast]}) do + get_binding_type(state, ast) + end + + # stepped range struct + def get_binding_type(_state, {:"..//", _, [_, _, _]}) do + {:struct, [], {:atom, Range}} + end + + # range struct + def get_binding_type(_state, {:.., _, [_, _]}) do + {:struct, [], {:atom, Range}} + end + + @builtin_sigils %{ + sigil_D: Date, + sigil_T: Time, + sigil_U: DateTime, + sigil_N: NaiveDateTime, + sigil_R: Regex, + sigil_r: Regex + } + + # builtin sigil struct + def get_binding_type(_state, {sigil, _, _}) when is_map_key(@builtin_sigils, sigil) do + # TODO support custom sigils + {:struct, [], {:atom, @builtin_sigils |> Map.fetch!(sigil)}} + end + + # tuple + # regular tuples use {:{}, [], [field_1, field_2]} ast + # two element use {field_1, field_2} ast (probably as an optimization) + # detect and convert to regular + def get_binding_type(state, ast) when is_tuple(ast) and tuple_size(ast) == 2 do + get_binding_type(state, {:{}, [], Tuple.to_list(ast)}) + end + + def get_binding_type(state, {:{}, _, list}) do + {:tuple, length(list), list |> Enum.map(&get_binding_type(state, &1))} + end + + def get_binding_type(state, list) when is_list(list) do + type = + case list do + [] -> :empty + [{:|, _, [head, _tail]}] -> get_binding_type(state, head) + [head | _] -> get_binding_type(state, head) + end + + {:list, type} + end + + def get_binding_type(state, list) when is_list(list) do + {:list, list |> Enum.map(&get_binding_type(state, &1))} + end + + # pinned variable + def get_binding_type(state, {:^, _, [pinned]}), do: get_binding_type(state, pinned) + + # local call + def get_binding_type(state, {var, _, args}) when is_atom(var) and is_list(args) do + {:local_call, var, Enum.map(args, &get_binding_type(state, &1))} + end + + # integer + def get_binding_type(_state, integer) when is_integer(integer) do + {:integer, integer} + end + + # other + def get_binding_type(_state, _), do: nil + + defp get_fields_binding_type(state, fields) do + for {field, value} <- fields, + is_atom(field) do + {field, get_binding_type(state, value)} + end + end +end diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 6e7c0f36..79fa740d 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -6,7 +6,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do alias ElixirSense.Core.State alias ElixirSense.Core.State.{VarInfo, CallInfo, StructInfo, ModFunInfo, AttributeInfo} - @attribute_binding_support Version.match?(System.version(), "< 1.17.0-dev") + @attribute_binding_support true or Version.match?(System.version(), "< 1.17.0-dev") @expand_eval false @binding_support Version.match?(System.version(), "< 1.17.0-dev") @macro_calls_support Version.match?(System.version(), "< 1.17.0-dev") @@ -1345,8 +1345,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } ] = get_line_attributes(state, 9) end - - if @attribute_binding_support do + + describe "binding" do test "module attributes binding" do state = """ @@ -1363,113 +1363,120 @@ defmodule ElixirSense.Core.MetadataBuilderTest do end """ |> string_to_state - + assert get_line_attributes(state, 10) == [ - %ElixirSense.Core.State.AttributeInfo{ - name: :myattribute, - positions: [{2, 3}, {3, 11}], - type: {:atom, String} - }, - %AttributeInfo{ - name: :otherattribute, - positions: [{10, 3}], - type: - {:call, {:atom, Application}, :get_env, + %ElixirSense.Core.State.AttributeInfo{ + name: :myattribute, + positions: [{2, 3}, {3, 11}], + type: {:atom, String} + }, + %AttributeInfo{ + name: :otherattribute, + positions: [{10, 3}], + type: + {:call, {:atom, Application}, :get_env, [atom: :elixir_sense, atom: :some_attribute, atom: MyModule.InnerModule]} - } - ] - + } + ] + assert get_line_attributes(state, 3) == [ - %AttributeInfo{ - name: :myattribute, - positions: [{2, 3}, {3, 11}], - type: {:atom, String} - } - ] - + %AttributeInfo{ + name: :myattribute, + positions: [{2, 3}, {3, 11}], + type: {:atom, String} + } + ] + assert get_line_attributes(state, 7) == [ - %AttributeInfo{ - name: :inner_attr, - positions: [{5, 5}, {7, 13}], - type: {:map, [abc: {:atom, nil}], nil} - }, - %AttributeInfo{ - name: :inner_attr_1, - positions: [{6, 5}], - type: {:atom, MyModule.InnerModule} - } - ] - + %AttributeInfo{ + name: :inner_attr, + positions: [{5, 5}, {7, 13}], + type: {:map, [abc: {:atom, nil}], nil} + }, + %AttributeInfo{ + name: :inner_attr_1, + positions: [{6, 5}], + type: {:atom, MyModule.InnerModule} + } + ] + assert get_line_attributes(state, 9) == [ - %AttributeInfo{ - name: :myattribute, - positions: [{2, 3}, {3, 11}], - type: {:atom, String} - } - ] + %AttributeInfo{ + name: :myattribute, + positions: [{2, 3}, {3, 11}], + type: {:atom, String} + } + ] end - end - if @binding_support do - describe "binding" do - test "module attributes rebinding" do - state = - """ - defmodule MyModule do - @myattribute String - @myattribute List + test "module attributes rebinding" do + state = + """ + defmodule MyModule do + @myattribute String + IO.puts "" + @myattribute List + @myattribute + IO.puts "" + def a do @myattribute - IO.puts "" - def a do - @myattribute - end - IO.puts "" end - """ - |> string_to_state + IO.puts "" + end + """ + |> string_to_state - assert get_line_attributes(state, 5) == [ - %AttributeInfo{ - name: :myattribute, - positions: [{2, 3}, {3, 3}, {4, 3}], - type: {:atom, List} - } - ] + assert get_line_attributes(state, 3) == [ + %AttributeInfo{ + name: :myattribute, + positions: [{2, 3}], + type: {:atom, String} + } + ] - assert get_line_attributes(state, 9) == [ - %AttributeInfo{ - name: :myattribute, - positions: [{2, 3}, {3, 3}, {4, 3}, {7, 5}], - type: {:atom, List} - } - ] - end + assert get_line_attributes(state, 6) == [ + %AttributeInfo{ + name: :myattribute, + positions: [{2, 3}, {4, 3}, {5, 3}], + type: {:atom, List} + } + ] - test "module attributes value binding" do - state = - """ - defmodule MyModule do - @myattribute %{abc: String} - @some_attr @myattribute - IO.puts "" - end - """ - |> string_to_state + assert get_line_attributes(state, 10) == [ + %AttributeInfo{ + name: :myattribute, + positions: [{2, 3}, {4, 3}, {5, 3}, {8, 5}], + type: {:atom, List} + } + ] + end - assert get_line_attributes(state, 4) == [ - %AttributeInfo{ - name: :myattribute, - positions: [{2, 3}, {3, 14}], - type: {:map, [abc: {:atom, String}], nil} - }, - %AttributeInfo{ - name: :some_attr, - positions: [{3, 3}], - type: {:attribute, :myattribute} - } - ] - end + test "module attributes value binding" do + state = + """ + defmodule MyModule do + @myattribute %{abc: String} + @some_attr @myattribute + IO.puts "" + end + """ + |> string_to_state + assert get_line_attributes(state, 4) == [ + %AttributeInfo{ + name: :myattribute, + positions: [{2, 3}, {3, 14}], + type: {:map, [abc: {:atom, String}], nil} + }, + %AttributeInfo{ + name: :some_attr, + positions: [{3, 3}], + type: {:attribute, :myattribute} + } + ] + end + + if @binding_support do test "module attributes value binding to and from variables" do state = """ @@ -2434,34 +2441,34 @@ defmodule ElixirSense.Core.MetadataBuilderTest do type: {:map, [b: {:variable, :b}], nil} } = Enum.find(vars, &(&1.name == :a2)) end + end + end - test "rebinding vars" do - state = - """ - defmodule MyModule do - var1 = 1 - def func(%{var: var1, key: [_|[_, var1]]}) do - var1 = 1 - var1 = 2 - IO.puts "" - end + describe "var" do + test "rebinding vars" do + state = + """ + defmodule MyModule do + var1 = 1 + def func(%{var: var1, key: [_|[_, var1]]}) do var1 = 1 + var1 = 2 + IO.puts "" end - """ - |> string_to_state + var1 = 1 + end + """ + |> string_to_state - vars = state |> get_line_vars(6) + vars = state |> get_line_vars(6) - assert [ - %VarInfo{name: :var1, positions: [{3, 19}, {3, 37}], scope_id: 3}, - %VarInfo{name: :var1, positions: [{4, 5}], scope_id: 4}, - %VarInfo{name: :var1, positions: [{5, 5}], scope_id: 4} - ] = vars - end + assert ([ + %VarInfo{name: :var1, positions: [{3, 19}, {3, 37}], scope_id: scope_id_1}, + %VarInfo{name: :var1, positions: [{4, 5}], scope_id: scope_id_2}, + %VarInfo{name: :var1, positions: [{5, 5}], scope_id: scope_id_2} + ] when scope_id_2 > scope_id_1) = vars end - end - describe "var" do test "vars defined inside a module" do state = """ From caf4270da78b33b836fa56f8122e4f253e4ee237 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 7 May 2024 19:05:37 +0200 Subject: [PATCH 021/235] first step --- lib/elixir_sense/core/compiler.ex | 27 +++++++++---------- .../core/metadata_builder_test.exs | 11 ++++++++ 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 230f2518..013d9c8c 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -2541,15 +2541,12 @@ defmodule ElixirSense.Core.Compiler do call_e = Map.put(e, :context, :match) {e_expr, %{vars: new_current, unused: new_unused} = s_expr, ee} = fun.(expr, call_s, call_e) - # TODO elixir does it like that, is it a bug? we lose state - # end_s = %{after_s | prematch: prematch, unused: new_unused, vars: new_current} - end_s = %{s_expr | - prematch: prematch, unused: new_unused, vars: new_current, - mods_funs_to_positions: after_s.mods_funs_to_positions, - types: after_s.types, - specs: after_s.specs, - structs: after_s.structs, - calls: after_s.calls + dbg(s_expr.vars) + + # TODO merge after_s.calls with s_expr.calls - we loose calls on the left side of = + + end_s = %{after_s | + prematch: prematch, unused: new_unused, vars: new_current } # dbg(hd(before_s.scope_vars_info)) @@ -2561,14 +2558,14 @@ defmodule ElixirSense.Core.Compiler do # dbg(new_current) # TODO I'm not sure this is correct - merged_vars = (hd(end_s.scope_vars_info) -- hd(after_s.scope_vars_info)) + merged_vars = (hd(s_expr.scope_vars_info) -- hd(after_s.scope_vars_info)) |> merge_same_name_vars() merged_vars = merged_vars ++ hd(after_s.scope_vars_info) end_s = %{end_s | scope_vars_info: [merged_vars | tl(end_s.scope_vars_info)], - lines_to_env: Map.merge(after_s.lines_to_env, end_s.lines_to_env) + lines_to_env: Map.merge(after_s.lines_to_env, s_expr.lines_to_env) } # dbg(Map.keys(end_s.lines_to_env)) @@ -2608,7 +2605,7 @@ defmodule ElixirSense.Core.Compiler do {e_args, sa, ea} = ElixirExpand.expand_args(args, sm, em) {e_guard, sg, eg} = - guard(guard, %{sa | prematch: prematch}, Map.put(ea, :context, :guard)) + guard(guard, %{sa | prematch: prematch}, %{ea | context: :guard}) {{e_args, e_guard}, sg, eg} end, @@ -2920,7 +2917,7 @@ defmodule ElixirSense.Core.Compiler do # rescue Alias => _ in [Alias] defp expand_rescue({:__aliases__, _, [_ | _]} = alias, s, e) do - expand_rescue({:in, [], [{:_, [], Map.get(e, :module)}, alias]}, s, e) + expand_rescue({:in, [], [{:_, [], e.module}, alias]}, s, e) end # rescue var in _ @@ -2953,7 +2950,7 @@ defmodule ElixirSense.Core.Compiler do # rescue expr() => rescue expanded_expr() defp expand_rescue({_meta, meta, _} = arg, s, e) do # TODO wut? - case Macro.expand_once(arg, Map.put(e, :line, line(meta))) do + case Macro.expand_once(arg, %{e | line: line(meta)}) do ^arg -> false new_arg -> expand_rescue(new_arg, s, e) end @@ -2961,7 +2958,7 @@ defmodule ElixirSense.Core.Compiler do # rescue list() or atom() => _ in (list() or atom()) defp expand_rescue(arg, s, e) do - expand_rescue({:in, [], [{:_, [], Map.get(e, :module)}, arg]}, s, e) + expand_rescue({:in, [], [{:_, [], e.module}, arg]}, s, e) end defp normalize_rescue(atom) when is_atom(atom) do diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 79fa740d..382d10eb 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -1477,6 +1477,17 @@ defmodule ElixirSense.Core.MetadataBuilderTest do end if @binding_support do + test "variable binding simple case" do + state = + """ + var = :my_var + IO.puts("") + """ + |> string_to_state + + assert [%VarInfo{type: {:atom, :my_var}}] = state |> get_line_vars(2) + end + test "module attributes value binding to and from variables" do state = """ From 199b196aa860404034bf77376ba052226162387b Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 8 May 2024 06:19:56 +0200 Subject: [PATCH 022/235] simplify var handling --- lib/elixir_sense/core/compiler.ex | 218 ++---- lib/elixir_sense/core/metadata_builder.ex | 716 ++---------------- lib/elixir_sense/core/state.ex | 445 ++--------- test/elixir_sense/core/compiler_test.exs | 21 +- .../core/metadata_builder_test.exs | 10 +- 5 files changed, 188 insertions(+), 1222 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 013d9c8c..89e737e3 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -27,7 +27,7 @@ defmodule ElixirSense.Core.Compiler do # dbg(sr) # dbg(e_right) {e_left, sl, el} = __MODULE__.Clauses.match(&expand/3, left, sr, s, er) - # IO.inspect(sl.scope_vars_info, label: "left") + # IO.inspect(sl.vars_info, label: "left") # dbg(e_left) # dbg(el.versioned_vars) # dbg(sl.vars) @@ -454,10 +454,8 @@ defmodule ElixirSense.Core.Compiler do no_match_s = %{s | prematch: :pin, vars: {prematch, write}} case expand(arg, no_match_s, %{e | context: nil}) do - {{name, var_meta, kind} = var, %{unused: unused}, _} when is_atom(name) and is_atom(kind) -> - line = var_meta[:line] - column = var_meta[:column] - s = if kind == nil, do: add_var(s, %State.VarInfo{name: name, is_definition: false, positions: [{line, column}]}, false), else: s + {{name, _var_meta, kind} = var, %{unused: unused}, _} when is_atom(name) and is_atom(kind) -> + s = add_var_read(s, var) {{:^, meta, [var]}, %{s | unused: unused}, e} _ -> @@ -484,7 +482,7 @@ defmodule ElixirSense.Core.Compiler do when is_atom(name) and is_atom(kind) do %{ prematch: {_, prematch_version, _}, - unused: {unused, version}, + unused: version, vars: {read, write} } = s @@ -493,40 +491,31 @@ defmodule ElixirSense.Core.Compiler do case read do # Variable was already overridden %{^pair => var_version} when var_version >= prematch_version -> - # maybe_warn_underscored_var_repeat(meta, name, kind, e) - new_unused = var_used(meta, pair, var_version, unused) var = {name, [{:version, var_version} | meta], kind} - line = meta[:line] - column = meta[:column] - s = if kind == nil, do: add_var(s, %State.VarInfo{name: name, is_definition: true, positions: [{line, column}]}, true), else: s - {var, %{s | unused: {new_unused, version}}, e} + # it's a write but for simplicity treat it as read + s = add_var_read(s, var) + {var, %{s | unused: version}, e} # Variable is being overridden now %{^pair => _} -> - new_unused = var_unused(pair, meta, version, unused, true) new_read = Map.put(read, pair, version) new_write = if write != false, do: Map.put(write, pair, version), else: write var = {name, [{:version, version} | meta], kind} - line = meta[:line] - column = meta[:column] - s = if kind == nil, do: add_var(s, %State.VarInfo{name: name, is_definition: true, positions: [{line, column}]}, true), else: s - {var, %{s | vars: {new_read, new_write}, unused: {new_unused, version + 1}}, e} + s = add_var_write(s, var) + {var, %{s | vars: {new_read, new_write}, unused: version + 1}, e} # Variable defined for the first time _ -> - new_unused = var_unused(pair, meta, version, unused, false) new_read = Map.put(read, pair, version) new_write = if write != false, do: Map.put(write, pair, version), else: write var = {name, [{:version, version} | meta], kind} - line = meta[:line] - column = meta[:column] - s = if kind == nil, do: add_var(s, %State.VarInfo{name: name, is_definition: true, positions: [{line, column}]}, true), else: s - {var, %{s | vars: {new_read, new_write}, unused: {new_unused, version + 1}}, e} + s = add_var_write(s, var) + {var, %{s | vars: {new_read, new_write}, unused: version + 1}, e} end end defp do_expand({name, meta, kind}, s, e) when is_atom(name) and is_atom(kind) do - %{vars: {read, _write}, unused: {unused, version}, prematch: prematch} = s + %{vars: {read, _write}, unused: version, prematch: prematch} = s pair = {name, var_context(meta, kind)} result = @@ -561,12 +550,9 @@ defmodule ElixirSense.Core.Compiler do case result do {:ok, pair_version} -> - # maybe_warn_underscored_var_access(meta, name, kind, e) var = {name, [{:version, pair_version} | meta], kind} - line = meta[:line] - column = meta[:column] - s = if kind == nil, do: add_var(s, %State.VarInfo{name: name, is_definition: false, positions: [{line, column}]}, false), else: s - {var, %{s | unused: {var_used(meta, pair, pair_version, unused), version}}, e} + s = add_var_read(s, var) + {var, %{s | unused: version}, e} error -> case Keyword.fetch(meta, :if_undefined) do @@ -620,8 +606,8 @@ defmodule ElixirSense.Core.Compiler do {:macro, module, callback} -> # TODO there is a subtle difference - callback will call expander with state derrived from env via # :elixir_env.env_to_ex(env) possibly losing some details - line = Keyword.get(meta, :line, 0) - column = Keyword.get(meta, :column, nil) + # line = Keyword.get(meta, :line, 0) + # column = Keyword.get(meta, :column, nil) # state = state # |> add_call_to_line({module, fun, length(args)}, {line, column}) # |> add_current_env_to_line(line, env) @@ -767,7 +753,7 @@ defmodule ElixirSense.Core.Compiler do Kernel, :defdelegate, [funs, opts], - callback, + _callback, state, env ) do @@ -1023,7 +1009,7 @@ defmodule ElixirSense.Core.Compiler do {expr, state, env} = __MODULE__.Typespec.expand(expr, state, env) case __MODULE__.Typespec.type_to_signature(expr) do - {name, [type_arg]} when name in [:required, :optional] -> + {name, [_type_arg]} when name in [:required, :optional] -> raise "type #{name}/#{1} is a reserved type and it cannot be defined" {name, type_args} -> @@ -1256,7 +1242,7 @@ defmodule ElixirSense.Core.Compiler do options ) |> add_call_to_line({module, call, length(args)}, {line, column}) - |> add_current_env_to_line(line) + |> add_current_env_to_line(line, env) {{{:., meta, [Record, call]}, meta, args}, state, env} end @@ -1265,7 +1251,7 @@ defmodule ElixirSense.Core.Compiler do meta, Kernel, :defprotocol, - [alias, [do: block]] = args, + [_alias, [do: _block]] = args, callback, state, env @@ -1462,7 +1448,7 @@ defmodule ElixirSense.Core.Compiler do {{:__block__, [], []}, state, env} end - defp expand_macro(meta, Protocol, :def, [{name, _, args = [_ | _]} = call], callback, state, env) when is_atom(name) do + defp expand_macro(meta, Protocol, :def, [{name, _, _args = [_ | _]} = call], callback, state, env) when is_atom(name) do # transform protocol def to def with empty body {ast, state, env} = expand_macro(meta, Kernel, :def, [call, {:__block__, [], []}], callback, state, env) {ast, state, env} @@ -1479,7 +1465,7 @@ defmodule ElixirSense.Core.Compiler do # dbg(call) # dbg(expr) assert_no_match_or_guard_scope(env.context, :"{def_kind}/2") - module = assert_module_scope(env, def_kind, 2) + _module = assert_module_scope(env, def_kind, 2) %{vars: vars, unused: unused} = state @@ -1513,7 +1499,7 @@ defmodule ElixirSense.Core.Compiler do # state # |> add_current_env_to_line(line, env) - state = %{state | vars: {%{}, false}, unused: {%{}, 0}} + state = %{state | vars: {%{}, false}, unused: 0} |> new_func_vars_scope {name_and_args, guards} = __MODULE__.Utils.extract_guards(call) @@ -1578,8 +1564,8 @@ defmodule ElixirSense.Core.Compiler do expand(expr, state, %{g_env | context: nil, function: {name, arity}}) # restore vars from outer scope - # TODO maybe_move_vars_to_outer_scope? state = %{state | vars: vars, unused: unused} + |> maybe_move_vars_to_outer_scope |> remove_vars_scope |> remove_func_vars_scope @@ -1598,7 +1584,7 @@ defmodule ElixirSense.Core.Compiler do catch # TODO raise? # For language servers, if expanding the macro fails, we just give up. - kind, payload -> + _kind, _payload -> # IO.inspect(payload, label: inspect(fun)) {{{:., meta, [module, fun]}, meta, args}, state, env} else @@ -1608,7 +1594,7 @@ defmodule ElixirSense.Core.Compiler do end end - defp expand_macro_callback!(meta, module, fun, args, callback, state, env) do + defp expand_macro_callback!(meta, _module, _fun, args, callback, state, env) do # dbg({module, fun, args}) ast = callback.(meta, args) {ast, state, env} = expand(ast, state, env) @@ -1969,7 +1955,7 @@ defmodule ElixirSense.Core.Compiler do nil -> raise "missing_option" end - {e_opts, so, eo} = expand(opts, __MODULE__.Env.reset_unused_vars(s), e) + {e_opts, so, eo} = expand(opts, __MODULE__.Env.reset_vars(s), e) {e_cases, sc, ec} = map_fold(&expand_for_generator/3, so, eo, cases) assert_generator_start(meta, e_cases, e) @@ -1990,7 +1976,7 @@ defmodule ElixirSense.Core.Compiler do end {{:for, meta, e_cases ++ [[{:do, e_expr} | normalized_opts]]}, - __MODULE__.Env.merge_and_check_unused_vars(se, s, ee), e} + __MODULE__.Env.merge_vars(se, s, ee), e} end defp expand_for_do_block(_meta, [{:->, _, _} | _], _s, _e, false), @@ -2001,12 +1987,12 @@ defmodule ElixirSense.Core.Compiler do defp expand_for_do_block(meta, [{:->, _, _} | _] = clauses, s, e, {:reduce, _}) do transformer = fn {_, _, [[_], _]} = clause, sa -> - s_reset = __MODULE__.Env.reset_unused_vars(sa) + s_reset = __MODULE__.Env.reset_vars(sa) {e_clause, s_acc, e_acc} = __MODULE__.Clauses.clause(meta, :fn, &__MODULE__.Clauses.head/3, clause, s_reset, e) - {e_clause, __MODULE__.Env.merge_and_check_unused_vars(s_acc, sa, e_acc)} + {e_clause, __MODULE__.Env.merge_vars(s_acc, sa, e_acc)} _, _ -> raise "for_with_reduce_bad_block" @@ -2105,7 +2091,6 @@ defmodule ElixirSense.Core.Compiler do end defp validate_for_options([], false, {:uniq, true}, false, false, meta, e, acc) do - # file_warn(meta, e, __MODULE__, :for_with_unused_uniq) acc_without_uniq = Keyword.delete(acc, :uniq) validate_for_options([], false, false, false, false, meta, e, acc_without_uniq) end @@ -2317,32 +2302,6 @@ defmodule ElixirSense.Core.Compiler do defp refute_parallel_bitstring_match_map_field(_args1, _args2, _e, _parallel), do: :ok - defp var_unused({_, kind} = pair, meta, version, unused, override) do - if kind == nil and should_warn(meta) do - Map.put(unused, {pair, version}, {meta, override}) - else - unused - end - end - - defp var_used(meta, {_, kind} = pair, version, unused) do - keep_unused = Keyword.has_key?(meta, :keep_unused) - - if keep_unused do - unused - else - if is_atom(kind) do - Map.put(unused, {pair, version}, false) - else - unused - end - end - end - - defp should_warn(meta) do - Keyword.get(meta, :generated) != true - end - defp var_context(meta, kind) do case Keyword.fetch(meta, :counter) do {:ok, counter} -> counter @@ -2399,8 +2358,8 @@ defmodule ElixirSense.Core.Compiler do defmodule Env do alias ElixirSense.Core.Compiler.Utils, as: ElixirUtils - def reset_unused_vars(%{unused: {_unused, version}} = s) do - %{s | unused: {%{}, version}} |> new_vars_scope + def reset_vars(s) do + s |> new_vars_scope end def reset_read(%{vars: {_, write}} = s, %{vars: {read, _}}) do @@ -2411,11 +2370,11 @@ defmodule ElixirSense.Core.Compiler do %{s | vars: {read, read}} end - def close_write(%{vars: {_read, write}} = s, %{vars: {_, false}} = s1) do + def close_write(%{vars: {_read, write}} = s, %{vars: {_, false}}) do %{s | vars: {write, false}} end - def close_write(%{vars: {_read, write}} = s, %{vars: {_, upper_write}} = s1) do + def close_write(%{vars: {_read, write}} = s, %{vars: {_, upper_write}}) do %{s | vars: {write, merge_vars(upper_write, write)}} end @@ -2434,44 +2393,18 @@ defmodule ElixirSense.Core.Compiler do ) end - def merge_and_check_unused_vars(s, s1 = %{vars: {read, write}, unused: {unused, _version}}, e) do - %{unused: {clause_unused, version}} = s - new_unused = merge_and_check_unused_vars(read, unused, clause_unused, e) - # dbg(s.scope_vars_info) + def merge_vars(s, %{vars: {read, write}}, _e) do + # dbg(s.vars_info) # dbg({read, write}) - s = %{s | unused: {new_unused, version}, vars: {read, write}} + s = %{s | vars: {read, write}} |> maybe_move_vars_to_outer_scope |> remove_vars_scope - # dbg(s.scope_vars_info) + # dbg(s.vars_info) # dbg(s.vars_info_per_scope_id) s end - def merge_and_check_unused_vars(current, unused, clause_unused, _e) do - :maps.fold( - fn - {var, count} = key, false, acc -> - case Map.fetch(current, var) do - {:ok, current_count} when count <= current_count -> - Map.put(acc, key, false) - - _ -> - acc - end - - {{_name, _kind}, _count}, {_meta, _overridden}, acc -> - # if kind == nil and is_unused_var(name) do - # warn = {:unused_var, name, overridden} - # file_warn(meta, e, __MODULE__, warn) - # end - acc - end, - unused, - clause_unused - ) - end - def calculate_span(meta, name) do case Keyword.fetch(meta, :column) do {:ok, column} -> @@ -2533,49 +2466,31 @@ defmodule ElixirSense.Core.Compiler do end def match(fun, expr, after_s, before_s, e) do - %{vars: current, unused: {_counter, unused} = unused_tuple} = after_s + %{vars: current, unused: unused} = after_s %{vars: {read, _write}, prematch: prematch} = before_s - call_s = %{before_s | prematch: {read, unused, :none}, unused: unused_tuple, vars: current} + call_s = %{before_s | + prematch: {read, unused, :none}, + unused: unused, + vars: current, + calls: after_s.calls, + lines_to_env: after_s.lines_to_env, + vars_info: after_s.vars_info + } call_e = Map.put(e, :context, :match) {e_expr, %{vars: new_current, unused: new_unused} = s_expr, ee} = fun.(expr, call_s, call_e) - dbg(s_expr.vars) - - # TODO merge after_s.calls with s_expr.calls - we loose calls on the left side of = end_s = %{after_s | - prematch: prematch, unused: new_unused, vars: new_current - } - - # dbg(hd(before_s.scope_vars_info)) - # dbg(hd(after_s.scope_vars_info)) - # dbg(hd(end_s.scope_vars_info)) - - # dbg(current) - # dbg(read) - # dbg(new_current) - - # TODO I'm not sure this is correct - merged_vars = (hd(s_expr.scope_vars_info) -- hd(after_s.scope_vars_info)) - |> merge_same_name_vars() - - merged_vars = merged_vars ++ hd(after_s.scope_vars_info) - - end_s = %{end_s | - scope_vars_info: [merged_vars | tl(end_s.scope_vars_info)], - lines_to_env: Map.merge(after_s.lines_to_env, s_expr.lines_to_env) + prematch: prematch, + unused: new_unused, + vars: new_current, + calls: s_expr.calls, + lines_to_env: s_expr.lines_to_env, + vars_info: s_expr.vars_info } - # dbg(Map.keys(end_s.lines_to_env)) - # dbg(Map.keys(after_s.lines_to_env)) - # dbg(Map.keys(before_s.lines_to_env)) - # dbg(Map.keys(before_s.lines_to_env)) - # dbg(before_s.scope_vars_info) - # dbg(after_s.scope_vars_info) - # dbg(end_s.scope_vars_info) - end_e = Map.put(ee, :context, Map.get(e, :context)) {e_expr, end_s, end_e} end @@ -2752,7 +2667,7 @@ defmodule ElixirSense.Core.Compiler do def with(meta, args, s, e) do {exprs, opts0} = ElixirUtils.split_opts(args) - s0 = ElixirEnv.reset_unused_vars(s) + s0 = ElixirEnv.reset_vars(s) {e_exprs, {s1, e1, has_match}} = Enum.map_reduce(exprs, {s0, e, false}, &expand_with/2) {e_do, opts1, s2} = expand_with_do(meta, opts0, s, s1, e1) {e_opts, opts2, s3} = expand_with_else(meta, opts1, s2, e, has_match) @@ -2799,7 +2714,7 @@ defmodule ElixirSense.Core.Compiler do s_acc = s_acc |> maybe_move_vars_to_outer_scope |> remove_vars_scope - {e_expr, rest_opts, ElixirEnv.merge_and_check_unused_vars(s_acc, s, e_acc)} + {e_expr, rest_opts, ElixirEnv.merge_vars(s_acc, s, e_acc)} end end @@ -2851,13 +2766,13 @@ defmodule ElixirSense.Core.Compiler do end defp expand_try(_meta, {:do, expr}, s, e) do - {e_expr, se, ee} = ElixirExpand.expand(expr, ElixirEnv.reset_unused_vars(s), e) - {{:do, e_expr}, ElixirEnv.merge_and_check_unused_vars(se, s, ee)} + {e_expr, se, ee} = ElixirExpand.expand(expr, ElixirEnv.reset_vars(s), e) + {{:do, e_expr}, ElixirEnv.merge_vars(se, s, ee)} end defp expand_try(_meta, {:after, expr}, s, e) do - {e_expr, se, ee} = ElixirExpand.expand(expr, ElixirEnv.reset_unused_vars(s), e) - {{:after, e_expr}, ElixirEnv.merge_and_check_unused_vars(se, s, ee)} + {e_expr, se, ee} = ElixirExpand.expand(expr, ElixirEnv.reset_vars(s), e) + {{:after, e_expr}, ElixirEnv.merge_vars(se, s, ee)} end defp expand_try(meta, {:else, _} = else_clause, s, e) do @@ -3000,9 +2915,9 @@ defmodule ElixirSense.Core.Compiler do defp expand_clauses_origin(meta, kind, fun, {key, [_ | _] = clauses}, s, e) do transformer = fn clause, sa -> {e_clause, s_acc, e_acc} = - clause(meta, {kind, key}, fun, clause, ElixirEnv.reset_unused_vars(sa), e) + clause(meta, {kind, key}, fun, clause, ElixirEnv.reset_vars(sa), e) - {e_clause, ElixirEnv.merge_and_check_unused_vars(s_acc, sa, e_acc)} + {e_clause, ElixirEnv.merge_vars(s_acc, sa, e_acc)} end {values, se} = Enum.map_reduce(clauses, s, transformer) @@ -3556,12 +3471,12 @@ defmodule ElixirSense.Core.Compiler do if Enum.any?(left, &is_invalid_arg/1) do raise "defaults_in_args" else - s_reset = ElixirEnv.reset_unused_vars(sa) + s_reset = ElixirEnv.reset_vars(sa) {e_clause, s_acc, e_acc} = ElixirClauses.clause(meta, :fn, &ElixirClauses.head/3, clause, s_reset, e) - {e_clause, ElixirEnv.merge_and_check_unused_vars(s_acc, sa, e_acc)} + {e_clause, ElixirEnv.merge_vars(s_acc, sa, e_acc)} end end @@ -4694,14 +4609,13 @@ defmodule ElixirSense.Core.Compiler do def type_to_signature(_), do: :error def expand(ast, state, env) do - {ast, {state, env}} = # TODO this should handle remote calls, attributes unquotes? - {ast, {_state, _env}} = Macro.prewalk(ast, {state, env}, fn - {:__aliases__, meta, list} = node, {state, env} when is_list(list) -> + {ast, {state, env}} = Macro.prewalk(ast, {state, env}, fn + {:__aliases__, _meta, list} = node, {state, env} when is_list(list) -> {node, state, env} = ElixirExpand.expand(node, state, env) {node, {state, env}} - {:__MODULE__, meta, ctx} = node, {state, env} when is_atom(ctx) -> + {:__MODULE__, _meta, ctx} = node, {state, env} when is_atom(ctx) -> {node, state, env} = ElixirExpand.expand(node, state, env) {node, {state, env}} diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index f62e9dad..5651df77 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -10,20 +10,13 @@ defmodule ElixirSense.Core.MetadataBuilder do alias ElixirSense.Core.Source alias ElixirSense.Core.State alias ElixirSense.Core.State.VarInfo - alias ElixirSense.Core.TypeInfo + # alias ElixirSense.Core.TypeInfo alias ElixirSense.Core.Guard alias ElixirSense.Core.Compiler @scope_keywords [:for, :fn, :with] @block_keywords [:do, :else, :rescue, :catch, :after] @defs [:def, :defp, :defmacro, :defmacrop, :defdelegate, :defguard, :defguardp] - @protocol_types [{:t, [], :type, "@type t :: term"}] - @protocol_functions [ - {:__protocol__, [:atom], :def}, - {:impl_for, [:data], :def}, - {:impl_for!, [:data], :def}, - {:behaviour_info, [:atom], :def} - ] defguardp is_call(call, params) when is_atom(call) and is_list(params) and @@ -145,77 +138,13 @@ defmodule ElixirSense.Core.MetadataBuilder do end end - defp pre_module(ast, state, meta, alias, types \\ [], functions \\ [], options \\ []) do - {position, end_position} = extract_range(meta) - - {full, module, state} = - case Keyword.get(options, :for) do - nil -> - {module, state, env} = expand(alias, state) - {full, state, _env} = alias_defmodule(alias, module, state, env) - - {full, module, state} - - implementations -> - {implementation_alias(alias, implementations), {alias, implementations}, state} - end - - state = - state - |> maybe_add_protocol_implementation(module) - |> add_module(full) - |> add_current_module_to_index(position, end_position, generated: state.generated) - |> new_lexical_scope - |> new_attributes_scope - |> new_vars_scope - - env = get_current_env(state) - {state, env} = maybe_add_protocol_behaviour(module, state, env) - - state = - types - |> Enum.reduce(state, fn {type_name, type_args, spec, kind}, acc -> - acc - |> add_type(env, type_name, type_args, kind, spec, position, end_position, - generated: true - ) - end) - - state = add_module_functions(state, env, functions, position, end_position) - - state - |> result(ast) - end - - defp post_module(ast, state) do - env = get_current_env(state) - - state - |> apply_optional_callbacks(env) - |> remove_attributes_scope - |> remove_lexical_scope - |> maybe_move_vars_to_outer_scope - |> remove_vars_scope - |> remove_module - |> remove_protocol_implementation - |> result(ast) - end - - def pre_protocol(ast, state, meta, module) do - # protocol defines a type `@type t :: term` - # and functions __protocol__/1, impl_for/1, impl_for!/1 - - pre_module(ast, state, meta, module, @protocol_types, @protocol_functions) - end - defp pre_func({type, meta, ast_args}, state, meta, name, params, options \\ []) when is_atom(name) do vars = state |> find_vars(params) - |> merge_same_name_vars() - vars = + _vars = if options[:guards], do: infer_type_from_guards(options[:guards], vars, state), else: vars @@ -226,15 +155,15 @@ defmodule ElixirSense.Core.MetadataBuilder do ast = {type, Keyword.put(meta, :func, true), ast_args} - env = get_current_env(state) + env = nil state |> new_named_func(name, length(params || [])) |> add_func_to_index(env, name, params || [], position, end_position, type, options) |> new_lexical_scope |> new_func_vars_scope - |> add_vars(vars, true) - |> add_current_env_to_line(Keyword.fetch!(meta, :line)) + # |> add_vars(vars, true) + # |> add_current_env_to_line(Keyword.fetch!(meta, :line)) |> result(ast) end @@ -277,7 +206,7 @@ defmodule ElixirSense.Core.MetadataBuilder do |> result(ast) end - defp pre_scope_keyword(ast, state, line) do + defp pre_scope_keyword(ast, state, _line) do state = case ast do {:for, _, _} -> @@ -288,7 +217,7 @@ defmodule ElixirSense.Core.MetadataBuilder do end state - |> add_current_env_to_line(line) + # |> add_current_env_to_line(line) |> new_vars_scope |> result(ast) end @@ -342,19 +271,18 @@ defmodule ElixirSense.Core.MetadataBuilder do |> result(ast) end - defp pre_clause({_clause, meta, _} = ast, state, lhs) do - line = meta |> Keyword.fetch!(:line) + defp pre_clause({_clause, _meta, _} = ast, state, lhs) do - vars = + + _vars = state |> find_vars(lhs, Enum.at(state.binding_context, 0)) - |> merge_same_name_vars() state |> new_lexical_scope |> new_vars_scope - |> add_vars(vars, true) - |> add_current_env_to_line(line) + # |> add_vars(vars, true) + # |> add_current_env_to_line(line) |> result(ast) end @@ -366,99 +294,14 @@ defmodule ElixirSense.Core.MetadataBuilder do |> result(ast) end - defp pre_spec(ast, state, meta, type_name, type_args, spec, kind) do - spec = TypeInfo.typespec_to_string(kind, spec) - - {position = {line, _column}, end_position} = extract_range(meta) - env = get_current_env(state) - - state = - if kind in [:callback, :macrocallback] do - state - |> add_func_to_index( - env, - :behaviour_info, - [{:atom, meta, nil}], - position, - end_position, - :def, - generated: true - ) - else - state - end - - state - |> add_spec(env, type_name, type_args, spec, kind, position, end_position, - generated: state.generated - ) - |> add_typespec_namespace(type_name, length(type_args)) - |> add_current_env_to_line(line) - |> result(ast) - end - - defp post_string_literal(ast, state, line, str) do + defp post_string_literal(ast, _state, _line, str) do str |> Source.split_lines() |> Enum.with_index() - |> Enum.reduce(state, fn {_s, i}, acc -> add_current_env_to_line(acc, line + i) end) + # |> Enum.reduce(state, fn {_s, i}, acc -> add_current_env_to_line(acc, line + i) end) |> result(ast) end - defp pre( - {:defmodule, meta, [module, _]} = ast, - state - ) do - pre_module(ast, state, meta, module) - end - - defp pre( - {:defprotocol, meta, [protocol, _]} = ast, - state - ) do - pre_protocol(ast, state, meta, protocol) - end - - defp pre( - {:defimpl, meta, [protocol, impl_args | _]} = ast, - state - ) do - pre_protocol_implementation(ast, state, meta, protocol, impl_args) - end - - defp pre( - {:defdelegate, meta, [{name, meta2, params}, body]}, - state - ) - when is_atom(name) and is_list(body) do - ast_without_params = {:defdelegate, meta, [{name, add_no_call(meta2), []}, body]} - target_module = body |> Keyword.get(:to) - - target_function = - case body |> Keyword.get(:as) do - nil -> {:ok, name} - as when is_atom(as) -> {:ok, as} - _ -> :error - end - - options = - with mod = target_module, - {:ok, target_function} <- target_function do - [target: {mod, target_function}] - else - _ -> [] - end - - pre_func(ast_without_params, state, meta, name, params, options) - end - - # quote do - # quote options do - defp pre({:quote, _meta, list}, state) when is_list(list) do - # replace with an empty AST node - {[], state} - end - # ex_unit describe defp pre( {:describe, meta, [name, _body]} = ast, @@ -473,7 +316,7 @@ defmodule ElixirSense.Core.MetadataBuilder do |> add_call_to_line({nil, :describe, 2}, {line, column}) %{state | context: Map.put(state.context, :ex_unit_describe, name)} - |> add_current_env_to_line(line) + # |> add_current_env_to_line(line) |> result(ast) end @@ -613,271 +456,23 @@ defmodule ElixirSense.Core.MetadataBuilder do pre_func(ast_without_params, state, meta, name, params) end - defp pre( - {:@, meta_attr, [{:moduledoc, meta, [doc_arg]}]}, - state - ) do - line = Keyword.fetch!(meta_attr, :line) - column = Keyword.fetch!(meta_attr, :column) - new_ast = {:@, meta_attr, [{:moduledoc, add_no_call(meta), [doc_arg]}]} - env = get_current_env(state) - - state - |> add_moduledoc_positions( - env, - meta_attr - ) - |> register_doc(env, :moduledoc, doc_arg) - |> result(new_ast) - end - - defp pre( - {:@, meta_attr, [{doc, meta, [doc_arg]}]}, - state - ) - when doc in [:doc, :typedoc] do - new_ast = {:@, meta_attr, [{doc, add_no_call(meta), [doc_arg]}]} - env = get_current_env(state) - - state - |> register_doc(env, doc, doc_arg) - |> result(new_ast) - end - - defp pre( - {:@, meta_attr, [{:impl, meta, [impl_arg]}]}, - state - ) do - new_ast = {:@, meta_attr, [{:impl, add_no_call(meta), [impl_arg]}]} - env = get_current_env(state) - # impl adds sets :hidden by default - state - |> register_doc(env, :doc, :impl) - |> result(new_ast) - end - - defp pre( - {:@, meta_attr, [{:optional_callbacks, meta, [args]}]}, - state - ) do - new_ast = {:@, meta_attr, [{:optional_callbacks, add_no_call(meta), [args]}]} - - state - |> register_optional_callbacks(args) - |> result(new_ast) - end - - defp pre( - {:@, meta_attr, [{:deprecated, meta, [deprecated_arg]}]}, - state - ) do - new_ast = {:@, meta_attr, [{:deprecated, add_no_call(meta), [deprecated_arg]}]} - env = get_current_env(state) - # treat @deprecated message as @doc deprecated: message - state - |> register_doc(env, :doc, deprecated: deprecated_arg) - |> result(new_ast) - end - - defp pre({:@, _meta, [{:behaviour, _, [_arg]}]} = ast, state) do - {ast, state, _env} = expand(ast, state) - {ast, state} - end - - # protocol derive - defp pre( - {:@, meta, [{:derive, _, [derived_protos]}]} = ast, - state - ) do - line = Keyword.fetch!(meta, :line) - column = Keyword.fetch!(meta, :column) - current_module = state |> get_current_module - - List.wrap(derived_protos) - |> Enum.map(fn - {proto, _opts} -> proto - proto -> proto - end) - |> Enum.reduce(state, fn proto, acc -> - case expand(proto, acc) do - {proto_module, acc, _env} when is_atom(proto_module) -> - # protocol implementation module for Any - mod_any = Module.concat(proto_module, Any) - - # protocol implementation module built by @derive - mod = Module.concat(proto_module, current_module) - - case acc.mods_funs_to_positions[{mod_any, nil, nil}] do - nil -> - # implementation for: Any not detected (is in other file etc.) - acc - |> add_module_to_index(mod, {line, column}, nil, generated: true) - - _any_mods_funs -> - # copy implementation for: Any - copied_mods_funs_to_positions = - for {{module, fun, arity}, val} <- acc.mods_funs_to_positions, - module == mod_any, - into: %{}, - do: {{mod, fun, arity}, val} - - %{ - acc - | mods_funs_to_positions: - acc.mods_funs_to_positions |> Map.merge(copied_mods_funs_to_positions) - } - end - - :error -> - acc - end - end) - |> result(ast) - end - - defp pre( - {:@, meta_attr, - [ - {kind, kind_meta, - [{:"::", _meta, _params = [{name, _, type_args}, _type_def]} = spec] = kind_args} - ]}, - state - ) - when kind in [:type, :typep, :opaque] and is_atom(name) and - (is_nil(type_args) or is_list(type_args)) do - ast = {:@, meta_attr, [{kind, add_no_call(kind_meta), kind_args}]} - spec = expand_aliases_in_ast(state, spec) - type_args = List.wrap(type_args) - - spec = TypeInfo.typespec_to_string(kind, spec) - - {position = {line, _column}, end_position} = extract_range(meta_attr) - env = get_current_env(state) - - state - |> add_type(env, name, type_args, spec, kind, position, end_position, - generated: state.generated - ) - |> add_typespec_namespace(name, length(type_args)) - |> add_current_env_to_line(line) - |> result(ast) - end - - defp pre( - {:@, meta_attr, - [ - {kind, kind_meta, - [{:when, _, [{:"::", _meta, _params = [{name, _, type_args}, _type_def]}, _]} = spec] = - kind_args} - ]}, - state - ) - when kind in [:spec, :callback, :macrocallback] and is_atom(name) and - (is_nil(type_args) or is_list(type_args)) do - pre_spec( - {:@, meta_attr, - [ - {kind, add_no_call(kind_meta), kind_args} - ]}, - state, - meta_attr, - name, - expand_aliases_in_ast(state, List.wrap(type_args)), - expand_aliases_in_ast(state, spec), - kind - ) - end - - defp pre( - {:@, meta_attr, - [ - {kind, meta_kind, - [{:"::", _meta, _params = [{name, _, type_args}, _type_def]} = spec] = kind_args} - ]}, - state - ) - when kind in [:spec, :callback, :macrocallback] and is_atom(name) and - (is_nil(type_args) or is_list(type_args)) do - pre_spec( - {:@, meta_attr, [{kind, add_no_call(meta_kind), kind_args}]}, - state, - meta_attr, - name, - expand_aliases_in_ast(state, List.wrap(type_args)), - expand_aliases_in_ast(state, spec), - kind - ) - end - # incomplete spec # @callback my(integer) defp pre( - {:@, meta_attr, [{kind, meta_kind, [{name, meta_name, type_args}]} = spec]}, - state + {:@, _meta_attr, [{kind, _meta_kind, [{name, _meta_name, type_args}]} = _spec]}, + _state ) when kind in [:spec, :callback, :macrocallback] and is_atom(name) and (is_nil(type_args) or is_list(type_args)) do - pre_spec( - {:@, meta_attr, [{kind, add_no_call(meta_kind), [{name, meta_name, type_args}]}]}, - state, - meta_attr, - name, - expand_aliases_in_ast(state, List.wrap(type_args)), - expand_aliases_in_ast(state, spec), - kind - ) - end - - defp pre({:@, meta_attr, [{name, meta, params}]}, state) when is_atom(name) do - name_string = Atom.to_string(name) - - if String.match?(name_string, ~r/^[_\p{Ll}\p{Lo}][\p{L}\p{N}_]*[?!]?$/u) and - not String.starts_with?(name_string, "__atom_elixir_marker_") do - line = Keyword.fetch!(meta_attr, :line) - column = Keyword.fetch!(meta_attr, :column) - - binding = - case List.wrap(params) do - [] -> - {nil, false} - - [param] -> - {get_binding_type(state, param), true} - - _ -> - :error - end - - case binding do - {type, is_definition} -> - new_ast = {:@, meta_attr, [{name, add_no_call(meta), params}]} - - state - |> add_attribute(name, type, is_definition, {line, column}) - |> add_current_env_to_line(line) - |> result(new_ast) - - _ -> - {[], state} - end - else - # most likely not an attribute - {[], state} - end - end - - defp pre( - {directive, _meta, _args} = ast, - state - ) - when directive in [:alias, :require, :import, :use] do - {ast, state, _env} = expand(ast, state) - {ast, state} - end - - defp pre({:defoverridable, meta, [arg]} = ast, state) do - {ast, state, _env} = expand(ast, state) - {ast, state} + # pre_spec( + # {:@, meta_attr, [{kind, add_no_call(meta_kind), [{name, meta_name, type_args}]}]}, + # state, + # meta_attr, + # name, + # expand_aliases_in_ast(state, List.wrap(type_args)), + # expand_aliases_in_ast(state, spec), + # kind + # ) end defp pre({atom, meta, [_ | _]} = ast, state) @@ -907,105 +502,16 @@ defmodule ElixirSense.Core.MetadataBuilder do result(state, {atom, meta, [lhs, rhs]}) end - defp pre({var_or_call, meta, nil} = ast, state) - when is_atom(var_or_call) and var_or_call != :__MODULE__ do - line = Keyword.fetch!(meta, :line) - column = Keyword.fetch!(meta, :column) - - if Enum.any?(get_current_vars(state), &(&1.name == var_or_call)) do - vars = - state - |> find_vars(ast) - |> merge_same_name_vars() - - add_vars(state, vars, false) - else - # pre Elixir 1.4 local call syntax - # TODO can we remove when we require elixir 1.15+? - # it's only legal inside typespecs - # credo:disable-for-next-line - if not Keyword.get(meta, :no_call, false) and - (Version.match?(System.version(), "< 1.15.0-dev") or - match?([{:typespec, _, _} | _], state.scopes)) do - add_call_to_line(state, {nil, var_or_call, 0}, {line, column}) - else - state - end - end - |> add_current_env_to_line(line) - |> result(ast) - end - defp pre({:when, meta, [lhs, rhs]}, state) do - vars = + _vars = state |> find_vars(lhs) - |> merge_same_name_vars() state - |> add_vars(vars, true) + # |> add_vars(vars, true) |> result({:when, meta, [:_, rhs]}) end - defp pre({type, meta, fields} = ast, state) - when type in [:defstruct, :defexception] do - {position, end_position} = extract_range(meta) - - fields = - case fields do - [fields] when is_list(fields) -> - fields - |> Enum.filter(fn - field when is_atom(field) -> true - {field, _} when is_atom(field) -> true - _ -> false - end) - |> Enum.map(fn - field when is_atom(field) -> {field, nil} - {field, value} when is_atom(field) -> {field, value} - end) - - _ -> - [] - end - - env = get_current_env(state) - - state - |> add_struct_or_exception(env, type, fields, position, end_position) - |> result(ast) - end - - # transform `a |> b(c)` calls into `b(a, c)` - defp pre({:|>, _, [params_1, {call, meta, params_rest}]}, state) do - params = [params_1 | params_rest || []] - pre({call, meta, params}, state) - end - - # transform external and local func capture into fake call - defp pre({:&, _, [{:/, _, [fun, arity]}]}, state) when is_integer(arity) do - fake_params = - if arity == 0 do - [] - else - for _ <- 1..arity, do: nil - end - - call = - case fun do - {func, position, nil} -> - {func, position, fake_params} - - {{:., _, [_ | _]} = ast_part, position, []} -> - {ast_part, position, fake_params} - - _ -> - nil - end - - pre(call, state) - end - defp pre( {:case, meta, [ @@ -1022,55 +528,7 @@ defmodule ElixirSense.Core.MetadataBuilder do state |> push_binding_context(get_binding_type(state, condition_ast)) |> add_call_to_line({nil, :case, 2}, {line, column}) - |> add_current_env_to_line(line) - |> result(ast) - end - - defp pre( - {{:., meta1, [{:__aliases__, _, [:Record]} = module_expression, call]}, _meta, - params = [name, _]} = ast, - state - ) - when is_call(call, params) and call in [:defrecord, :defrecordp] and - is_atom(name) do - {position = {line, column}, end_position} = extract_range(meta1) - - env = get_current_env(state) - {module, state, env} = expand(module_expression, state, env) - - type = - case call do - :defrecord -> :defmacro - :defrecordp -> :defmacrop - end - - options = [generated: true] - - shift = if state.generated, do: 0, else: 1 - - state - |> new_named_func(name, 1) - |> add_func_to_index( - env, - name, - [{:\\, [], [{:args, [], nil}, []]}], - position, - end_position, - type, - options - ) - |> new_named_func(name, 2) - |> add_func_to_index( - env, - name, - [{:record, [], nil}, {:args, [], nil}], - position, - end_position, - type, - options - ) - |> add_call_to_line({module, call, length(params)}, {line, column + shift}) - |> add_current_env_to_line(line) + # |> add_current_env_to_line(line) |> result(ast) end @@ -1091,7 +549,7 @@ defmodule ElixirSense.Core.MetadataBuilder do state |> add_call_to_line({module, call, length(params)}, {line, column + shift}) - |> add_current_env_to_line(line) + # |> add_current_env_to_line(line) |> result(ast) rescue _ -> @@ -1113,7 +571,7 @@ defmodule ElixirSense.Core.MetadataBuilder do state |> add_call_to_line({module, call, length(params)}, {line, column + shift}) - |> add_current_env_to_line(line) + # |> add_current_env_to_line(line) |> result(ast) end @@ -1129,7 +587,7 @@ defmodule ElixirSense.Core.MetadataBuilder do state |> add_call_to_line({{:attribute, attribute}, call, length(params)}, {line, column + shift}) - |> add_current_env_to_line(line) + # |> add_current_env_to_line(line) |> result(ast) end @@ -1145,7 +603,7 @@ defmodule ElixirSense.Core.MetadataBuilder do state |> add_call_to_line({nil, {:attribute, attribute}, length(params)}, {line, column + shift}) - |> add_current_env_to_line(line) + # |> add_current_env_to_line(line) |> result(ast) end @@ -1161,7 +619,7 @@ defmodule ElixirSense.Core.MetadataBuilder do state |> add_call_to_line({nil, {:variable, variable}, length(params)}, {line, column + shift}) - |> add_current_env_to_line(line) + # |> add_current_env_to_line(line) |> result(ast) end @@ -1177,7 +635,7 @@ defmodule ElixirSense.Core.MetadataBuilder do state |> add_call_to_line({{:variable, variable}, call, length(params)}, {line, column + shift}) - |> add_current_env_to_line(line) + # |> add_current_env_to_line(line) |> result(ast) end @@ -1193,7 +651,7 @@ defmodule ElixirSense.Core.MetadataBuilder do state |> add_call_to_line({module, call, length(params)}, {line, column + shift}) - |> add_current_env_to_line(line) + # |> add_current_env_to_line(line) |> result(ast) end @@ -1218,11 +676,11 @@ defmodule ElixirSense.Core.MetadataBuilder do end state - |> add_current_env_to_line(line) + # |> add_current_env_to_line(line) |> result(ast) else state - |> add_current_env_to_line(line) + # |> add_current_env_to_line(line) |> result(ast) end end @@ -1234,9 +692,9 @@ defmodule ElixirSense.Core.MetadataBuilder do nil -> {ast, state} - line -> + _line -> state - |> add_current_env_to_line(line) + # |> add_current_env_to_line(line) |> result(ast) end end @@ -1245,29 +703,6 @@ defmodule ElixirSense.Core.MetadataBuilder do {ast, state} end - defp post( - {:defmodule, _meta, [_module, _]} = ast, - state - ) do - post_module(ast, state) - end - - defp post( - {:defprotocol, _meta, [_protocol, _]} = ast, - state - ) do - env = get_current_env(state) - state = generate_protocol_callbacks(state, env) - post_module(ast, state) - end - - defp post( - {:defimpl, _meta, [_protocol, _impl_args | _]} = ast, - state - ) do - post_module(ast, state) - end - # ex_unit describe defp post( {:describe, _meta, [name, _body]} = ast, @@ -1374,7 +809,7 @@ defmodule ElixirSense.Core.MetadataBuilder do defp post({atom, meta, [lhs, rhs]} = ast, state) when atom in [:=, :<-] do - line = Keyword.fetch!(meta, :line) + _line = Keyword.fetch!(meta, :line) match_context_r = get_binding_type(state, rhs) match_context_r = @@ -1386,7 +821,7 @@ defmodule ElixirSense.Core.MetadataBuilder do vars_l = find_vars(state, lhs, match_context_r) - vars = + _vars = case rhs do {:=, _, [nested_lhs, _nested_rhs]} -> match_context_l = get_binding_type(state, lhs) @@ -1397,17 +832,10 @@ defmodule ElixirSense.Core.MetadataBuilder do _ -> vars_l end - |> merge_same_name_vars() - - # Variables and calls were added for the left side of the assignment in `pre` call, but without - # the context of an assignment. Thus, they have to be removed here. On their place there will - # be added new variables having types merged with types of corresponding deleted variables. - remove_positions = Enum.flat_map(vars, fn %VarInfo{positions: positions} -> positions end) state - |> remove_calls(remove_positions) - |> merge_new_vars(vars, remove_positions) - |> add_current_env_to_line(line) + # |> remove_calls(remove_positions) + # |> add_current_env_to_line(line) |> result(ast) end @@ -1659,62 +1087,6 @@ defmodule ElixirSense.Core.MetadataBuilder do [{:no_call, true} | meta] end - defp pre_protocol_implementation( - ast, - state, - meta, - protocol, - for_expression - ) do - # TODO pass env - {protocol, state, _env} = expand(protocol, state) - implementations = get_implementations_from_for_expression(state, for_expression) - - pre_module(ast, state, meta, protocol, [], [{:__impl__, [:atom], :def}], for: implementations) - end - - defp get_implementations_from_for_expression(state, for: for_expression) do - # TODO fold state? - for_expression - |> List.wrap() - |> Enum.map(fn alias -> - {module, _state, _env} = expand(alias, state) - module - end) - end - - defp get_implementations_from_for_expression(state, _other) do - [state |> get_current_module] - end - - defp maybe_add_protocol_behaviour({protocol, _}, state, env) do - {_, state, env} = add_behaviour(protocol, state, env) - {state, env} - end - - defp maybe_add_protocol_behaviour(_, state, env), do: {state, env} - - defp expand_aliases_in_ast(state, ast) do - # TODO shouldn't that handle more cases? - Macro.prewalk(ast, fn - {:__aliases__, meta, [Elixir]} -> - {:__aliases__, meta, [Elixir]} - - {:__aliases__, meta, _list} = module -> - {module, _state, _env} = expand(module, state) - list = module |> Module.split() |> Enum.map(&String.to_atom/1) - {:__aliases__, meta, list} - - {:__MODULE__, meta, nil} = module -> - {module, _state, _env} = expand(module, state) - list = module |> Module.split() |> Enum.map(&String.to_atom/1) - {:__aliases__, meta, list} - - other -> - other - end) - end - defp ex_unit_test_name(state, name) do case state.context[:ex_unit_describe] do nil -> "test #{name}" diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 20600b22..9bfb5b4f 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -43,8 +43,7 @@ defmodule ElixirSense.Core.State do scope_attributes: list(list(atom)), behaviours: %{optional(module) => [module]}, specs: specs_t, - vars_info: list(list(ElixirSense.Core.State.VarInfo.t())), - scope_vars_info: list(list(ElixirSense.Core.State.VarInfo.t())), + vars_info: list(%{optional({atom, non_neg_integer}) => ElixirSense.Core.State.VarInfo.t()}), scope_id_count: non_neg_integer, scope_ids: list(scope_id_t), vars_info_per_scope_id: vars_info_per_scope_id_t, @@ -87,10 +86,9 @@ defmodule ElixirSense.Core.State do scope_attributes: [[]], behaviours: %{}, specs: %{}, - vars_info: [[]], - scope_vars_info: [[]], + vars_info: [%{}], vars: {%{}, false}, - unused: {%{}, 0}, + unused: 0, prematch: :raise, stacktrace: false, caller: false, @@ -324,57 +322,7 @@ defmodule ElixirSense.Core.State do state.requires |> :lists.reverse() |> List.flatten() |> Enum.uniq() |> Enum.sort() end - def get_current_env(%__MODULE__{} = state) do - current_module = get_current_module(state) - current_functions = state.functions |> hd() - current_macros = state.macros |> hd() - current_requires = current_requires(state) - current_aliases = current_aliases(state) - current_vars = state |> get_current_vars() - current_attributes = state |> get_current_attributes() - current_behaviours = state.behaviours |> Map.get(current_module, []) - current_scope = hd(state.scopes) - current_scope_id = hd(state.scope_ids) - current_scope_protocol = hd(state.protocols) - - current_function = - case current_scope do - {_name, _arity} = function -> function - _ -> nil - end - - current_typespec = - case current_scope do - {:typespec, name, arity} -> {name, arity} - _ -> nil - end - - versioned_vars = - current_vars - |> Enum.map(&{&1.name, nil}) - |> Enum.sort() - |> Enum.with_index() - |> Map.new() - - %Env{ - functions: current_functions, - macros: current_macros, - requires: current_requires, - aliases: current_aliases, - module: current_module, - function: current_function, - typespec: current_typespec, - vars: current_vars, - versioned_vars: versioned_vars, - attributes: current_attributes, - behaviours: current_behaviours, - scope_id: current_scope_id, - protocol: current_scope_protocol - } - end - def get_current_env(%__MODULE__{} = state, macro_env) do - current_vars = state.vars |> elem(0) current_attributes = state |> get_current_attributes() current_behaviours = state.behaviours |> Map.get(macro_env.module, []) @@ -387,15 +335,11 @@ defmodule ElixirSense.Core.State do |> elem(0) |> Map.new() - # TODO this is a hack that hides a problem somewhere - vars = state - |> get_current_vars() + # vars_info has both read and write vars + # filter to return only read + vars = hd(state.vars_info) |> Map.values |> Enum.filter(& Map.has_key?(elem(state.vars, 0), {&1.name, nil})) - # dbg(vars) - # dbg(state.vars) - # dbg(state.scope_vars_info) - current_protocol = case state.protocol do nil -> nil {protocol, for_list} -> @@ -427,45 +371,11 @@ defmodule ElixirSense.Core.State do state.module |> hd end - def add_current_env_to_line(%__MODULE__{} = state, line) when is_integer(line) do - previous_env = state.lines_to_env[line] - current_env = get_current_env(state) - - env = merge_env_vars(current_env, previous_env) - %__MODULE__{state | lines_to_env: Map.put(state.lines_to_env, line, env)} - end - def add_current_env_to_line(%__MODULE__{} = state, line, macro_env) when is_integer(line) do - _previous_env = state.lines_to_env[line] - current_env = get_current_env(state, macro_env) - - # TODO - # env = merge_env_vars(current_env, previous_env) - env = current_env + env = get_current_env(state, macro_env) %__MODULE__{state | lines_to_env: Map.put(state.lines_to_env, line, env)} end - defp merge_env_vars(%Env{vars: current_vars} = current_env, previous_env) do - case previous_env do - nil -> - current_env - - %Env{vars: previous_vars} -> - vars_to_preserve = - Enum.filter(previous_vars, fn previous_var -> - Enum.all?(current_vars, fn current_var -> - current_var_positions = MapSet.new(current_var.positions) - - previous_var.positions - |> MapSet.new() - |> MapSet.disjoint?(current_var_positions) - end) - end) - - %Env{current_env | vars: current_vars ++ vars_to_preserve} - end - end - def add_moduledoc_positions( %__MODULE__{} = state, env, @@ -491,7 +401,7 @@ defmodule ElixirSense.Core.State do def add_first_alias_positions( %__MODULE__{} = state, - env = %{module: module, function: nil}, + %{module: module, function: nil}, meta ) do # TODO shouldn't that look for end_of_expression @@ -510,9 +420,6 @@ defmodule ElixirSense.Core.State do end def add_first_alias_positions(%__MODULE__{} = state, _env, _meta), do: state - # TODO remove this - def add_call_to_line(%__MODULE__{} = state, {nil, :__block__, _}, _position), do: state - def add_call_to_line( %__MODULE__{} = state, {{:@, _meta, [{name, _name_meta, _args}]}, func, arity}, @@ -560,26 +467,6 @@ defmodule ElixirSense.Core.State do %__MODULE__{state | calls: calls} end - def remove_calls(%__MODULE__{} = state, positions) do - Enum.reduce(positions, state, fn {line, _column} = position, state -> - case state.calls[line] do - nil -> - state - - calls -> - updated_calls = Enum.reject(calls, fn call_info -> call_info.position == position end) - - case updated_calls do - [] -> - %__MODULE__{state | calls: Map.delete(state.calls, line)} - - _non_empty_list -> - %__MODULE__{state | calls: Map.put(state.calls, line, updated_calls)} - end - end - end) - end - def add_struct(%__MODULE__{} = state, env, type, fields) do structs = state.structs @@ -588,29 +475,10 @@ defmodule ElixirSense.Core.State do %__MODULE__{state | structs: structs} end - def get_current_vars(%__MODULE__{} = state) do - state.scope_vars_info - |> List.flatten() - |> reduce_vars() - |> Enum.flat_map(fn {_var, scopes} -> scopes end) - end - - def get_current_vars_refs(%__MODULE__{} = state) do - state.scope_vars_info |> List.flatten() - end - def get_current_attributes(%__MODULE__{} = state) do state.scope_attributes |> :lists.reverse() |> List.flatten() end - def is_variable_defined(%__MODULE__{} = state, var_name) do - state - |> get_current_vars_refs() - |> Enum.any?(fn %VarInfo{name: name, is_definition: is_definition} -> - name == var_name && is_definition - end) - end - def add_mod_fun_to_position( %__MODULE__{} = state, {module, fun, arity}, @@ -924,8 +792,7 @@ defmodule ElixirSense.Core.State do state | scope_ids: [scope_id | state.scope_ids], scope_id_count: scope_id, - vars_info: [[] | state.vars_info], - scope_vars_info: [[] | state.scope_vars_info] + vars_info: [hd(state.vars_info) | state.vars_info] } end @@ -950,8 +817,7 @@ defmodule ElixirSense.Core.State do state | scope_ids: [scope_id | state.scope_ids], scope_id_count: scope_id, - vars_info: [[] | state.vars_info], - scope_vars_info: [[]] + vars_info: [%{} | state.vars_info] } end @@ -964,7 +830,6 @@ defmodule ElixirSense.Core.State do state | scope_ids: tl(state.scope_ids), vars_info: tl(state.vars_info), - scope_vars_info: tl(state.scope_vars_info), vars_info_per_scope_id: update_vars_info_per_scope_id(state) } end @@ -974,76 +839,15 @@ defmodule ElixirSense.Core.State do state | scope_ids: tl(state.scope_ids), vars_info: tl(state.vars_info), - scope_vars_info: tl(state.vars_info), vars_info_per_scope_id: update_vars_info_per_scope_id(state) } end def update_vars_info_per_scope_id(state) do [scope_id | _other_scope_ids] = state.scope_ids + [current_scope_vars | _other_scope_vars] = state.vars_info - [current_scope_vars | other_scope_vars] = state.scope_vars_info - - current_scope_reduced_vars = reduce_vars(current_scope_vars) - - vars_info = - other_scope_vars - |> List.flatten() - |> reduce_vars(current_scope_reduced_vars, false) - |> Enum.flat_map(fn {_var, scopes} -> scopes end) - # |> dbg - - Map.put(state.vars_info_per_scope_id, scope_id, vars_info) - end - - defp reduce_vars(vars, initial_acc \\ %{}, keep_all_same_name_vars \\ true) do - Enum.reduce(vars, initial_acc, fn %VarInfo{name: var, positions: positions} = el, acc -> - updated = - case acc[var] do - nil -> - [el] - - [%VarInfo{is_definition: false} = var_info | same_name_vars] -> - type = - if Enum.all?(positions, fn position -> position in var_info.positions end) do - merge_type(el.type, var_info.type) - else - el.type - end - - [ - %VarInfo{ - el - | positions: (var_info.positions ++ positions) |> Enum.uniq() |> Enum.sort(), - type: type - } - | same_name_vars - ] - - [%VarInfo{is_definition: true} = var_info | same_name_vars] -> - cond do - Enum.all?(positions, fn position -> position in var_info.positions end) -> - type = merge_type(el.type, var_info.type) - - [ - %VarInfo{ - var_info - | positions: (var_info.positions ++ positions) |> Enum.uniq() |> Enum.sort(), - type: type - } - | same_name_vars - ] - - keep_all_same_name_vars -> - [el, var_info | same_name_vars] - - true -> - [var_info | same_name_vars] - end - end - - Map.put(acc, var, updated) - end) + Map.put(state.vars_info_per_scope_id, scope_id, current_scope_vars |> Map.values) end def remove_attributes_scope(%__MODULE__{} = state) do @@ -1295,48 +1099,40 @@ defmodule ElixirSense.Core.State do %__MODULE__{state | specs: specs} end - def add_var( - %__MODULE__{} = state, - %VarInfo{name: var_name} = var_info, - is_definition - ) do - # dbg(var_info) - scope = hd(state.scopes) + def add_var_write(%__MODULE__{} = state, {name, meta, nil}) when name != :_ do + version = meta[:version] + line = meta[:line] + column = meta[:column] + scope_id = hd(state.scope_ids) + info = %VarInfo{name: name, is_definition: true, positions: [{line, column}], scope_id: scope_id} + [vars_from_scope | other_vars] = state.vars_info - is_var_defined = is_variable_defined(state, var_name) - var_name_as_string = Atom.to_string(var_name) - - vars_from_scope = - case {is_definition and var_info.is_definition, is_var_defined, var_name_as_string} do - {_, _, "__MODULE__"} -> - raise "foo" - - {_, _, "_"} -> - vars_from_scope - - {_, _, ^scope} -> - vars_from_scope - - {is_definition, is_var_defined, _} when is_definition or is_var_defined -> - [ - %VarInfo{ - var_info - | scope_id: hd(state.scope_ids), - is_definition: is_definition - } - | vars_from_scope - ] + vars_from_scope = Map.put(vars_from_scope, {name, version}, info) - _ -> - vars_from_scope - end + %__MODULE__{ + state + | vars_info: [vars_from_scope | other_vars] + } + end + def add_var_write(%__MODULE__{} = state, _), do: state + + def add_var_read(%__MODULE__{} = state, {name, meta, nil}) when name != :_ do + version = meta[:version] + line = meta[:line] + column = meta[:column] + + [vars_from_scope | other_vars] = state.vars_info + info = Map.fetch!(vars_from_scope, {name, version}) + + info = %VarInfo{info | positions: (info.positions ++ [{line, column}]) |> Enum.uniq} + vars_from_scope = Map.put(vars_from_scope, {name, version}, info) %__MODULE__{ state - | vars_info: [vars_from_scope | other_vars], - scope_vars_info: [vars_from_scope | tl(state.scope_vars_info)] + | vars_info: [vars_from_scope | other_vars] } end + def add_var_read(%__MODULE__{} = state, _), do: state @builtin_attributes ElixirSense.Core.BuiltinAttributes.all() @@ -1509,87 +1305,6 @@ defmodule ElixirSense.Core.State do end end - def add_vars(%__MODULE__{} = state, vars, is_definition) do - vars |> Enum.reduce(state, fn var, state -> add_var(state, var, is_definition) end) - end - - # Simultaneously performs two operations: - # - deletes variables that contain any of `remove_positions` - # - adds `vars` to the state, but with types merged with the corresponding removed variables - def merge_new_vars(%__MODULE__{} = state, vars, remove_positions) do - {state, vars} = - Enum.reduce(remove_positions, {state, vars}, fn position, {state, vars} -> - case pop_var(state, position) do - {nil, state} -> - {state, vars} - - {removed_var, state} -> - vars = - Enum.reduce(vars, [], fn %VarInfo{positions: positions} = var, vars -> - if positions == removed_var.positions do - type = merge_type(var.type, removed_var.type) - - [%VarInfo{var | type: type} | vars] - else - [var | vars] - end - end) - - {state, vars} - end - end) - - add_vars(state, vars, true) - end - - defp pop_var(%__MODULE__{} = state, position) do - [current_scope_vars | other_vars] = state.vars_info - - var = - Enum.find(current_scope_vars, fn %VarInfo{positions: positions} -> position in positions end) - - current_scope_vars = - Enum.reject(current_scope_vars, fn %VarInfo{positions: positions} -> - position in positions - end) - - state = %__MODULE__{ - state - | vars_info: [current_scope_vars | other_vars], - scope_vars_info: [current_scope_vars | tl(state.scope_vars_info)] - } - - {var, state} - end - - def merge_same_name_vars(vars) do - vars - |> Enum.reduce(%{}, fn %VarInfo{name: var, positions: positions} = el, acc -> - updated = - case acc[var] do - nil -> - el - - %VarInfo{} = var_info -> - type = - if Enum.all?(positions, fn position -> position in var_info.positions end) do - merge_type(el.type, var_info.type) - else - el.type - end - - %VarInfo{ - var_info - | positions: (var_info.positions ++ positions) |> Enum.uniq() |> Enum.sort(), - type: type - } - end - - Map.put(acc, var, updated) - end) - |> Enum.map(fn {_name, var_info} -> var_info end) - end - defp merge_type(nil, new), do: new defp merge_type(old, nil), do: old defp merge_type(old, old), do: old @@ -1598,7 +1313,7 @@ defmodule ElixirSense.Core.State do def default_env, do: %ElixirSense.Core.State.Env{} def expand(ast, %__MODULE__{} = state) do - expand(ast, state, get_current_env(state)) + expand(ast, state, nil) end def expand({:@, meta, [{:behaviour, _, [arg]}]}, state, env) do @@ -1606,7 +1321,7 @@ defmodule ElixirSense.Core.State do state = state - |> add_current_env_to_line(line) + |> add_current_env_to_line(line, env) {arg, state, env} = expand(arg, state, env) add_behaviour(arg, state, env) @@ -1662,7 +1377,6 @@ defmodule ElixirSense.Core.State do def expand({:alias, meta, [arg, opts]}, state, env) do line = Keyword.fetch!(meta, :line) - column = Keyword.fetch!(meta, :column) {arg, state, env} = expand(arg, state, env) # options = expand(no_alias_opts(arg), state, env, env) @@ -1682,7 +1396,7 @@ defmodule ElixirSense.Core.State do state = state - |> add_current_env_to_line(line) + |> add_current_env_to_line(line, env) |> add_alias(alias_tuple) {arg, state, env} @@ -1702,7 +1416,7 @@ defmodule ElixirSense.Core.State do if is_atom(arg) do state = state - |> add_current_env_to_line(line) + |> add_current_env_to_line(line, env) state = case Keyword.get(opts, :as) do @@ -1731,7 +1445,7 @@ defmodule ElixirSense.Core.State do if is_atom(arg) do state = state - |> add_current_env_to_line(line) + |> add_current_env_to_line(line, env) |> add_require(arg) |> add_import(arg, opts) @@ -1752,7 +1466,7 @@ defmodule ElixirSense.Core.State do state = state - |> add_current_env_to_line(line) + |> add_current_env_to_line(line, env) # TODO pass env expanded_ast = @@ -1796,57 +1510,16 @@ defmodule ElixirSense.Core.State do {ast, state, env} end - def maybe_move_vars_to_outer_scope(%__MODULE__{} = state) do - scope_vars = move_references_to_outer_scope(state.scope_vars_info) - vars = move_references_to_outer_scope(state.vars_info) - - %__MODULE__{state | vars_info: vars, scope_vars_info: scope_vars} - end - - defp move_references_to_outer_scope(vars) do - {current_scope_vars, outer_scope_vars, other_scopes_vars} = - case vars do - [current_scope_vars, outer_scope_vars | other_scopes_vars] -> - {current_scope_vars, outer_scope_vars, other_scopes_vars} - - [current_scope_vars | []] -> - {current_scope_vars, [], []} - end - - vars_to_move = - current_scope_vars - |> Enum.reduce(%{}, fn - %VarInfo{name: var, is_definition: true}, acc -> - Map.delete(acc, var) - - %VarInfo{name: var, positions: positions, is_definition: false} = el, acc -> - updated = - case acc[var] do - nil -> - el - - var_info -> - type = - if Enum.all?(positions, fn position -> position in var_info.positions end) do - merge_type(el.type, var_info.type) - else - el.type - end - - %VarInfo{ - el - | positions: (var_info.positions ++ positions) |> Enum.uniq() |> Enum.sort(), - type: type - } - end - - Map.put(acc, var, updated) - end) - |> Enum.map(fn {_name, var_info} -> var_info end) - |> Enum.reject(fn var_info -> is_nil(var_info) end) + def maybe_move_vars_to_outer_scope(%__MODULE__{vars_info: [current_scope_vars, outer_scope_vars | other_scopes_vars]} = state) do + outer_scope_vars = for {key, _} <- outer_scope_vars, into: %{}, do: ( + # TODO merge type? + {key, current_scope_vars[key]} + ) + vars_info = [current_scope_vars, outer_scope_vars | other_scopes_vars] - [current_scope_vars, vars_to_move ++ outer_scope_vars | other_scopes_vars] + %__MODULE__{state | vars_info: vars_info} end + def maybe_move_vars_to_outer_scope(state), do: state def no_alias_expansion({:__aliases__, _, [h | t]} = _aliases) when is_atom(h) do Module.concat([h | t]) @@ -1962,12 +1635,6 @@ defmodule ElixirSense.Core.State do end def macro_env(%__MODULE__{} = state, %__MODULE__.Env{} = env, line) do - function = - case env.scope do - {function, arity} -> {function, arity} - _ -> nil - end - context_modules = state.mods_funs_to_positions |> Enum.filter(&match?({{_module, nil, nil}, _}, &1)) @@ -1978,14 +1645,14 @@ defmodule ElixirSense.Core.State do requires: env.requires, module: env.module, line: line, - function: function, + function: env.function, # TODO context :guard, :match context: nil, context_modules: context_modules, functions: env.functions, - macros: env.macros + macros: env.macros, # TODO macro_aliases - # TODO versioned_vars + versioned_vars: elem(state.vars, 0) } end diff --git a/test/elixir_sense/core/compiler_test.exs b/test/elixir_sense/core/compiler_test.exs index 5773fc65..ccf447d3 100644 --- a/test/elixir_sense/core/compiler_test.exs +++ b/test/elixir_sense/core/compiler_test.exs @@ -25,7 +25,7 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do caller: caller, prematch: prematch, stacktrace: stacktrace, - unused: unused, + unused: {_, unused}, runtime_modules: runtime_modules, vars: vars ) @@ -549,10 +549,27 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do """) end - test "expands var" do + test "expands underscored var write" do assert_expansion("_ = 5") + end + + test "expands var write" do assert_expansion("a = 5") + end + + test "expands var read" do assert_expansion("a = 5; a") + end + + test "expands var overwrite" do + assert_expansion("a = 5; a = 6") + end + + test "expands var overwrite already overwritten" do + assert_expansion("[a, a] = [5, 5]") + end + + test "expands var pin" do assert_expansion("a = 5; ^a = 6") end diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 382d10eb..30cafad9 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -800,11 +800,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert Map.keys(state.lines_to_env[5].versioned_vars) == [{:y, nil}, {:z, nil}] - # TODO sort? assert [ %VarInfo{name: :y, positions: [{4, 3}]}, %VarInfo{name: :z, positions: [{4, 6}, {4, 24}]} - ] = state |> get_line_vars(5) |> Enum.sort_by(& &1.name) + ] = state |> get_line_vars(5) assert Map.keys(state.lines_to_env[7].versioned_vars) == [{:a, nil}] @@ -1251,7 +1250,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert state.protocols == [] assert state.scope_attributes == [] assert state.vars_info == [] - assert state.scope_vars_info == [] assert state.scope_ids == [] end @@ -1487,7 +1485,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert [%VarInfo{type: {:atom, :my_var}}] = state |> get_line_vars(2) end - + test "module attributes value binding to and from variables" do state = """ @@ -4580,7 +4578,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert get_line_protocol(state, 16) == nil # protocol and implementations inside protocol implementation creates a cross product - assert get_line_module(state, 21) == My.Reversible.Map.Other + assert get_line_module(state, 21) == My.Reversible.My.List.Other assert get_line_protocol(state, 21) == nil assert get_line_module(state, 26) == My.Reversible.My.List.Other.My.Map @@ -5527,8 +5525,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do MyMacros.Nested, MyMacros.One, MyMacros.Two.Three, - # TODO why isn't this required? - # Some.List, :ets, :lists ] From 61c37f65e4fa7f537b969d1e27a3cdcbea022e02 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 8 May 2024 19:11:32 +0200 Subject: [PATCH 023/235] fix scopes --- lib/elixir_sense/core/compiler.ex | 2 ++ lib/elixir_sense/core/state.ex | 11 +++-------- test/elixir_sense/core/metadata_builder_test.exs | 2 -- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 89e737e3..a9069cce 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1408,6 +1408,7 @@ defmodule ElixirSense.Core.Compiler do state = state |> add_module_to_index(full, position, end_position, []) + |> add_module |> add_current_env_to_line(line, %{env | module: full}) |> add_module_functions(%{env | module: full}, module_functions, position, end_position) |> new_vars_scope @@ -1441,6 +1442,7 @@ defmodule ElixirSense.Core.Compiler do |> maybe_move_vars_to_outer_scope |> remove_vars_scope |> remove_attributes_scope + |> remove_module # TODO hardcode expansion? # to result of require (a module atom) and :elixir_module.compile dot call in block diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 3e3cf9cb..f3b38532 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -566,13 +566,10 @@ defmodule ElixirSense.Core.State do Module.concat(protocol, first) end - def add_module(%__MODULE__{} = state, module) do - # TODO refactor to allow {:implementation, protocol, [implementations]} in scope + def add_module(%__MODULE__{} = state) do %__MODULE__{ state - | module: [module | state.module], - scopes: [module | state.scopes], - doc_context: [[] | state.doc_context], + | doc_context: [[] | state.doc_context], typedoc_context: [[] | state.typedoc_context], optional_callbacks_context: [[] | state.optional_callbacks_context] } @@ -581,9 +578,7 @@ defmodule ElixirSense.Core.State do def remove_module(%__MODULE__{} = state) do %{ state - | module: tl(state.module), - scopes: tl(state.scopes), - doc_context: tl(state.doc_context), + | doc_context: tl(state.doc_context), typedoc_context: tl(state.typedoc_context), optional_callbacks_context: tl(state.optional_callbacks_context) } diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 44260445..45b0a47d 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -1239,8 +1239,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.module == [] - assert state.scopes == [] assert state.functions == [] assert state.macros == [] assert state.requires == [] From d02daf54ec578fb515c954066a140068cf044ed5 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 9 May 2024 09:55:28 +0200 Subject: [PATCH 024/235] expand local structs recover from errors --- lib/elixir_sense/core/compiler.ex | 72 ++++++------------- test/elixir_sense/core/compiler_test.exs | 5 +- .../core/metadata_builder_test.exs | 68 ++++++++++++++++++ 3 files changed, 92 insertions(+), 53 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index a9069cce..23ffd5c1 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -4383,14 +4383,16 @@ defmodule ElixirSense.Core.Compiler do case extract_struct_assocs(meta, e_right, e) do {:expand, map_meta, assocs} when context != :match -> assoc_keys = Enum.map(assocs, fn {k, _} -> k end) - struct = load_struct(meta, e_left, [assocs], assoc_keys, ee) + struct = load_struct(meta, e_left, [assocs], assoc_keys, se, ee) keys = [:__struct__ | assoc_keys] without_keys = Elixir.Map.drop(struct, keys) + # TODO is escape safe? struct_assocs = Macro.escape(Enum.sort(Elixir.Map.to_list(without_keys))) {{:%, meta, [e_left, {:%{}, map_meta, struct_assocs ++ assocs}]}, se, ee} {_, _, assocs} -> - _ = load_struct(meta, e_left, [], Enum.map(assocs, fn {k, _} -> k end), ee) + # we don't need to validate keys + # _ = load_struct(meta, e_left, [], Enum.map(assocs, fn {k, _} -> k end), se, ee) {{:%, meta, [e_left, e_right]}, se, ee} end @@ -4540,58 +4542,24 @@ defmodule ElixirSense.Core.Compiler do Keyword.delete(assocs, :__struct__) end - defp load_struct(meta, name, args, keys, e) do - module = e.module - in_context = name in [module | e.context_modules] - - _arity = length(args) - # TODO - # or (not ensure_loaded(name) and wait_for_struct(name)) - external = in_context - - try do - # TODO the condition includes - # and ElixirDef.external_for(meta, name, :__struct__, arity, [:def]) - case external do - false when module == name -> - raise UndefinedFunctionError - - false -> + defp load_struct(meta, name, args, keys, s, e) do + case s.structs[name] do + nil -> + try do apply(name, :__struct__, args) - - external_fun -> - try do - apply(external_fun, args) - rescue - UndefinedFunctionError -> apply(name, :__struct__, args) - end - end - rescue - UndefinedFunctionError -> - cond do - in_context and e.function == nil -> - raise "inaccessible_struct" - - true -> - raise "undefined_struct" + else + %{:__struct__ => ^name} = struct -> + struct + _ -> + # recover from invalid return value + [__struct__: name] |> Keyword.merge(hd(args)) |> Elixir.Map.new + rescue + _ -> + # recover from error by building the fake struct + [__struct__: name] |> Keyword.merge(hd(args)) |> Elixir.Map.new end - else - %{:__struct__ => struct_name} = struct when is_atom(struct_name) -> - assert_struct_keys(meta, name, struct, keys, e) - # ElixirEnv.trace({:struct_expansion, meta, name, keys}, e) - struct - - %{:__struct__ => struct_name} when is_atom(struct_name) -> - raise "struct_name_mismatch" - - _other -> - raise "invalid_struct_return_value" - end - end - - defp assert_struct_keys(_meta, _name, struct, keys, _e) do - for key <- keys, not Elixir.Map.has_key?(struct, key) do - raise "unknown_key_for_struct" + info -> + info.fields |> Keyword.merge(hd(args)) |> Elixir.Map.new end end end diff --git a/test/elixir_sense/core/compiler_test.exs b/test/elixir_sense/core/compiler_test.exs index ccf447d3..3fdf6e73 100644 --- a/test/elixir_sense/core/compiler_test.exs +++ b/test/elixir_sense/core/compiler_test.exs @@ -105,14 +105,16 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do assert_expansion("%{a: a} = %{}") assert_expansion("%{1 => a} = %{}") assert_expansion("%{%{a: 1} | a: 2}") + assert_expansion("%{%{\"a\" => 1} | \"a\" => 2}") end test "expands %" do assert_expansion("%Date{year: 2024, month: 2, day: 18}") assert_expansion("%Date{calendar: Calendar.ISO, year: 2024, month: 2, day: 18}") assert_expansion("%{year: x} = %Date{year: 2024, month: 2, day: 18}") + assert_expansion("%Date{year: x} = %Date{year: 2024, month: 2, day: 18}") assert_expansion("%Date{%Date{year: 2024, month: 2, day: 18} | day: 1}") - assert_expansion("%{%Date{year: 2024, month: 2, day: 18} | day: 1}") + assert_expansion("%x{} = %Date{year: 2024, month: 2, day: 18}") end test "expands <<>>" do @@ -608,6 +610,7 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do assert_expansion("[1, 2]") assert_expansion("[1 | [2]]") assert_expansion("[a | b] = [1, 2, 3]") + assert_expansion("[a] ++ [b] = [1, 2]") end test "expands function" do diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 45b0a47d..403a71e2 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -5970,6 +5970,74 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } } = state.mods_funs_to_positions end + + test "expands local struct" do + state = + """ + defmodule MyStruct do + defstruct [:some_field, a_field: 1] + var = %MyStruct{some_field: 3} + var = %MyStruct{} + IO.puts "" + end + """ + |> string_to_state + + assert state.structs == %{ + MyStruct => %StructInfo{ + type: :defstruct, + fields: [some_field: nil, a_field: 1, __struct__: MyStruct] + } + } + end + + test "expands local not existing struct" do + state = + """ + defmodule MyStruct do + var = %MyStruct{some_field: 3} + IO.puts "" + end + """ + |> string_to_state + + assert state.structs == %{} + end + + test "expands remote not existing struct" do + state = + """ + defmodule MyStruct do + var = %FooStruct{some_field: 3} + IO.puts "" + end + """ + |> string_to_state + + assert state.structs == %{} + end + + test "expands local struct defined in other module" do + state = + """ + defmodule MyStruct do + defstruct [:some_field, a_field: 1] + end + + defmodule Foo do + var = %MyStruct{some_field: 3} + IO.puts "" + end + """ + |> string_to_state + + assert state.structs == %{ + MyStruct => %StructInfo{ + type: :defstruct, + fields: [some_field: nil, a_field: 1, __struct__: MyStruct] + } + } + end end describe "calls" do From f6f1806912abb87a08f6a695ffed46ee355256e2 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 9 May 2024 10:35:12 +0200 Subject: [PATCH 025/235] recover from map key errors --- lib/elixir_sense/core/compiler.ex | 120 +++++++++--------------------- 1 file changed, 37 insertions(+), 83 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 23ffd5c1..829c497f 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -4373,24 +4373,24 @@ defmodule ElixirSense.Core.Compiler do alias ElixirSense.Core.Compiler, as: ElixirExpand def expand_struct(meta, left, {:%{}, map_meta, map_args}, s, %{context: context} = e) do - clean_map_args = clean_struct_key_from_map_args(meta, map_args, e) + clean_map_args = clean_struct_key_from_map_args(map_args) {[e_left, e_right], se, ee} = ElixirExpand.expand_args([left, {:%{}, map_meta, clean_map_args}], s, e) case validate_struct(e_left, context) do true when is_atom(e_left) -> - case extract_struct_assocs(meta, e_right, e) do + case extract_struct_assocs(e_right) do {:expand, map_meta, assocs} when context != :match -> assoc_keys = Enum.map(assocs, fn {k, _} -> k end) - struct = load_struct(meta, e_left, [assocs], assoc_keys, se, ee) + struct = load_struct(e_left, [assocs], se, ee) keys = [:__struct__ | assoc_keys] without_keys = Elixir.Map.drop(struct, keys) # TODO is escape safe? struct_assocs = Macro.escape(Enum.sort(Elixir.Map.to_list(without_keys))) {{:%, meta, [e_left, {:%{}, map_meta, struct_assocs ++ assocs}]}, se, ee} - {_, _, assocs} -> + {_, _, _assocs} -> # we don't need to validate keys # _ = load_struct(meta, e_left, [], Enum.map(assocs, fn {k, _} -> k end), se, ee) {{:%, meta, [e_left, e_right]}, se, ee} @@ -4399,9 +4399,6 @@ defmodule ElixirSense.Core.Compiler do true -> {{:%, meta, [e_left, e_right]}, se, ee} - false when context == :match -> - raise "invalid_struct_name_in_match" - false -> raise "invalid_struct_name" end @@ -4411,7 +4408,7 @@ defmodule ElixirSense.Core.Compiler do def expand_map(meta, [{:|, update_meta, [left, right]}], s, %{context: nil} = e) do {[e_left | e_right], se, ee} = ElixirExpand.expand_args([left | right], s, e) - validate_kv(meta, e_right, right, e) + e_right = sanitize_kv(e_right, e) {{:%{}, meta, [{:|, update_meta, [e_left, e_right]}]}, se, ee} end @@ -4421,103 +4418,60 @@ defmodule ElixirSense.Core.Compiler do def expand_map(meta, args, s, e) do {e_args, se, ee} = ElixirExpand.expand_args(args, s, e) - validate_kv(meta, e_args, args, e) + e_args = sanitize_kv(e_args, e) {{:%{}, meta, e_args}, se, ee} end - defp clean_struct_key_from_map_args(meta, [{:|, pipe_meta, [left, map_assocs]}], e) do - [{:|, pipe_meta, [left, clean_struct_key_from_map_assocs(meta, map_assocs, e)]}] + defp clean_struct_key_from_map_args([{:|, pipe_meta, [left, map_assocs]}]) do + [{:|, pipe_meta, [left, delete_struct_key(map_assocs)]}] end - defp clean_struct_key_from_map_args(meta, map_assocs, e) do - clean_struct_key_from_map_assocs(meta, map_assocs, e) + defp clean_struct_key_from_map_args(map_assocs) do + delete_struct_key(map_assocs) end - defp clean_struct_key_from_map_assocs(_meta, assocs, _e) do - case Keyword.pop(assocs, :__struct__) do - {nil, cleaned_assocs} -> - cleaned_assocs - - {_struct_value, cleaned_assocs} -> - # file_warn(meta, Map.get(e, :file), __MODULE__, :ignored_struct_key_in_struct) - cleaned_assocs - end - end - - defp validate_kv(meta, kv, _original, %{context: context} = e) do - Enum.reduce(kv, {1, %{}}, fn - {k, _v}, {index, used} -> + defp sanitize_kv(kv, %{context: context}) do + Enum.filter(kv, fn + {k, _v} -> if context == :match do - validate_match_key(meta, k, e) + validate_match_key(k) + else + true end - - new_used = validate_not_repeated(meta, k, used, e) - {index + 1, new_used} - - _, {_index, _used} -> - raise "not_kv_pair" + _ -> + false end) end - defp validate_not_repeated(_meta, key, used, e) do - if is_literal(key) and Elixir.Map.has_key?(used, key) do - case e do - %{context: :match} -> - # raise "repeated_key" - # function_error(meta, Map.get(e, :file), __MODULE__, {:repeated_key, key}) - :ok - - _ -> - # file_warn(meta, Map.get(e, :file), __MODULE__, {:repeated_key, key}) - :ok - end - - used - else - Elixir.Map.put(used, key, true) - end - end - - defp validate_match_key(_meta, {name, _, context}, _e) + defp validate_match_key({name, _, context}) when is_atom(name) and is_atom(context) do - raise "invalid_variable_in_map_key_match" + # invalid_variable_in_map_key_match + false end - defp validate_match_key(meta, {:"::", _, [left, _]}, e) do - validate_match_key(meta, left, e) + defp validate_match_key({:"::", _, [left, _]}) do + validate_match_key(left) end - defp validate_match_key(_, {:^, _, [{name, _, context}]}, _) + defp validate_match_key({:^, _, [{name, _, context}]}) when is_atom(name) and is_atom(context), - do: :ok + do: true - defp validate_match_key(_, {:%{}, _, [_ | _]}, _), do: :ok + defp validate_match_key({:%{}, _, [_ | _]}), do: true - defp validate_match_key(meta, {left, _, right}, e) do - validate_match_key(meta, left, e) - validate_match_key(meta, right, e) + defp validate_match_key({left, _, right}) do + validate_match_key(left) and validate_match_key(right) end - defp validate_match_key(meta, {left, right}, e) do - validate_match_key(meta, left, e) - validate_match_key(meta, right, e) + defp validate_match_key({left, right}) do + validate_match_key(left) and validate_match_key(right) end - defp validate_match_key(meta, list, e) when is_list(list) do - for each <- list do - validate_match_key(meta, each, e) - end + defp validate_match_key(list) when is_list(list) do + Enum.all?(list, &validate_match_key/1) end - defp validate_match_key(_, _, _), do: :ok - - defp is_literal({_, _, _}), do: false - - defp is_literal({left, right}), do: is_literal(left) and is_literal(right) - - defp is_literal(list) when is_list(list), do: Enum.all?(list, &is_literal/1) - - defp is_literal(_), do: true + defp validate_match_key(_), do: true defp validate_struct({:^, _, [{var, _, ctx}]}, :match) when is_atom(var) and is_atom(ctx), do: true @@ -4526,15 +4480,15 @@ defmodule ElixirSense.Core.Compiler do defp validate_struct(atom, _) when is_atom(atom), do: true defp validate_struct(_, _), do: false - defp extract_struct_assocs(_, {:%{}, meta, [{:|, _, [_, assocs]}]}, _) do + defp extract_struct_assocs({:%{}, meta, [{:|, _, [_, assocs]}]}) do {:update, meta, delete_struct_key(assocs)} end - defp extract_struct_assocs(_, {:%{}, meta, assocs}, _) do + defp extract_struct_assocs({:%{}, meta, assocs}) do {:expand, meta, delete_struct_key(assocs)} end - defp extract_struct_assocs(_meta, _other, _e) do + defp extract_struct_assocs(_other) do raise "non_map_after_struct" end @@ -4542,7 +4496,7 @@ defmodule ElixirSense.Core.Compiler do Keyword.delete(assocs, :__struct__) end - defp load_struct(meta, name, args, keys, s, e) do + defp load_struct(name, args, s, _e) do case s.structs[name] do nil -> try do From 2e4bc72393f2b696c47e6b0fb738f4306c2b0318 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 9 May 2024 11:06:39 +0200 Subject: [PATCH 026/235] error tolerant opts --- lib/elixir_sense/core/compiler.ex | 44 ++++++++++++++++++------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 829c497f..3497243e 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -111,9 +111,8 @@ defmodule ElixirSense.Core.Compiler do expand_multi_alias_call(form, meta, base, refs, [], state, env) [opts] -> - if Keyword.has_key?(opts, :as) do - raise "as_in_multi_alias_call" - end + # elixir raises if there is :as in opts, we omit it + opts = Keyword.delete(opts, :as) expand_multi_alias_call(form, meta, base, refs, opts, state, env) end @@ -144,10 +143,12 @@ defmodule ElixirSense.Core.Compiler do {arg, state, env} {:error, _} -> - raise "elixir_aliases" + # elixir_aliases + {arg, state, env} end else - raise "expected_compile_time_module" + # expected_compile_time_module + {arg, state, env} end end @@ -186,7 +187,8 @@ defmodule ElixirSense.Core.Compiler do {arg, state, env} {:error, _} -> - raise "elixir_aliases" + # elixir_aliases + {arg, state, env} end else {arg, state, env} @@ -203,11 +205,13 @@ defmodule ElixirSense.Core.Compiler do {arg, state, env} {:error, _} -> - raise "elixir_aliases" + # elixir_aliases + {arg, state, env} end :error -> - raise "expected_compile_time_module" + # expected_compile_time_module + {arg, state, env} end end @@ -231,10 +235,12 @@ defmodule ElixirSense.Core.Compiler do {arg, state, env} else _ -> - raise "elixir_import" + # elixir_import + {arg, state, env} end else - raise "expected_compile_time_module" + # expected_compile_time_module + {arg, state, env} end end @@ -1771,7 +1777,7 @@ defmodule ElixirSense.Core.Compiler do defp expand_opts(meta, kind, allowed, opts, s, e) do {e_opts, se, ee} = expand(opts, s, e) - validate_opts(meta, kind, allowed, e_opts, ee) + e_opts = sanitize_opts(allowed, e_opts) {e_opts, se, ee} end @@ -1863,8 +1869,10 @@ defmodule ElixirSense.Core.Compiler do ref, state, env when is_atom(ref) -> expand({kind, meta, [Module.concat([base_ref, ref]), opts]}, state, env) - _other, _s, _e -> - raise "expected_compile_time_module" + other, s, e -> + # elixir raises here + # expected_compile_time_module + {other, s, e} end map_fold(fun, state, env, refs) @@ -1949,7 +1957,7 @@ defmodule ElixirSense.Core.Compiler do defp expand_for({:for, meta, [_ | _] = args}, s, e, return) do assert_no_match_or_guard_scope(e.context, "for") {cases, block} = __MODULE__.Utils.split_opts(args) - validate_opts(meta, :for, [:do, :into, :uniq, :reduce], block, e) + block = sanitize_opts([:do, :into, :uniq, :reduce], block) {expr, opts} = case Keyword.pop(block, :do) do @@ -2101,13 +2109,11 @@ defmodule ElixirSense.Core.Compiler do {:ok, reduce, Enum.reverse(acc)} end - defp validate_opts(_meta, _kind, allowed, opts, _e) when is_list(opts) do - for {key, _} <- opts, not Enum.member?(allowed, key), do: raise("unsupported_option") + defp sanitize_opts(allowed, opts) when is_list(opts) do + for {key, value} <- opts, Enum.member?(allowed, key), do: {key, value} end - defp validate_opts(_meta, _kind, _allowed, _opts, _e) do - raise "options_are_not_keyword" - end + defp sanitize_opts(_allowed, _opts), do: [] defp escape_env_entries(meta, %{vars: {read, _}}, env) do env = From 320eef84663058412a40038bcf20656959e4f706 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 9 May 2024 14:26:56 +0200 Subject: [PATCH 027/235] recover from incomplete code with naked when --- lib/elixir_sense/core/compiler.ex | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 3497243e..f6b0115d 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1739,6 +1739,27 @@ defmodule ElixirSense.Core.Compiler do {:ok, :elixir_rewrite.rewrite(receiver, dot_meta, right, meta, e_args)} end + # This fixes exactly 1 test... + defp expand_local(meta, :when, [_, _] = args, state, env = %{context: nil}) do + # naked when, try to transform into a case + ast = {:case, meta, + [ + {:_, meta, nil}, + [ + do: [ + {:->, meta, + [ + [ + {:when, meta, args} + ], + :ok + ]} + ] + ] + ]} + expand(ast, state, env) + end + defp expand_local(meta, fun, args, state, env = %{function: function}) when function != nil do assert_no_clauses(fun, meta, args, env) From ff4a89c7ffc285bfec3602784421a9f0bdfedf5e Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 9 May 2024 15:00:41 +0200 Subject: [PATCH 028/235] rescue errors on captures --- lib/elixir_sense/core/compiler.ex | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index f6b0115d..a6d494ec 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -3575,8 +3575,15 @@ defmodule ElixirSense.Core.Compiler do raise "capture_arg_outside_of_capture" end - def capture(_meta, _arg, _s, _e) do - raise "invalid_args_for_capture" + def capture(meta, arg, s, e) do + # elixir raises invalid_args_for_capture + # we try to transform the capture to local fun capture + case arg do + {var, _, context} when is_atom(var) and is_atom(context) -> + capture(meta, {:/, meta, [arg, 0]}, s, e) + _ -> + raise "invalid_args_for_capture #{inspect(arg)}" + end end defp capture_import({atom, import_meta, args} = expr, s, e, sequential) do @@ -3627,8 +3634,11 @@ defmodule ElixirSense.Core.Compiler do defp capture_expr(meta, expr, s, e, escaped, sequential) do case escape(expr, e, escaped) do - {_, []} when not sequential -> - raise "invalid_args_for_capture" + {e_expr, []} when not sequential -> + # elixir raises here invalid_args_for_capture + # we emit fn without args + fn_expr = {:fn, meta, [{:->, meta, [[], e_expr]}]} + {:expand, fn_expr, s, e} {e_expr, e_dict} -> e_vars = validate(meta, e_dict, 1, e) From fb4900877228b05b15d3a2dd67e83b5cd98fba5d Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 9 May 2024 17:42:11 +0200 Subject: [PATCH 029/235] cleanup leftovers --- test/support/plugins/ecto/fake_schemas.ex | 77 ------------ test/support/plugins/ecto/migration.ex | 12 -- test/support/plugins/ecto/query.ex | 117 ------------------ test/support/plugins/ecto/schema.ex | 32 ----- test/support/plugins/ecto/uuid.ex | 19 --- .../plugins/phoenix/page_controller.ex | 10 -- test/support/plugins/phoenix/router.ex | 4 - 7 files changed, 271 deletions(-) delete mode 100644 test/support/plugins/ecto/fake_schemas.ex delete mode 100644 test/support/plugins/ecto/migration.ex delete mode 100644 test/support/plugins/ecto/query.ex delete mode 100644 test/support/plugins/ecto/schema.ex delete mode 100644 test/support/plugins/ecto/uuid.ex delete mode 100644 test/support/plugins/phoenix/page_controller.ex delete mode 100644 test/support/plugins/phoenix/router.ex diff --git a/test/support/plugins/ecto/fake_schemas.ex b/test/support/plugins/ecto/fake_schemas.ex deleted file mode 100644 index 006f15ae..00000000 --- a/test/support/plugins/ecto/fake_schemas.ex +++ /dev/null @@ -1,77 +0,0 @@ -defmodule ElixirSense.Plugins.Ecto.FakeSchemas.User do - @moduledoc """ - Fake User schema. - - The docs. - """ - - def __schema__(:fields), do: [:id, :name, :email] - def __schema__(:associations), do: [:assoc1, :assoc2] - def __schema__(:type, :id), do: :id - def __schema__(:type, :name), do: :string - def __schema__(:type, :email), do: :string - - def __schema__(:association, :assoc1), - do: %{related: FakeAssoc1, owner: __MODULE__, owner_key: :assoc1_id} - - def __schema__(:association, :assoc2), - do: %{related: FakeAssoc2, owner: __MODULE__, owner_key: :assoc2_id} -end - -defmodule ElixirSense.Plugins.Ecto.FakeSchemas.Comment do - @moduledoc """ - Fake Comment schema. - """ - - alias ElixirSense.Plugins.Ecto.FakeSchemas.Post - - def __schema__(:fields), do: [:content, :date] - def __schema__(:associations), do: [:post] - def __schema__(:type, :content), do: :string - def __schema__(:type, :date), do: :date - - def __schema__(:association, :post), - do: %{related: Post, owner: __MODULE__, owner_key: :post_id} -end - -defmodule ElixirSense.Plugins.Ecto.FakeSchemas.Post do - @moduledoc """ - Fake Post schema. - """ - - alias ElixirSense.Plugins.Ecto.FakeSchemas.User - alias ElixirSense.Plugins.Ecto.FakeSchemas.Comment - - def __schema__(:fields), do: [:id, :title, :text, :date, :user_id] - def __schema__(:associations), do: [:user, :comments, :tags] - def __schema__(:type, :id), do: :id - def __schema__(:type, :user_id), do: :id - def __schema__(:type, :title), do: :string - def __schema__(:type, :text), do: :string - def __schema__(:type, :date), do: :date - - def __schema__(:association, :user), - do: %{related: User, related_key: :id, owner: __MODULE__, owner_key: :user_id} - - def __schema__(:association, :comments), - do: %{related: Comment, related_key: :post_id, owner: __MODULE__, owner_key: :id} - - def __schema__(:association, :tags), - do: %{related: Tag, owner: __MODULE__, owner_key: :id} -end - -defmodule ElixirSense.Plugins.Ecto.FakeSchemas.Tag do - @moduledoc """ - Fake Tag schema. - """ - - alias ElixirSense.Plugins.Ecto.FakeSchemas.Post - - def __schema__(:fields), do: [:id, :name] - def __schema__(:associations), do: [:posts] - def __schema__(:type, :id), do: :id - def __schema__(:type, :name), do: :string - - def __schema__(:association, :posts), - do: %{related: Post, owner: __MODULE__, owner_key: :id} -end diff --git a/test/support/plugins/ecto/migration.ex b/test/support/plugins/ecto/migration.ex deleted file mode 100644 index c733a454..00000000 --- a/test/support/plugins/ecto/migration.ex +++ /dev/null @@ -1,12 +0,0 @@ -defmodule Ecto.Migration do - @moduledoc ~S""" - Fake Migration module. - """ - - @doc """ - Defines a field on the schema with given name and type. - """ - def add(column, type, opts \\ []) when is_atom(column) and is_list(opts) do - {column, type, opts} - end -end diff --git a/test/support/plugins/ecto/query.ex b/test/support/plugins/ecto/query.ex deleted file mode 100644 index c9696ac0..00000000 --- a/test/support/plugins/ecto/query.ex +++ /dev/null @@ -1,117 +0,0 @@ -defmodule Ecto.Query do - @moduledoc ~S""" - Fake Query module. - """ - - @doc """ - Creates a query. - """ - defmacro from(expr, kw \\ []) do - {expr, kw} - end - - @doc """ - A select query expression. - - Selects which fields will be selected from the schema and any transformations - that should be performed on the fields. Any expression that is accepted in a - query can be a select field. - - ## Keywords examples - - from(c in City, select: c) # returns the schema as a struct - from(c in City, select: {c.name, c.population}) - from(c in City, select: [c.name, c.county]) - - It is also possible to select a struct and limit the returned - fields at the same time: - - from(City, select: [:name]) - - The syntax above is equivalent to: - - from(city in City, select: struct(city, [:name])) - - ## Expressions examples - - City |> select([c], c) - City |> select([c], {c.name, c.country}) - City |> select([c], %{"name" => c.name}) - - """ - defmacro select(query, binding \\ [], expr) do - {query, binding, expr} - end - - @doc """ - Mergeable select query expression. - - This macro is similar to `select/3` except it may be specified - multiple times as long as every entry is a map. This is useful - for merging and composing selects. For example: - - query = from p in Post, select: %{} - - query = - if include_title? do - from p in query, select_merge: %{title: p.title} - else - query - end - - query = - if include_visits? do - from p in query, select_merge: %{visits: p.visits} - else - query - end - - In the example above, the query is built little by little by merging - into a final map. If both conditions above are true, the final query - would be equivalent to: - - from p in Post, select: %{title: p.title, visits: p.visits} - - If `:select_merge` is called and there is no value selected previously, - it will default to the source, `p` in the example above. - """ - defmacro select_merge(query, binding \\ [], expr) do - {query, binding, expr} - end - - @doc """ - A distinct query expression. - - When true, only keeps distinct values from the resulting - select expression. - - ## Keywords examples - - # Returns the list of different categories in the Post schema - from(p in Post, distinct: true, select: p.category) - - # If your database supports DISTINCT ON(), - # you can pass expressions to distinct too - from(p in Post, - distinct: p.category, - order_by: [p.date]) - - # The DISTINCT ON() also supports ordering similar to ORDER BY. - from(p in Post, - distinct: [desc: p.category], - order_by: [p.date]) - - # Using atoms - from(p in Post, distinct: :category, order_by: :date) - - ## Expressions example - - Post - |> distinct(true) - |> order_by([p], [p.category, p.author]) - - """ - defmacro distinct(query, binding \\ [], expr) do - {query, binding, expr} - end -end diff --git a/test/support/plugins/ecto/schema.ex b/test/support/plugins/ecto/schema.ex deleted file mode 100644 index ecf5a38b..00000000 --- a/test/support/plugins/ecto/schema.ex +++ /dev/null @@ -1,32 +0,0 @@ -defmodule Ecto.Schema do - @moduledoc ~S""" - Fake Schema module. - """ - - @doc """ - Defines a field on the schema with given name and type. - """ - defmacro field(name, type \\ :string, opts \\ []) do - {name, type, opts} - end - - defmacro schema(source, do: block) do - {source, block} - end - - defmacro has_many(name, queryable, opts \\ []) do - {name, queryable, opts} - end - - defmacro has_one(name, queryable, opts \\ []) do - {name, queryable, opts} - end - - defmacro belongs_to(name, queryable, opts \\ []) do - {name, queryable, opts} - end - - defmacro many_to_many(name, queryable, opts \\ []) do - {name, queryable, opts} - end -end diff --git a/test/support/plugins/ecto/uuid.ex b/test/support/plugins/ecto/uuid.ex deleted file mode 100644 index 050cc02d..00000000 --- a/test/support/plugins/ecto/uuid.ex +++ /dev/null @@ -1,19 +0,0 @@ -defmodule Ecto.Type do - @moduledoc """ - Fake Ecto.Type - """ - - @callback fake :: true -end - -defmodule Ecto.UUID do - @moduledoc """ - Fake Ecto.UUID - """ - - @behaviour Ecto.Type - - def fake() do - true - end -end diff --git a/test/support/plugins/phoenix/page_controller.ex b/test/support/plugins/phoenix/page_controller.ex deleted file mode 100644 index 3c70e506..00000000 --- a/test/support/plugins/phoenix/page_controller.ex +++ /dev/null @@ -1,10 +0,0 @@ -defmodule ExampleWeb.PageController do - def call(_conn, _params) do - end - - def action(_conn, _params) do - end - - def home(_conn, _params) do - end -end diff --git a/test/support/plugins/phoenix/router.ex b/test/support/plugins/phoenix/router.ex deleted file mode 100644 index fa0d3064..00000000 --- a/test/support/plugins/phoenix/router.ex +++ /dev/null @@ -1,4 +0,0 @@ -defmodule Phoenix.Router do - def get(_route, _plug, _plut_opts, _opts \\ []) do - end -end From 9c4c4a98b157562a20e99345c1ea6f8551334947 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 9 May 2024 17:42:57 +0200 Subject: [PATCH 030/235] macro call tracking --- lib/elixir_sense/core/compiler.ex | 1057 +++++++++-------- lib/elixir_sense/core/metadata_builder.ex | 3 +- lib/elixir_sense/core/state.ex | 63 +- .../core/metadata_builder_test.exs | 887 +++++++------- test/elixir_sense/core/parser_test.exs | 33 +- 5 files changed, 1115 insertions(+), 928 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index a6d494ec..2f157917 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -14,7 +14,10 @@ defmodule ElixirSense.Core.Compiler do do_expand(ast, state, env) catch kind, payload -> - Logger.warning("Unable to expand ast node #{inspect(ast)}: #{Exception.format(kind, payload, __STACKTRACE__)}") + Logger.warning( + "Unable to expand ast node #{inspect(ast)}: #{Exception.format(kind, payload, __STACKTRACE__)}" + ) + {ast, state, env} end end @@ -105,7 +108,7 @@ defmodule ElixirSense.Core.Compiler do # require, alias, import defp do_expand({form, meta, [{{:., _, [base, :{}]}, _, refs} | rest]}, state, env) - when form in [:require, :alias, :import] do + when form in [:require, :alias, :import] do case rest do [] -> expand_multi_alias_call(form, meta, base, refs, [], state, env) @@ -292,7 +295,7 @@ defmodule ElixirSense.Core.Compiler do end defp do_expand({{:., dot_meta, [{:__ENV__, meta, atom}, field]}, call_meta, []}, s, e) - when is_atom(atom) and is_atom(field) do + when is_atom(atom) and is_atom(field) do assert_no_match_scope(e.context, "__ENV__") line = Keyword.get(call_meta, :line, 0) @@ -309,8 +312,8 @@ defmodule ElixirSense.Core.Compiler do # Quote defp do_expand({unquote_call, _meta, [_]}, _s, _e) - when unquote_call in [:unquote, :unquote_splicing], - do: raise("unquote_outside_quote") + when unquote_call in [:unquote, :unquote_splicing], + do: raise("unquote_outside_quote") defp do_expand({:quote, meta, [opts]}, s, e) when is_list(opts) do case Keyword.fetch(opts, :do) do @@ -386,8 +389,12 @@ defmodule ElixirSense.Core.Compiler do end end - defp do_expand({:&, meta, [{:/, arity_meta, [{:super, super_meta, context}, arity]} = expr]}, s, e) - when is_atom(context) and is_integer(arity) do + defp do_expand( + {:&, meta, [{:/, arity_meta, [{:super, super_meta, context}, arity]} = expr]}, + s, + e + ) + when is_atom(context) and is_integer(arity) do assert_no_match_or_guard_scope(e.context, "&") case resolve_super(meta, arity, s, e) do @@ -460,7 +467,8 @@ defmodule ElixirSense.Core.Compiler do no_match_s = %{s | prematch: :pin, vars: {prematch, write}} case expand(arg, no_match_s, %{e | context: nil}) do - {{name, _var_meta, kind} = var, %{unused: unused}, _} when is_atom(name) and is_atom(kind) -> + {{name, _var_meta, kind} = var, %{unused: unused}, _} + when is_atom(name) and is_atom(kind) -> s = add_var_read(s, var) {{:^, meta, [var]}, %{s | unused: unused}, e} @@ -485,7 +493,7 @@ defmodule ElixirSense.Core.Compiler do end defp do_expand({name, meta, kind}, s, %{context: :match} = e) - when is_atom(name) and is_atom(kind) do + when is_atom(name) and is_atom(kind) do %{ prematch: {_, prematch_version, _}, unused: version, @@ -596,7 +604,7 @@ defmodule ElixirSense.Core.Compiler do # Local calls defp do_expand({fun, meta, args}, state, env) - when is_atom(fun) and is_list(meta) and is_list(args) do + when is_atom(fun) and is_list(meta) and is_list(args) do assert_no_ambiguous_op(fun, meta, args, state, env) arity = length(args) @@ -631,8 +639,8 @@ defmodule ElixirSense.Core.Compiler do # Remote call defp do_expand({{:., dot_meta, [module, fun]}, meta, args}, state, env) - when (is_tuple(module) or is_atom(module)) and is_atom(fun) and is_list(meta) and - is_list(args) do + when (is_tuple(module) or is_atom(module)) and is_atom(fun) and is_list(meta) and + is_list(args) do # dbg({module, fun, args}) {module, state_l, env} = expand(module, __MODULE__.Env.prepare_write(state), env) arity = length(args) @@ -668,32 +676,35 @@ defmodule ElixirSense.Core.Compiler do assert_no_match_or_guard_scope(e.context, "anonymous call") {[e_expr | e_args], sa, ea} = expand_args([expr | args], s, e) - sa = if is_atom(e_expr) do - # function_error(meta, e, __MODULE__, {:invalid_function_call, e_expr}) - sa - else - line = Keyword.get(dot_meta, :line, 0) - column = Keyword.get(dot_meta, :column, nil) - column = if column do - # for remote calls we emit position of right side of . - # to make it consistent we shift dot position here - column + 1 + sa = + if is_atom(e_expr) do + # function_error(meta, e, __MODULE__, {:invalid_function_call, e_expr}) + sa else - column - end + line = Keyword.get(dot_meta, :line, 0) + column = Keyword.get(dot_meta, :column, nil) + + column = + if column do + # for remote calls we emit position of right side of . + # to make it consistent we shift dot position here + column + 1 + else + column + end - sa - |> add_call_to_line({nil, e_expr, length(e_args)}, {line, column}) - |> add_current_env_to_line(line, e) - end + sa + |> add_call_to_line({nil, e_expr, length(e_args)}, {line, column}) + |> add_current_env_to_line(line, e) + end {{{:., dot_meta, [e_expr]}, meta, e_args}, sa, ea} end # Invalid calls - defp do_expand({_, meta, args} = _invalid, _s, _e) when is_list(meta) and is_list(args) do - raise "invalid_call" + defp do_expand({_, meta, args} = invalid, _s, _e) when is_list(meta) and is_list(args) do + raise "invalid_call #{inspect(invalid)}" end # Literals @@ -722,8 +733,8 @@ defmodule ElixirSense.Core.Compiler do {{:type, :external}, {:env, []}} -> {__MODULE__.Quote.fun_to_quoted(function), s, e} - _ -> - raise "invalid_quoted_expr" + other -> + raise "invalid_quoted_expr when expanding fun #{inspect(other)}" end end @@ -763,37 +774,40 @@ defmodule ElixirSense.Core.Compiler do state, env ) do - assert_no_match_or_guard_scope(env.context, :"def/2") - module = assert_module_scope(env, :def, 2) + assert_no_match_or_guard_scope(env.context, :"def/2") + module = assert_module_scope(env, :def, 2) - {position, end_position} = extract_range(meta) - {line, _} = position + {position, end_position} = extract_range(meta) + {line, _} = position - {opts, state, env} = expand(opts, state, env) - target = Kernel.Utils.defdelegate_all(funs, opts, env) - # TODO: Remove List.wrap when multiple funs are no longer supported - state = funs - |> List.wrap + {opts, state, env} = expand(opts, state, env) + target = Kernel.Utils.defdelegate_all(funs, opts, env) + # TODO: Remove List.wrap when multiple funs are no longer supported + state = + funs + |> List.wrap() |> Enum.reduce(state, fn fun, state -> # TODO expand args? {name, args, as, as_args} = Kernel.Utils.defdelegate_each(fun, opts) arity = length(args) - state - |> add_current_env_to_line(line, %{env | context: nil, function: {name, arity}}) - |> add_mod_fun_to_position( - {module, name, arity}, - position, - end_position, - args, - :defdelegate, - "", - # doc, - %{delegate_to: {target, as, length(as_args)}}, - # meta - [target: {target, as}] - ) - end) - {[], state, env} + + state + |> add_current_env_to_line(line, %{env | context: nil, function: {name, arity}}) + |> add_mod_fun_to_position( + {module, name, arity}, + position, + end_position, + args, + :defdelegate, + "", + # doc, + %{delegate_to: {target, as, length(as_args)}}, + # meta + target: {target, as} + ) + end) + + {[], state, env} end defp expand_macro( @@ -805,16 +819,16 @@ defmodule ElixirSense.Core.Compiler do state, env ) do - assert_module_scope(env, :@, 1) - unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") - line = Keyword.fetch!(meta, :line) + assert_module_scope(env, :@, 1) + unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") + line = Keyword.fetch!(meta, :line) - state = - state - |> add_current_env_to_line(line, env) - - {arg, state, env} = expand(arg, state, env) - add_behaviour(arg, state, env) + state = + state + |> add_current_env_to_line(line, env) + + {arg, state, env} = expand(arg, state, env) + add_behaviour(arg, state, env) end defp expand_macro( @@ -826,23 +840,25 @@ defmodule ElixirSense.Core.Compiler do state, env ) do - assert_module_scope(env, :@, 1) - unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") - line = Keyword.fetch!(meta, :line) + assert_module_scope(env, :@, 1) + unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") + line = Keyword.fetch!(meta, :line) - state = - state - |> add_current_env_to_line(line, env) - - {arg, state, env} = expand(arg, state, env) + state = + state + |> add_current_env_to_line(line, env) - state = state - |> add_moduledoc_positions( - env, - meta - ) - |> register_doc(env, :moduledoc, arg) - {{:@, meta, [{:moduledoc, doc_meta, [arg]}]}, state, env} + {arg, state, env} = expand(arg, state, env) + + state = + state + |> add_moduledoc_positions( + env, + meta + ) + |> register_doc(env, :moduledoc, arg) + + {{:@, meta, [{:moduledoc, doc_meta, [arg]}]}, state, env} end defp expand_macro( @@ -853,20 +869,23 @@ defmodule ElixirSense.Core.Compiler do _callback, state, env - ) when doc in [:doc, :typedoc] do - assert_module_scope(env, :@, 1) - unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") - line = Keyword.fetch!(meta, :line) + ) + when doc in [:doc, :typedoc] do + assert_module_scope(env, :@, 1) + unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") + line = Keyword.fetch!(meta, :line) - state = - state - |> add_current_env_to_line(line, env) - - {arg, state, env} = expand(arg, state, env) + state = + state + |> add_current_env_to_line(line, env) + + {arg, state, env} = expand(arg, state, env) + + state = + state + |> register_doc(env, doc, arg) - state = state - |> register_doc(env, doc, arg) - {{:@, meta, [{doc, doc_meta, [arg]}]}, state, env} + {{:@, meta, [{doc, doc_meta, [arg]}]}, state, env} end defp expand_macro( @@ -878,20 +897,22 @@ defmodule ElixirSense.Core.Compiler do state, env ) do - assert_module_scope(env, :@, 1) - unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") - line = Keyword.fetch!(meta, :line) + assert_module_scope(env, :@, 1) + unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") + line = Keyword.fetch!(meta, :line) - state = - state - |> add_current_env_to_line(line, env) - - {arg, state, env} = expand(arg, state, env) + state = + state + |> add_current_env_to_line(line, env) - # impl adds sets :hidden by default - state = state - |> register_doc(env, :doc, :impl) - {{:@, meta, [{:impl, doc_meta, [arg]}]}, state, env} + {arg, state, env} = expand(arg, state, env) + + # impl adds sets :hidden by default + state = + state + |> register_doc(env, :doc, :impl) + + {{:@, meta, [{:impl, doc_meta, [arg]}]}, state, env} end defp expand_macro( @@ -903,19 +924,21 @@ defmodule ElixirSense.Core.Compiler do state, env ) do - assert_module_scope(env, :@, 1) - unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") - line = Keyword.fetch!(meta, :line) + assert_module_scope(env, :@, 1) + unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") + line = Keyword.fetch!(meta, :line) - state = - state - |> add_current_env_to_line(line, env) - - {arg, state, env} = expand(arg, state, env) + state = + state + |> add_current_env_to_line(line, env) + + {arg, state, env} = expand(arg, state, env) - state = state - |> register_optional_callbacks(arg) - {{:@, meta, [{:optional_callbacks, doc_meta, [arg]}]}, state, env} + state = + state + |> register_optional_callbacks(arg) + + {{:@, meta, [{:optional_callbacks, doc_meta, [arg]}]}, state, env} end defp expand_macro( @@ -927,19 +950,21 @@ defmodule ElixirSense.Core.Compiler do state, env ) do - assert_module_scope(env, :@, 1) - unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") - line = Keyword.fetch!(meta, :line) + assert_module_scope(env, :@, 1) + unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") + line = Keyword.fetch!(meta, :line) - state = - state - |> add_current_env_to_line(line, env) - - {arg, state, env} = expand(arg, state, env) + state = + state + |> add_current_env_to_line(line, env) - state = state - |> register_doc(env, :doc, deprecated: arg) - {{:@, meta, [{:deprecated, doc_meta, [arg]}]}, state, env} + {arg, state, env} = expand(arg, state, env) + + state = + state + |> register_doc(env, :doc, deprecated: arg) + + {{:@, meta, [{:deprecated, doc_meta, [arg]}]}, state, env} end defp expand_macro( @@ -951,53 +976,54 @@ defmodule ElixirSense.Core.Compiler do state, env ) do - current_module = assert_module_scope(env, :@, 1) - unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") - - line = Keyword.fetch!(meta, :line) - column = Keyword.fetch!(meta, :column) + current_module = assert_module_scope(env, :@, 1) + unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") - state = List.wrap(derived_protos) - |> Enum.map(fn - {proto, _opts} -> proto - proto -> proto - end) - |> Enum.reduce(state, fn proto, acc -> - case expand(proto, acc, env) do - {proto_module, acc, _env} when is_atom(proto_module) -> - # protocol implementation module for Any - mod_any = Module.concat(proto_module, Any) - - # protocol implementation module built by @derive - mod = Module.concat(proto_module, current_module) - - case acc.mods_funs_to_positions[{mod_any, nil, nil}] do - nil -> - # implementation for: Any not detected (is in other file etc.) + line = Keyword.fetch!(meta, :line) + column = Keyword.fetch!(meta, :column) + + state = + List.wrap(derived_protos) + |> Enum.map(fn + {proto, _opts} -> proto + proto -> proto + end) + |> Enum.reduce(state, fn proto, acc -> + case expand(proto, acc, env) do + {proto_module, acc, _env} when is_atom(proto_module) -> + # protocol implementation module for Any + mod_any = Module.concat(proto_module, Any) + + # protocol implementation module built by @derive + mod = Module.concat(proto_module, current_module) + + case acc.mods_funs_to_positions[{mod_any, nil, nil}] do + nil -> + # implementation for: Any not detected (is in other file etc.) + acc + |> add_module_to_index(mod, {line, column}, nil, generated: true) + + _any_mods_funs -> + # copy implementation for: Any + copied_mods_funs_to_positions = + for {{module, fun, arity}, val} <- acc.mods_funs_to_positions, + module == mod_any, + into: %{}, + do: {{mod, fun, arity}, val} + + %{ acc - |> add_module_to_index(mod, {line, column}, nil, generated: true) - - _any_mods_funs -> - # copy implementation for: Any - copied_mods_funs_to_positions = - for {{module, fun, arity}, val} <- acc.mods_funs_to_positions, - module == mod_any, - into: %{}, - do: {{mod, fun, arity}, val} - - %{ - acc - | mods_funs_to_positions: - acc.mods_funs_to_positions |> Map.merge(copied_mods_funs_to_positions) - } - end + | mods_funs_to_positions: + acc.mods_funs_to_positions |> Map.merge(copied_mods_funs_to_positions) + } + end - :error -> - acc - end - end) + :error -> + acc + end + end) - {{:@, meta, [{:derive, doc_meta, [derived_protos]}]}, state, env} + {{:@, meta, [{:derive, doc_meta, [derived_protos]}]}, state, env} end defp expand_macro( @@ -1008,33 +1034,36 @@ defmodule ElixirSense.Core.Compiler do _callback, state, env - ) when kind in [:type, :typep, :opaque] do - assert_module_scope(env, :@, 1) - unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") + ) + when kind in [:type, :typep, :opaque] do + assert_module_scope(env, :@, 1) + unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") - {expr, state, env} = __MODULE__.Typespec.expand(expr, state, env) + {expr, state, env} = __MODULE__.Typespec.expand(expr, state, env) - case __MODULE__.Typespec.type_to_signature(expr) do - {name, [_type_arg]} when name in [:required, :optional] -> - raise "type #{name}/#{1} is a reserved type and it cannot be defined" - - {name, type_args} -> - # TODO elixir does Macro.escape with unquote: true + case __MODULE__.Typespec.type_to_signature(expr) do + {name, [_type_arg]} when name in [:required, :optional] -> + raise "type #{name}/#{1} is a reserved type and it cannot be defined" - spec = TypeInfo.typespec_to_string(kind, expr) + {name, type_args} -> + # TODO elixir does Macro.escape with unquote: true - {position = {line, _column}, end_position} = extract_range(attr_meta) + spec = TypeInfo.typespec_to_string(kind, expr) - state = state - |> add_type(env, name, type_args, spec, kind, position, end_position) - |> with_typespec({name, length(type_args)}) - |> add_current_env_to_line(line, env) - |> with_typespec(nil) + {position = {line, _column}, end_position} = extract_range(attr_meta) - {{:@, attr_meta, [{kind, kind_meta, [expr]}]}, state, env} - :error -> - {{:@, attr_meta, [{kind, kind_meta, [expr]}]}, state, env} - end + state = + state + |> add_type(env, name, type_args, spec, kind, position, end_position) + |> with_typespec({name, length(type_args)}) + |> add_current_env_to_line(line, env) + |> with_typespec(nil) + + {{:@, attr_meta, [{kind, kind_meta, [expr]}]}, state, env} + + :error -> + {{:@, attr_meta, [{kind, kind_meta, [expr]}]}, state, env} + end end defp expand_macro( @@ -1045,47 +1074,49 @@ defmodule ElixirSense.Core.Compiler do _callback, state, env - ) when kind in [:callback, :macrocallback, :spec] do - assert_module_scope(env, :@, 1) - unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") - - {expr, state, env} = __MODULE__.Typespec.expand(expr, state, env) - - case __MODULE__.Typespec.spec_to_signature(expr) do - {name, type_args} -> - spec = TypeInfo.typespec_to_string(kind, expr) - - {position = {line, _column}, end_position} = extract_range(attr_meta) - - state = - if kind in [:callback, :macrocallback] do - state - |> add_func_to_index( - env, - :behaviour_info, - [{:atom, attr_meta, nil}], - position, - end_position, - :def, - generated: true - ) - else - state - end + ) + when kind in [:callback, :macrocallback, :spec] do + assert_module_scope(env, :@, 1) + unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") + + {expr, state, env} = __MODULE__.Typespec.expand(expr, state, env) + + case __MODULE__.Typespec.spec_to_signature(expr) do + {name, type_args} -> + spec = TypeInfo.typespec_to_string(kind, expr) - state = state - |> add_spec(env, name, type_args, spec, kind, position, end_position, - generated: state.generated + {position = {line, _column}, end_position} = extract_range(attr_meta) + + state = + if kind in [:callback, :macrocallback] do + state + |> add_func_to_index( + env, + :behaviour_info, + [{:atom, attr_meta, nil}], + position, + end_position, + :def, + generated: true ) - |> with_typespec({name, length(type_args)}) - |> add_current_env_to_line(line, env) - |> with_typespec(nil) + else + state + end - {{:@, attr_meta, [{kind, kind_meta, [expr]}]}, state, env} + state = + state + |> add_spec(env, name, type_args, spec, kind, position, end_position, + generated: state.generated + ) + |> with_typespec({name, length(type_args)}) + |> add_current_env_to_line(line, env) + |> with_typespec(nil) - :error -> - {{:@, attr_meta, [{kind, kind_meta, [expr]}]}, state, env} - end + {{:@, attr_meta, [{kind, kind_meta, [expr]}]}, state, env} + + :error -> + {{:@, attr_meta, [{kind, kind_meta, [expr]}]}, state, env} + end end defp expand_macro( @@ -1096,39 +1127,45 @@ defmodule ElixirSense.Core.Compiler do _callback, state, env - ) when is_atom(name) do - assert_module_scope(env, :@, 1) - unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") - line = Keyword.fetch!(meta, :line) - column = Keyword.get(meta, :column, 1) - - {is_definition, {e_args, state, env}} = case args do - arg when is_atom(arg) -> - # @attribute - {false, {nil, state, env}} - [] -> - # deprecated @attribute() - {false, {nil, state, env}} - [_] -> - # @attribute(arg) - if env.function, do: raise "cannot set attribute @#{name} inside function/macro" - if name == :behavior, do: raise "@behavior attribute is not supported" - {true, expand_args(args, state, env)} - _ -> raise "invalid @ call" - end + ) + when is_atom(name) do + assert_module_scope(env, :@, 1) + unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") + line = Keyword.fetch!(meta, :line) + column = Keyword.get(meta, :column, 1) - inferred_type = case e_args do - nil -> nil - [arg] -> TypeInference.get_binding_type(state, arg) - end + {is_definition, {e_args, state, env}} = + case args do + arg when is_atom(arg) -> + # @attribute + {false, {nil, state, env}} - state = - state - |> add_attribute(name, inferred_type, is_definition, {line, column}) - |> add_current_env_to_line(line, env) - - - {{:@, meta, [{name, name_meta, e_args}]}, state, env} + [] -> + # deprecated @attribute() + {false, {nil, state, env}} + + [_] -> + # @attribute(arg) + if env.function, do: raise("cannot set attribute @#{name} inside function/macro") + if name == :behavior, do: raise("@behavior attribute is not supported") + {true, expand_args(args, state, env)} + + args -> + raise "invalid @ call #{inspect(args)}" + end + + inferred_type = + case e_args do + nil -> nil + [arg] -> TypeInference.get_binding_type(state, arg) + end + + state = + state + |> add_attribute(name, inferred_type, is_definition, {line, column}) + |> add_current_env_to_line(line, env) + + {{:@, meta, [{name, name_meta, e_args}]}, state, env} end defp expand_macro( @@ -1140,7 +1177,7 @@ defmodule ElixirSense.Core.Compiler do state, env ) do - assert_module_scope(env, :defoverridable, 1) + assert_module_scope(env, :defoverridable, 1) {arg, state, env} = expand(arg, state, env) case arg do @@ -1172,38 +1209,43 @@ defmodule ElixirSense.Core.Compiler do _callback, state, env - ) when type in [:defstruct, :defexception] do - module = assert_module_scope(env, type, 1) - if Map.has_key?(state.structs, module) do - raise ArgumentError, + ) + when type in [:defstruct, :defexception] do + module = assert_module_scope(env, type, 1) + + if Map.has_key?(state.structs, module) do + raise ArgumentError, "defstruct has already been called for " <> "#{inspect(module)}, defstruct can only be called once per module" - end - case fields do - fs when is_list(fs) -> - :ok - - other -> - raise ArgumentError, "struct fields definition must be list, got: #{inspect(other)}" - end + end - {position, end_position} = extract_range(meta) + case fields do + fs when is_list(fs) -> + :ok - fields = fields - |> Enum.filter(fn - field when is_atom(field) -> true - {field, _} when is_atom(field) -> true - _ -> false - end) - |> Enum.map(fn - field when is_atom(field) -> {field, nil} - {field, value} when is_atom(field) -> {field, value} - end) + other -> + raise ArgumentError, "struct fields definition must be list, got: #{inspect(other)}" + end - state = state - |> add_struct_or_exception(env, type, fields, position, end_position) + {position, end_position} = extract_range(meta) + + fields = + fields + |> Enum.filter(fn + field when is_atom(field) -> true + {field, _} when is_atom(field) -> true + _ -> false + end) + |> Enum.map(fn + field when is_atom(field) -> {field, nil} + {field, value} when is_atom(field) -> {field, value} + end) - {{type, meta, [fields]}, state, env} + state = + state + |> add_struct_or_exception(env, type, fields, position, end_position) + + {{type, meta, [fields]}, state, env} end defp expand_macro( @@ -1214,7 +1256,8 @@ defmodule ElixirSense.Core.Compiler do _callback, state, env - ) when call in [:defrecord, :defrecordp] do + ) + when call in [:defrecord, :defrecordp] do assert_no_match_or_guard_scope(env.context, :"{call}/2") module = assert_module_scope(env, call, 2) @@ -1228,27 +1271,28 @@ defmodule ElixirSense.Core.Compiler do options = [generated: true] - state = state - |> add_func_to_index( - env, - name, - [{:\\, [], [{:args, [], nil}, []]}], - position, - end_position, - type, - options - ) - |> add_func_to_index( - env, - name, - [{:record, [], nil}, {:args, [], nil}], - position, - end_position, - type, - options - ) - |> add_call_to_line({module, call, length(args)}, {line, column}) - |> add_current_env_to_line(line, env) + state = + state + |> add_func_to_index( + env, + name, + [{:\\, [], [{:args, [], nil}, []]}], + position, + end_position, + type, + options + ) + |> add_func_to_index( + env, + name, + [{:record, [], nil}, {:args, [], nil}], + position, + end_position, + type, + options + ) + |> add_call_to_line({module, call, length(args)}, {line, column}) + |> add_current_env_to_line(line, env) {{{:., meta, [Record, call]}, meta, args}, state, env} end @@ -1265,22 +1309,25 @@ defmodule ElixirSense.Core.Compiler do {position, end_position} = extract_range(meta) original_env = env # expand the macro normally - {ast, state, env} = expand_macro_callback!(meta, Kernel, :defprotocol, args, callback, state, env) + {ast, state, env} = + expand_macro_callback!(meta, Kernel, :defprotocol, args, callback, state, env) [module] = env.context_modules -- original_env.context_modules # add behaviour_info builtin # generate callbacks as macro expansion currently fails - state = state - |> add_func_to_index( - %{env | module: module}, - :behaviour_info, - [:atom], - position, - end_position, - :def, - [generated: true] - ) - |> generate_protocol_callbacks(%{env | module: module}) + state = + state + |> add_func_to_index( + %{env | module: module}, + :behaviour_info, + [:atom], + position, + end_position, + :def, + generated: true + ) + |> generate_protocol_callbacks(%{env | module: module}) + {ast, state, env} end @@ -1293,15 +1340,15 @@ defmodule ElixirSense.Core.Compiler do state, env ) do - expand_macro( - meta, - Kernel, - :defimpl, - [name, [], do_block], - callback, - state, - env - ) + expand_macro( + meta, + Kernel, + :defimpl, + [name, [], do_block], + callback, + state, + env + ) end defp expand_macro( @@ -1322,36 +1369,49 @@ defmodule ElixirSense.Core.Compiler do end) # TODO elixir uses expand_literals here - {for, state, _env} = expand(for, state, %{env | module: env.module || Elixir, function: {:__impl__, 1}}) + {for, state, _env} = + expand(for, state, %{env | module: env.module || Elixir, function: {:__impl__, 1}}) + {protocol, state, _env} = expand(name, state, env) impl = fn protocol, for, block, state, env -> name = Module.concat(protocol, for) + expand_macro( - meta, - Kernel, - :defmodule, - [name, [do: block]], - callback, - state, - env - ) + meta, + Kernel, + :defmodule, + [name, [do: block]], + callback, + state, + env + ) end - block = case opts do - [] -> raise ArgumentError, "defimpl expects a do-end block" - [do: block] -> block - _ -> raise ArgumentError, "unknown options given to defimpl, got: #{Macro.to_string(opts)}" - end + block = + case opts do + [] -> + raise ArgumentError, "defimpl expects a do-end block" + + [do: block] -> + block - for_wrapped = for - |> List.wrap + _ -> + raise ArgumentError, "unknown options given to defimpl, got: #{Macro.to_string(opts)}" + end - {ast, state, env} = for_wrapped - |> Enum.reduce({[], state, env}, fn for, {acc, state, env} -> - {ast, state, env} = impl.(protocol, for, block, %{state | protocol: {protocol, for_wrapped}}, env) - {[ast | acc], state, env} - end) + for_wrapped = + for + |> List.wrap() + + {ast, state, env} = + for_wrapped + |> Enum.reduce({[], state, env}, fn for, {acc, state, env} -> + {ast, state, env} = + impl.(protocol, for, block, %{state | protocol: {protocol, for_wrapped}}, env) + + {[ast | acc], state, env} + end) {Enum.reverse(ast), %{state | protocol: nil}, env} end @@ -1406,10 +1466,11 @@ defmodule ElixirSense.Core.Compiler do line = Keyword.fetch!(meta, :line) - module_functions = case state.protocol do - nil -> [] - _ -> [{:__impl__, [:atom], :def}] - end + module_functions = + case state.protocol do + nil -> [] + _ -> [{:__impl__, [:atom], :def}] + end state = state @@ -1419,14 +1480,16 @@ defmodule ElixirSense.Core.Compiler do |> add_module_functions(%{env | module: full}, module_functions, position, end_position) |> new_vars_scope |> new_attributes_scope - # TODO magic with ElixirEnv instead of new_vars_scope? + + # TODO magic with ElixirEnv instead of new_vars_scope? {state, _env} = maybe_add_protocol_behaviour(state, %{env | module: full}) {result, state, _env} = expand(block, state, %{env | module: full}) - state = state - |> apply_optional_callbacks(%{env | module: full}) + state = + state + |> apply_optional_callbacks(%{env | module: full}) {result, state, env} else @@ -1444,11 +1507,12 @@ defmodule ElixirSense.Core.Compiler do end # restore vars from outer scope - state = %{state | vars: vars, unused: unused} - |> maybe_move_vars_to_outer_scope - |> remove_vars_scope - |> remove_attributes_scope - |> remove_module + state = + %{state | vars: vars, unused: unused} + |> maybe_move_vars_to_outer_scope + |> remove_vars_scope + |> remove_attributes_scope + |> remove_module # TODO hardcode expansion? # to result of require (a module atom) and :elixir_module.compile dot call in block @@ -1456,9 +1520,20 @@ defmodule ElixirSense.Core.Compiler do {{:__block__, [], []}, state, env} end - defp expand_macro(meta, Protocol, :def, [{name, _, _args = [_ | _]} = call], callback, state, env) when is_atom(name) do + defp expand_macro( + meta, + Protocol, + :def, + [{name, _, _args = [_ | _]} = call], + callback, + state, + env + ) + when is_atom(name) do # transform protocol def to def with empty body - {ast, state, env} = expand_macro(meta, Kernel, :def, [call, {:__block__, [], []}], callback, state, env) + {ast, state, env} = + expand_macro(meta, Kernel, :def, [call, {:__block__, [], []}], callback, state, env) + {ast, state, env} end @@ -1500,15 +1575,17 @@ defmodule ElixirSense.Core.Compiler do else {call, expr} end - # dbg(call) - # dbg(expr) + + # dbg(call) + # dbg(expr) # state = # state # |> add_current_env_to_line(line, env) - state = %{state | vars: {%{}, false}, unused: 0} - |> new_func_vars_scope + state = + %{state | vars: {%{}, false}, unused: 0} + |> new_func_vars_scope {name_and_args, guards} = __MODULE__.Utils.extract_guards(call) # dbg(name_and_args) @@ -1568,14 +1645,16 @@ defmodule ElixirSense.Core.Compiler do # %{state | prematch: :warn} # TODO not sure vars scope is needed state = state |> new_vars_scope + {_e_body, state, _env} = expand(expr, state, %{g_env | context: nil, function: {name, arity}}) # restore vars from outer scope - state = %{state | vars: vars, unused: unused} - |> maybe_move_vars_to_outer_scope - |> remove_vars_scope - |> remove_func_vars_scope + state = + %{state | vars: vars, unused: unused} + |> maybe_move_vars_to_outer_scope + |> remove_vars_scope + |> remove_func_vars_scope # result of def expansion is fa tuple {{name, arity}, state, env} @@ -1586,6 +1665,13 @@ defmodule ElixirSense.Core.Compiler do end defp expand_macro_callback(meta, module, fun, args, callback, state, env) do + line = Keyword.get(meta, :line, 0) + column = Keyword.get(meta, :column) + + state = + state + |> add_call_to_line({module, fun, length(args)}, {line, column}) + # dbg({module, fun, args}) try do callback.(meta, args) @@ -1602,7 +1688,14 @@ defmodule ElixirSense.Core.Compiler do end end - defp expand_macro_callback!(meta, _module, _fun, args, callback, state, env) do + defp expand_macro_callback!(meta, module, fun, args, callback, state, env) do + line = Keyword.get(meta, :line, 0) + column = Keyword.get(meta, :column) + + state = + state + |> add_call_to_line({module, fun, length(args)}, {line, column}) + # dbg({module, fun, args}) ast = callback.(meta, args) {ast, state, env} = expand(ast, state, env) @@ -1682,12 +1775,13 @@ defmodule ElixirSense.Core.Compiler do line = Keyword.get(meta, :line, 0) column = Keyword.get(meta, :column, nil) - sl = if line > 0 do - sl - |> add_current_env_to_line(line, e) - else - sl - end + sl = + if line > 0 do + sl + |> add_current_env_to_line(line, e) + else + sl + end if context == :guard and is_tuple(receiver) do if Keyword.get(meta, :no_parens) != true do @@ -1701,9 +1795,11 @@ defmodule ElixirSense.Core.Compiler do case rewrite(context, receiver, dot_meta, right, attached_meta, e_args, s) do {:ok, rewritten} -> - s = __MODULE__.Env.close_write(sa, s) - |> add_call_to_line({receiver, right, length(e_args)}, {line, column}) - |> add_current_env_to_line(line, e) + s = + __MODULE__.Env.close_write(sa, s) + |> add_call_to_line({receiver, right, length(e_args)}, {line, column}) + |> add_current_env_to_line(line, e) + {rewritten, s, ea} {:error, _error} -> @@ -1712,8 +1808,8 @@ defmodule ElixirSense.Core.Compiler do end end - defp expand_remote(_receiver, _dot_meta, _right, _meta, _args, _, _, _e), - do: raise("invalid_call") + defp expand_remote(receiver, dot_meta, right, meta, args, _, _, _e), + do: raise("invalid_call remote #{inspect({{:., dot_meta, [receiver, right]}, meta, args})}") defp attach_runtime_module(receiver, meta, s, _e) do if receiver in s.runtime_modules do @@ -1742,21 +1838,23 @@ defmodule ElixirSense.Core.Compiler do # This fixes exactly 1 test... defp expand_local(meta, :when, [_, _] = args, state, env = %{context: nil}) do # naked when, try to transform into a case - ast = {:case, meta, - [ - {:_, meta, nil}, - [ - do: [ - {:->, meta, - [ + ast = + {:case, meta, + [ + {:_, meta, nil}, + [ + do: [ + {:->, meta, [ - {:when, meta, args} - ], - :ok - ]} - ] - ] - ]} + [ + {:when, meta, args} + ], + :ok + ]} + ] + ] + ]} + expand(ast, state, env) end @@ -1911,27 +2009,35 @@ defmodule ElixirSense.Core.Compiler do case state.mods_funs_to_positions[{module, name, arity}] do %State.ModFunInfo{overridable: {true, _}} = info -> - kind = case info.type do - :defdelegate -> :def - :defguard -> :defmacro - :defguardp -> :defmacrop - other -> other - end + kind = + case info.type do + :defdelegate -> :def + :defguard -> :defmacro + :defguardp -> :defmacrop + other -> other + end + hidden = Map.get(info.meta, :hidden, false) # def meta is not used anyway so let's pass empty meta = [] # TODO count 1 hardcoded but that's probably OK count = 1 + case hidden do false -> {kind, name, meta} + true when kind in [:defmacro, :defmacrop] -> {:defmacrop, overridable_name(name, count), meta} + true -> {:defp, overridable_name(name, count), meta} end - nil -> raise "no_super" + + nil -> + raise "no_super" end + _ -> raise "wrong_number_of_args_for_super" end @@ -1948,9 +2054,10 @@ defmodule ElixirSense.Core.Compiler do line = Keyword.get(attached_meta, :line, 0) column = Keyword.get(attached_meta, :column, nil) - se = se - |> add_call_to_line({remote, fun, arity}, {line, column}) - |> add_current_env_to_line(line, ee) + se = + se + |> add_call_to_line({remote, fun, arity}, {line, column}) + |> add_current_env_to_line(line, ee) {{:&, meta, [{:/, [], [{{:., dot_meta, [remote, fun]}, attached_meta, []}, arity]}]}, se, ee} @@ -1960,13 +2067,13 @@ defmodule ElixirSense.Core.Compiler do raise "undefined_local_capture" {{:local, fun, arity}, local_meta, _, se, ee} -> - line = Keyword.get(local_meta, :line, 0) column = Keyword.get(local_meta, :column, nil) - se = se - |> add_call_to_line({nil, fun, arity}, {line, column}) - |> add_current_env_to_line(line, ee) + se = + se + |> add_call_to_line({nil, fun, arity}, {line, column}) + |> add_current_env_to_line(line, ee) {{:&, meta, [{:/, [], [{fun, local_meta, nil}, arity]}]}, se, ee} @@ -1996,9 +2103,12 @@ defmodule ElixirSense.Core.Compiler do # TODO not sure new vars scope is actually needed sc = sc |> new_vars_scope {ed, sd, envd} = expand_for_do_block(meta, expr, sc, ec, maybe_reduce) - sd = sd - |> maybe_move_vars_to_outer_scope - |> remove_vars_scope + + sd = + sd + |> maybe_move_vars_to_outer_scope + |> remove_vars_scope + {{ed, sd, envd}, nopts} {:error, _error} -> @@ -2425,9 +2535,10 @@ defmodule ElixirSense.Core.Compiler do def merge_vars(s, %{vars: {read, write}}, _e) do # dbg(s.vars_info) # dbg({read, write}) - s = %{s | vars: {read, write}} - |> maybe_move_vars_to_outer_scope - |> remove_vars_scope + s = + %{s | vars: {read, write}} + |> maybe_move_vars_to_outer_scope + |> remove_vars_scope # dbg(s.vars_info) # dbg(s.vars_info_per_scope_id) @@ -2498,26 +2609,27 @@ defmodule ElixirSense.Core.Compiler do %{vars: current, unused: unused} = after_s %{vars: {read, _write}, prematch: prematch} = before_s - call_s = %{before_s | - prematch: {read, unused, :none}, - unused: unused, - vars: current, - calls: after_s.calls, - lines_to_env: after_s.lines_to_env, - vars_info: after_s.vars_info + call_s = %{ + before_s + | prematch: {read, unused, :none}, + unused: unused, + vars: current, + calls: after_s.calls, + lines_to_env: after_s.lines_to_env, + vars_info: after_s.vars_info } call_e = Map.put(e, :context, :match) {e_expr, %{vars: new_current, unused: new_unused} = s_expr, ee} = fun.(expr, call_s, call_e) - - end_s = %{after_s | - prematch: prematch, - unused: new_unused, - vars: new_current, - calls: s_expr.calls, - lines_to_env: s_expr.lines_to_env, - vars_info: s_expr.vars_info + end_s = %{ + after_s + | prematch: prematch, + unused: new_unused, + vars: new_current, + calls: s_expr.calls, + lines_to_env: s_expr.lines_to_env, + vars_info: s_expr.vars_info } end_e = Map.put(ee, :context, Map.get(e, :context)) @@ -2740,9 +2852,12 @@ defmodule ElixirSense.Core.Compiler do # TODO not sure new vars scope is needed acc = acc |> new_vars_scope {e_expr, s_acc, e_acc} = ElixirExpand.expand(expr, acc, e) - s_acc = s_acc - |> maybe_move_vars_to_outer_scope - |> remove_vars_scope + + s_acc = + s_acc + |> maybe_move_vars_to_outer_scope + |> remove_vars_scope + {e_expr, rest_opts, ElixirEnv.merge_vars(s_acc, s, e_acc)} end end @@ -3581,6 +3696,7 @@ defmodule ElixirSense.Core.Compiler do case arg do {var, _, context} when is_atom(var) and is_atom(context) -> capture(meta, {:/, meta, [arg, 0]}, s, e) + _ -> raise "invalid_args_for_capture #{inspect(arg)}" end @@ -3734,30 +3850,37 @@ defmodule ElixirSense.Core.Compiler do def has_unquotes({:quote, _, [child]}, quote_level) do has_unquotes(child, quote_level + 1) end + def has_unquotes({:quote, _, [quote_opts, child]}, quote_level) do case disables_unquote(quote_opts) do true -> false _ -> has_unquotes(child, quote_level + 1) end end + def has_unquotes({unquote, _, [child]}, quote_level) - when unquote in [:unquote, :unquote_splicing] do + when unquote in [:unquote, :unquote_splicing] do case quote_level do 0 -> true - _ -> has_unquotes(child, quote_level - 1) - end + _ -> has_unquotes(child, quote_level - 1) + end end + def has_unquotes({{:., _, [_, :unquote]}, _, [_]}, _), do: true def has_unquotes({var, _, ctx}, _) when is_atom(var) and is_atom(ctx), do: false + def has_unquotes({name, _, args}, quote_level) when is_list(args) do has_unquotes(name) or Enum.any?(args, fn child -> has_unquotes(child, quote_level) end) end + def has_unquotes({left, right}, quote_level) do has_unquotes(left, quote_level) or has_unquotes(right, quote_level) end + def has_unquotes(list, quote_level) when is_list(list) do Enum.any?(list, fn child -> has_unquotes(child, quote_level) end) end + def has_unquotes(_other, _), do: false defp disables_unquote([{:unquote, false} | _]), do: true @@ -3824,14 +3947,18 @@ defmodule ElixirSense.Core.Compiler do def is_valid(:unquote, unquote), do: is_boolean(unquote) def escape(expr, kind, unquote) do - do_quote(expr, %__MODULE__{ - line: true, - file: nil, - vars_hygiene: false, - aliases_hygiene: false, - imports_hygiene: false, - unquote: unquote - }, kind) + do_quote( + expr, + %__MODULE__{ + line: true, + file: nil, + vars_hygiene: false, + aliases_hygiene: false, + imports_hygiene: false, + unquote: unquote + }, + kind + ) end def quote(_meta, {:unquote_splicing, _, [_]}, _binding, %__MODULE__{unquote: true}, _, _), @@ -4437,7 +4564,7 @@ defmodule ElixirSense.Core.Compiler do {{:%, meta, [e_left, e_right]}, se, ee} false -> - raise "invalid_struct_name" + raise "invalid_struct_name #{inspect(e_left)}" end end @@ -4475,6 +4602,7 @@ defmodule ElixirSense.Core.Compiler do else true end + _ -> false end) @@ -4541,16 +4669,18 @@ defmodule ElixirSense.Core.Compiler do else %{:__struct__ => ^name} = struct -> struct + _ -> # recover from invalid return value - [__struct__: name] |> Keyword.merge(hd(args)) |> Elixir.Map.new + [__struct__: name] |> Keyword.merge(hd(args)) |> Elixir.Map.new() rescue _ -> # recover from error by building the fake struct - [__struct__: name] |> Keyword.merge(hd(args)) |> Elixir.Map.new + [__struct__: name] |> Keyword.merge(hd(args)) |> Elixir.Map.new() end + info -> - info.fields |> Keyword.merge(hd(args)) |> Elixir.Map.new + info.fields |> Keyword.merge(hd(args)) |> Elixir.Map.new() end end end @@ -4571,18 +4701,19 @@ defmodule ElixirSense.Core.Compiler do def expand(ast, state, env) do # TODO this should handle remote calls, attributes unquotes? - {ast, {state, env}} = Macro.prewalk(ast, {state, env}, fn - {:__aliases__, _meta, list} = node, {state, env} when is_list(list) -> - {node, state, env} = ElixirExpand.expand(node, state, env) - {node, {state, env}} - - {:__MODULE__, _meta, ctx} = node, {state, env} when is_atom(ctx) -> - {node, state, env} = ElixirExpand.expand(node, state, env) - {node, {state, env}} - - other, acc -> - {other, acc} - end) + {ast, {state, env}} = + Macro.prewalk(ast, {state, env}, fn + {:__aliases__, _meta, list} = node, {state, env} when is_list(list) -> + {node, state, env} = ElixirExpand.expand(node, state, env) + {node, {state, env}} + + {:__MODULE__, _meta, ctx} = node, {state, env} when is_atom(ctx) -> + {node, state, env} = ElixirExpand.expand(node, state, env) + {node, {state, env}} + + other, acc -> + {other, acc} + end) {ast, state, env} end diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index 5651df77..f05fd3fe 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -30,6 +30,7 @@ defmodule ElixirSense.Core.MetadataBuilder do def build(ast) do if Version.match?(System.version(), ">= 1.17.0-dev") do {_ast, state, _env} = Compiler.expand(ast, %State{}, Compiler.env()) + state |> remove_attributes_scope |> remove_lexical_scope @@ -272,8 +273,6 @@ defmodule ElixirSense.Core.MetadataBuilder do end defp pre_clause({_clause, _meta, _} = ast, state, lhs) do - - _vars = state |> find_vars(lhs, Enum.at(state.binding_context, 0)) diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index f3b38532..9a23132d 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -43,7 +43,8 @@ defmodule ElixirSense.Core.State do scope_attributes: list(list(atom)), behaviours: %{optional(module) => [module]}, specs: specs_t, - vars_info: list(%{optional({atom, non_neg_integer}) => ElixirSense.Core.State.VarInfo.t()}), + vars_info: + list(%{optional({atom, non_neg_integer}) => ElixirSense.Core.State.VarInfo.t()}), scope_id_count: non_neg_integer, scope_ids: list(scope_id_t), vars_info_per_scope_id: vars_info_per_scope_id_t, @@ -337,18 +338,22 @@ defmodule ElixirSense.Core.State do # vars_info has both read and write vars # filter to return only read - vars = hd(state.vars_info) |> Map.values - |> Enum.filter(& Map.has_key?(elem(state.vars, 0), {&1.name, nil})) - - current_protocol = case state.protocol do - nil -> nil - {protocol, for_list} -> - # check wether we are in implementation or implementation child module - if Enum.any?(for_list, fn for -> macro_env.module == Module.concat(protocol, for) end) do - {protocol, for_list} - end - end - + vars = + hd(state.vars_info) + |> Map.values() + |> Enum.filter(&Map.has_key?(elem(state.vars, 0), {&1.name, nil})) + + current_protocol = + case state.protocol do + nil -> + nil + + {protocol, for_list} -> + # check wether we are in implementation or implementation child module + if Enum.any?(for_list, fn for -> macro_env.module == Module.concat(protocol, for) end) do + {protocol, for_list} + end + end %Env{ functions: macro_env.functions, @@ -418,6 +423,7 @@ defmodule ElixirSense.Core.State do state end end + def add_first_alias_positions(%__MODULE__{} = state, _env, _meta), do: state def add_call_to_line( @@ -842,7 +848,7 @@ defmodule ElixirSense.Core.State do [scope_id | _other_scope_ids] = state.scope_ids [current_scope_vars | _other_scope_vars] = state.vars_info - Map.put(state.vars_info_per_scope_id, scope_id, current_scope_vars |> Map.values) + Map.put(state.vars_info_per_scope_id, scope_id, current_scope_vars |> Map.values()) end def remove_attributes_scope(%__MODULE__{} = state) do @@ -1099,7 +1105,13 @@ defmodule ElixirSense.Core.State do line = meta[:line] column = meta[:column] scope_id = hd(state.scope_ids) - info = %VarInfo{name: name, is_definition: true, positions: [{line, column}], scope_id: scope_id} + + info = %VarInfo{ + name: name, + is_definition: true, + positions: [{line, column}], + scope_id: scope_id + } [vars_from_scope | other_vars] = state.vars_info vars_from_scope = Map.put(vars_from_scope, {name, version}, info) @@ -1109,6 +1121,7 @@ defmodule ElixirSense.Core.State do | vars_info: [vars_from_scope | other_vars] } end + def add_var_write(%__MODULE__{} = state, _), do: state def add_var_read(%__MODULE__{} = state, {name, meta, nil}) when name != :_ do @@ -1119,7 +1132,7 @@ defmodule ElixirSense.Core.State do [vars_from_scope | other_vars] = state.vars_info info = Map.fetch!(vars_from_scope, {name, version}) - info = %VarInfo{info | positions: (info.positions ++ [{line, column}]) |> Enum.uniq} + info = %VarInfo{info | positions: (info.positions ++ [{line, column}]) |> Enum.uniq()} vars_from_scope = Map.put(vars_from_scope, {name, version}, info) %__MODULE__{ @@ -1127,6 +1140,7 @@ defmodule ElixirSense.Core.State do | vars_info: [vars_from_scope | other_vars] } end + def add_var_read(%__MODULE__{} = state, _), do: state @builtin_attributes ElixirSense.Core.BuiltinAttributes.all() @@ -1507,15 +1521,20 @@ defmodule ElixirSense.Core.State do {ast, state, env} end - def maybe_move_vars_to_outer_scope(%__MODULE__{vars_info: [current_scope_vars, outer_scope_vars | other_scopes_vars]} = state) do - outer_scope_vars = for {key, _} <- outer_scope_vars, into: %{}, do: ( - # TODO merge type? - {key, current_scope_vars[key]} - ) + def maybe_move_vars_to_outer_scope( + %__MODULE__{vars_info: [current_scope_vars, outer_scope_vars | other_scopes_vars]} = state + ) do + outer_scope_vars = + for {key, _} <- outer_scope_vars, + into: %{}, + # TODO merge type? + do: {key, current_scope_vars[key]} + vars_info = [current_scope_vars, outer_scope_vars | other_scopes_vars] %__MODULE__{state | vars_info: vars_info} end + def maybe_move_vars_to_outer_scope(state), do: state def no_alias_expansion({:__aliases__, _, [h | t]} = _aliases) when is_atom(h) do @@ -1611,7 +1630,7 @@ defmodule ElixirSense.Core.State do ) end) end - + def macro_env(%__MODULE__{} = state, meta \\ []) do function = case hd(hd(state.scopes)) do diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 403a71e2..36a803f1 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -9,7 +9,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do @attribute_binding_support true or Version.match?(System.version(), "< 1.17.0-dev") @expand_eval false @binding_support Version.match?(System.version(), "< 1.17.0-dev") - @macro_calls_support Version.match?(System.version(), "< 1.17.0-dev") @typespec_calls_support Version.match?(System.version(), "< 1.17.0-dev") @compiler Code.ensure_loaded?(ElixirSense.Core.Compiler) @@ -1340,7 +1339,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } ] = get_line_attributes(state, 9) end - + describe "binding" do test "module attributes binding" do state = @@ -1358,50 +1357,50 @@ defmodule ElixirSense.Core.MetadataBuilderTest do end """ |> string_to_state - + assert get_line_attributes(state, 10) == [ - %ElixirSense.Core.State.AttributeInfo{ - name: :myattribute, - positions: [{2, 3}, {3, 11}], - type: {:atom, String} - }, - %AttributeInfo{ - name: :otherattribute, - positions: [{10, 3}], - type: - {:call, {:atom, Application}, :get_env, + %ElixirSense.Core.State.AttributeInfo{ + name: :myattribute, + positions: [{2, 3}, {3, 11}], + type: {:atom, String} + }, + %AttributeInfo{ + name: :otherattribute, + positions: [{10, 3}], + type: + {:call, {:atom, Application}, :get_env, [atom: :elixir_sense, atom: :some_attribute, atom: MyModule.InnerModule]} - } - ] - + } + ] + assert get_line_attributes(state, 3) == [ - %AttributeInfo{ - name: :myattribute, - positions: [{2, 3}, {3, 11}], - type: {:atom, String} - } - ] - + %AttributeInfo{ + name: :myattribute, + positions: [{2, 3}, {3, 11}], + type: {:atom, String} + } + ] + assert get_line_attributes(state, 7) == [ - %AttributeInfo{ - name: :inner_attr, - positions: [{5, 5}, {7, 13}], - type: {:map, [abc: {:atom, nil}], nil} - }, - %AttributeInfo{ - name: :inner_attr_1, - positions: [{6, 5}], - type: {:atom, MyModule.InnerModule} - } - ] - + %AttributeInfo{ + name: :inner_attr, + positions: [{5, 5}, {7, 13}], + type: {:map, [abc: {:atom, nil}], nil} + }, + %AttributeInfo{ + name: :inner_attr_1, + positions: [{6, 5}], + type: {:atom, MyModule.InnerModule} + } + ] + assert get_line_attributes(state, 9) == [ - %AttributeInfo{ - name: :myattribute, - positions: [{2, 3}, {3, 11}], - type: {:atom, String} - } - ] + %AttributeInfo{ + name: :myattribute, + positions: [{2, 3}, {3, 11}], + type: {:atom, String} + } + ] end test "module attributes rebinding" do @@ -1422,28 +1421,28 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert get_line_attributes(state, 3) == [ - %AttributeInfo{ - name: :myattribute, - positions: [{2, 3}], - type: {:atom, String} - } - ] + %AttributeInfo{ + name: :myattribute, + positions: [{2, 3}], + type: {:atom, String} + } + ] assert get_line_attributes(state, 6) == [ - %AttributeInfo{ - name: :myattribute, - positions: [{2, 3}, {4, 3}, {5, 3}], - type: {:atom, List} - } - ] + %AttributeInfo{ + name: :myattribute, + positions: [{2, 3}, {4, 3}, {5, 3}], + type: {:atom, List} + } + ] assert get_line_attributes(state, 10) == [ - %AttributeInfo{ - name: :myattribute, - positions: [{2, 3}, {4, 3}, {5, 3}, {8, 5}], - type: {:atom, List} - } - ] + %AttributeInfo{ + name: :myattribute, + positions: [{2, 3}, {4, 3}, {5, 3}, {8, 5}], + type: {:atom, List} + } + ] end test "module attributes value binding" do @@ -1458,17 +1457,17 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert get_line_attributes(state, 4) == [ - %AttributeInfo{ - name: :myattribute, - positions: [{2, 3}, {3, 14}], - type: {:map, [abc: {:atom, String}], nil} - }, - %AttributeInfo{ - name: :some_attr, - positions: [{3, 3}], - type: {:attribute, :myattribute} - } - ] + %AttributeInfo{ + name: :myattribute, + positions: [{2, 3}, {3, 14}], + type: {:map, [abc: {:atom, String}], nil} + }, + %AttributeInfo{ + name: :some_attr, + positions: [{3, 3}], + type: {:attribute, :myattribute} + } + ] end if @binding_support do @@ -1479,7 +1478,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do IO.puts("") """ |> string_to_state - + assert [%VarInfo{type: {:atom, :my_var}}] = state |> get_line_vars(2) end @@ -2469,10 +2468,11 @@ defmodule ElixirSense.Core.MetadataBuilderTest do vars = state |> get_line_vars(6) assert ([ - %VarInfo{name: :var1, positions: [{3, 19}, {3, 37}], scope_id: scope_id_1}, - %VarInfo{name: :var1, positions: [{4, 5}], scope_id: scope_id_2}, - %VarInfo{name: :var1, positions: [{5, 5}], scope_id: scope_id_2} - ] when scope_id_2 > scope_id_1) = vars + %VarInfo{name: :var1, positions: [{3, 19}, {3, 37}], scope_id: scope_id_1}, + %VarInfo{name: :var1, positions: [{4, 5}], scope_id: scope_id_2}, + %VarInfo{name: :var1, positions: [{5, 5}], scope_id: scope_id_2} + ] + when scope_id_2 > scope_id_1) = vars end test "vars defined inside a module" do @@ -4513,12 +4513,13 @@ defmodule ElixirSense.Core.MetadataBuilderTest do end test "current module and protocol implementation - simple case" do - state = """ - defimpl Inspect, for: Atom do - IO.puts("") - end - """ - |> string_to_state + state = + """ + defimpl Inspect, for: Atom do + IO.puts("") + end + """ + |> string_to_state assert get_line_module(state, 2) == Inspect.Atom assert get_line_protocol(state, 2) == {Inspect, [Atom]} @@ -4762,31 +4763,31 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert %{ - {Enumerable.MyOtherStruct, nil, nil} => %ModFunInfo{ - params: [nil], - type: :defmodule - }, - {Proto.Any, nil, nil} => %ModFunInfo{ - params: [nil], - type: :defmodule - }, - {Proto.MyOtherStruct, nil, nil} => %ModFunInfo{ - params: [nil], - type: :defmodule - }, - {Proto.MyOtherStruct, :reverse, 1} => %ModFunInfo{ - params: [[{:term, [line: 6, column: 15], nil}]], - type: :def - }, - {Proto.MyStruct, nil, nil} => %ModFunInfo{ - params: [nil], - type: :defmodule - }, - {Proto.MyStruct, :reverse, 1} => %ModFunInfo{ - params: [[{:term, [line: 6, column: 15], nil}]], - type: :def - } - } = state.mods_funs_to_positions + {Enumerable.MyOtherStruct, nil, nil} => %ModFunInfo{ + params: [nil], + type: :defmodule + }, + {Proto.Any, nil, nil} => %ModFunInfo{ + params: [nil], + type: :defmodule + }, + {Proto.MyOtherStruct, nil, nil} => %ModFunInfo{ + params: [nil], + type: :defmodule + }, + {Proto.MyOtherStruct, :reverse, 1} => %ModFunInfo{ + params: [[{:term, [line: 6, column: 15], nil}]], + type: :def + }, + {Proto.MyStruct, nil, nil} => %ModFunInfo{ + params: [nil], + type: :defmodule + }, + {Proto.MyStruct, :reverse, 1} => %ModFunInfo{ + params: [[{:term, [line: 6, column: 15], nil}]], + type: :def + } + } = state.mods_funs_to_positions end end @@ -4804,42 +4805,47 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert %{ - {Proto, :with_spec, 2} => %ElixirSense.Core.State.SpecInfo{ - args: [["t", "boolean"], ["t", "integer"]], - kind: :callback, - name: :with_spec, - positions: [{3, 3}, {2, 3}], - end_positions: [{3, 40}, {2, 42}], - generated: [false, false], - specs: [ - "@callback with_spec(t, boolean) :: number", - "@callback with_spec(t, integer) :: String.t()", - "@spec with_spec(t, boolean) :: number", - "@spec with_spec(t, integer) :: String.t()" - ] - }, - {Proto, :without_spec, 2} => %ElixirSense.Core.State.SpecInfo{ - args: [["t", "integer"]], - kind: :callback, - name: :without_spec, - positions: [{6, 3}], - end_positions: [nil], - generated: [true], - specs: ["@callback without_spec(t, integer) :: term"] - }, - {Proto, :__protocol__, 1} => %ElixirSense.Core.State.SpecInfo{ - kind: :spec, - specs: ["@spec __protocol__(:impls) :: :not_consolidated | {:consolidated, [module]}", "@spec __protocol__(:consolidated?) :: boolean", "@spec __protocol__(:functions) :: unquote(Protocol.__functions_spec__(@__functions__))", "@spec __protocol__(:module) :: Proto"] - }, - {Proto, :impl_for, 1} => %ElixirSense.Core.State.SpecInfo{ - kind: :spec, - specs: ["@spec impl_for(term) :: atom | nil"] - }, - {Proto, :impl_for!, 1} => %ElixirSense.Core.State.SpecInfo{ - kind: :spec, - specs: ["@spec impl_for!(term) :: atom"] - } - } = state.specs + {Proto, :with_spec, 2} => %ElixirSense.Core.State.SpecInfo{ + args: [["t", "boolean"], ["t", "integer"]], + kind: :callback, + name: :with_spec, + positions: [{3, 3}, {2, 3}], + end_positions: [{3, 40}, {2, 42}], + generated: [false, false], + specs: [ + "@callback with_spec(t, boolean) :: number", + "@callback with_spec(t, integer) :: String.t()", + "@spec with_spec(t, boolean) :: number", + "@spec with_spec(t, integer) :: String.t()" + ] + }, + {Proto, :without_spec, 2} => %ElixirSense.Core.State.SpecInfo{ + args: [["t", "integer"]], + kind: :callback, + name: :without_spec, + positions: [{6, 3}], + end_positions: [nil], + generated: [true], + specs: ["@callback without_spec(t, integer) :: term"] + }, + {Proto, :__protocol__, 1} => %ElixirSense.Core.State.SpecInfo{ + kind: :spec, + specs: [ + "@spec __protocol__(:impls) :: :not_consolidated | {:consolidated, [module]}", + "@spec __protocol__(:consolidated?) :: boolean", + "@spec __protocol__(:functions) :: unquote(Protocol.__functions_spec__(@__functions__))", + "@spec __protocol__(:module) :: Proto" + ] + }, + {Proto, :impl_for, 1} => %ElixirSense.Core.State.SpecInfo{ + kind: :spec, + specs: ["@spec impl_for(term) :: atom | nil"] + }, + {Proto, :impl_for!, 1} => %ElixirSense.Core.State.SpecInfo{ + kind: :spec, + specs: ["@spec impl_for!(term) :: atom"] + } + } = state.specs end test "registers positions" do @@ -4889,12 +4895,12 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert %{ - {Reversible, :reverse, 1} => %ModFunInfo{ - params: [[{:term, [line: 2, column: 15], nil}]], - positions: [{2, 3}], - type: :def - } - } = state.mods_funs_to_positions + {Reversible, :reverse, 1} => %ModFunInfo{ + params: [[{:term, [line: 2, column: 15], nil}]], + positions: [{2, 3}], + type: :def + } + } = state.mods_funs_to_positions end test "registers def positions in protocol implementation" do @@ -4922,17 +4928,17 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert %{ - {Impls, nil, nil} => %ModFunInfo{ - params: [nil], - positions: [{11, 1}], - type: :defmodule - }, - {Reversible.String, :__impl__, 1} => %ElixirSense.Core.State.ModFunInfo{ - params: [[{:atom, [line: 6, column: 1], nil}]], - positions: [{6, 1}], - type: :def - } - } = state.mods_funs_to_positions + {Impls, nil, nil} => %ModFunInfo{ + params: [nil], + positions: [{11, 1}], + type: :defmodule + }, + {Reversible.String, :__impl__, 1} => %ElixirSense.Core.State.ModFunInfo{ + params: [[{:atom, [line: 6, column: 1], nil}]], + positions: [{6, 1}], + type: :def + } + } = state.mods_funs_to_positions end test "functions head" do @@ -4953,7 +4959,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do [nil], [1], [ - {:\\, [line: 2, column: 13], [{:a, [line: 2, column: 11], nil}, nil]}, + {:\\, [line: 2, column: 13], [{:a, [line: 2, column: 11], nil}, nil]} ] ] } @@ -5549,160 +5555,160 @@ defmodule ElixirSense.Core.MetadataBuilderTest do %AttributeInfo{name: :my_attribute, positions: [{2, _}]} ] = get_line_attributes(state, 4) - assert %{ - {InheritMod, :handle_call, 3} => %ModFunInfo{ - params: [ - [ - {:msg, _, _}, - {:_from, _, _}, - {:state, _, _} - ] - ], - type: :def - }, - {InheritMod, nil, nil} => %ModFunInfo{ - type: :defmodule - }, - {InheritMod, :private_func, 0} => %ModFunInfo{ - params: [[]], - type: :defp - }, - {InheritMod, :private_func_arg, 1} => %ModFunInfo{ - params: [ - [{:a, _, _}], - [{:\\, _, [{:a, _, _}, nil]}] - ], - type: :defp - }, - {InheritMod, :private_guard, 0} => %ModFunInfo{ - params: [[]], - type: :defguardp - }, - {InheritMod, :private_guard_arg, 1} => %ModFunInfo{ - params: [ - [ - {:a, _, _} - ] - ], - type: :defguardp - }, - {InheritMod, :private_macro, 0} => %ModFunInfo{ - params: [[]], - type: :defmacrop - }, - {InheritMod, :private_macro_arg, 1} => %ModFunInfo{ - params: [ - [ - {:a, _, _} - ] - ], - type: :defmacrop - }, - {InheritMod, :public_func, 0} => %ModFunInfo{ - params: [[]], - type: :def, - overridable: {true, ElixirSenseExample.ExampleBehaviour} - }, - {InheritMod, :public_func_arg, 2} => %ModFunInfo{ - params: [ - [ - {:b, _, _}, - {:\\, _, - [ - {:a, _, _}, - "def" - ]} - ] - ], - type: :def - }, - {InheritMod, :public_guard, 0} => %ModFunInfo{ - params: [[]], - type: :defguard - }, - {InheritMod, :public_guard_arg, 1} => %ModFunInfo{ - params: [ - [ - {:a, _, _} - ] - ], - type: :defguard - }, - {InheritMod, :public_macro, 0} => %ModFunInfo{ - params: [[]], - type: :defmacro - }, - {InheritMod, :public_macro_arg, 1} => %ModFunInfo{ - params: [ - [ - {:a, _, _} - ] - ], - type: :defmacro - }, - {InheritMod.Deeply.Nested, nil, nil} => %ModFunInfo{ - type: :defmodule - }, - {InheritMod.Nested, nil, nil} => %ModFunInfo{ - type: :defmodule - }, - {InheritMod.ProtocolEmbedded, nil, nil} => %ModFunInfo{ - type: :defmodule - }, - {InheritMod, :behaviour_info, 1} => %ModFunInfo{ - params: [[{:atom, _, nil}]], - type: :def - }, - {InheritMod.ProtocolEmbedded, :module_info, 1} => %ModFunInfo{} - } = state.mods_funs_to_positions + assert %{ + {InheritMod, :handle_call, 3} => %ModFunInfo{ + params: [ + [ + {:msg, _, _}, + {:_from, _, _}, + {:state, _, _} + ] + ], + type: :def + }, + {InheritMod, nil, nil} => %ModFunInfo{ + type: :defmodule + }, + {InheritMod, :private_func, 0} => %ModFunInfo{ + params: [[]], + type: :defp + }, + {InheritMod, :private_func_arg, 1} => %ModFunInfo{ + params: [ + [{:a, _, _}], + [{:\\, _, [{:a, _, _}, nil]}] + ], + type: :defp + }, + {InheritMod, :private_guard, 0} => %ModFunInfo{ + params: [[]], + type: :defguardp + }, + {InheritMod, :private_guard_arg, 1} => %ModFunInfo{ + params: [ + [ + {:a, _, _} + ] + ], + type: :defguardp + }, + {InheritMod, :private_macro, 0} => %ModFunInfo{ + params: [[]], + type: :defmacrop + }, + {InheritMod, :private_macro_arg, 1} => %ModFunInfo{ + params: [ + [ + {:a, _, _} + ] + ], + type: :defmacrop + }, + {InheritMod, :public_func, 0} => %ModFunInfo{ + params: [[]], + type: :def, + overridable: {true, ElixirSenseExample.ExampleBehaviour} + }, + {InheritMod, :public_func_arg, 2} => %ModFunInfo{ + params: [ + [ + {:b, _, _}, + {:\\, _, + [ + {:a, _, _}, + "def" + ]} + ] + ], + type: :def + }, + {InheritMod, :public_guard, 0} => %ModFunInfo{ + params: [[]], + type: :defguard + }, + {InheritMod, :public_guard_arg, 1} => %ModFunInfo{ + params: [ + [ + {:a, _, _} + ] + ], + type: :defguard + }, + {InheritMod, :public_macro, 0} => %ModFunInfo{ + params: [[]], + type: :defmacro + }, + {InheritMod, :public_macro_arg, 1} => %ModFunInfo{ + params: [ + [ + {:a, _, _} + ] + ], + type: :defmacro + }, + {InheritMod.Deeply.Nested, nil, nil} => %ModFunInfo{ + type: :defmodule + }, + {InheritMod.Nested, nil, nil} => %ModFunInfo{ + type: :defmodule + }, + {InheritMod.ProtocolEmbedded, nil, nil} => %ModFunInfo{ + type: :defmodule + }, + {InheritMod, :behaviour_info, 1} => %ModFunInfo{ + params: [[{:atom, _, nil}]], + type: :def + }, + {InheritMod.ProtocolEmbedded, :module_info, 1} => %ModFunInfo{} + } = state.mods_funs_to_positions - assert %{ - {InheritMod, :my_opaque_type, 0} => %State.TypeInfo{ - args: [[]], - kind: :opaque, - name: :my_opaque_type, - # positions: [{2, 3}], - specs: ["@opaque my_opaque_type :: any"] - }, - {InheritMod, :my_priv_type, 0} => %State.TypeInfo{ - args: [[]], - kind: :typep, - name: :my_priv_type, - # positions: [{2, 3}], - specs: ["@typep my_priv_type :: any"] - }, - {InheritMod, :my_pub_type, 0} => %State.TypeInfo{ - args: [[]], - kind: :type, - name: :my_pub_type, - # positions: [{2, 3}], - specs: ["@type my_pub_type :: any"] - }, - {InheritMod, :my_pub_type_arg, 2} => %State.TypeInfo{ - args: [["a", "b"]], - kind: :type, - name: :my_pub_type_arg, - # positions: [{2, 3}], - specs: ["@type my_pub_type_arg(a, b) :: {b, a}"] - } - } = state.types + assert %{ + {InheritMod, :my_opaque_type, 0} => %State.TypeInfo{ + args: [[]], + kind: :opaque, + name: :my_opaque_type, + # positions: [{2, 3}], + specs: ["@opaque my_opaque_type :: any"] + }, + {InheritMod, :my_priv_type, 0} => %State.TypeInfo{ + args: [[]], + kind: :typep, + name: :my_priv_type, + # positions: [{2, 3}], + specs: ["@typep my_priv_type :: any"] + }, + {InheritMod, :my_pub_type, 0} => %State.TypeInfo{ + args: [[]], + kind: :type, + name: :my_pub_type, + # positions: [{2, 3}], + specs: ["@type my_pub_type :: any"] + }, + {InheritMod, :my_pub_type_arg, 2} => %State.TypeInfo{ + args: [["a", "b"]], + kind: :type, + name: :my_pub_type_arg, + # positions: [{2, 3}], + specs: ["@type my_pub_type_arg(a, b) :: {b, a}"] + } + } = state.types - assert %{ - {InheritMod, :private_func, 0} => %State.SpecInfo{ - args: [[]], - kind: :spec, - name: :private_func, - # positions: [{2, 3}], - specs: ["@spec private_func() :: String.t()"] - }, - {InheritMod, :some_callback, 1} => %State.SpecInfo{ - args: [["abc"]], - kind: :callback, - name: :some_callback, - # positions: [{2, 3}], - specs: ["@callback some_callback(abc) :: :ok when abc: integer"] - } - } = state.specs + assert %{ + {InheritMod, :private_func, 0} => %State.SpecInfo{ + args: [[]], + kind: :spec, + name: :private_func, + # positions: [{2, 3}], + specs: ["@spec private_func() :: String.t()"] + }, + {InheritMod, :some_callback, 1} => %State.SpecInfo{ + args: [["abc"]], + kind: :callback, + name: :some_callback, + # positions: [{2, 3}], + specs: ["@callback some_callback(abc) :: :ok when abc: integer"] + } + } = state.specs end test "use defining struct" do @@ -6341,9 +6347,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ - 3 => [%CallInfo{arity: 1, func: :func, position: {3, 26}, mod: NyModule}] - } + assert Enum.any?( + state.calls[3], + &match?(%CallInfo{arity: 1, position: {3, 26}, func: :func, mod: NyModule}, &1) + ) end test "registers calls pipe operator no parens" do @@ -6357,9 +6364,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ - 3 => [%CallInfo{arity: 1, func: :func, position: {3, 21}, mod: MyMod}] - } + assert Enum.any?( + state.calls[3], + &match?(%CallInfo{arity: 1, position: {3, 21}, func: :func, mod: MyMod}, &1) + ) end test "registers calls pipe operator" do @@ -6373,9 +6381,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ - 3 => [%CallInfo{arity: 1, func: :func, position: {3, 21}, mod: MyMod}] - } + assert Enum.any?( + state.calls[3], + &match?(%CallInfo{arity: 1, position: {3, 21}, func: :func, mod: MyMod}, &1) + ) end test "registers calls pipe operator with arg" do @@ -6389,9 +6398,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ - 3 => [%CallInfo{arity: 2, func: :func, position: {3, 21}, mod: MyMod}] - } + assert Enum.any?( + state.calls[3], + &match?(%CallInfo{arity: 2, position: {3, 21}, func: :func, mod: MyMod}, &1) + ) end test "registers calls pipe operator erlang module" do @@ -6405,9 +6415,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ - 3 => [%CallInfo{arity: 2, func: :func, position: {3, 23}, mod: :my_mod}] - } + assert Enum.any?( + state.calls[3], + &match?(%CallInfo{arity: 2, position: {3, 23}, func: :func, mod: :my_mod}, &1) + ) end test "registers calls pipe operator atom module" do @@ -6421,9 +6432,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ - 3 => [%CallInfo{arity: 2, func: :func, position: {3, 31}, mod: MyMod}] - } + assert Enum.any?( + state.calls[3], + &match?(%CallInfo{arity: 2, position: {3, 31}, func: :func, mod: MyMod}, &1) + ) end test "registers calls pipe operator local" do @@ -6437,9 +6449,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ - 3 => [%CallInfo{arity: 2, func: :func, position: {3, 15}, mod: nil}] - } + assert Enum.any?( + state.calls[3], + &match?(%CallInfo{arity: 2, position: {3, 15}, func: :func, mod: nil}, &1) + ) end test "registers calls pipe operator nested external into local" do @@ -6453,12 +6466,15 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ - 3 => [ - %CallInfo{arity: 1, position: {3, 21}, func: :func, mod: MyMod}, - %CallInfo{arity: 1, position: {3, 31}, func: :other, mod: nil} - ] - } + assert Enum.any?( + state.calls[3], + &match?(%CallInfo{arity: 1, position: {3, 21}, func: :func, mod: MyMod}, &1) + ) + + assert Enum.any?( + state.calls[3], + &match?(%CallInfo{arity: 1, position: {3, 31}, func: :other, mod: nil}, &1) + ) end test "registers calls pipe operator nested external into external" do @@ -6472,12 +6488,15 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls |> sort_calls == %{ - 3 => [ - %CallInfo{arity: 1, position: {3, 21}, func: :func, mod: MyMod}, - %CallInfo{arity: 1, position: {3, 37}, func: :other, mod: Other} - ] - } + assert Enum.any?( + state.calls[3], + &match?(%CallInfo{arity: 1, position: {3, 21}, func: :func, mod: MyMod}, &1) + ) + + assert Enum.any?( + state.calls[3], + &match?(%CallInfo{arity: 1, position: {3, 37}, func: :other, mod: Other}, &1) + ) end test "registers calls pipe operator nested local into external" do @@ -6491,12 +6510,15 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls |> sort_calls == %{ - 3 => [ - %CallInfo{arity: 1, position: {3, 15}, func: :func_1, mod: nil}, - %CallInfo{arity: 1, position: {3, 32}, func: :other, mod: Some} - ] - } + assert Enum.any?( + state.calls[3], + &match?(%CallInfo{arity: 1, position: {3, 15}, func: :func_1, mod: nil}, &1) + ) + + assert Enum.any?( + state.calls[3], + &match?(%CallInfo{arity: 1, position: {3, 32}, func: :other, mod: Some}, &1) + ) end test "registers calls pipe operator nested local into local" do @@ -6510,12 +6532,15 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ - 3 => [ - %CallInfo{arity: 1, position: {3, 15}, func: :func_1, mod: nil}, - %CallInfo{arity: 1, position: {3, 27}, func: :other, mod: nil} - ] - } + assert Enum.any?( + state.calls[3], + &match?(%CallInfo{arity: 1, position: {3, 15}, func: :func_1, mod: nil}, &1) + ) + + assert Enum.any?( + state.calls[3], + &match?(%CallInfo{arity: 1, position: {3, 27}, func: :other, mod: nil}, &1) + ) end test "registers calls capture operator __MODULE__" do @@ -6600,37 +6625,49 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } end - if @macro_calls_support do - test "registers calls on ex_unit DSL" do - state = - """ - defmodule MyModuleTest do - use ExUnit.Case - - describe "describe1" do - test "test1" do - end - end + test "registers calls on ex_unit DSL" do + state = + """ + defmodule MyModuleTest do + use ExUnit.Case - test "test2", %{some: param} do + describe "describe1" do + test "test1" do end + end - test "not implemented" + test "test2", %{some: param} do end - """ - |> string_to_state - assert state.calls == %{ - 2 => [ - %CallInfo{arity: 2, position: {2, 3}, func: :__register__, mod: ExUnit.Case}, - %CallInfo{arity: 2, position: {2, 3}, func: :unless, mod: nil} - ], - 4 => [%CallInfo{arity: 2, position: {4, 3}, func: :describe, mod: nil}], - 5 => [%CallInfo{arity: 2, position: {5, 5}, func: :test, mod: nil}], - 9 => [%CallInfo{arity: 3, position: {9, 3}, func: :test, mod: nil}], - 12 => [%CallInfo{arity: 0, position: {12, 3}, func: :test, mod: nil}] - } - end + test "not implemented" + end + """ + |> string_to_state + + assert Enum.any?( + state.calls[2], + &match?(%CallInfo{arity: 2, position: {2, _}, func: :__register__}, &1) + ) + + assert Enum.any?( + state.calls[4], + &match?(%CallInfo{arity: 2, position: {4, 3}, func: :describe}, &1) + ) + + assert Enum.any?( + state.calls[5], + &match?(%CallInfo{arity: 2, position: {5, 5}, func: :test}, &1) + ) + + assert Enum.any?( + state.calls[9], + &match?(%CallInfo{arity: 3, position: {9, 3}, func: :test}, &1) + ) + + assert Enum.any?( + state.calls[12], + &match?(%CallInfo{arity: 1, position: {12, 3}, func: :test}, &1) + ) end end @@ -6709,14 +6746,14 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert %{ - {Proto, :t, 0} => %ElixirSense.Core.State.TypeInfo{ - args: [[]], - kind: :type, - name: :t, - specs: ["@type t :: term"], - doc: "All the types that implement this protocol" <> _ - } - } = state.types + {Proto, :t, 0} => %ElixirSense.Core.State.TypeInfo{ + args: [[]], + kind: :type, + name: :t, + specs: ["@type t :: term"], + doc: "All the types that implement this protocol" <> _ + } + } = state.types end test "specs and callbacks" do @@ -6863,26 +6900,26 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert %{ - {MyRecords, :user, 1} => %ModFunInfo{ - params: [[{:\\, [], [{:args, [], nil}, []]}]], - positions: [{3, 10}], - type: :defmacro - }, - {MyRecords, :user, 2} => %ModFunInfo{ - params: [[{:record, [], nil}, {:args, [], nil}]], - positions: [{3, 10}], - type: :defmacro - }, - {MyRecords, :userp, 1} => %ModFunInfo{type: :defmacrop}, - {MyRecords, :my_rec, 1} => %ModFunInfo{type: :defmacro} - } = state.mods_funs_to_positions + {MyRecords, :user, 1} => %ModFunInfo{ + params: [[{:\\, [], [{:args, [], nil}, []]}]], + positions: [{3, 10}], + type: :defmacro + }, + {MyRecords, :user, 2} => %ModFunInfo{ + params: [[{:record, [], nil}, {:args, [], nil}]], + positions: [{3, 10}], + type: :defmacro + }, + {MyRecords, :userp, 1} => %ModFunInfo{type: :defmacrop}, + {MyRecords, :my_rec, 1} => %ModFunInfo{type: :defmacro} + } = state.mods_funs_to_positions assert %{ - {MyRecords, :user, 0} => %State.TypeInfo{ - name: :user, - specs: ["@type user :: record(:user, name: String.t(), age: integer)"] - } - } = state.types + {MyRecords, :user, 0} => %State.TypeInfo{ + name: :user, + specs: ["@type user :: record(:user, name: String.t(), age: integer)"] + } + } = state.types end test "defrecord imported defines record macros" do @@ -6897,24 +6934,24 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert %{ - {MyRecords, :user, 1} => %ModFunInfo{ - params: [[{:\\, [], [{:args, [], nil}, []]}]], - positions: [{3, 3}], - type: :defmacro - }, - {MyRecords, :user, 2} => %ModFunInfo{ - params: [[{:record, [], nil}, {:args, [], nil}]], - positions: [{3, 3}], - type: :defmacro - } - } = state.mods_funs_to_positions + {MyRecords, :user, 1} => %ModFunInfo{ + params: [[{:\\, [], [{:args, [], nil}, []]}]], + positions: [{3, 3}], + type: :defmacro + }, + {MyRecords, :user, 2} => %ModFunInfo{ + params: [[{:record, [], nil}, {:args, [], nil}]], + positions: [{3, 3}], + type: :defmacro + } + } = state.mods_funs_to_positions assert %{ - {MyRecords, :user, 0} => %State.TypeInfo{ - name: :user, - specs: ["@type user :: record(:user, name: String.t(), age: integer)"] - } - } = state.types + {MyRecords, :user, 0} => %State.TypeInfo{ + name: :user, + specs: ["@type user :: record(:user, name: String.t(), age: integer)"] + } + } = state.types end end diff --git a/test/elixir_sense/core/parser_test.exs b/test/elixir_sense/core/parser_test.exs index 6fb1e947..bc675374 100644 --- a/test/elixir_sense/core/parser_test.exs +++ b/test/elixir_sense/core/parser_test.exs @@ -254,22 +254,23 @@ defmodule ElixirSense.Core.ParserTest do # assert_received {:result, result} assert (%Metadata{ - error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{ - module: MyModule, - scope_id: scope_id_1 - }, - 3 => %Env{ - module: MyModule, - requires: _, - scope_id: scope_id_2, - vars: [ - %VarInfo{name: :x} - ] - } - } - } when scope_id_2 > scope_id_1) = result + error: {:error, :parse_error}, + lines_to_env: %{ + 1 => %Env{ + module: MyModule, + scope_id: scope_id_1 + }, + 3 => %Env{ + module: MyModule, + requires: _, + scope_id: scope_id_2, + vars: [ + %VarInfo{name: :x} + ] + } + } + } + when scope_id_2 > scope_id_1) = result end test "parse_string with missing terminator \"end\" attempts to insert `end` at correct indentation" do From 8fa028351be688cd2753f3526e93e4e91ace90fc Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 11 May 2024 13:33:19 +0200 Subject: [PATCH 031/235] track calls and vars in typespecs fix protocol callback spec --- lib/elixir_sense/core/compiler.ex | 199 +++++++++++++- lib/elixir_sense/core/state.ex | 15 +- .../core/metadata_builder_test.exs | 249 +++++++++++++++--- 3 files changed, 403 insertions(+), 60 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 2f157917..65d37ffa 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -135,7 +135,7 @@ defmodule ElixirSense.Core.Compiler do # no need to call expand_without_aliases_report - we never report {arg, state, env} = expand(arg, state, env) - {opts, state, env} = expand_opts(meta, :alias, [:as, :warn], no_alias_opts(opts), state, env) + {opts, state, env} = expand_opts([:as, :warn], no_alias_opts(opts), state, env) if is_atom(arg) do # TODO check difference with @@ -167,7 +167,7 @@ defmodule ElixirSense.Core.Compiler do {arg, state, env} = expand(arg, state, env) {opts, state, env} = - expand_opts(meta, :require, [:as, :warn], no_alias_opts(opts), state, env) + expand_opts([:as, :warn], no_alias_opts(opts), state, env) case Keyword.fetch(meta, :defined) do {:ok, mod} when is_atom(mod) -> @@ -227,7 +227,7 @@ defmodule ElixirSense.Core.Compiler do # no need to call expand_without_aliases_report - we never report {arg, state, env} = expand(arg, state, env) - {opts, state, env} = expand_opts(meta, :import, [:only, :except, :warn], opts, state, env) + {opts, state, env} = expand_opts([:only, :except, :warn], opts, state, env) if is_atom(arg) do # TODO check difference @@ -336,7 +336,7 @@ defmodule ElixirSense.Core.Compiler do end valid_opts = [:context, :location, :line, :file, :unquote, :bind_quoted, :generated] - {e_opts, st, et} = expand_opts(meta, :quote, valid_opts, opts, s, e) + {e_opts, st, et} = expand_opts(valid_opts, opts, s, e) context = Keyword.get(e_opts, :context, e.module || :"Elixir") @@ -1039,13 +1039,16 @@ defmodule ElixirSense.Core.Compiler do assert_module_scope(env, :@, 1) unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") - {expr, state, env} = __MODULE__.Typespec.expand(expr, state, env) + {expr, state, env} = __MODULE__.Typespec.expand_type(expr, state, env) case __MODULE__.Typespec.type_to_signature(expr) do {name, [_type_arg]} when name in [:required, :optional] -> raise "type #{name}/#{1} is a reserved type and it cannot be defined" {name, type_args} -> + if __MODULE__.Typespec.built_in_type?(name, length(type_args)) do + raise"type #{name}/#{length(type_args)} is a built-in type and it cannot be redefined" + end # TODO elixir does Macro.escape with unquote: true spec = TypeInfo.typespec_to_string(kind, expr) @@ -1079,7 +1082,7 @@ defmodule ElixirSense.Core.Compiler do assert_module_scope(env, :@, 1) unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") - {expr, state, env} = __MODULE__.Typespec.expand(expr, state, env) + {expr, state, env} = __MODULE__.Typespec.expand_spec(expr, state, env) case __MODULE__.Typespec.spec_to_signature(expr) do {name, type_args} -> @@ -1894,7 +1897,7 @@ defmodule ElixirSense.Core.Compiler do {{fun, meta, args}, state, env} end - defp expand_opts(meta, kind, allowed, opts, s, e) do + defp expand_opts(allowed, opts, s, e) do {e_opts, se, ee} = expand(opts, s, e) e_opts = sanitize_opts(allowed, e_opts) {e_opts, se, ee} @@ -4694,15 +4697,139 @@ defmodule ElixirSense.Core.Compiler do when is_atom(name) and name != :"::" and is_atom(context), do: {name, []} - def type_to_signature({:"::", _, [{name, _, args}, _]}) when is_atom(name) and name != :"::", + def type_to_signature({:"::", _, [{name, _, args}, _]}) + when is_atom(name) and name != :"::", do: {name, args} def type_to_signature(_), do: :error - def expand(ast, state, env) do + def expand_spec(ast, state, env) do + # TODO not sure this is correct. Are module vars accessible? + state = state + |> new_func_vars_scope + + {ast, state, env} = do_expand_spec(ast, state, env) + + state = state + |> remove_func_vars_scope + + {ast, state, env} + end + + defp do_expand_spec({:when, meta, [spec, guard]}, state, env) when is_list(guard) do + {spec, guard, state, env} = do_expand_spec(spec, guard, meta, state, env) + {{:when, meta, [spec, guard]}, state, env} + end + defp do_expand_spec(spec, state, env) do + {spec, guard, state, env} = do_expand_spec(spec, [], [], state, env) + {spec, state, env} + end + + defp do_expand_spec({:"::", meta, [{name, name_meta, args}, return]}, guard, guard_meta, state, env) + when is_atom(name) and name != :"::" do + args = if is_atom(args) do + [] + else + args + end + |> sanitize_args() + + guard = if Keyword.keyword?(guard), do: guard, else: [] + + state = Enum.reduce(guard, state, fn {name, val}, state -> + # guard is a keyword list so we don't have exact meta on keys + add_var_write(state, {name, guard_meta, nil}) + end) + + {args_reverse, state, env} = Enum.reduce(args, {[], state, env}, fn arg, {acc, state, env} -> + {arg, state, env} = expand_typespec(arg, state, env) + {[arg | acc], state, env} + end) + args = Enum.reverse(args_reverse) + + {return, state, env} = expand_typespec(return, state, env) + + {guard_reverse, state, env} = Enum.reduce(guard, {[], state, env}, fn + {_name, {:var, _, context}} = pair, {acc, state, env} when is_atom(context) -> + # special type var + {[pair | acc], state, env} + {name, type}, {acc, state, env} -> + {type, state, env} = expand_typespec(type, state, env) + {[{name, type} | acc], state, env} + end) + guard = Enum.reverse(guard_reverse) + + {{:"::", meta, [{name, name_meta, args}, return]}, guard, state, env} + end + + defp do_expand_spec(other, guard, _guard_meta, state, env) do + # invalid or incomplete spec + {other, guard, state, env} + end + + defp sanitize_args(args) do + Enum.map(args, fn + {:"::", meta, [left, right]} -> + {:"::", meta, [remove_default(left), remove_default(right)]} + + other -> + remove_default(other) + end) + end + + defp remove_default({:\\, _, [left, _]}), do: left + defp remove_default(other), do: other + + + def expand_type(ast, state, env) do + state = state + |> new_func_vars_scope + + {ast, state, env} = do_expand_type(ast, state, env) + + state = state + |> remove_func_vars_scope + + {ast, state, env} + end + + defp do_expand_type({:"::", meta, [{name, name_meta, args}, definition]}, state, env) do + args = if is_atom(args) do + [] + else + args + end + + state = Enum.reduce(args, state, fn + {name, meta, context}, state when is_atom(name) and is_atom(context) and name != :_ -> + add_var_write(state, {name, meta, context}) + _, state -> + # silently skip invalid typespec params + state + end) + + {definition, state, env} = expand_typespec(definition, state, env) + {{:"::", meta, [{name, name_meta, args}, definition]}, state, env} + end + + defp do_expand_type(other, state, env) do + {other, state, env} + end + + @special_forms [ + :|, :<<>>, :%{}, :%, :.., :->, :"::", :+, :-, :., :{}, :__block__, :... + ] + + defp expand_typespec(ast, state, env) do # TODO this should handle remote calls, attributes unquotes? + # TODO attribute remote call should expand attribute + # {{:., meta, [{:@, _, [{attr, _, _}]}, name]}, _, args} + # TODO remote call should expand remote + # {{:., meta, [remote, name]}, _, args} + # TODO expand struct module + # {:%, _, [name, {:%{}, meta, fields}]} {ast, {state, env}} = - Macro.prewalk(ast, {state, env}, fn + Macro.traverse(ast, {state, env}, fn {:__aliases__, _meta, list} = node, {state, env} when is_list(list) -> {node, state, env} = ElixirExpand.expand(node, state, env) {node, {state, env}} @@ -4710,12 +4837,64 @@ defmodule ElixirSense.Core.Compiler do {:__MODULE__, _meta, ctx} = node, {state, env} when is_atom(ctx) -> {node, state, env} = ElixirExpand.expand(node, state, env) {node, {state, env}} + + {:"::", meta, [{var_name, var_meta, context}, expr]}, {state, env} when is_atom(var_name) and is_atom(context) -> + # mark as annotation + {{:"::", meta, [{var_name, [{:annotation, true} | var_meta], context}, expr]}, {state, env}} + + {name, meta, args}, {state, env} when is_atom(name) and is_atom(args) and name not in @special_forms and hd(meta) != {:annotation, true} -> + [vars_from_scope | _other_vars] = state.vars_info + ast = case Elixir.Map.get(vars_from_scope, {name, nil}) do + nil -> + # add parens to no parens local call + {name, meta, []} + _ -> + {name, meta, args} + end + + {ast, {state, env}} + + other, acc -> + {other, acc} + end, fn + {{:., dot_meta, [remote, name]}, meta, args}, {state, env} when is_atom(remote) -> + line = Keyword.get(meta, :line, 0) + column = Keyword.get(meta, :column, nil) + args = if is_atom(args) do + [] + else + args + end + + state = add_call_to_line(state, {remote, name, length(args)}, {line, column}) + + {{{:., dot_meta, [remote, name]}, meta, args}, {state, env}} + + {name, meta, args}, {state, env} when is_atom(name) and is_list(args) and name not in @special_forms -> + line = Keyword.get(meta, :line, 0) + column = Keyword.get(meta, :column, nil) + + state = add_call_to_line(state, {nil, name, length(args)}, {line, column}) + {{name, meta, args}, {state, env}} + {name, meta, context} = var, {state, env} when is_atom(name) and is_atom(context) and hd(meta) != {:annotation, true} -> + state = add_var_read(state, var) + {var, {state, env}} other, acc -> {other, acc} end) {ast, state, env} end + # TODO: Remove char_list type by v2.0 + def built_in_type?(:char_list, 0), do: true + def built_in_type?(:charlist, 0), do: true + def built_in_type?(:as_boolean, 1), do: true + def built_in_type?(:struct, 0), do: true + def built_in_type?(:nonempty_charlist, 0), do: true + def built_in_type?(:keyword, 0), do: true + def built_in_type?(:keyword, 1), do: true + def built_in_type?(:var, 0), do: true + def built_in_type?(name, arity), do: :erl_internal.is_type(name, arity) end end diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 9a23132d..bc9cb1ea 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -1100,7 +1100,7 @@ defmodule ElixirSense.Core.State do %__MODULE__{state | specs: specs} end - def add_var_write(%__MODULE__{} = state, {name, meta, nil}) when name != :_ do + def add_var_write(%__MODULE__{} = state, {name, meta, _}) when name != :_ do version = meta[:version] line = meta[:line] column = meta[:column] @@ -1124,7 +1124,7 @@ defmodule ElixirSense.Core.State do def add_var_write(%__MODULE__{} = state, _), do: state - def add_var_read(%__MODULE__{} = state, {name, meta, nil}) when name != :_ do + def add_var_read(%__MODULE__{} = state, {name, meta, _}) when name != :_ do version = meta[:version] line = meta[:line] column = meta[:column] @@ -1778,16 +1778,17 @@ defmodule ElixirSense.Core.State do %ModFunInfo{positions: positions, params: params} = state.mods_funs_to_positions[key] - args = - for param_variant <- params do - param_variant - |> Enum.map(&Macro.to_string/1) + args = for param_variant <- params do + case tl(param_variant) do + [] -> ["t()"] + other -> ["t()" | Enum.map(other, fn _ -> "term()" end)] end + end specs = for arg <- args do joined = Enum.join(arg, ", ") - "@callback #{name}(#{joined}) :: term" + "@callback #{name}(#{joined}) :: term()" end %SpecInfo{ diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 36a803f1..09d56d3b 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -9,7 +9,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do @attribute_binding_support true or Version.match?(System.version(), "< 1.17.0-dev") @expand_eval false @binding_support Version.match?(System.version(), "< 1.17.0-dev") - @typespec_calls_support Version.match?(System.version(), "< 1.17.0-dev") + @typespec_calls_support true or Version.match?(System.version(), "< 1.17.0-dev") @compiler Code.ensure_loaded?(ElixirSense.Core.Compiler) describe "versioned_vars" do @@ -1212,6 +1212,63 @@ defmodule ElixirSense.Core.MetadataBuilderTest do end end + describe "typespec vars" do + test "registers type parameters" do + state = + """ + defmodule A do + @type some(p) :: {p, list(p), integer} + end + """ + |> string_to_state + + assert [ + %VarInfo{name: :p, positions: [{2, 14}, {2, 21}, {2, 29}]} + ] = state.vars_info_per_scope_id[2] + end + + test "registers spec parameters" do + state = + """ + defmodule A do + @callback some(p) :: {p, list(p), integer, q} when p: integer, q: {p} + end + """ + |> string_to_state + + # no position in guard, elixir parses guards as keyword list so p is an atom with no metadata + # we use when meta instead so the position is not exact... + assert [ + %VarInfo{name: :p, positions: [{2, 49}, {2, 18}, {2, 25}, {2, 33}, {2, 70}]}, + %VarInfo{name: :q, positions: [{2, 49}, {2, 46}]} + ] = state.vars_info_per_scope_id[2] + end + + test "does not register annotated spec params as type variables" do + state = + """ + defmodule A do + @callback some(p :: integer) :: integer + end + """ + |> string_to_state + + assert [] == state.vars_info_per_scope_id[2] + end + + test "does not register annotated type elements as variables" do + state = + """ + defmodule A do + @type color :: {red :: integer, green :: integer, blue :: integer} + end + """ + |> string_to_state + + assert [] == state.vars_info_per_scope_id[2] + end + end + @tag requires_source: true test "build metadata from kernel.ex" do assert get_subject_definition_line(Kernel, :defmodule, 2) =~ @@ -4806,44 +4863,45 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert %{ {Proto, :with_spec, 2} => %ElixirSense.Core.State.SpecInfo{ - args: [["t", "boolean"], ["t", "integer"]], + args: [["t()", "boolean()"], ["t()", "integer()"]], kind: :callback, name: :with_spec, positions: [{3, 3}, {2, 3}], end_positions: [{3, 40}, {2, 42}], generated: [false, false], specs: [ - "@callback with_spec(t, boolean) :: number", - "@callback with_spec(t, integer) :: String.t()", - "@spec with_spec(t, boolean) :: number", - "@spec with_spec(t, integer) :: String.t()" + "@callback with_spec(t(), boolean()) :: number()", + "@callback with_spec(t(), integer()) :: String.t()", + "@spec with_spec(t(), boolean()) :: number()", + "@spec with_spec(t(), integer()) :: String.t()" ] }, {Proto, :without_spec, 2} => %ElixirSense.Core.State.SpecInfo{ - args: [["t", "integer"]], + args: [["t()", "term()"]], kind: :callback, name: :without_spec, positions: [{6, 3}], end_positions: [nil], generated: [true], - specs: ["@callback without_spec(t, integer) :: term"] + specs: ["@callback without_spec(t(), term()) :: term()"] }, + # TODO there is raw unquote in spec {Proto, :__protocol__, 1} => %ElixirSense.Core.State.SpecInfo{ kind: :spec, specs: [ - "@spec __protocol__(:impls) :: :not_consolidated | {:consolidated, [module]}", - "@spec __protocol__(:consolidated?) :: boolean", - "@spec __protocol__(:functions) :: unquote(Protocol.__functions_spec__(@__functions__))", + "@spec __protocol__(:impls) :: :not_consolidated | {:consolidated, [module()]}", + "@spec __protocol__(:consolidated?) :: boolean()", + "@spec __protocol__(:functions) :: unquote(Protocol.__functions_spec__(@__functions__()))", "@spec __protocol__(:module) :: Proto" ] }, {Proto, :impl_for, 1} => %ElixirSense.Core.State.SpecInfo{ kind: :spec, - specs: ["@spec impl_for(term) :: atom | nil"] + specs: ["@spec impl_for(term()) :: atom() | nil"] }, {Proto, :impl_for!, 1} => %ElixirSense.Core.State.SpecInfo{ kind: :spec, - specs: ["@spec impl_for!(term) :: atom"] + specs: ["@spec impl_for!(term()) :: atom()"] } } = state.specs end @@ -5668,21 +5726,21 @@ defmodule ElixirSense.Core.MetadataBuilderTest do kind: :opaque, name: :my_opaque_type, # positions: [{2, 3}], - specs: ["@opaque my_opaque_type :: any"] + specs: ["@opaque my_opaque_type() :: any()"] }, {InheritMod, :my_priv_type, 0} => %State.TypeInfo{ args: [[]], kind: :typep, name: :my_priv_type, # positions: [{2, 3}], - specs: ["@typep my_priv_type :: any"] + specs: ["@typep my_priv_type() :: any()"] }, {InheritMod, :my_pub_type, 0} => %State.TypeInfo{ args: [[]], kind: :type, name: :my_pub_type, # positions: [{2, 3}], - specs: ["@type my_pub_type :: any"] + specs: ["@type my_pub_type() :: any()"] }, {InheritMod, :my_pub_type_arg, 2} => %State.TypeInfo{ args: [["a", "b"]], @@ -5706,7 +5764,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do kind: :callback, name: :some_callback, # positions: [{2, 3}], - specs: ["@callback some_callback(abc) :: :ok when abc: integer"] + specs: ["@callback some_callback(abc) :: :ok when abc: integer()"] } } = state.specs end @@ -6168,23 +6226,116 @@ defmodule ElixirSense.Core.MetadataBuilderTest do end end - if @typespec_calls_support do - test "registers typespec no parens calls" do - state = - """ - defmodule NyModule do - @type a :: integer - end - """ - |> string_to_state + test "registers typespec no parens calls" do + state = + """ + defmodule NyModule do + @type a :: integer + end + """ + |> string_to_state - assert state.calls == %{ - 2 => [ - %CallInfo{arity: 0, func: :integer, position: {2, 14}, mod: nil}, - %CallInfo{arity: 0, func: :a, position: {2, 9}, mod: nil} - ] - } - end + assert state.calls == %{ + 2 => [ + %CallInfo{arity: 0, func: :integer, position: {2, 14}, mod: nil} + ] + } + end + + test "registers typespec parens calls" do + state = + """ + defmodule NyModule do + @type a() :: integer() + end + """ + |> string_to_state + + assert state.calls == %{ + 2 => [ + %CallInfo{arity: 0, func: :integer, position: {2, 16}, mod: nil} + ] + } + end + + test "registers typespec no parens remote calls" do + state = + """ + defmodule NyModule do + @type a :: Enum.t + end + """ + |> string_to_state + + assert state.calls == %{ + 2 => [ + %CallInfo{arity: 0, func: :t, position: {2, 19}, mod: Enum} + ] + } + end + + test "registers typespec parens remote calls" do + state = + """ + defmodule NyModule do + @type a() :: Enum.t() + @type a(x) :: {Enum.t(), x} + end + """ + |> string_to_state + + assert state.calls == %{ + 2 => [ + %CallInfo{arity: 0, func: :t, position: {2, 21}, mod: Enum} + ], + 3 => [ + %CallInfo{arity: 0, func: :t, position: {3, 23}, mod: Enum} + ] + } + end + + test "registers typespec calls in specs with when guard" do + state = + """ + defmodule NyModule do + @callback a(b, c, d) :: {b, integer(), c} when b: map(), c: var, d: pos_integer + end + """ + |> string_to_state + + # NOTE var is not a type but a special variable + assert state.calls == %{ + 2 => [ + %CallInfo{arity: 0, func: :pos_integer, position: {2, 71}, mod: nil}, + %CallInfo{arity: 0, func: :map, position: {2, 53}, mod: nil}, + %CallInfo{arity: 0, func: :integer, position: {2, 31}, mod: nil} + ] + } + end + + test "registers typespec calls in typespec with named args" do + state = + """ + defmodule NyModule do + @callback days_since_epoch(year :: integer, month :: integer, day :: integer) :: integer + @type color :: {red :: integer, green :: integer, blue :: integer} + end + """ + |> string_to_state + + assert state.calls == %{ + 2 => [ + %CallInfo{arity: 0, func: :integer, position: {2, 84}, mod: nil}, + %CallInfo{arity: 0, func: :integer, position: {2, 72}, mod: nil}, + %CallInfo{arity: 0, func: :integer, position: {2, 56}, mod: nil}, + %CallInfo{arity: 0, func: :integer, position: {2, 38}, mod: nil}, + ], + 3 => [ + %CallInfo{arity: 0, func: :integer, position: {3, 61}, mod: nil}, + %CallInfo{arity: 0, func: :integer, position: {3, 44}, mod: nil}, + %CallInfo{arity: 0, func: :integer, position: {3, 26}, mod: nil}, + ] + } end test "registers calls local no arg" do @@ -6694,7 +6845,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do positions: [{2, 3}], end_positions: [{2, 36}], generated: [false], - specs: ["@type no_arg_no_parens :: integer"] + specs: ["@type no_arg_no_parens() :: integer()"] }, {My, :no_args, 0} => %ElixirSense.Core.State.TypeInfo{ args: [[]], @@ -6703,7 +6854,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do positions: [{3, 3}], end_positions: [{3, 30}], generated: [false], - specs: ["@typep no_args() :: integer"] + specs: ["@typep no_args() :: integer()"] }, {My, :overloaded, 0} => %ElixirSense.Core.State.TypeInfo{ args: [[]], @@ -6712,7 +6863,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do positions: [{5, 3}], end_positions: [{5, 25}], generated: [false], - specs: ["@type overloaded :: {}"] + specs: ["@type overloaded() :: {}"] }, {My, :overloaded, 1} => %ElixirSense.Core.State.TypeInfo{ kind: :type, @@ -6750,7 +6901,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do args: [[]], kind: :type, name: :t, - specs: ["@type t :: term"], + specs: ["@type t() :: term()"], doc: "All the types that implement this protocol" <> _ } } = state.types @@ -6780,16 +6931,16 @@ defmodule ElixirSense.Core.MetadataBuilderTest do positions: [{3, 3}, {2, 3}], end_positions: [{3, 25}, {2, 30}], generated: [false, false], - specs: ["@spec abc :: reference", "@spec abc :: atom | integer"] + specs: ["@spec abc() :: reference()", "@spec abc() :: atom() | integer()"] }, {Proto, :my, 1} => %ElixirSense.Core.State.SpecInfo{ kind: :callback, name: :my, - args: [["a :: integer"]], + args: [["a :: integer()"]], positions: [{4, 3}], end_positions: [{4, 37}], generated: [false], - specs: ["@callback my(a :: integer) :: atom"] + specs: ["@callback my(a :: integer()) :: atom()"] }, {Proto, :other, 1} => %ElixirSense.Core.State.SpecInfo{ kind: :macrocallback, @@ -6798,7 +6949,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do positions: [{5, 3}], end_positions: [_], generated: [false], - specs: ["@macrocallback other(x) :: Macro.t() when x: integer"] + specs: ["@macrocallback other(x) :: Macro.t() when x: integer()"] } } = state.specs else @@ -6917,7 +7068,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert %{ {MyRecords, :user, 0} => %State.TypeInfo{ name: :user, - specs: ["@type user :: record(:user, name: String.t(), age: integer)"] + specs: ["@type user() :: record(:user, name: String.t(), age: integer())"] } } = state.types end @@ -6949,7 +7100,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert %{ {MyRecords, :user, 0} => %State.TypeInfo{ name: :user, - specs: ["@type user :: record(:user, name: String.t(), age: integer)"] + specs: ["@type user() :: record(:user, name: String.t(), age: integer())"] } } = state.types end @@ -7692,6 +7843,18 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> MetadataBuilder.build() end + defp get_scope_vars(state, line) do + case state.lines_to_env[line] do + nil -> + [] + + env -> + dbg(state) + state.vars_info_per_scope_id[env.scope_id] + end + |> Enum.sort() + end + defp get_line_vars(state, line) do case state.lines_to_env[line] do nil -> From fe120d7106b95d95d85a7e20a107b082e8063f76 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 12 May 2024 11:02:52 +0200 Subject: [PATCH 032/235] fix capture call registration on 0 arity --- lib/elixir_sense/core/compiler.ex | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 65d37ffa..8247cc2b 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -3649,14 +3649,14 @@ defmodule ElixirSense.Core.Compiler do def capture(meta, {:/, _, [{{:., _, [_m, f]} = dot, require_meta, []}, a]}, s, e) when is_atom(f) and is_integer(a) do - args = args_from_arity(meta, a, e) + args = args_from_arity(meta, a) # handle_capture_possible_warning(meta, require_meta, m, f, a, e) capture_require({dot, require_meta, args}, s, e, true) end def capture(meta, {:/, _, [{f, import_meta, c}, a]}, s, e) when is_atom(f) and is_integer(a) and is_atom(c) do - args = args_from_arity(meta, a, e) + args = args_from_arity(meta, a) capture_import({f, import_meta, args}, s, e, true) end @@ -3812,12 +3812,17 @@ defmodule ElixirSense.Core.Compiler do {other, dict} end - defp args_from_arity(_meta, a, _e) when is_integer(a) and a >= 0 and a <= 255 do - Enum.map(1..a, fn x -> {:&, [], [x]} end) + defp args_from_arity(_meta, 0), do: [] + + defp args_from_arity(meta, a) when is_integer(a) and a >= 1 and a <= 255 do + for x <- 1..a do + {:&, meta, [x]} + end end - defp args_from_arity(_meta, _a, _e) do - raise "invalid_arity_for_capture" + defp args_from_arity(_meta, _a) do + # elixir raises invalid_arity_for_capture + [] end defp is_sequential_and_not_empty([]), do: false From 2b059d4993b2f2b76616ae39feb3f809a6869ad2 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 12 May 2024 13:35:26 +0200 Subject: [PATCH 033/235] register super calls --- lib/elixir_sense/core/compiler.ex | 25 +++++- .../core/metadata_builder_test.exs | 84 ++++++++++++++++++- 2 files changed, 107 insertions(+), 2 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 8247cc2b..d84a971f 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -233,6 +233,7 @@ defmodule ElixirSense.Core.Compiler do # TODO check difference # elixir_aliases:ensure_loaded(Meta, ERef, ET) # elixir_import:import(Meta, ERef, EOpts, ET, true, true) + # TODO this does not work for context modules with true <- Code.ensure_loaded?(arg), {:ok, env} <- Macro.Env.define_import(env, meta, arg, [trace: false] ++ opts) do {arg, state, env} @@ -399,6 +400,15 @@ defmodule ElixirSense.Core.Compiler do case resolve_super(meta, arity, s, e) do {kind, name, _} when kind in [:def, :defp] -> + + line = Keyword.get(super_meta, :line, 0) + column = Keyword.get(super_meta, :column, nil) + + s = + s + |> add_call_to_line({nil, name, arity}, {line, column}) + |> add_current_env_to_line(line, e) + {{:&, meta, [{:/, arity_meta, [{name, super_meta, context}, arity]}]}, s, e} _ -> @@ -453,8 +463,18 @@ defmodule ElixirSense.Core.Compiler do defp do_expand({:super, meta, args}, s, e) when is_list(args) do assert_no_match_or_guard_scope(e.context, "super") - {kind, name, _} = resolve_super(meta, length(args), s, e) + arity = length(args) + {kind, name, _} = resolve_super(meta, arity, s, e) {e_args, sa, ea} = expand_args(args, s, e) + + line = Keyword.get(meta, :line, 0) + column = Keyword.get(meta, :column, nil) + + sa = + sa + |> add_call_to_line({nil, name, arity}, {line, column}) + |> add_current_env_to_line(line, ea) + {{:super, [{:super, {kind, name}} | meta], e_args}, sa, ea} end @@ -4769,6 +4789,7 @@ defmodule ElixirSense.Core.Compiler do defp do_expand_spec(other, guard, _guard_meta, state, env) do # invalid or incomplete spec + # TODO try to wrap in :: expression {other, guard, state, env} end @@ -4818,6 +4839,8 @@ defmodule ElixirSense.Core.Compiler do end defp do_expand_type(other, state, env) do + # invalid or incomplete spec + # TODO try to wrap in :: expression {other, state, env} end diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 09d56d3b..9a3d3682 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -6694,6 +6694,54 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ) end + test "registers super call" do + state = + """ + defmodule My do + use ElixirSenseExample.OverridableFunctions + + def test(a, b) do + super(a, b) + end + end + """ + |> string_to_state + + assert state.calls[5] == [%CallInfo{arity: 2, position: {5, 5}, func: :test, mod: nil}] + end + + test "registers super capture expression" do + state = + """ + defmodule My do + use ElixirSenseExample.OverridableFunctions + + def test(a, b) do + a |> Enum.map(&super(&1, b)) + end + end + """ + |> string_to_state + + assert [_, %CallInfo{arity: 2, position: {5, 20}, func: :test, mod: nil}, _] = state.calls[5] + end + + test "registers super capture" do + state = + """ + defmodule My do + use ElixirSenseExample.OverridableFunctions + + def test(a, b) do + a |> Enum.map_reduce([], &super/2) + end + end + """ + |> string_to_state + + assert [_, %CallInfo{arity: 2, position: {5, 31}, func: :test, mod: nil}, _] = state.calls[5] + end + test "registers calls capture operator __MODULE__" do state = """ @@ -6728,6 +6776,22 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } end + test "registers calls capture expression external" do + state = + """ + defmodule NyModule do + def func do + &MyMod.func(1, &1) + end + end + """ + |> string_to_state + + assert state.calls == %{ + 3 => [%CallInfo{arity: 2, position: {3, 12}, func: :func, mod: MyMod}] + } + end + test "registers calls capture operator external erlang module" do state = """ @@ -6766,13 +6830,31 @@ defmodule ElixirSense.Core.MetadataBuilderTest do defmodule NyModule do def func do &func/1 + &func/0 + end + end + """ + |> string_to_state + + assert state.calls == %{ + 3 => [%CallInfo{arity: 1, func: :func, position: {3, 6}, mod: nil}], + 4 => [%CallInfo{arity: 0, func: :func, position: {4, 6}, mod: nil}] + } + end + + test "registers calls capture expression local" do + state = + """ + defmodule NyModule do + def func do + &func(1, &1) end end """ |> string_to_state assert state.calls == %{ - 3 => [%CallInfo{arity: 1, func: :func, position: {3, 6}, mod: nil}] + 3 => [%CallInfo{arity: 2, func: :func, position: {3, 6}, mod: nil}] } end From 7fd437039f52c3ed59b6cbf1c848c8f21ad27249 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 12 May 2024 21:48:48 +0200 Subject: [PATCH 034/235] use the same cursor as Code.Fragment does --- lib/elixir_sense/core/parser.ex | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/elixir_sense/core/parser.ex b/lib/elixir_sense/core/parser.ex index 54724bcb..87b39da9 100644 --- a/lib/elixir_sense/core/parser.ex +++ b/lib/elixir_sense/core/parser.ex @@ -218,7 +218,7 @@ defmodule ElixirSense.Core.Parser do |> List.update_at(line_number - 1, fn line -> # try to replace token do with do: marker line - |> String.replace("do", "do: " <> marker(line_number), global: false) + |> String.replace("do", "do: " <> marker(), global: false) end) |> Enum.join("\n") end @@ -325,7 +325,7 @@ defmodule ElixirSense.Core.Parser do |> List.update_at(line_number - 1, fn line -> # try to prepend unexpected terminator with marker line - |> String.replace(terminator, marker(line_number) <> " " <> terminator, global: false) + |> String.replace(terminator, marker() <> " " <> terminator, global: false) end) |> Enum.join("\n") end @@ -448,7 +448,7 @@ defmodule ElixirSense.Core.Parser do replaced_line = case terminator do - "end" -> previous <> "; " <> marker(length(rest)) <> "; end" + "end" -> previous <> "; " <> marker() <> "; end" _ -> previous <> " " <> terminator end @@ -515,7 +515,7 @@ defmodule ElixirSense.Core.Parser do |> Source.split_lines() # by replacing a line here we risk introducing a syntax error # instead we append marker to the existing line - |> List.update_at(line_number - 1, &(&1 <> "; " <> marker(line_number))) + |> List.update_at(line_number - 1, &(&1 <> "; " <> marker())) |> Enum.join("\n") end @@ -523,7 +523,7 @@ defmodule ElixirSense.Core.Parser do # IO.puts :stderr, "REPLACING LINE: #{line}" source |> Source.split_lines() - |> List.replace_at(line_number - 1, marker(line_number)) + |> List.replace_at(line_number - 1, marker()) |> Enum.join("\n") end @@ -535,7 +535,7 @@ defmodule ElixirSense.Core.Parser do |> Enum.join("\n") end - defp marker(line_number), do: "(__atom_elixir_marker_#{line_number}__())" + defp marker(), do: "(__cursor__())" defp get_line_from_meta(meta) when is_integer(meta), do: meta defp get_line_from_meta(meta), do: Keyword.fetch!(meta, :line) From 167fc2e64abf485631f4fdfe3cc2fe4a2f679f47 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 12 May 2024 21:49:22 +0200 Subject: [PATCH 035/235] return only visible versions of vars --- lib/elixir_sense/core/state.ex | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index bc9cb1ea..1b5e57c7 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -331,17 +331,14 @@ defmodule ElixirSense.Core.State do # Macro.Env versioned_vars is not updated # versioned_vars: macro_env.versioned_vars, - versioned_vars = - state.vars - |> elem(0) - |> Map.new() + {versioned_vars, _} = state.vars # vars_info has both read and write vars # filter to return only read - vars = - hd(state.vars_info) - |> Map.values() - |> Enum.filter(&Map.has_key?(elem(state.vars, 0), {&1.name, nil})) + [current_vars_info | _] = state.vars_info + vars = for {{name, context}, version} <- versioned_vars, context == nil do + Map.fetch!(current_vars_info, {name, version}) + end current_protocol = case state.protocol do From 52009d86f7d8913a23cded981f2c63ceebeababe Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 12 May 2024 21:49:41 +0200 Subject: [PATCH 036/235] improve var selection --- lib/elixir_sense/core/metadata.ex | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/elixir_sense/core/metadata.ex b/lib/elixir_sense/core/metadata.ex index 79bf62b8..511ad549 100644 --- a/lib/elixir_sense/core/metadata.ex +++ b/lib/elixir_sense/core/metadata.ex @@ -199,7 +199,11 @@ defmodule ElixirSense.Core.Metadata do predicate.(var) end) - %{env | vars: env.vars ++ scope_vars_missing_in_env} + env_vars = for var <- env.vars do + scope_vars |> Enum.find(& &1.name == var.name && &1.scope_id == var.scope_id) + end + + %{env | vars: env_vars ++ scope_vars_missing_in_env} end @spec at_module_body?(State.Env.t()) :: boolean() From f78737a249c90ccc551782f37b2a7780d6ee454d Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 12 May 2024 21:50:30 +0200 Subject: [PATCH 037/235] store cursor env --- lib/elixir_sense/core/compiler.ex | 16 +++++++++++++--- lib/elixir_sense/core/state.ex | 11 +++++++++-- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index d84a971f..58e498ac 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -459,6 +459,14 @@ defmodule ElixirSense.Core.Compiler do __MODULE__.Clauses.with(meta, args, s, e) end + # Cursor + + defp do_expand({:__cursor__, meta, []}, s, e) do + s = s + |> add_cursor_env(meta, e) + {{:__cursor__, meta, []}, s, e} + end + # Super defp do_expand({:super, meta, args}, s, e) when is_list(args) do @@ -1593,7 +1601,7 @@ defmodule ElixirSense.Core.Compiler do {{call, expr}, _} = Code.eval_quoted({call, expr}, [], env) {call, expr} rescue - _ -> raise "unable to eval" + _ -> raise "unable to eval #{inspect({call, expr})}" end else {call, expr} @@ -2639,7 +2647,8 @@ defmodule ElixirSense.Core.Compiler do vars: current, calls: after_s.calls, lines_to_env: after_s.lines_to_env, - vars_info: after_s.vars_info + vars_info: after_s.vars_info, + cursor_env: after_s.cursor_env } call_e = Map.put(e, :context, :match) @@ -2652,7 +2661,8 @@ defmodule ElixirSense.Core.Compiler do vars: new_current, calls: s_expr.calls, lines_to_env: s_expr.lines_to_env, - vars_info: s_expr.vars_info + vars_info: s_expr.vars_info, + cursor_env: s_expr.cursor_env } end_e = Map.put(ee, :context, Map.get(e, :context)) diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 1b5e57c7..a8cf359d 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -64,7 +64,8 @@ defmodule ElixirSense.Core.State do binding_context: list, macro_env: list(Macro.Env.t()), typespec: nil | {atom, arity}, - protocol: nil | {atom, [atom]} + protocol: nil | {atom, [atom]}, + cursor_env: nil | {keyword, ElixirSense.Core.State.Env.t()} } @auto_imported_functions :elixir_env.new().functions @@ -112,7 +113,8 @@ defmodule ElixirSense.Core.State do moduledoc_positions: %{}, macro_env: [:elixir_env.new()], typespec: nil, - protocol: nil + protocol: nil, + cursor_env: nil defmodule Env do @moduledoc """ @@ -373,6 +375,11 @@ defmodule ElixirSense.Core.State do state.module |> hd end + def add_cursor_env(%__MODULE__{} = state, meta, macro_env) do + env = get_current_env(state, macro_env) + %__MODULE__{state | cursor_env: {meta, env}} + end + def add_current_env_to_line(%__MODULE__{} = state, line, macro_env) when is_integer(line) do env = get_current_env(state, macro_env) %__MODULE__{state | lines_to_env: Map.put(state.lines_to_env, line, env)} From 1086eb4d27f067ad4b38a839414f47d3404dbc01 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 12 May 2024 21:51:09 +0200 Subject: [PATCH 038/235] error recovery on case, cond, try, receive --- lib/elixir_sense/core/compiler.ex | 161 ++++---- .../metadata_builder/error_recovery_test.exs | 365 ++++++++++++++++++ 2 files changed, 436 insertions(+), 90 deletions(-) create mode 100644 test/elixir_sense/core/metadata_builder/error_recovery_test.exs diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 58e498ac..046d3445 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -2680,8 +2680,10 @@ defmodule ElixirSense.Core.Compiler do {{:->, meta, [e_left, e_right]}, sr, er} end - def clause(_meta, _kind, _fun, _, _, _e) do - raise ArgumentError, "bad_or_missing_clauses" + def clause(meta, kind, fun, expr, s, e) do + # try to recover from error by wrapping the expression in clause + # elixir raises here bad_or_missing_clauses + clause(meta, kind, fun, {:->, meta, [[expr], :ok]}, s, e) end def head([{:when, meta, [_ | _] = all}], s, e) do @@ -2734,10 +2736,7 @@ defmodule ElixirSense.Core.Compiler do end def case(meta, opts, s, e) do - :ok = - assert_at_most_once(:do, opts, 0, fn _key -> - raise ArgumentError, "duplicated_clauses" - end) + opts = sanitize_opts(opts, [:do]) {case_clauses, sa} = Enum.map_reduce(opts, s, fn x, sa -> @@ -2752,10 +2751,6 @@ defmodule ElixirSense.Core.Compiler do expand_clauses(meta, :case, fun, do_clause, s, e) end - defp expand_case(_meta, {_key, _}, _s, _e) do - raise ArgumentError, "unexpected_option" - end - # cond def cond(_meta, [], _s, _e) do @@ -2767,10 +2762,7 @@ defmodule ElixirSense.Core.Compiler do end def cond(meta, opts, s, e) do - :ok = - assert_at_most_once(:do, opts, 0, fn _key -> - raise ArgumentError, "duplicated_clauses" - end) + opts = sanitize_opts(opts, [:do]) {cond_clauses, sa} = Enum.map_reduce(opts, s, fn x, sa -> @@ -2785,10 +2777,6 @@ defmodule ElixirSense.Core.Compiler do expand_clauses(meta, :cond, fun, do_clause, s, e) end - defp expand_cond(_meta, {_key, _}, _s, _e) do - raise ArgumentError, "unexpected_option" - end - # receive def receive(_meta, [], _s, _e) do @@ -2800,12 +2788,7 @@ defmodule ElixirSense.Core.Compiler do end def receive(meta, opts, s, e) do - raise_error = fn _key -> - raise ArgumentError, "duplicated_clauses" - end - - :ok = assert_at_most_once(:do, opts, 0, raise_error) - :ok = assert_at_most_once(:after, opts, 0, raise_error) + opts = sanitize_opts(opts, [:do, :after]) {receive_clauses, sa} = Enum.map_reduce(opts, s, fn x, sa -> @@ -2829,12 +2812,20 @@ defmodule ElixirSense.Core.Compiler do expand_clauses(meta, :receive, fun, after_clause, s, e) end - defp expand_receive(_meta, {:after, _}, _s, _e) do - raise ArgumentError, "multiple_after_clauses_in_receive" - end - - defp expand_receive(_meta, {_key, _}, _s, _e) do - raise ArgumentError, "unexpected_option" + defp expand_receive(meta, {:after, expr}, s, e) when not is_list(expr) do + # elixir raises here multiple_after_clauses_in_receive + case expr do + expr when not is_list(expr) -> + # try to recover from error by wrapping the expression in list + expand_receive(meta, {:after, [expr]}, s, e) + [first | _] -> + # try to recover from error by taking first clause only + # TODO maybe search for clause with cursor? + expand_receive(meta, {:after, [first]}, s, e) + [] -> + # try to recover from error by inserting a fake clause + expand_receive(meta, {:after, [{:->, meta, [[0], :ok]}]}, s, e) + end end # with @@ -2911,28 +2902,10 @@ defmodule ElixirSense.Core.Compiler do # try def try(_meta, [], _s, _e), do: raise("missing_option") - def try(_meta, [{:do, _}], _s, _e), do: raise("missing_option") def try(_meta, opts, _s, _e) when not is_list(opts), do: raise("invalid_args") def try(meta, opts, s, e) do - # TODO: Make this an error on v2.0 - # case opts do - # [{:do, _}, {:else, _}] -> - # file_warn(meta, Map.get(e, :file), __MODULE__, {:try_with_only_else_clause, origin(meta, :try)}) - # _ -> - # :ok - # end - - raise_error = fn _key -> - raise "duplicated_clauses" - end - - :ok = assert_at_most_once(:do, opts, 0, raise_error) - :ok = assert_at_most_once(:rescue, opts, 0, raise_error) - :ok = assert_at_most_once(:catch, opts, 0, raise_error) - :ok = assert_at_most_once(:else, opts, 0, raise_error) - :ok = assert_at_most_once(:after, opts, 0, raise_error) - # :ok = warn_catch_before_rescue(opts, meta, e, false) + opts = sanitize_opts(opts, [:do, :rescue, :catch, :else, :after]) {try_clauses, sa} = Enum.map_reduce(opts, s, fn x, sa -> @@ -2965,10 +2938,6 @@ defmodule ElixirSense.Core.Compiler do expand_clauses_with_stacktrace(meta, &expand_rescue/4, rescue_clause, s, e) end - defp expand_try(_meta, {_key, _}, _s, _e) do - raise ArgumentError, "unexpected_option" - end - defp expand_clauses_with_stacktrace(meta, fun, clauses, s, e) do old_stacktrace = s.stacktrace ss = %{s | stacktrace: true} @@ -2984,22 +2953,22 @@ defmodule ElixirSense.Core.Compiler do head(args, s, e) end - defp expand_catch(_meta, _, _, _e) do - raise ArgumentError, "wrong_number_of_args_for_clause" + defp expand_catch(meta, [a1, a2 | _], s, e) do + # attempt to recover from error by taking 2 first args + # elixir raises here wrong_number_of_args_for_clause + expand_catch(meta, [a1, a2], s, e) end defp expand_rescue(_meta, [arg], s, e) do - case expand_rescue(arg, s, e) do - {e_arg, sa, ea} -> - {[e_arg], sa, ea} - - false -> - raise ArgumentError, "invalid_rescue_clause" - end + # elixir is strict here and raises invalid_rescue_clause on invalid args + {e_arg, sa, ea} = expand_rescue(arg, s, e) + {[e_arg], sa, ea} end - defp expand_rescue(_meta, _, _, _e) do - raise ArgumentError, "wrong_number_of_args_for_clause" + defp expand_rescue(meta, [a1 | _], s, e) do + # try to recover from error by taking first argument only + # elixir raises here wrong_number_of_args_for_clause + expand_rescue(meta, [a1], s, e) end # rescue var @@ -3029,21 +2998,22 @@ defmodule ElixirSense.Core.Compiler do case e_left do {name, _, atom} when is_atom(name) and is_atom(atom) -> - case normalize_rescue(e_right) do - false -> false - other -> {{:in, meta, [e_left, other]}, sr, er} - end + {{:in, meta, [e_left, normalize_rescue(e_right, e)]}, sr, er} _ -> - false + # elixir rejects this case, we normalize to underscore + {{:in, meta, [{:_, [], e.module}, normalize_rescue(e_right, e)]}, sr, er} end end # rescue expr() => rescue expanded_expr() - defp expand_rescue({_meta, meta, _} = arg, s, e) do + defp expand_rescue({_, meta, _} = arg, s, e) do # TODO wut? case Macro.expand_once(arg, %{e | line: line(meta)}) do - ^arg -> false + ^arg -> + # elixir rejects this case + # try to recover from error by generating fake expression + expand_rescue({:in, meta, [arg, {:_, [], e.module}]}, s, e) new_arg -> expand_rescue(new_arg, s, e) end end @@ -3053,24 +3023,35 @@ defmodule ElixirSense.Core.Compiler do expand_rescue({:in, [], [{:_, [], e.module}, arg]}, s, e) end - defp normalize_rescue(atom) when is_atom(atom) do + defp normalize_rescue(atom, _e) when is_atom(atom) do [atom] end - defp normalize_rescue(other) do - if is_list(other) and Enum.all?(other, &is_atom/1), do: other, else: false + defp normalize_rescue(other, e) do + # elixir is strict here, we reject invalid nodes + res = if is_list(other) do + Enum.filter(other, &is_atom/1) + else + [] + end + + if res == [] do + [{:_, [], e.module}] + else + res + end end defp expand_head(_meta, _kind, _key) do fn - [{:when, _, [_, _, _ | _]}], _, _e -> - raise ArgumentError, "wrong_number_of_args_for_clause" + [{:when, _, [args, _, _ | _]}], _, _e -> + raise ArgumentError, "wrong_number_of_args_for_clause #{inspect(args)}" [_] = args, s, e -> head(args, s, e) - _, _, _e -> - raise ArgumentError, "wrong_number_of_args_for_clause" + args, _, _e -> + raise ArgumentError, "wrong_number_of_args_for_clause #{inspect(args)}" end end @@ -3101,24 +3082,24 @@ defmodule ElixirSense.Core.Compiler do {{key, values}, se} end - defp expand_clauses_origin(_meta, _kind, _fun, {_key, _}, _, _e) do - raise ArgumentError, "bad_or_missing_clauses" + defp expand_clauses_origin(meta, kind, fun, {key, expr}, s, e) do + # try to recover from error by wrapping the expression in a clauses list + # elixir raises here bad_or_missing_clauses + expand_clauses_origin(meta, kind, fun, {key, [expr]}, s, e) end # helpers - defp assert_at_most_once(_kind, [], _count, _fun), do: :ok - - defp assert_at_most_once(kind, [{kind, _} | _], 1, error_fun) do - error_fun.(kind) - end - - defp assert_at_most_once(kind, [{kind, _} | rest], count, fun) do - assert_at_most_once(kind, rest, count + 1, fun) + defp sanitize_opt(opts, opt) do + # TODO look for opt with cursor? + case Keyword.fetch(opts, opt) do + :error -> [] + {:ok, value} -> [{opt, value}] + end end - defp assert_at_most_once(kind, [_ | rest], count, fun) do - assert_at_most_once(kind, rest, count, fun) + defp sanitize_opts(opts, allowed) do + Enum.flat_map(allowed, fn opt -> sanitize_opt(opts, opt) end) end defp origin(meta, default) do diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs new file mode 100644 index 00000000..4ea34792 --- /dev/null +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -0,0 +1,365 @@ +defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do + use ExUnit.Case, async: true + + alias ElixirSense.Core.MetadataBuilder + alias ElixirSense.Core.Normalized.Code, as: NormalizedCode + + defp get_cursor_env(code) do + {:ok, ast} = NormalizedCode.Fragment.container_cursor_to_quoted(code, [columns: true, token_metadata: true]) + state = MetadataBuilder.build(ast) + state.cursor_env + end + + describe "incomplete case" do + test "cursor in argument" do + code = """ + x = 5 + case \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in clause left side" do + code = """ + case a do + [x, \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in clause guard" do + code = """ + case a do + x when \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in clause guard call" do + code = """ + case a do + x when is_atom(\ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in clause right side" do + code = """ + case a do + x -> \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in clause right side after expressions" do + code = """ + case a do + x -> + foo(1) + \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + end + + describe "incomplete cond" do + test "cursor in clause left side" do + code = """ + x = foo() + cond do + \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in clause left side with assignment" do + code = """ + cond do + (x = foo(); \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in clause right side" do + code = """ + cond do + x = foo() -> \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in clause right side after expressions" do + code = """ + cond do + x = foo() -> + foo(1) + \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + end + + describe "incomplete receive" do + test "cursor in clause left side" do + code = """ + x = foo() + receive do + \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in clause left side pin" do + code = """ + x = foo() + receive do + {^\ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in clause left side multiple matches" do + code = """ + receive do + {:msg, x, \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in clause left side guard" do + code = """ + receive do + {:msg, x} when \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in clause left side guard call" do + code = """ + receive do + {:msg, x} when is_atom(\ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in clause right side" do + code = """ + receive do + {:msg, x} -> \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in after clause left side" do + code = """ + x = foo() + receive do + a -> :ok + after + \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in after clause right side" do + code = """ + x = foo() + receive do + a -> :ok + after + 0 -> \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + end + + describe "incomplete try" do + test "cursor in do block" do + code = """ + x = foo() + try do + \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in left side of rescue clause" do + code = """ + x = foo() + try do + bar(x) + rescue + \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in left side of rescue clause match expression - invalid var" do + code = """ + x = foo() + try do + bar(x) + rescue + bar() in [\ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in left side of rescue clause match expression" do + code = """ + x = foo() + try do + bar(x) + rescue + e in [\ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in right side of rescue clause" do + code = """ + try do + bar() + rescue + x in [Error] -> \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in left side of catch clause" do + code = """ + x = foo() + try do + bar() + catch + \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in left side of catch clause guard" do + code = """ + try do + bar() + catch + x when \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in left side of catch clause after type" do + code = """ + try do + bar() + catch + x, \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in left side of catch clause 2 arg guard" do + code = """ + try do + bar() + catch + x, _ when \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in right side of catch clause" do + code = """ + try do + bar() + catch + x -> \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in right side of catch clause 2 arg" do + code = """ + try do + bar() + catch + x, _ -> \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in left side of else clause" do + code = """ + x = foo() + try do + bar() + else + \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in left side of else clause guard" do + code = """ + try do + bar() + else + x when \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in right side of else clause" do + code = """ + try do + bar() + else + x -> \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in after block" do + code = """ + x = foo() + try do + bar() + after + \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + end +end From 6da2a713087db1a2ce3f9f878082d476c07af296 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 12 May 2024 22:46:18 +0200 Subject: [PATCH 039/235] with error recovery --- lib/elixir_sense/core/compiler.ex | 54 +++++-------- .../metadata_builder/error_recovery_test.exs | 80 +++++++++++++++++++ 2 files changed, 100 insertions(+), 34 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 046d3445..0ebfc9e4 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -2832,61 +2832,47 @@ defmodule ElixirSense.Core.Compiler do def with(meta, args, s, e) do {exprs, opts0} = ElixirUtils.split_opts(args) + opts0 = sanitize_opts(opts0, [:do, :else]) s0 = ElixirEnv.reset_vars(s) - {e_exprs, {s1, e1, has_match}} = Enum.map_reduce(exprs, {s0, e, false}, &expand_with/2) + {e_exprs, {s1, e1}} = Enum.map_reduce(exprs, {s0, e}, &expand_with/2) {e_do, opts1, s2} = expand_with_do(meta, opts0, s, s1, e1) - {e_opts, opts2, s3} = expand_with_else(meta, opts1, s2, e, has_match) - - case opts2 do - [{_key, _} | _] -> - raise "unexpected_option" - - [] -> - :ok - end + {e_opts, opts2, s3} = expand_with_else(meta, opts1, s2, e) {{:with, meta, e_exprs ++ [[{:do, e_do} | e_opts]]}, s3, e} end - defp expand_with({:<-, meta, [left, right]}, {s, e, has_match}) do + defp expand_with({:<-, meta, [left, right]}, {s, e}) do {e_right, sr, er} = ElixirExpand.expand(right, s, e) sm = ElixirEnv.reset_read(sr, s) {[e_left], sl, el} = head([left], sm, er) - new_has_match = - case e_left do - {var, _, ctx} when is_atom(var) and is_atom(ctx) -> has_match - _ -> true - end - - {{:<-, meta, [e_left, e_right]}, {sl, el, new_has_match}} + {{:<-, meta, [e_left, e_right]}, {sl, el}} end - defp expand_with(expr, {s, e, has_match}) do + defp expand_with(expr, {s, e}) do {e_expr, se, ee} = ElixirExpand.expand(expr, s, e) - {e_expr, {se, ee, has_match}} + {e_expr, {se, ee}} end defp expand_with_do(_meta, opts, s, acc, e) do - case Keyword.pop(opts, :do) do - {nil, _} -> - raise "missing_option" + {expr, rest_opts} = Keyword.pop(opts, :do) + # elixir raises here missing_option + # we return empty expression + expr = expr || [] - {expr, rest_opts} -> - # TODO not sure new vars scope is needed - acc = acc |> new_vars_scope - {e_expr, s_acc, e_acc} = ElixirExpand.expand(expr, acc, e) + # TODO not sure new vars scope is needed + acc = acc |> new_vars_scope + {e_expr, s_acc, e_acc} = ElixirExpand.expand(expr, acc, e) - s_acc = - s_acc - |> maybe_move_vars_to_outer_scope - |> remove_vars_scope + s_acc = + s_acc + |> maybe_move_vars_to_outer_scope + |> remove_vars_scope - {e_expr, rest_opts, ElixirEnv.merge_vars(s_acc, s, e_acc)} - end + {e_expr, rest_opts, ElixirEnv.merge_vars(s_acc, s, e_acc)} end - defp expand_with_else(meta, opts, s, e, _has_match) do + defp expand_with_else(meta, opts, s, e) do case Keyword.pop(opts, :else) do {nil, _} -> {[], opts, s} diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index 4ea34792..3d530035 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -6,6 +6,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do defp get_cursor_env(code) do {:ok, ast} = NormalizedCode.Fragment.container_cursor_to_quoted(code, [columns: true, token_metadata: true]) + # dbg(ast) state = MetadataBuilder.build(ast) state.cursor_env end @@ -362,4 +363,83 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert Enum.any?(env.vars, & &1.name == :x) end end + + describe "incomplete with" do + test "cursor in match expressions" do + code = """ + x = foo() + with \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in match expressions guard" do + code = """ + with x when \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in match expressions - right side" do + code = """ + x = foo() + with 1 <- \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in match expressions - right side next expression" do + code = """ + with x <- foo(), \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in do block" do + code = """ + with x <- foo() do + \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in else clause left side" do + code = """ + x = foo() + with 1 <- foo() do + :ok + else + \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in else clause left side guard" do + code = """ + with 1 <- foo() do + :ok + else + x when \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in else clause right side" do + code = """ + with 1 <- foo() do + :ok + else + x -> \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + end end From ffe71c84382c8189c6cd2d69af7c9b424b8d60db Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 13 May 2024 10:42:10 +0200 Subject: [PATCH 040/235] test uniq omission --- test/elixir_sense/core/compiler_test.exs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/elixir_sense/core/compiler_test.exs b/test/elixir_sense/core/compiler_test.exs index 3fdf6e73..3df1e8e5 100644 --- a/test/elixir_sense/core/compiler_test.exs +++ b/test/elixir_sense/core/compiler_test.exs @@ -493,6 +493,13 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do :ok """) + assert_expansion(""" + for i <- [1, 2, 3], uniq: true do + i + end + :ok + """) + assert_expansion(""" _ = for i <- [1, 2, 3] do i From 1a27894aad7ceac58d2fc37dacac8c9562a7a642 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 13 May 2024 10:42:37 +0200 Subject: [PATCH 041/235] error recovery in for --- lib/elixir_sense/core/compiler.ex | 109 ++++++------ .../metadata_builder/error_recovery_test.exs | 167 ++++++++++++++++++ 2 files changed, 222 insertions(+), 54 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 0ebfc9e4..46795dc4 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -2120,62 +2120,73 @@ defmodule ElixirSense.Core.Compiler do {expr, opts} = case Keyword.pop(block, :do) do + {nil, do_opts} -> + # elixir raises missing_option here + {[], do_opts} {do_expr, do_opts} -> {do_expr, do_opts} - nil -> raise "missing_option" end {e_opts, so, eo} = expand(opts, __MODULE__.Env.reset_vars(s), e) {e_cases, sc, ec} = map_fold(&expand_for_generator/3, so, eo, cases) - assert_generator_start(meta, e_cases, e) + # elixir raises here for_generator_start on invalid start generator - {{e_expr, se, ee}, normalized_opts} = - case validate_for_options(e_opts, false, false, false, return, meta, e, []) do - {:ok, maybe_reduce, nopts} -> - # TODO not sure new vars scope is actually needed - sc = sc |> new_vars_scope - {ed, sd, envd} = expand_for_do_block(meta, expr, sc, ec, maybe_reduce) + {maybe_reduce, normalized_opts} = sanitize_for_options(e_opts, false, false, false, return, meta, e, []) - sd = - sd - |> maybe_move_vars_to_outer_scope - |> remove_vars_scope + # TODO not sure new vars scope is actually needed + sc = sc |> new_vars_scope + {e_expr, se, ee} = expand_for_do_block(meta, expr, sc, ec, maybe_reduce) - {{ed, sd, envd}, nopts} - - {:error, _error} -> - # {file_error(meta, e, __MODULE__, error), e_opts} - raise "invalid_option" - end + se = + se + |> maybe_move_vars_to_outer_scope + |> remove_vars_scope {{:for, meta, e_cases ++ [[{:do, e_expr} | normalized_opts]]}, __MODULE__.Env.merge_vars(se, s, ee), e} end - defp expand_for_do_block(_meta, [{:->, _, _} | _], _s, _e, false), - do: raise("for_without_reduce_bad_block") + defp expand_for_do_block(meta, [{:->, _, _} | _] = clauses, s, e, false) do + # elixir raises here for_without_reduce_bad_block + # try to recover from error by emitting fake reduce + expand_for_do_block(meta, clauses, s, e, {:reduce, []}) + end defp expand_for_do_block(_meta, expr, s, e, false), do: expand(expr, s, e) defp expand_for_do_block(meta, [{:->, _, _} | _] = clauses, s, e, {:reduce, _}) do transformer = fn - {_, _, [[_], _]} = clause, sa -> + {:->, clause_meta, [args, right]} = clause, sa -> + # elixir checks here that clause has exactly 1 arg by matching against {_, _, [[_], _]} + # we drop excessive or generate a fake arg + # TODO check if there is cursor in dropped arg? + args = case args do + [] -> [{:_, [], e.module}] + [head | _] -> [head] + end + clause = {:->, clause_meta, [args, right]} s_reset = __MODULE__.Env.reset_vars(sa) {e_clause, s_acc, e_acc} = __MODULE__.Clauses.clause(meta, :fn, &__MODULE__.Clauses.head/3, clause, s_reset, e) {e_clause, __MODULE__.Env.merge_vars(s_acc, sa, e_acc)} - - _, _ -> - raise "for_with_reduce_bad_block" end {do_expr, sa} = Enum.map_reduce(clauses, s, transformer) {do_expr, sa, e} end - defp expand_for_do_block(_meta, _expr, _s, _e, {:reduce, _}), - do: raise("for_with_reduce_bad_block") + defp expand_for_do_block(meta, expr, s, e, {:reduce, _} = reduce) do + # elixir raises here for_with_reduce_bad_block + case expr do + [] -> + # try to recover from error by emitting a fake clause + expand_for_do_block(meta, [{:->, meta, [[{:_, [], e.module}], :ok]}], s, e, reduce) + _ -> + # try to recover from error by wrapping the expression in clause + expand_for_do_block(meta, [{:->, meta, [[expr], :ok]}], s, e, reduce) + end + end defp expand_for_generator({:<-, meta, [left, right]}, s, e) do {e_right, sr, er} = expand(right, s, e) @@ -2213,16 +2224,12 @@ defmodule ElixirSense.Core.Compiler do {x, s, e} end - defp assert_generator_start(_, [{:<-, _, [_, _]} | _], _), do: :ok - defp assert_generator_start(_, [{:<<>>, _, [{:<-, _, [_, _]}]} | _], _), do: :ok - defp assert_generator_start(_meta, _, _e), do: raise("for_generator_start") - - defp validate_for_options([{:into, _} = pair | opts], _into, uniq, reduce, return, meta, e, acc) do - validate_for_options(opts, pair, uniq, reduce, return, meta, e, [pair | acc]) + defp sanitize_for_options([{:into, _} = pair | opts], _into, uniq, reduce, return, meta, e, acc) do + sanitize_for_options(opts, pair, uniq, reduce, return, meta, e, [pair | acc]) end - defp validate_for_options( - [{:uniq, boolean} = pair | opts], + defp sanitize_for_options( + [{:uniq, _} = pair | opts], into, _uniq, reduce, @@ -2230,16 +2237,13 @@ defmodule ElixirSense.Core.Compiler do meta, e, acc - ) - when is_boolean(boolean) do - validate_for_options(opts, into, pair, reduce, return, meta, e, [pair | acc]) - end - - defp validate_for_options([{:uniq, value} | _], _, _, _, _, _, _, _) do - {:error, {:for_invalid_uniq, value}} + ) do + # elixir checks if uniq value is boolean + # we do not care - there may be cursor in it + sanitize_for_options(opts, into, pair, reduce, return, meta, e, [pair | acc]) end - defp validate_for_options( + defp sanitize_for_options( [{:reduce, _} = pair | opts], into, uniq, @@ -2249,26 +2253,23 @@ defmodule ElixirSense.Core.Compiler do e, acc ) do - validate_for_options(opts, into, uniq, pair, return, meta, e, [pair | acc]) - end - - defp validate_for_options([], into, uniq, {:reduce, _}, _return, _meta, _e, _acc) - when into != false or uniq != false do - {:error, :for_conflicting_reduce_into_uniq} + # elixir raises for_conflicting_reduce_into_uniq when reduce, uniq and true is enabled + sanitize_for_options(opts, into, uniq, pair, return, meta, e, [pair | acc]) end - defp validate_for_options([], false, uniq, false, true, meta, e, acc) do + defp sanitize_for_options([], false, uniq, false, true, meta, e, acc) do pair = {:into, []} - validate_for_options([pair], pair, uniq, false, true, meta, e, acc) + sanitize_for_options([pair], pair, uniq, false, true, meta, e, acc) end - defp validate_for_options([], false, {:uniq, true}, false, false, meta, e, acc) do + defp sanitize_for_options([], false, {:uniq, true}, false, false, meta, e, acc) do + # TODO check if there is cursor in dropped unique acc_without_uniq = Keyword.delete(acc, :uniq) - validate_for_options([], false, false, false, false, meta, e, acc_without_uniq) + sanitize_for_options([], false, false, false, false, meta, e, acc_without_uniq) end - defp validate_for_options([], _into, _uniq, reduce, _return, _meta, _e, acc) do - {:ok, reduce, Enum.reverse(acc)} + defp sanitize_for_options([], _into, _uniq, reduce, _return, _meta, _e, acc) do + {reduce, Enum.reverse(acc)} end defp sanitize_opts(allowed, opts) when is_list(opts) do diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index 3d530035..657e62cd 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -442,4 +442,171 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert Enum.any?(env.vars, & &1.name == :x) end end + + describe "incomplete for" do + test "cursor in generator match expressions" do + code = """ + x = foo() + for \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in generator match expression guard" do + code = """ + for x when \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in generator match expression right side" do + code = """ + x = foo() + for a <- \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in generator match expressions bitstring" do + code = """ + x = foo() + for <<\ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in generator match expression guard bitstring" do + code = """ + for < \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, & &1.name == :y) + end + + test "cursor in do block reduce right side of clause too many args" do + code = """ + for x <- [], reduce: %{} do + y, z -> \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, & &1.name == :y) + end + + test "cursor in do block reduce right side of clause too little args" do + code = """ + for x <- [], reduce: %{} do + -> \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "cursor in do block right side of clause without reduce" do + code = """ + for x <- [] do + y -> \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, & &1.name == :y) + end + end end From fd291abee2d426e1e4bef007dda20216d43f782b Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 13 May 2024 11:57:34 +0200 Subject: [PATCH 042/235] register first cursor --- lib/elixir_sense/core/compiler.ex | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 46795dc4..1c7ae19f 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -462,8 +462,13 @@ defmodule ElixirSense.Core.Compiler do # Cursor defp do_expand({:__cursor__, meta, []}, s, e) do - s = s - |> add_cursor_env(meta, e) + s = unless s.cursor_env do + s + |> add_cursor_env(meta, e) + else + s + end + {{:__cursor__, meta, []}, s, e} end From c95584f7fb145d666eb7646fa53de57bc0e07aee Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 13 May 2024 11:57:48 +0200 Subject: [PATCH 043/235] error recovery in fn --- lib/elixir_sense/core/compiler.ex | 36 +++++------- .../metadata_builder/error_recovery_test.exs | 55 +++++++++++++++++++ 2 files changed, 68 insertions(+), 23 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 1c7ae19f..87dc05e3 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -2863,7 +2863,7 @@ defmodule ElixirSense.Core.Compiler do defp expand_with_do(_meta, opts, s, acc, e) do {expr, rest_opts} = Keyword.pop(opts, :do) # elixir raises here missing_option - # we return empty expression + # we return empty expression expr = expr || [] # TODO not sure new vars scope is needed @@ -3618,35 +3618,25 @@ defmodule ElixirSense.Core.Compiler do def expand(meta, clauses, s, e) when is_list(clauses) do transformer = fn {:->, _, [left, _right]} = clause, sa -> - if Enum.any?(left, &is_invalid_arg/1) do - raise "defaults_in_args" - else - s_reset = ElixirEnv.reset_vars(sa) + # elixir raises defaults_in_args + left = sanitize_fn_arg(left) - {e_clause, s_acc, e_acc} = - ElixirClauses.clause(meta, :fn, &ElixirClauses.head/3, clause, s_reset, e) + s_reset = ElixirEnv.reset_vars(sa) - {e_clause, ElixirEnv.merge_vars(s_acc, sa, e_acc)} - end + {e_clause, s_acc, e_acc} = + ElixirClauses.clause(meta, :fn, &ElixirClauses.head/3, clause, s_reset, e) + + {e_clause, ElixirEnv.merge_vars(s_acc, sa, e_acc)} end {e_clauses, se} = Enum.map_reduce(clauses, s, transformer) - e_arities = Enum.map(e_clauses, fn {:->, _, [args, _]} -> fn_arity(args) end) - - case Enum.uniq(e_arities) do - [_] -> - {{:fn, meta, e_clauses}, se, e} - _ -> - raise "clauses_with_different_arities" - end + {{:fn, meta, e_clauses}, se, e} end - defp is_invalid_arg({:"\\\\", _, _}), do: true - defp is_invalid_arg(_), do: false - - defp fn_arity([{:when, _, args}]), do: length(args) - 1 - defp fn_arity(args), do: length(args) + # TODO check if there is cursor in default + defp sanitize_fn_arg({:"\\\\", _, [value, _default]}), do: value + defp sanitize_fn_arg(value), do: value # Capture @@ -4621,7 +4611,7 @@ defmodule ElixirSense.Core.Compiler do defp validate_match_key({name, _, context}) when is_atom(name) and is_atom(context) do - # invalid_variable_in_map_key_match + # elixir raises here invalid_variable_in_map_key_match false end diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index 657e62cd..e45080b1 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -609,4 +609,59 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert Enum.any?(env.vars, & &1.name == :y) end end + + describe "invalid fn" do + # unfortunately container_cursor_to_quoted cannot handle fn + test "different clause arities" do + code = """ + fn + _ -> :ok + x, _ -> __cursor__() + end + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "default args in clause" do + code = """ + fn + x \\\\ nil -> __cursor__() + end + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "incomplete clause left side" do + code = """ + x = foo() + fn + __cursor__() + end + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "incomplete clause left side guard" do + code = """ + fn + x when __cursor__() + end + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + + test "incomplete clause right side" do + code = """ + fn + x -> __cursor__() + end + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + end end From eef12e87d122851a2441aebed5413982eeab002f Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 13 May 2024 21:45:40 +0200 Subject: [PATCH 044/235] error recovery in capture --- lib/elixir_sense/core/compiler.ex | 93 +++--- .../metadata_builder/error_recovery_test.exs | 275 ++++++++++++++++++ 2 files changed, 333 insertions(+), 35 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 87dc05e3..b179163d 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -2631,6 +2631,21 @@ defmodule ElixirSense.Core.Compiler do def extract_or_guards({:when, _, [left, right]}), do: [left | extract_or_guards(right)] def extract_or_guards(term), do: [term] + + def select_with_cursor(ast_list) do + Enum.find(ast_list, &has_cursor?/1) + end + + def has_cursor?(ast) do + # TODO rewrite to lazy prewalker + {_, result} = Macro.prewalk(ast, false, fn + {:__cursor__, _, list} = node, state when is_list(list) -> + {node, true} + node, state -> + {node, state} + end) + result + end end defmodule Clauses do @@ -3614,6 +3629,7 @@ defmodule ElixirSense.Core.Compiler do alias ElixirSense.Core.Compiler.Env, as: ElixirEnv alias ElixirSense.Core.Compiler.Clauses, as: ElixirClauses alias ElixirSense.Core.Compiler.Dispatch, as: ElixirDispatch + alias ElixirSense.Core.Compiler.Utils, as: ElixirUtils def expand(meta, clauses, s, e) when is_list(clauses) do transformer = fn @@ -3643,7 +3659,7 @@ defmodule ElixirSense.Core.Compiler do def capture(meta, {:/, _, [{{:., _, [_m, f]} = dot, require_meta, []}, a]}, s, e) when is_atom(f) and is_integer(a) do args = args_from_arity(meta, a) - # handle_capture_possible_warning(meta, require_meta, m, f, a, e) + capture_require({dot, require_meta, args}, s, e, true) end @@ -3666,8 +3682,16 @@ defmodule ElixirSense.Core.Compiler do capture(meta, expr, s, e) end - def capture(_meta, {:__block__, _, _} = _expr, _s, _e) do - raise "block_expr_in_capture" + def capture(meta, {:__block__, _, expr}, s, e) do + # elixir raises block_expr_in_capture + # try to recover from error + expr = case expr do + [] -> {:"&1", meta, e.module} + list -> + ElixirUtils.select_with_cursor(list) || hd(list) + end + + capture(meta, expr, s, e) end def capture(_meta, {atom, _, args} = expr, s, e) when is_atom(atom) and is_list(args) do @@ -3682,8 +3706,10 @@ defmodule ElixirSense.Core.Compiler do capture_expr(meta, list, s, e, is_sequential_and_not_empty(list)) end - def capture(_meta, integer, _s, _e) when is_integer(integer) do - raise "capture_arg_outside_of_capture" + def capture(meta, integer, s, e) when is_integer(integer) do + # elixir raises here capture_arg_outside_of_capture + # emit fake capture + capture(meta, [{:&, meta, [1]}], s, e) end def capture(meta, arg, s, e) do @@ -3694,7 +3720,8 @@ defmodule ElixirSense.Core.Compiler do capture(meta, {:/, meta, [arg, 0]}, s, e) _ -> - raise "invalid_args_for_capture #{inspect(arg)}" + # try to wrap it in list + capture(meta, [arg], s, e) end end @@ -3705,7 +3732,7 @@ defmodule ElixirSense.Core.Compiler do end defp capture_require({{:., dot_meta, [left, right]}, require_meta, args}, s, e, sequential) do - case escape(left, e, []) do + case escape(left, []) do {esc_left, []} -> {e_left, se, ee} = ElixirExpand.expand(esc_left, s, e) @@ -3745,7 +3772,7 @@ defmodule ElixirSense.Core.Compiler do end defp capture_expr(meta, expr, s, e, escaped, sequential) do - case escape(expr, e, escaped) do + case escape(expr, escaped) do {e_expr, []} when not sequential -> # elixir raises here invalid_args_for_capture # we emit fn without args @@ -3753,23 +3780,15 @@ defmodule ElixirSense.Core.Compiler do {:expand, fn_expr, s, e} {e_expr, e_dict} -> - e_vars = validate(meta, e_dict, 1, e) + # elixir raises capture_arg_without_predecessor here + # if argument vars are not consecutive + e_vars = Enum.map(e_dict, & elem(&1, 1)) fn_expr = {:fn, meta, [{:->, meta, [e_vars, e_expr]}]} {:expand, fn_expr, s, e} end end - defp validate(meta, [{pos, var} | t], pos, e) do - [var | validate(meta, t, pos + 1, e)] - end - - defp validate(_meta, [{_pos, _} | _], _expected, _e) do - raise "capture_arg_without_predecessor" - end - - defp validate(_meta, [], _pos, _e), do: [] - - defp escape({:&, meta, [pos]}, _e, dict) when is_integer(pos) and pos > 0 do + defp escape({:&, meta, [pos]}, dict) when is_integer(pos) and pos > 0 do # Using a nil context here to emit warnings when variable is unused. # This might pollute user space but is unlikely because variables # named :"&1" are not valid syntax. @@ -3777,31 +3796,35 @@ defmodule ElixirSense.Core.Compiler do {var, :orddict.store(pos, var, dict)} end - defp escape({:&, _meta, [pos]}, _e, _dict) when is_integer(pos) do - raise "invalid_arity_for_capture" + defp escape({:&, meta, [pos]}, dict) when is_integer(pos) do + # elixir raises here invalid_arity_for_capture + # we substitute arg number + escape({:&, meta, [1]}, dict) end - defp escape({:&, _meta, _} = _arg, _e, _dict) do - raise "nested_capture" + defp escape({:&, _meta, args}, dict) do + # elixir raises here nested_capture + # try to recover from error by dropping & + escape(args, dict) end - defp escape({left, meta, right}, e, dict0) do - {t_left, dict1} = escape(left, e, dict0) - {t_right, dict2} = escape(right, e, dict1) + defp escape({left, meta, right}, dict0) do + {t_left, dict1} = escape(left, dict0) + {t_right, dict2} = escape(right, dict1) {{t_left, meta, t_right}, dict2} end - defp escape({left, right}, e, dict0) do - {t_left, dict1} = escape(left, e, dict0) - {t_right, dict2} = escape(right, e, dict1) + defp escape({left, right}, dict0) do + {t_left, dict1} = escape(left, dict0) + {t_right, dict2} = escape(right, dict1) {{t_left, t_right}, dict2} end - defp escape(list, e, dict) when is_list(list) do - Enum.map_reduce(list, dict, fn x, acc -> escape(x, e, acc) end) + defp escape(list, dict) when is_list(list) do + Enum.map_reduce(list, dict, fn x, acc -> escape(x, acc) end) end - defp escape(other, _e, dict) do + defp escape(other, dict) do {other, dict} end @@ -3813,14 +3836,14 @@ defmodule ElixirSense.Core.Compiler do end end - defp args_from_arity(_meta, _a) do + defp args_from_arity(_meta, a) do # elixir raises invalid_arity_for_capture [] end defp is_sequential_and_not_empty([]), do: false defp is_sequential_and_not_empty(list), do: is_sequential(list, 1) - + # TODO need to understand if we need it defp is_sequential([{:&, _, [int]} | t], int), do: is_sequential(t, int + 1) defp is_sequential([], _int), do: true defp is_sequential(_, _int), do: false diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index e45080b1..d3c17bc8 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -664,4 +664,279 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert Enum.any?(env.vars, & &1.name == :x) end end + + describe "capture" do + test "empty" do + code = """ + &\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "local" do + code = """ + &foo\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "local slash no arity" do + code = """ + &foo/\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "local slash arity" do + code = """ + &foo/1\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "local slash invalid arity" do + code = """ + &foo/1000; \ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "local dot" do + code = """ + &foo.\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "local dot call" do + code = """ + &foo.(\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "local dot call closed" do + code = """ + &foo.()\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "local dot right" do + code = """ + &foo.bar\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "remote" do + code = """ + &Foo\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "remote dot" do + code = """ + &Foo.\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "remote dot right" do + code = """ + &Foo.bar\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "remote dot right no arity" do + code = """ + &Foo.bar/\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "remote dot right arity" do + code = """ + &Foo.bar/1\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "remote dot call" do + code = """ + &Foo.bar(\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "remote dot call closed" do + code = """ + &Foo.bar()\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "tuple" do + code = """ + &{\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "tuple closed" do + code = """ + &{}\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "list" do + code = """ + &[\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "list closed" do + code = """ + &[]\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "bitstring" do + code = """ + &<<\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "bitstring closed" do + code = """ + &<<>>\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "map no braces" do + code = """ + &%\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "map" do + code = """ + &%{\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "map closed" do + code = """ + &%{}\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "struct no braces" do + code = """ + &%Foo\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "struct" do + code = """ + &%Foo{\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "struct closed" do + code = """ + &%Foo{}\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "block" do + code = """ + & (\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "block multiple expressions" do + code = """ + & (:ok; \ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "arg var incomplete" do + code = """ + & &\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "arg var" do + code = """ + & &2\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "arg var in list" do + code = """ + &[&1, \ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "arg var in list without predecessor" do + code = """ + &[&2, \ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "no arg" do + code = """ + &{}; \ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "invalid arg nuber" do + code = """ + & &0; \ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "outside of capture" do + code = """ + &1; \ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "invalid arg local" do + code = """ + &foo; \ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "invalid arg" do + code = """ + &"foo"; \ + """ + assert {meta, env} = get_cursor_env(code) + end + end end From 4e148facd0768ccbb0a7303eadfb49ea81f71b3d Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 13 May 2024 22:03:09 +0200 Subject: [PATCH 045/235] error recovery in pin --- lib/elixir_sense/core/compiler.ex | 12 +++++++----- .../metadata_builder/error_recovery_test.exs | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index b179163d..33072e18 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -499,21 +499,23 @@ defmodule ElixirSense.Core.Compiler do defp do_expand({:^, meta, [arg]}, %{prematch: {prematch, _, _}, vars: {_, write}} = s, e) do no_match_s = %{s | prematch: :pin, vars: {prematch, write}} - case expand(arg, no_match_s, %{e | context: nil}) do + case expand(arg, no_match_s, %{e | context: nil}) |> dbg do {{name, _var_meta, kind} = var, %{unused: unused}, _} when is_atom(name) and is_atom(kind) -> s = add_var_read(s, var) {{:^, meta, [var]}, %{s | unused: unused}, e} - _ -> - # function_error(meta, e, __MODULE__, {:invalid_arg_for_pin, arg}) + {arg, s, _e} -> + # elixir raises here invalid_arg_for_pin + # we may have cursor in arg {{:^, meta, [arg]}, s, e} end end defp do_expand({:^, meta, [arg]}, s, e) do - # function_error(meta, e, __MODULE__, {:pin_outside_of_match, arg}) - {{:^, meta, [arg]}, s, e} + # elixir raises here pin_outside_of_match + # try to recover from error by dropping the pin and expanding arg + expand(arg, s, e) end defp do_expand({:_, _meta, kind} = var, s, %{context: _context} = e) when is_atom(kind) do diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index d3c17bc8..5a35a064 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -939,4 +939,20 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert {meta, env} = get_cursor_env(code) end end + + describe "pin" do + test "outside of match" do + code = """ + ^\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "cursor in match" do + code = """ + ^__cursor__() = x\ + """ + assert {meta, env} = get_cursor_env(code) + end + end end From c816fb321a3c0ba2e12760dcaf7975ca27c67415 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 13 May 2024 22:42:58 +0200 Subject: [PATCH 046/235] map error recovery --- lib/elixir_sense/core/compiler.ex | 7 ++---- .../metadata_builder/error_recovery_test.exs | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 33072e18..82f0a030 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -4596,16 +4596,13 @@ defmodule ElixirSense.Core.Compiler do def expand_struct(_meta, _left, _right, _s, _e), do: raise("non_map_after_struct") - def expand_map(meta, [{:|, update_meta, [left, right]}], s, %{context: nil} = e) do + def expand_map(meta, [{:|, update_meta, [left, right]}], s, e) do + # elixir raises update_syntax_in_wrong_context if e.context is not nil {[e_left | e_right], se, ee} = ElixirExpand.expand_args([left | right], s, e) e_right = sanitize_kv(e_right, e) {{:%{}, meta, [{:|, update_meta, [e_left, e_right]}]}, se, ee} end - def expand_map(_meta, [{:|, _, [_, _]}] = _args, _s, _e) do - raise "update_syntax_in_wrong_context" - end - def expand_map(meta, args, s, e) do {e_args, se, ee} = ElixirExpand.expand_args(args, s, e) e_args = sanitize_kv(e_args, e) diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index 5a35a064..87b1fd1a 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -955,4 +955,27 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert {meta, env} = get_cursor_env(code) end end + + describe "map" do + test "invalid key in match" do + code = """ + %{foo => x} = x\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "update in match" do + code = """ + %{a | x: __cursor__()} = x\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "cursor in place of key value pair" do + code = """ + %{a: "123", \ + """ + assert {meta, env} = get_cursor_env(code) + end + end end From e8ad8225fba11aaae640f85d32100682ba0398a7 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 14 May 2024 09:54:00 +0200 Subject: [PATCH 047/235] error recovery in struct --- lib/elixir_sense/core/compiler.ex | 56 +++++++++++++------ .../metadata_builder/error_recovery_test.exs | 23 ++++++++ 2 files changed, 62 insertions(+), 17 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 82f0a030..31a0e17a 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -499,7 +499,7 @@ defmodule ElixirSense.Core.Compiler do defp do_expand({:^, meta, [arg]}, %{prematch: {prematch, _, _}, vars: {_, write}} = s, e) do no_match_s = %{s | prematch: :pin, vars: {prematch, write}} - case expand(arg, no_match_s, %{e | context: nil}) |> dbg do + case expand(arg, no_match_s, %{e | context: nil}) do {{name, _var_meta, kind} = var, %{unused: unused}, _} when is_atom(name) and is_atom(kind) -> s = add_var_read(s, var) @@ -4573,7 +4573,7 @@ defmodule ElixirSense.Core.Compiler do case extract_struct_assocs(e_right) do {:expand, map_meta, assocs} when context != :match -> assoc_keys = Enum.map(assocs, fn {k, _} -> k end) - struct = load_struct(e_left, [assocs], se, ee) + struct = load_struct(e_left, assocs, se, ee) keys = [:__struct__ | assoc_keys] without_keys = Elixir.Map.drop(struct, keys) # TODO is escape safe? @@ -4581,20 +4581,36 @@ defmodule ElixirSense.Core.Compiler do {{:%, meta, [e_left, {:%{}, map_meta, struct_assocs ++ assocs}]}, se, ee} {_, _, _assocs} -> + # elixir validates assocs against struct keys # we don't need to validate keys - # _ = load_struct(meta, e_left, [], Enum.map(assocs, fn {k, _} -> k end), se, ee) {{:%, meta, [e_left, e_right]}, se, ee} end - true -> + _ -> + # elixir raises invalid_struct_name if validate_struct returns false {{:%, meta, [e_left, e_right]}, se, ee} - - false -> - raise "invalid_struct_name #{inspect(e_left)}" end end - def expand_struct(_meta, _left, _right, _s, _e), do: raise("non_map_after_struct") + def expand_struct(meta, left, right, s, e) do + # elixir raises here non_map_after_struct + # try to recover from error by wrapping the expression in map + expand_struct(meta, left, wrap_in_fake_map(right), s, e) + end + + defp wrap_in_fake_map(right) do + map_args = case right do + list when is_list(list) -> + if Keyword.keyword?(list) do + list + else + [__fake_key__: list] + end + _ -> + [__fake_key__: right] + end + {:%{}, [], map_args} + end def expand_map(meta, [{:|, update_meta, [left, right]}], s, e) do # elixir raises update_syntax_in_wrong_context if e.context is not nil @@ -4668,42 +4684,48 @@ defmodule ElixirSense.Core.Compiler do defp validate_struct(atom, _) when is_atom(atom), do: true defp validate_struct(_, _), do: false + defp sanitize_assocs(list) do + Enum.filter(list, &match?({k, _} when is_atom(k), &1)) + end + defp extract_struct_assocs({:%{}, meta, [{:|, _, [_, assocs]}]}) do - {:update, meta, delete_struct_key(assocs)} + {:update, meta, delete_struct_key(sanitize_assocs(assocs))} end defp extract_struct_assocs({:%{}, meta, assocs}) do - {:expand, meta, delete_struct_key(assocs)} + {:expand, meta, delete_struct_key(sanitize_assocs(assocs))} end - defp extract_struct_assocs(_other) do - raise "non_map_after_struct" + defp extract_struct_assocs(right) do + # elixir raises here non_map_after_struct + # try to recover from error by wrapping the expression in map + extract_struct_assocs(wrap_in_fake_map(right)) end defp delete_struct_key(assocs) do Keyword.delete(assocs, :__struct__) end - defp load_struct(name, args, s, _e) do + defp load_struct(name, assocs, s, _e) do case s.structs[name] do nil -> try do - apply(name, :__struct__, args) + apply(name, :__struct__, [assocs]) else %{:__struct__ => ^name} = struct -> struct _ -> # recover from invalid return value - [__struct__: name] |> Keyword.merge(hd(args)) |> Elixir.Map.new() + [__struct__: name] |> Keyword.merge(assocs) |> Elixir.Map.new() rescue _ -> # recover from error by building the fake struct - [__struct__: name] |> Keyword.merge(hd(args)) |> Elixir.Map.new() + [__struct__: name] |> Keyword.merge(assocs) |> Elixir.Map.new() end info -> - info.fields |> Keyword.merge(hd(args)) |> Elixir.Map.new() + info.fields |> Keyword.merge(assocs) |> Elixir.Map.new() end end end diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index 87b1fd1a..6a9a1fa9 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -978,4 +978,27 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert {meta, env} = get_cursor_env(code) end end + + describe "struct" do + test "no map" do + code = """ + %\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "invalid map name" do + code = """ + %foo{\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "invalid key" do + code = """ + %Foo{"asd" => [\ + """ + assert {meta, env} = get_cursor_env(code) + end + end end From b9dbe8094ad0cb4e781b372ab8de7fecc26dc54c Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 14 May 2024 22:12:00 +0200 Subject: [PATCH 048/235] bitstring error recovery --- lib/elixir_sense/core/compiler.ex | 190 +++++------------- .../metadata_builder/error_recovery_test.exs | 142 +++++++++++++ 2 files changed, 197 insertions(+), 135 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 31a0e17a..c17e9a36 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -3139,13 +3139,7 @@ defmodule ElixirSense.Core.Compiler do {e_args, alignment, {sa, _}, ea} = expand(meta, &expand_match/3, args, [], {s, s}, e, 0, require_size) - case find_match(e_args) do - false -> - :ok - - _match -> - raise "nested_match" - end + # elixir validates if there is no nested match {{:<<>>, [{:alignment, alignment} | meta], e_args}, sa, ea} @@ -3241,8 +3235,10 @@ defmodule ElixirSense.Core.Compiler do defp expand_expr(_meta, component, fun, s, e) do case fun.(component, s, e) do - {e_component, _, _error_e} when is_list(e_component) or is_atom(e_component) -> - raise "invalid_literal" + {e_component, s, e} when is_list(e_component) or is_atom(e_component) -> + # elixir raises here invalid_literal + # try to recover from error by replacing it with "" + {"", s, e} expanded -> expanded @@ -3256,9 +3252,11 @@ defmodule ElixirSense.Core.Compiler do {specs, ss, es} = expand_each_spec(meta, unpack_specs(info, []), default, s, original_s, e) - merged_type = type(meta, expr_type, specs.type, e) - validate_size_required(meta, expect_size, expr_type, merged_type, specs.size, es) - size_and_unit = size_and_unit(meta, expr_type, specs.size, specs.unit, es) + merged_type = type(expr_type, specs.type) + + # elixir validates if unsized binary is not on the end + + size_and_unit = size_and_unit(expr_type, specs.size, specs.unit) alignment = compute_alignment(merged_type, specs.size, specs.unit) maybe_inferred_size = @@ -3275,65 +3273,51 @@ defmodule ElixirSense.Core.Compiler do [h | t] = build_spec( - meta, specs.size, specs.unit, merged_type, specs.endianness, specs.sign, - maybe_inferred_size, - es + maybe_inferred_size ) {Enum.reduce(t, h, fn i, acc -> {:-, meta, [acc, i]} end), alignment, ss, es} end - defp type(_, :default, :default, _), do: :integer - defp type(_, expr_type, :default, _), do: expr_type - - defp type(_, :binary, type, _) when type in [:binary, :bitstring, :utf8, :utf16, :utf32], + defp type(:default, :default), do: :integer + defp type(expr_type, :default), do: expr_type + defp type(:binary, type) when type in [:binary, :bitstring, :utf8, :utf16, :utf32], do: type - defp type(_, :bitstring, type, _) when type in [:binary, :bitstring], do: type + defp type(:bitstring, type) when type in [:binary, :bitstring], do: type - defp type(_, :integer, type, _) when type in [:integer, :float, :utf8, :utf16, :utf32], + defp type(:integer, type) when type in [:integer, :float, :utf8, :utf16, :utf32], do: type - defp type(_, :float, :float, _), do: :float - defp type(_, :default, type, _), do: type + defp type(:float, :float), do: :float + defp type(:default, type), do: type - defp type(_meta, _other, type, _e) do - # function_error(meta, e, __MODULE__, {:bittype_mismatch, type, other, :type}) - type + defp type(_other, _type) do + # elixir raises here bittype_mismatch + type(:default, :default) end + defp expand_each_spec(meta, [{:__cursor__, _, args} = h | t], map, s, original_s, e) when is_list(args) do + {_, s, e} = ElixirExpand.expand(h, s, e) + expand_each_spec(meta, t, map, s, original_s, e) + end defp expand_each_spec(meta, [{expr, meta_e, args} = h | t], map, s, original_s, e) when is_atom(expr) do case validate_spec(expr, args) do {key, arg} -> - # if args != [], do: :ok, else: file_warn(meta, e, __MODULE__, {:parens_bittype, expr}) - {value, se, ee} = expand_spec_arg(arg, s, original_s, e) - validate_spec_arg(meta, key, value, se, original_s, ee) - - case Map.get(map, key, :default) do - :default -> - :ok - - ^value -> - :ok - - _other -> - # function_error(meta, e, __MODULE__, {:bittype_mismatch, value, _other, key}) - :ok - end - + # elixir validates spec arg here + # elixir raises bittype_mismatch in some cases expand_each_spec(meta, t, Map.put(map, key, value), se, original_s, ee) :none -> ha = if args == nil do - # file_warn(meta, e, __MODULE__, {:unknown_bittype, expr}) {expr, meta_e, []} else h @@ -3342,7 +3326,8 @@ defmodule ElixirSense.Core.Compiler do # TODO not call it here case Macro.expand(ha, Map.put(e, :line, ElixirUtils.get_line(meta))) do ^ha -> - # function_error(meta, e, __MODULE__, {:undefined_bittype, h}) + # elixir raises here undefined_bittype + # we omit the spec expand_each_spec(meta, t, map, s, original_s, e) new_types -> @@ -3352,7 +3337,8 @@ defmodule ElixirSense.Core.Compiler do end defp expand_each_spec(meta, [_expr | tail], map, s, original_s, e) do - # function_error(meta, e, __MODULE__, {:undefined_bittype, expr}) + # elixir raises undefined_bittype + # we skip it expand_each_spec(meta, tail, map, s, original_s, e) end @@ -3405,31 +3391,14 @@ defmodule ElixirSense.Core.Compiler do e, require_size ) do - case e do - %{context: :match} when require_size -> - case List.last(parts) do - {:"::", _spec_meta, [bin, {:binary, _, nil}]} when not is_binary(bin) -> - # function_error(spec_meta, e, __MODULE__, :unsized_binary) - :ok - - {:"::", _spec_meta, [_, {:bitstring, _, nil}]} -> - # function_error(spec_meta, e, __MODULE__, :unsized_binary) - :ok - - _ -> - :ok - end - - _ -> - :ok - end + # elixir raises unsized_binary in some cases case e_right do {:binary, _, nil} -> {alignment, alignment} = Keyword.fetch!(parts_meta, :alignment) - if is_integer(alignment) and alignment != 0 do - # function_error(meta, e, __MODULE__, {:unaligned_binary, e_left}) + if is_integer(alignment) do + # elixir raises unaligned_binary if alignment != 0 Enum.reverse(parts, acc) else [{:"::", meta, [e_left, e_right]} | acc] @@ -3505,74 +3474,36 @@ defmodule ElixirSense.Core.Compiler do ElixirExpand.expand(expr, ElixirEnv.reset_read(s, original_s), e) end - defp validate_spec_arg(_meta, :unit, value, _s, _original_s, _e) when not is_integer(value) do - # function_error(meta, e, __MODULE__, {:bad_unit_argument, value}) - :ok - end - - defp validate_spec_arg(_meta, _key, _value, _s, _original_s, _e), do: :ok - - defp validate_size_required(_meta, :required, :default, type, :default, _e) - when type in [:binary, :bitstring] do - # function_error(meta, e, __MODULE__, :unsized_binary) - :ok + defp size_and_unit(type, size, unit) + when type in [:bitstring, :binary] and (size != :default or unit != :default) do + # elixir raises here bittype_literal_bitstring or bittype_literal_string + # we don't care + size_and_unit(type, :default, :default) end - defp validate_size_required(_, _, _, _, _, _), do: :ok - - defp size_and_unit(_meta, :bitstring, size, unit, _e) - when size != :default or unit != :default do - # function_error(meta, e, __MODULE__, :bittype_literal_bitstring) - [] - end - - defp size_and_unit(_meta, :binary, size, unit, _e) - when size != :default or unit != :default do - # function_error(meta, e, __MODULE__, :bittype_literal_string) - [] - end - - defp size_and_unit(_meta, _expr_type, size, unit, _e) do + defp size_and_unit(_expr_type, size, unit) do add_arg(:unit, unit, add_arg(:size, size, [])) end - defp build_spec(_meta, size, unit, type, endianness, sign, spec, _e) + defp build_spec(size, unit, type, endianness, sign, spec) when type in [:utf8, :utf16, :utf32] do - cond do - size != :default or unit != :default -> - # function_error(meta, e, __MODULE__, :bittype_utf) - :ok - - sign != :default -> - # function_error(meta, e, __MODULE__, :bittype_signed) - :ok - - true -> - :ok - end + # elixir raises bittype_signed if signed + # elixir raises bittype_utf if size specified + # we don't care add_spec(type, add_spec(endianness, spec)) end - defp build_spec(_meta, _size, unit, type, _endianness, sign, spec, _e) + defp build_spec(_size, unit, type, _endianness, sign, spec) when type in [:binary, :bitstring] do - cond do - type == :bitstring and unit != :default and unit != 1 -> - # function_error(meta, e, __MODULE__, {:bittype_mismatch, unit, 1, :unit}) - :ok - - sign != :default -> - # function_error(meta, e, __MODULE__, :bittype_signed) - :ok - - true -> - :ok - end + # elixir raises bittype_signed if signed + # elixir raises bittype_mismatch if bitstring unit != 1 or default + # we don't care add_spec(type, spec) end - defp build_spec(_meta, size, unit, type, endianness, sign, spec, _e) + defp build_spec(size, unit, type, endianness, sign, spec) when type in [:integer, :float] do number_size = number_size(size, unit) @@ -3581,13 +3512,15 @@ defmodule ElixirSense.Core.Compiler do if valid_float_size(number_size) do add_spec(type, add_spec(endianness, add_spec(sign, spec))) else - # function_error(meta, e, __MODULE__, {:bittype_float_size, number_size}) - [] + # elixir raises here bittype_float_size + # we fall back to 64 + build_spec(64, :default, type, endianness, sign, spec) end size == :default and unit != :default -> - # function_error(meta, e, __MODULE__, :bittype_unit) - [] + # elixir raises here bittype_unit + # we fall back to default + build_spec(size, :default, type, endianness, sign, spec) true -> add_spec(type, add_spec(endianness, add_spec(sign, spec))) @@ -3611,19 +3544,6 @@ defmodule ElixirSense.Core.Compiler do defp is_match_size([_ | _], %{context: :match}), do: true defp is_match_size(_, _), do: false - - defp find_match([{:=, _, [_left, _right]} = expr | _rest]), do: expr - - defp find_match([{_, _, args} | rest]) when is_list(args) do - case find_match(args) do - false -> find_match(rest) - match -> match - end - end - - defp find_match([_arg | rest]), do: find_match(rest) - - defp find_match([]), do: false end defmodule Fn do diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index 6a9a1fa9..b0b39b45 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -1001,4 +1001,146 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert {meta, env} = get_cursor_env(code) end end + + describe "bitstring" do + test "no size specifier with unit" do + code = """ + <>)::32, \ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "unsized" do + code = """ + <> = \ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "bad argument" do + code = """ + <<"foo"::size(8)-unit(:oops), \ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "undefined" do + code = """ + <<1::unknown(), \ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "unknown" do + code = """ + <<1::refb_spec, \ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "invalid literal" do + code = """ + <<:ok, \ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "nested match" do + code = """ + <> = \ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "incomplete" do + code = """ + <<\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "incomplete ::" do + code = """ + <<1::\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "incomplete -" do + code = """ + <<1::binary-\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "incomplete open parens" do + code = """ + <<1::size(\ + """ + assert {meta, env} = get_cursor_env(code) + end + end end From 8e3a36f072ee3da67dab218059887bf84bbcd43f Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 16 May 2024 08:03:09 +0200 Subject: [PATCH 049/235] quote unquote error recovery --- lib/elixir_sense/core/compiler.ex | 75 ++++++++--- .../metadata_builder/error_recovery_test.exs | 125 ++++++++++++++++++ 2 files changed, 183 insertions(+), 17 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index c17e9a36..4e6f8c68 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -312,9 +312,13 @@ defmodule ElixirSense.Core.Compiler do # Quote - defp do_expand({unquote_call, _meta, [_]}, _s, _e) - when unquote_call in [:unquote, :unquote_splicing], - do: raise("unquote_outside_quote") + defp do_expand({unquote_call, meta, [arg]}, s, e) + when unquote_call in [:unquote, :unquote_splicing] do + # elixir raises here unquote_outside_quote + # we may have cursor there + {arg, s, e} = expand(arg, s, e) + {{unquote_call, meta, [arg]}, s, e} + end defp do_expand({:quote, meta, [opts]}, s, e) when is_list(opts) do case Keyword.fetch(opts, :do) do @@ -323,17 +327,27 @@ defmodule ElixirSense.Core.Compiler do expand({:quote, meta, [new_opts, [{:do, do_block}]]}, s, e) :error -> - raise "missing_option" + # elixir raises here missing_option + # generate a fake do block + expand({:quote, meta, [opts, [{:do, {:__block__, [], []}}]]}, s, e) end end - defp do_expand({:quote, _meta, [_]}, _s, _e), do: raise("invalid_args") + defp do_expand({:quote, meta, [arg]}, s, e) do + # elixir raises here invalid_args + # we may have cursor there + {arg, s, e} = expand(arg, s, e) + {{:quote, meta, [arg]}, s, e} + end defp do_expand({:quote, meta, [opts, do_block]}, s, e) when is_list(do_block) do exprs = case Keyword.fetch(do_block, :do) do {:ok, expr} -> expr - :error -> raise "missing_option" + :error -> + # elixir raises here missing_option + # try to recover from error by generating a fake do block + {:__block__, [], [do_block]} end valid_opts = [:context, :location, :line, :file, :unquote, :bind_quoted, :generated] @@ -350,10 +364,12 @@ defmodule ElixirSense.Core.Compiler do {binding, default_unquote} = case Keyword.fetch(e_opts, :bind_quoted) do {:ok, bq} -> - if is_list(bq) and Enum.all?(bq, &match?({key, _} when is_atom(key), &1)) do + if is_list(bq) do + # TODO check if there's cursor? + bq = Enum.filter(bq, &match?({key, _} when is_atom(key), &1)) {bq, false} else - raise "invalid_bind_quoted_for_quote" + {[], false} end :error -> @@ -374,7 +390,11 @@ defmodule ElixirSense.Core.Compiler do expand(quoted, st, et) end - defp do_expand({:quote, _meta, [_, _]}, _s, _e), do: raise("invalid_args") + defp do_expand({:quote, meta, [arg1, arg2]}, s, e) do + # elixir raises here invalid_args + # try to recover from error by wrapping arg in a do block + expand({:quote, meta, [arg1, [{:do, {:__block__, [], [arg2]}}]]}, s, e) + end # Functions @@ -2280,6 +2300,7 @@ defmodule ElixirSense.Core.Compiler do end defp sanitize_opts(allowed, opts) when is_list(opts) do + # TODO check if there's cursor for {key, value} <- opts, Enum.member?(allowed, key), do: {key, value} end @@ -3841,8 +3862,8 @@ defmodule ElixirSense.Core.Compiler do {e_file, acc2} = validate_compile(meta, :file, file, acc1) {e_context, acc3} = validate_compile(meta, :context, context, acc2) - validate_runtime(:unquote, unquote) - validate_runtime(:generated, generated) + unquote = validate_runtime(:unquote, unquote) + generated = validate_runtime(:generated, generated) q = %__MODULE__{ line: e_line, @@ -3881,8 +3902,8 @@ defmodule ElixirSense.Core.Compiler do value false -> - raise ArgumentError, - "invalid runtime value for option :#{Atom.to_string(key)} in quote, got: #{inspect(value)}" + # elixir raises here invalid runtime value for option + default(key) end end @@ -3891,6 +3912,8 @@ defmodule ElixirSense.Core.Compiler do def is_valid(:context, context), do: is_atom(context) and context != nil def is_valid(:generated, generated), do: is_boolean(generated) def is_valid(:unquote, unquote), do: is_boolean(unquote) + defp default(:unquote), do: true + defp default(:generated), do: false def escape(expr, kind, unquote) do do_quote( @@ -3907,8 +3930,11 @@ defmodule ElixirSense.Core.Compiler do ) end - def quote(_meta, {:unquote_splicing, _, [_]}, _binding, %__MODULE__{unquote: true}, _, _), - do: raise("unquote_splicing only works inside arguments and block contexts") + def quote(meta, {:unquote_splicing, _, [_]} = expr, binding, %__MODULE__{unquote: true} = q, prelude, e) do + # elixir raises here unquote_splicing only works inside arguments and block contexts + # try to recover from error by wrapping it in block + quote(meta, {:__block__, [], [expr]}, binding, q, prelude, e) + end def quote(meta, expr, binding, q, prelude, e) do context = q.context @@ -3995,6 +4021,17 @@ defmodule ElixirSense.Core.Compiler do {:{}, [], [name, meta(import_meta, q), q.context]} end + # cursor + + defp do_quote( + {:__cursor__, meta, args}, + %__MODULE__{unquote: _} = q, + e + ) when is_list(args) do + # emit cursor as is regardless of unquote + {:__cursor__, meta, args} + end + # Unquote defp do_quote( @@ -4287,11 +4324,15 @@ defmodule ElixirSense.Core.Compiler do fun_to_quoted(fun) _ -> - raise ArgumentError + # elixir raises here ArgumentError + nil end end - defp do_escape(_other, _, _), do: raise(ArgumentError) + defp do_escape(_other, _, _) do + # elixir raises here ArgumentError + nil + end defp reverse_improper([h | t], acc), do: reverse_improper(t, [h | acc]) defp reverse_improper([], acc), do: acc diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index b0b39b45..4e7955ce 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -1143,4 +1143,129 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert {meta, env} = get_cursor_env(code) end end + + describe "quote/unquote/unquote_splicing" do + test "invalid bind quoted" do + code = """ + quote [bind_quoted: 123] do\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "incomplete 1" do + code = """ + quote \ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "incomplete 2" do + code = """ + quote [\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "incomplete 3" do + code = """ + quote [bind_quoted: \ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "incomplete 4" do + code = """ + quote [bind_quoted: [\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "incomplete 5" do + code = """ + quote [bind_quoted: [asd: \ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "incomplete 6" do + code = """ + quote [bind_quoted: [asd: 1]], \ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "incomplete 7" do + code = """ + quote [bind_quoted: [asd: 1]], [\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "incomplete 8" do + code = """ + quote :foo, [\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "in do block" do + code = """ + quote do + \ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "in do block unquote" do + code = """ + quote do + unquote(\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "in do block unquote_splicing" do + code = """ + quote do + unquote_splicing(\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "in do block unquote with bind_quoted" do + code = """ + quote bind_quoted: [a: 1] do + unquote(\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "unquote without quote" do + code = """ + unquote(\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "invalid compile option" do + code = """ + quote [file: 1] do\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "invalid runtime option" do + code = """ + quote [unquote: 1] do\ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "unquote_splicing not in block" do + code = """ + quote do: unquote_splicing(\ + """ + assert {meta, env} = get_cursor_env(code) + end + end end From 97f0ab532beaf3996c3489213e375f65635cfc22 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 16 May 2024 08:14:13 +0200 Subject: [PATCH 050/235] remove warnings --- lib/elixir_sense/core/compiler.ex | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 4e6f8c68..d993b62f 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -597,9 +597,7 @@ defmodule ElixirSense.Core.Compiler do {:ok, current_version} Map.has_key?(pre, pair) -> - # TODO: Enable this warning on Elixir v1.19 - # TODO: Remove me on Elixir 2.0 - # warn about unpinned bitsize var + # elixir plans to remove this case on 2.0 {:ok, current_version} not Map.has_key?(original, pair) -> @@ -629,17 +627,15 @@ defmodule ElixirSense.Core.Compiler do # TODO check if this can happen expand({name, meta, []}, s, e) - # TODO: Remove this clause on v2.0 as we will raise by default + # elixir plans to remove this clause on v2.0 {:ok, :raise} -> # TODO is it worth registering var access # function_error(meta, e, __MODULE__, {:undefined_var, name, kind}) {{name, meta, kind}, s, e} - # TODO: Remove this clause on v2.0 as we will no longer support warn + # elixir plans to remove this clause on v2.0 _ when error == :warn -> - # TODO is it worth registering var access - # Warn about undefined var to call - # elixir_errors:file_warn(Meta, E, ?MODULE, {undefined_var_to_call, Name}), + # TODO is it worth registering var access? expand({name, [{:if_undefined, :warn} | meta], []}, s, e) _ when error == :pin -> @@ -799,17 +795,11 @@ defmodule ElixirSense.Core.Compiler do {pid, s, e} _function -> - # TODO: Make me an error on v2.0 - # ElixirErrors.file_warn([], e, __MODULE__, {:invalid_pid_in_function, pid, function}) + # elixir plans to error here invalid_pid_in_function on 2.0 {pid, s, e} end end - # defp do_expand(0.0 = zero, s, %{context: :match} = e) when is_float(zero) do - # # ElixirErrors.file_warn([], e, __MODULE__, :invalid_match_on_zero_float) - # {zero, s, e} - # end - defp do_expand(other, s, e) when is_number(other) or is_atom(other) or is_binary(other) do {other, s, e} end @@ -2765,7 +2755,6 @@ defmodule ElixirSense.Core.Compiler do def guard(guard, s, e) do {e_guard, sg, eg} = ElixirExpand.expand(guard, s, e) - # warn_zero_length_guard(e_guard, eg) {e_guard, sg, eg} end @@ -3732,7 +3721,6 @@ defmodule ElixirSense.Core.Compiler do end defp escape({:&, meta, [pos]}, dict) when is_integer(pos) and pos > 0 do - # Using a nil context here to emit warnings when variable is unused. # This might pollute user space but is unlikely because variables # named :"&1" are not valid syntax. var = {:"&#{pos}", meta, nil} From d602c6cca0a331860e3599582c4a13a3109056d5 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 17 May 2024 08:00:02 +0200 Subject: [PATCH 051/235] handle a few more case/cond/receive/try cases --- lib/elixir_sense/core/compiler.ex | 48 +++++++--- .../metadata_builder/error_recovery_test.exs | 94 ++++++++++++++++++- 2 files changed, 127 insertions(+), 15 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index d993b62f..ee44d53d 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -2760,12 +2760,16 @@ defmodule ElixirSense.Core.Compiler do # case - def case(_meta, [], _s, _e) do - raise ArgumentError, "missing_option" + def case(meta, [], s, e) do + # elixir raises here missing_option + # emit a fake do block + case(meta, [do: []], s, e) end - def case(_meta, opts, _s, _e) when not is_list(opts) do - raise ArgumentError, "invalid_args" + def case(_meta, opts, s, e) when not is_list(opts) do + # elixir raises here invalid_args + # there may be cursor + ElixirExpand.expand(opts, s, e) end def case(meta, opts, s, e) do @@ -2786,12 +2790,16 @@ defmodule ElixirSense.Core.Compiler do # cond - def cond(_meta, [], _s, _e) do - raise ArgumentError, "missing_option" + def cond(meta, [], s, e) do + # elixir raises here missing_option + # emit a fake do block + cond(meta, [do: []], s, e) end - def cond(_meta, opts, _s, _e) when not is_list(opts) do - raise ArgumentError, "invalid_args" + def cond(_meta, opts, s, e) when not is_list(opts) do + # elixir raises here invalid_args + # there may be cursor + ElixirExpand.expand(opts, s, e) end def cond(meta, opts, s, e) do @@ -2812,12 +2820,16 @@ defmodule ElixirSense.Core.Compiler do # receive - def receive(_meta, [], _s, _e) do - raise ArgumentError, "missing_option" + def receive(meta, [], s, e) do + # elixir raises here missing_option + # emit a fake do block + receive(meta, [do: []], s, e) end - def receive(_meta, opts, _s, _e) when not is_list(opts) do - raise ArgumentError, "invalid_args" + def receive(_meta, opts, s, e) when not is_list(opts) do + # elixir raises here invalid_args + # there may be cursor + ElixirExpand.expand(opts, s, e) end def receive(meta, opts, s, e) do @@ -2920,8 +2932,16 @@ defmodule ElixirSense.Core.Compiler do # try - def try(_meta, [], _s, _e), do: raise("missing_option") - def try(_meta, opts, _s, _e) when not is_list(opts), do: raise("invalid_args") + def try(meta, [], s, e) do + # elixir raises here missing_option + # emit a fake do block + try(meta, [do: []], s, e) + end + def try(_meta, opts, s, e) when not is_list(opts) do + # elixir raises here invalid_args + # there may be cursor + ElixirExpand.expand(opts, s, e) + end def try(meta, opts, s, e) do opts = sanitize_opts(opts, [:do, :rescue, :catch, :else, :after]) diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index 4e7955ce..6a38f551 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -12,7 +12,30 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do end describe "incomplete case" do - test "cursor in argument" do + test "no arg 1" do + code = """ + case [] + \ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "no arg 2" do + code = """ + case [], [] + \ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "cursor in argument 1" do + code = """ + case [], \ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "cursor in argument 2" do code = """ x = 5 case \ @@ -70,6 +93,23 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do end describe "incomplete cond" do + test "no arg" do + code = """ + cond [] + \ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "cursor in arg" do + code = """ + x = foo() + cond \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + test "cursor in clause left side" do code = """ x = foo() @@ -111,6 +151,23 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do end describe "incomplete receive" do + test "no arg" do + code = """ + receive [] + \ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "cursor in arg" do + code = """ + x = foo() + receive \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + test "cursor in clause left side" do code = """ x = foo() @@ -193,6 +250,23 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do end describe "incomplete try" do + test "no arg" do + code = """ + try [] + \ + """ + assert {meta, env} = get_cursor_env(code) + end + + test "cursor in arg" do + code = """ + x = foo() + try \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + test "cursor in do block" do code = """ x = foo() @@ -365,6 +439,15 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do end describe "incomplete with" do + test "cursor in arg" do + code = """ + x = foo() + with [], \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + test "cursor in match expressions" do code = """ x = foo() @@ -444,6 +527,15 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do end describe "incomplete for" do + test "cursor in arg" do + code = """ + x = foo() + for [], \ + """ + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, & &1.name == :x) + end + test "cursor in generator match expressions" do code = """ x = foo() From 14da3fa0d7532d46cf3bcc6fba35be9662a4deab Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 17 May 2024 22:54:34 +0200 Subject: [PATCH 052/235] error recovery in calls --- lib/elixir_sense/core/compiler.ex | 188 ++++++------------ .../metadata_builder/error_recovery_test.exs | 118 +++++++++-- 2 files changed, 166 insertions(+), 140 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index ee44d53d..4db280f9 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -61,7 +61,10 @@ defmodule ElixirSense.Core.Compiler do __MODULE__.Bitstring.expand(meta, args, state, env, false) end - defp do_expand({:->, _meta, [_, _]}, _s, _e), do: raise("unhandled_arrow_op") + defp do_expand({:->, meta, [left, right]}, s, e) do + # elixir raises here unhandled_arrow_op + expand({:"__->__", meta, [left, right]}, s, e) + end defp do_expand({:"::", _meta, [_, _]}, _s, _e), do: raise("unhandled_type_op") @@ -532,7 +535,7 @@ defmodule ElixirSense.Core.Compiler do end end - defp do_expand({:^, meta, [arg]}, s, e) do + defp do_expand({:^, _meta, [arg]}, s, e) do # elixir raises here pin_outside_of_match # try to recover from error by dropping the pin and expanding arg expand(arg, s, e) @@ -656,7 +659,7 @@ defmodule ElixirSense.Core.Compiler do defp do_expand({fun, meta, args}, state, env) when is_atom(fun) and is_list(meta) and is_list(args) do - assert_no_ambiguous_op(fun, meta, args, state, env) + # elixir checks here id fall is not ambiguous arity = length(args) # TODO check if it works in our case @@ -724,38 +727,35 @@ defmodule ElixirSense.Core.Compiler do # Anonymous calls defp do_expand({{:., dot_meta, [expr]}, meta, args}, s, e) when is_list(args) do - assert_no_match_or_guard_scope(e.context, "anonymous call") {[e_expr | e_args], sa, ea} = expand_args([expr | args], s, e) - sa = - if is_atom(e_expr) do - # function_error(meta, e, __MODULE__, {:invalid_function_call, e_expr}) - sa - else - line = Keyword.get(dot_meta, :line, 0) - column = Keyword.get(dot_meta, :column, nil) - - column = - if column do - # for remote calls we emit position of right side of . - # to make it consistent we shift dot position here - column + 1 - else - column - end + # elixir validates if e_expr is not atom and raises invalid_function_call - sa - |> add_call_to_line({nil, e_expr, length(e_args)}, {line, column}) - |> add_current_env_to_line(line, e) + line = Keyword.get(dot_meta, :line, 0) + column = Keyword.get(dot_meta, :column, nil) + + column = + if column do + # for remote calls we emit position of right side of . + # to make it consistent we shift dot position here + column + 1 + else + column end + sa = sa + |> add_call_to_line({nil, e_expr, length(e_args)}, {line, column}) + |> add_current_env_to_line(line, e) + {{{:., dot_meta, [e_expr]}, meta, e_args}, sa, ea} end # Invalid calls - defp do_expand({_, meta, args} = invalid, _s, _e) when is_list(meta) and is_list(args) do - raise "invalid_call #{inspect(invalid)}" + defp do_expand({other, meta, args}, s, e) when is_list(meta) and is_list(args) do + # elixir raises invalid_call + {args, s, e} = expand_args(args, s, e) + {{other, meta, args}, s, e} end # Literals @@ -1818,8 +1818,6 @@ defmodule ElixirSense.Core.Compiler do defp expand_remote(receiver, dot_meta, right, meta, args, s, sl, %{context: context} = e) when is_atom(receiver) or is_tuple(receiver) do - assert_no_clauses(right, meta, args, e) - line = Keyword.get(meta, :line, 0) column = Keyword.get(meta, :column, nil) @@ -1832,10 +1830,8 @@ defmodule ElixirSense.Core.Compiler do end if context == :guard and is_tuple(receiver) do - if Keyword.get(meta, :no_parens) != true do - raise "parens_map_lookup" - end - + # elixir raises parens_map_lookup unless no_parens is set in meta + # TODO there may be cursor in discarded args {{{:., dot_meta, [receiver, right]}, meta, []}, sl, e} else attached_meta = attach_runtime_module(receiver, meta, s, e) @@ -1851,13 +1847,29 @@ defmodule ElixirSense.Core.Compiler do {rewritten, s, ea} {:error, _error} -> - raise "elixir_rewrite" + # elixir raises here elixir_rewrite + s = + __MODULE__.Env.close_write(sa, s) + |> add_call_to_line({receiver, right, length(e_args)}, {line, column}) + |> add_current_env_to_line(line, e) + {{{:., dot_meta, [receiver, right]}, attached_meta, e_args}, s, ea} end end end - defp expand_remote(receiver, dot_meta, right, meta, args, _, _, _e), - do: raise("invalid_call remote #{inspect({{:., dot_meta, [receiver, right]}, meta, args})}") + defp expand_remote(receiver, dot_meta, right, meta, args, s, sl, e) do + # elixir raises here invalid_call + {e_args, {sa, _}, ea} = map_fold(&expand_arg/3, {sl, s}, e, args) + + line = Keyword.get(meta, :line, 0) + column = Keyword.get(meta, :column, nil) + + s = + __MODULE__.Env.close_write(sa, s) + |> add_call_to_line({receiver, right, length(e_args)}, {line, column}) + |> add_current_env_to_line(line, e) + {{{:., dot_meta, [receiver, right]}, meta, e_args}, s, ea} + end defp attach_runtime_module(receiver, meta, s, _e) do if receiver in s.runtime_modules do @@ -1883,7 +1895,6 @@ defmodule ElixirSense.Core.Compiler do {:ok, :elixir_rewrite.rewrite(receiver, dot_meta, right, meta, e_args)} end - # This fixes exactly 1 test... defp expand_local(meta, :when, [_, _] = args, state, env = %{context: nil}) do # naked when, try to transform into a case ast = @@ -1906,29 +1917,10 @@ defmodule ElixirSense.Core.Compiler do expand(ast, state, env) end - defp expand_local(meta, fun, args, state, env = %{function: function}) when function != nil do - assert_no_clauses(fun, meta, args, env) - - if env.context in [:match, :guard] do - raise "invalid_local_invocation" - end - - line = Keyword.get(meta, :line, 0) - column = Keyword.get(meta, :column, nil) - - state = - state - |> add_call_to_line({nil, fun, length(args)}, {line, column}) - |> add_current_env_to_line(line, env) - - # state = update_in(state.locals, &[{fun, length(args)} | &1]) - {args, state, env} = expand_args(args, state, env) - {{fun, meta, args}, state, env} - end - defp expand_local(meta, fun, args, state, env) do - # elixir compiler raises here - # raise "undefined_function" + # elixir check if there are no clauses + # elixir raises here invalid_local_invocation if context is match or guard + # elixir compiler raises here undefined_function if env.function is nil line = Keyword.get(meta, :line, 0) column = Keyword.get(meta, :column, nil) @@ -1938,7 +1930,6 @@ defmodule ElixirSense.Core.Compiler do |> add_current_env_to_line(line, env) {args, state, env} = expand_args(args, state, env) - {{fun, meta, args}, state, env} end @@ -2172,7 +2163,7 @@ defmodule ElixirSense.Core.Compiler do defp expand_for_do_block(meta, [{:->, _, _} | _] = clauses, s, e, {:reduce, _}) do transformer = fn - {:->, clause_meta, [args, right]} = clause, sa -> + {:->, clause_meta, [args, right]}, sa -> # elixir checks here that clause has exactly 1 arg by matching against {_, _, [[_], _]} # we drop excessive or generate a fake arg # TODO check if there is cursor in dropped arg? @@ -2319,24 +2310,6 @@ defmodule ElixirSense.Core.Compiler do defp map_fold(_fun, s, e, [], acc), do: {Enum.reverse(acc), s, e} - defp assert_no_clauses(_name, _meta, [], _e), do: :ok - - defp assert_no_clauses(name, meta, args, e) do - assert_arg_with_no_clauses(name, meta, List.last(args), e) - end - - defp assert_arg_with_no_clauses(name, meta, [{key, value} | rest], e) when is_atom(key) do - case value do - [{:->, _, _} | _] -> - raise "invalid_clauses" - - _ -> - assert_arg_with_no_clauses(name, meta, rest, e) - end - end - - defp assert_arg_with_no_clauses(_name, _meta, _arg, _e), do: :ok - defp assert_module_scope(env, fun, arity) do case env.module do nil -> raise ArgumentError, "cannot invoke #{fun}/#{arity} outside module" @@ -2404,26 +2377,6 @@ defmodule ElixirSense.Core.Compiler do defp assert_no_underscore_clause_in_cond(_other, _e), do: :ok - defp assert_no_ambiguous_op(name, meta, [_arg], s, _e) do - case Keyword.fetch(meta, :ambiguous_op) do - {:ok, kind} -> - pair = {name, kind} - - case Map.get(s.vars, pair) do - nil -> - :ok - - _ -> - raise "op_ambiguity" - end - - _ -> - :ok - end - end - - defp assert_no_ambiguous_op(_atom, _meta, _args, _s, _e), do: :ok - defp refute_parallel_bitstring_match({:<<>>, _, _}, {:<<>>, _meta, _} = _arg, _e, true) do # file_error(meta, e, __MODULE__, {:parallel_bitstring_match, arg}) raise ArgumentError, "parallel_bitstring_match" @@ -2652,7 +2605,7 @@ defmodule ElixirSense.Core.Compiler do def has_cursor?(ast) do # TODO rewrite to lazy prewalker {_, result} = Macro.prewalk(ast, false, fn - {:__cursor__, _, list} = node, state when is_list(list) -> + {:__cursor__, _, list} = node, _state when is_list(list) -> {node, true} node, state -> {node, state} @@ -2881,7 +2834,7 @@ defmodule ElixirSense.Core.Compiler do s0 = ElixirEnv.reset_vars(s) {e_exprs, {s1, e1}} = Enum.map_reduce(exprs, {s0, e}, &expand_with/2) {e_do, opts1, s2} = expand_with_do(meta, opts0, s, s1, e1) - {e_opts, opts2, s3} = expand_with_else(meta, opts1, s2, e) + {e_opts, _opts2, s3} = expand_with_else(meta, opts1, s2, e) {{:with, meta, e_exprs ++ [[{:do, e_do} | e_opts]]}, s3, e} end @@ -3212,7 +3165,7 @@ defmodule ElixirSense.Core.Compiler do {e_right, e_alignment, ss, es} = expand_specs(e_type, meta, right, sl, original_s, el, expect_size) - e_acc = concat_or_prepend_bitstring(meta, e_left, e_right, acc, es, match_or_require_size) + e_acc = concat_or_prepend_bitstring(meta, e_left, e_right, acc) expand( bitstr_meta, @@ -3230,7 +3183,6 @@ defmodule ElixirSense.Core.Compiler do meta = extract_meta(h, bitstr_meta) {e_left, {ss, original_s}, es} = expand_expr(meta, h, fun, s, e) - match_or_require_size = require_size or is_match_size(t, es) e_type = expr_type(e_left) e_right = infer_spec(e_type, meta) @@ -3241,9 +3193,7 @@ defmodule ElixirSense.Core.Compiler do inferred_meta, e_left, e_right, - acc, - es, - match_or_require_size + acc ) expand(meta, fun, t, e_acc, {ss, original_s}, es, alignment, require_size) @@ -3410,16 +3360,14 @@ defmodule ElixirSense.Core.Compiler do defp expr_type({:<<>>, _, _}), do: :bitstring defp expr_type(_), do: :default - defp concat_or_prepend_bitstring(_meta, {:<<>>, _, []}, _e_right, acc, _e, _require_size), + defp concat_or_prepend_bitstring(_meta, {:<<>>, _, []}, _e_right, acc), do: acc defp concat_or_prepend_bitstring( meta, {:<<>>, parts_meta, parts} = e_left, e_right, - acc, - e, - require_size + acc ) do # elixir raises unsized_binary in some cases @@ -3439,7 +3387,7 @@ defmodule ElixirSense.Core.Compiler do end end - defp concat_or_prepend_bitstring(meta, e_left, e_right, acc, _e, _require_size) do + defp concat_or_prepend_bitstring(meta, e_left, e_right, acc) do [{:"::", meta, [e_left, e_right]} | acc] end @@ -3515,7 +3463,7 @@ defmodule ElixirSense.Core.Compiler do add_arg(:unit, unit, add_arg(:size, size, [])) end - defp build_spec(size, unit, type, endianness, sign, spec) + defp build_spec(_size, _unit, type, endianness, _sign, spec) when type in [:utf8, :utf16, :utf32] do # elixir raises bittype_signed if signed # elixir raises bittype_utf if size specified @@ -3524,7 +3472,7 @@ defmodule ElixirSense.Core.Compiler do add_spec(type, add_spec(endianness, spec)) end - defp build_spec(_size, unit, type, _endianness, sign, spec) + defp build_spec(_size, _unit, type, _endianness, _sign, spec) when type in [:binary, :bitstring] do # elixir raises bittype_signed if signed # elixir raises bittype_mismatch if bitstring unit != 1 or default @@ -3585,10 +3533,8 @@ defmodule ElixirSense.Core.Compiler do def expand(meta, clauses, s, e) when is_list(clauses) do transformer = fn - {:->, _, [left, _right]} = clause, sa -> + {:->, _, [_left, _right]} = clause, sa -> # elixir raises defaults_in_args - left = sanitize_fn_arg(left) - s_reset = ElixirEnv.reset_vars(sa) {e_clause, s_acc, e_acc} = @@ -3602,10 +3548,6 @@ defmodule ElixirSense.Core.Compiler do {{:fn, meta, e_clauses}, se, e} end - # TODO check if there is cursor in default - defp sanitize_fn_arg({:"\\\\", _, [value, _default]}), do: value - defp sanitize_fn_arg(value), do: value - # Capture def capture(meta, {:/, _, [{{:., _, [_m, f]} = dot, require_meta, []}, a]}, s, e) @@ -3787,7 +3729,7 @@ defmodule ElixirSense.Core.Compiler do end end - defp args_from_arity(_meta, a) do + defp args_from_arity(_meta, _a) do # elixir raises invalid_arity_for_capture [] end @@ -4033,8 +3975,8 @@ defmodule ElixirSense.Core.Compiler do defp do_quote( {:__cursor__, meta, args}, - %__MODULE__{unquote: _} = q, - e + %__MODULE__{unquote: _}, + _e ) when is_list(args) do # emit cursor as is regardless of unquote {:__cursor__, meta, args} @@ -4732,7 +4674,7 @@ defmodule ElixirSense.Core.Compiler do {{:when, meta, [spec, guard]}, state, env} end defp do_expand_spec(spec, state, env) do - {spec, guard, state, env} = do_expand_spec(spec, [], [], state, env) + {spec, _guard, state, env} = do_expand_spec(spec, [], [], state, env) {spec, state, env} end @@ -4747,7 +4689,7 @@ defmodule ElixirSense.Core.Compiler do guard = if Keyword.keyword?(guard), do: guard, else: [] - state = Enum.reduce(guard, state, fn {name, val}, state -> + state = Enum.reduce(guard, state, fn {name, _val}, state -> # guard is a keyword list so we don't have exact meta on keys add_var_write(state, {name, guard_meta, nil}) end) diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index 6a38f551..33547176 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -1241,63 +1241,63 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ quote [bind_quoted: 123] do\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "incomplete 1" do code = """ quote \ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "incomplete 2" do code = """ quote [\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "incomplete 3" do code = """ quote [bind_quoted: \ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "incomplete 4" do code = """ quote [bind_quoted: [\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "incomplete 5" do code = """ quote [bind_quoted: [asd: \ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "incomplete 6" do code = """ quote [bind_quoted: [asd: 1]], \ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "incomplete 7" do code = """ quote [bind_quoted: [asd: 1]], [\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "incomplete 8" do code = """ quote :foo, [\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "in do block" do @@ -1305,7 +1305,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do quote do \ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "in do block unquote" do @@ -1313,7 +1313,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do quote do unquote(\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "in do block unquote_splicing" do @@ -1321,7 +1321,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do quote do unquote_splicing(\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "in do block unquote with bind_quoted" do @@ -1329,35 +1329,119 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do quote bind_quoted: [a: 1] do unquote(\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "unquote without quote" do code = """ unquote(\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "invalid compile option" do code = """ quote [file: 1] do\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "invalid runtime option" do code = """ quote [unquote: 1] do\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "unquote_splicing not in block" do code = """ quote do: unquote_splicing(\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) + end + end + + describe "calls" do + test "invalid anonymous call" do + code = """ + :foo.(a, \ + """ + assert get_cursor_env(code) + end + + test "anonymous call in match" do + code = """ + a.() = \ + """ + assert get_cursor_env(code) + end + + test "anonymous call in guard" do + code = """ + case x do + y when a.() -> \ + """ + assert get_cursor_env(code) + end + + test "parens map lookup guard" do + code = """ + case x do + y when map.field() -> \ + """ + assert get_cursor_env(code) + end + + test "remote call in match" do + code = """ + Foo.bar() = \ + """ + assert get_cursor_env(code) + end + + test "invalid remote call" do + code = """ + __ENV__.line.foo \ + """ + assert get_cursor_env(code) + end + + test "clause in remote call" do + code = """ + Foo.foo do + a -> \ + """ + assert get_cursor_env(code) + end + + test "invalid local call" do + code = """ + 1.foo \ + """ + assert get_cursor_env(code) + end + + test "ambiguous local call" do + code = """ + a = 1 + a -1 .. \ + """ + assert get_cursor_env(code) + end + + test "clause in local call" do + code = """ + foo do + a -> \ + """ + assert get_cursor_env(code) + end + + test "local call in match" do + code = """ + bar() = \ + """ + assert get_cursor_env(code) end end end From 48f4ca1ab55f3074e42274ae22b1d869dc17d02b Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 17 May 2024 23:16:05 +0200 Subject: [PATCH 053/235] alias error recovery --- lib/elixir_sense/core/compiler.ex | 42 +++++++++++-------- .../metadata_builder/error_recovery_test.exs | 10 +++++ 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 4db280f9..d7adc22f 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -66,9 +66,15 @@ defmodule ElixirSense.Core.Compiler do expand({:"__->__", meta, [left, right]}, s, e) end - defp do_expand({:"::", _meta, [_, _]}, _s, _e), do: raise("unhandled_type_op") + defp do_expand({:"::", meta, [left, right]}, s, e) do + # elixir raises here unhandled_type_op + expand({:"__::__", meta, [left, right]}, s, e) + end - defp do_expand({:|, _meta, [_, _]}, _s, _e), do: raise("unhandled_cons_op") + defp do_expand({:|, meta, [left, right]}, s, e) do + # elixir raises here unhandled_cons_op + expand({:"__|__", meta, [left, right]}, s, e) + end # __block__ @@ -102,8 +108,8 @@ defmodule ElixirSense.Core.Compiler do # A compiler may want to emit a :local_function trace in here. {Module.concat([head | tail]), state, env} else - raise "invalid_alias" - # {{:__aliases__, meta, [head | tail]}, state, env} + # elixir raises here invalid_alias + {{:__aliases__, meta, [head | tail]}, state, env} end end end @@ -784,8 +790,9 @@ defmodule ElixirSense.Core.Compiler do {{:type, :external}, {:env, []}} -> {__MODULE__.Quote.fun_to_quoted(function), s, e} - other -> - raise "invalid_quoted_expr when expanding fun #{inspect(other)}" + _other -> + # elixir raises here invalid_quoted_expr + {nil, s, e} end end @@ -804,8 +811,9 @@ defmodule ElixirSense.Core.Compiler do {other, s, e} end - defp do_expand(other, _s, _e) do - raise "invalid_quoted_expr #{inspect(other)}" + defp do_expand(_other, s, e) do + # elixir raises here invalid_quoted_expr + {nil, s, e} end # Macro handling @@ -3826,15 +3834,15 @@ defmodule ElixirSense.Core.Compiler do {q, acc3} end - def validate_compile(_meta, :line, value, acc) when is_boolean(value) do + defp validate_compile(_meta, :line, value, acc) when is_boolean(value) do {value, acc} end - def validate_compile(_meta, :file, nil, acc) do + defp validate_compile(_meta, :file, nil, acc) do {nil, acc} end - def validate_compile(meta, key, value, acc) do + defp validate_compile(meta, key, value, acc) do case is_valid(key, value) do true -> {value, acc} @@ -3846,7 +3854,7 @@ defmodule ElixirSense.Core.Compiler do end end - def validate_runtime(key, value) do + defp validate_runtime(key, value) do case is_valid(key, value) do true -> value @@ -3857,11 +3865,11 @@ defmodule ElixirSense.Core.Compiler do end end - def is_valid(:line, line), do: is_integer(line) - def is_valid(:file, file), do: is_binary(file) - def is_valid(:context, context), do: is_atom(context) and context != nil - def is_valid(:generated, generated), do: is_boolean(generated) - def is_valid(:unquote, unquote), do: is_boolean(unquote) + defp is_valid(:line, line), do: is_integer(line) + defp is_valid(:file, file), do: is_binary(file) + defp is_valid(:context, context), do: is_atom(context) and context != nil + defp is_valid(:generated, generated), do: is_boolean(generated) + defp is_valid(:unquote, unquote), do: is_boolean(unquote) defp default(:unquote), do: true defp default(:generated), do: false diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index 33547176..38e67cb6 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -1444,4 +1444,14 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert get_cursor_env(code) end end + + describe "alias/import/require" do + test "invalid alias" do + code = """ + foo = :foo + foo.Foo.a(\ + """ + assert get_cursor_env(code) + end + end end From e19806f56dbf5243c03a98c6e94545e2642542f7 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 18 May 2024 08:25:13 +0200 Subject: [PATCH 054/235] error recovery in super --- lib/elixir_sense/core/compiler.ex | 55 ++-- .../metadata_builder/error_recovery_test.exs | 276 +++++++++++++----- 2 files changed, 234 insertions(+), 97 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index d7adc22f..13d0ba03 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -135,8 +135,6 @@ defmodule ElixirSense.Core.Compiler do end defp do_expand({:alias, meta, [arg, opts]}, state, env) do - assert_no_match_or_guard_scope(env.context, "alias") - state = state |> add_first_alias_positions(env, meta) @@ -165,7 +163,6 @@ defmodule ElixirSense.Core.Compiler do end defp do_expand({:require, meta, [arg, opts]}, state, env) do - assert_no_match_or_guard_scope(env.context, "require") original_env = env state = @@ -228,8 +225,6 @@ defmodule ElixirSense.Core.Compiler do end defp do_expand({:import, meta, [arg, opts]}, state, env) do - assert_no_match_or_guard_scope(env.context, "import") - state = state |> add_current_env_to_line(Keyword.fetch!(meta, :line), env) @@ -504,20 +499,24 @@ defmodule ElixirSense.Core.Compiler do # Super defp do_expand({:super, meta, args}, s, e) when is_list(args) do - assert_no_match_or_guard_scope(e.context, "super") arity = length(args) - {kind, name, _} = resolve_super(meta, arity, s, e) - {e_args, sa, ea} = expand_args(args, s, e) - - line = Keyword.get(meta, :line, 0) - column = Keyword.get(meta, :column, nil) - - sa = - sa - |> add_call_to_line({nil, name, arity}, {line, column}) - |> add_current_env_to_line(line, ea) + case resolve_super(meta, arity, s, e) do + {kind, name, _} -> + {e_args, sa, ea} = expand_args(args, s, e) + + line = Keyword.get(meta, :line, 0) + column = Keyword.get(meta, :column, nil) + + sa = + sa + |> add_call_to_line({nil, name, arity}, {line, column}) + |> add_current_env_to_line(line, ea) - {{:super, [{:super, {kind, name}} | meta], e_args}, sa, ea} + {{:super, [{:super, {kind, name}} | meta], e_args}, sa, ea} + _ -> + # elixir does not allow this branch + expand_local(meta, :super, args, s, e) + end end # Vars @@ -2046,10 +2045,11 @@ defmodule ElixirSense.Core.Compiler do defp overridable_name(name, count) when is_integer(count), do: :"#{name} (overridable #{count})" - defp resolve_super(_meta, arity, state, e) do - module = assert_module_scope(e) - function = assert_function_scope(e) - + defp resolve_super(_meta, _arity, _state, %{module: module, function: function}) when module == nil or function == nil do + # elixir asserts scope is function + nil + end + defp resolve_super(_meta, arity, state, %{module: module, function: function}) do case function do {name, ^arity} -> state.mods_funs_to_positions @@ -2081,12 +2081,14 @@ defmodule ElixirSense.Core.Compiler do {:defp, overridable_name(name, count), meta} end - nil -> - raise "no_super" + _ -> + # elixir raises here no_super + nil end _ -> - raise "wrong_number_of_args_for_super" + # elixir raises here wrong_number_of_args_for_super + nil end end @@ -2325,11 +2327,6 @@ defmodule ElixirSense.Core.Compiler do end end - defp assert_module_scope(%{module: nil}), do: raise("invalid_expr_in_scope") - defp assert_module_scope(%{module: module}), do: module - defp assert_function_scope(%{function: nil}), do: raise("invalid_expr_in_scope") - defp assert_function_scope(%{function: function}), do: function - defp assert_no_match_scope(context, _exp) do case context do :match -> diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index 38e67cb6..ec9af7e7 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -762,273 +762,273 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "local" do code = """ &foo\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "local slash no arity" do code = """ &foo/\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "local slash arity" do code = """ &foo/1\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "local slash invalid arity" do code = """ &foo/1000; \ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "local dot" do code = """ &foo.\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "local dot call" do code = """ &foo.(\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "local dot call closed" do code = """ &foo.()\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "local dot right" do code = """ &foo.bar\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "remote" do code = """ &Foo\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "remote dot" do code = """ &Foo.\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "remote dot right" do code = """ &Foo.bar\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "remote dot right no arity" do code = """ &Foo.bar/\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "remote dot right arity" do code = """ &Foo.bar/1\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "remote dot call" do code = """ &Foo.bar(\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "remote dot call closed" do code = """ &Foo.bar()\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "tuple" do code = """ &{\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "tuple closed" do code = """ &{}\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "list" do code = """ &[\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "list closed" do code = """ &[]\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "bitstring" do code = """ &<<\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "bitstring closed" do code = """ &<<>>\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "map no braces" do code = """ &%\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "map" do code = """ &%{\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "map closed" do code = """ &%{}\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "struct no braces" do code = """ &%Foo\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "struct" do code = """ &%Foo{\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "struct closed" do code = """ &%Foo{}\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "block" do code = """ & (\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "block multiple expressions" do code = """ & (:ok; \ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "arg var incomplete" do code = """ & &\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "arg var" do code = """ & &2\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "arg var in list" do code = """ &[&1, \ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "arg var in list without predecessor" do code = """ &[&2, \ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "no arg" do code = """ &{}; \ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end - test "invalid arg nuber" do + test "invalid arg number" do code = """ & &0; \ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "outside of capture" do code = """ &1; \ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "invalid arg local" do code = """ &foo; \ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "invalid arg" do code = """ &"foo"; \ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end end @@ -1037,14 +1037,14 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ ^\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "cursor in match" do code = """ ^__cursor__() = x\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end end @@ -1053,21 +1053,21 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ %{foo => x} = x\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "update in match" do code = """ %{a | x: __cursor__()} = x\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "cursor in place of key value pair" do code = """ %{a: "123", \ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end end @@ -1076,21 +1076,21 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ %\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "invalid map name" do code = """ %foo{\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "invalid key" do code = """ %Foo{"asd" => [\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end end @@ -1106,133 +1106,133 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ <<12.3::32*4, \ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "signed binary" do code = """ <>)::32, \ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "unsized" do code = """ <> = \ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "bad argument" do code = """ <<"foo"::size(8)-unit(:oops), \ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "undefined" do code = """ <<1::unknown(), \ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "unknown" do code = """ <<1::refb_spec, \ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "invalid literal" do code = """ <<:ok, \ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "nested match" do code = """ <> = \ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "incomplete" do code = """ <<\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "incomplete ::" do code = """ <<1::\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "incomplete -" do code = """ <<1::binary-\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end test "incomplete open parens" do code = """ <<1::size(\ """ - assert {meta, env} = get_cursor_env(code) + assert get_cursor_env(code) end end @@ -1446,12 +1446,152 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do end describe "alias/import/require" do - test "invalid alias" do + test "invalid alias expansion" do code = """ foo = :foo foo.Foo.a(\ """ assert get_cursor_env(code) end + + test "incomplete" do + code = """ + alias \ + """ + assert get_cursor_env(code) + + code = """ + require \ + """ + assert get_cursor_env(code) + + code = """ + import \ + """ + assert get_cursor_env(code) + end + + test "invalid" do + code = """ + alias A.a\ + """ + assert get_cursor_env(code) + + code = """ + require A.a\ + """ + assert get_cursor_env(code) + + code = """ + import A.a\ + """ + assert get_cursor_env(code) + end + + test "in options" do + code = """ + alias A.B, \ + """ + assert get_cursor_env(code) + + code = """ + require A.B, \ + """ + assert get_cursor_env(code) + + code = """ + import A.B, \ + """ + assert get_cursor_env(code) + end + + test "in option" do + code = """ + alias A.B, warn: \ + """ + assert get_cursor_env(code) + + code = """ + require A.B, warn: \ + """ + assert get_cursor_env(code) + + code = """ + import A.B, warn: \ + """ + assert get_cursor_env(code) + end + end + + describe "super" do + test "call outside module" do + code = """ + super(\ + """ + assert get_cursor_env(code) + end + + test "call outside function" do + code = """ + defmodule A do + super(\ + """ + assert get_cursor_env(code) + end + + test "call in match" do + code = """ + super() = \ + """ + assert get_cursor_env(code) + end + + test "capture expression outside module" do + code = """ + & super(&1, \ + """ + assert get_cursor_env(code) + end + + test "capture outside module" do + code = """ + &super\ + """ + assert get_cursor_env(code) + + code = """ + &super/\ + """ + assert get_cursor_env(code) + + code = """ + &super/1 \ + """ + assert get_cursor_env(code) + + code = """ + (&super/1) +\ + """ + assert get_cursor_env(code) + end + + test "call wrong args" do + code = """ + defmodule A do + def a do + super(\ + """ + assert get_cursor_env(code) + end + + test "call no super" do + code = """ + defmodule A do + def a(1), do: :ok + def a(x) do + super(x) +\ + """ + assert get_cursor_env(code) + end end end From cb9c055c313ce60327269d41a89b35244528ef4d Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 18 May 2024 11:11:21 +0200 Subject: [PATCH 055/235] fix typo --- lib/elixir_sense/core/compiler.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 13d0ba03..52e45125 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -4074,7 +4074,7 @@ defmodule ElixirSense.Core.Compiler do defp do_quote(other, _, _), do: other defp import_meta(meta, name, arity, q, e) do - case Keyword.get(meta, :import, false) == false && + case Keyword.get(meta, :imports, false) == false && ElixirDispatch.find_imports(meta, name, e) do [] -> case arity == 1 && Keyword.fetch(meta, :ambiguous_op) do From 461e118309959bc057f1cb2df6846bf930b346ec Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 18 May 2024 11:11:39 +0200 Subject: [PATCH 056/235] fix inverted condition --- lib/elixir_sense/core/compiler.ex | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 52e45125..e2150197 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -4360,14 +4360,14 @@ defmodule ElixirSense.Core.Compiler do function = e.function # TODO the condition has this at the end - # and not ElixirDef.local_for(meta, name, arity, [:defmacro, :defmacrop], e) + # and ElixirDef.local_for(meta, name, arity, [:defmacro, :defmacrop], e) if function != nil and function != tuple do + false + else # ElixirEnv.trace({:local_function, meta, name, arity}, e) # ElixirLocals.record_local(tuple, e.module, function, meta, false) # TODO we may want to record {:local, name, arity} - else - false end end end @@ -4376,19 +4376,15 @@ defmodule ElixirSense.Core.Compiler do def require_function(meta, receiver, name, arity, e) do required = receiver in e.requires - case is_macro(name, arity, receiver, required) do - true -> - false - - false -> - # ElixirEnv.trace({:remote_function, meta, receiver, name, arity}, e) - remote_function(meta, receiver, name, arity, e) + if is_macro(name, arity, receiver, required) do + false + else + # ElixirEnv.trace({:remote_function, meta, receiver, name, arity}, e) + remote_function(meta, receiver, name, arity, e) end end defp remote_function(_meta, receiver, name, arity, _e) do - # check_deprecated(:function, meta, receiver, name, arity, e) - # TODO rewrite is safe to use as it does not emit traces and does not have side effects # but we may need to translate it anyway case :elixir_rewrite.inline(receiver, name, arity) do From bd254979caa76e2788d1f12218638b84039b7fd1 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 18 May 2024 11:12:03 +0200 Subject: [PATCH 057/235] recover one more capture error --- lib/elixir_sense/core/compiler.ex | 5 +---- .../core/metadata_builder/error_recovery_test.exs | 8 ++++++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index e2150197..e1d24e43 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -2111,11 +2111,8 @@ defmodule ElixirSense.Core.Compiler do {{:&, meta, [{:/, [], [{{:., dot_meta, [remote, fun]}, attached_meta, []}, arity]}]}, se, ee} - {{:local, _fun, _arity}, _, _, _se, %{function: nil}} -> - # TODO register call? - raise "undefined_local_capture" - {{:local, fun, arity}, local_meta, _, se, ee} -> + # elixir raises undefined_local_capture if ee.function is nil line = Keyword.get(local_meta, :line, 0) column = Keyword.get(local_meta, :column, nil) diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index ec9af7e7..5ff03023 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -1030,6 +1030,14 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert get_cursor_env(code) end + + test "undefined local capture" do + code = """ + defmodule A do + (&asdf/1) +\ + """ + assert get_cursor_env(code) + end end describe "pin" do From a123534dcf0d7709f5df05110a8d231e07104a12 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 18 May 2024 11:56:52 +0200 Subject: [PATCH 058/235] ambiguous calls --- lib/elixir_sense/core/compiler.ex | 13 +++++++++---- .../metadata_builder/error_recovery_test.exs | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index e1d24e43..2c936d03 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -671,6 +671,8 @@ defmodule ElixirSense.Core.Compiler do # If we are inside a function, we support reading from locals. allow_locals = match?({n, a} when fun != n or arity != a, env.function) + # TODO this crashes with CompileError ambiguous_call + dbg({meta, fun, arity}) case Macro.Env.expand_import(env, meta, fun, arity, trace: false, allow_locals: allow_locals, @@ -3562,7 +3564,7 @@ defmodule ElixirSense.Core.Compiler do def capture(meta, {:/, _, [{f, import_meta, c}, a]}, s, e) when is_atom(f) and is_integer(a) and is_atom(c) do args = args_from_arity(meta, a) - capture_import({f, import_meta, args}, s, e, true) + capture_import({f |> dbg, import_meta, args}, s, e, true) end def capture(_meta, {{:., _, [_, fun]}, _, args} = expr, s, e) @@ -4403,7 +4405,8 @@ defmodule ElixirSense.Core.Compiler do find_imports_by_name(name, imports, Map.put(acc, arity, mod), mod, meta, e) _other_mod -> - raise "ambiguous_call" + # elixir raises here ambiguous_call + find_imports_by_name(name, imports, acc, mod, meta, e) end end @@ -4435,8 +4438,10 @@ defmodule ElixirSense.Core.Compiler do {[], []} -> false - _ -> - raise "ambiguous_call" + {_, [receiver]} -> + # elixir raises ambiguous_call + # we prefer macro + {:macro, receiver} end end end diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index 5ff03023..caf32bcd 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -1038,6 +1038,15 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert get_cursor_env(code) end + + test "ambiguous local" do + code = """ + defmodule Kernel.ErrorsTest.FunctionImportConflict do + import :erlang, only: [exit: 1], warn: false + def foo, do: (&exit/1) +\ + """ + assert get_cursor_env(code) + end end describe "pin" do @@ -1451,6 +1460,15 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert get_cursor_env(code) end + + test "ambiguous call" do + code = """ + defmodule Kernel.ErrorsTest.FunctionImportConflict do + import :erlang, only: [exit: 1], warn: false + def foo, do: exit(\ + """ + assert get_cursor_env(code) + end end describe "alias/import/require" do From 5df54f42d90d851a1e8cca470660068197b4ab7f Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 19 May 2024 08:14:57 +0200 Subject: [PATCH 059/235] resolve todos --- lib/elixir_sense/core/compiler.ex | 40 ++++++++++--------- test/elixir_sense/core/compiler_test.exs | 22 +++++++++- .../metadata_builder/error_recovery_test.exs | 13 ++++++ 3 files changed, 56 insertions(+), 19 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 2c936d03..7ad44288 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -672,14 +672,13 @@ defmodule ElixirSense.Core.Compiler do allow_locals = match?({n, a} when fun != n or arity != a, env.function) # TODO this crashes with CompileError ambiguous_call - dbg({meta, fun, arity}) case Macro.Env.expand_import(env, meta, fun, arity, trace: false, allow_locals: allow_locals, check_deprecations: false ) do {:macro, module, callback} -> - # TODO there is a subtle difference - callback will call expander with state derrived from env via + # TODO there is a subtle difference - callback will call expander with state derived from env via # :elixir_env.env_to_ex(env) possibly losing some details # line = Keyword.get(meta, :line, 0) # column = Keyword.get(meta, :column, nil) @@ -3564,7 +3563,7 @@ defmodule ElixirSense.Core.Compiler do def capture(meta, {:/, _, [{f, import_meta, c}, a]}, s, e) when is_atom(f) and is_integer(a) and is_atom(c) do args = args_from_arity(meta, a) - capture_import({f |> dbg, import_meta, args}, s, e, true) + capture_import({f, import_meta, args}, s, e, true) end def capture(_meta, {{:., _, [_, fun]}, _, args} = expr, s, e) @@ -3624,29 +3623,34 @@ defmodule ElixirSense.Core.Compiler do end defp capture_import({atom, import_meta, args} = expr, s, e, sequential) do - # TODO check similarity to macro expand_import - res = sequential && ElixirDispatch.import_function(import_meta, atom, length(args), e) + res = if sequential do + ElixirDispatch.import_function(import_meta, atom, length(args), e) + else + false + end + |> dbg handle_capture(res, import_meta, import_meta, expr, s, e, sequential) end - defp capture_require({{:., dot_meta, [left, right]}, require_meta, args}, s, e, sequential) do + defp capture_require({{:., dot_meta, [left, right]}, require_meta, args} = o, s, e, sequential) do case escape(left, []) do {esc_left, []} -> {e_left, se, ee} = ElixirExpand.expand(esc_left, s, e) - res = - sequential && - case e_left do - {name, _, context} when is_atom(name) and is_atom(context) -> - {:remote, e_left, right, length(args)} + res = if sequential do + case e_left do + {name, _, context} when is_atom(name) and is_atom(context) -> + {:remote, e_left, right, length(args)} - _ when is_atom(e_left) -> - # TODO check similarity to macro expand_require - ElixirDispatch.require_function(require_meta, e_left, right, length(args), ee) + _ when is_atom(e_left) -> + ElixirDispatch.require_function(require_meta, e_left, right, length(args), ee) - _ -> - false - end + _ -> + false + end + else + false + end dot = {{:., dot_meta, [e_left, right]}, require_meta, args} handle_capture(res, require_meta, dot_meta, dot, se, ee, sequential) @@ -3740,7 +3744,7 @@ defmodule ElixirSense.Core.Compiler do defp is_sequential_and_not_empty([]), do: false defp is_sequential_and_not_empty(list), do: is_sequential(list, 1) - # TODO need to understand if we need it + defp is_sequential([{:&, _, [int]} | t], int), do: is_sequential(t, int + 1) defp is_sequential([], _int), do: true defp is_sequential(_, _int), do: false diff --git a/test/elixir_sense/core/compiler_test.exs b/test/elixir_sense/core/compiler_test.exs index 3df1e8e5..82f619ff 100644 --- a/test/elixir_sense/core/compiler_test.exs +++ b/test/elixir_sense/core/compiler_test.exs @@ -61,7 +61,7 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do {expanded, state, env} = expand(ast) dbg(expanded) - assert expanded == elixir_expanded + assert clean_capture_arg_position(expanded) == elixir_expanded assert env == elixir_env assert state_to_map(state) == elixir_ex_to_map(elixir_state) end @@ -397,6 +397,11 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do assert_expansion("&[&1 | &2]") assert_expansion("&inspect/1") assert_expansion("&Enum.count/1") + assert_expansion("a = %{}; &a.b(&1)") + assert_expansion("&Enum.count(&1)") + assert_expansion("&inspect(&1)") + assert_expansion("&Enum.map(&2, &1)") + assert_expansion("&inspect([&2, &1])") end test "expands fn" do @@ -749,5 +754,20 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do # assert env == elixir_env end end + + defp clean_capture_arg_position(ast) do + {ast, _} = Macro.prewalk(ast, nil, fn + {atom, meta, nil} = node, state when is_atom(atom) -> + # elixir does not store position meta, we need to clean it to make tests pass + meta = with "&" <> int <- to_string(atom), {_, ""} <- Integer.parse(int) do + meta |> Keyword.delete(:line) |> Keyword.delete(:column) + else + _ -> meta + end + {{atom, meta, nil}, state} + node, state -> {node, state} + end) + ast + end end end diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index caf32bcd..e9a61c35 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -1469,6 +1469,19 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert get_cursor_env(code) end + + # this is not crashing because we don't support local macros yet + test "conflicting macro" do + code = """ + defmodule Kernel.ErrorsTest.MacroLocalConflict do + def hello, do: 1 || 2 + defmacro _ || _, do: :ok + + defmacro _ && _, do: :error + def world, do: 1 && \ + """ + assert get_cursor_env(code) + end end describe "alias/import/require" do From 071a3e4b3e376845fb231fc926d5c696a6eb22c6 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 19 May 2024 08:27:38 +0200 Subject: [PATCH 060/235] cover more cases --- lib/elixir_sense/core/compiler.ex | 95 ++----------------- .../metadata_builder/error_recovery_test.exs | 25 +++++ 2 files changed, 32 insertions(+), 88 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 7ad44288..1b120167 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -30,11 +30,13 @@ defmodule ElixirSense.Core.Compiler do # dbg(sr) # dbg(e_right) {e_left, sl, el} = __MODULE__.Clauses.match(&expand/3, left, sr, s, er) + + # elixir raises parallel_bitstring_match if detected + # IO.inspect(sl.vars_info, label: "left") # dbg(e_left) # dbg(el.versioned_vars) # dbg(sl.vars) - refute_parallel_bitstring_match(e_left, e_right, e, Map.get(e, :context) == :match) # {{:=, meta, [e_left, e_right]}, sl, el |> Map.from_struct()} |> dbg(limit: :infinity) # el = el |> :elixir_env.with_vars(sl.vars |> elem(0)) # sl = sl |> add_current_env_to_line(Keyword.fetch!(meta, :line), el) @@ -454,7 +456,7 @@ defmodule ElixirSense.Core.Compiler do defp do_expand({:cond, meta, [opts]}, s, e) do assert_no_match_or_guard_scope(e.context, "cond") - assert_no_underscore_clause_in_cond(opts, e) + # elixir raises underscore_in_cond if the last clause is _ {e_clauses, sc, ec} = __MODULE__.Clauses.cond(meta, opts, s, e) {{:cond, meta, [e_clauses]}, sc, ec} end @@ -2350,7 +2352,9 @@ defmodule ElixirSense.Core.Compiler do defp assert_no_match_or_guard_scope(context, exp) do case context do :match -> - invalid_match!(exp) + raise ArgumentError, + "invalid expression in match, #{exp} is not allowed in patterns " <> + "such as function clauses, case clauses or on the left side of the = operator" :guard -> raise ArgumentError, @@ -2362,91 +2366,6 @@ defmodule ElixirSense.Core.Compiler do end end - defp invalid_match!(exp) do - raise ArgumentError, - "invalid expression in match, #{exp} is not allowed in patterns " <> - "such as function clauses, case clauses or on the left side of the = operator" - end - - defp assert_no_underscore_clause_in_cond([{:do, clauses}], _e) when is_list(clauses) do - case List.last(clauses) do - {:->, _meta, [[{:_, _, atom}], _]} when is_atom(atom) -> - raise ArgumentError, "underscore_in_cond" - - _other -> - :ok - end - end - - defp assert_no_underscore_clause_in_cond(_other, _e), do: :ok - - defp refute_parallel_bitstring_match({:<<>>, _, _}, {:<<>>, _meta, _} = _arg, _e, true) do - # file_error(meta, e, __MODULE__, {:parallel_bitstring_match, arg}) - raise ArgumentError, "parallel_bitstring_match" - end - - defp refute_parallel_bitstring_match(left, {:=, _meta, [match_left, match_right]}, e, parallel) do - refute_parallel_bitstring_match(left, match_left, e, true) - refute_parallel_bitstring_match(left, match_right, e, parallel) - end - - defp refute_parallel_bitstring_match(left = [_ | _], right = [_ | _], e, parallel) do - refute_parallel_bitstring_match_each(left, right, e, parallel) - end - - defp refute_parallel_bitstring_match({left1, left2}, {right1, right2}, e, parallel) do - refute_parallel_bitstring_match_each([left1, left2], [right1, right2], e, parallel) - end - - defp refute_parallel_bitstring_match({:tuple, _, args1}, {:tuple, _, args2}, e, parallel) do - refute_parallel_bitstring_match_each(args1, args2, e, parallel) - end - - defp refute_parallel_bitstring_match({:%{}, _, args1}, {:%{}, _, args2}, e, parallel) do - refute_parallel_bitstring_match_map_field(Enum.sort(args1), Enum.sort(args2), e, parallel) - end - - defp refute_parallel_bitstring_match({:%, _, [_, args]}, right, e, parallel) do - refute_parallel_bitstring_match(args, right, e, parallel) - end - - defp refute_parallel_bitstring_match(left, {:%, _, [_, args]}, e, parallel) do - refute_parallel_bitstring_match(left, args, e, parallel) - end - - defp refute_parallel_bitstring_match(_left, _right, _e, _parallel), do: :ok - - defp refute_parallel_bitstring_match_each([arg1 | rest1], [arg2 | rest2], e, parallel) do - refute_parallel_bitstring_match(arg1, arg2, e, parallel) - refute_parallel_bitstring_match_each(rest1, rest2, e, parallel) - end - - defp refute_parallel_bitstring_match_each(_list1, _list2, _e, _parallel), do: :ok - - defp refute_parallel_bitstring_match_map_field( - [{key, val1} | rest1], - [{key, val2} | rest2], - e, - parallel - ) do - refute_parallel_bitstring_match(val1, val2, e, parallel) - refute_parallel_bitstring_match_map_field(rest1, rest2, e, parallel) - end - - defp refute_parallel_bitstring_match_map_field( - [field1 | rest1] = args1, - [field2 | rest2] = args2, - e, - parallel - ) do - cond do - field1 > field2 -> refute_parallel_bitstring_match_map_field(args1, rest2, e, parallel) - true -> refute_parallel_bitstring_match_map_field(rest1, args2, e, parallel) - end - end - - defp refute_parallel_bitstring_match_map_field(_args1, _args2, _e, _parallel), do: :ok - defp var_context(meta, kind) do case Keyword.fetch(meta, :counter) do {:ok, counter} -> counter diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index e9a61c35..7c562aa9 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -1633,4 +1633,29 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert get_cursor_env(code) end end + + describe "var" do + test "_ in cond" do + code = """ + cond do + x -> x + _ -> \ + """ + assert get_cursor_env(code) + end + + test "_ outside of match" do + code = """ + {1, _, [\ + """ + assert get_cursor_env(code) + end + + test "parallel bitstring match" do + code = """ + <> = <> = \ + """ + assert get_cursor_env(code) + end + end end From 252ecb683de0f649eb44a6a769ebd02d6e09bff0 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 19 May 2024 09:09:38 +0200 Subject: [PATCH 061/235] do not validate scope --- lib/elixir_sense/core/compiler.ex | 47 ++----------------------------- 1 file changed, 2 insertions(+), 45 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 1b120167..81534a35 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -405,8 +405,6 @@ defmodule ElixirSense.Core.Compiler do # Functions defp do_expand({:&, meta, [{:super, super_meta, args} = expr]}, s, e) when is_list(args) do - assert_no_match_or_guard_scope(e.context, "&") - case resolve_super(meta, length(args), s, e) do {kind, name, _} when kind in [:def, :defp] -> expand_fn_capture(meta, {name, super_meta, args}, s, e) @@ -422,8 +420,6 @@ defmodule ElixirSense.Core.Compiler do e ) when is_atom(context) and is_integer(arity) do - assert_no_match_or_guard_scope(e.context, "&") - case resolve_super(meta, arity, s, e) do {kind, name, _} when kind in [:def, :defp] -> @@ -443,37 +439,31 @@ defmodule ElixirSense.Core.Compiler do end defp do_expand({:&, meta, [arg]}, s, e) do - assert_no_match_or_guard_scope(e.context, "&") expand_fn_capture(meta, arg, s, e) end defp do_expand({:fn, meta, pairs}, s, e) do - assert_no_match_or_guard_scope(e.context, "fn") __MODULE__.Fn.expand(meta, pairs, s, e) end # case/cond/try/receive defp do_expand({:cond, meta, [opts]}, s, e) do - assert_no_match_or_guard_scope(e.context, "cond") # elixir raises underscore_in_cond if the last clause is _ {e_clauses, sc, ec} = __MODULE__.Clauses.cond(meta, opts, s, e) {{:cond, meta, [e_clauses]}, sc, ec} end defp do_expand({:case, meta, [expr, options]}, s, e) do - assert_no_match_or_guard_scope(e.context, "case") expand_case(meta, expr, options, s, e) end defp do_expand({:receive, meta, [opts]}, s, e) do - assert_no_match_or_guard_scope(e.context, "receive") {e_clauses, sc, ec} = __MODULE__.Clauses.receive(meta, opts, s, e) {{:receive, meta, [e_clauses]}, sc, ec} end defp do_expand({:try, meta, [opts]}, s, e) do - assert_no_match_or_guard_scope(e.context, "try") {e_clauses, sc, ec} = __MODULE__.Clauses.try(meta, opts, s, e) {{:try, meta, [e_clauses]}, sc, ec} end @@ -481,7 +471,6 @@ defmodule ElixirSense.Core.Compiler do defp do_expand({:for, _, [_ | _]} = expr, s, e), do: expand_for(expr, s, e, true) defp do_expand({:with, meta, [_ | _] = args}, s, e) do - assert_no_match_or_guard_scope(e.context, "with") __MODULE__.Clauses.with(meta, args, s, e) end @@ -829,7 +818,6 @@ defmodule ElixirSense.Core.Compiler do state, env ) do - assert_no_match_or_guard_scope(env.context, :"def/2") module = assert_module_scope(env, :def, 2) {position, end_position} = extract_range(meta) @@ -875,7 +863,6 @@ defmodule ElixirSense.Core.Compiler do env ) do assert_module_scope(env, :@, 1) - unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") line = Keyword.fetch!(meta, :line) state = @@ -896,7 +883,6 @@ defmodule ElixirSense.Core.Compiler do env ) do assert_module_scope(env, :@, 1) - unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") line = Keyword.fetch!(meta, :line) state = @@ -927,7 +913,6 @@ defmodule ElixirSense.Core.Compiler do ) when doc in [:doc, :typedoc] do assert_module_scope(env, :@, 1) - unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") line = Keyword.fetch!(meta, :line) state = @@ -953,7 +938,6 @@ defmodule ElixirSense.Core.Compiler do env ) do assert_module_scope(env, :@, 1) - unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") line = Keyword.fetch!(meta, :line) state = @@ -980,7 +964,6 @@ defmodule ElixirSense.Core.Compiler do env ) do assert_module_scope(env, :@, 1) - unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") line = Keyword.fetch!(meta, :line) state = @@ -1006,7 +989,6 @@ defmodule ElixirSense.Core.Compiler do env ) do assert_module_scope(env, :@, 1) - unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") line = Keyword.fetch!(meta, :line) state = @@ -1032,7 +1014,6 @@ defmodule ElixirSense.Core.Compiler do env ) do current_module = assert_module_scope(env, :@, 1) - unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") line = Keyword.fetch!(meta, :line) column = Keyword.fetch!(meta, :column) @@ -1092,7 +1073,6 @@ defmodule ElixirSense.Core.Compiler do ) when kind in [:type, :typep, :opaque] do assert_module_scope(env, :@, 1) - unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") {expr, state, env} = __MODULE__.Typespec.expand_type(expr, state, env) @@ -1135,7 +1115,6 @@ defmodule ElixirSense.Core.Compiler do ) when kind in [:callback, :macrocallback, :spec] do assert_module_scope(env, :@, 1) - unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") {expr, state, env} = __MODULE__.Typespec.expand_spec(expr, state, env) @@ -1188,7 +1167,6 @@ defmodule ElixirSense.Core.Compiler do ) when is_atom(name) do assert_module_scope(env, :@, 1) - unless env.function, do: assert_no_match_or_guard_scope(env.context, "@/1") line = Keyword.fetch!(meta, :line) column = Keyword.get(meta, :column, 1) @@ -1316,7 +1294,6 @@ defmodule ElixirSense.Core.Compiler do env ) when call in [:defrecord, :defrecordp] do - assert_no_match_or_guard_scope(env.context, :"{call}/2") module = assert_module_scope(env, call, 2) {position = {line, column}, end_position} = extract_range(meta) @@ -1485,7 +1462,6 @@ defmodule ElixirSense.Core.Compiler do ) do %{vars: vars, unused: unused} = state original_env = env - assert_no_match_or_guard_scope(env.context, "defmodule/2") {expanded, _state, _env} = expand(alias, state, env) @@ -1605,7 +1581,6 @@ defmodule ElixirSense.Core.Compiler do when def_kind in [:def, :defp, :defmacro, :defmacrop, :defguard, :defguardp] do # dbg(call) # dbg(expr) - assert_no_match_or_guard_scope(env.context, :"{def_kind}/2") _module = assert_module_scope(env, def_kind, 2) %{vars: vars, unused: unused} = state @@ -2132,7 +2107,6 @@ defmodule ElixirSense.Core.Compiler do end defp expand_for({:for, meta, [_ | _] = args}, s, e, return) do - assert_no_match_or_guard_scope(e.context, "for") {cases, block} = __MODULE__.Utils.split_opts(args) block = sanitize_opts([:do, :into, :uniq, :reduce], block) @@ -2349,23 +2323,6 @@ defmodule ElixirSense.Core.Compiler do end end - defp assert_no_match_or_guard_scope(context, exp) do - case context do - :match -> - raise ArgumentError, - "invalid expression in match, #{exp} is not allowed in patterns " <> - "such as function clauses, case clauses or on the left side of the = operator" - - :guard -> - raise ArgumentError, - "invalid expression in guard, #{exp} is not allowed in guards. " <> - "To learn more about guards, visit: https://hexdocs.pm/elixir/patterns-and-guards.html" - - _ -> - :ok - end - end - defp var_context(meta, kind) do case Keyword.fetch(meta, :counter) do {:ok, counter} -> counter @@ -3547,11 +3504,11 @@ defmodule ElixirSense.Core.Compiler do else false end - |> dbg + handle_capture(res, import_meta, import_meta, expr, s, e, sequential) end - defp capture_require({{:., dot_meta, [left, right]}, require_meta, args} = o, s, e, sequential) do + defp capture_require({{:., dot_meta, [left, right]}, require_meta, args}, s, e, sequential) do case escape(left, []) do {esc_left, []} -> {e_left, se, ee} = ElixirExpand.expand(esc_left, s, e) From dfb831eb82ccd4f3f6b9472663ff3ac1f0aba939 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 19 May 2024 16:33:22 +0200 Subject: [PATCH 062/235] handle a few more cases --- lib/elixir_sense/core/compiler.ex | 50 +++++-------------- .../metadata_builder/error_recovery_test.exs | 42 ++++++++++++++++ 2 files changed, 55 insertions(+), 37 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 81534a35..177debce 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -2616,8 +2616,7 @@ defmodule ElixirSense.Core.Compiler do end defp expand_case(meta, {:do, _} = do_clause, s, e) do - fun = expand_head(meta, :case, :do) - expand_clauses(meta, :case, fun, do_clause, s, e) + expand_clauses(meta, :case, &head/3, do_clause, s, e) end # cond @@ -2646,8 +2645,7 @@ defmodule ElixirSense.Core.Compiler do end defp expand_cond(meta, {:do, _} = do_clause, s, e) do - fun = expand_one(meta, :cond, :do, &ElixirExpand.expand_args/3) - expand_clauses(meta, :cond, fun, do_clause, s, e) + expand_clauses(meta, :cond, &ElixirExpand.expand_args/3, do_clause, s, e) end # receive @@ -2680,13 +2678,11 @@ defmodule ElixirSense.Core.Compiler do end defp expand_receive(meta, {:do, _} = do_clause, s, e) do - fun = expand_head(meta, :receive, :do) - expand_clauses(meta, :receive, fun, do_clause, s, e) + expand_clauses(meta, :receive, &head/3, do_clause, s, e) end - defp expand_receive(meta, {:after, [_]} = after_clause, s, e) do - fun = expand_one(meta, :receive, :after, &ElixirExpand.expand_args/3) - expand_clauses(meta, :receive, fun, after_clause, s, e) + defp expand_receive(meta, {:after, [_ | _]} = after_clause, s, e) do + expand_clauses(meta, :receive, &ElixirExpand.expand_args/3, after_clause, s, e) end defp expand_receive(meta, {:after, expr}, s, e) when not is_list(expr) do @@ -2756,8 +2752,7 @@ defmodule ElixirSense.Core.Compiler do {expr, rest_opts} -> pair = {:else, expr} - fun = expand_head(meta, :with, :else) - {e_pair, se} = expand_clauses(meta, :with, fun, pair, s, e) + {e_pair, se} = expand_clauses(meta, :with, &head/3, pair, s, e) {[e_pair], rest_opts, se} end end @@ -2797,8 +2792,7 @@ defmodule ElixirSense.Core.Compiler do end defp expand_try(meta, {:else, _} = else_clause, s, e) do - fun = expand_head(meta, :try, :else) - expand_clauses(meta, :try, fun, else_clause, s, e) + expand_clauses(meta, :try, &head/3, else_clause, s, e) end defp expand_try(meta, {:catch, _} = catch_clause, s, e) do @@ -2913,29 +2907,6 @@ defmodule ElixirSense.Core.Compiler do end end - defp expand_head(_meta, _kind, _key) do - fn - [{:when, _, [args, _, _ | _]}], _, _e -> - raise ArgumentError, "wrong_number_of_args_for_clause #{inspect(args)}" - - [_] = args, s, e -> - head(args, s, e) - - args, _, _e -> - raise ArgumentError, "wrong_number_of_args_for_clause #{inspect(args)}" - end - end - - defp expand_one(_meta, _kind, _key, fun) do - fn - [_] = args, s, e -> - fun.(args, s, e) - - _, _, _e -> - raise ArgumentError, "wrong_number_of_args_for_clause" - end - end - defp expand_clauses(meta, kind, fun, clauses, s, e) do new_kind = origin(meta, kind) expand_clauses_origin(meta, new_kind, fun, clauses, s, e) @@ -4318,10 +4289,15 @@ defmodule ElixirSense.Core.Compiler do {[], []} -> false - {_, [receiver]} -> + {_, [receiver | _]} -> # elixir raises ambiguous_call # we prefer macro {:macro, receiver} + + {[receiver | _], _} -> + # elixir raises ambiguous_call + # we prefer macro + {:function, receiver} end end end diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index 7c562aa9..86bd7fdd 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -90,6 +90,20 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert {meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, & &1.name == :x) end + + test "invalid number of args with when" do + code = """ + case nil do 0, z when not is_nil(z) -> \ + """ + assert get_cursor_env(code) + end + + test "invalid number of args" do + code = """ + case nil do 0, z -> \ + """ + assert get_cursor_env(code) + end end describe "incomplete cond" do @@ -148,6 +162,13 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert {meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, & &1.name == :x) end + + test "invalid number of args" do + code = """ + cond do 0, z -> \ + """ + assert get_cursor_env(code) + end end describe "incomplete receive" do @@ -247,6 +268,27 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert {meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, & &1.name == :x) end + + test "invalid number of args in after" do + code = """ + receive do + a -> :ok + after + 0, z -> \ + """ + assert get_cursor_env(code) + end + + test "invalid number of clauses in after" do + code = """ + receive do + a -> :ok + after + 0 -> :ok + 1 -> \ + """ + assert get_cursor_env(code) + end end describe "incomplete try" do From 1d128be001dadf2faff6097a38e3af713d2cab5d Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 19 May 2024 16:40:44 +0200 Subject: [PATCH 063/235] a few more cases --- lib/elixir_sense/core/compiler.ex | 44 +++---------------- .../metadata_builder/error_recovery_test.exs | 15 +++++++ 2 files changed, 22 insertions(+), 37 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 177debce..80d843d7 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -25,7 +25,7 @@ defmodule ElixirSense.Core.Compiler do # =/2 defp do_expand({:=, meta, [left, right]}, s, e) do - assert_no_guard_scope(e.context, "=/2") + # elixir validates we are not in guard context {e_right, sr, er} = expand(right, s, e) # dbg(sr) # dbg(e_right) @@ -271,10 +271,7 @@ defmodule ElixirSense.Core.Compiler do end defp do_expand({:__CALLER__, meta, ctx} = caller, s, e) when is_atom(ctx) do - assert_no_match_scope(e.context, "__CALLER__") - # unless s.caller do - # function_error(meta, e, __MODULE__, :caller_not_allowed) - # end + # elixir checks if context is not match and if caller is allowed line = Keyword.get(meta, :line, 0) s = if line > 0, do: add_current_env_to_line(s, line, e), else: s @@ -282,10 +279,7 @@ defmodule ElixirSense.Core.Compiler do end defp do_expand({:__STACKTRACE__, meta, ctx} = stacktrace, s, e) when is_atom(ctx) do - assert_no_match_scope(e.context, "__STACKTRACE__") - # unless s.stacktrace do - # function_error(meta, e, __MODULE__, :stacktrace_not_allowed) - # end + # elixir checks if context is not match and if stacktrace is allowed line = Keyword.get(meta, :line, 0) s = if line > 0, do: add_current_env_to_line(s, line, e), else: s @@ -293,8 +287,7 @@ defmodule ElixirSense.Core.Compiler do end defp do_expand({:__ENV__, meta, ctx}, s, e) when is_atom(ctx) do - assert_no_match_scope(e.context, "__ENV__") - + # elixir checks if context is not match line = Keyword.get(meta, :line, 0) s = if line > 0, do: add_current_env_to_line(s, line, e), else: s @@ -303,8 +296,7 @@ defmodule ElixirSense.Core.Compiler do defp do_expand({{:., dot_meta, [{:__ENV__, meta, atom}, field]}, call_meta, []}, s, e) when is_atom(atom) and is_atom(field) do - assert_no_match_scope(e.context, "__ENV__") - + # elixir checks if context is not match line = Keyword.get(call_meta, :line, 0) s = if line > 0, do: add_current_env_to_line(s, line, e), else: s @@ -1182,8 +1174,8 @@ defmodule ElixirSense.Core.Compiler do [_] -> # @attribute(arg) - if env.function, do: raise("cannot set attribute @#{name} inside function/macro") - if name == :behavior, do: raise("@behavior attribute is not supported") + if env.function, do: raise "cannot set attribute @#{name} inside function/macro" + if name == :behavior, do: raise "@behavior attribute is not supported" {true, expand_args(args, state, env)} args -> @@ -2301,28 +2293,6 @@ defmodule ElixirSense.Core.Compiler do end end - defp assert_no_match_scope(context, _exp) do - case context do - :match -> - raise "invalid_pattern_in_match" - - _ -> - :ok - end - end - - defp assert_no_guard_scope(context, exp) do - case context do - :guard -> - raise ArgumentError, - "invalid expression in guard, #{exp} is not allowed in guards. " <> - "To learn more about guards, visit: https://hexdocs.pm/elixir/patterns-and-guards.html" - - _ -> - :ok - end - end - defp var_context(meta, kind) do case Keyword.fetch(meta, :counter) do {:ok, counter} -> counter diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index 86bd7fdd..78f03dfc 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -1699,5 +1699,20 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert get_cursor_env(code) end + + test "match in guard" do + code = """ + cond do + x when x = \ + """ + assert get_cursor_env(code) + end + + test "stacktrace in match" do + code = """ + __STACKTRACE__ = \ + """ + assert get_cursor_env(code) + end end end From b591ef5f25ae234976b1b92d119b99b3adc48fff Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 19 May 2024 16:47:04 +0200 Subject: [PATCH 064/235] remove asserts --- lib/elixir_sense/core/compiler.ex | 88 +++++++++++-------------------- 1 file changed, 30 insertions(+), 58 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 80d843d7..bd4f7edd 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -808,10 +808,8 @@ defmodule ElixirSense.Core.Compiler do [funs, opts], _callback, state, - env - ) do - module = assert_module_scope(env, :def, 2) - + env = %{module: module} + ) when module != nil do {position, end_position} = extract_range(meta) {line, _} = position @@ -852,9 +850,8 @@ defmodule ElixirSense.Core.Compiler do [{:behaviour, _meta, [arg]}], _callback, state, - env - ) do - assert_module_scope(env, :@, 1) + env = %{module: module} + ) when module != nil do line = Keyword.fetch!(meta, :line) state = @@ -872,9 +869,8 @@ defmodule ElixirSense.Core.Compiler do [{:moduledoc, doc_meta, [arg]}], _callback, state, - env - ) do - assert_module_scope(env, :@, 1) + env = %{module: module} + ) when module != nil do line = Keyword.fetch!(meta, :line) state = @@ -901,10 +897,9 @@ defmodule ElixirSense.Core.Compiler do [{doc, doc_meta, [arg]}], _callback, state, - env + env = %{module: module} ) - when doc in [:doc, :typedoc] do - assert_module_scope(env, :@, 1) + when doc in [:doc, :typedoc] and module != nil do line = Keyword.fetch!(meta, :line) state = @@ -927,9 +922,8 @@ defmodule ElixirSense.Core.Compiler do [{:impl, doc_meta, [arg]}], _callback, state, - env - ) do - assert_module_scope(env, :@, 1) + env = %{module: module} + ) when module != nil do line = Keyword.fetch!(meta, :line) state = @@ -953,9 +947,8 @@ defmodule ElixirSense.Core.Compiler do [{:optional_callbacks, doc_meta, [arg]}], _callback, state, - env - ) do - assert_module_scope(env, :@, 1) + env = %{module: module} + ) when module != nil do line = Keyword.fetch!(meta, :line) state = @@ -978,9 +971,8 @@ defmodule ElixirSense.Core.Compiler do [{:deprecated, doc_meta, [arg]}], _callback, state, - env - ) do - assert_module_scope(env, :@, 1) + env = %{module: module} + ) when module != nil do line = Keyword.fetch!(meta, :line) state = @@ -1003,10 +995,8 @@ defmodule ElixirSense.Core.Compiler do [{:derive, doc_meta, [derived_protos]}], _callback, state, - env - ) do - current_module = assert_module_scope(env, :@, 1) - + env = %{module: module} + ) when module != nil do line = Keyword.fetch!(meta, :line) column = Keyword.fetch!(meta, :column) @@ -1023,7 +1013,7 @@ defmodule ElixirSense.Core.Compiler do mod_any = Module.concat(proto_module, Any) # protocol implementation module built by @derive - mod = Module.concat(proto_module, current_module) + mod = Module.concat(proto_module, module) case acc.mods_funs_to_positions[{mod_any, nil, nil}] do nil -> @@ -1061,11 +1051,9 @@ defmodule ElixirSense.Core.Compiler do [{kind, kind_meta, [expr | _]}], _callback, state, - env + env = %{module: module} ) - when kind in [:type, :typep, :opaque] do - assert_module_scope(env, :@, 1) - + when kind in [:type, :typep, :opaque] and module != nil do {expr, state, env} = __MODULE__.Typespec.expand_type(expr, state, env) case __MODULE__.Typespec.type_to_signature(expr) do @@ -1103,11 +1091,9 @@ defmodule ElixirSense.Core.Compiler do [{kind, kind_meta, [expr | _]}], _callback, state, - env + env = %{module: module} ) - when kind in [:callback, :macrocallback, :spec] do - assert_module_scope(env, :@, 1) - + when kind in [:callback, :macrocallback, :spec] and module != nil do {expr, state, env} = __MODULE__.Typespec.expand_spec(expr, state, env) case __MODULE__.Typespec.spec_to_signature(expr) do @@ -1155,10 +1141,9 @@ defmodule ElixirSense.Core.Compiler do [{name, name_meta, args}], _callback, state, - env + env = %{module: module} ) - when is_atom(name) do - assert_module_scope(env, :@, 1) + when is_atom(name) and module != nil do line = Keyword.fetch!(meta, :line) column = Keyword.get(meta, :column, 1) @@ -1203,9 +1188,8 @@ defmodule ElixirSense.Core.Compiler do [arg], _callback, state, - env - ) do - assert_module_scope(env, :defoverridable, 1) + env = %{module: module} + ) when module != nil do {arg, state, env} = expand(arg, state, env) case arg do @@ -1236,11 +1220,9 @@ defmodule ElixirSense.Core.Compiler do [fields], _callback, state, - env + env = %{module: module} ) - when type in [:defstruct, :defexception] do - module = assert_module_scope(env, type, 1) - + when type in [:defstruct, :defexception] and module != nil do if Map.has_key?(state.structs, module) do raise ArgumentError, "defstruct has already been called for " <> @@ -1283,11 +1265,9 @@ defmodule ElixirSense.Core.Compiler do [name, _] = args, _callback, state, - env + env = %{module: module} ) - when call in [:defrecord, :defrecordp] do - module = assert_module_scope(env, call, 2) - + when call in [:defrecord, :defrecordp] and module != nil do {position = {line, column}, end_position} = extract_range(meta) type = @@ -1569,11 +1549,10 @@ defmodule ElixirSense.Core.Compiler do expand_macro(meta, Kernel, def_kind, [call, {:__block__, [], []}], callback, state, env) end - defp expand_macro(meta, Kernel, def_kind, [call, expr], _callback, state, env) + defp expand_macro(meta, Kernel, def_kind, [call, expr], _callback, state, env = %{module: module}) when module != nil when def_kind in [:def, :defp, :defmacro, :defmacrop, :defguard, :defguardp] do # dbg(call) # dbg(expr) - _module = assert_module_scope(env, def_kind, 2) %{vars: vars, unused: unused} = state @@ -2286,13 +2265,6 @@ defmodule ElixirSense.Core.Compiler do defp map_fold(_fun, s, e, [], acc), do: {Enum.reverse(acc), s, e} - defp assert_module_scope(env, fun, arity) do - case env.module do - nil -> raise ArgumentError, "cannot invoke #{fun}/#{arity} outside module" - mod -> mod - end - end - defp var_context(meta, kind) do case Keyword.fetch(meta, :counter) do {:ok, counter} -> counter From b5b8f75be6c138738d7220280fe9fd425dc6495c Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 22 May 2024 22:02:11 +0200 Subject: [PATCH 065/235] update to master changes --- lib/elixir_sense/core/compiler.ex | 12 ++++++-- .../core/metadata_builder_test.exs | 28 ++++++++----------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index bd4f7edd..003fb8c8 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -674,8 +674,13 @@ defmodule ElixirSense.Core.Compiler do # Transform to remote call - we may need to do rewrites expand({{:., meta, [module, fun]}, meta, args}, state, env) - :error -> + {:error, :not_found} -> expand_local(meta, fun, args, state, env) + + {:error, {:conflict, _module}} -> + raise "conflict" + {:error, {:ambiguous, _module}} -> + raise "ambiguous" end end @@ -1550,9 +1555,10 @@ defmodule ElixirSense.Core.Compiler do end defp expand_macro(meta, Kernel, def_kind, [call, expr], _callback, state, env = %{module: module}) when module != nil - when def_kind in [:def, :defp, :defmacro, :defmacrop, :defguard, :defguardp] do + and def_kind in [:def, :defp, :defmacro, :defmacrop, :defguard, :defguardp] do # dbg(call) # dbg(expr) + # dbg(def_kind) %{vars: vars, unused: unused} = state @@ -1598,7 +1604,7 @@ defmodule ElixirSense.Core.Compiler do case name_and_args do {n, m, a} when is_atom(n) and is_atom(a) -> {n, m, []} {n, m, a} when is_atom(n) and is_list(a) -> {n, m, a} - _ -> raise "invalid_def" + _ -> raise "invalid_def #{inspect(name_and_args)}" end {_e_args, state, a_env} = diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 9a3d3682..d4847829 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -58,7 +58,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert [ %VarInfo{name: :abc, positions: [{1, 1}]}, - %VarInfo{name: :abc, positions: [{1, 13}]}, + # %VarInfo{name: :abc, positions: [{1, 13}]}, %VarInfo{name: :cde, positions: [{1, 7}]} ] = state |> get_line_vars(2) end @@ -138,7 +138,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert Map.has_key?(state.lines_to_env[3].versioned_vars, {:abc, nil}) assert [ - %VarInfo{name: :abc, positions: [{1, 1}]}, + # %VarInfo{name: :abc, positions: [{1, 1}]}, %VarInfo{name: :abc, positions: [{2, 1}]} ] = state |> get_line_vars(3) end @@ -352,12 +352,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert Map.has_key?(state.lines_to_env[4].versioned_vars, {:cde, nil}) - # TODO is it OK assert ([ - %VarInfo{name: :cde, positions: [{1, 1}], scope_id: scope_id_1}, - %VarInfo{name: :cde, positions: [{3, 3}], scope_id: scope_id_2} - ] - when scope_id_1 != scope_id_2) = state |> get_line_vars(4) + # %VarInfo{name: :cde, positions: [{1, 1}], scope_id: scope_id_1}, + %VarInfo{name: :cde, positions: [{3, 3}]} + ]) = state |> get_line_vars(4) assert Map.has_key?(state.lines_to_env[6].versioned_vars, {:cde, nil}) @@ -874,10 +872,9 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert Map.keys(state.lines_to_env[4].versioned_vars) == [{:abc, nil}] assert ([ - %VarInfo{name: :abc, positions: [{1, 1}], scope_id: scope_id_1}, - %VarInfo{name: :abc, positions: [{3, 3}], scope_id: scope_id_2} - ] - when scope_id_1 != scope_id_2) = state |> get_line_vars(4) + # %VarInfo{name: :abc, positions: [{1, 1}], scope_id: scope_id_1}, + %VarInfo{name: :abc, positions: [{3, 3}]} + ]) = state |> get_line_vars(4) assert Map.keys(state.lines_to_env[6].versioned_vars) == [{:abc, nil}] @@ -2525,11 +2522,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do vars = state |> get_line_vars(6) assert ([ - %VarInfo{name: :var1, positions: [{3, 19}, {3, 37}], scope_id: scope_id_1}, - %VarInfo{name: :var1, positions: [{4, 5}], scope_id: scope_id_2}, - %VarInfo{name: :var1, positions: [{5, 5}], scope_id: scope_id_2} - ] - when scope_id_2 > scope_id_1) = vars + # %VarInfo{name: :var1, positions: [{3, 19}, {3, 37}], scope_id: scope_id_1}, + # %VarInfo{name: :var1, positions: [{4, 5}], scope_id: scope_id_2}, + %VarInfo{name: :var1, positions: [{5, 5}]} + ]) = vars end test "vars defined inside a module" do From 49586ef57b2091eb21f5ce46dd28073c9fe1ff22 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 23 May 2024 20:48:01 +0200 Subject: [PATCH 066/235] first step on type inference --- lib/elixir_sense/core/compiler.ex | 15 + lib/elixir_sense/core/metadata_builder.ex | 225 +-------- lib/elixir_sense/core/type_inference.ex | 204 ++++++++ .../core/metadata_builder_test.exs | 462 ++++++++---------- 4 files changed, 450 insertions(+), 456 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 003fb8c8..e1a08c84 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -27,9 +27,24 @@ defmodule ElixirSense.Core.Compiler do defp do_expand({:=, meta, [left, right]}, s, e) do # elixir validates we are not in guard context {e_right, sr, er} = expand(right, s, e) + match_context_r = TypeInference.get_binding_type(sr, e_right) |> dbg # dbg(sr) # dbg(e_right) {e_left, sl, el} = __MODULE__.Clauses.match(&expand/3, left, sr, s, er) + vars_l_with_infered_types = TypeInference.find_vars(sl, e_left, match_context_r) + vars_r_with_infered_types = case e.context do + :match -> + match_context_l = TypeInference.get_binding_type(sl, e_left) + TypeInference.find_vars(sr, e_right, match_context_l) + _ -> %{} + end + + [h | t] = sl.vars_info + + h = h |> Map.merge(vars_r_with_infered_types) |> Map.merge(vars_l_with_infered_types) + + sl = %{sl | vars_info: [Map.merge(h, vars_l_with_infered_types) | t]} + # match_context_l = TypeInference.get_binding_type(sl, e_left) |> dbg # elixir raises parallel_bitstring_match if detected diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index f05fd3fe..91d5ae93 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -143,7 +143,7 @@ defmodule ElixirSense.Core.MetadataBuilder do when is_atom(name) do vars = state - |> find_vars(params) + |> find_vars(params, nil) _vars = if options[:guards], @@ -504,7 +504,7 @@ defmodule ElixirSense.Core.MetadataBuilder do defp pre({:when, meta, [lhs, rhs]}, state) do _vars = state - |> find_vars(lhs) + |> find_vars(lhs, nil) state # |> add_vars(vars, true) @@ -858,217 +858,22 @@ defmodule ElixirSense.Core.MetadataBuilder do {ast, state} end - defp find_vars(state, ast, match_context \\ nil) + # defp find_vars(state, ast, match_context \\ nil) - defp find_vars(_state, {var, _meta, nil}, _) - when var in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__] do - # TODO local calls? - [] - end - - defp find_vars(_state, {var, meta, nil}, :rescue) when is_atom(var) do - line = Keyword.fetch!(meta, :line) - column = Keyword.fetch!(meta, :column) - match_context = {:struct, [], {:atom, Exception}, nil} - [%VarInfo{name: var, positions: [{line, column}], type: match_context, is_definition: true}] - end - - defp find_vars(state, ast, match_context) do - {_ast, {vars, _match_context}} = - Macro.prewalk(ast, {[], match_context}, &match_var(state, &1, &2)) - - vars - end - - defp match_var( - state, - {:in, _meta, - [ - left, - right - ]}, - {vars, _match_context} - ) do - exception_type = - case right do - [elem] -> - get_binding_type(state, elem) - - list when is_list(list) -> - types = for elem <- list, do: get_binding_type(state, elem) - if Enum.all?(types, &match?({:atom, _}, &1)), do: {:atom, Exception} - - elem -> - get_binding_type(state, elem) - end - - match_context = - case exception_type do - {:atom, atom} -> {:struct, [], {:atom, atom}, nil} - _ -> nil - end - - match_var(state, left, {vars, match_context}) - end - - defp match_var( - state, - {:=, _meta, - [ - left, - right - ]}, - {vars, _match_context} - ) do - {_ast, {vars, _match_context}} = - match_var(state, left, {vars, get_binding_type(state, right)}) - - {_ast, {vars, _match_context}} = - match_var(state, right, {vars, get_binding_type(state, left)}) - - {[], {vars, nil}} - end - - defp match_var( - _state, - {:^, _meta, [{var, meta, nil}]}, - {vars, match_context} = ast - ) - when is_atom(var) do - line = Keyword.fetch!(meta, :line) - column = Keyword.fetch!(meta, :column) - var_info = %VarInfo{name: var, positions: [{line, column}], type: match_context} - {ast, {[var_info | vars], nil}} - end + # defp find_vars(_state, {var, _meta, nil}, _) + # when var in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__] do + # # TODO local calls? + # [] + # end - defp match_var( - _state, - {var, meta, nil} = ast, - {vars, match_context} - ) - when is_atom(var) and - var not in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__] do - # TODO local calls? - # TODO {:__MODULE__, meta, nil} is not expanded here - line = Keyword.fetch!(meta, :line) - column = Keyword.fetch!(meta, :column) - - var_info = %VarInfo{ - name: var, - positions: [{line, column}], - type: match_context, - is_definition: true - } + # defp find_vars(_state, {var, meta, nil}, :rescue) when is_atom(var) do + # line = Keyword.fetch!(meta, :line) + # column = Keyword.fetch!(meta, :column) + # match_context = {:struct, [], {:atom, Exception}, nil} + # [%VarInfo{name: var, positions: [{line, column}], type: match_context, is_definition: true}] + # end - {ast, {[var_info | vars], nil}} - end - - # drop right side of guard expression as guards cannot define vars - defp match_var(state, {:when, _, [left, _right]}, {vars, _match_context}) do - match_var(state, left, {vars, nil}) - end - - defp match_var(state, {:%, _, [type_ast, {:%{}, _, ast}]}, {vars, match_context}) - when not is_nil(match_context) do - {_ast, {type_vars, _match_context}} = match_var(state, type_ast, {[], nil}) - - destructured_vars = - ast - |> Enum.flat_map(fn {key, value_ast} -> - key_type = get_binding_type(state, key) - - {_ast, {new_vars, _match_context}} = - match_var(state, value_ast, {[], {:map_key, match_context, key_type}}) - - new_vars - end) - - {ast, {vars ++ destructured_vars ++ type_vars, nil}} - end - - defp match_var(state, {:%{}, _, ast}, {vars, match_context}) when not is_nil(match_context) do - destructured_vars = - ast - |> Enum.flat_map(fn {key, value_ast} -> - key_type = get_binding_type(state, key) - - {_ast, {new_vars, _match_context}} = - match_var(state, value_ast, {[], {:map_key, match_context, key_type}}) - - new_vars - end) - - {ast, {vars ++ destructured_vars, nil}} - end - - # regular tuples use {:{}, [], [field_1, field_2]} ast - # two element use `{field_1, field_2}` ast (probably as an optimization) - # detect and convert to regular - defp match_var(state, ast, {vars, match_context}) - when is_tuple(ast) and tuple_size(ast) == 2 do - match_var(state, {:{}, [], ast |> Tuple.to_list()}, {vars, match_context}) - end - - defp match_var(state, {:{}, _, ast}, {vars, match_context}) when not is_nil(match_context) do - indexed = ast |> Enum.with_index() - total = length(ast) - - destructured_vars = - indexed - |> Enum.flat_map(fn {nth_elem_ast, n} -> - bond = - {:tuple, total, - indexed |> Enum.map(&if(n != elem(&1, 1), do: get_binding_type(state, elem(&1, 0))))} - - match_context = - if match_context != bond do - {:intersection, [match_context, bond]} - else - match_context - end - - {_ast, {new_vars, _match_context}} = - match_var(state, nth_elem_ast, {[], {:tuple_nth, match_context, n}}) - - new_vars - end) - - {ast, {vars ++ destructured_vars, nil}} - end - - # two element tuples on the left of `->` are encoded as list `[field1, field2]` - # detect and convert to regular - defp match_var(state, {:->, meta, [[left], right]}, {vars, match_context}) do - match_var(state, {:->, meta, [left, right]}, {vars, match_context}) - end - - defp match_var(state, list, {vars, match_context}) - when not is_nil(match_context) and is_list(list) do - match_var_list = fn head, tail -> - {_ast, {new_vars_head, _match_context}} = - match_var(state, head, {[], {:list_head, match_context}}) - - {_ast, {new_vars_tail, _match_context}} = - match_var(state, tail, {[], {:list_tail, match_context}}) - - {list, {vars ++ new_vars_head ++ new_vars_tail, nil}} - end - - case list do - [] -> - {list, {vars, nil}} - - [{:|, _, [head, tail]}] -> - match_var_list.(head, tail) - - [head | tail] -> - match_var_list.(head, tail) - end - end - - defp match_var(_state, ast, {vars, match_context}) do - {ast, {vars, match_context}} - end + def infer_type_from_guards(guard_ast, vars, state) do type_info = Guard.type_information_from_guards(guard_ast, state) diff --git a/lib/elixir_sense/core/type_inference.ex b/lib/elixir_sense/core/type_inference.ex index 7ba5bf08..b56c6e2d 100644 --- a/lib/elixir_sense/core/type_inference.ex +++ b/lib/elixir_sense/core/type_inference.ex @@ -1,6 +1,7 @@ defmodule ElixirSense.Core.TypeInference do # TODO remove state arg # struct or struct update + alias ElixirSense.Core.State.VarInfo def get_binding_type( state, {:%, _meta, @@ -172,4 +173,207 @@ defmodule ElixirSense.Core.TypeInference do {field, get_binding_type(state, value)} end end + + def find_vars(state, ast, match_context) do + {_ast, {vars, _match_context}} = + Macro.prewalk(ast, {[], match_context}, &match_var(state, &1, &2)) + + vars |> Map.new + end + + defp match_var( + state, + {:in, _meta, + [ + left, + right + ]}, + {vars, _match_context} + ) do + exception_type = + case right do + [elem] -> + get_binding_type(state, elem) + + list when is_list(list) -> + types = for elem <- list, do: get_binding_type(state, elem) + if Enum.all?(types, &match?({:atom, _}, &1)), do: {:atom, Exception} + + elem -> + get_binding_type(state, elem) + end + + match_context = + case exception_type do + {:atom, atom} -> {:struct, [], {:atom, atom}, nil} + _ -> nil + end + + match_var(state, left, {vars, match_context}) + end + + defp match_var( + state, + {:=, _meta, + [ + left, + right + ]}, + {vars, _match_context} + ) do + {_ast, {vars, _match_context}} = + match_var(state, left, {vars, get_binding_type(state, right)}) + + {_ast, {vars, _match_context}} = + match_var(state, right, {vars, get_binding_type(state, left)}) + + {[], {vars, nil}} + end + + defp match_var( + state, + {:^, _meta, [{var, meta, context}]}, + {vars, match_context} + ) + when is_atom(var) and is_atom(context) and + var not in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__, :_] do + line = Keyword.fetch!(meta, :line) + column = Keyword.fetch!(meta, :column) + version = meta |> Keyword.fetch!(:version) + var_info = state.vars_info |> hd |> Map.fetch!({var, version}) + + var_info = %VarInfo{var_info | type: match_context} + + {nil, {[{{var, version}, var_info} | vars], nil}} + end + + defp match_var( + state, + {var, meta, context} = ast, + {vars, match_context} + ) + when is_atom(var) and is_atom(context) and + var not in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__, :_] do + # TODO local calls? + # TODO {:__MODULE__, meta, nil} is not expanded here + line = Keyword.fetch!(meta, :line) + column = Keyword.fetch!(meta, :column) + + dbg(state.vars) + dbg(state.vars_info) + + version = meta |> Keyword.fetch!(:version) + var_info = state.vars_info |> hd |> Map.fetch!({var, version}) + + var_info = %VarInfo{var_info | type: match_context} + + {nil, {[{{var, version}, var_info} | vars], nil}} + end + + # drop right side of guard expression as guards cannot define vars + defp match_var(state, {:when, _, [left, _right]}, {vars, _match_context}) do + match_var(state, left, {vars, nil}) + end + + defp match_var(state, {:%, _, [type_ast, {:%{}, _, ast}]}, {vars, match_context}) + when not is_nil(match_context) do + {_ast, {type_vars, _match_context}} = match_var(state, type_ast, {[], nil}) + + destructured_vars = + ast + |> Enum.flat_map(fn {key, value_ast} -> + key_type = get_binding_type(state, key) + + {_ast, {new_vars, _match_context}} = + match_var(state, value_ast, {[], {:map_key, match_context, key_type}}) + + new_vars + end) + + {ast, {vars ++ destructured_vars ++ type_vars, nil}} + end + + defp match_var(state, {:%{}, _, ast}, {vars, match_context}) when not is_nil(match_context) do + destructured_vars = + ast + |> Enum.flat_map(fn {key, value_ast} -> + key_type = get_binding_type(state, key) + + {_ast, {new_vars, _match_context}} = + match_var(state, value_ast, {[], {:map_key, match_context, key_type}}) + + new_vars + end) + + {ast, {vars ++ destructured_vars, nil}} + end + + # regular tuples use {:{}, [], [field_1, field_2]} ast + # two element use `{field_1, field_2}` ast (probably as an optimization) + # detect and convert to regular + defp match_var(state, ast, {vars, match_context}) + when is_tuple(ast) and tuple_size(ast) == 2 do + match_var(state, {:{}, [], ast |> Tuple.to_list()}, {vars, match_context}) + end + + defp match_var(state, {:{}, _, ast}, {vars, match_context}) when not is_nil(match_context) do + indexed = ast |> Enum.with_index() + total = length(ast) + + destructured_vars = + indexed + |> Enum.flat_map(fn {nth_elem_ast, n} -> + bond = + {:tuple, total, + indexed |> Enum.map(&if(n != elem(&1, 1), do: get_binding_type(state, elem(&1, 0))))} + + match_context = + if match_context != bond do + {:intersection, [match_context, bond]} + else + match_context + end + + {_ast, {new_vars, _match_context}} = + match_var(state, nth_elem_ast, {[], {:tuple_nth, match_context, n}}) + + new_vars + end) + + {ast, {vars ++ destructured_vars, nil}} + end + + # two element tuples on the left of `->` are encoded as list `[field1, field2]` + # detect and convert to regular + defp match_var(state, {:->, meta, [[left], right]}, {vars, match_context}) do + match_var(state, {:->, meta, [left, right]}, {vars, match_context}) + end + + defp match_var(state, list, {vars, match_context}) + when not is_nil(match_context) and is_list(list) do + match_var_list = fn head, tail -> + {_ast, {new_vars_head, _match_context}} = + match_var(state, head, {[], {:list_head, match_context}}) + + {_ast, {new_vars_tail, _match_context}} = + match_var(state, tail, {[], {:list_tail, match_context}}) + + {list, {vars ++ new_vars_head ++ new_vars_tail, nil}} + end + + case list do + [] -> + {list, {vars, nil}} + + [{:|, _, [head, tail]}] -> + match_var_list.(head, tail) + + [head | tail] -> + match_var_list.(head, tail) + end + end + + defp match_var(_state, ast, {vars, match_context}) do + {ast, {vars, match_context}} + end end diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index d4847829..da3b9454 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -8,8 +8,9 @@ defmodule ElixirSense.Core.MetadataBuilderTest do @attribute_binding_support true or Version.match?(System.version(), "< 1.17.0-dev") @expand_eval false - @binding_support Version.match?(System.version(), "< 1.17.0-dev") + @binding_support true or Version.match?(System.version(), "< 1.17.0-dev") @typespec_calls_support true or Version.match?(System.version(), "< 1.17.0-dev") + @var_in_ex_unit Version.match?(System.version(), "< 1.17.0-dev") @compiler Code.ensure_loaded?(ElixirSense.Core.Compiler) describe "versioned_vars" do @@ -1207,6 +1208,125 @@ defmodule ElixirSense.Core.MetadataBuilderTest do %VarInfo{name: :abc, positions: [{2, 11}, {3, 10}]} ] = state |> get_line_vars(3) end + + test "variables are added to environment" do + state = + """ + defmodule MyModule do + def func do + var = :my_var + IO.puts "" + end + end + """ + |> string_to_state + + assert [%VarInfo{scope_id: scope_id}] = state |> get_line_vars(4) + assert [%VarInfo{name: :var}] = state.vars_info_per_scope_id[scope_id] + end + + test "vars defined inside a function without params" do + state = + """ + defmodule MyModule do + var_out1 = 1 + def func do + var_in1 = 1 + var_in2 = 1 + IO.puts "" + end + var_out2 = 1 + IO.puts "" + end + """ + |> string_to_state + + assert [ + %VarInfo{name: :var_in1, positions: [{4, 5}], scope_id: scope_id}, + %VarInfo{name: :var_in2, positions: [{5, 5}], scope_id: scope_id} + ] = state |> get_line_vars(6) + end + end + + if @var_in_ex_unit do + describe "vars in ex_unit" do + test "variables are added to environment in ex_unit test" do + state = + """ + defmodule MyModuleTests do + use ExUnit.Case, async: true + + test "it does what I want", %{some: some} do + IO.puts("") + end + + describe "this" do + test "too does what I want" do + IO.puts("") + end + end + + test "is not implemented" + end + """ + |> string_to_state + + assert [%VarInfo{type: nil, scope_id: scope_id}] = state |> get_line_vars(5) + assert [%VarInfo{name: :some}] = state.vars_info_per_scope_id[scope_id] + + assert Map.has_key?( + state.mods_funs_to_positions, + {MyModuleTests, :"test it does what I want", 1} + ) + + assert Map.has_key?( + state.mods_funs_to_positions, + {MyModuleTests, :"test this too does what I want", 1} + ) + + assert Map.has_key?( + state.mods_funs_to_positions, + {MyModuleTests, :"test is not implemented", 1} + ) + end + + test "variables are added to environment in ex_unit setup" do + state = + """ + defmodule MyModuleTests do + use ExUnit.Case, async: true + + setup_all %{some: some} do + IO.puts("") + end + + setup %{some: other} do + IO.puts("") + end + + setup do + IO.puts("") + end + + setup :clean_up_tmp_directory + + setup [:clean_up_tmp_directory, :another_setup] + + setup {MyModule, :my_setup_function} + end + """ + |> string_to_state + + assert [%VarInfo{type: nil, scope_id: scope_id}] = state |> get_line_vars(5) + assert [%VarInfo{name: :some}] = state.vars_info_per_scope_id[scope_id] + + assert [%VarInfo{type: nil, scope_id: scope_id}] = state |> get_line_vars(9) + assert [%VarInfo{name: :other}] = state.vars_info_per_scope_id[scope_id] + + # we do not generate defs - ExUnit.Callbacks.__setup__ is too complicated and generates def names with counters, e.g. + # :"__ex_unit_setup_#{counter}_#{length(setup)}" + end + end end describe "typespec vars" do @@ -1569,31 +1689,21 @@ defmodule ElixirSense.Core.MetadataBuilderTest do test "variable rebinding" do state = """ - def my() do - abc = 1 - some(abc) - abc = %Abc{cde: 1} - IO.puts "" - end + abc = 1 + some(abc) + abc = %Abc{cde: 1} + IO.puts "" """ |> string_to_state assert [ - %State.VarInfo{ - name: :abc, - type: {:integer, 1}, - is_definition: true, - positions: [{2, 3}, {3, 8}], - scope_id: 2 - }, %State.VarInfo{ name: :abc, type: {:struct, [cde: {:integer, 1}], {:atom, Abc}, nil}, is_definition: true, - positions: [{4, 3}], - scope_id: 2 + positions: [{3, 1}] } - ] = state |> get_line_vars(5) + ] = state |> get_line_vars(4) end test "tuple destructuring" do @@ -1622,7 +1732,14 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert [ %VarInfo{ name: :other, - type: {:local_call, :elem, [{:attribute, :myattribute}, {:integer, 0}]} + # TODO do we need to rewrite? change Binding + # type: {:local_call, :elem, [{:attribute, :myattribute}, {:integer, 0}]} + type: { + :call, + {:atom, :erlang}, + :element, + [integer: 1, attribute: :myattribute] + } }, %VarInfo{ name: :var, @@ -1631,7 +1748,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do {:intersection, [{:attribute, :myattribute}, {:tuple, 2, [{:atom, :ok}, nil]}]}, 1} } - ] = state |> get_line_vars(4) + ] = state |> get_line_vars(5) assert [ %VarInfo{ @@ -1857,28 +1974,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] = state |> get_line_vars(5) end - test "vars defined inside a function without params" do - state = - """ - defmodule MyModule do - var_out1 = 1 - def func do - var_in1 = 1 - var_in2 = 1 - IO.puts "" - end - var_out2 = 1 - IO.puts "" - end - """ - |> string_to_state - - assert [ - %VarInfo{name: :var_in1, positions: [{4, 5}], scope_id: 4}, - %VarInfo{name: :var_in2, positions: [{5, 5}], scope_id: 4} - ] = state |> get_line_vars(6) - end - test "vars binding" do state = """ @@ -1907,145 +2002,33 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert [%VarInfo{type: {:atom, String}}] = state |> get_line_vars(4) - assert [%VarInfo{type: {:atom, String}}, %VarInfo{type: {:atom, Map}}] = + assert [%VarInfo{type: {:atom, Map}}] = state |> get_line_vars(6) - assert [%VarInfo{type: {:atom, String}}, %VarInfo{type: {:atom, Map}}] = + assert [%VarInfo{type: {:atom, Map}}] = state |> get_line_vars(8) assert [ - %VarInfo{type: {:atom, String}, scope_id: 4}, - %VarInfo{type: {:atom, Map}, scope_id: 4}, - %VarInfo{type: {:atom, List}, scope_id: 5} + %VarInfo{type: {:atom, List}} ] = state |> get_line_vars(10) assert [ - %VarInfo{type: {:atom, String}, scope_id: 4}, - %VarInfo{type: {:atom, Map}, scope_id: 4}, - %VarInfo{type: {:atom, List}, scope_id: 5}, - %VarInfo{type: {:atom, Enum}, scope_id: 5} + %VarInfo{type: {:atom, Enum}} ] = state |> get_line_vars(12) - assert [%VarInfo{type: {:atom, String}}, %VarInfo{type: {:atom, Map}}] = + assert [%VarInfo{type: {:atom, Map}}] = state |> get_line_vars(14) assert [ - %VarInfo{type: {:atom, String}}, - %VarInfo{type: {:atom, Map}}, %VarInfo{type: {:atom, Atom}} ] = state |> get_line_vars(16) assert [ %VarInfo{name: :other, type: {:variable, :var}}, - %VarInfo{type: {:atom, String}}, - %VarInfo{type: {:atom, Map}}, %VarInfo{type: {:atom, Atom}} ] = state |> get_line_vars(18) end - test "variables are added to environment" do - state = - """ - defmodule MyModule do - def func do - var = :my_var - end - end - """ - |> string_to_state - - assert [%VarInfo{type: {:atom, :my_var}, scope_id: scope_id}] = state |> get_line_vars(3) - assert [%VarInfo{name: :var}] = state.vars_info_per_scope_id[scope_id] - end - - test "variables are added to environment in ex_unit test" do - state = - """ - defmodule MyModuleTests do - use ExUnit.Case, async: true - - test "it does what I want", %{some: some} do - IO.puts("") - end - - describe "this" do - test "too does what I want" do - IO.puts("") - end - end - - test "is not implemented" - end - """ - |> string_to_state - - assert [%VarInfo{type: nil, scope_id: scope_id}] = state |> get_line_vars(5) - assert [%VarInfo{name: :some}] = state.vars_info_per_scope_id[scope_id] - - assert Map.has_key?( - state.mods_funs_to_positions, - {MyModuleTests, :"test it does what I want", 1} - ) - - assert Map.has_key?( - state.mods_funs_to_positions, - {MyModuleTests, :"test this too does what I want", 1} - ) - - assert Map.has_key?( - state.mods_funs_to_positions, - {MyModuleTests, :"test is not implemented", 1} - ) - end - - test "variables are added to environment in ex_unit setup" do - state = - """ - defmodule MyModuleTests do - use ExUnit.Case, async: true - - setup_all %{some: some} do - IO.puts("") - end - - setup %{some: other} do - IO.puts("") - end - - setup do - IO.puts("") - end - - setup :clean_up_tmp_directory - - setup [:clean_up_tmp_directory, :another_setup] - - setup {MyModule, :my_setup_function} - end - """ - |> string_to_state - - assert [%VarInfo{type: nil, scope_id: scope_id}] = state |> get_line_vars(5) - assert [%VarInfo{name: :some}] = state.vars_info_per_scope_id[scope_id] - - assert [%VarInfo{type: nil, scope_id: scope_id}] = state |> get_line_vars(9) - assert [%VarInfo{name: :other}] = state.vars_info_per_scope_id[scope_id] - - # we do not generate defs - ExUnit.Callbacks.__setup__ is too complicated and generates def names with counters, e.g. - # :"__ex_unit_setup_#{counter}_#{length(setup)}" - end - - test "variables from outside module are added to environment" do - state = - """ - var = :my_var - """ - |> string_to_state - - assert [%VarInfo{type: {:atom, :my_var}, scope_id: scope_id}] = state |> get_line_vars(1) - assert [%VarInfo{name: :var}] = state.vars_info_per_scope_id[scope_id] - end - test "call binding" do state = """ @@ -2094,8 +2077,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] = state |> get_line_vars(16) assert [ - %VarInfo{name: :var1, type: nil, scope_id: 7}, - %VarInfo{name: :var1, type: {:call, {:variable, :var1}, :abc, []}, scope_id: 8}, + %VarInfo{name: :var1, type: {:call, {:variable, :var1}, :abc, []}}, %VarInfo{name: :var2, type: {:call, {:attribute, :attr}, :qwe, [{:integer, 0}]}}, %VarInfo{ name: :var3, @@ -2130,26 +2112,16 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert [%VarInfo{type: {:map, [asd: {:integer, 5}], nil}}] = state |> get_line_vars(4) assert [ - %VarInfo{type: {:map, [asd: {:integer, 5}], nil}}, %VarInfo{ type: {:map, [asd: {:integer, 5}, nested: {:map, [wer: nil], nil}], nil} } ] = state |> get_line_vars(6) assert [ - %VarInfo{type: {:map, [asd: {:integer, 5}], nil}}, - %VarInfo{ - type: {:map, [asd: {:integer, 5}, nested: {:map, [wer: nil], nil}], nil} - }, %VarInfo{type: {:map, [], nil}} ] = state |> get_line_vars(8) assert [ - %VarInfo{type: {:map, [asd: {:integer, 5}], nil}}, - %VarInfo{ - type: {:map, [asd: {:integer, 5}, nested: {:map, [wer: nil], nil}], nil} - }, - %VarInfo{type: {:map, [], nil}}, %VarInfo{type: {:map, [asd: {:integer, 5}, zxc: {:atom, String}], nil}} ] = state |> get_line_vars(10) @@ -2161,9 +2133,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do state |> get_line_vars(12) |> Enum.filter(&(&1.name == :qwe)) assert [ - %VarInfo{ - type: {:map, [asd: {:integer, 2}, zxc: {:integer, 5}], {:variable, :var}} - }, %VarInfo{type: {:map, [{:asd, {:integer, 2}}], {:variable, :var}}} ] = state |> get_line_vars(14) |> Enum.filter(&(&1.name == :qwe)) end @@ -2405,66 +2374,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] = state |> get_line_vars(15) end - test "vars defined inside a function `after`/`rescue`/`catch`" do - state = - """ - defmodule MyModule do - var_out1 = 1 - def func(var_arg) do - var_in1 = 1 - var_in2 = 1 - IO.puts "" - after - var_after = 1 - IO.puts "" - end - end - """ - |> string_to_state - - assert [ - %VarInfo{name: :var_arg, positions: [{3, 12}], scope_id: 3}, - %VarInfo{name: :var_in1, positions: [{4, 5}], scope_id: 4}, - %VarInfo{name: :var_in2, positions: [{5, 5}], scope_id: 4} - ] = state |> get_line_vars(6) - - assert [ - %VarInfo{name: :var_after, positions: [{8, 5}], scope_id: 5}, - %VarInfo{name: :var_arg, positions: [{3, 12}], scope_id: 3} - ] = state |> get_line_vars(9) - end - - test "vars defined inside a function with params" do - state = - """ - defmodule MyModule do - var_out1 = 1 - def func(%{key1: par1, key2: [par2|[par3, _]]}, par4, _par5) do - var_in1 = 1 - var_in2 = 1 - IO.puts "" - end - defp func1(arg), do: arg + 1 - var_out2 = 1 - end - """ - |> string_to_state - - assert [ - %VarInfo{name: :_par5, positions: [{3, 57}], scope_id: 3}, - %VarInfo{name: :par1, positions: [{3, 20}], scope_id: 3}, - %VarInfo{name: :par2, positions: [{3, 33}], scope_id: 3}, - %VarInfo{name: :par3, positions: [{3, 39}], scope_id: 3}, - %VarInfo{name: :par4, positions: [{3, 51}], scope_id: 3}, - %VarInfo{name: :var_in1, positions: [{4, 5}], scope_id: 4}, - %VarInfo{name: :var_in2, positions: [{5, 5}], scope_id: 4} - ] = state |> get_line_vars(6) - - assert [ - %VarInfo{name: :arg, positions: [{8, 14}, {8, 24}], scope_id: 5} - ] = state |> get_line_vars(8) - end - test "vars binding by pattern matching with pin operators" do state = """ @@ -2472,30 +2381,31 @@ defmodule ElixirSense.Core.MetadataBuilderTest do def func(a) do b = 1 case a do - %{b: ^2} = a1 -> 2 - %{b: ^b} = a2 -> b + %{b: 2} = a1 -> + IO.puts "" + %{b: ^b} = a2 -> + IO.puts "" end end end """ |> string_to_state - vars = state |> get_line_vars(5) + vars = state |> get_line_vars(6) - assert %VarInfo{ - name: :a1, - positions: [{5, 18}], - scope_id: 6, - is_definition: true, - type: {:map, [b: {:integer, 2}], nil} - } = Enum.find(vars, &(&1.name == :a1)) + # assert %VarInfo{ + # name: :a1, + # positions: [{5, 18}], + # scope_id: 6, + # is_definition: true, + # type: {:map, [b: {:integer, 2}], nil} + # } = Enum.find(vars, &(&1.name == :a1)) - vars = state |> get_line_vars(6) + vars = state |> get_line_vars(8) |> dbg assert %VarInfo{ name: :a2, - positions: [{6, 18}], - scope_id: 7, + positions: [{7, 18}], is_definition: true, type: {:map, [b: {:variable, :b}], nil} } = Enum.find(vars, &(&1.name == :a2)) @@ -2504,6 +2414,66 @@ defmodule ElixirSense.Core.MetadataBuilderTest do end describe "var" do + test "vars defined inside a function `after`/`rescue`/`catch`" do + state = + """ + defmodule MyModule do + var_out1 = 1 + def func(var_arg) do + var_in1 = 1 + var_in2 = 1 + IO.puts "" + after + var_after = 1 + IO.puts "" + end + end + """ + |> string_to_state + + assert ([ + %VarInfo{name: :var_arg, positions: [{3, 12}], scope_id: scope_id_1}, + %VarInfo{name: :var_in1, positions: [{4, 5}], scope_id: scope_id_2}, + %VarInfo{name: :var_in2, positions: [{5, 5}], scope_id: scope_id_2} + ] when scope_id_2 > scope_id_1) = state |> get_line_vars(6) + + assert ([ + %VarInfo{name: :var_after, positions: [{8, 5}], scope_id: scope_id_2}, + %VarInfo{name: :var_arg, positions: [{3, 12}], scope_id: scope_id_1} + ] when scope_id_2 > scope_id_1) = state |> get_line_vars(9) + end + + test "vars defined inside a function with params" do + state = + """ + defmodule MyModule do + var_out1 = 1 + def func(%{key1: par1, key2: [par2|[par3, _]]}, par4, _par5) do + var_in1 = 1 + var_in2 = 1 + IO.puts "" + end + defp func1(arg), do: arg + 1 + var_out2 = 1 + end + """ + |> string_to_state + + assert ([ + %VarInfo{name: :_par5, positions: [{3, 57}], scope_id: scope_id_1}, + %VarInfo{name: :par1, positions: [{3, 20}], scope_id: scope_id_1}, + %VarInfo{name: :par2, positions: [{3, 33}], scope_id: scope_id_1}, + %VarInfo{name: :par3, positions: [{3, 39}], scope_id: scope_id_1}, + %VarInfo{name: :par4, positions: [{3, 51}], scope_id: scope_id_1}, + %VarInfo{name: :var_in1, positions: [{4, 5}], scope_id: scope_id_2}, + %VarInfo{name: :var_in2, positions: [{5, 5}], scope_id: scope_id_2} + ] when scope_id_2 > scope_id_1) = state |> get_line_vars(6) + + assert [ + %VarInfo{name: :arg, positions: [{8, 14}, {8, 24}]} + ] = state |> get_line_vars(8) + end + test "rebinding vars" do state = """ From 903a4c6d1e14df2389440bee19a62d5cae89fcaa Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 23 May 2024 22:49:53 +0200 Subject: [PATCH 067/235] improve def expansion --- lib/elixir_sense/core/compiler.ex | 77 +++++++------------ lib/elixir_sense/core/metadata_builder.ex | 1 - lib/elixir_sense/core/type_inference.ex | 11 +-- .../core/metadata_builder_test.exs | 54 +++++++++++++ 4 files changed, 84 insertions(+), 59 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index e1a08c84..3d23ed14 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -544,12 +544,8 @@ defmodule ElixirSense.Core.Compiler do expand(arg, s, e) end - defp do_expand({:_, _meta, kind} = var, s, %{context: _context} = e) when is_atom(kind) do - # if context != :match, do: function_error(meta, e, __MODULE__, :unbound_underscore) - {var, s, e} - end - - defp do_expand({:_, _meta, kind} = var, s, %{context: :match} = e) when is_atom(kind) do + defp do_expand({:_, _meta, kind} = var, s, e) when is_atom(kind) do + # elixir raises unbound_underscore if context is not match {var, s, e} end @@ -1565,7 +1561,7 @@ defmodule ElixirSense.Core.Compiler do defp expand_macro(meta, Kernel, def_kind, [call], callback, state, env) when def_kind in [:def, :defp, :defmacro, :defmacrop, :defguard, :defguardp] do - # transform guard to def with empty body + # transform guard and function head to def with empty body expand_macro(meta, Kernel, def_kind, [call, {:__block__, [], []}], callback, state, env) end @@ -1601,19 +1597,7 @@ defmodule ElixirSense.Core.Compiler do {call, expr} end - # dbg(call) - # dbg(expr) - - # state = - # state - # |> add_current_env_to_line(line, env) - - state = - %{state | vars: {%{}, false}, unused: 0} - |> new_func_vars_scope - {name_and_args, guards} = __MODULE__.Utils.extract_guards(call) - # dbg(name_and_args) {name, _meta_1, args} = case name_and_args do @@ -1622,41 +1606,35 @@ defmodule ElixirSense.Core.Compiler do _ -> raise "invalid_def #{inspect(name_and_args)}" end - {_e_args, state, a_env} = - expand_args(args, %{state | prematch: {%{}, 0, :none}}, %{env | context: :match}) + arity = length(args) + + # based on :elixir_def.env_for_expansion + state = %{state | + vars: {%{}, false}, + unused: 0, + caller: def_kind in [:defmacro, :defmacrop, :defguard, :defguardp] + } + |> new_func_vars_scope + env_for_expand = %{env | function: {name, arity}} + + # based on :elixir_clauses.def + {_e_args, state, env_for_expand} = + expand_args(args, %{state | prematch: {%{}, 0, :none}}, %{env_for_expand | context: :match}) - {_e_guard, state, g_env} = + {_e_guard, state, env_for_expand} = __MODULE__.Clauses.guard( guards, %{state | prematch: :raise}, - Map.put(a_env, :context, :guard) + %{env_for_expand | context: :guard} ) - # The env inside the block is discarded. - # TODO name_arity from call - # TODO expand call - # TODO what should it be for macros? - # TODO how to handle guards? - # {call, e_call, state, env} = case call do - # {:when, meta_2, [call, guard]} -> - # {name, meta_1, args} = call - # {e_args, state, env} = expand_args(args, %{state | prematch: {%{}, 0, :none}}, %{env | context: :match}) - # {e_guard, state, env} = expand(guard, state, %{env | context: :guard}) - # {{name, meta_1, e_args}, {:when, meta_2, [{name, meta_1, e_args}, e_guard]}, state, env} - # call -> - # {name, meta_1, args} = call - # {e_args, state, _env} = expand_args(args, %{state | prematch: {%{}, 0, :none}}, %{env | context: :match}) - # {{name, meta_1, e_args}, {name, meta_1, e_args}, state, env} - # end - - # {name, _meta_1, args} = call - arity = length(args) + env_for_expand = %{env_for_expand | context: nil} {position, end_position} = extract_range(meta) state = state - |> add_current_env_to_line(line, %{g_env | context: nil, function: {name, arity}}) + |> add_current_env_to_line(line, env_for_expand) |> add_func_to_index( env, name, @@ -1666,17 +1644,15 @@ defmodule ElixirSense.Core.Compiler do def_kind ) - # expand_macro_callback(meta, Kernel, def_kind, [call, expr], callback, state, env) - # %{state | prematch: :warn} # TODO not sure vars scope is needed state = state |> new_vars_scope - {_e_body, state, _env} = - expand(expr, state, %{g_env | context: nil, function: {name, arity}}) + {_e_body, state, _env_for_expand} = + expand(expr, state, env_for_expand) # restore vars from outer scope state = - %{state | vars: vars, unused: unused} + %{state | vars: vars, unused: unused, caller: false} |> maybe_move_vars_to_outer_scope |> remove_vars_scope |> remove_func_vars_scope @@ -2535,6 +2511,8 @@ defmodule ElixirSense.Core.Compiler do e ) + # TODO infer type from guard here + {[{:when, meta, e_args ++ [e_guard]}], sg, eg} end @@ -2802,6 +2780,7 @@ defmodule ElixirSense.Core.Compiler do # rescue var defp expand_rescue({name, _, atom} = var, s, e) when is_atom(name) and is_atom(atom) do match(&ElixirExpand.expand/3, var, s, s, e) + # TODO infer type to Exception here end # rescue Alias => _ in [Alias] @@ -2817,6 +2796,7 @@ defmodule ElixirSense.Core.Compiler do ) when is_atom(name) and is_atom(var_context) and is_atom(underscore_context) do match(&ElixirExpand.expand/3, var, s, s, e) + # TODO infer type to Exception here end # rescue var in (list() or atom()) @@ -2827,6 +2807,7 @@ defmodule ElixirSense.Core.Compiler do case e_left do {name, _, atom} when is_atom(name) and is_atom(atom) -> {{:in, meta, [e_left, normalize_rescue(e_right, e)]}, sr, er} + # TODO infer type _ -> # elixir rejects this case, we normalize to underscore diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index 91d5ae93..1e7346ea 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -9,7 +9,6 @@ defmodule ElixirSense.Core.MetadataBuilder do alias ElixirSense.Core.Source alias ElixirSense.Core.State - alias ElixirSense.Core.State.VarInfo # alias ElixirSense.Core.TypeInfo alias ElixirSense.Core.Guard alias ElixirSense.Core.Compiler diff --git a/lib/elixir_sense/core/type_inference.ex b/lib/elixir_sense/core/type_inference.ex index b56c6e2d..e614c50d 100644 --- a/lib/elixir_sense/core/type_inference.ex +++ b/lib/elixir_sense/core/type_inference.ex @@ -237,8 +237,6 @@ defmodule ElixirSense.Core.TypeInference do ) when is_atom(var) and is_atom(context) and var not in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__, :_] do - line = Keyword.fetch!(meta, :line) - column = Keyword.fetch!(meta, :column) version = meta |> Keyword.fetch!(:version) var_info = state.vars_info |> hd |> Map.fetch!({var, version}) @@ -249,19 +247,12 @@ defmodule ElixirSense.Core.TypeInference do defp match_var( state, - {var, meta, context} = ast, + {var, meta, context}, {vars, match_context} ) when is_atom(var) and is_atom(context) and var not in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__, :_] do # TODO local calls? - # TODO {:__MODULE__, meta, nil} is not expanded here - line = Keyword.fetch!(meta, :line) - column = Keyword.fetch!(meta, :column) - - dbg(state.vars) - dbg(state.vars_info) - version = meta |> Keyword.fetch!(:version) var_info = state.vars_info |> hd |> Map.fetch!({var, version}) diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index da3b9454..fd4f0713 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -29,6 +29,21 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] = state |> get_line_vars(2) end + test "call does not create a scope" do + state = + """ + inspect(abc = 5) + record_env() + """ + |> string_to_state + + assert Map.has_key?(state.lines_to_env[2].versioned_vars, {:abc, nil}) + + assert [ + %VarInfo{name: :abc, positions: [{1, 9}]} + ] = state |> get_line_vars(2) + end + test "nested binding" do state = """ @@ -1656,6 +1671,45 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert [%VarInfo{type: {:atom, :my_var}}] = state |> get_line_vars(2) end + test "variable binding simple case match context" do + state = + """ + case x do + var = :my_var -> + IO.puts("") + end + """ + |> string_to_state + + assert [%VarInfo{type: {:atom, :my_var}}] = state |> get_line_vars(3) + end + + test "variable binding simple case match context reverse order" do + state = + """ + case x do + :my_var = var -> + IO.puts("") + end + """ + |> string_to_state + + assert [%VarInfo{type: {:atom, :my_var}}] = state |> get_line_vars(3) + end + + test "variable binding simple case match context guard" do + state = + """ + case x do + var when is_map(var) -> + IO.puts("") + end + """ + |> string_to_state + + assert [%VarInfo{type: {:atom, :my_var}}] = state |> get_line_vars(3) + end + test "module attributes value binding to and from variables" do state = """ From 56ad715077f340b0ba10031fb3c1e58dd92eab28 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 24 May 2024 10:28:42 +0200 Subject: [PATCH 068/235] type inference in with, case, for --- lib/elixir_sense/core/compiler.ex | 61 ++++++++++++----- lib/elixir_sense/core/guard.ex | 2 + lib/elixir_sense/core/state.ex | 6 ++ lib/elixir_sense/core/type_inference.ex | 7 +- .../core/metadata_builder_test.exs | 65 ++++++++++++++++++- 5 files changed, 119 insertions(+), 22 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 3d23ed14..db2e7c19 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -27,23 +27,20 @@ defmodule ElixirSense.Core.Compiler do defp do_expand({:=, meta, [left, right]}, s, e) do # elixir validates we are not in guard context {e_right, sr, er} = expand(right, s, e) - match_context_r = TypeInference.get_binding_type(sr, e_right) |> dbg # dbg(sr) # dbg(e_right) {e_left, sl, el} = __MODULE__.Clauses.match(&expand/3, left, sr, s, er) - vars_l_with_infered_types = TypeInference.find_vars(sl, e_left, match_context_r) - vars_r_with_infered_types = case e.context do + + match_context_r = TypeInference.get_binding_type(sr, e_right) + vars_l_with_inferred_types = TypeInference.find_vars(sl, e_left, match_context_r) + vars_r_with_inferred_types = case e.context do :match -> match_context_l = TypeInference.get_binding_type(sl, e_left) TypeInference.find_vars(sr, e_right, match_context_l) _ -> %{} end - [h | t] = sl.vars_info - - h = h |> Map.merge(vars_r_with_infered_types) |> Map.merge(vars_l_with_infered_types) - - sl = %{sl | vars_info: [Map.merge(h, vars_l_with_infered_types) | t]} + sl = annotate_vars_with_inferred_types(sl, Map.merge(vars_l_with_inferred_types, vars_r_with_inferred_types)) # match_context_l = TypeInference.get_binding_type(sl, e_left) |> dbg # elixir raises parallel_bitstring_match if detected @@ -2126,6 +2123,7 @@ defmodule ElixirSense.Core.Compiler do clause = {:->, clause_meta, [args, right]} s_reset = __MODULE__.Env.reset_vars(sa) + # no point in doing type inference here, we are only certain of the initial value of the accumulator {e_clause, s_acc, e_acc} = __MODULE__.Clauses.clause(meta, :fn, &__MODULE__.Clauses.head/3, clause, s_reset, e) @@ -2152,6 +2150,12 @@ defmodule ElixirSense.Core.Compiler do {e_right, sr, er} = expand(right, s, e) sm = __MODULE__.Env.reset_read(sr, s) {[e_left], sl, el} = __MODULE__.Clauses.head([left], sm, er) + + match_context_r = TypeInference.get_binding_type(sr, e_right) |> dbg + vars_l_with_inferred_types = TypeInference.find_vars(sl, e_left |> dbg, {:for_expression, match_context_r}) |> dbg + + sl = State.annotate_vars_with_inferred_types(sl, vars_l_with_inferred_types) + {{:<-, meta, [e_left, e_right]}, sl, el} end @@ -2171,6 +2175,7 @@ defmodule ElixirSense.Core.Compiler do sm, er ) + # no point in doing type inference here, we're only going to find integers and binaries {{:<<>>, meta, [{:<-, op_meta, [e_left, e_right]}]}, sl, el} @@ -2287,7 +2292,7 @@ defmodule ElixirSense.Core.Compiler do # opts # end - {e_opts, so, eo} = __MODULE__.Clauses.case(meta, r_opts, se, ee) + {e_opts, so, eo} = __MODULE__.Clauses.case(meta, e_expr, r_opts, se, ee) {{:case, meta, [e_expr, e_opts]}, so, eo} end @@ -2436,6 +2441,8 @@ defmodule ElixirSense.Core.Compiler do alias ElixirSense.Core.Compiler, as: ElixirExpand alias ElixirSense.Core.Compiler.Env, as: ElixirEnv alias ElixirSense.Core.Compiler.Utils, as: ElixirUtils + alias ElixirSense.Core.State + alias ElixirSense.Core.TypeInference def match(fun, expr, after_s, _before_s, %{context: :match} = e) do fun.(expr, after_s, e) @@ -2533,31 +2540,42 @@ defmodule ElixirSense.Core.Compiler do # case - def case(meta, [], s, e) do + def case(meta, e_expr, [], s, e) do # elixir raises here missing_option # emit a fake do block - case(meta, [do: []], s, e) + case(meta, e_expr, [do: []], s, e) end - def case(_meta, opts, s, e) when not is_list(opts) do + def case(_meta, _e_expr, opts, s, e) when not is_list(opts) do # elixir raises here invalid_args # there may be cursor ElixirExpand.expand(opts, s, e) end - def case(meta, opts, s, e) do + def case(meta, e_expr, opts, s, e) do opts = sanitize_opts(opts, [:do]) + match_context = TypeInference.get_binding_type(s, e_expr) + {case_clauses, sa} = Enum.map_reduce(opts, s, fn x, sa -> - expand_case(meta, x, sa, e) + expand_case(meta, x, match_context, sa, e) end) {case_clauses, sa, e} end - defp expand_case(meta, {:do, _} = do_clause, s, e) do - expand_clauses(meta, :case, &head/3, do_clause, s, e) + defp expand_case(meta, {:do, _} = do_clause, match_context, s, e) do + expand_clauses(meta, :case, fn c, s, e -> + case head(c, s, e) do + {[h | _] = c, s, e} -> + clause_vars_with_inferred_types = TypeInference.find_vars(s, h, match_context) + s = State.annotate_vars_with_inferred_types(s, clause_vars_with_inferred_types) + + {c, s, e} + other -> other + end + end, do_clause, s, e) end # cond @@ -2619,6 +2637,7 @@ defmodule ElixirSense.Core.Compiler do end defp expand_receive(meta, {:do, _} = do_clause, s, e) do + # no point in doing type inference here, we have no idea what message we may get expand_clauses(meta, :receive, &head/3, do_clause, s, e) end @@ -2660,6 +2679,11 @@ defmodule ElixirSense.Core.Compiler do sm = ElixirEnv.reset_read(sr, s) {[e_left], sl, el} = head([left], sm, er) + match_context_r = TypeInference.get_binding_type(sr, e_right) |> dbg + vars_l_with_inferred_types = TypeInference.find_vars(sl, e_left |> dbg, match_context_r) |> dbg + + sl = State.annotate_vars_with_inferred_types(sl, vars_l_with_inferred_types) + {{:<-, meta, [e_left, e_right]}, {sl, el}} end @@ -2693,6 +2717,7 @@ defmodule ElixirSense.Core.Compiler do {expr, rest_opts} -> pair = {:else, expr} + # no point in doing type inference here, we have no idea what data we are matching against {e_pair, se} = expand_clauses(meta, :with, &head/3, pair, s, e) {[e_pair], rest_opts, se} end @@ -2733,6 +2758,7 @@ defmodule ElixirSense.Core.Compiler do end defp expand_try(meta, {:else, _} = else_clause, s, e) do + # TODO we could try to infer type from last try block expression expand_clauses(meta, :try, &head/3, else_clause, s, e) end @@ -2752,10 +2778,12 @@ defmodule ElixirSense.Core.Compiler do end defp expand_catch(_meta, args = [_], s, e) do + # no point in doing type inference here, we have no idea what throw we caught head(args, s, e) end defp expand_catch(_meta, args = [_, _], s, e) do + # TODO is it worth to infer type of the first arg? :error | :exit | :throw | {:EXIT, pid()} head(args, s, e) end @@ -3331,6 +3359,7 @@ defmodule ElixirSense.Core.Compiler do # elixir raises defaults_in_args s_reset = ElixirEnv.reset_vars(sa) + # no point in doing type inference here, we have no idea what the fn will be called with {e_clause, s_acc, e_acc} = ElixirClauses.clause(meta, :fn, &ElixirClauses.head/3, clause, s_reset, e) diff --git a/lib/elixir_sense/core/guard.ex b/lib/elixir_sense/core/guard.ex index 2500d7c7..77f9c1ca 100644 --- a/lib/elixir_sense/core/guard.ex +++ b/lib/elixir_sense/core/guard.ex @@ -35,6 +35,8 @@ defmodule ElixirSense.Core.Guard do end) end + # TODO does it handle nested when? + def type_information_from_guards(guard_ast, state) do {_, acc} = Macro.prewalk(guard_ast, [], fn diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index a8cf359d..88d544b3 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -1833,4 +1833,10 @@ defmodule ElixirSense.Core.State do end def maybe_add_protocol_behaviour(state, env), do: {state, env} + + def annotate_vars_with_inferred_types(state, vars_with_inferred_types) do + [h | t] = state.vars_info + h = Map.merge(h, vars_with_inferred_types) + %{state | vars_info: [h | t]} + end end diff --git a/lib/elixir_sense/core/type_inference.ex b/lib/elixir_sense/core/type_inference.ex index e614c50d..8da8b428 100644 --- a/lib/elixir_sense/core/type_inference.ex +++ b/lib/elixir_sense/core/type_inference.ex @@ -252,7 +252,6 @@ defmodule ElixirSense.Core.TypeInference do ) when is_atom(var) and is_atom(context) and var not in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__, :_] do - # TODO local calls? version = meta |> Keyword.fetch!(:version) var_info = state.vars_info |> hd |> Map.fetch!({var, version}) @@ -262,12 +261,14 @@ defmodule ElixirSense.Core.TypeInference do end # drop right side of guard expression as guards cannot define vars - defp match_var(state, {:when, _, [left, _right]}, {vars, _match_context}) do - match_var(state, left, {vars, nil}) + defp match_var(state, {:when, _, [left, _right]}, {vars, match_context}) do + # TODO should we infer from guard here? + match_var(state, left, {vars, match_context}) end defp match_var(state, {:%, _, [type_ast, {:%{}, _, ast}]}, {vars, match_context}) when not is_nil(match_context) do + # TODO pass mach_context here as map __struct__ key access {_ast, {type_vars, _match_context}} = match_var(state, type_ast, {[], nil}) destructured_vars = diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index fd4f0713..50aab2a8 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -1700,14 +1700,14 @@ defmodule ElixirSense.Core.MetadataBuilderTest do test "variable binding simple case match context guard" do state = """ - case x do - var when is_map(var) -> + receive do + [v = :ok, var] when is_map(var) -> IO.puts("") end """ |> string_to_state - assert [%VarInfo{type: {:atom, :my_var}}] = state |> get_line_vars(3) + assert [%VarInfo{type: {:atom, :ok}}, %VarInfo{type: {:map, [], []}}] = state |> get_line_vars(3) end test "module attributes value binding to and from variables" do @@ -2028,6 +2028,65 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] = state |> get_line_vars(5) end + test "binding in with expression more complex" do + state = + """ + defmodule MyModule do + @myattribute [:ok, :error, :other] + with a <- @myattribute, + b = Date.utc_now(), + [c | _] <- a do + IO.puts + end + end + """ + |> string_to_state + + assert [ + %VarInfo{name: :a, type: {:attribute, :myattribute}}, + %VarInfo{name: :b, type: {:call, {:atom, Date}, :utc_now, []}}, + %VarInfo{name: :c, type: {:list_head, {:variable, :a}}}, + ] = state |> get_line_vars(6) + end + + test "binding in with expression with guard" do + state = + """ + defmodule MyModule do + @myattribute [:ok, :error, :other] + with [a | _] when is_atom(a) <- @myattribute do + IO.puts + end + end + """ + |> string_to_state + + assert [ + %VarInfo{name: :a, type: {:list_head, {:attribute, :myattribute}}}, + ] = state |> get_line_vars(4) + end + + test "binding in with expression else" do + state = + """ + defmodule MyModule do + @myattribute [:ok, :error, :other] + with a <- @myattribute do + b = a + IO.puts + else + a = :ok -> + IO.puts + end + end + """ + |> string_to_state + + assert [ + %VarInfo{name: :a, type: {:atom, :ok}} + ] = state |> get_line_vars(8) + end + test "vars binding" do state = """ From e530e927eda7029e1401334f29826e49d5070680 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 25 May 2024 11:39:14 +0200 Subject: [PATCH 069/235] infer types from guards --- lib/elixir_sense/core/compiler.ex | 11 +++- lib/elixir_sense/core/guard.ex | 61 +++++++++++++++++-- lib/elixir_sense/core/state.ex | 17 ++++++ .../core/metadata_builder_test.exs | 10 ++- 4 files changed, 90 insertions(+), 9 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index db2e7c19..f408c664 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -5,6 +5,7 @@ defmodule ElixirSense.Core.Compiler do alias ElixirSense.Core.Introspection alias ElixirSense.Core.TypeInfo alias ElixirSense.Core.TypeInference + alias ElixirSense.Core.Guard @env :elixir_env.new() def env, do: @env @@ -1618,13 +1619,17 @@ defmodule ElixirSense.Core.Compiler do {_e_args, state, env_for_expand} = expand_args(args, %{state | prematch: {%{}, 0, :none}}, %{env_for_expand | context: :match}) - {_e_guard, state, env_for_expand} = + {e_guard, state, env_for_expand} = __MODULE__.Clauses.guard( guards, %{state | prematch: :raise}, %{env_for_expand | context: :guard} ) + type_info = Guard.type_information_from_guards(e_guard |> dbg, state) |> dbg + + state = merge_inferred_types(state, type_info) + env_for_expand = %{env_for_expand | context: nil} {position, end_position} = extract_range(meta) @@ -2510,6 +2515,10 @@ defmodule ElixirSense.Core.Compiler do {e_guard, sg, eg} = guard(guard, %{sa | prematch: prematch}, %{ea | context: :guard}) + type_info = Guard.type_information_from_guards(e_guard |> dbg, sg) |> dbg + + sg = merge_inferred_types(sg, type_info) + {{e_args, e_guard}, sg, eg} end, :ok, diff --git a/lib/elixir_sense/core/guard.ex b/lib/elixir_sense/core/guard.ex index 77f9c1ca..703dd2b8 100644 --- a/lib/elixir_sense/core/guard.ex +++ b/lib/elixir_sense/core/guard.ex @@ -14,6 +14,27 @@ defmodule ElixirSense.Core.Guard do # # type information from :and subtrees are mergeable # type information from :or subtrees are discarded + def type_information_from_guards(list, state) when is_list(list) do + for expr <- list, reduce: %{} do + acc -> + right = type_information_from_guards(expr, state) + Map.merge(acc, right, fn _k, v1, v2 -> + case {v1, v2} do + {{:union, types_1}, {:union, types_2}} -> {:union, types_1 ++ types_2} + {{:union, types}, _} -> {:union, types ++ [v2]} + {_, {:union, types}} -> {:union, [v1 | types]} + _ -> {:union, [v1, v2]} + end + end) + end + end + def type_information_from_guards({{:., _, [:erlang, :andalso]}, _, [guard_l, guard_r]}, state) do + left = type_information_from_guards(guard_l, state) + right = type_information_from_guards(guard_r, state) + + Map.merge(left, right, fn _k, v1, v2 -> {:intersection, [v1, v2]} end) + end + # TODO remove? def type_information_from_guards({:and, _, [guard_l, guard_r]}, state) do left = type_information_from_guards(guard_l, state) right = type_information_from_guards(guard_r, state) @@ -21,6 +42,21 @@ defmodule ElixirSense.Core.Guard do Keyword.merge(left, right, fn _k, v1, v2 -> {:intersection, [v1, v2]} end) end + def type_information_from_guards({{:., _, [:erlang, :orelse]}, _, [guard_l, guard_r]}, state) do + left = type_information_from_guards(guard_l, state) + right = type_information_from_guards(guard_r, state) + + Map.merge(left, right, fn _k, v1, v2 -> + case {v1, v2} do + {{:union, types_1}, {:union, types_2}} -> {:union, types_1 ++ types_2} + {{:union, types}, _} -> {:union, types ++ [v2]} + {_, {:union, types}} -> {:union, [v1 | types]} + _ -> {:union, [v1, v2]} + end + end) + end + + # TODO remove def type_information_from_guards({:or, _, [guard_l, guard_r]}, state) do left = type_information_from_guards(guard_l, state) right = type_information_from_guards(guard_r, state) @@ -39,17 +75,32 @@ defmodule ElixirSense.Core.Guard do def type_information_from_guards(guard_ast, state) do {_, acc} = - Macro.prewalk(guard_ast, [], fn + Macro.prewalk(guard_ast, %{}, fn # Standalone variable: func my_func(x) when x - {var, _, nil} = node, acc -> - {node, [{var, :boolean} | acc]} + {var, meta, context} = node, acc when is_atom(var) and is_atom(context) -> + version = Keyword.fetch!(meta, :version) + {node, Map.put(acc, {var, version}, :boolean)} + + {{:., _dot_meta, [:erlang, fun]}, _call_meta, params} = node, acc when is_atom(fun) -> + case guard_predicate_type(fun, params, state) do + {type, binding} -> + {var, meta, _context} = binding + # If we found the predicate type, we can prematurely exit traversing the subtree + version = Keyword.fetch!(meta, :version) + {[], Map.put(acc, {var, version}, type)} + + nil -> + {node, acc} + end + # TODO can we drop this clause? {guard_predicate, _, params} = node, acc -> case guard_predicate_type(guard_predicate, params, state) do {type, binding} -> - {var, _, nil} = binding + {var, meta, _context} = binding # If we found the predicate type, we can prematurely exit traversing the subtree - {[], [{var, type} | acc]} + version = Keyword.fetch!(meta, :version) + {[], Map.put(acc, {var, version}, type)} nil -> {node, acc} diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 88d544b3..67bb7ab4 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -1839,4 +1839,21 @@ defmodule ElixirSense.Core.State do h = Map.merge(h, vars_with_inferred_types) %{state | vars_info: [h | t]} end + + def merge_inferred_types(state, []), do: state + def merge_inferred_types(state, inferred_types) do + [h | t] = state.vars_info + + h = for {{var, version}, type} <- inferred_types, reduce: h do + acc -> + updated_var = case acc[{var, version}] do + %VarInfo{type: nil} = v -> %{v | type: type} + %VarInfo{type: ^type} = v -> v + %VarInfo{type: old_type} = v -> %{v | type: {:intersection, [type, old_type]}} + end + Map.put(acc, {var, version}, updated_var) + end + + %{state | vars_info: [h | t]} + end end diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 50aab2a8..f3680a51 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -1707,7 +1707,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert [%VarInfo{type: {:atom, :ok}}, %VarInfo{type: {:map, [], []}}] = state |> get_line_vars(3) + assert [%VarInfo{type: {:atom, :ok}}, %VarInfo{type: {:map, [], nil}}] = state |> get_line_vars(3) end test "module attributes value binding to and from variables" do @@ -3402,7 +3402,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ defmodule MyModule do def func(x) when #{guard} do - x + IO.puts "" end end """ @@ -3435,6 +3435,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert %VarInfo{name: :x, type: :bitstring} = var_with_guards("byte_size(x) == 1") end + test "multiple guards" do + assert %VarInfo{name: :x, type: {:union, [:bitstring, :number]}} = var_with_guards("is_bitstring(x) when is_integer(x)") + end + test "list guards" do assert %VarInfo{name: :x, type: :list} = var_with_guards("is_list(x)") assert %VarInfo{name: :x, type: {:list, :number}} = var_with_guards("hd(x) == 1") @@ -3482,7 +3486,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do alias URI, as: MyURI def func(x) when is_struct(x, MyURI) do - x + IO.puts "" end end """ From d2b2ce748736c692902931bf5f06625df087e85f Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 25 May 2024 11:39:56 +0200 Subject: [PATCH 070/235] apply inline fix from master --- lib/elixir_sense/core/compiler.ex | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index f408c664..5726cee7 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -680,8 +680,13 @@ defmodule ElixirSense.Core.Compiler do expand_macro(meta, module, fun, args, callback, state, env) {:function, module, fun} -> - # Transform to remote call - we may need to do rewrites - expand({{:., meta, [module, fun]}, meta, args}, state, env) + {ar, af} = case :elixir_rewrite.inline(module, fun, arity) do + {ar, an} -> + {ar, an} + false -> + {module, fun} + end + expand_remote(ar, meta, af, meta, args, state, __MODULE__.Env.prepare_write(state), env) {:error, :not_found} -> expand_local(meta, fun, args, state, env) @@ -2043,9 +2048,6 @@ defmodule ElixirSense.Core.Compiler do defp expand_fn_capture(meta, arg, s, e) do case __MODULE__.Fn.capture(meta, arg, s, e) do {{:remote, remote, fun, arity}, require_meta, dot_meta, se, ee} -> - # if is_atom(remote) do - # ElixirEnv.trace({:remote_function, require_meta, remote, fun, arity}, e) - # end attached_meta = attach_runtime_module(remote, require_meta, s, e) line = Keyword.get(attached_meta, :line, 0) From ff843aed98c0128346e93f0e86ef5b9b3f1064e3 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 25 May 2024 12:19:00 +0200 Subject: [PATCH 071/235] apply master fixes --- lib/elixir_sense/core/compiler.ex | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 5726cee7..c9864b1e 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -3910,7 +3910,9 @@ defmodule ElixirSense.Core.Compiler do defp import_meta(meta, name, arity, q, e) do case Keyword.get(meta, :imports, false) == false && ElixirDispatch.find_imports(meta, name, e) do - [] -> + [_ | _] = imports -> + keystore(:imports, keystore(:context, meta, q.context), imports) + _ -> case arity == 1 && Keyword.fetch(meta, :ambiguous_op) do {:ok, nil} -> keystore(:ambiguous_op, meta, q.context) @@ -3918,9 +3920,6 @@ defmodule ElixirSense.Core.Compiler do _ -> meta end - - imports -> - keystore(:imports, keystore(:context, meta, q.context), imports) end end @@ -4147,13 +4146,19 @@ defmodule ElixirSense.Core.Compiler do case find_import_by_name_arity(meta, tuple, [], e) do {:function, receiver} -> + # TODO trace call? # ElixirEnv.trace({:imported_function, meta, receiver, name, arity}, e) receiver {:macro, receiver} -> + # TODO trace call? # ElixirEnv.trace({:imported_macro, meta, receiver, name, arity}, e) receiver + {:ambiguous, [head | _]} -> + # elixir raises here, we choose first one + # TODO trace call? + head _ -> false end @@ -4187,6 +4192,9 @@ defmodule ElixirSense.Core.Compiler do {:import, receiver} -> require_function(meta, receiver, name, arity, e) + {:ambiguous, ambiguous} -> + raise "ambiguous #{inspect(ambiguous)}" + false -> if Macro.special_form?(name, arity) do false @@ -4273,15 +4281,7 @@ defmodule ElixirSense.Core.Compiler do {[], []} -> false - {_, [receiver | _]} -> - # elixir raises ambiguous_call - # we prefer macro - {:macro, receiver} - - {[receiver | _], _} -> - # elixir raises ambiguous_call - # we prefer macro - {:function, receiver} + _ -> {:ambiguous, fun_match ++ mac_match} end end end @@ -4291,7 +4291,7 @@ defmodule ElixirSense.Core.Compiler do end defp is_import(meta, arity) do - with {:ok, imports} <- Keyword.fetch(meta, :imports), + with {:ok, imports = [_ | _]} <- Keyword.fetch(meta, :imports), {:ok, _} <- Keyword.fetch(meta, :context), {:ok, receiver} <- Keyword.fetch(imports, arity) do {:import, receiver} From f477291ac59085681b477e3d5efde82974a1c1d5 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 25 May 2024 22:59:58 +0200 Subject: [PATCH 072/235] apply master fixes --- lib/elixir_sense/core/compiler.ex | 276 +++++++++++++++--------------- 1 file changed, 137 insertions(+), 139 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index c9864b1e..c1cb4a56 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -332,15 +332,13 @@ defmodule ElixirSense.Core.Compiler do end defp do_expand({:quote, meta, [opts]}, s, e) when is_list(opts) do - case Keyword.fetch(opts, :do) do - {:ok, do_block} -> - new_opts = Keyword.delete(opts, :do) - expand({:quote, meta, [new_opts, [{:do, do_block}]]}, s, e) - - :error -> + case Keyword.pop(opts, :do) do + {nil, _} -> # elixir raises here missing_option # generate a fake do block expand({:quote, meta, [opts, [{:do, {:__block__, [], []}}]]}, s, e) + {do_block, new_opts} -> + expand({:quote, meta, [new_opts, [{:do, do_block}]]}, s, e) end end @@ -368,7 +366,7 @@ defmodule ElixirSense.Core.Compiler do {file, line} = case Keyword.fetch(e_opts, :location) do - {:ok, :keep} -> {e.file, false} + {:ok, :keep} -> {e.file, true} :error -> {Keyword.get(e_opts, :file, nil), Keyword.get(e_opts, :line, false)} end @@ -394,11 +392,27 @@ defmodule ElixirSense.Core.Compiler do # res = expand_quote(exprs, st, et) # res |> elem(0) |> IO.inspect # res - {q, prelude} = - __MODULE__.Quote.build(meta, line, file, context, unquote_opt, generated) + {q, q_context, q_prelude} = + __MODULE__.Quote.build(meta, line, file, context, unquote_opt, generated, et) - quoted = __MODULE__.Quote.quote(meta, exprs, binding, q, prelude, et) - expand(quoted, st, et) + {e_prelude, sp, ep} = expand(q_prelude, st, et) + {e_context, sc, ec} = expand(q_context, sp, ep) + quoted = __MODULE__.Quote.quote(exprs, q) + {e_quoted, es, eq} = expand(quoted, sc, ec) + + e_binding = for {k, v} <- binding do + {:{}, [], [:=, [], [{:{}, [], [k, meta, e_context]}, v]]} + end + + e_binding_quoted = case e_binding do + [] -> e_quoted + _ -> {:{}, [], [:__block__, [], e_binding ++ [e_quoted]]} + end + + case e_prelude do + [] -> {e_binding_quoted, es, eq} + _ -> {{:__block__, [], e_prelude ++ [e_binding_quoted]}, es, eq} + end end defp do_expand({:quote, meta, [arg1, arg2]}, s, e) do @@ -3587,9 +3601,9 @@ defmodule ElixirSense.Core.Compiler do defstruct line: false, file: nil, context: nil, - vars_hygiene: true, - aliases_hygiene: true, - imports_hygiene: true, + op: :none, + aliases_hygiene: nil, + imports_hygiene: nil, unquote: true, generated: false @@ -3644,25 +3658,28 @@ defmodule ElixirSense.Core.Compiler do defp disables_unquote([_h | t]), do: disables_unquote(t) defp disables_unquote(_), do: false - def build(meta, line, file, context, unquote, generated) do + def build(meta, line, file, context, unquote, generated, e) do acc0 = [] - {e_line, acc1} = validate_compile(meta, :line, line, acc0) - {e_file, acc2} = validate_compile(meta, :file, file, acc1) - {e_context, acc3} = validate_compile(meta, :context, context, acc2) + {v_line, acc1} = validate_compile(meta, :line, line, acc0) + {v_file, acc2} = validate_compile(meta, :file, file, acc1) + {v_context, acc3} = validate_compile(meta, :context, context, acc2) unquote = validate_runtime(:unquote, unquote) generated = validate_runtime(:generated, generated) q = %__MODULE__{ - line: e_line, - file: e_file, + op: :add_context, + aliases_hygiene: e, + imports_hygiene: e, + line: v_line, + file: v_file, unquote: unquote, - context: e_context, + context: v_context, generated: generated } - {q, acc3} + {q, v_context, acc3} end defp validate_compile(_meta, :line, value, acc) when is_boolean(value) do @@ -3704,57 +3721,36 @@ defmodule ElixirSense.Core.Compiler do defp default(:unquote), do: true defp default(:generated), do: false - def escape(expr, kind, unquote) do + def escape(expr, op, unquote) do do_quote( expr, %__MODULE__{ line: true, file: nil, - vars_hygiene: false, - aliases_hygiene: false, - imports_hygiene: false, + op: op, unquote: unquote - }, - kind + } ) end - def quote(meta, {:unquote_splicing, _, [_]} = expr, binding, %__MODULE__{unquote: true} = q, prelude, e) do + def quote({:unquote_splicing, _, [_]} = expr, %__MODULE__{unquote: true} = q) do # elixir raises here unquote_splicing only works inside arguments and block contexts # try to recover from error by wrapping it in block - quote(meta, {:__block__, [], [expr]}, binding, q, prelude, e) + __MODULE__.quote({:__block__, [], [expr]}, q) end - def quote(meta, expr, binding, q, prelude, e) do - context = q.context - - vars = - Enum.map(binding, fn {k, v} -> - {:{}, [], [:=, [], [{:{}, [], [k, meta, context]}, v]]} - end) - - quoted = do_quote(expr, q, e) - - with_vars = - case vars do - [] -> quoted - _ -> {:{}, [], [:__block__, [], vars ++ [quoted]]} - end - - case prelude do - [] -> with_vars - _ -> {:__block__, [], prelude ++ [with_vars]} - end + def quote(expr, q) do + do_quote(expr, q) end # quote/unquote - defp do_quote({:quote, meta, [arg]}, q, e) do - t_arg = do_quote(arg, %__MODULE__{q | unquote: false}, e) + defp do_quote({:quote, meta, [arg]}, q) when is_list(meta) do + t_arg = do_quote(arg, %__MODULE__{q | unquote: false}) new_meta = case q do - %__MODULE__{vars_hygiene: true, context: context} -> + %__MODULE__{op: :add_context, context: context} -> keystore(:context, meta, context) _ -> @@ -3764,13 +3760,13 @@ defmodule ElixirSense.Core.Compiler do {:{}, [], [:quote, meta(new_meta, q), [t_arg]]} end - defp do_quote({:quote, meta, [opts, arg]}, q, e) do - t_opts = do_quote(opts, q, e) - t_arg = do_quote(arg, %__MODULE__{q | unquote: false}, e) + defp do_quote({:quote, meta, [opts, arg]}, q) when is_list(meta) do + t_opts = do_quote(opts, q) + t_arg = do_quote(arg, %__MODULE__{q | unquote: false}) new_meta = case q do - %__MODULE__{vars_hygiene: true, context: context} -> + %__MODULE__{op: :add_context, context: context} -> keystore(:context, meta, context) _ -> @@ -3780,12 +3776,12 @@ defmodule ElixirSense.Core.Compiler do {:{}, [], [:quote, meta(new_meta, q), [t_opts, t_arg]]} end - defp do_quote({:unquote, _meta, [expr]}, %__MODULE__{unquote: true}, _), do: expr + defp do_quote({:unquote, meta, [expr]}, %__MODULE__{unquote: true}) when is_list(meta), do: expr # Aliases - defp do_quote({:__aliases__, meta, [h | t] = list}, %__MODULE__{aliases_hygiene: true} = q, e) - when is_atom(h) and h != :"Elixir" do + defp do_quote({:__aliases__, meta, [h | t] = list}, %__MODULE__{aliases_hygiene: e = %{}} = q) + when is_atom(h) and h != :"Elixir" and is_list(meta) do annotation = case Macro.Env.expand_alias(e, meta, list, trace: false) do {:alias, atom} -> atom @@ -3793,18 +3789,17 @@ defmodule ElixirSense.Core.Compiler do end alias_meta = keystore(:alias, Keyword.delete(meta, :counter), annotation) - do_quote_tuple(:__aliases__, alias_meta, [h | t], q, e) + do_quote_tuple(:__aliases__, alias_meta, [h | t], q) end # Vars - defp do_quote({name, meta, nil}, %__MODULE__{vars_hygiene: true} = q, e) + defp do_quote({name, meta, nil}, %__MODULE__{op: :add_context} = q) when is_atom(name) and is_list(meta) do import_meta = - if q.imports_hygiene do - import_meta(meta, name, 0, q, e) - else - meta + case q.imports_hygiene do + nil -> meta + e -> import_meta(meta, name, 0, q, e) end {:{}, [], [name, meta(import_meta, q), q.context]} @@ -3814,8 +3809,7 @@ defmodule ElixirSense.Core.Compiler do defp do_quote( {:__cursor__, meta, args}, - %__MODULE__{unquote: _}, - _e + %__MODULE__{unquote: _} ) when is_list(args) do # emit cursor as is regardless of unquote {:__cursor__, meta, args} @@ -3825,24 +3819,22 @@ defmodule ElixirSense.Core.Compiler do defp do_quote( {{{:., meta, [left, :unquote]}, _, [expr]}, _, args}, - %__MODULE__{unquote: true} = q, - e - ) do - do_quote_call(left, meta, expr, args, q, e) + %__MODULE__{unquote: true} = q + ) when is_list(meta) do + do_quote_call(left, meta, expr, args, q) end - defp do_quote({{:., meta, [left, :unquote]}, _, [expr]}, %__MODULE__{unquote: true} = q, e) do - do_quote_call(left, meta, expr, nil, q, e) + defp do_quote({{:., meta, [left, :unquote]}, _, [expr]}, %__MODULE__{unquote: true} = q) when is_list(meta) do + do_quote_call(left, meta, expr, nil, q) end # Imports defp do_quote( {:&, meta, [{:/, _, [{f, _, c}, a]}] = args}, - %__MODULE__{imports_hygiene: true} = q, - e + %__MODULE__{imports_hygiene: e = %{}} = q ) - when is_atom(f) and is_integer(a) and is_atom(c) do + when is_atom(f) and is_integer(a) and is_atom(c) and is_list(meta) do new_meta = case ElixirDispatch.find_import(meta, f, a, e) do false -> @@ -3852,10 +3844,10 @@ defmodule ElixirSense.Core.Compiler do keystore(:context, keystore(:imports, meta, [{a, receiver}]), q.context) end - do_quote_tuple(:&, new_meta, args, q, e) + do_quote_tuple(:&, new_meta, args, q) end - defp do_quote({name, meta, args_or_context}, %__MODULE__{imports_hygiene: true} = q, e) + defp do_quote({name, meta, args_or_context}, %__MODULE__{imports_hygiene: e = %{}} = q) when is_atom(name) and is_list(meta) and (is_list(args_or_context) or is_atom(args_or_context)) do arity = @@ -3866,46 +3858,46 @@ defmodule ElixirSense.Core.Compiler do import_meta = import_meta(meta, name, arity, q, e) annotated = annotate({name, import_meta, args_or_context}, q.context) - do_quote_tuple(annotated, q, e) + do_quote_tuple(annotated, q) end # Two-element tuples - defp do_quote({left, right}, %__MODULE__{unquote: true} = q, e) + defp do_quote({left, right}, %__MODULE__{unquote: true} = q) when is_tuple(left) and elem(left, 0) == :unquote_splicing and is_tuple(right) and elem(right, 0) == :unquote_splicing do - do_quote({:{}, [], [left, right]}, q, e) + do_quote({:{}, [], [left, right]}, q) end - defp do_quote({left, right}, q, e) do - t_left = do_quote(left, q, e) - t_right = do_quote(right, q, e) + defp do_quote({left, right}, q) do + t_left = do_quote(left, q) + t_right = do_quote(right, q) {t_left, t_right} end # Everything else - defp do_quote(other, q, e) when is_atom(e) do - do_escape(other, q, e) + defp do_quote(other, q = %{op: op}) when op != :add_context do + do_escape(other, q) end - defp do_quote({_, _, _} = tuple, q, e) do + defp do_quote({_, _, _} = tuple, q) do annotated = annotate(tuple, q.context) - do_quote_tuple(annotated, q, e) + do_quote_tuple(annotated, q) end - defp do_quote([], _, _), do: [] + defp do_quote([], _), do: [] - defp do_quote([h | t], %__MODULE__{unquote: false} = q, e) do - head_quoted = do_quote(h, q, e) - do_quote_simple_list(t, head_quoted, q, e) + defp do_quote([h | t], %__MODULE__{unquote: false} = q) do + head_quoted = do_quote(h, q) + do_quote_simple_list(t, head_quoted, q) end - defp do_quote([h | t], q, e) do - do_quote_tail(:lists.reverse(t, [h]), q, e) + defp do_quote([h | t], q) do + do_quote_tail(:lists.reverse(t, [h]), q) end - defp do_quote(other, _, _), do: other + defp do_quote(other, _), do: other defp import_meta(meta, name, arity, q, e) do case Keyword.get(meta, :imports, false) == false && @@ -3923,63 +3915,61 @@ defmodule ElixirSense.Core.Compiler do end end - defp do_quote_call(left, meta, expr, args, q, e) do + defp do_quote_call(left, meta, expr, args, q) do all = [left, {:unquote, meta, [expr]}, args, q.context] - tall = Enum.map(all, fn x -> do_quote(x, q, e) end) + tall = Enum.map(all, fn x -> do_quote(x, q) end) {{:., meta, [:elixir_quote, :dot]}, meta, [meta(meta, q) | tall]} end - defp do_quote_tuple({left, meta, right}, q, e) do - do_quote_tuple(left, meta, right, q, e) + defp do_quote_tuple({left, meta, right}, q) do + do_quote_tuple(left, meta, right, q) end - defp do_quote_tuple(left, meta, right, q, e) do - t_left = do_quote(left, q, e) - t_right = do_quote(right, q, e) + defp do_quote_tuple(left, meta, right, q) do + t_left = do_quote(left, q) + t_right = do_quote(right, q) {:{}, [], [t_left, meta(meta, q), t_right]} end - defp do_quote_simple_list([], prev, _, _), do: [prev] + defp do_quote_simple_list([], prev, _), do: [prev] - defp do_quote_simple_list([h | t], prev, q, e) do - [prev | do_quote_simple_list(t, do_quote(h, q, e), q, e)] + defp do_quote_simple_list([h | t], prev, q) do + [prev | do_quote_simple_list(t, do_quote(h, q), q)] end - defp do_quote_simple_list(other, prev, q, e) do - [{:|, [], [prev, do_quote(other, q, e)]}] + defp do_quote_simple_list(other, prev, q) do + [{:|, [], [prev, do_quote(other, q)]}] end defp do_quote_tail( [{:|, meta, [{:unquote_splicing, _, [left]}, right]} | t], - %__MODULE__{unquote: true} = q, - e + %__MODULE__{unquote: true} = q ) do - tt = do_quote_splice(t, q, e, [], []) - tr = do_quote(right, q, e) + tt = do_quote_splice(t, q, [], []) + tr = do_quote(right, q) do_runtime_list(meta, :tail_list, [left, tr, tt]) end - defp do_quote_tail(list, q, e) do - do_quote_splice(list, q, e, [], []) + defp do_quote_tail(list, q) do + do_quote_splice(list, q, [], []) end defp do_quote_splice( [{:unquote_splicing, meta, [expr]} | t], %__MODULE__{unquote: true} = q, - e, buffer, acc ) do runtime = do_runtime_list(meta, :list, [expr, do_list_concat(buffer, acc)]) - do_quote_splice(t, q, e, [], runtime) + do_quote_splice(t, q, [], runtime) end - defp do_quote_splice([h | t], q, e, buffer, acc) do - th = do_quote(h, q, e) - do_quote_splice(t, q, e, [th | buffer], acc) + defp do_quote_splice([h | t], q, buffer, acc) do + th = do_quote(h, q) + do_quote_splice(t, q, [th | buffer], acc) end - defp do_quote_splice([], _q, _e, buffer, acc) do + defp do_quote_splice([], _q, buffer, acc) do do_list_concat(buffer, acc) end @@ -4005,7 +3995,7 @@ defmodule ElixirSense.Core.Compiler do line(meta, line) end - defp keep(meta, %__MODULE__{file: file}) do + defp keep(meta, %__MODULE__{file: file, line: true}) do case Keyword.pop(meta, :line) do {nil, _} -> [{:keep, {file, 0}} | meta] @@ -4015,6 +4005,14 @@ defmodule ElixirSense.Core.Compiler do end end + defp keep(meta, %__MODULE__{file: file, line: false}) do + [{:keep, {file, 0}} | Keyword.delete(meta, :line)] + end + + defp keep(meta, %__MODULE__{file: file, line: line}) do + [{:keep, {file, line}} | Keyword.delete(meta, :line)] + end + defp line(meta, true), do: meta defp line(meta, false) do @@ -4053,19 +4051,19 @@ defmodule ElixirSense.Core.Compiler do defp annotate_def(other, _context), do: other - defp do_escape({left, meta, right}, q, e = :prune_metadata) do + defp do_escape({left, meta, right}, q = %{op: :prune_metadata}) when is_list(meta) do tm = for {k, v} <- meta, k == :no_parens or k == :line, do: {k, v} - tl = do_quote(left, q, e) - tr = do_quote(right, q, e) + tl = do_quote(left, q) + tr = do_quote(right, q) {:{}, [], [tl, tm, tr]} end - defp do_escape(tuple, q, e) when is_tuple(tuple) do - tt = do_quote(Tuple.to_list(tuple), q, e) + defp do_escape(tuple, q) when is_tuple(tuple) do + tt = do_quote(Tuple.to_list(tuple), q) {:{}, [], tt} end - defp do_escape(bitstring, _, _) when is_bitstring(bitstring) do + defp do_escape(bitstring, _) when is_bitstring(bitstring) do case Bitwise.band(bit_size(bitstring), 7) do 0 -> bitstring @@ -4078,35 +4076,35 @@ defmodule ElixirSense.Core.Compiler do end end - defp do_escape(map, q, e) when is_map(map) do - tt = do_quote(Enum.sort(Map.to_list(map)), q, e) + defp do_escape(map, q) when is_map(map) do + tt = do_quote(Enum.sort(Map.to_list(map)), q) {:%{}, [], tt} end - defp do_escape([], _, _), do: [] + defp do_escape([], _), do: [] - defp do_escape([h | t], %__MODULE__{unquote: false} = q, e) do - do_quote_simple_list(t, do_quote(h, q, e), q, e) + defp do_escape([h | t], %__MODULE__{unquote: false} = q) do + do_quote_simple_list(t, do_quote(h, q), q) end - defp do_escape([h | t], q, e) do + defp do_escape([h | t], q) do # The improper case is inefficient, but improper lists are rare. try do l = Enum.reverse(t, [h]) - do_quote_tail(l, q, e) + do_quote_tail(l, q) catch _ -> {l, r} = reverse_improper(t, [h]) - tl = do_quote_splice(l, q, e, [], []) - tr = do_quote(r, q, e) + tl = do_quote_splice(l, q, [], []) + tr = do_quote(r, q) update_last(tl, fn x -> {:|, [], [x, tr]} end) end end - defp do_escape(other, _, _) when is_number(other) or is_pid(other) or is_atom(other), + defp do_escape(other, _) when is_number(other) or is_pid(other) or is_atom(other), do: other - defp do_escape(fun, _, _) when is_function(fun) do + defp do_escape(fun, _) when is_function(fun) do case {Function.info(fun, :env), Function.info(fun, :type)} do {{:env, []}, {:type, :external}} -> fun_to_quoted(fun) @@ -4117,7 +4115,7 @@ defmodule ElixirSense.Core.Compiler do end end - defp do_escape(_other, _, _) do + defp do_escape(_other, _) do # elixir raises here ArgumentError nil end From e499d25ffc1c5a0e5cf0b22511e08699197e7468 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 25 May 2024 23:13:27 +0200 Subject: [PATCH 073/235] fix crash on bitstring type inference --- lib/elixir_sense/core/guard.ex | 31 +++++++++++++------------ lib/elixir_sense/core/type_inference.ex | 28 ++++++++++++++-------- 2 files changed, 34 insertions(+), 25 deletions(-) diff --git a/lib/elixir_sense/core/guard.ex b/lib/elixir_sense/core/guard.ex index 703dd2b8..beb369f7 100644 --- a/lib/elixir_sense/core/guard.ex +++ b/lib/elixir_sense/core/guard.ex @@ -78,32 +78,33 @@ defmodule ElixirSense.Core.Guard do Macro.prewalk(guard_ast, %{}, fn # Standalone variable: func my_func(x) when x {var, meta, context} = node, acc when is_atom(var) and is_atom(context) -> - version = Keyword.fetch!(meta, :version) - {node, Map.put(acc, {var, version}, :boolean)} + case Keyword.fetch(meta, :version) do + {:ok, version} -> + {node, Map.put(acc, {var, version}, :boolean)} + _ -> + {node, acc} + end {{:., _dot_meta, [:erlang, fun]}, _call_meta, params} = node, acc when is_atom(fun) -> - case guard_predicate_type(fun, params, state) do - {type, binding} -> - {var, meta, _context} = binding + with {type, binding} <- guard_predicate_type(fun, params, state), + {var, meta, context} when is_atom(var) and is_atom(context) <- binding, + {:ok, version} <- Keyword.fetch(meta, :version) do # If we found the predicate type, we can prematurely exit traversing the subtree - version = Keyword.fetch!(meta, :version) {[], Map.put(acc, {var, version}, type)} - - nil -> - {node, acc} + else + _ -> {node, acc} end # TODO can we drop this clause? {guard_predicate, _, params} = node, acc -> - case guard_predicate_type(guard_predicate, params, state) do - {type, binding} -> - {var, meta, _context} = binding + with {type, binding} <- guard_predicate_type(guard_predicate, params, state), + {var, meta, context} when is_atom(var) and is_atom(context) <- binding, + {:ok, version} <- Keyword.fetch(meta, :version) do # If we found the predicate type, we can prematurely exit traversing the subtree - version = Keyword.fetch!(meta, :version) {[], Map.put(acc, {var, version}, type)} - nil -> - {node, acc} + else + _ -> {node, acc} end node, acc -> diff --git a/lib/elixir_sense/core/type_inference.ex b/lib/elixir_sense/core/type_inference.ex index 8da8b428..97d0bd7b 100644 --- a/lib/elixir_sense/core/type_inference.ex +++ b/lib/elixir_sense/core/type_inference.ex @@ -232,32 +232,40 @@ defmodule ElixirSense.Core.TypeInference do defp match_var( state, - {:^, _meta, [{var, meta, context}]}, + {:^, _meta, [{var, meta, context}]} = ast, {vars, match_context} ) when is_atom(var) and is_atom(context) and var not in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__, :_] do - version = meta |> Keyword.fetch!(:version) - var_info = state.vars_info |> hd |> Map.fetch!({var, version}) + case Keyword.fetch(meta, :version) do + {:ok, version} -> + var_info = state.vars_info |> hd |> Map.fetch!({var, version}) - var_info = %VarInfo{var_info | type: match_context} + var_info = %VarInfo{var_info | type: match_context} - {nil, {[{{var, version}, var_info} | vars], nil}} + {nil, {[{{var, version}, var_info} | vars], nil}} + _ -> + {ast, {vars, match_context}} + end end defp match_var( state, - {var, meta, context}, + {var, meta, context} = ast, {vars, match_context} ) when is_atom(var) and is_atom(context) and var not in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__, :_] do - version = meta |> Keyword.fetch!(:version) - var_info = state.vars_info |> hd |> Map.fetch!({var, version}) + case Keyword.fetch(meta, :version) do + {:ok, version} -> + var_info = state.vars_info |> hd |> Map.fetch!({var, version}) - var_info = %VarInfo{var_info | type: match_context} + var_info = %VarInfo{var_info | type: match_context} - {nil, {[{{var, version}, var_info} | vars], nil}} + {nil, {[{{var, version}, var_info} | vars], nil}} + _ -> + {ast, {vars, match_context}} + end end # drop right side of guard expression as guards cannot define vars From 54b0daf42211ca3d7783dd06b54211570bee1c1b Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 25 May 2024 23:38:35 +0200 Subject: [PATCH 074/235] type inference in rescue clause --- lib/elixir_sense/core/compiler.ex | 40 ++++++++++++++++--- .../core/metadata_builder_test.exs | 39 ++++++++++++------ 2 files changed, 61 insertions(+), 18 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index c1cb4a56..4c2e50e2 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -2832,8 +2832,14 @@ defmodule ElixirSense.Core.Compiler do # rescue var defp expand_rescue({name, _, atom} = var, s, e) when is_atom(name) and is_atom(atom) do - match(&ElixirExpand.expand/3, var, s, s, e) - # TODO infer type to Exception here + {e_left, sl, el} = match(&ElixirExpand.expand/3, var, s, s, e) + + match_context = {:struct, [], {:atom, Exception}, nil} + + vars_with_inferred_types = TypeInference.find_vars(sl, e_left, match_context) + sl = State.annotate_vars_with_inferred_types(sl, vars_with_inferred_types) + + {e_left, sl, el} end # rescue Alias => _ in [Alias] @@ -2848,8 +2854,14 @@ defmodule ElixirSense.Core.Compiler do e ) when is_atom(name) and is_atom(var_context) and is_atom(underscore_context) do - match(&ElixirExpand.expand/3, var, s, s, e) - # TODO infer type to Exception here + {e_left, sl, el} = match(&ElixirExpand.expand/3, var, s, s, e) + + match_context = {:struct, [], {:atom, Exception}, nil} + + vars_with_inferred_types = TypeInference.find_vars(sl, e_left, match_context) + sl = State.annotate_vars_with_inferred_types(sl, vars_with_inferred_types) + + {e_left, sl, el} end # rescue var in (list() or atom()) @@ -2859,8 +2871,24 @@ defmodule ElixirSense.Core.Compiler do case e_left do {name, _, atom} when is_atom(name) and is_atom(atom) -> - {{:in, meta, [e_left, normalize_rescue(e_right, e)]}, sr, er} - # TODO infer type + normalized = normalize_rescue(e_right, e) + + match_context = for exception <- normalized, reduce: nil do + nil -> {:struct, [], {:atom, exception}, nil} + other -> {:union, [other, {:struct, [], {:atom, exception}, nil}]} + end + + match_context = if match_context == nil do + {:struct, [], {:atom, Exception}, nil} + else + match_context + end + + vars_with_inferred_types = TypeInference.find_vars(sl, e_left, match_context) + sr = State.annotate_vars_with_inferred_types(sr, vars_with_inferred_types) + + {{:in, meta, [e_left, normalized]}, sr, er} + _ -> # elixir rejects this case, we normalize to underscore diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index f3680a51..4df992cf 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -2435,16 +2435,18 @@ defmodule ElixirSense.Core.MetadataBuilderTest do Some.call() rescue e0 in ArgumentError -> - :ok + IO.puts "" e1 in [ArgumentError] -> - :ok + IO.puts "" e2 in [RuntimeError, Enum.EmptyError] -> - :ok - e3 -> - :ok + IO.puts "" + e3 in _ -> + IO.puts "" + e4 -> + IO.puts "" else a -> - :ok + IO.puts "" end end end @@ -2456,35 +2458,48 @@ defmodule ElixirSense.Core.MetadataBuilderTest do name: :e0, type: {:struct, [], {:atom, ArgumentError}, nil} } - ] = state |> get_line_vars(6) + ] = state |> get_line_vars(7) assert [ %VarInfo{ name: :e1, type: {:struct, [], {:atom, ArgumentError}, nil} } - ] = state |> get_line_vars(8) + ] = state |> get_line_vars(9) assert [ %VarInfo{ name: :e2, - type: {:struct, [], {:atom, Exception}, nil} + type: { + :union, + [ + {:struct, [], {:atom, RuntimeError}, nil}, + {:struct, [], {:atom, Enum.EmptyError}, nil} + ] + } } - ] = state |> get_line_vars(10) + ] = state |> get_line_vars(11) assert [ %VarInfo{ name: :e3, type: {:struct, [], {:atom, Exception}, nil} } - ] = state |> get_line_vars(12) + ] = state |> get_line_vars(13) + + assert [ + %VarInfo{ + name: :e4, + type: {:struct, [], {:atom, Exception}, nil} + } + ] = state |> get_line_vars(15) assert [ %VarInfo{ name: :a, type: nil } - ] = state |> get_line_vars(15) + ] = state |> get_line_vars(18) end test "vars binding by pattern matching with pin operators" do From 35070916dc8e9f698855f1cbdf966892d68b977d Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 25 May 2024 23:48:00 +0200 Subject: [PATCH 075/235] fix invalid test --- lib/elixir_sense/core/guard.ex | 2 -- test/elixir_sense/core/metadata_builder_test.exs | 12 ------------ 2 files changed, 14 deletions(-) diff --git a/lib/elixir_sense/core/guard.ex b/lib/elixir_sense/core/guard.ex index beb369f7..b9f509b7 100644 --- a/lib/elixir_sense/core/guard.ex +++ b/lib/elixir_sense/core/guard.ex @@ -71,8 +71,6 @@ defmodule ElixirSense.Core.Guard do end) end - # TODO does it handle nested when? - def type_information_from_guards(guard_ast, state) do {_, acc} = Macro.prewalk(guard_ast, %{}, fn diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 4df992cf..82d8dbc7 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -2285,10 +2285,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do state |> get_line_vars(9) |> Enum.find(&(&1.name == :asd)) assert [ - %VarInfo{ - name: :asd, - type: {:struct, [{:sub, {:atom, Atom}}], {:atom, Some}, nil} - }, %VarInfo{ name: :asd, type: {:struct, [{:sub, {:atom, Atom}}], {:atom, Other}, {:variable, :a}} @@ -2296,14 +2292,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] = state |> get_line_vars(11) |> Enum.filter(&(&1.name == :asd)) assert [ - %VarInfo{ - name: :asd, - type: {:struct, [{:sub, {:atom, Atom}}], {:atom, Some}, nil} - }, - %VarInfo{ - name: :asd, - type: {:struct, [{:sub, {:atom, Atom}}], {:atom, Other}, {:variable, :a}} - }, %VarInfo{ name: :asd, type: {:map, [{:other, {:integer, 123}}], {:variable, :asd}} From a9bbdd8d7c6e3b73d482bcdd7a56a77c8b0e3dcc Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 26 May 2024 07:49:47 +0200 Subject: [PATCH 076/235] fix test --- lib/elixir_sense/core/type_inference.ex | 29 ++++++++++++++++--------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/lib/elixir_sense/core/type_inference.ex b/lib/elixir_sense/core/type_inference.ex index 97d0bd7b..aa653ffd 100644 --- a/lib/elixir_sense/core/type_inference.ex +++ b/lib/elixir_sense/core/type_inference.ex @@ -29,10 +29,11 @@ defmodule ElixirSense.Core.TypeInference do end # pipe - def get_binding_type(state, {:|>, _, [params_1, {call, meta, params_rest}]}) do - params = [params_1 | params_rest || []] - get_binding_type(state, {call, meta, params}) - end + # TODO no pipes in expanded code + # def get_binding_type(state, {:|>, _, [params_1, {call, meta, params_rest}]}) do + # params = [params_1 | params_rest || []] + # get_binding_type(state, {call, meta, params}) + # end # remote call def get_binding_type(state, {{:., _, [target, fun]}, _, args}) @@ -41,12 +42,14 @@ defmodule ElixirSense.Core.TypeInference do {:call, target, fun, Enum.map(args, &get_binding_type(state, &1))} end + # TODO no __MODULE__ in expanded code # # current module # def get_binding_type(state, {:__MODULE__, _, nil} = module) do # {module, _state, _env} = expand(module, state) # {:atom, module} # end + # TODO no expandable __aliases__ in expanded code # # elixir module # def get_binding_type(state, {:__aliases__, _, list} = module) when is_list(list) do # try do @@ -58,11 +61,13 @@ defmodule ElixirSense.Core.TypeInference do # end # variable or local no parens call - def get_binding_type(_state, {var, _, nil}) when is_atom(var) do + # TODO version? + def get_binding_type(_state, {var, _, context}) when is_atom(var) and is_atom(context) do {:variable, var} end # attribute + # expanded attribute reference has nil arg def get_binding_type(_state, {:@, _, [{attribute, _, nil}]}) when is_atom(attribute) do {:attribute, attribute} @@ -296,13 +301,17 @@ defmodule ElixirSense.Core.TypeInference do defp match_var(state, {:%{}, _, ast}, {vars, match_context}) when not is_nil(match_context) do destructured_vars = ast - |> Enum.flat_map(fn {key, value_ast} -> - key_type = get_binding_type(state, key) + |> Enum.flat_map(fn + {:|, _, [_left, _right]} -> + # map update is forbidden in match, we're in invalid code + [] + {key, value_ast} -> + key_type = get_binding_type(state, key) - {_ast, {new_vars, _match_context}} = - match_var(state, value_ast, {[], {:map_key, match_context, key_type}}) + {_ast, {new_vars, _match_context}} = + match_var(state, value_ast, {[], {:map_key, match_context, key_type}}) - new_vars + new_vars end) {ast, {vars ++ destructured_vars, nil}} From d4077cb144fa161112de481c18d690cc73f43e88 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 26 May 2024 08:08:51 +0200 Subject: [PATCH 077/235] enable rewrite only in tests --- lib/elixir_sense/core/compiler.ex | 69 +++++++++++++++--------- test/elixir_sense/core/compiler_test.exs | 8 +++ 2 files changed, 51 insertions(+), 26 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 4c2e50e2..a5b9f578 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -694,7 +694,7 @@ defmodule ElixirSense.Core.Compiler do expand_macro(meta, module, fun, args, callback, state, env) {:function, module, fun} -> - {ar, af} = case :elixir_rewrite.inline(module, fun, arity) do + {ar, af} = case __MODULE__.Rewrite.inline(module, fun, arity) do {ar, an} -> {ar, an} false -> @@ -722,7 +722,7 @@ defmodule ElixirSense.Core.Compiler do arity = length(args) if is_atom(module) do - case :elixir_rewrite.inline(module, fun, arity) do + case __MODULE__.Rewrite.inline(module, fun, arity) do {ar, an} -> expand_remote(ar, dot_meta, an, meta, args, state, state_l, env) @@ -1811,7 +1811,7 @@ defmodule ElixirSense.Core.Compiler do attached_meta = attach_runtime_module(receiver, meta, s, e) {e_args, {sa, _}, ea} = map_fold(&expand_arg/3, {sl, s}, e, args) - case rewrite(context, receiver, dot_meta, right, attached_meta, e_args, s) do + case __MODULE__.Rewrite.rewrite(context, receiver, dot_meta, right, attached_meta, e_args, s) do {:ok, rewritten} -> s = __MODULE__.Env.close_write(sa, s) @@ -1853,22 +1853,6 @@ defmodule ElixirSense.Core.Compiler do end end - defp rewrite(_, :erlang, _, :+, _, [arg], _s) when is_number(arg), do: {:ok, arg} - - defp rewrite(_, :erlang, _, :-, _, [arg], _s) when is_number(arg), do: {:ok, -arg} - - defp rewrite(:match, receiver, dot_meta, right, meta, e_args, _s) do - :elixir_rewrite.match_rewrite(receiver, dot_meta, right, meta, e_args) - end - - defp rewrite(:guard, receiver, dot_meta, right, meta, e_args, s) do - :elixir_rewrite.guard_rewrite(receiver, dot_meta, right, meta, e_args, guard_context(s)) - end - - defp rewrite(_, receiver, dot_meta, right, meta, e_args, _s) do - {:ok, :elixir_rewrite.rewrite(receiver, dot_meta, right, meta, e_args)} - end - defp expand_local(meta, :when, [_, _] = args, state, env = %{context: nil}) do # naked when, try to transform into a case ast = @@ -2295,10 +2279,6 @@ defmodule ElixirSense.Core.Compiler do end end - # TODO probably we can remove it/hardcode, used only for generating error message - defp guard_context(%{prematch: {_, _, {:bitsize, _}}}), do: "bitstring size specifier" - defp guard_context(_), do: "guard" - defp expand_case(meta, expr, opts, s, e) do {e_expr, se, ee} = expand(expr, s, e) @@ -4165,6 +4145,7 @@ defmodule ElixirSense.Core.Compiler do end defmodule Dispatch do + alias ElixirSense.Core.Compiler.Rewrite, as: ElixirRewrite import :ordsets, only: [is_element: 2] def find_import(meta, name, arity, e) do @@ -4253,9 +4234,7 @@ defmodule ElixirSense.Core.Compiler do end defp remote_function(_meta, receiver, name, arity, _e) do - # TODO rewrite is safe to use as it does not emit traces and does not have side effects - # but we may need to translate it anyway - case :elixir_rewrite.inline(receiver, name, arity) do + case ElixirRewrite.inline(receiver, name, arity) do {ar, an} -> {:remote, ar, an, arity} false -> {:remote, receiver, name, arity} end @@ -4722,4 +4701,42 @@ defmodule ElixirSense.Core.Compiler do def built_in_type?(:var, 0), do: true def built_in_type?(name, arity), do: :erl_internal.is_type(name, arity) end + + defmodule Rewrite do + def inline(module, fun, arity) do + if Application.get_env(:elixir_sense, :compiler_rewrite) do + :elixir_rewrite.inline(module, fun, arity) + else + false + end + end + + def rewrite(context, receiver, dot_meta, right, meta, e_args, s) do + if Application.get_env(:elixir_sense, :compiler_rewrite) do + do_rewrite(context, receiver, dot_meta, right, meta, e_args, s) + else + {:ok, {{:., dot_meta, [receiver, right]}, meta, e_args}} + end + end + + defp do_rewrite(_, :erlang, _, :+, _, [arg], _s) when is_number(arg), do: {:ok, arg} + + defp do_rewrite(_, :erlang, _, :-, _, [arg], _s) when is_number(arg), do: {:ok, -arg} + + defp do_rewrite(:match, receiver, dot_meta, right, meta, e_args, _s) do + :elixir_rewrite.match_rewrite(receiver, dot_meta, right, meta, e_args) + end + + defp do_rewrite(:guard, receiver, dot_meta, right, meta, e_args, s) do + :elixir_rewrite.guard_rewrite(receiver, dot_meta, right, meta, e_args, guard_context(s)) + end + + defp do_rewrite(_, receiver, dot_meta, right, meta, e_args, _s) do + {:ok, :elixir_rewrite.rewrite(receiver, dot_meta, right, meta, e_args)} + end + + # TODO probably we can remove it/hardcode, used only for generating error message + defp guard_context(%{prematch: {_, _, {:bitsize, _}}}), do: "bitstring size specifier" + defp guard_context(_), do: "guard" + end end diff --git a/test/elixir_sense/core/compiler_test.exs b/test/elixir_sense/core/compiler_test.exs index 82f619ff..22550ef1 100644 --- a/test/elixir_sense/core/compiler_test.exs +++ b/test/elixir_sense/core/compiler_test.exs @@ -82,6 +82,14 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do end end + setup do + Application.put_env(:elixir_sense, :compiler_rewrite, true) + on_exit(fn -> + Application.put_env(:elixir_sense, :compiler_rewrite, false) + end) + {:ok, %{}} + end + test "initial" do elixir_env = :elixir_env.new() assert Compiler.env() == elixir_env From 46f6cd3d488e0c10fe31c83ffba4213f4e2fc2be Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 28 May 2024 22:06:23 +0200 Subject: [PATCH 078/235] resolve most issues in guard type inference --- lib/elixir_sense/core/binding.ex | 18 +- lib/elixir_sense/core/compiler.ex | 16 +- lib/elixir_sense/core/guard.ex | 199 ++++++++++-------- lib/elixir_sense/core/metadata_builder.ex | 4 +- lib/elixir_sense/core/type_inference.ex | 12 +- test/elixir_sense/core/binding_test.exs | 114 +++++++++- test/elixir_sense/core/compiler_test.exs | 29 ++- .../core/metadata_builder_test.exs | 134 ++++++++++-- 8 files changed, 403 insertions(+), 123 deletions(-) diff --git a/lib/elixir_sense/core/binding.ex b/lib/elixir_sense/core/binding.ex index 83b823fa..b179c060 100644 --- a/lib/elixir_sense/core/binding.ex +++ b/lib/elixir_sense/core/binding.ex @@ -339,11 +339,19 @@ defmodule ElixirSense.Core.Binding do def do_expand(_env, {:integer, integer}, _stack), do: {:integer, integer} - def do_expand(_env, {:union, [first | rest]} = u, _stack) do - if Enum.all?(rest, &(&1 == first)) do - first - else - u + def do_expand(_env, {:union, all}, _stack) do + all = Enum.filter(all, & &1 != :none) + cond do + all == [] -> :none + Enum.any?(all, & &1 == nil) -> nil + match?([_], all) -> hd(all) + true -> + first = hd(all) + if Enum.all?(tl(all), &(&1 == first)) do + first + else + {:union, all} + end end end diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index a5b9f578..a24c72ee 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1645,7 +1645,7 @@ defmodule ElixirSense.Core.Compiler do %{env_for_expand | context: :guard} ) - type_info = Guard.type_information_from_guards(e_guard |> dbg, state) |> dbg + type_info = Guard.type_information_from_guards(e_guard) state = merge_inferred_types(state, type_info) @@ -2156,8 +2156,8 @@ defmodule ElixirSense.Core.Compiler do sm = __MODULE__.Env.reset_read(sr, s) {[e_left], sl, el} = __MODULE__.Clauses.head([left], sm, er) - match_context_r = TypeInference.get_binding_type(sr, e_right) |> dbg - vars_l_with_inferred_types = TypeInference.find_vars(sl, e_left |> dbg, {:for_expression, match_context_r}) |> dbg + match_context_r = TypeInference.get_binding_type(sr, e_right) + vars_l_with_inferred_types = TypeInference.find_vars(sl, e_left, {:for_expression, match_context_r}) sl = State.annotate_vars_with_inferred_types(sl, vars_l_with_inferred_types) @@ -2511,7 +2511,7 @@ defmodule ElixirSense.Core.Compiler do {e_guard, sg, eg} = guard(guard, %{sa | prematch: prematch}, %{ea | context: :guard}) - type_info = Guard.type_information_from_guards(e_guard |> dbg, sg) |> dbg + type_info = Guard.type_information_from_guards(e_guard) sg = merge_inferred_types(sg, type_info) @@ -2684,8 +2684,8 @@ defmodule ElixirSense.Core.Compiler do sm = ElixirEnv.reset_read(sr, s) {[e_left], sl, el} = head([left], sm, er) - match_context_r = TypeInference.get_binding_type(sr, e_right) |> dbg - vars_l_with_inferred_types = TypeInference.find_vars(sl, e_left |> dbg, match_context_r) |> dbg + match_context_r = TypeInference.get_binding_type(sr, e_right) + vars_l_with_inferred_types = TypeInference.find_vars(sl, e_left, match_context_r) sl = State.annotate_vars_with_inferred_types(sl, vars_l_with_inferred_types) @@ -4704,7 +4704,7 @@ defmodule ElixirSense.Core.Compiler do defmodule Rewrite do def inline(module, fun, arity) do - if Application.get_env(:elixir_sense, :compiler_rewrite) do + if true || Application.get_env(:elixir_sense, :compiler_rewrite) do :elixir_rewrite.inline(module, fun, arity) else false @@ -4712,7 +4712,7 @@ defmodule ElixirSense.Core.Compiler do end def rewrite(context, receiver, dot_meta, right, meta, e_args, s) do - if Application.get_env(:elixir_sense, :compiler_rewrite) do + if true || Application.get_env(:elixir_sense, :compiler_rewrite) do do_rewrite(context, receiver, dot_meta, right, meta, e_args, s) else {:ok, {{:., dot_meta, [receiver, right]}, meta, e_args}} diff --git a/lib/elixir_sense/core/guard.ex b/lib/elixir_sense/core/guard.ex index b9f509b7..9c50431c 100644 --- a/lib/elixir_sense/core/guard.ex +++ b/lib/elixir_sense/core/guard.ex @@ -3,21 +3,15 @@ defmodule ElixirSense.Core.Guard do This module is responsible for infer type information from guard expressions """ - import ElixirSense.Core.State - - alias ElixirSense.Core.TypeInference - # A guard expression can be in either these form: # :and :or - # / \ or / \ or guard_expr + # / \ or / \ or :not guard_expr or guard_expr or list(guard_expr) # guard_expr guard_expr guard_expr guard_expr # - # type information from :and subtrees are mergeable - # type information from :or subtrees are discarded - def type_information_from_guards(list, state) when is_list(list) do + def type_information_from_guards(list) when is_list(list) do for expr <- list, reduce: %{} do acc -> - right = type_information_from_guards(expr, state) + right = type_information_from_guards(expr) Map.merge(acc, right, fn _k, v1, v2 -> case {v1, v2} do {{:union, types_1}, {:union, types_2}} -> {:union, types_1 ++ types_2} @@ -28,40 +22,24 @@ defmodule ElixirSense.Core.Guard do end) end end - def type_information_from_guards({{:., _, [:erlang, :andalso]}, _, [guard_l, guard_r]}, state) do - left = type_information_from_guards(guard_l, state) - right = type_information_from_guards(guard_r, state) - - Map.merge(left, right, fn _k, v1, v2 -> {:intersection, [v1, v2]} end) - end - # TODO remove? - def type_information_from_guards({:and, _, [guard_l, guard_r]}, state) do - left = type_information_from_guards(guard_l, state) - right = type_information_from_guards(guard_r, state) - Keyword.merge(left, right, fn _k, v1, v2 -> {:intersection, [v1, v2]} end) + def type_information_from_guards({{:., _, [:erlang, :not]}, _, [guard_l]}) do + left = type_information_from_guards(guard_l) + for {k, _v} <- left, into: %{}, do: {k, nil} end - def type_information_from_guards({{:., _, [:erlang, :orelse]}, _, [guard_l, guard_r]}, state) do - left = type_information_from_guards(guard_l, state) - right = type_information_from_guards(guard_r, state) + def type_information_from_guards({{:., _, [:erlang, :andalso]}, _, [guard_l, guard_r]}) do + left = type_information_from_guards(guard_l) + right = type_information_from_guards(guard_r) - Map.merge(left, right, fn _k, v1, v2 -> - case {v1, v2} do - {{:union, types_1}, {:union, types_2}} -> {:union, types_1 ++ types_2} - {{:union, types}, _} -> {:union, types ++ [v2]} - {_, {:union, types}} -> {:union, [v1 | types]} - _ -> {:union, [v1, v2]} - end - end) + Map.merge(left, right, fn _k, v1, v2 -> {:intersection, [v1, v2]} end) end - # TODO remove - def type_information_from_guards({:or, _, [guard_l, guard_r]}, state) do - left = type_information_from_guards(guard_l, state) - right = type_information_from_guards(guard_r, state) + def type_information_from_guards({{:., _, [:erlang, :orelse]}, _, [guard_l, guard_r]}) do + left = type_information_from_guards(guard_l) + right = type_information_from_guards(guard_r) - Keyword.merge(left, right, fn _k, v1, v2 -> + Map.merge(left, right, fn _k, v1, v2 -> case {v1, v2} do {{:union, types_1}, {:union, types_2}} -> {:union, types_1 ++ types_2} {{:union, types}, _} -> {:union, types ++ [v2]} @@ -71,7 +49,7 @@ defmodule ElixirSense.Core.Guard do end) end - def type_information_from_guards(guard_ast, state) do + def type_information_from_guards(guard_ast) do {_, acc} = Macro.prewalk(guard_ast, %{}, fn # Standalone variable: func my_func(x) when x @@ -83,27 +61,21 @@ defmodule ElixirSense.Core.Guard do {node, acc} end - {{:., _dot_meta, [:erlang, fun]}, _call_meta, params} = node, acc when is_atom(fun) -> - with {type, binding} <- guard_predicate_type(fun, params, state), + {{:., _dot_meta, [:erlang, fun]}, _call_meta, params}, acc when is_atom(fun) and is_list(params) -> + with {type, binding} <- guard_predicate_type(fun, params), {var, meta, context} when is_atom(var) and is_atom(context) <- binding, {:ok, version} <- Keyword.fetch(meta, :version) do # If we found the predicate type, we can prematurely exit traversing the subtree {[], Map.put(acc, {var, version}, type)} else - _ -> {node, acc} + _ -> + # traverse params + {params, acc} end - # TODO can we drop this clause? - {guard_predicate, _, params} = node, acc -> - with {type, binding} <- guard_predicate_type(guard_predicate, params, state), - {var, meta, context} when is_atom(var) and is_atom(context) <- binding, - {:ok, version} <- Keyword.fetch(meta, :version) do - # If we found the predicate type, we can prematurely exit traversing the subtree - {[], Map.put(acc, {var, version}, type)} - - else - _ -> {node, acc} - end + {{:., _dot_meta, [_remote, _fun]}, _call_meta, params}, acc -> + # not expanded remote or fun - traverse params + {params, acc} node, acc -> {node, acc} @@ -112,24 +84,25 @@ defmodule ElixirSense.Core.Guard do acc end - defp guard_predicate_type(p, params, _) - when p in [:is_number, :is_float, :is_integer, :round, :trunc, :div, :rem, :abs], - do: {:number, hd(params)} + # TODO div and rem only work on first arg + defp guard_predicate_type(p, [first | _]) + when p in [:is_number, :is_float, :is_integer, :round, :trunc, :div, :rem, :abs, :ceil, :floor], + do: {:number, first} - defp guard_predicate_type(p, params, _) when p in [:is_binary, :binary_part], - do: {:binary, hd(params)} + defp guard_predicate_type(p, [first | _]) when p in [:is_binary, :binary_part], + do: {:binary, first} - defp guard_predicate_type(p, params, _) when p in [:is_bitstring, :bit_size, :byte_size], - do: {:bitstring, hd(params)} + defp guard_predicate_type(p, [first | _]) when p in [:is_bitstring, :bit_size, :byte_size], + do: {:bitstring, first} - defp guard_predicate_type(p, params, _) when p in [:is_list, :length], do: {:list, hd(params)} + defp guard_predicate_type(p, [first | _]) when p in [:is_list, :length], do: {:list, first} - defp guard_predicate_type(p, params, _) when p in [:hd, :tl], - do: {{:list, :boolean}, hd(params)} + defp guard_predicate_type(p, [first | _]) when p in [:hd, :tl], + do: {{:list, :boolean}, first} # when hd(x) == 1 - # when tl(x) <= 2 - defp guard_predicate_type(p, [{guard, _, guard_params}, rhs], _) + # when tl(x) == [2] + defp guard_predicate_type(p, [{{:., _, [:erlang, guard]}, _, [first | _]}, rhs]) when p in [:==, :===, :>=, :>, :<=, :<] and guard in [:hd, :tl] do rhs_type = cond do @@ -141,51 +114,105 @@ defmodule ElixirSense.Core.Guard do true -> nil end - rhs_type = if rhs_type, do: {:list, rhs_type}, else: :list + rhs_type = if guard == :hd and rhs_type, do: {:list, rhs_type}, else: :list - {rhs_type, hd(guard_params)} + {rhs_type, first} end + defp guard_predicate_type(p, [lhs, {{:., _, [:erlang, guard]}, _, _guard_params} = call]) + when p in [:==, :===, :>=, :>, :<=, :<] and guard in [:hd, :tl] do + guard_predicate_type(p, [call, lhs]) + end + + defp guard_predicate_type(p, [first | _]) when p in [:is_tuple], + do: {:tuple, first} - defp guard_predicate_type(p, params, _) when p in [:is_tuple, :elem], - do: {:tuple, hd(params)} + defp guard_predicate_type(p, [_, second | _]) when p in [:element], + do: {:tuple, second} # when tuple_size(x) == 1 # when tuple_size(x) == 2 - defp guard_predicate_type(p, [{:tuple_size, _, guard_params}, size], _) - when p in [:==, :===] do + defp guard_predicate_type(p, [{{:., _, [:erlang, :tuple_size]}, _, [first | _]}, size]) + when p in [:==, :===, :>=, :>, :<=, :<] do type = - if is_integer(size) do + if is_integer(size) and p in [:==, :===] do {:tuple, size, if(size > 0, do: Enum.map(1..size, fn _ -> nil end), else: [])} else :tuple end - {type, hd(guard_params)} + {type, first} end - defp guard_predicate_type(:is_map, params, _), do: {{:map, [], nil}, hd(params)} - defp guard_predicate_type(:map_size, params, _), do: {{:map, [], nil}, hd(params)} + defp guard_predicate_type(p, [size, {{:., _, [:erlang, :tuple_size]}, _, _guard_params} = call]) + when p in [:==, :===, :>=, :>, :<=, :<] do + guard_predicate_type(p, [call, size]) + end - defp guard_predicate_type(:is_map_key, [var, key], state) do - type = - case TypeInference.get_binding_type(state, key) do - {:atom, key} -> {:map, [{key, nil}], nil} - nil when is_binary(key) -> {:map, [{key, nil}], nil} - _ -> {:map, [], nil} + defp guard_predicate_type(p, [{{:., _, [:erlang, :map_get]}, _, [key, second | _]}, value]) + when p in [:==, :===] do + type = cond do + key == :__struct__ and is_atom(value) -> + {:struct, [], {:atom, value}, nil} + key == :__struct__ -> + {:struct, [], nil, nil} + is_atom(key) or is_binary(key) -> + # TODO other types of keys? + rhs_type = + cond do + is_number(value) -> {:number, value} + is_binary(value) -> :binary + is_bitstring(value) -> :bitstring + is_atom(value) -> {:atom, value} + is_boolean(value) -> :boolean + true -> nil + end + {:map, [{key, rhs_type}], nil} end + {type, second} + end + + defp guard_predicate_type(p, [value, {{:., _, [:erlang, :map_get]}, _, _guard_params} = call]) + when p in [:==, :===] do + guard_predicate_type(p, [call, value]) + end + + defp guard_predicate_type(:is_map, [first | _]), do: {{:map, [], nil}, first} + defp guard_predicate_type(:is_non_struct_map, [first | _]), do: {{:map, [], nil}, first} + defp guard_predicate_type(:map_size, [first | _]), do: {{:map, [], nil}, first} + + # TODO macro + defp guard_predicate_type(:is_map_key, [key, var | _]) do + # TODO other types of keys? + type = + case key do + :__struct__ -> {:struct, [], nil, nil} + key when is_atom(key) -> {:map, [{key, nil}], nil} + key when is_binary(key) -> {:map, [{key, nil}], nil} + _ -> {:map, [], nil} + end + {type, var} end - defp guard_predicate_type(:is_atom, params, _), do: {:atom, hd(params)} - defp guard_predicate_type(:is_boolean, params, _), do: {:boolean, hd(params)} + defp guard_predicate_type(:map_get, [key, var | _]) do + # TODO other types of keys? + type = + case key do + :__struct__ -> {:struct, [], nil, nil} + key when is_atom(key) -> {:map, [{key, nil}], nil} + key when is_binary(key) -> {:map, [{key, nil}], nil} + _ -> {:map, [], nil} + end - defp guard_predicate_type(:is_struct, [var, {:__aliases__, _, _list} = module], state) do - {module, _state, _env} = expand(module, state) - type = {:struct, [], {:atom, module}, nil} {type, var} end - defp guard_predicate_type(:is_struct, params, _), do: {{:struct, [], nil, nil}, hd(params)} - defp guard_predicate_type(_, _, _), do: nil + defp guard_predicate_type(:is_atom, [first | _]), do: {:atom, first} + defp guard_predicate_type(:is_boolean, [first | _]), do: {:boolean, first} + + defp guard_predicate_type(_, _), do: nil end + +# :in :is_function/1-2 :is_nil :is_pid :is_port :is_reference :node/0-1 +# :self diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index 1e7346ea..1b365b56 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -874,8 +874,8 @@ defmodule ElixirSense.Core.MetadataBuilder do - def infer_type_from_guards(guard_ast, vars, state) do - type_info = Guard.type_information_from_guards(guard_ast, state) + def infer_type_from_guards(guard_ast, vars, _state) do + type_info = Guard.type_information_from_guards(guard_ast) Enum.reduce(type_info, vars, fn {var, type}, acc -> index = Enum.find_index(acc, &(&1.name == var)) diff --git a/lib/elixir_sense/core/type_inference.ex b/lib/elixir_sense/core/type_inference.ex index aa653ffd..bca6dbce 100644 --- a/lib/elixir_sense/core/type_inference.ex +++ b/lib/elixir_sense/core/type_inference.ex @@ -96,7 +96,11 @@ defmodule ElixirSense.Core.TypeInference do # map def get_binding_type(state, {:%{}, _meta, fields}) when is_list(fields) do - {:map, get_fields_binding_type(state, fields), nil} + field_type = get_fields_binding_type(state, fields) + case field_type |> Keyword.fetch(:__struct__) do + {:ok, type} -> {:struct, [], type, nil} + _ -> {:map, field_type, nil} + end end # match @@ -106,12 +110,12 @@ defmodule ElixirSense.Core.TypeInference do # stepped range struct def get_binding_type(_state, {:"..//", _, [_, _, _]}) do - {:struct, [], {:atom, Range}} + {:struct, [], {:atom, Range}, nil} end # range struct def get_binding_type(_state, {:.., _, [_, _]}) do - {:struct, [], {:atom, Range}} + {:struct, [], {:atom, Range}, nil} end @builtin_sigils %{ @@ -126,7 +130,7 @@ defmodule ElixirSense.Core.TypeInference do # builtin sigil struct def get_binding_type(_state, {sigil, _, _}) when is_map_key(@builtin_sigils, sigil) do # TODO support custom sigils - {:struct, [], {:atom, @builtin_sigils |> Map.fetch!(sigil)}} + {:struct, [], {:atom, @builtin_sigils |> Map.fetch!(sigil)}, nil} end # tuple diff --git a/test/elixir_sense/core/binding_test.exs b/test/elixir_sense/core/binding_test.exs index 63f8dede..d1167497 100644 --- a/test/elixir_sense/core/binding_test.exs +++ b/test/elixir_sense/core/binding_test.exs @@ -106,6 +106,104 @@ defmodule ElixirSense.Core.BindingTest do ) end + test "introspection struct from guard" do + assert {:struct, [__struct__: nil], nil, nil} == + Binding.expand( + @env, + { + :intersection, + [ + {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, + {:struct, [], nil, nil} + ] + } + ) + + assert { + :struct, + [ + {:__struct__, {:atom, URI}}, + {:port, nil}, + {:scheme, nil}, + {:path, nil}, + {:host, nil}, + {:userinfo, nil}, + {:fragment, nil}, + {:query, nil}, + {:authority, nil} + ], + {:atom, URI}, + nil + } == + Binding.expand( + @env, + { + :intersection, + [ + {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, + {:struct, [], {:atom, URI}, nil} + ] + } + ) + + assert {:struct, [__struct__: nil, __exception__: {:atom, true}], nil, nil} == + Binding.expand( + @env, + { + :intersection, + [ + { + :intersection, + [ + { + :intersection, + [ + {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, + {:struct, [], nil, nil} + ] + }, + {:map, [{:__exception__, nil}], nil} + ] + }, + {:map, [{:__exception__, {:atom, true}}], nil} + ] + } + ) + + assert { + :struct, + [ + {:__struct__, {:atom, ArgumentError}}, + {:message, nil}, + {:__exception__, {:atom, true}} + ], + {:atom, ArgumentError}, + nil + } == + Binding.expand( + @env, + { + :intersection, + [ + { + :intersection, + [ + { + :intersection, + [ + {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, + {:struct, [], {:atom, ArgumentError}, nil} + ] + }, + {:map, [{:__exception__, nil}], nil} + ] + }, + {:map, [{:__exception__, {:atom, true}}], nil} + ] + } + ) + end + test "introspection module not a struct" do assert :none == Binding.expand(@env, {:struct, [], {:atom, ElixirSenseExample.EmptyModule}, nil}) @@ -1857,18 +1955,30 @@ defmodule ElixirSense.Core.BindingTest do ) end - test "none" do + test "none intersection" do assert :none == Binding.expand(@env, {:intersection, [{:atom, A}, :none]}) assert :none == Binding.expand(@env, {:intersection, [:none, {:atom, A}]}) assert :none == Binding.expand(@env, {:intersection, [:none, :none]}) end - test "unknown" do + test "none union" do + assert {:atom, A} == Binding.expand(@env, {:union, [{:atom, A}, :none]}) + assert {:atom, A} == Binding.expand(@env, {:union, [:none, {:atom, A}]}) + assert :none == Binding.expand(@env, {:union, [:none, :none]}) + end + + test "unknown intersection" do assert {:atom, A} == Binding.expand(@env, {:intersection, [{:atom, A}, nil]}) assert {:atom, A} == Binding.expand(@env, {:intersection, [nil, {:atom, A}]}) assert nil == Binding.expand(@env, {:intersection, [nil, nil]}) end + test "unknown union" do + assert nil == Binding.expand(@env, {:union, [{:atom, A}, nil]}) + assert nil == Binding.expand(@env, {:union, [nil, {:atom, A}]}) + assert nil == Binding.expand(@env, {:union, [nil, nil]}) + end + test "equal" do assert {:atom, A} == Binding.expand(@env, {:intersection, [{:atom, A}, {:atom, A}]}) assert :none == Binding.expand(@env, {:intersection, [{:atom, A}, {:atom, B}]}) diff --git a/test/elixir_sense/core/compiler_test.exs b/test/elixir_sense/core/compiler_test.exs index 22550ef1..bdf71766 100644 --- a/test/elixir_sense/core/compiler_test.exs +++ b/test/elixir_sense/core/compiler_test.exs @@ -83,7 +83,7 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do end setup do - Application.put_env(:elixir_sense, :compiler_rewrite, true) + # Application.put_env(:elixir_sense, :compiler_rewrite, true) on_exit(fn -> Application.put_env(:elixir_sense, :compiler_rewrite, false) end) @@ -763,6 +763,33 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do end end + defmodule Foo do + defguard my(a) when is_integer(a) and a > 1 + defmacro aaa(a) do + quote do + is_integer(unquote(a)) and unquote(a) > 1 + end + end + end + + test "guard" do + code = """ + require ElixirSense.Core.CompilerTest.Foo, as: Foo + Foo.my(5) + """ + + assert_expansion(code) + end + + test "macro" do + code = """ + require ElixirSense.Core.CompilerTest.Foo, as: Foo + Foo.aaa(5) + """ + + assert_expansion(code) + end + defp clean_capture_arg_position(ast) do {ast, _} = Macro.prewalk(ast, nil, fn {atom, meta, nil} = node, state when is_atom(atom) -> diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 82d8dbc7..3e2877d5 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -2316,6 +2316,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do var5 = ~r/foo/iu var6 = ~R(f\#{1,3}o) var7 = 12..34 + var8 = 12..34//1 IO.puts "" end end @@ -2323,14 +2324,15 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert [ - %VarInfo{name: :var1, type: {:struct, [], {:atom, Date}}}, - %VarInfo{name: :var2, type: {:struct, [], {:atom, Time}}}, - %VarInfo{name: :var3, type: {:struct, [], {:atom, DateTime}}}, - %VarInfo{name: :var4, type: {:struct, [], {:atom, NaiveDateTime}}}, - %VarInfo{name: :var5, type: {:struct, [], {:atom, Regex}}}, - %VarInfo{name: :var6, type: {:struct, [], {:atom, Regex}}}, - %VarInfo{name: :var7, type: {:struct, [], {:atom, Range}}} - ] = state |> get_line_vars(10) + %VarInfo{name: :var1, type: {:struct, [], {:atom, Date}, nil}}, + %VarInfo{name: :var2, type: {:struct, [], {:atom, Time}, nil}}, + %VarInfo{name: :var3, type: {:struct, [], {:atom, DateTime}, nil}}, + %VarInfo{name: :var4, type: {:struct, [], {:atom, NaiveDateTime}, nil}}, + %VarInfo{name: :var5, type: {:struct, [], {:atom, Regex}, nil}}, + %VarInfo{name: :var6, type: {:struct, [], {:atom, Regex}, nil}}, + %VarInfo{name: :var7, type: {:struct, [], {:atom, Range}, nil}}, + %VarInfo{name: :var8, type: {:struct, [], {:atom, Range}, nil}} + ] = state |> get_line_vars(11) end test "struct binding understands stepped ranges" do @@ -2346,7 +2348,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert [ - %VarInfo{name: :var1, type: {:struct, [], {:atom, Range}}} + %VarInfo{name: :var1, type: {:struct, [], {:atom, Range}, nil}} ] = state |> get_line_vars(4) end @@ -3420,9 +3422,11 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert %VarInfo{name: :x, type: :number} = var_with_guards("is_integer(x)") assert %VarInfo{name: :x, type: :number} = var_with_guards("round(x)") assert %VarInfo{name: :x, type: :number} = var_with_guards("trunc(x)") - assert %VarInfo{name: :x, type: :number} = var_with_guards("div(x)") - assert %VarInfo{name: :x, type: :number} = var_with_guards("rem(x)") + assert %VarInfo{name: :x, type: :number} = var_with_guards("div(x, 1)") + assert %VarInfo{name: :x, type: :number} = var_with_guards("rem(x, 1)") assert %VarInfo{name: :x, type: :number} = var_with_guards("abs(x)") + assert %VarInfo{name: :x, type: :number} = var_with_guards("ceil(x)") + assert %VarInfo{name: :x, type: :number} = var_with_guards("floor(x)") end test "binary guards" do @@ -3445,8 +3449,11 @@ defmodule ElixirSense.Core.MetadataBuilderTest do test "list guards" do assert %VarInfo{name: :x, type: :list} = var_with_guards("is_list(x)") assert %VarInfo{name: :x, type: {:list, :number}} = var_with_guards("hd(x) == 1") + assert %VarInfo{name: :x, type: {:list, :number}} = var_with_guards("1 == hd(x)") assert %VarInfo{name: :x, type: :list} = var_with_guards("tl(x) == [1]") assert %VarInfo{name: :x, type: :list} = var_with_guards("length(x) == 1") + assert %VarInfo{name: :x, type: :list} = var_with_guards("1 == length(x)") + assert %VarInfo{name: :x, type: {:list, :boolean}} = var_with_guards("hd(x)") end test "tuple guards" do @@ -3455,6 +3462,9 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert %VarInfo{name: :x, type: {:tuple, 1, [nil]}} = var_with_guards("tuple_size(x) == 1") + assert %VarInfo{name: :x, type: {:tuple, 1, [nil]}} = + var_with_guards("1 == tuple_size(x)") + assert %VarInfo{name: :x, type: :tuple} = var_with_guards("elem(x, 0) == 1") end @@ -3468,7 +3478,9 @@ defmodule ElixirSense.Core.MetadataBuilderTest do test "map guards" do assert %VarInfo{name: :x, type: {:map, [], nil}} = var_with_guards("is_map(x)") + assert %VarInfo{name: :x, type: {:intersection, [{:map, [], nil}, nil]}} = var_with_guards("is_non_struct_map(x)") assert %VarInfo{name: :x, type: {:map, [], nil}} = var_with_guards("map_size(x) == 1") + assert %VarInfo{name: :x, type: {:map, [], nil}} = var_with_guards("1 == map_size(x)") assert %VarInfo{name: :x, type: {:map, [a: nil], nil}} = var_with_guards("is_map_key(x, :a)") @@ -3478,12 +3490,30 @@ defmodule ElixirSense.Core.MetadataBuilderTest do end test "struct guards" do - assert %VarInfo{name: :x, type: {:struct, [], nil, nil}} = var_with_guards("is_struct(x)") - - assert %VarInfo{name: :x, type: {:struct, [], {:atom, URI}, nil}} = + assert %VarInfo{name: :x, type: { + :intersection, + [ + {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, + {:struct, [], nil, nil} + ] + }} = var_with_guards("is_struct(x)") + + assert %VarInfo{name: :x, type: { + :intersection, + [ + {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, + {:struct, [], {:atom, URI}, nil} + ] + }} = var_with_guards("is_struct(x, URI)") - assert %VarInfo{name: :x, type: {:struct, [], {:atom, URI}, nil}} = + assert %VarInfo{name: :x, type: { + :intersection, + [ + {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, + {:struct, [], {:atom, URI}, nil} + ] + }} = """ defmodule MyModule do alias URI, as: MyURI @@ -3498,6 +3528,69 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> hd() end + test "exception guards" do + assert %VarInfo{name: :x, type: { + :intersection, + [ + { + :intersection, + [ + { + :intersection, + [ + {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, + {:struct, [], nil, nil} + ] + }, + {:map, [{:__exception__, nil}], nil} + ] + }, + {:map, [{:__exception__, {:atom, true}}], nil} + ] + }} = var_with_guards("is_exception(x)") + + assert %VarInfo{name: :x, type: { + :intersection, + [ + { + :intersection, + [ + { + :intersection, + [ + {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, + {:struct, [], {:atom, ArgumentError}, nil} + ] + }, + {:map, [{:__exception__, nil}], nil} + ] + }, + {:map, [{:__exception__, {:atom, true}}], nil} + ] + }} = + var_with_guards("is_exception(x, ArgumentError)") + + assert %VarInfo{name: :x, type: { + :intersection, + [ + {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, + {:struct, [], {:atom, ArgumentError}, nil} + ] + }} = + """ + defmodule MyModule do + alias ArgumentError, as: MyURI + + def func(x) when is_struct(x, MyURI) do + IO.puts "" + end + end + """ + |> string_to_state() + |> get_line_vars(5) + |> hd() + end + test "and combination predicate guards can be merge" do assert %VarInfo{name: :x, type: {:intersection, [:number, :boolean]}} = var_with_guards("is_number(x) and x >= 1") @@ -3515,6 +3608,17 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert %VarInfo{name: :x, type: {:union, [:number, :atom, :binary]}} = var_with_guards("is_number(x) or is_atom(x) or is_binary(x)") end + + test "negated guards cannot be used for inference" do + assert %VarInfo{name: :x, type: nil} = + var_with_guards("not is_map(x)") + + assert %VarInfo{name: :x, type: {:union, [nil, :atom]}} = + var_with_guards("not is_map(x) or is_atom(x)") + + assert %VarInfo{name: :x, type: {:intersection, [:nil, :atom]}} = + var_with_guards("not is_map(x) and is_atom(x)") + end end end From bf92da8a69b8bfe6b59623f3b130d5d15c85ca78 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 29 May 2024 22:55:17 +0200 Subject: [PATCH 079/235] nested match --- lib/elixir_sense/core/compiler.ex | 35 +++++--- lib/elixir_sense/core/type_inference.ex | 88 +++++++++---------- .../core/metadata_builder_test.exs | 25 +++++- 3 files changed, 89 insertions(+), 59 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index a24c72ee..442322a7 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -34,14 +34,23 @@ defmodule ElixirSense.Core.Compiler do match_context_r = TypeInference.get_binding_type(sr, e_right) vars_l_with_inferred_types = TypeInference.find_vars(sl, e_left, match_context_r) - vars_r_with_inferred_types = case e.context do - :match -> - match_context_l = TypeInference.get_binding_type(sl, e_left) - TypeInference.find_vars(sr, e_right, match_context_l) - _ -> %{} - end - sl = annotate_vars_with_inferred_types(sl, Map.merge(vars_l_with_inferred_types, vars_r_with_inferred_types)) + expressions_to_refine = TypeInference.find_refinable(e_right, [], e) |> dbg + vars_r_with_inferred_types = if expressions_to_refine != [] do + # we are in match context and the right side is also a pattern, we can refine types + # on the right side using the inferred type of the left side + match_context_l = TypeInference.get_binding_type(sl, e_left) |> dbg + for expr <- expressions_to_refine, reduce: [] do + acc -> + vars_in_expr_with_inferred_types = TypeInference.find_vars(sl, expr, match_context_l) |> dbg + # TODO this merge is wrong + acc ++ vars_in_expr_with_inferred_types + end + else + [] + end |> dbg + + sl = merge_inferred_types(sl, vars_l_with_inferred_types ++ vars_r_with_inferred_types) # match_context_l = TypeInference.get_binding_type(sl, e_left) |> dbg # elixir raises parallel_bitstring_match if detected @@ -2159,7 +2168,7 @@ defmodule ElixirSense.Core.Compiler do match_context_r = TypeInference.get_binding_type(sr, e_right) vars_l_with_inferred_types = TypeInference.find_vars(sl, e_left, {:for_expression, match_context_r}) - sl = State.annotate_vars_with_inferred_types(sl, vars_l_with_inferred_types) + sl = State.merge_inferred_types(sl, vars_l_with_inferred_types) {{:<-, meta, [e_left, e_right]}, sl, el} end @@ -2575,7 +2584,7 @@ defmodule ElixirSense.Core.Compiler do case head(c, s, e) do {[h | _] = c, s, e} -> clause_vars_with_inferred_types = TypeInference.find_vars(s, h, match_context) - s = State.annotate_vars_with_inferred_types(s, clause_vars_with_inferred_types) + s = State.merge_inferred_types(s, clause_vars_with_inferred_types) {c, s, e} other -> other @@ -2687,7 +2696,7 @@ defmodule ElixirSense.Core.Compiler do match_context_r = TypeInference.get_binding_type(sr, e_right) vars_l_with_inferred_types = TypeInference.find_vars(sl, e_left, match_context_r) - sl = State.annotate_vars_with_inferred_types(sl, vars_l_with_inferred_types) + sl = State.merge_inferred_types(sl, vars_l_with_inferred_types) {{:<-, meta, [e_left, e_right]}, {sl, el}} end @@ -2817,7 +2826,7 @@ defmodule ElixirSense.Core.Compiler do match_context = {:struct, [], {:atom, Exception}, nil} vars_with_inferred_types = TypeInference.find_vars(sl, e_left, match_context) - sl = State.annotate_vars_with_inferred_types(sl, vars_with_inferred_types) + sl = State.merge_inferred_types(sl, vars_with_inferred_types) {e_left, sl, el} end @@ -2839,7 +2848,7 @@ defmodule ElixirSense.Core.Compiler do match_context = {:struct, [], {:atom, Exception}, nil} vars_with_inferred_types = TypeInference.find_vars(sl, e_left, match_context) - sl = State.annotate_vars_with_inferred_types(sl, vars_with_inferred_types) + sl = State.merge_inferred_types(sl, vars_with_inferred_types) {e_left, sl, el} end @@ -2865,7 +2874,7 @@ defmodule ElixirSense.Core.Compiler do end vars_with_inferred_types = TypeInference.find_vars(sl, e_left, match_context) - sr = State.annotate_vars_with_inferred_types(sr, vars_with_inferred_types) + sr = State.merge_inferred_types(sr, vars_with_inferred_types) {{:in, meta, [e_left, normalized]}, sr, er} diff --git a/lib/elixir_sense/core/type_inference.ex b/lib/elixir_sense/core/type_inference.ex index bca6dbce..0b6bdc2c 100644 --- a/lib/elixir_sense/core/type_inference.ex +++ b/lib/elixir_sense/core/type_inference.ex @@ -187,39 +187,40 @@ defmodule ElixirSense.Core.TypeInference do {_ast, {vars, _match_context}} = Macro.prewalk(ast, {[], match_context}, &match_var(state, &1, &2)) - vars |> Map.new + vars end - defp match_var( - state, - {:in, _meta, - [ - left, - right - ]}, - {vars, _match_context} - ) do - exception_type = - case right do - [elem] -> - get_binding_type(state, elem) - - list when is_list(list) -> - types = for elem <- list, do: get_binding_type(state, elem) - if Enum.all?(types, &match?({:atom, _}, &1)), do: {:atom, Exception} - - elem -> - get_binding_type(state, elem) - end - - match_context = - case exception_type do - {:atom, atom} -> {:struct, [], {:atom, atom}, nil} - _ -> nil - end - - match_var(state, left, {vars, match_context}) - end + # TODO not needed + # defp match_var( + # state, + # {:in, _meta, + # [ + # left, + # right + # ]}, + # {vars, _match_context} + # ) do + # exception_type = + # case right do + # [elem] -> + # get_binding_type(state, elem) + + # list when is_list(list) -> + # types = for elem <- list, do: get_binding_type(state, elem) + # if Enum.all?(types, &match?({:atom, _}, &1)), do: {:atom, Exception} + + # elem -> + # get_binding_type(state, elem) + # end + + # match_context = + # case exception_type do + # {:atom, atom} -> {:struct, [], {:atom, atom}, nil} + # _ -> nil + # end + + # match_var(state, left, {vars, match_context}) + # end defp match_var( state, @@ -248,11 +249,7 @@ defmodule ElixirSense.Core.TypeInference do var not in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__, :_] do case Keyword.fetch(meta, :version) do {:ok, version} -> - var_info = state.vars_info |> hd |> Map.fetch!({var, version}) - - var_info = %VarInfo{var_info | type: match_context} - - {nil, {[{{var, version}, var_info} | vars], nil}} + {nil, {[{{var, version}, match_context} | vars], nil}} _ -> {ast, {vars, match_context}} end @@ -267,21 +264,18 @@ defmodule ElixirSense.Core.TypeInference do var not in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__, :_] do case Keyword.fetch(meta, :version) do {:ok, version} -> - var_info = state.vars_info |> hd |> Map.fetch!({var, version}) - - var_info = %VarInfo{var_info | type: match_context} - - {nil, {[{{var, version}, var_info} | vars], nil}} + {nil, {[{{var, version}, match_context} | vars], nil}} _ -> {ast, {vars, match_context}} end end # drop right side of guard expression as guards cannot define vars - defp match_var(state, {:when, _, [left, _right]}, {vars, match_context}) do - # TODO should we infer from guard here? - match_var(state, left, {vars, match_context}) - end + # TODO not needed + # defp match_var(state, {:when, _, [left, _right]}, {vars, match_context}) do + # # TODO should we infer from guard here? + # match_var(state, left, {vars, match_context}) + # end defp match_var(state, {:%, _, [type_ast, {:%{}, _, ast}]}, {vars, match_context}) when not is_nil(match_context) do @@ -389,4 +383,8 @@ defmodule ElixirSense.Core.TypeInference do defp match_var(_state, ast, {vars, match_context}) do {ast, {vars, match_context}} end + + def find_refinable({:=, _, [left, right]}, acc, e), do: find_refinable(right, [left | acc], e) + def find_refinable(other, acc, e) when e.context == :match, do: [other | acc] + def find_refinable(_, acc, _), do: acc end diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 3e2877d5..5eaa0a1b 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -2352,7 +2352,30 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] = state |> get_line_vars(4) end - test "nested `=` binding" do + test "two way refinement in match context" do + state = + """ + defmodule MyModule do + def some(%MyState{formatted: formatted} = state) do + IO.puts "" + end + end + """ + |> string_to_state + + assert [ + %VarInfo{ + name: :formatted, + type: {:map_key, {:variable, :state}, {:atom, :formatted}}, + }, + %VarInfo{ + name: :state, + type: {:struct, [formatted: {:variable, :formatted}], {:atom, MyState}, nil} + } + ] = state |> get_line_vars(3) + end + + test "two way refinement in nested `=` binding" do state = """ defmodule MyModule do From 3dcf8a23d8e5f65d506816a80c6ab78eee8cdfce Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 30 May 2024 10:22:40 +0200 Subject: [PATCH 080/235] simplify type inference --- lib/elixir_sense/core/compiler.ex | 56 ++----- lib/elixir_sense/core/metadata_builder.ex | 22 +-- lib/elixir_sense/core/state.ex | 20 +-- lib/elixir_sense/core/type_inference.ex | 153 ++++++++---------- .../core/metadata_builder_test.exs | 113 ++++++++++++- 5 files changed, 203 insertions(+), 161 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 442322a7..88950fa1 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -28,22 +28,19 @@ defmodule ElixirSense.Core.Compiler do defp do_expand({:=, meta, [left, right]}, s, e) do # elixir validates we are not in guard context {e_right, sr, er} = expand(right, s, e) - # dbg(sr) - # dbg(e_right) {e_left, sl, el} = __MODULE__.Clauses.match(&expand/3, left, sr, s, er) - match_context_r = TypeInference.get_binding_type(sr, e_right) - vars_l_with_inferred_types = TypeInference.find_vars(sl, e_left, match_context_r) + match_context_r = TypeInference.get_binding_type(e_right) + vars_l_with_inferred_types = TypeInference.find_vars(e_left, match_context_r) expressions_to_refine = TypeInference.find_refinable(e_right, [], e) |> dbg vars_r_with_inferred_types = if expressions_to_refine != [] do # we are in match context and the right side is also a pattern, we can refine types # on the right side using the inferred type of the left side - match_context_l = TypeInference.get_binding_type(sl, e_left) |> dbg + match_context_l = TypeInference.get_binding_type(e_left) |> dbg for expr <- expressions_to_refine, reduce: [] do acc -> - vars_in_expr_with_inferred_types = TypeInference.find_vars(sl, expr, match_context_l) |> dbg - # TODO this merge is wrong + vars_in_expr_with_inferred_types = TypeInference.find_vars(expr, match_context_l) |> dbg acc ++ vars_in_expr_with_inferred_types end else @@ -51,18 +48,7 @@ defmodule ElixirSense.Core.Compiler do end |> dbg sl = merge_inferred_types(sl, vars_l_with_inferred_types ++ vars_r_with_inferred_types) - # match_context_l = TypeInference.get_binding_type(sl, e_left) |> dbg - - # elixir raises parallel_bitstring_match if detected - - # IO.inspect(sl.vars_info, label: "left") - # dbg(e_left) - # dbg(el.versioned_vars) - # dbg(sl.vars) - # {{:=, meta, [e_left, e_right]}, sl, el |> Map.from_struct()} |> dbg(limit: :infinity) - # el = el |> :elixir_env.with_vars(sl.vars |> elem(0)) - # sl = sl |> add_current_env_to_line(Keyword.fetch!(meta, :line), el) - # dbg(sl) + {{:=, meta, [e_left, e_right]}, sl, el} end @@ -1212,7 +1198,7 @@ defmodule ElixirSense.Core.Compiler do inferred_type = case e_args do nil -> nil - [arg] -> TypeInference.get_binding_type(state, arg) + [arg] -> TypeInference.get_binding_type(arg) end state = @@ -2165,8 +2151,8 @@ defmodule ElixirSense.Core.Compiler do sm = __MODULE__.Env.reset_read(sr, s) {[e_left], sl, el} = __MODULE__.Clauses.head([left], sm, er) - match_context_r = TypeInference.get_binding_type(sr, e_right) - vars_l_with_inferred_types = TypeInference.find_vars(sl, e_left, {:for_expression, match_context_r}) + match_context_r = TypeInference.get_binding_type(e_right) + vars_l_with_inferred_types = TypeInference.find_vars(e_left, {:for_expression, match_context_r}) sl = State.merge_inferred_types(sl, vars_l_with_inferred_types) @@ -2569,7 +2555,7 @@ defmodule ElixirSense.Core.Compiler do def case(meta, e_expr, opts, s, e) do opts = sanitize_opts(opts, [:do]) - match_context = TypeInference.get_binding_type(s, e_expr) + match_context = TypeInference.get_binding_type(e_expr) {case_clauses, sa} = Enum.map_reduce(opts, s, fn x, sa -> @@ -2583,7 +2569,7 @@ defmodule ElixirSense.Core.Compiler do expand_clauses(meta, :case, fn c, s, e -> case head(c, s, e) do {[h | _] = c, s, e} -> - clause_vars_with_inferred_types = TypeInference.find_vars(s, h, match_context) + clause_vars_with_inferred_types = TypeInference.find_vars(h, match_context) s = State.merge_inferred_types(s, clause_vars_with_inferred_types) {c, s, e} @@ -2693,8 +2679,8 @@ defmodule ElixirSense.Core.Compiler do sm = ElixirEnv.reset_read(sr, s) {[e_left], sl, el} = head([left], sm, er) - match_context_r = TypeInference.get_binding_type(sr, e_right) - vars_l_with_inferred_types = TypeInference.find_vars(sl, e_left, match_context_r) + match_context_r = TypeInference.get_binding_type(e_right) + vars_l_with_inferred_types = TypeInference.find_vars(e_left, match_context_r) sl = State.merge_inferred_types(sl, vars_l_with_inferred_types) @@ -2825,7 +2811,7 @@ defmodule ElixirSense.Core.Compiler do match_context = {:struct, [], {:atom, Exception}, nil} - vars_with_inferred_types = TypeInference.find_vars(sl, e_left, match_context) + vars_with_inferred_types = TypeInference.find_vars(e_left, match_context) sl = State.merge_inferred_types(sl, vars_with_inferred_types) {e_left, sl, el} @@ -2847,7 +2833,7 @@ defmodule ElixirSense.Core.Compiler do match_context = {:struct, [], {:atom, Exception}, nil} - vars_with_inferred_types = TypeInference.find_vars(sl, e_left, match_context) + vars_with_inferred_types = TypeInference.find_vars(e_left, match_context) sl = State.merge_inferred_types(sl, vars_with_inferred_types) {e_left, sl, el} @@ -2873,7 +2859,7 @@ defmodule ElixirSense.Core.Compiler do match_context end - vars_with_inferred_types = TypeInference.find_vars(sl, e_left, match_context) + vars_with_inferred_types = TypeInference.find_vars(e_left, match_context) sr = State.merge_inferred_types(sr, vars_with_inferred_types) {{:in, meta, [e_left, normalized]}, sr, er} @@ -4713,19 +4699,11 @@ defmodule ElixirSense.Core.Compiler do defmodule Rewrite do def inline(module, fun, arity) do - if true || Application.get_env(:elixir_sense, :compiler_rewrite) do - :elixir_rewrite.inline(module, fun, arity) - else - false - end + :elixir_rewrite.inline(module, fun, arity) end def rewrite(context, receiver, dot_meta, right, meta, e_args, s) do - if true || Application.get_env(:elixir_sense, :compiler_rewrite) do - do_rewrite(context, receiver, dot_meta, right, meta, e_args, s) - else - {:ok, {{:., dot_meta, [receiver, right]}, meta, e_args}} - end + do_rewrite(context, receiver, dot_meta, right, meta, e_args, s) end defp do_rewrite(_, :erlang, _, :+, _, [arg], _s) when is_number(arg), do: {:ok, arg} diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index 1b365b56..74336703 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -140,9 +140,7 @@ defmodule ElixirSense.Core.MetadataBuilder do defp pre_func({type, meta, ast_args}, state, meta, name, params, options \\ []) when is_atom(name) do - vars = - state - |> find_vars(params, nil) + vars = find_vars(params, nil) _vars = if options[:guards], @@ -272,9 +270,7 @@ defmodule ElixirSense.Core.MetadataBuilder do end defp pre_clause({_clause, _meta, _} = ast, state, lhs) do - _vars = - state - |> find_vars(lhs, Enum.at(state.binding_context, 0)) + _vars = find_vars(lhs, Enum.at(state.binding_context, 0)) state |> new_lexical_scope @@ -501,9 +497,7 @@ defmodule ElixirSense.Core.MetadataBuilder do end defp pre({:when, meta, [lhs, rhs]}, state) do - _vars = - state - |> find_vars(lhs, nil) + _vars = find_vars(lhs, nil) state # |> add_vars(vars, true) @@ -524,7 +518,7 @@ defmodule ElixirSense.Core.MetadataBuilder do column = Keyword.fetch!(meta, :column) state - |> push_binding_context(get_binding_type(state, condition_ast)) + |> push_binding_context(get_binding_type(condition_ast)) |> add_call_to_line({nil, :case, 2}, {line, column}) # |> add_current_env_to_line(line) |> result(ast) @@ -808,7 +802,7 @@ defmodule ElixirSense.Core.MetadataBuilder do defp post({atom, meta, [lhs, rhs]} = ast, state) when atom in [:=, :<-] do _line = Keyword.fetch!(meta, :line) - match_context_r = get_binding_type(state, rhs) + match_context_r = get_binding_type(rhs) match_context_r = if atom == :<- and match?([:for | _], state.binding_context) do @@ -817,13 +811,13 @@ defmodule ElixirSense.Core.MetadataBuilder do match_context_r end - vars_l = find_vars(state, lhs, match_context_r) + vars_l = find_vars(lhs, match_context_r) _vars = case rhs do {:=, _, [nested_lhs, _nested_rhs]} -> - match_context_l = get_binding_type(state, lhs) - nested_vars = find_vars(state, nested_lhs, match_context_l) + match_context_l = get_binding_type(lhs) + nested_vars = find_vars(nested_lhs, match_context_l) vars_l ++ nested_vars diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 67bb7ab4..30a67c75 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -1320,11 +1320,6 @@ defmodule ElixirSense.Core.State do end end - defp merge_type(nil, new), do: new - defp merge_type(old, nil), do: old - defp merge_type(old, old), do: old - defp merge_type(old, new), do: {:intersection, [old, new]} - def default_env, do: %ElixirSense.Core.State.Env{} def expand(ast, %__MODULE__{} = state) do @@ -1844,16 +1839,15 @@ defmodule ElixirSense.Core.State do def merge_inferred_types(state, inferred_types) do [h | t] = state.vars_info - h = for {{var, version}, type} <- inferred_types, reduce: h do - acc -> - updated_var = case acc[{var, version}] do - %VarInfo{type: nil} = v -> %{v | type: type} - %VarInfo{type: ^type} = v -> v - %VarInfo{type: old_type} = v -> %{v | type: {:intersection, [type, old_type]}} - end - Map.put(acc, {var, version}, updated_var) + h = for {key, type} <- inferred_types, reduce: h do + acc -> Map.update!(acc, key, fn %VarInfo{type: old} = v -> %{v | type: merge_type(old, type)} end) end %{state | vars_info: [h | t]} end + + defp merge_type(nil, new), do: new + defp merge_type(old, nil), do: old + defp merge_type(old, old), do: old + defp merge_type(old, new), do: {:intersection, [old, new]} end diff --git a/lib/elixir_sense/core/type_inference.ex b/lib/elixir_sense/core/type_inference.ex index 0b6bdc2c..5deea6f4 100644 --- a/lib/elixir_sense/core/type_inference.ex +++ b/lib/elixir_sense/core/type_inference.ex @@ -1,9 +1,7 @@ defmodule ElixirSense.Core.TypeInference do - # TODO remove state arg - # struct or struct update - alias ElixirSense.Core.State.VarInfo + # TODO struct or struct update + def get_binding_type( - state, {:%, _meta, [ struct_ast, @@ -11,7 +9,7 @@ defmodule ElixirSense.Core.TypeInference do ]} ) do {fields, updated_struct} = - case get_binding_type(state, ast) do + case get_binding_type(ast) do {:map, fields, updated_map} -> {fields, updated_map} {:struct, fields, _, updated_struct} -> {fields, updated_struct} _ -> {[], nil} @@ -19,7 +17,7 @@ defmodule ElixirSense.Core.TypeInference do # expand struct type - only compile type atoms or attributes are supported type = - case get_binding_type(state, struct_ast) do + case get_binding_type(struct_ast) do {:atom, atom} -> {:atom, atom} {:attribute, attribute} -> {:attribute, attribute} _ -> nil @@ -30,57 +28,38 @@ defmodule ElixirSense.Core.TypeInference do # pipe # TODO no pipes in expanded code - # def get_binding_type(state, {:|>, _, [params_1, {call, meta, params_rest}]}) do + # def get_binding_type({:|>, _, [params_1, {call, meta, params_rest}]}) do # params = [params_1 | params_rest || []] - # get_binding_type(state, {call, meta, params}) + # get_binding_type({call, meta, params}) # end # remote call - def get_binding_type(state, {{:., _, [target, fun]}, _, args}) + def get_binding_type({{:., _, [target, fun]}, _, args}) when is_atom(fun) and is_list(args) do - target = get_binding_type(state, target) - {:call, target, fun, Enum.map(args, &get_binding_type(state, &1))} + target = get_binding_type(target) + {:call, target, fun, Enum.map(args, &get_binding_type(&1))} end - # TODO no __MODULE__ in expanded code - # # current module - # def get_binding_type(state, {:__MODULE__, _, nil} = module) do - # {module, _state, _env} = expand(module, state) - # {:atom, module} - # end - - # TODO no expandable __aliases__ in expanded code - # # elixir module - # def get_binding_type(state, {:__aliases__, _, list} = module) when is_list(list) do - # try do - # {module, _state, _env} = expand(module, state) - # {:atom, module} - # rescue - # _ -> nil - # end - # end - # variable or local no parens call # TODO version? - def get_binding_type(_state, {var, _, context}) when is_atom(var) and is_atom(context) do + def get_binding_type({var, _, context}) when is_atom(var) and is_atom(context) do {:variable, var} end # attribute # expanded attribute reference has nil arg - def get_binding_type(_state, {:@, _, [{attribute, _, nil}]}) + def get_binding_type({:@, _, [{attribute, _, nil}]}) when is_atom(attribute) do {:attribute, attribute} end # erlang module or atom - def get_binding_type(_state, atom) when is_atom(atom) do + def get_binding_type(atom) when is_atom(atom) do {:atom, atom} end # map update def get_binding_type( - state, {:%{}, _meta, [ {:|, _meta1, @@ -91,12 +70,12 @@ defmodule ElixirSense.Core.TypeInference do ]} ) when is_list(fields) do - {:map, get_fields_binding_type(state, fields), get_binding_type(state, updated_map)} + {:map, get_fields_binding_type(fields), get_binding_type(updated_map)} end # map - def get_binding_type(state, {:%{}, _meta, fields}) when is_list(fields) do - field_type = get_fields_binding_type(state, fields) + def get_binding_type({:%{}, _meta, fields}) when is_list(fields) do + field_type = get_fields_binding_type(fields) case field_type |> Keyword.fetch(:__struct__) do {:ok, type} -> {:struct, [], type, nil} _ -> {:map, field_type, nil} @@ -104,17 +83,17 @@ defmodule ElixirSense.Core.TypeInference do end # match - def get_binding_type(state, {:=, _, [_, ast]}) do - get_binding_type(state, ast) + def get_binding_type({:=, _, [_, ast]}) do + get_binding_type(ast) end # stepped range struct - def get_binding_type(_state, {:"..//", _, [_, _, _]}) do + def get_binding_type({:"..//", _, [_, _, _]}) do {:struct, [], {:atom, Range}, nil} end # range struct - def get_binding_type(_state, {:.., _, [_, _]}) do + def get_binding_type({:.., _, [_, _]}) do {:struct, [], {:atom, Range}, nil} end @@ -128,7 +107,7 @@ defmodule ElixirSense.Core.TypeInference do } # builtin sigil struct - def get_binding_type(_state, {sigil, _, _}) when is_map_key(@builtin_sigils, sigil) do + def get_binding_type({sigil, _, _}) when is_map_key(@builtin_sigils, sigil) do # TODO support custom sigils {:struct, [], {:atom, @builtin_sigils |> Map.fetch!(sigil)}, nil} end @@ -137,62 +116,61 @@ defmodule ElixirSense.Core.TypeInference do # regular tuples use {:{}, [], [field_1, field_2]} ast # two element use {field_1, field_2} ast (probably as an optimization) # detect and convert to regular - def get_binding_type(state, ast) when is_tuple(ast) and tuple_size(ast) == 2 do - get_binding_type(state, {:{}, [], Tuple.to_list(ast)}) + def get_binding_type(ast) when is_tuple(ast) and tuple_size(ast) == 2 do + get_binding_type({:{}, [], Tuple.to_list(ast)}) end - def get_binding_type(state, {:{}, _, list}) do - {:tuple, length(list), list |> Enum.map(&get_binding_type(state, &1))} + def get_binding_type({:{}, _, list}) do + {:tuple, length(list), list |> Enum.map(&get_binding_type(&1))} end - def get_binding_type(state, list) when is_list(list) do + def get_binding_type(list) when is_list(list) do type = case list do [] -> :empty - [{:|, _, [head, _tail]}] -> get_binding_type(state, head) - [head | _] -> get_binding_type(state, head) + [{:|, _, [head, _tail]}] -> get_binding_type(head) + [head | _] -> get_binding_type(head) end {:list, type} end - def get_binding_type(state, list) when is_list(list) do - {:list, list |> Enum.map(&get_binding_type(state, &1))} + def get_binding_type(list) when is_list(list) do + {:list, list |> Enum.map(&get_binding_type(&1))} end # pinned variable - def get_binding_type(state, {:^, _, [pinned]}), do: get_binding_type(state, pinned) + def get_binding_type({:^, _, [pinned]}), do: get_binding_type(pinned) # local call - def get_binding_type(state, {var, _, args}) when is_atom(var) and is_list(args) do - {:local_call, var, Enum.map(args, &get_binding_type(state, &1))} + def get_binding_type({var, _, args}) when is_atom(var) and is_list(args) do + {:local_call, var, Enum.map(args, &get_binding_type(&1))} end # integer - def get_binding_type(_state, integer) when is_integer(integer) do + def get_binding_type(integer) when is_integer(integer) do {:integer, integer} end # other - def get_binding_type(_state, _), do: nil + def get_binding_type(_), do: nil - defp get_fields_binding_type(state, fields) do + defp get_fields_binding_type(fields) do for {field, value} <- fields, is_atom(field) do - {field, get_binding_type(state, value)} + {field, get_binding_type(value)} end end - def find_vars(state, ast, match_context) do + def find_vars(ast, match_context) do {_ast, {vars, _match_context}} = - Macro.prewalk(ast, {[], match_context}, &match_var(state, &1, &2)) + Macro.prewalk(ast, {[], match_context}, &match_var(&1, &2)) vars end # TODO not needed # defp match_var( - # state, # {:in, _meta, # [ # left, @@ -203,14 +181,14 @@ defmodule ElixirSense.Core.TypeInference do # exception_type = # case right do # [elem] -> - # get_binding_type(state, elem) + # get_binding_type(elem) # list when is_list(list) -> - # types = for elem <- list, do: get_binding_type(state, elem) + # types = for elem <- list, do: get_binding_type(elem) # if Enum.all?(types, &match?({:atom, _}, &1)), do: {:atom, Exception} # elem -> - # get_binding_type(state, elem) + # get_binding_type(elem) # end # match_context = @@ -219,11 +197,10 @@ defmodule ElixirSense.Core.TypeInference do # _ -> nil # end - # match_var(state, left, {vars, match_context}) + # match_var(left, {vars, match_context}) # end defp match_var( - state, {:=, _meta, [ left, @@ -232,16 +209,15 @@ defmodule ElixirSense.Core.TypeInference do {vars, _match_context} ) do {_ast, {vars, _match_context}} = - match_var(state, left, {vars, get_binding_type(state, right)}) + match_var(left, {vars, get_binding_type(right)}) {_ast, {vars, _match_context}} = - match_var(state, right, {vars, get_binding_type(state, left)}) + match_var(right, {vars, get_binding_type(left)}) {[], {vars, nil}} end defp match_var( - state, {:^, _meta, [{var, meta, context}]} = ast, {vars, match_context} ) @@ -256,7 +232,6 @@ defmodule ElixirSense.Core.TypeInference do end defp match_var( - state, {var, meta, context} = ast, {vars, match_context} ) @@ -272,23 +247,23 @@ defmodule ElixirSense.Core.TypeInference do # drop right side of guard expression as guards cannot define vars # TODO not needed - # defp match_var(state, {:when, _, [left, _right]}, {vars, match_context}) do + # defp match_var({:when, _, [left, _right]}, {vars, match_context}) do # # TODO should we infer from guard here? - # match_var(state, left, {vars, match_context}) + # match_var(left, {vars, match_context}) # end - defp match_var(state, {:%, _, [type_ast, {:%{}, _, ast}]}, {vars, match_context}) + defp match_var({:%, _, [type_ast, {:%{}, _, ast}]}, {vars, match_context}) when not is_nil(match_context) do # TODO pass mach_context here as map __struct__ key access - {_ast, {type_vars, _match_context}} = match_var(state, type_ast, {[], nil}) + {_ast, {type_vars, _match_context}} = match_var(type_ast, {[], nil}) destructured_vars = ast |> Enum.flat_map(fn {key, value_ast} -> - key_type = get_binding_type(state, key) + key_type = get_binding_type(key) {_ast, {new_vars, _match_context}} = - match_var(state, value_ast, {[], {:map_key, match_context, key_type}}) + match_var(value_ast, {[], {:map_key, match_context, key_type}}) new_vars end) @@ -296,7 +271,7 @@ defmodule ElixirSense.Core.TypeInference do {ast, {vars ++ destructured_vars ++ type_vars, nil}} end - defp match_var(state, {:%{}, _, ast}, {vars, match_context}) when not is_nil(match_context) do + defp match_var({:%{}, _, ast}, {vars, match_context}) when not is_nil(match_context) do destructured_vars = ast |> Enum.flat_map(fn @@ -304,10 +279,10 @@ defmodule ElixirSense.Core.TypeInference do # map update is forbidden in match, we're in invalid code [] {key, value_ast} -> - key_type = get_binding_type(state, key) + key_type = get_binding_type(key) {_ast, {new_vars, _match_context}} = - match_var(state, value_ast, {[], {:map_key, match_context, key_type}}) + match_var(value_ast, {[], {:map_key, match_context, key_type}}) new_vars end) @@ -318,12 +293,12 @@ defmodule ElixirSense.Core.TypeInference do # regular tuples use {:{}, [], [field_1, field_2]} ast # two element use `{field_1, field_2}` ast (probably as an optimization) # detect and convert to regular - defp match_var(state, ast, {vars, match_context}) + defp match_var(ast, {vars, match_context}) when is_tuple(ast) and tuple_size(ast) == 2 do - match_var(state, {:{}, [], ast |> Tuple.to_list()}, {vars, match_context}) + match_var({:{}, [], ast |> Tuple.to_list()}, {vars, match_context}) end - defp match_var(state, {:{}, _, ast}, {vars, match_context}) when not is_nil(match_context) do + defp match_var({:{}, _, ast}, {vars, match_context}) when not is_nil(match_context) do indexed = ast |> Enum.with_index() total = length(ast) @@ -332,7 +307,7 @@ defmodule ElixirSense.Core.TypeInference do |> Enum.flat_map(fn {nth_elem_ast, n} -> bond = {:tuple, total, - indexed |> Enum.map(&if(n != elem(&1, 1), do: get_binding_type(state, elem(&1, 0))))} + indexed |> Enum.map(&if(n != elem(&1, 1), do: get_binding_type(elem(&1, 0))))} match_context = if match_context != bond do @@ -342,7 +317,7 @@ defmodule ElixirSense.Core.TypeInference do end {_ast, {new_vars, _match_context}} = - match_var(state, nth_elem_ast, {[], {:tuple_nth, match_context, n}}) + match_var(nth_elem_ast, {[], {:tuple_nth, match_context, n}}) new_vars end) @@ -352,18 +327,18 @@ defmodule ElixirSense.Core.TypeInference do # two element tuples on the left of `->` are encoded as list `[field1, field2]` # detect and convert to regular - defp match_var(state, {:->, meta, [[left], right]}, {vars, match_context}) do - match_var(state, {:->, meta, [left, right]}, {vars, match_context}) + defp match_var({:->, meta, [[left], right]}, {vars, match_context}) do + match_var({:->, meta, [left, right]}, {vars, match_context}) end - defp match_var(state, list, {vars, match_context}) + defp match_var(list, {vars, match_context}) when not is_nil(match_context) and is_list(list) do match_var_list = fn head, tail -> {_ast, {new_vars_head, _match_context}} = - match_var(state, head, {[], {:list_head, match_context}}) + match_var(head, {[], {:list_head, match_context}}) {_ast, {new_vars_tail, _match_context}} = - match_var(state, tail, {[], {:list_tail, match_context}}) + match_var(tail, {[], {:list_tail, match_context}}) {list, {vars ++ new_vars_head ++ new_vars_tail, nil}} end @@ -380,7 +355,7 @@ defmodule ElixirSense.Core.TypeInference do end end - defp match_var(_state, ast, {vars, match_context}) do + defp match_var(ast, {vars, match_context}) do {ast, {vars, match_context}} end diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 5eaa0a1b..d3238c43 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -2062,7 +2062,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert [ - %VarInfo{name: :a, type: {:list_head, {:attribute, :myattribute}}}, + %VarInfo{name: :a, type: {:intersection, [:atom, {:list_head, {:attribute, :myattribute}}]}}, ] = state |> get_line_vars(4) end @@ -2299,7 +2299,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] = state |> get_line_vars(13) |> Enum.filter(&(&1.name == :asd)) assert [ - %VarInfo{name: :x, type: {:intersection, [{:variable, :z}, {:variable, :asd}]}}, + %VarInfo{name: :x, type: {:intersection, [{:variable, :asd}, {:variable, :z}]}}, %VarInfo{name: :z, type: {:variable, :asd}} ] = state |> get_line_vars(15) |> Enum.filter(&(&1.name in [:x, :z])) end @@ -2358,6 +2358,11 @@ defmodule ElixirSense.Core.MetadataBuilderTest do defmodule MyModule do def some(%MyState{formatted: formatted} = state) do IO.puts "" + + case :ok do + %{foo: 1} = state = %{bar: 1} = x -> + IO.puts "" + end end end """ @@ -2373,6 +2378,74 @@ defmodule ElixirSense.Core.MetadataBuilderTest do type: {:struct, [formatted: {:variable, :formatted}], {:atom, MyState}, nil} } ] = state |> get_line_vars(3) + + assert [ + %VarInfo{ + name: :formatted + }, + %VarInfo{ + name: :state, + type: {:struct, [formatted: {:variable, :formatted}], {:atom, MyState}, nil} + } + ] = state |> get_line_vars(7) + end + + test "two way refinement in match context nested" do + state = + """ + defmodule MyModule do + def some(%{foo: 1} = state = %{bar: 1} = x) do + IO.puts "" + end + end + """ + |> string_to_state + + assert [ + %VarInfo{ + name: :formatted, + type: {:map_key, {:variable, :state}, {:atom, :formatted}}, + }, + %VarInfo{ + name: :state, + type: {:struct, [formatted: {:variable, :formatted}], {:atom, MyState}, nil} + } + ] = state |> get_line_vars(3) + + assert [ + %VarInfo{ + name: :formatted + }, + %VarInfo{ + name: :state, + type: {:struct, [formatted: {:variable, :formatted}], {:atom, MyState}, nil} + } + ] = state |> get_line_vars(7) + end + + test "two way refinement in match context nested case" do + state = + """ + defmodule MyModule do + def some(state) do + case :ok do + %{foo: 1} = state = %{bar: 1} = x -> + IO.puts "" + end + end + end + """ + |> string_to_state + + assert [ + %VarInfo{ + name: :formatted + }, + %VarInfo{ + name: :state, + type: {:struct, [formatted: {:variable, :formatted}], {:atom, MyState}, nil} + } + ] = state |> get_line_vars(5) end test "two way refinement in nested `=` binding" do @@ -2380,7 +2453,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ defmodule MyModule do def some() do - %State{formatted: formatted} = state = socket.assigns.state + %MyState{formatted: formatted} = state = socket.assigns.state IO.puts "" end end @@ -2406,9 +2479,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do type: {:intersection, [ - {:struct, [formatted: {:variable, :formatted}], {:atom, Elixir.State}, - nil}, - {:call, {:call, {:variable, :socket}, :assigns, []}, :state, []} + {:call, {:call, {:variable, :socket}, :assigns, []}, :state, []}, + {:struct, [formatted: {:variable, :formatted}], {:atom, MyState}, nil} ]} } ] = state |> get_line_vars(4) @@ -2439,6 +2511,35 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] = state |> get_line_vars(5) end + test "case binding with match" do + state = + """ + defmodule MyModule do + def some() do + case Some.call() do + {:ok, x} = res -> + IO.puts "" + end + end + end + """ + |> string_to_state + + assert [ + %VarInfo{ + name: :res, + type: :todo + }, + %VarInfo{ + name: :x, + type: + {:tuple_nth, + {:intersection, + [{:call, {:atom, Some}, :call, []}, {:tuple, 2, [{:atom, :ok}, nil]}]}, 1} + } + ] = state |> get_line_vars(5) + end + test "rescue binding" do state = """ From b5f93b515c3e6a998b05d7c8e9869d82bb15817d Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 30 May 2024 17:09:42 +0200 Subject: [PATCH 081/235] provide env from cursor --- lib/elixir_sense/core/compiler.ex | 8 ++++---- lib/elixir_sense/core/metadata.ex | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 88950fa1..d3961626 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -33,19 +33,19 @@ defmodule ElixirSense.Core.Compiler do match_context_r = TypeInference.get_binding_type(e_right) vars_l_with_inferred_types = TypeInference.find_vars(e_left, match_context_r) - expressions_to_refine = TypeInference.find_refinable(e_right, [], e) |> dbg + expressions_to_refine = TypeInference.find_refinable(e_right, [], e) vars_r_with_inferred_types = if expressions_to_refine != [] do # we are in match context and the right side is also a pattern, we can refine types # on the right side using the inferred type of the left side - match_context_l = TypeInference.get_binding_type(e_left) |> dbg + match_context_l = TypeInference.get_binding_type(e_left) for expr <- expressions_to_refine, reduce: [] do acc -> - vars_in_expr_with_inferred_types = TypeInference.find_vars(expr, match_context_l) |> dbg + vars_in_expr_with_inferred_types = TypeInference.find_vars(expr, match_context_l) acc ++ vars_in_expr_with_inferred_types end else [] - end |> dbg + end sl = merge_inferred_types(sl, vars_l_with_inferred_types ++ vars_r_with_inferred_types) diff --git a/lib/elixir_sense/core/metadata.ex b/lib/elixir_sense/core/metadata.ex index 511ad549..ba167552 100644 --- a/lib/elixir_sense/core/metadata.ex +++ b/lib/elixir_sense/core/metadata.ex @@ -59,6 +59,24 @@ defmodule ElixirSense.Core.Metadata do } end + def get_cursor_env(%__MODULE__{} = metadata, {line, column}) do + prefix = ElixirSense.Core.Source.text_before(metadata.source, line, column) + + {meta, cursor_env} = case NormalizedCode.Fragment.container_cursor_to_quoted(prefix, [columns: true, token_metadata: true]) do + {:ok, ast} -> + ElixirSense.Core.MetadataBuilder.build(ast).cursor_env || {[], nil} + + _ -> + {[], nil} + end + + env = if cursor_env != nil do + cursor_env + else + get_env(metadata, {line, column}) + end + end + @spec get_env(__MODULE__.t(), {pos_integer, pos_integer}) :: State.Env.t() def get_env(%__MODULE__{} = metadata, {line, column}) do all_scopes = From ca7986a2cf09c7f138a09ad8285527356c5f6bc2 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 30 May 2024 17:15:36 +0200 Subject: [PATCH 082/235] store variable version --- lib/elixir_sense/core/state.ex | 6 ++--- .../core/metadata_builder_test.exs | 23 +------------------ 2 files changed, 4 insertions(+), 25 deletions(-) diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 30a67c75..d62d8dca 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -162,13 +162,13 @@ defmodule ElixirSense.Core.State do name: atom, positions: list(ElixirSense.Core.State.position_t()), scope_id: nil | ElixirSense.Core.State.scope_id_t(), - is_definition: boolean, + version: non_neg_integer(), type: ElixirSense.Core.State.var_type() } defstruct name: nil, positions: [], scope_id: nil, - is_definition: false, + version: 0, type: nil end @@ -1112,7 +1112,7 @@ defmodule ElixirSense.Core.State do info = %VarInfo{ name: name, - is_definition: true, + version: version, positions: [{line, column}], scope_id: scope_id } diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index d3238c43..437d8aad 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -1754,7 +1754,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do %State.VarInfo{ name: :abc, type: {:struct, [cde: {:integer, 1}], {:atom, Abc}, nil}, - is_definition: true, positions: [{3, 1}] } ] = state |> get_line_vars(4) @@ -2635,11 +2634,11 @@ defmodule ElixirSense.Core.MetadataBuilderTest do vars = state |> get_line_vars(6) + # TODO wtf # assert %VarInfo{ # name: :a1, # positions: [{5, 18}], # scope_id: 6, - # is_definition: true, # type: {:map, [b: {:integer, 2}], nil} # } = Enum.find(vars, &(&1.name == :a1)) @@ -2648,7 +2647,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert %VarInfo{ name: :a2, positions: [{7, 18}], - is_definition: true, type: {:map, [b: {:variable, :b}], nil} } = Enum.find(vars, &(&1.name == :a2)) end @@ -2796,25 +2794,21 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert ([ %VarInfo{ - is_definition: true, name: :var_in, positions: [{5, 5}], scope_id: scope_id_3 }, %VarInfo{ - is_definition: true, name: :var_on, positions: [{4, 7}, {4, 24}, {4, 47}], scope_id: scope_id_2 }, %VarInfo{ - is_definition: true, name: :var_on1, positions: [{4, 37}], scope_id: scope_id_2 }, %VarInfo{ - is_definition: true, name: :var_out1, positions: [{2, 3}], scope_id: scope_id_1 @@ -2864,25 +2858,21 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert ([ %VarInfo{ - is_definition: true, name: :var_in, positions: [{5, 5}], scope_id: scope_id_3 }, %VarInfo{ - is_definition: true, name: :var_on, positions: [{4, 8}, {4, 25}, {4, 48}], scope_id: scope_id_2 }, %VarInfo{ - is_definition: true, name: :var_on1, positions: [{4, 38}], scope_id: scope_id_2 }, %VarInfo{ - is_definition: true, name: :var_out1, positions: [{2, 3}], scope_id: scope_id_1 @@ -2982,19 +2972,16 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert ([ %VarInfo{ - is_definition: true, name: :var_in, positions: [{4, 5}], scope_id: scope_id_2 }, %VarInfo{ - is_definition: true, name: :var_on, positions: [{3, 6}], scope_id: scope_id_2 }, %VarInfo{ - is_definition: true, name: :var_out1, positions: [{2, 3}], scope_id: scope_id_1 @@ -3042,25 +3029,21 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert ([ %VarInfo{ - is_definition: true, name: :var_in1, positions: [{5, 7}], scope_id: scope_id_2 }, %VarInfo{ - is_definition: true, name: :var_on0, positions: [{3, 8}], scope_id: scope_id_1 }, %VarInfo{ - is_definition: true, name: :var_on1, positions: [{4, 6}], scope_id: scope_id_2 }, %VarInfo{ - is_definition: true, name: :var_out1, positions: [{2, 3}, {3, 18}], scope_id: scope_id_1 @@ -3497,25 +3480,21 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert ([ %VarInfo{ - is_definition: true, name: :_my_other, positions: [{2, 24}], scope_id: scope_id_1 }, %VarInfo{ - is_definition: true, name: :abc, positions: [{3, 6}], scope_id: scope_id_2 }, %VarInfo{ - is_definition: true, name: :my_var, positions: [{2, 13}], scope_id: scope_id_1 }, %VarInfo{ - is_definition: true, name: :x, positions: [{2, 43}, {3, 14}], scope_id: scope_id_1 From 2e018b5154eb7ef9d7f6e0f7a460be3ebc64586e Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 30 May 2024 19:49:22 +0200 Subject: [PATCH 083/235] improve var scope updates --- lib/elixir_sense/core/metadata.ex | 12 +++++++----- lib/elixir_sense/core/state.ex | 11 +++++++++-- test/elixir_sense/core/metadata_builder_test.exs | 10 +++++----- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/lib/elixir_sense/core/metadata.ex b/lib/elixir_sense/core/metadata.ex index ba167552..f61266a6 100644 --- a/lib/elixir_sense/core/metadata.ex +++ b/lib/elixir_sense/core/metadata.ex @@ -207,18 +207,20 @@ defmodule ElixirSense.Core.Metadata do {line, column}, predicate \\ fn _ -> true end ) do - scope_vars = vars_info_per_scope_id[env.scope_id] || [] - env_vars_names = env.vars |> Enum.map(& &1.name) + scope_vars = vars_info_per_scope_id[env.scope_id] || %{} + env_vars_keys = env.vars |> Enum.map(& {&1.name, &1.version}) scope_vars_missing_in_env = scope_vars - |> Enum.filter(fn var -> - var.name not in env_vars_names and Enum.min(var.positions) <= {line, column} and + |> Enum.filter(fn {key, var} -> + key not in env_vars_keys and Enum.min(var.positions) <= {line, column} and predicate.(var) end) + |> Enum.map(fn {_, value} -> value end) env_vars = for var <- env.vars do - scope_vars |> Enum.find(& &1.name == var.name && &1.scope_id == var.scope_id) + key = {var.name, var.version} + Map.fetch!(scope_vars, key) end %{env | vars: env_vars ++ scope_vars_missing_in_env} diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index d62d8dca..d7891226 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -27,7 +27,7 @@ defmodule ElixirSense.Core.State do optional({module, atom, nil | non_neg_integer}) => ElixirSense.Core.State.SpecInfo.t() } @type vars_info_per_scope_id_t :: %{ - optional(scope_id_t) => %{optional(atom) => ElixirSense.Core.State.VarInfo.t()} + optional(scope_id_t) => [%{optional({atom(), non_neg_integer()}) => ElixirSense.Core.State.VerInfo.t()}] } @type structs_t :: %{optional(module) => ElixirSense.Core.State.StructInfo.t()} @type protocol_t :: {module, nonempty_list(module)} @@ -852,7 +852,14 @@ defmodule ElixirSense.Core.State do [scope_id | _other_scope_ids] = state.scope_ids [current_scope_vars | _other_scope_vars] = state.vars_info - Map.put(state.vars_info_per_scope_id, scope_id, current_scope_vars |> Map.values()) + for {scope_id, vars} <- state.vars_info_per_scope_id, into: %{} do + updated_vars = for {key, var} <- vars, into: %{} do + {key, Map.get(current_scope_vars, key, var)} + end + + {scope_id, updated_vars} + end + |> Map.put(scope_id, current_scope_vars) end def remove_attributes_scope(%__MODULE__{} = state) do diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 437d8aad..96421a9a 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -1237,7 +1237,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert [%VarInfo{scope_id: scope_id}] = state |> get_line_vars(4) - assert [%VarInfo{name: :var}] = state.vars_info_per_scope_id[scope_id] + assert [%VarInfo{name: :var}] = state.vars_info_per_scope_id[scope_id] |> Map.values() end test "vars defined inside a function without params" do @@ -1356,7 +1356,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert [ %VarInfo{name: :p, positions: [{2, 14}, {2, 21}, {2, 29}]} - ] = state.vars_info_per_scope_id[2] + ] = state.vars_info_per_scope_id[2] |> Map.values() end test "registers spec parameters" do @@ -1373,7 +1373,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert [ %VarInfo{name: :p, positions: [{2, 49}, {2, 18}, {2, 25}, {2, 33}, {2, 70}]}, %VarInfo{name: :q, positions: [{2, 49}, {2, 46}]} - ] = state.vars_info_per_scope_id[2] + ] = state.vars_info_per_scope_id[2] |> Map.values end test "does not register annotated spec params as type variables" do @@ -1385,7 +1385,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert [] == state.vars_info_per_scope_id[2] + assert %{} == state.vars_info_per_scope_id[2] end test "does not register annotated type elements as variables" do @@ -1397,7 +1397,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert [] == state.vars_info_per_scope_id[2] + assert %{} == state.vars_info_per_scope_id[2] end end From d88a94ddd6b436af807ed0101a34eee4903bdf4c Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 1 Jun 2024 11:10:00 +0200 Subject: [PATCH 084/235] try to get cursor env by replacing range with cursor --- lib/elixir_sense/core/metadata.ex | 37 +++++++++++++++++++++++++++++-- lib/elixir_sense/core/state.ex | 2 +- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/lib/elixir_sense/core/metadata.ex b/lib/elixir_sense/core/metadata.ex index f61266a6..89440d29 100644 --- a/lib/elixir_sense/core/metadata.ex +++ b/lib/elixir_sense/core/metadata.ex @@ -59,10 +59,43 @@ defmodule ElixirSense.Core.Metadata do } end + def get_cursor_env(%__MODULE__{} = metadata, {{begin_line, begin_column}, {end_line, end_column}}) do + prefix = ElixirSense.Core.Source.text_before(metadata.source, begin_line, begin_column) + suffix = ElixirSense.Core.Source.text_after(metadata.source, end_line, end_column) + + source_with_cursor = prefix <> "(__cursor__())" <> suffix + + {meta, cursor_env} = case Code.string_to_quoted(source_with_cursor, [columns: true, token_metadata: true]) do + {:ok, ast} -> + ElixirSense.Core.MetadataBuilder.build(ast).cursor_env || {[], nil} + + _ -> + {[], nil} + end + + {_meta, cursor_env} = if cursor_env != nil do + {meta, cursor_env} + else + case NormalizedCode.Fragment.container_cursor_to_quoted(prefix, [columns: true, token_metadata: true]) do + {:ok, ast} -> + ElixirSense.Core.MetadataBuilder.build(ast).cursor_env || {[], nil} + + _ -> + {[], nil} + end + end + + if cursor_env != nil do + cursor_env + else + get_env(metadata, {begin_line, begin_column}) + end + end + def get_cursor_env(%__MODULE__{} = metadata, {line, column}) do prefix = ElixirSense.Core.Source.text_before(metadata.source, line, column) - {meta, cursor_env} = case NormalizedCode.Fragment.container_cursor_to_quoted(prefix, [columns: true, token_metadata: true]) do + {_meta, cursor_env} = case NormalizedCode.Fragment.container_cursor_to_quoted(prefix, [columns: true, token_metadata: true]) do {:ok, ast} -> ElixirSense.Core.MetadataBuilder.build(ast).cursor_env || {[], nil} @@ -70,7 +103,7 @@ defmodule ElixirSense.Core.Metadata do {[], nil} end - env = if cursor_env != nil do + if cursor_env != nil do cursor_env else get_env(metadata, {line, column}) diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index d7891226..b6bbc3ed 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -1533,7 +1533,7 @@ defmodule ElixirSense.Core.State do outer_scope_vars = for {key, _} <- outer_scope_vars, into: %{}, - # TODO merge type? + # TODO merge type and positions? do: {key, current_scope_vars[key]} vars_info = [current_scope_vars, outer_scope_vars | other_scopes_vars] From 401588af23efdcf699234a9a7a03556df9939b1d Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 2 Jun 2024 12:47:17 +0200 Subject: [PATCH 085/235] current buffer import --- lib/elixir_sense/core/compiler.ex | 50 +++++++++++++++---- lib/elixir_sense/core/state.ex | 2 + .../core/metadata_builder_test.exs | 36 +++++++++++++ 3 files changed, 79 insertions(+), 9 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index d3961626..bd3f4957 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -244,16 +244,17 @@ defmodule ElixirSense.Core.Compiler do {opts, state, env} = expand_opts([:only, :except, :warn], opts, state, env) if is_atom(arg) do - # TODO check difference - # elixir_aliases:ensure_loaded(Meta, ERef, ET) - # elixir_import:import(Meta, ERef, EOpts, ET, true, true) - # TODO this does not work for context modules - with true <- Code.ensure_loaded?(arg), - {:ok, env} <- Macro.Env.define_import(env, meta, arg, [trace: false] ++ opts) do - {arg, state, env} - else + opts = opts + |> Keyword.merge([ + trace: false, + emit_warnings: false, + info_callback: import_info_callback(arg, state) + ]) + + case Macro.Env.define_import(env, meta, arg, opts) do + {:ok, env} -> + {arg, state, env} _ -> - # elixir_import {arg, state, env} end else @@ -2316,6 +2317,37 @@ defmodule ElixirSense.Core.Compiler do {e_args, __MODULE__.Env.close_write(sa, s), ea} end + @internals [{:behaviour_info, 1}, {:module_info, 1}, {:module_info, 0}] + defp import_info_callback(module, state) do + fn kind -> + if Map.has_key?(state.mods_funs_to_positions, {module, nil, nil}) do + category = if kind == :functions, do: :function, else: :macro + for {{^module, fun, arity}, info} when fun != nil <- state.mods_funs_to_positions, + {fun, arity} not in @internals, + State.ModFunInfo.get_category(info) == category, + not State.ModFunInfo.private?(info) do + {fun, arity} + end + else + # this branch is based on implementation in :elixir_import + if Code.ensure_loaded?(module) do + try do + module.__info__(kind) + rescue + UndefinedFunctionError -> + if kind == :functions do + module.module_info(:exports) -- @internals + else + [] + end + end + else + [] + end + end + end + end + defmodule Env do alias ElixirSense.Core.Compiler.Utils, as: ElixirUtils diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index b6bbc3ed..9f5323d0 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -315,6 +315,8 @@ defmodule ElixirSense.Core.State do do: :function def get_category(%ModFunInfo{}), do: :module + + def private?(%ModFunInfo{type: type}), do: type in [:defp, :defmacrop, :defguardp] end def current_aliases(%__MODULE__{} = state) do diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 96421a9a..c03a6e94 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -4426,6 +4426,42 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert Keyword.has_key?(functions, Enum) end + test "imports current buffer module" do + state = + """ + defmodule ImportedModule do + def some_fun(a), do: a + def _some_fun_underscored(a), do: a + defp some_fun_priv(a), do: a + defguard my_guard(x) when x > 0 + defguardp my_guard_priv(x) when x > 0 + defdelegate to_list(map), to: Map + defmacro some(a, b) do + quote do: unquote(a) + unquote(b) + end + defmacrop some_priv(a, b) do + quote do: unquote(a) + unquote(b) + end + defmacro _some_underscored(a, b) do + quote do: unquote(a) + unquote(b) + end + end + + defmodule OuterModule do + import ImportedModule + IO.puts "" + end + """ + |> string_to_state + + {functions, macros} = get_line_imports(state, 21) + assert Keyword.has_key?(functions, ImportedModule) + assert functions[ImportedModule] == [{:some_fun, 1}, {:to_list, 1}] + + assert Keyword.has_key?(macros, ImportedModule) + assert macros[ImportedModule] == [{:my_guard, 1}, {:some, 2}] + end + test "imports inside protocol" do state = """ From 9f2adb753eb587c08e637556c67dce6b6b14d99a Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 15 Jun 2024 08:05:38 +0200 Subject: [PATCH 086/235] apply capture fix from elixir --- lib/elixir_sense/core/compiler.ex | 12 +++++++++++ test/elixir_sense/core/compiler_test.exs | 26 +++++++++++++++++------- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index bd3f4957..17332fd8 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -3575,6 +3575,18 @@ defmodule ElixirSense.Core.Compiler do # named :"&1" are not valid syntax. var = {:"&#{pos}", meta, nil} {var, :orddict.store(pos, var, dict)} + + case :orddict.find(pos, dict) do + {:ok, var} -> + {var, dict}; + :error -> + # elixir uses here elixir_module:next_counter(?key(E, module)) + # but we are not compiling and do not need to keep count in module scope + # elixir 1.17 also renames the var to `capture` + next = System.unique_integer() + var = {:"&#{pos}", [{:counter, next} | meta], nil} + {var, :orddict.store(pos, var, dict)} + end end defp escape({:&, meta, [pos]}, dict) when is_integer(pos) do diff --git a/test/elixir_sense/core/compiler_test.exs b/test/elixir_sense/core/compiler_test.exs index bdf71766..ae23ba8e 100644 --- a/test/elixir_sense/core/compiler_test.exs +++ b/test/elixir_sense/core/compiler_test.exs @@ -61,7 +61,7 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do {expanded, state, env} = expand(ast) dbg(expanded) - assert clean_capture_arg_position(expanded) == elixir_expanded + assert clean_capture_arg(expanded) == clean_capture_arg_elixir(elixir_expanded) assert env == elixir_env assert state_to_map(state) == elixir_ex_to_map(elixir_state) end @@ -790,16 +790,28 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do assert_expansion(code) end - defp clean_capture_arg_position(ast) do + defp clean_capture_arg(ast) do {ast, _} = Macro.prewalk(ast, nil, fn {atom, meta, nil} = node, state when is_atom(atom) -> - # elixir does not store position meta, we need to clean it to make tests pass - meta = with "&" <> int <- to_string(atom), {_, ""} <- Integer.parse(int) do - meta |> Keyword.delete(:line) |> Keyword.delete(:column) + # elixir changes the name to capture and does different counter tracking + node = with "&" <> int <- to_string(atom), {_, ""} <- Integer.parse(int) do + meta = Keyword.delete(meta, :counter) + {:capture, meta, nil} else - _ -> meta + _ -> node end - {{atom, meta, nil}, state} + {node, state} + node, state -> {node, state} + end) + ast + end + + defp clean_capture_arg_elixir(ast) do + {ast, _} = Macro.prewalk(ast, nil, fn + {:capture, meta, nil} = node, state -> + # elixir changes the name to capture and does different counter tracking + meta = Keyword.delete(meta, :counter) + {{:capture, meta, nil}, state} node, state -> {node, state} end) ast From a4c379e1ddd86ccd7519d57207c8d0f4ec323930 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 17 Jul 2024 15:13:03 +0200 Subject: [PATCH 087/235] backport Macro.Env APIs --- lib/elixir_sense/core/binding.ex | 16 +- lib/elixir_sense/core/compiler.ex | 673 +++++++++------ lib/elixir_sense/core/guard.ex | 79 +- lib/elixir_sense/core/metadata.ex | 65 +- lib/elixir_sense/core/metadata_builder.ex | 65 +- lib/elixir_sense/core/normalized/macro/env.ex | 666 +++++++++++++++ lib/elixir_sense/core/state.ex | 41 +- lib/elixir_sense/core/type_inference.ex | 20 +- test/elixir_sense/core/binding_test.exs | 158 ++-- test/elixir_sense/core/compiler_test.exs | 135 +-- .../metadata_builder/error_recovery_test.exs | 362 ++++++-- .../core/metadata_builder_test.exs | 789 ++++++++---------- 12 files changed, 2030 insertions(+), 1039 deletions(-) create mode 100644 lib/elixir_sense/core/normalized/macro/env.ex diff --git a/lib/elixir_sense/core/binding.ex b/lib/elixir_sense/core/binding.ex index b179c060..de8c3e33 100644 --- a/lib/elixir_sense/core/binding.ex +++ b/lib/elixir_sense/core/binding.ex @@ -340,13 +340,21 @@ defmodule ElixirSense.Core.Binding do def do_expand(_env, {:integer, integer}, _stack), do: {:integer, integer} def do_expand(_env, {:union, all}, _stack) do - all = Enum.filter(all, & &1 != :none) + all = Enum.filter(all, &(&1 != :none)) + cond do - all == [] -> :none - Enum.any?(all, & &1 == nil) -> nil - match?([_], all) -> hd(all) + all == [] -> + :none + + Enum.any?(all, &(&1 == nil)) -> + nil + + match?([_], all) -> + hd(all) + true -> first = hd(all) + if Enum.all?(tl(all), &(&1 == first)) do first else diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 17332fd8..1513ed69 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -6,6 +6,7 @@ defmodule ElixirSense.Core.Compiler do alias ElixirSense.Core.TypeInfo alias ElixirSense.Core.TypeInference alias ElixirSense.Core.Guard + alias ElixirSense.Core.Normalized.Macro.Env, as: NormalizedMacroEnv @env :elixir_env.new() def env, do: @env @@ -34,18 +35,21 @@ defmodule ElixirSense.Core.Compiler do vars_l_with_inferred_types = TypeInference.find_vars(e_left, match_context_r) expressions_to_refine = TypeInference.find_refinable(e_right, [], e) - vars_r_with_inferred_types = if expressions_to_refine != [] do - # we are in match context and the right side is also a pattern, we can refine types - # on the right side using the inferred type of the left side - match_context_l = TypeInference.get_binding_type(e_left) - for expr <- expressions_to_refine, reduce: [] do - acc -> - vars_in_expr_with_inferred_types = TypeInference.find_vars(expr, match_context_l) - acc ++ vars_in_expr_with_inferred_types + + vars_r_with_inferred_types = + if expressions_to_refine != [] do + # we are in match context and the right side is also a pattern, we can refine types + # on the right side using the inferred type of the left side + match_context_l = TypeInference.get_binding_type(e_left) + + for expr <- expressions_to_refine, reduce: [] do + acc -> + vars_in_expr_with_inferred_types = TypeInference.find_vars(expr, match_context_l) + acc ++ vars_in_expr_with_inferred_types + end + else + [] end - else - [] - end sl = merge_inferred_types(sl, vars_l_with_inferred_types ++ vars_r_with_inferred_types) @@ -103,7 +107,7 @@ defmodule ElixirSense.Core.Compiler do # __aliases__ defp do_expand({:__aliases__, meta, [head | tail] = list}, state, env) do - case Macro.Env.expand_alias(env, meta, list, trace: false) do + case NormalizedMacroEnv.expand_alias(env, meta, list, trace: false) do {:alias, alias} -> # TODO? # A compiler may want to emit a :local_function trace in here. @@ -158,7 +162,7 @@ defmodule ElixirSense.Core.Compiler do # TODO check difference with # elixir_aliases:alias(Meta, Ref, IncludeByDefault, Opts, E, true) # TODO PR to elixir with is_atom(module) check? - case Macro.Env.define_alias(env, meta, arg, [trace: false] ++ opts) do + case NormalizedMacroEnv.define_alias(env, meta, arg, [trace: false] ++ opts) do {:ok, env} -> {arg, state, env} @@ -201,7 +205,7 @@ defmodule ElixirSense.Core.Compiler do # we counter it with only calling it if :as option is set # {arg, state, alias(meta, e_ref, false, e_opts, ea)} if Keyword.has_key?(opts, :as) do - case Macro.Env.define_alias(env, meta, arg, [trace: false] ++ opts) do + case NormalizedMacroEnv.define_alias(env, meta, arg, [trace: false] ++ opts) do {:ok, env} -> {arg, state, env} @@ -219,7 +223,7 @@ defmodule ElixirSense.Core.Compiler do # ElixirAliases.ensure_loaded(meta, e_ref, et) # re = ElixirAliases.require(meta, e_ref, e_opts, et, true) # {e_ref, st, alias(meta, e_ref, false, e_opts, re)} - case Macro.Env.define_require(env, meta, arg, [trace: false] ++ opts) do + case NormalizedMacroEnv.define_require(env, meta, arg, [trace: false] ++ opts) do {:ok, env} -> {arg, state, env} @@ -244,16 +248,18 @@ defmodule ElixirSense.Core.Compiler do {opts, state, env} = expand_opts([:only, :except, :warn], opts, state, env) if is_atom(arg) do - opts = opts - |> Keyword.merge([ - trace: false, - emit_warnings: false, - info_callback: import_info_callback(arg, state) - ]) - - case Macro.Env.define_import(env, meta, arg, opts) do + opts = + opts + |> Keyword.merge( + trace: false, + emit_warnings: false, + info_callback: import_info_callback(arg, state) + ) + + case NormalizedMacroEnv.define_import(env, meta, arg, opts) do {:ok, env} -> - {arg, state, env} + {arg, state, env} + _ -> {arg, state, env} end @@ -333,6 +339,7 @@ defmodule ElixirSense.Core.Compiler do # elixir raises here missing_option # generate a fake do block expand({:quote, meta, [opts, [{:do, {:__block__, [], []}}]]}, s, e) + {do_block, new_opts} -> expand({:quote, meta, [new_opts, [{:do, do_block}]]}, s, e) end @@ -348,7 +355,9 @@ defmodule ElixirSense.Core.Compiler do defp do_expand({:quote, meta, [opts, do_block]}, s, e) when is_list(do_block) do exprs = case Keyword.fetch(do_block, :do) do - {:ok, expr} -> expr + {:ok, expr} -> + expr + :error -> # elixir raises here missing_option # try to recover from error by generating a fake do block @@ -396,14 +405,16 @@ defmodule ElixirSense.Core.Compiler do quoted = __MODULE__.Quote.quote(exprs, q) {e_quoted, es, eq} = expand(quoted, sc, ec) - e_binding = for {k, v} <- binding do - {:{}, [], [:=, [], [{:{}, [], [k, meta, e_context]}, v]]} - end + e_binding = + for {k, v} <- binding do + {:{}, [], [:=, [], [{:{}, [], [k, meta, e_context]}, v]]} + end - e_binding_quoted = case e_binding do - [] -> e_quoted - _ -> {:{}, [], [:__block__, [], e_binding ++ [e_quoted]]} - end + e_binding_quoted = + case e_binding do + [] -> e_quoted + _ -> {:{}, [], [:__block__, [], e_binding ++ [e_quoted]]} + end case e_prelude do [] -> {e_binding_quoted, es, eq} @@ -437,7 +448,6 @@ defmodule ElixirSense.Core.Compiler do when is_atom(context) and is_integer(arity) do case resolve_super(meta, arity, s, e) do {kind, name, _} when kind in [:def, :defp] -> - line = Keyword.get(super_meta, :line, 0) column = Keyword.get(super_meta, :column, nil) @@ -492,12 +502,13 @@ defmodule ElixirSense.Core.Compiler do # Cursor defp do_expand({:__cursor__, meta, []}, s, e) do - s = unless s.cursor_env do - s - |> add_cursor_env(meta, e) - else - s - end + s = + unless s.cursor_env do + s + |> add_cursor_env(meta, e) + else + s + end {{:__cursor__, meta, []}, s, e} end @@ -506,10 +517,11 @@ defmodule ElixirSense.Core.Compiler do defp do_expand({:super, meta, args}, s, e) when is_list(args) do arity = length(args) + case resolve_super(meta, arity, s, e) do {kind, name, _} -> {e_args, sa, ea} = expand_args(args, s, e) - + line = Keyword.get(meta, :line, 0) column = Keyword.get(meta, :column, nil) @@ -519,6 +531,7 @@ defmodule ElixirSense.Core.Compiler do |> add_current_env_to_line(line, ea) {{:super, [{:super, {kind, name}} | meta], e_args}, sa, ea} + _ -> # elixir does not allow this branch expand_local(meta, :super, args, s, e) @@ -674,7 +687,7 @@ defmodule ElixirSense.Core.Compiler do allow_locals = match?({n, a} when fun != n or arity != a, env.function) # TODO this crashes with CompileError ambiguous_call - case Macro.Env.expand_import(env, meta, fun, arity, + case NormalizedMacroEnv.expand_import(env, meta, fun, arity, trace: false, allow_locals: allow_locals, check_deprecations: false @@ -690,19 +703,23 @@ defmodule ElixirSense.Core.Compiler do expand_macro(meta, module, fun, args, callback, state, env) {:function, module, fun} -> - {ar, af} = case __MODULE__.Rewrite.inline(module, fun, arity) do - {ar, an} -> - {ar, an} - false -> - {module, fun} - end + {ar, af} = + case __MODULE__.Rewrite.inline(module, fun, arity) do + {ar, an} -> + {ar, an} + + false -> + {module, fun} + end + expand_remote(ar, meta, af, meta, args, state, __MODULE__.Env.prepare_write(state), env) {:error, :not_found} -> expand_local(meta, fun, args, state, env) - + {:error, {:conflict, _module}} -> raise "conflict" + {:error, {:ambiguous, _module}} -> raise "ambiguous" end @@ -723,7 +740,7 @@ defmodule ElixirSense.Core.Compiler do expand_remote(ar, dot_meta, an, meta, args, state, state_l, env) false -> - case Macro.Env.expand_require(env, meta, module, fun, arity, + case NormalizedMacroEnv.expand_require(env, meta, module, fun, arity, trace: false, check_deprecations: false ) do @@ -761,9 +778,10 @@ defmodule ElixirSense.Core.Compiler do column end - sa = sa - |> add_call_to_line({nil, e_expr, length(e_args)}, {line, column}) - |> add_current_env_to_line(line, e) + sa = + sa + |> add_call_to_line({nil, e_expr, length(e_args)}, {line, column}) + |> add_current_env_to_line(line, e) {{{:., dot_meta, [e_expr]}, meta, e_args}, sa, ea} end @@ -838,7 +856,8 @@ defmodule ElixirSense.Core.Compiler do _callback, state, env = %{module: module} - ) when module != nil do + ) + when module != nil do {position, end_position} = extract_range(meta) {line, _} = position @@ -880,7 +899,8 @@ defmodule ElixirSense.Core.Compiler do _callback, state, env = %{module: module} - ) when module != nil do + ) + when module != nil do line = Keyword.fetch!(meta, :line) state = @@ -899,7 +919,8 @@ defmodule ElixirSense.Core.Compiler do _callback, state, env = %{module: module} - ) when module != nil do + ) + when module != nil do line = Keyword.fetch!(meta, :line) state = @@ -952,7 +973,8 @@ defmodule ElixirSense.Core.Compiler do _callback, state, env = %{module: module} - ) when module != nil do + ) + when module != nil do line = Keyword.fetch!(meta, :line) state = @@ -977,7 +999,8 @@ defmodule ElixirSense.Core.Compiler do _callback, state, env = %{module: module} - ) when module != nil do + ) + when module != nil do line = Keyword.fetch!(meta, :line) state = @@ -1001,7 +1024,8 @@ defmodule ElixirSense.Core.Compiler do _callback, state, env = %{module: module} - ) when module != nil do + ) + when module != nil do line = Keyword.fetch!(meta, :line) state = @@ -1025,7 +1049,8 @@ defmodule ElixirSense.Core.Compiler do _callback, state, env = %{module: module} - ) when module != nil do + ) + when module != nil do line = Keyword.fetch!(meta, :line) column = Keyword.fetch!(meta, :column) @@ -1091,8 +1116,9 @@ defmodule ElixirSense.Core.Compiler do {name, type_args} -> if __MODULE__.Typespec.built_in_type?(name, length(type_args)) do - raise"type #{name}/#{length(type_args)} is a built-in type and it cannot be redefined" + raise "type #{name}/#{length(type_args)} is a built-in type and it cannot be redefined" end + # TODO elixir does Macro.escape with unquote: true spec = TypeInfo.typespec_to_string(kind, expr) @@ -1188,8 +1214,8 @@ defmodule ElixirSense.Core.Compiler do [_] -> # @attribute(arg) - if env.function, do: raise "cannot set attribute @#{name} inside function/macro" - if name == :behavior, do: raise "@behavior attribute is not supported" + if env.function, do: raise("cannot set attribute @#{name} inside function/macro") + if name == :behavior, do: raise("@behavior attribute is not supported") {true, expand_args(args, state, env)} args -> @@ -1218,7 +1244,8 @@ defmodule ElixirSense.Core.Compiler do _callback, state, env = %{module: module} - ) when module != nil do + ) + when module != nil do {arg, state, env} = expand(arg, state, env) case arg do @@ -1578,8 +1605,17 @@ defmodule ElixirSense.Core.Compiler do expand_macro(meta, Kernel, def_kind, [call, {:__block__, [], []}], callback, state, env) end - defp expand_macro(meta, Kernel, def_kind, [call, expr], _callback, state, env = %{module: module}) when module != nil - and def_kind in [:def, :defp, :defmacro, :defmacrop, :defguard, :defguardp] do + defp expand_macro( + meta, + Kernel, + def_kind, + [call, expr], + _callback, + state, + env = %{module: module} + ) + when module != nil and + def_kind in [:def, :defp, :defmacro, :defmacrop, :defguard, :defguardp] do # dbg(call) # dbg(expr) # dbg(def_kind) @@ -1622,12 +1658,15 @@ defmodule ElixirSense.Core.Compiler do arity = length(args) # based on :elixir_def.env_for_expansion - state = %{state | - vars: {%{}, false}, - unused: 0, - caller: def_kind in [:defmacro, :defmacrop, :defguard, :defguardp] + state = + %{ + state + | vars: {%{}, false}, + unused: 0, + caller: def_kind in [:defmacro, :defmacrop, :defguard, :defguardp] } |> new_func_vars_scope + env_for_expand = %{env | function: {name, arity}} # based on :elixir_clauses.def @@ -1771,7 +1810,7 @@ defmodule ElixirSense.Core.Compiler do defp alias_defmodule({:__aliases__, meta, [h | t]}, _module, env) when is_atom(h) do module = Module.concat([env.module, h]) alias = String.to_atom("Elixir." <> Atom.to_string(h)) - {:ok, env} = Macro.Env.define_alias(env, meta, module, as: alias, trace: false) + {:ok, env} = NormalizedMacroEnv.define_alias(env, meta, module, as: alias, trace: false) case t do [] -> {module, env} @@ -1807,7 +1846,15 @@ defmodule ElixirSense.Core.Compiler do attached_meta = attach_runtime_module(receiver, meta, s, e) {e_args, {sa, _}, ea} = map_fold(&expand_arg/3, {sl, s}, e, args) - case __MODULE__.Rewrite.rewrite(context, receiver, dot_meta, right, attached_meta, e_args, s) do + case __MODULE__.Rewrite.rewrite( + context, + receiver, + dot_meta, + right, + attached_meta, + e_args, + s + ) do {:ok, rewritten} -> s = __MODULE__.Env.close_write(sa, s) @@ -1822,6 +1869,7 @@ defmodule ElixirSense.Core.Compiler do __MODULE__.Env.close_write(sa, s) |> add_call_to_line({receiver, right, length(e_args)}, {line, column}) |> add_current_env_to_line(line, e) + {{{:., dot_meta, [receiver, right]}, attached_meta, e_args}, s, ea} end end @@ -1835,10 +1883,11 @@ defmodule ElixirSense.Core.Compiler do column = Keyword.get(meta, :column, nil) s = - __MODULE__.Env.close_write(sa, s) - |> add_call_to_line({receiver, right, length(e_args)}, {line, column}) - |> add_current_env_to_line(line, e) - {{{:., dot_meta, [receiver, right]}, meta, e_args}, s, ea} + __MODULE__.Env.close_write(sa, s) + |> add_call_to_line({receiver, right, length(e_args)}, {line, column}) + |> add_current_env_to_line(line, e) + + {{{:., dot_meta, [receiver, right]}, meta, e_args}, s, ea} end defp attach_runtime_module(receiver, meta, s, _e) do @@ -1992,10 +2041,12 @@ defmodule ElixirSense.Core.Compiler do defp overridable_name(name, count) when is_integer(count), do: :"#{name} (overridable #{count})" - defp resolve_super(_meta, _arity, _state, %{module: module, function: function}) when module == nil or function == nil do + defp resolve_super(_meta, _arity, _state, %{module: module, function: function}) + when module == nil or function == nil do # elixir asserts scope is function nil end + defp resolve_super(_meta, arity, state, %{module: module, function: function}) do case function do {name, ^arity} -> @@ -2081,23 +2132,26 @@ defmodule ElixirSense.Core.Compiler do {nil, do_opts} -> # elixir raises missing_option here {[], do_opts} - {do_expr, do_opts} -> {do_expr, do_opts} + + {do_expr, do_opts} -> + {do_expr, do_opts} end {e_opts, so, eo} = expand(opts, __MODULE__.Env.reset_vars(s), e) {e_cases, sc, ec} = map_fold(&expand_for_generator/3, so, eo, cases) # elixir raises here for_generator_start on invalid start generator - {maybe_reduce, normalized_opts} = sanitize_for_options(e_opts, false, false, false, return, meta, e, []) + {maybe_reduce, normalized_opts} = + sanitize_for_options(e_opts, false, false, false, return, meta, e, []) - # TODO not sure new vars scope is actually needed - sc = sc |> new_vars_scope - {e_expr, se, ee} = expand_for_do_block(meta, expr, sc, ec, maybe_reduce) + # TODO not sure new vars scope is actually needed + sc = sc |> new_vars_scope + {e_expr, se, ee} = expand_for_do_block(meta, expr, sc, ec, maybe_reduce) - se = + se = se - |> maybe_move_vars_to_outer_scope - |> remove_vars_scope + |> maybe_move_vars_to_outer_scope + |> remove_vars_scope {{:for, meta, e_cases ++ [[{:do, e_expr} | normalized_opts]]}, __MODULE__.Env.merge_vars(se, s, ee), e} @@ -2117,10 +2171,12 @@ defmodule ElixirSense.Core.Compiler do # elixir checks here that clause has exactly 1 arg by matching against {_, _, [[_], _]} # we drop excessive or generate a fake arg # TODO check if there is cursor in dropped arg? - args = case args do - [] -> [{:_, [], e.module}] - [head | _] -> [head] - end + args = + case args do + [] -> [{:_, [], e.module}] + [head | _] -> [head] + end + clause = {:->, clause_meta, [args, right]} s_reset = __MODULE__.Env.reset_vars(sa) @@ -2141,6 +2197,7 @@ defmodule ElixirSense.Core.Compiler do [] -> # try to recover from error by emitting a fake clause expand_for_do_block(meta, [{:->, meta, [[{:_, [], e.module}], :ok]}], s, e, reduce) + _ -> # try to recover from error by wrapping the expression in clause expand_for_do_block(meta, [{:->, meta, [[expr], :ok]}], s, e, reduce) @@ -2153,7 +2210,9 @@ defmodule ElixirSense.Core.Compiler do {[e_left], sl, el} = __MODULE__.Clauses.head([left], sm, er) match_context_r = TypeInference.get_binding_type(e_right) - vars_l_with_inferred_types = TypeInference.find_vars(e_left, {:for_expression, match_context_r}) + + vars_l_with_inferred_types = + TypeInference.find_vars(e_left, {:for_expression, match_context_r}) sl = State.merge_inferred_types(sl, vars_l_with_inferred_types) @@ -2176,6 +2235,7 @@ defmodule ElixirSense.Core.Compiler do sm, er ) + # no point in doing type inference here, we're only going to find integers and binaries {{:<<>>, meta, [{:<-, op_meta, [e_left, e_right]}]}, sl, el} @@ -2322,10 +2382,11 @@ defmodule ElixirSense.Core.Compiler do fn kind -> if Map.has_key?(state.mods_funs_to_positions, {module, nil, nil}) do category = if kind == :functions, do: :function, else: :macro + for {{^module, fun, arity}, info} when fun != nil <- state.mods_funs_to_positions, - {fun, arity} not in @internals, - State.ModFunInfo.get_category(info) == category, - not State.ModFunInfo.private?(info) do + {fun, arity} not in @internals, + State.ModFunInfo.get_category(info) == category, + not State.ModFunInfo.private?(info) do {fun, arity} end else @@ -2455,12 +2516,15 @@ defmodule ElixirSense.Core.Compiler do def has_cursor?(ast) do # TODO rewrite to lazy prewalker - {_, result} = Macro.prewalk(ast, false, fn - {:__cursor__, _, list} = node, _state when is_list(list) -> - {node, true} - node, state -> - {node, state} - end) + {_, result} = + Macro.prewalk(ast, false, fn + {:__cursor__, _, list} = node, _state when is_list(list) -> + {node, true} + + node, state -> + {node, state} + end) + result end end @@ -2598,16 +2662,25 @@ defmodule ElixirSense.Core.Compiler do end defp expand_case(meta, {:do, _} = do_clause, match_context, s, e) do - expand_clauses(meta, :case, fn c, s, e -> - case head(c, s, e) do - {[h | _] = c, s, e} -> - clause_vars_with_inferred_types = TypeInference.find_vars(h, match_context) - s = State.merge_inferred_types(s, clause_vars_with_inferred_types) - - {c, s, e} - other -> other - end - end, do_clause, s, e) + expand_clauses( + meta, + :case, + fn c, s, e -> + case head(c, s, e) do + {[h | _] = c, s, e} -> + clause_vars_with_inferred_types = TypeInference.find_vars(h, match_context) + s = State.merge_inferred_types(s, clause_vars_with_inferred_types) + + {c, s, e} + + other -> + other + end + end, + do_clause, + s, + e + ) end # cond @@ -2683,10 +2756,12 @@ defmodule ElixirSense.Core.Compiler do expr when not is_list(expr) -> # try to recover from error by wrapping the expression in list expand_receive(meta, {:after, [expr]}, s, e) + [first | _] -> # try to recover from error by taking first clause only # TODO maybe search for clause with cursor? expand_receive(meta, {:after, [first]}, s, e) + [] -> # try to recover from error by inserting a fake clause expand_receive(meta, {:after, [{:->, meta, [[0], :ok]}]}, s, e) @@ -2749,6 +2824,7 @@ defmodule ElixirSense.Core.Compiler do {expr, rest_opts} -> pair = {:else, expr} + # no point in doing type inference here, we have no idea what data we are matching against {e_pair, se} = expand_clauses(meta, :with, &head/3, pair, s, e) {[e_pair], rest_opts, se} @@ -2762,6 +2838,7 @@ defmodule ElixirSense.Core.Compiler do # emit a fake do block try(meta, [do: []], s, e) end + def try(_meta, opts, s, e) when not is_list(opts) do # elixir raises here invalid_args # there may be cursor @@ -2840,12 +2917,12 @@ defmodule ElixirSense.Core.Compiler do # rescue var defp expand_rescue({name, _, atom} = var, s, e) when is_atom(name) and is_atom(atom) do {e_left, sl, el} = match(&ElixirExpand.expand/3, var, s, s, e) - + match_context = {:struct, [], {:atom, Exception}, nil} vars_with_inferred_types = TypeInference.find_vars(e_left, match_context) sl = State.merge_inferred_types(sl, vars_with_inferred_types) - + {e_left, sl, el} end @@ -2867,7 +2944,7 @@ defmodule ElixirSense.Core.Compiler do vars_with_inferred_types = TypeInference.find_vars(e_left, match_context) sl = State.merge_inferred_types(sl, vars_with_inferred_types) - + {e_left, sl, el} end @@ -2880,23 +2957,24 @@ defmodule ElixirSense.Core.Compiler do {name, _, atom} when is_atom(name) and is_atom(atom) -> normalized = normalize_rescue(e_right, e) - match_context = for exception <- normalized, reduce: nil do - nil -> {:struct, [], {:atom, exception}, nil} - other -> {:union, [other, {:struct, [], {:atom, exception}, nil}]} - end + match_context = + for exception <- normalized, reduce: nil do + nil -> {:struct, [], {:atom, exception}, nil} + other -> {:union, [other, {:struct, [], {:atom, exception}, nil}]} + end - match_context = if match_context == nil do - {:struct, [], {:atom, Exception}, nil} - else - match_context - end + match_context = + if match_context == nil do + {:struct, [], {:atom, Exception}, nil} + else + match_context + end vars_with_inferred_types = TypeInference.find_vars(e_left, match_context) sr = State.merge_inferred_types(sr, vars_with_inferred_types) {{:in, meta, [e_left, normalized]}, sr, er} - _ -> # elixir rejects this case, we normalize to underscore {{:in, meta, [{:_, [], e.module}, normalize_rescue(e_right, e)]}, sr, er} @@ -2911,7 +2989,9 @@ defmodule ElixirSense.Core.Compiler do # elixir rejects this case # try to recover from error by generating fake expression expand_rescue({:in, meta, [arg, {:_, [], e.module}]}, s, e) - new_arg -> expand_rescue(new_arg, s, e) + + new_arg -> + expand_rescue(new_arg, s, e) end end @@ -2926,11 +3006,12 @@ defmodule ElixirSense.Core.Compiler do defp normalize_rescue(other, e) do # elixir is strict here, we reject invalid nodes - res = if is_list(other) do - Enum.filter(other, &is_atom/1) - else - [] - end + res = + if is_list(other) do + Enum.filter(other, &is_atom/1) + else + [] + end if res == [] do [{:_, [], e.module}] @@ -3148,6 +3229,7 @@ defmodule ElixirSense.Core.Compiler do defp type(:default, :default), do: :integer defp type(expr_type, :default), do: expr_type + defp type(:binary, type) when type in [:binary, :bitstring, :utf8, :utf16, :utf32], do: type @@ -3164,10 +3246,12 @@ defmodule ElixirSense.Core.Compiler do type(:default, :default) end - defp expand_each_spec(meta, [{:__cursor__, _, args} = h | t], map, s, original_s, e) when is_list(args) do + defp expand_each_spec(meta, [{:__cursor__, _, args} = h | t], map, s, original_s, e) + when is_list(args) do {_, s, e} = ElixirExpand.expand(h, s, e) expand_each_spec(meta, t, map, s, original_s, e) end + defp expand_each_spec(meta, [{expr, meta_e, args} = h | t], map, s, original_s, e) when is_atom(expr) do case validate_spec(expr, args) do @@ -3462,11 +3546,14 @@ defmodule ElixirSense.Core.Compiler do def capture(meta, {:__block__, _, expr}, s, e) do # elixir raises block_expr_in_capture # try to recover from error - expr = case expr do - [] -> {:"&1", meta, e.module} - list -> - ElixirUtils.select_with_cursor(list) || hd(list) - end + expr = + case expr do + [] -> + {:"&1", meta, e.module} + + list -> + ElixirUtils.select_with_cursor(list) || hd(list) + end capture(meta, expr, s, e) end @@ -3503,11 +3590,12 @@ defmodule ElixirSense.Core.Compiler do end defp capture_import({atom, import_meta, args} = expr, s, e, sequential) do - res = if sequential do - ElixirDispatch.import_function(import_meta, atom, length(args), e) - else - false - end + res = + if sequential do + ElixirDispatch.import_function(import_meta, atom, length(args), e) + else + false + end handle_capture(res, import_meta, import_meta, expr, s, e, sequential) end @@ -3517,20 +3605,21 @@ defmodule ElixirSense.Core.Compiler do {esc_left, []} -> {e_left, se, ee} = ElixirExpand.expand(esc_left, s, e) - res = if sequential do - case e_left do - {name, _, context} when is_atom(name) and is_atom(context) -> - {:remote, e_left, right, length(args)} + res = + if sequential do + case e_left do + {name, _, context} when is_atom(name) and is_atom(context) -> + {:remote, e_left, right, length(args)} - _ when is_atom(e_left) -> - ElixirDispatch.require_function(require_meta, e_left, right, length(args), ee) + _ when is_atom(e_left) -> + ElixirDispatch.require_function(require_meta, e_left, right, length(args), ee) - _ -> - false + _ -> + false + end + else + false end - else - false - end dot = {{:., dot_meta, [e_left, right]}, require_meta, args} handle_capture(res, require_meta, dot_meta, dot, se, ee, sequential) @@ -3564,7 +3653,7 @@ defmodule ElixirSense.Core.Compiler do {e_expr, e_dict} -> # elixir raises capture_arg_without_predecessor here # if argument vars are not consecutive - e_vars = Enum.map(e_dict, & elem(&1, 1)) + e_vars = Enum.map(e_dict, &elem(&1, 1)) fn_expr = {:fn, meta, [{:->, meta, [e_vars, e_expr]}]} {:expand, fn_expr, s, e} end @@ -3578,7 +3667,8 @@ defmodule ElixirSense.Core.Compiler do case :orddict.find(pos, dict) do {:ok, var} -> - {var, dict}; + {var, dict} + :error -> # elixir uses here elixir_module:next_counter(?key(E, module)) # but we are not compiling and do not need to keep count in module scope @@ -3823,14 +3913,15 @@ defmodule ElixirSense.Core.Compiler do {:{}, [], [:quote, meta(new_meta, q), [t_opts, t_arg]]} end - defp do_quote({:unquote, meta, [expr]}, %__MODULE__{unquote: true}) when is_list(meta), do: expr + defp do_quote({:unquote, meta, [expr]}, %__MODULE__{unquote: true}) when is_list(meta), + do: expr # Aliases defp do_quote({:__aliases__, meta, [h | t] = list}, %__MODULE__{aliases_hygiene: e = %{}} = q) - when is_atom(h) and h != :"Elixir" and is_list(meta) do + when is_atom(h) and h != Elixir and is_list(meta) do annotation = - case Macro.Env.expand_alias(e, meta, list, trace: false) do + case NormalizedMacroEnv.expand_alias(e, meta, list, trace: false) do {:alias, atom} -> atom :error -> false end @@ -3857,7 +3948,8 @@ defmodule ElixirSense.Core.Compiler do defp do_quote( {:__cursor__, meta, args}, %__MODULE__{unquote: _} - ) when is_list(args) do + ) + when is_list(args) do # emit cursor as is regardless of unquote {:__cursor__, meta, args} end @@ -3867,11 +3959,13 @@ defmodule ElixirSense.Core.Compiler do defp do_quote( {{{:., meta, [left, :unquote]}, _, [expr]}, _, args}, %__MODULE__{unquote: true} = q - ) when is_list(meta) do + ) + when is_list(meta) do do_quote_call(left, meta, expr, args, q) end - defp do_quote({{:., meta, [left, :unquote]}, _, [expr]}, %__MODULE__{unquote: true} = q) when is_list(meta) do + defp do_quote({{:., meta, [left, :unquote]}, _, [expr]}, %__MODULE__{unquote: true} = q) + when is_list(meta) do do_quote_call(left, meta, expr, nil, q) end @@ -3951,6 +4045,7 @@ defmodule ElixirSense.Core.Compiler do ElixirDispatch.find_imports(meta, name, e) do [_ | _] = imports -> keystore(:imports, keystore(:context, meta, q.context), imports) + _ -> case arity == 1 && Keyword.fetch(meta, :ambiguous_op) do {:ok, nil} -> @@ -4205,6 +4300,7 @@ defmodule ElixirSense.Core.Compiler do # elixir raises here, we choose first one # TODO trace call? head + _ -> false end @@ -4325,7 +4421,8 @@ defmodule ElixirSense.Core.Compiler do {[], []} -> false - _ -> {:ambiguous, fun_match ++ mac_match} + _ -> + {:ambiguous, fun_match ++ mac_match} end end end @@ -4337,7 +4434,7 @@ defmodule ElixirSense.Core.Compiler do defp is_import(meta, arity) do with {:ok, imports = [_ | _]} <- Keyword.fetch(meta, :imports), {:ok, _} <- Keyword.fetch(meta, :context), - {:ok, receiver} <- Keyword.fetch(imports, arity) do + {_arity, receiver} <- :lists.keyfind(arity, 1, imports) do {:import, receiver} else _ -> false @@ -4397,16 +4494,19 @@ defmodule ElixirSense.Core.Compiler do end defp wrap_in_fake_map(right) do - map_args = case right do - list when is_list(list) -> - if Keyword.keyword?(list) do - list - else - [__fake_key__: list] - end - _ -> - [__fake_key__: right] - end + map_args = + case right do + list when is_list(list) -> + if Keyword.keyword?(list) do + list + else + [__fake_key__: list] + end + + _ -> + [__fake_key__: right] + end + {:%{}, [], map_args} end @@ -4539,19 +4639,21 @@ defmodule ElixirSense.Core.Compiler do def type_to_signature({:"::", _, [{name, _, args}, _]}) when is_atom(name) and name != :"::", - do: {name, args} + do: {name, args} def type_to_signature(_), do: :error def expand_spec(ast, state, env) do # TODO not sure this is correct. Are module vars accessible? - state = state - |> new_func_vars_scope + state = + state + |> new_func_vars_scope {ast, state, env} = do_expand_spec(ast, state, env) - state = state - |> remove_func_vars_scope + state = + state + |> remove_func_vars_scope {ast, state, env} end @@ -4560,14 +4662,22 @@ defmodule ElixirSense.Core.Compiler do {spec, guard, state, env} = do_expand_spec(spec, guard, meta, state, env) {{:when, meta, [spec, guard]}, state, env} end + defp do_expand_spec(spec, state, env) do {spec, _guard, state, env} = do_expand_spec(spec, [], [], state, env) {spec, state, env} end - defp do_expand_spec({:"::", meta, [{name, name_meta, args}, return]}, guard, guard_meta, state, env) - when is_atom(name) and name != :"::" do - args = if is_atom(args) do + defp do_expand_spec( + {:"::", meta, [{name, name_meta, args}, return]}, + guard, + guard_meta, + state, + env + ) + when is_atom(name) and name != :"::" do + args = + if is_atom(args) do [] else args @@ -4576,27 +4686,33 @@ defmodule ElixirSense.Core.Compiler do guard = if Keyword.keyword?(guard), do: guard, else: [] - state = Enum.reduce(guard, state, fn {name, _val}, state -> - # guard is a keyword list so we don't have exact meta on keys - add_var_write(state, {name, guard_meta, nil}) - end) + state = + Enum.reduce(guard, state, fn {name, _val}, state -> + # guard is a keyword list so we don't have exact meta on keys + add_var_write(state, {name, guard_meta, nil}) + end) + + {args_reverse, state, env} = + Enum.reduce(args, {[], state, env}, fn arg, {acc, state, env} -> + {arg, state, env} = expand_typespec(arg, state, env) + {[arg | acc], state, env} + end) - {args_reverse, state, env} = Enum.reduce(args, {[], state, env}, fn arg, {acc, state, env} -> - {arg, state, env} = expand_typespec(arg, state, env) - {[arg | acc], state, env} - end) args = Enum.reverse(args_reverse) {return, state, env} = expand_typespec(return, state, env) - {guard_reverse, state, env} = Enum.reduce(guard, {[], state, env}, fn - {_name, {:var, _, context}} = pair, {acc, state, env} when is_atom(context) -> - # special type var - {[pair | acc], state, env} - {name, type}, {acc, state, env} -> - {type, state, env} = expand_typespec(type, state, env) - {[{name, type} | acc], state, env} - end) + {guard_reverse, state, env} = + Enum.reduce(guard, {[], state, env}, fn + {_name, {:var, _, context}} = pair, {acc, state, env} when is_atom(context) -> + # special type var + {[pair | acc], state, env} + + {name, type}, {acc, state, env} -> + {type, state, env} = expand_typespec(type, state, env) + {[{name, type} | acc], state, env} + end) + guard = Enum.reverse(guard_reverse) {{:"::", meta, [{name, name_meta, args}, return]}, guard, state, env} @@ -4612,42 +4728,46 @@ defmodule ElixirSense.Core.Compiler do Enum.map(args, fn {:"::", meta, [left, right]} -> {:"::", meta, [remove_default(left), remove_default(right)]} - + other -> remove_default(other) end) end - + defp remove_default({:\\, _, [left, _]}), do: left defp remove_default(other), do: other - def expand_type(ast, state, env) do - state = state - |> new_func_vars_scope + state = + state + |> new_func_vars_scope {ast, state, env} = do_expand_type(ast, state, env) - state = state - |> remove_func_vars_scope + state = + state + |> remove_func_vars_scope {ast, state, env} end defp do_expand_type({:"::", meta, [{name, name_meta, args}, definition]}, state, env) do - args = if is_atom(args) do - [] - else - args - end + args = + if is_atom(args) do + [] + else + args + end - state = Enum.reduce(args, state, fn - {name, meta, context}, state when is_atom(name) and is_atom(context) and name != :_ -> - add_var_write(state, {name, meta, context}) - _, state -> - # silently skip invalid typespec params - state - end) + state = + Enum.reduce(args, state, fn + {name, meta, context}, state when is_atom(name) and is_atom(context) and name != :_ -> + add_var_write(state, {name, meta, context}) + + _, state -> + # silently skip invalid typespec params + state + end) {definition, state, env} = expand_typespec(definition, state, env) {{:"::", meta, [{name, name_meta, args}, definition]}, state, env} @@ -4660,7 +4780,19 @@ defmodule ElixirSense.Core.Compiler do end @special_forms [ - :|, :<<>>, :%{}, :%, :.., :->, :"::", :+, :-, :., :{}, :__block__, :... + :|, + :<<>>, + :%{}, + :%, + :.., + :->, + :"::", + :+, + :-, + :., + :{}, + :__block__, + :... ] defp expand_typespec(ast, state, env) do @@ -4672,64 +4804,83 @@ defmodule ElixirSense.Core.Compiler do # TODO expand struct module # {:%, _, [name, {:%{}, meta, fields}]} {ast, {state, env}} = - Macro.traverse(ast, {state, env}, fn - {:__aliases__, _meta, list} = node, {state, env} when is_list(list) -> - {node, state, env} = ElixirExpand.expand(node, state, env) - {node, {state, env}} - - {:__MODULE__, _meta, ctx} = node, {state, env} when is_atom(ctx) -> - {node, state, env} = ElixirExpand.expand(node, state, env) - {node, {state, env}} - - {:"::", meta, [{var_name, var_meta, context}, expr]}, {state, env} when is_atom(var_name) and is_atom(context) -> - # mark as annotation - {{:"::", meta, [{var_name, [{:annotation, true} | var_meta], context}, expr]}, {state, env}} - - {name, meta, args}, {state, env} when is_atom(name) and is_atom(args) and name not in @special_forms and hd(meta) != {:annotation, true} -> - [vars_from_scope | _other_vars] = state.vars_info - ast = case Elixir.Map.get(vars_from_scope, {name, nil}) do - nil -> - # add parens to no parens local call - {name, meta, []} - _ -> - {name, meta, args} - end + Macro.traverse( + ast, + {state, env}, + fn + {:__aliases__, _meta, list} = node, {state, env} when is_list(list) -> + {node, state, env} = ElixirExpand.expand(node, state, env) + {node, {state, env}} + + {:__MODULE__, _meta, ctx} = node, {state, env} when is_atom(ctx) -> + {node, state, env} = ElixirExpand.expand(node, state, env) + {node, {state, env}} + + {:"::", meta, [{var_name, var_meta, context}, expr]}, {state, env} + when is_atom(var_name) and is_atom(context) -> + # mark as annotation + {{:"::", meta, [{var_name, [{:annotation, true} | var_meta], context}, expr]}, + {state, env}} + + {name, meta, args}, {state, env} + when is_atom(name) and is_atom(args) and name not in @special_forms and + hd(meta) != {:annotation, true} -> + [vars_from_scope | _other_vars] = state.vars_info + + ast = + case Elixir.Map.get(vars_from_scope, {name, nil}) do + nil -> + # add parens to no parens local call + {name, meta, []} + + _ -> + {name, meta, args} + end + + {ast, {state, env}} + + other, acc -> + {other, acc} + end, + fn + {{:., dot_meta, [remote, name]}, meta, args}, {state, env} when is_atom(remote) -> + line = Keyword.get(meta, :line, 0) + column = Keyword.get(meta, :column, nil) - {ast, {state, env}} + args = + if is_atom(args) do + [] + else + args + end - other, acc -> - {other, acc} - end, fn - {{:., dot_meta, [remote, name]}, meta, args}, {state, env} when is_atom(remote) -> - line = Keyword.get(meta, :line, 0) - column = Keyword.get(meta, :column, nil) - args = if is_atom(args) do - [] - else - args - end + state = add_call_to_line(state, {remote, name, length(args)}, {line, column}) - state = add_call_to_line(state, {remote, name, length(args)}, {line, column}) + {{{:., dot_meta, [remote, name]}, meta, args}, {state, env}} - {{{:., dot_meta, [remote, name]}, meta, args}, {state, env}} + {name, meta, args}, {state, env} + when is_atom(name) and is_list(args) and name not in @special_forms -> + line = Keyword.get(meta, :line, 0) + column = Keyword.get(meta, :column, nil) - {name, meta, args}, {state, env} when is_atom(name) and is_list(args) and name not in @special_forms -> - line = Keyword.get(meta, :line, 0) - column = Keyword.get(meta, :column, nil) + state = add_call_to_line(state, {nil, name, length(args)}, {line, column}) - state = add_call_to_line(state, {nil, name, length(args)}, {line, column}) + {{name, meta, args}, {state, env}} - {{name, meta, args}, {state, env}} - {name, meta, context} = var, {state, env} when is_atom(name) and is_atom(context) and hd(meta) != {:annotation, true} -> - state = add_var_read(state, var) - {var, {state, env}} - other, acc -> - {other, acc} - end) + {name, meta, context} = var, {state, env} + when is_atom(name) and is_atom(context) and hd(meta) != {:annotation, true} -> + state = add_var_read(state, var) + {var, {state, env}} + + other, acc -> + {other, acc} + end + ) {ast, state, env} end - # TODO: Remove char_list type by v2.0 + + # TODO: Remove char_list type by v2.0 def built_in_type?(:char_list, 0), do: true def built_in_type?(:charlist, 0), do: true def built_in_type?(:as_boolean, 1), do: true diff --git a/lib/elixir_sense/core/guard.ex b/lib/elixir_sense/core/guard.ex index 9c50431c..4c12db7e 100644 --- a/lib/elixir_sense/core/guard.ex +++ b/lib/elixir_sense/core/guard.ex @@ -12,6 +12,7 @@ defmodule ElixirSense.Core.Guard do for expr <- list, reduce: %{} do acc -> right = type_information_from_guards(expr) + Map.merge(acc, right, fn _k, v1, v2 -> case {v1, v2} do {{:union, types_1}, {:union, types_2}} -> {:union, types_1 ++ types_2} @@ -57,16 +58,18 @@ defmodule ElixirSense.Core.Guard do case Keyword.fetch(meta, :version) do {:ok, version} -> {node, Map.put(acc, {var, version}, :boolean)} + _ -> {node, acc} end - {{:., _dot_meta, [:erlang, fun]}, _call_meta, params}, acc when is_atom(fun) and is_list(params) -> + {{:., _dot_meta, [:erlang, fun]}, _call_meta, params}, acc + when is_atom(fun) and is_list(params) -> with {type, binding} <- guard_predicate_type(fun, params), - {var, meta, context} when is_atom(var) and is_atom(context) <- binding, - {:ok, version} <- Keyword.fetch(meta, :version) do - # If we found the predicate type, we can prematurely exit traversing the subtree - {[], Map.put(acc, {var, version}, type)} + {var, meta, context} when is_atom(var) and is_atom(context) <- binding, + {:ok, version} <- Keyword.fetch(meta, :version) do + # If we found the predicate type, we can prematurely exit traversing the subtree + {[], Map.put(acc, {var, version}, type)} else _ -> # traverse params @@ -86,7 +89,18 @@ defmodule ElixirSense.Core.Guard do # TODO div and rem only work on first arg defp guard_predicate_type(p, [first | _]) - when p in [:is_number, :is_float, :is_integer, :round, :trunc, :div, :rem, :abs, :ceil, :floor], + when p in [ + :is_number, + :is_float, + :is_integer, + :round, + :trunc, + :div, + :rem, + :abs, + :ceil, + :floor + ], do: {:number, first} defp guard_predicate_type(p, [first | _]) when p in [:is_binary, :binary_part], @@ -118,8 +132,9 @@ defmodule ElixirSense.Core.Guard do {rhs_type, first} end + defp guard_predicate_type(p, [lhs, {{:., _, [:erlang, guard]}, _, _guard_params} = call]) - when p in [:==, :===, :>=, :>, :<=, :<] and guard in [:hd, :tl] do + when p in [:==, :===, :>=, :>, :<=, :<] and guard in [:hd, :tl] do guard_predicate_type(p, [call, lhs]) end @@ -150,23 +165,27 @@ defmodule ElixirSense.Core.Guard do defp guard_predicate_type(p, [{{:., _, [:erlang, :map_get]}, _, [key, second | _]}, value]) when p in [:==, :===] do - type = cond do - key == :__struct__ and is_atom(value) -> - {:struct, [], {:atom, value}, nil} - key == :__struct__ -> - {:struct, [], nil, nil} - is_atom(key) or is_binary(key) -> - # TODO other types of keys? - rhs_type = - cond do - is_number(value) -> {:number, value} - is_binary(value) -> :binary - is_bitstring(value) -> :bitstring - is_atom(value) -> {:atom, value} - is_boolean(value) -> :boolean - true -> nil - end - {:map, [{key, rhs_type}], nil} + type = + cond do + key == :__struct__ and is_atom(value) -> + {:struct, [], {:atom, value}, nil} + + key == :__struct__ -> + {:struct, [], nil, nil} + + is_atom(key) or is_binary(key) -> + # TODO other types of keys? + rhs_type = + cond do + is_number(value) -> {:number, value} + is_binary(value) -> :binary + is_bitstring(value) -> :bitstring + is_atom(value) -> {:atom, value} + is_boolean(value) -> :boolean + true -> nil + end + + {:map, [{key, rhs_type}], nil} end {type, second} @@ -185,12 +204,12 @@ defmodule ElixirSense.Core.Guard do defp guard_predicate_type(:is_map_key, [key, var | _]) do # TODO other types of keys? type = - case key do - :__struct__ -> {:struct, [], nil, nil} - key when is_atom(key) -> {:map, [{key, nil}], nil} - key when is_binary(key) -> {:map, [{key, nil}], nil} - _ -> {:map, [], nil} - end + case key do + :__struct__ -> {:struct, [], nil, nil} + key when is_atom(key) -> {:map, [{key, nil}], nil} + key when is_binary(key) -> {:map, [{key, nil}], nil} + _ -> {:map, [], nil} + end {type, var} end diff --git a/lib/elixir_sense/core/metadata.ex b/lib/elixir_sense/core/metadata.ex index 89440d29..b3f44b6b 100644 --- a/lib/elixir_sense/core/metadata.ex +++ b/lib/elixir_sense/core/metadata.ex @@ -59,31 +59,39 @@ defmodule ElixirSense.Core.Metadata do } end - def get_cursor_env(%__MODULE__{} = metadata, {{begin_line, begin_column}, {end_line, end_column}}) do + def get_cursor_env( + %__MODULE__{} = metadata, + {{begin_line, begin_column}, {end_line, end_column}} + ) do prefix = ElixirSense.Core.Source.text_before(metadata.source, begin_line, begin_column) suffix = ElixirSense.Core.Source.text_after(metadata.source, end_line, end_column) source_with_cursor = prefix <> "(__cursor__())" <> suffix - {meta, cursor_env} = case Code.string_to_quoted(source_with_cursor, [columns: true, token_metadata: true]) do - {:ok, ast} -> - ElixirSense.Core.MetadataBuilder.build(ast).cursor_env || {[], nil} - - _ -> - {[], nil} - end - - {_meta, cursor_env} = if cursor_env != nil do - {meta, cursor_env} - else - case NormalizedCode.Fragment.container_cursor_to_quoted(prefix, [columns: true, token_metadata: true]) do + {meta, cursor_env} = + case Code.string_to_quoted(source_with_cursor, columns: true, token_metadata: true) do {:ok, ast} -> ElixirSense.Core.MetadataBuilder.build(ast).cursor_env || {[], nil} - + _ -> {[], nil} end - end + + {_meta, cursor_env} = + if cursor_env != nil do + {meta, cursor_env} + else + case NormalizedCode.Fragment.container_cursor_to_quoted(prefix, + columns: true, + token_metadata: true + ) do + {:ok, ast} -> + ElixirSense.Core.MetadataBuilder.build(ast).cursor_env || {[], nil} + + _ -> + {[], nil} + end + end if cursor_env != nil do cursor_env @@ -95,13 +103,17 @@ defmodule ElixirSense.Core.Metadata do def get_cursor_env(%__MODULE__{} = metadata, {line, column}) do prefix = ElixirSense.Core.Source.text_before(metadata.source, line, column) - {_meta, cursor_env} = case NormalizedCode.Fragment.container_cursor_to_quoted(prefix, [columns: true, token_metadata: true]) do - {:ok, ast} -> - ElixirSense.Core.MetadataBuilder.build(ast).cursor_env || {[], nil} + {_meta, cursor_env} = + case NormalizedCode.Fragment.container_cursor_to_quoted(prefix, + columns: true, + token_metadata: true + ) do + {:ok, ast} -> + ElixirSense.Core.MetadataBuilder.build(ast).cursor_env || {[], nil} - _ -> - {[], nil} - end + _ -> + {[], nil} + end if cursor_env != nil do cursor_env @@ -241,7 +253,7 @@ defmodule ElixirSense.Core.Metadata do predicate \\ fn _ -> true end ) do scope_vars = vars_info_per_scope_id[env.scope_id] || %{} - env_vars_keys = env.vars |> Enum.map(& {&1.name, &1.version}) + env_vars_keys = env.vars |> Enum.map(&{&1.name, &1.version}) scope_vars_missing_in_env = scope_vars @@ -251,10 +263,11 @@ defmodule ElixirSense.Core.Metadata do end) |> Enum.map(fn {_, value} -> value end) - env_vars = for var <- env.vars do - key = {var.name, var.version} - Map.fetch!(scope_vars, key) - end + env_vars = + for var <- env.vars do + key = {var.name, var.version} + Map.fetch!(scope_vars, key) + end %{env | vars: env_vars ++ scope_vars_missing_in_env} end diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index 5da47337..fb4ee224 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -27,63 +27,14 @@ defmodule ElixirSense.Core.MetadataBuilder do """ @spec build(Macro.t()) :: State.t() def build(ast) do - if Version.match?(System.version(), ">= 1.17.0-dev") do - {_ast, state, _env} = Compiler.expand(ast, %State{}, Compiler.env()) + {_ast, state, _env} = Compiler.expand(ast, %State{}, Compiler.env()) - state - |> remove_attributes_scope - |> remove_lexical_scope - |> remove_vars_scope - |> remove_module - |> remove_protocol_implementation - else - # dbg(ast) - {_ast, [state]} = - Macro.traverse(ast, [%State{}], &safe_call_pre/2, &safe_call_post/2) - - try do - state - |> remove_attributes_scope - |> remove_lexical_scope - |> remove_vars_scope - |> remove_module - |> remove_protocol_implementation - rescue - exception -> - warn( - Exception.format( - :error, - "#{inspect(exception.__struct__)} during metadata build scope closing:\n" <> - "#{Exception.message(exception)}\n" <> - "ast node: #{inspect(ast, limit: :infinity)}", - __STACKTRACE__ - ) - ) - - vars_info_per_scope_id = - try do - update_vars_info_per_scope_id(state) - rescue - _ -> - state.vars_info_per_scope_id - end - - %{ - state - | attributes: [], - scope_attributes: [], - aliases: [], - requires: [], - scope_ids: [], - vars: [], - scope_vars: [], - vars_info_per_scope_id: vars_info_per_scope_id, - module: [], - scopes: [], - protocols: [] - } - end - end + state + |> remove_attributes_scope + |> remove_lexical_scope + |> remove_vars_scope + |> remove_module + |> remove_protocol_implementation end defp safe_call_pre(ast, [state = %State{} | _] = states) do @@ -865,8 +816,6 @@ defmodule ElixirSense.Core.MetadataBuilder do # [%VarInfo{name: var, positions: [{line, column}], type: match_context, is_definition: true}] # end - - def infer_type_from_guards(guard_ast, vars, _state) do type_info = Guard.type_information_from_guards(guard_ast) diff --git a/lib/elixir_sense/core/normalized/macro/env.ex b/lib/elixir_sense/core/normalized/macro/env.ex new file mode 100644 index 00000000..42b4b813 --- /dev/null +++ b/lib/elixir_sense/core/normalized/macro/env.ex @@ -0,0 +1,666 @@ +defmodule ElixirSense.Core.Normalized.Macro.Env do + if Version.match?(System.version(), ">= 1.17.0-dev") do + defdelegate expand_import(env, meta, fun, arity, opts), to: Macro.Env + defdelegate expand_require(env, meta, module, fun, arity, opts), to: Macro.Env + defdelegate expand_alias(env, meta, list, opts), to: Macro.Env + defdelegate define_alias(env, meta, arg, opts), to: Macro.Env + else + def fake_expand_callback(_meta, _args) do + {:__block__, [], []} + end + + def expand_import(env, meta, name, arity, opts \\ []) + when is_list(meta) and is_atom(name) and is_integer(arity) and is_list(opts) do + case Macro.special_form?(name, arity) do + true -> + {:error, :not_found} + + false -> + allow_locals = Keyword.get(opts, :allow_locals, true) + trace = Keyword.get(opts, :trace, true) + module = env.module + + extra = + case allow_locals and function_exported?(module, :__info__, 1) do + true -> [{module, module.__info__(:macros)}] + false -> [] + end + + case __MODULE__.Dispatch.expand_import( + meta, + name, + arity, + env, + extra, + allow_locals, + trace + ) do + {:macro, receiver, expander} -> + {:macro, receiver, wrap_expansion(receiver, expander, meta, name, arity, env, opts)} + + {:function, receiver, name} -> + {:function, receiver, name} + + error -> + {:error, error} + end + end + end + + def expand_require(env, meta, module, name, arity, opts \\ []) + when is_list(meta) and is_atom(module) and is_atom(name) and is_integer(arity) and + is_list(opts) do + trace = Keyword.get(opts, :trace, true) + + case __MODULE__.Dispatch.expand_require(meta, module, name, arity, env, trace) do + {:macro, receiver, expander} -> + {:macro, receiver, wrap_expansion(receiver, expander, meta, name, arity, env, opts)} + + :error -> + :error + end + end + + defp wrap_expansion(receiver, expander, meta, name, arity, env, opts) do + fn expansion_meta, args -> + if Keyword.get(opts, :check_deprecations, true) do + :elixir_dispatch.check_deprecated(:macro, meta, receiver, name, arity, env) + end + + quoted = expander.(args, env) + next = :elixir_module.next_counter(env.module) + :elixir_quote.linify_with_context_counter(expansion_meta, {receiver, next}, quoted) + end + end + + def expand_alias(env, meta, list, opts \\ []) + when is_list(meta) and is_list(list) and is_list(opts) do + trace = Keyword.get(opts, :trace, true) + + case __MODULE__.Aliases.expand(meta, list, env, trace) do + atom when is_atom(atom) -> {:alias, atom} + [_ | _] -> :error + end + end + + def define_require(env, meta, module, opts \\ []) + when is_list(meta) and is_atom(module) and is_list(opts) do + {trace, opts} = Keyword.pop(opts, :trace, true) + env = __MODULE__.Aliases.require(meta, module, opts, env, trace) + result = __MODULE__.Aliases.alias(meta, module, false, opts, env, trace) + maybe_define_error(result, :elixir_aliases) + end + + def define_alias(env, meta, module, opts \\ []) + when is_list(meta) and is_atom(module) and is_list(opts) do + {trace, opts} = Keyword.pop(opts, :trace, true) + result = __MODULE__.Aliases.alias(meta, module, true, opts, env, trace) + maybe_define_error(result, :elixir_aliases) + end + + def define_import(env, meta, module, opts \\ []) + when is_list(meta) and is_atom(module) and is_list(opts) do + {trace, opts} = Keyword.pop(opts, :trace, true) + {warnings, opts} = Keyword.pop(opts, :emit_warnings, true) + {info_callback, opts} = Keyword.pop(opts, :info_callback, &module.__info__/1) + + result = __MODULE__.Import.import(meta, module, opts, env, warnings, trace, info_callback) + maybe_define_error(result, :elixir_import) + end + + defp maybe_define_error({:ok, env}, _mod), + do: {:ok, env} + + defp maybe_define_error({:error, reason}, _mod), + do: {:error, inspect(reason)} + + defmodule Aliases do + def require(_meta, ref, _opts, e, _trace) do + %{e | requires: :ordsets.add_element(ref, e.requires)} + end + + def alias(meta, ref, include_by_default, opts, e, _trace) do + %{aliases: aliases, macro_aliases: macro_aliases} = e + + case expand_as(:lists.keyfind(:as, 1, opts), include_by_default, ref) do + {:ok, ^ref} -> + {:ok, + %{ + e + | aliases: remove_alias(ref, aliases), + macro_aliases: remove_macro_alias(meta, ref, macro_aliases) + }} + + {:ok, new} -> + {:ok, + %{ + e + | aliases: store_alias(new, ref, aliases), + macro_aliases: store_macro_alias(meta, new, ref, macro_aliases) + }} + + :none -> + {:ok, e} + + {:error, reason} -> + {:error, reason} + end + end + + defp expand_as({:as, atom}, _include_by_default, _ref) + when is_atom(atom) and not is_boolean(atom) do + case Atom.to_charlist(atom) do + ~c"Elixir." ++ ([first_letter | _] = rest) when first_letter in ?A..?Z -> + case :string.tokens(rest, ~c".") do + [_] -> + {:ok, atom} + + _ -> + {:error, {:invalid_alias_for_as, :nested_alias, atom}} + end + + _ -> + {:error, {:invalid_alias_for_as, :not_alias, atom}} + end + end + + defp expand_as({:as, other}, _include_by_default, _ref) do + {:error, {:invalid_alias_for_as, :not_alias, other}} + end + + defp expand_as(false, true, ref) do + case Atom.to_charlist(ref) do + ~c"Elixir." ++ [first_letter | _] = list when first_letter in ?A..?Z -> + last = last(Enum.reverse(list), []) + {:ok, :"Elixir.#{last}"} + + _ -> + {:error, {:invalid_alias_module, ref}} + end + end + + defp expand_as(false, false, _ref) do + :none + end + + defp last([?. | _], acc), do: acc + defp last([h | t], acc), do: last(t, [h | acc]) + defp last([], acc), do: acc + + defp store_alias(new, old, aliases) do + :lists.keystore(new, 1, aliases, {new, old}) + end + + defp store_macro_alias(meta, new, old, aliases) do + case :lists.keyfind(:counter, 1, meta) do + {:counter, counter} -> + :lists.keystore(new, 1, aliases, {new, {counter, old}}) + + false -> + aliases + end + end + + defp remove_alias(atom, aliases) do + :lists.keydelete(atom, 1, aliases) + end + + defp remove_macro_alias(meta, atom, aliases) do + case :lists.keyfind(:counter, 1, meta) do + {:counter, _counter} -> + :lists.keydelete(atom, 1, aliases) + + false -> + aliases + end + end + + def expand(_meta, [Elixir | _] = list, _e, _trace) do + list + end + + def expand(_meta, [h | _] = list, _e, _trace) when not is_atom(h) do + list + end + + def expand(meta, list, %{aliases: aliases, macro_aliases: macro_aliases} = e, trace) do + case :lists.keyfind(:alias, 1, meta) do + {:alias, false} -> + expand(meta, list, macro_aliases, e, trace) + + {:alias, atom} when is_atom(atom) -> + atom + + false -> + expand(meta, list, aliases, e, trace) + end + end + + def expand(meta, [h | t], aliases, _e, _trace) do + lookup = String.to_atom("Elixir." <> Atom.to_string(h)) + + counter = + case :lists.keyfind(:counter, 1, meta) do + {:counter, c} -> c + _ -> nil + end + + case lookup(lookup, aliases, counter) do + ^lookup -> + [h | t] + + atom -> + case t do + [] -> atom + _ -> Module.concat([atom | t]) + end + end + end + + defp lookup(else_val, list, counter) do + case :lists.keyfind(else_val, 1, list) do + {^else_val, {^counter, value}} -> value + {^else_val, value} when is_atom(value) -> value + _ -> else_val + end + end + end + + defmodule Import do + alias ElixirSense.Core.Normalized.Macro.Env.Aliases + + def import(meta, ref, opts, e, warn, trace) do + import(meta, ref, opts, e, warn, trace, &ref.__info__/1) + end + + def import(meta, ref, opts, e, warn, trace, info_callback) do + case import_only_except(meta, ref, opts, e, warn, info_callback) do + {functions, macros, _added} -> + ei = %{e | functions: functions, macros: macros} + {:ok, Aliases.require(meta, ref, opts, ei, trace)} + + {:error, reason} -> + {:error, reason} + end + end + + defp import_only_except(meta, ref, opts, e, warn, info_callback) do + maybe_only = :lists.keyfind(:only, 1, opts) + + case :lists.keyfind(:except, 1, opts) do + false -> + import_only_except(meta, ref, maybe_only, false, e, warn, info_callback) + + {:except, dup_except} when is_list(dup_except) -> + case ensure_keyword_list(dup_except) do + :ok -> + except = ensure_no_duplicates(dup_except, :except, meta, e, warn) + import_only_except(meta, ref, maybe_only, except, e, warn, info_callback) + + :error -> + {:error, {:invalid_option, :except, dup_except}} + end + + {:except, other} -> + {:error, {:invalid_option, :except, other}} + end + end + + def import_only_except(meta, ref, maybe_only, except, e, warn, info_callback) do + case maybe_only do + {:only, :functions} -> + {added1, _used1, funs} = import_functions(meta, ref, except, e, warn, info_callback) + {funs, :lists.keydelete(ref, 1, e.macros), added1} + + {:only, :macros} -> + {added2, _used2, macs} = import_macros(meta, ref, except, e, warn, info_callback) + {:lists.keydelete(ref, 1, e.functions), macs, added2} + + {:only, :sigils} -> + {added1, _used1, funs} = + import_sigil_functions(meta, ref, except, e, warn, info_callback) + + {added2, _used2, macs} = + import_sigil_macros(meta, ref, except, e, warn, info_callback) + + {funs, macs, added1 or added2} + + {:only, dup_only} when is_list(dup_only) -> + case ensure_keyword_list(dup_only) do + :ok when except == false -> + only = ensure_no_duplicates(dup_only, :only, meta, e, warn) + + {added1, used1, funs} = + import_listed_functions(meta, ref, only, e, warn, info_callback) + + {added2, used2, macs} = + import_listed_macros(meta, ref, only, e, warn, info_callback) + + # for {name, arity} <- (only -- used1) -- used2, warn, do: elixir_errors.file_warn(meta, e, __MODULE__, {:invalid_import, {ref, name, arity}}) + {funs, macs, added1 or added2} + + :ok -> + {:error, :only_and_except_given} + + :error -> + {:error, {:invalid_option, :only, dup_only}} + end + + {:only, other} -> + {:error, {:invalid_option, :only, other}} + + false -> + {added1, _used1, funs} = import_functions(meta, ref, except, e, warn, info_callback) + {added2, _used2, macs} = import_macros(meta, ref, except, e, warn, info_callback) + {funs, macs, added1 or added2} + end + end + + def import_listed_functions(meta, ref, only, e, warn, info_callback) do + new = intersection(only, get_functions(ref, info_callback)) + calculate_key(meta, ref, Map.get(e, :functions), new, e, warn) + end + + def import_listed_macros(meta, ref, only, e, warn, info_callback) do + new = intersection(only, get_macros(info_callback)) + calculate_key(meta, ref, Map.get(e, :macros), new, e, warn) + end + + def import_functions(meta, ref, except, e, warn, info_callback) do + calculate_except(meta, ref, except, Map.get(e, :functions), e, warn, fn -> + get_functions(ref, info_callback) + end) + end + + def import_macros(meta, ref, except, e, warn, info_callback) do + calculate_except(meta, ref, except, Map.get(e, :macros), e, warn, fn -> + get_macros(info_callback) + end) + end + + def import_sigil_functions(meta, ref, except, e, warn, info_callback) do + calculate_except(meta, ref, except, Map.get(e, :functions), e, warn, fn -> + filter_sigils(info_callback.(:functions)) + end) + end + + def import_sigil_macros(meta, ref, except, e, warn, info_callback) do + calculate_except(meta, ref, except, Map.get(e, :macros), e, warn, fn -> + filter_sigils(info_callback.(:macros)) + end) + end + + defp calculate_except(meta, key, false, old, e, warn, existing) do + new = remove_underscored(existing.()) + calculate_key(meta, key, old, new, e, warn) + end + + defp calculate_except(meta, key, except, old, e, warn, existing) do + new = + case :lists.keyfind(key, 1, old) do + false -> remove_underscored(existing.()) -- except + {^key, old_imports} -> old_imports -- except + end + + calculate_key(meta, key, old, new, e, warn) + end + + defp calculate_key(meta, key, old, new, e, warn) do + case :ordsets.from_list(new) do + [] -> + {false, [], :lists.keydelete(key, 1, old)} + + set -> + final_set = ensure_no_special_form_conflict(set, key, meta, e, warn) + {true, final_set, [{key, final_set} | :lists.keydelete(key, 1, old)]} + end + end + + defp get_functions(module, info_callback) do + try do + info_callback.(:functions) + catch + _, _ -> remove_internals(module.module_info(:exports)) + end + end + + defp get_macros(info_callback) do + try do + info_callback.(:macros) + catch + _, _ -> [] + end + end + + defp filter_sigils(funs) do + Enum.filter(funs, &is_sigil/1) + end + + defp is_sigil({name, 2}) do + case Atom.to_string(name) do + "sigil_" <> letters -> + case letters do + <> when l in ?a..?z -> + true + + "" -> + false + + <> when h in ?A..?Z -> + String.to_charlist(t) + |> Enum.all?(fn l -> l in ?0..?9 or l in ?A..?Z end) + + _ -> + false + end + + _ -> + false + end + end + + defp is_sigil(_) do + false + end + + defp intersection([h | t], all) do + if Enum.member?(all, h) do + [h | intersection(t, all)] + else + intersection(t, all) + end + end + + defp intersection([], _all) do + [] + end + + defp remove_underscored(list) do + Enum.filter(list, fn {name, _} -> + case Atom.to_string(name) do + "_" <> _ -> false + _ -> true + end + end) + end + + defp remove_internals(set) do + set -- [{:behaviour_info, 1}, {:module_info, 1}, {:module_info, 0}] + end + + defp ensure_keyword_list([]) do + :ok + end + + defp ensure_keyword_list([{key, value} | rest]) when is_atom(key) and is_integer(value) do + ensure_keyword_list(rest) + end + + defp ensure_keyword_list(_other) do + :error + end + + defp ensure_no_special_form_conflict(set, _key, _meta, _e, _warn) do + Enum.filter(set, fn {name, arity} -> + if Macro.special_form?(name, arity) do + false + else + true + end + end) + end + + defp ensure_no_duplicates(option, _kind, _meta, _e, _warn) do + Enum.reduce(option, [], fn {name, arity}, acc -> + if Enum.member?(acc, {name, arity}) do + acc + else + [{name, arity} | acc] + end + end) + end + end + + defmodule Dispatch do + def expand_import(meta, name, arity, e, extra, allow_locals, trace) do + tuple = {name, arity} + module = e.module + dispatch = find_import_by_name_arity(meta, tuple, extra, e) + + case dispatch do + {:ambiguous, ambiguous} -> + {:ambiguous, ambiguous} + + {:import, _} -> + do_expand_import(dispatch, meta, name, arity, module, e, trace) + + _ -> + local = + allow_locals and + :elixir_def.local_for(meta, name, arity, [:defmacro, :defmacrop], e) + + case dispatch do + {_, receiver} when local != false and receiver != module -> + {:conflict, receiver} + + _ when local == false -> + do_expand_import(dispatch, meta, name, arity, module, e, trace) + + _ -> + {:macro, module, expander_macro_fun(meta, local, module, name, e)} + end + end + end + + defp do_expand_import(result, meta, name, arity, module, e, trace) do + case result do + {:function, receiver} -> + {:function, receiver, name} + + {:macro, receiver} -> + {:macro, receiver, expander_macro_named(meta, receiver, name, arity, e)} + + {:import, receiver} -> + case expand_require(true, meta, receiver, name, arity, e, trace) do + {:macro, _, _} = response -> response + :error -> {:function, receiver, name} + end + + false when module == Kernel -> + case :elixir_rewrite.inline(module, name, arity) do + {ar, an} -> {:function, ar, an} + false -> :not_found + end + + false -> + :not_found + end + end + + def expand_require(meta, receiver, name, arity, e, trace) do + required = + receiver == e.module or + :lists.keyfind(:required, 1, meta) == {:required, true} or + receiver in e.requires + + expand_require(required, meta, receiver, name, arity, e, trace) + end + + defp expand_require(required, meta, receiver, name, arity, e, trace) do + if is_macro(name, arity, receiver, required) do + {:macro, receiver, expander_macro_named(meta, receiver, name, arity, e)} + else + :error + end + end + + defp expander_macro_fun(meta, fun, receiver, name, e) do + fn args, caller -> expand_macro_fun(meta, fun, receiver, name, args, caller, e) end + end + + defp expander_macro_named(meta, receiver, name, arity, e) do + proper_name = :"MACRO-#{name}" + proper_arity = arity + 1 + fun = Function.capture(receiver, proper_name, proper_arity) + fn args, caller -> expand_macro_fun(meta, fun, receiver, name, args, caller, e) end + end + + defp expand_macro_fun(meta, fun, receiver, name, args, caller, e) do + apply(fun, [caller | args]) + end + + defp is_macro(_name, _arity, _module, false), do: false + + defp is_macro(name, arity, receiver, true) do + try do + macros = receiver.__info__(:macros) + :lists.member({name, arity}, macros) + rescue + _ -> false + end + end + + defp find_import_by_name_arity(meta, {_name, arity} = tuple, extra, e) do + case is_import(meta, arity) do + {:import, _} = import_res -> + import_res + + false -> + funs = e.functions + macs = extra ++ e.macros + fun_match = find_import_by_name_arity(tuple, funs) + mac_match = find_import_by_name_arity(tuple, macs) + + case {fun_match, mac_match} do + {[], [receiver]} -> + {:macro, receiver} + + {[receiver], []} -> + {:function, receiver} + + {[], []} -> + false + + _ -> + {:ambiguous, fun_match ++ mac_match} + end + end + end + + defp find_import_by_name_arity(tuple, list) do + import :ordsets, only: [is_element: 2] + for {receiver, set} <- list, is_element(tuple, set), do: receiver + end + + defp is_import(meta, arity) do + with {:ok, imports = [_ | _]} <- Keyword.fetch(meta, :imports), + {:ok, _} <- Keyword.fetch(meta, :context), + {_arity, receiver} <- :lists.keyfind(arity, 1, imports) do + {:import, receiver} + else + _ -> false + end + end + end + end +end diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 9f5323d0..f0222112 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -27,7 +27,9 @@ defmodule ElixirSense.Core.State do optional({module, atom, nil | non_neg_integer}) => ElixirSense.Core.State.SpecInfo.t() } @type vars_info_per_scope_id_t :: %{ - optional(scope_id_t) => [%{optional({atom(), non_neg_integer()}) => ElixirSense.Core.State.VerInfo.t()}] + optional(scope_id_t) => [ + %{optional({atom(), non_neg_integer()}) => ElixirSense.Core.State.VerInfo.t()} + ] } @type structs_t :: %{optional(module) => ElixirSense.Core.State.StructInfo.t()} @type protocol_t :: {module, nonempty_list(module)} @@ -340,9 +342,11 @@ defmodule ElixirSense.Core.State do # vars_info has both read and write vars # filter to return only read [current_vars_info | _] = state.vars_info - vars = for {{name, context}, version} <- versioned_vars, context == nil do - Map.fetch!(current_vars_info, {name, version}) - end + + vars = + for {{name, context}, version} <- versioned_vars, context == nil do + Map.fetch!(current_vars_info, {name, version}) + end current_protocol = case state.protocol do @@ -855,9 +859,10 @@ defmodule ElixirSense.Core.State do [current_scope_vars | _other_scope_vars] = state.vars_info for {scope_id, vars} <- state.vars_info_per_scope_id, into: %{} do - updated_vars = for {key, var} <- vars, into: %{} do - {key, Map.get(current_scope_vars, key, var)} - end + updated_vars = + for {key, var} <- vars, into: %{} do + {key, Map.get(current_scope_vars, key, var)} + end {scope_id, updated_vars} end @@ -1786,12 +1791,13 @@ defmodule ElixirSense.Core.State do %ModFunInfo{positions: positions, params: params} = state.mods_funs_to_positions[key] - args = for param_variant <- params do - case tl(param_variant) do - [] -> ["t()"] - other -> ["t()" | Enum.map(other, fn _ -> "term()" end)] + args = + for param_variant <- params do + case tl(param_variant) do + [] -> ["t()"] + other -> ["t()" | Enum.map(other, fn _ -> "term()" end)] + end end - end specs = for arg <- args do @@ -1845,12 +1851,17 @@ defmodule ElixirSense.Core.State do end def merge_inferred_types(state, []), do: state + def merge_inferred_types(state, inferred_types) do [h | t] = state.vars_info - h = for {key, type} <- inferred_types, reduce: h do - acc -> Map.update!(acc, key, fn %VarInfo{type: old} = v -> %{v | type: merge_type(old, type)} end) - end + h = + for {key, type} <- inferred_types, reduce: h do + acc -> + Map.update!(acc, key, fn %VarInfo{type: old} = v -> + %{v | type: merge_type(old, type)} + end) + end %{state | vars_info: [h | t]} end diff --git a/lib/elixir_sense/core/type_inference.ex b/lib/elixir_sense/core/type_inference.ex index 5deea6f4..548a58aa 100644 --- a/lib/elixir_sense/core/type_inference.ex +++ b/lib/elixir_sense/core/type_inference.ex @@ -76,6 +76,7 @@ defmodule ElixirSense.Core.TypeInference do # map def get_binding_type({:%{}, _meta, fields}) when is_list(fields) do field_type = get_fields_binding_type(fields) + case field_type |> Keyword.fetch(:__struct__) do {:ok, type} -> {:struct, [], type, nil} _ -> {:map, field_type, nil} @@ -222,13 +223,14 @@ defmodule ElixirSense.Core.TypeInference do {vars, match_context} ) when is_atom(var) and is_atom(context) and - var not in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__, :_] do - case Keyword.fetch(meta, :version) do - {:ok, version} -> - {nil, {[{{var, version}, match_context} | vars], nil}} - _ -> - {ast, {vars, match_context}} - end + var not in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__, :_] do + case Keyword.fetch(meta, :version) do + {:ok, version} -> + {nil, {[{{var, version}, match_context} | vars], nil}} + + _ -> + {ast, {vars, match_context}} + end end defp match_var( @@ -240,9 +242,10 @@ defmodule ElixirSense.Core.TypeInference do case Keyword.fetch(meta, :version) do {:ok, version} -> {nil, {[{{var, version}, match_context} | vars], nil}} + _ -> {ast, {vars, match_context}} - end + end end # drop right side of guard expression as guards cannot define vars @@ -278,6 +281,7 @@ defmodule ElixirSense.Core.TypeInference do {:|, _, [_left, _right]} -> # map update is forbidden in match, we're in invalid code [] + {key, value_ast} -> key_type = get_binding_type(key) diff --git a/test/elixir_sense/core/binding_test.exs b/test/elixir_sense/core/binding_test.exs index 1a31987e..46bcec6f 100644 --- a/test/elixir_sense/core/binding_test.exs +++ b/test/elixir_sense/core/binding_test.exs @@ -111,96 +111,96 @@ defmodule ElixirSense.Core.BindingTest do Binding.expand( @env, { - :intersection, - [ - {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, - {:struct, [], nil, nil} - ] - } - ) - - assert { - :struct, - [ - {:__struct__, {:atom, URI}}, - {:port, nil}, - {:scheme, nil}, - {:path, nil}, - {:host, nil}, - {:userinfo, nil}, - {:fragment, nil}, - {:query, nil}, - {:authority, nil} - ], - {:atom, URI}, - nil - } == + :intersection, + [ + {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, + {:struct, [], nil, nil} + ] + } + ) + + assert { + :struct, + [ + {:__struct__, {:atom, URI}}, + {:port, nil}, + {:scheme, nil}, + {:path, nil}, + {:host, nil}, + {:userinfo, nil}, + {:fragment, nil}, + {:query, nil}, + {:authority, nil} + ], + {:atom, URI}, + nil + } == Binding.expand( @env, { - :intersection, - [ - {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, - {:struct, [], {:atom, URI}, nil} - ] - } + :intersection, + [ + {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, + {:struct, [], {:atom, URI}, nil} + ] + } ) - assert {:struct, [__struct__: nil, __exception__: {:atom, true}], nil, nil} == + assert {:struct, [__struct__: nil, __exception__: {:atom, true}], nil, nil} == Binding.expand( @env, { - :intersection, - [ - { - :intersection, - [ - { - :intersection, - [ - {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, - {:struct, [], nil, nil} - ] - }, - {:map, [{:__exception__, nil}], nil} - ] - }, - {:map, [{:__exception__, {:atom, true}}], nil} - ] - } - ) - - assert { - :struct, - [ - {:__struct__, {:atom, ArgumentError}}, - {:message, nil}, - {:__exception__, {:atom, true}} - ], - {:atom, ArgumentError}, - nil - } == + :intersection, + [ + { + :intersection, + [ + { + :intersection, + [ + {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, + {:struct, [], nil, nil} + ] + }, + {:map, [{:__exception__, nil}], nil} + ] + }, + {:map, [{:__exception__, {:atom, true}}], nil} + ] + } + ) + + assert { + :struct, + [ + {:__struct__, {:atom, ArgumentError}}, + {:message, nil}, + {:__exception__, {:atom, true}} + ], + {:atom, ArgumentError}, + nil + } == Binding.expand( @env, { - :intersection, - [ - { - :intersection, - [ - { - :intersection, - [ - {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, - {:struct, [], {:atom, ArgumentError}, nil} - ] - }, - {:map, [{:__exception__, nil}], nil} - ] - }, - {:map, [{:__exception__, {:atom, true}}], nil} - ] - } + :intersection, + [ + { + :intersection, + [ + { + :intersection, + [ + {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, + {:struct, [], {:atom, ArgumentError}, nil} + ] + }, + {:map, [{:__exception__, nil}], nil} + ] + }, + {:map, [{:__exception__, {:atom, true}}], nil} + ] + } ) end diff --git a/test/elixir_sense/core/compiler_test.exs b/test/elixir_sense/core/compiler_test.exs index ae23ba8e..f78b4b57 100644 --- a/test/elixir_sense/core/compiler_test.exs +++ b/test/elixir_sense/core/compiler_test.exs @@ -1,4 +1,4 @@ -if Version.match?(System.version(), ">= 1.17.0-dev") do +if true or Version.match?(System.version(), ">= 1.17.0-dev") do defmodule ElixirSense.Core.CompilerTest do use ExUnit.Case, async: true alias ElixirSense.Core.Compiler @@ -11,33 +11,62 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do defp to_quoted!(string, false), do: Code.string_to_quoted!(string, columns: true, token_metadata: true) - Record.defrecordp(:elixir_ex, - caller: false, - prematch: :raise, - stacktrace: false, - unused: {%{}, 0}, - runtime_modules: [], - vars: {%{}, false} - ) - - defp elixir_ex_to_map( - elixir_ex( - caller: caller, - prematch: prematch, - stacktrace: stacktrace, - unused: {_, unused}, - runtime_modules: runtime_modules, - vars: vars - ) - ) do - %{ - caller: caller, - prematch: prematch, - stacktrace: stacktrace, - unused: unused, - runtime_modules: runtime_modules, - vars: vars - } + if Version.match?(System.version(), ">= 1.17.0-dev") do + Record.defrecordp(:elixir_ex, + caller: false, + prematch: :raise, + stacktrace: false, + unused: {%{}, 0}, + runtime_modules: [], + vars: {%{}, false} + ) + + defp elixir_ex_to_map( + elixir_ex( + caller: caller, + prematch: prematch, + stacktrace: stacktrace, + unused: {_, unused}, + runtime_modules: runtime_modules, + vars: vars + ) + ) do + %{ + caller: caller, + prematch: prematch, + stacktrace: stacktrace, + unused: unused, + runtime_modules: runtime_modules, + vars: vars + } + end + else + Record.defrecordp(:elixir_ex, + caller: false, + prematch: :raise, + stacktrace: false, + unused: {%{}, 0}, + vars: {%{}, false} + ) + + defp elixir_ex_to_map( + elixir_ex( + caller: caller, + prematch: prematch, + stacktrace: stacktrace, + unused: {_, unused}, + vars: vars + ) + ) do + %{ + caller: caller, + prematch: prematch, + stacktrace: stacktrace, + unused: unused, + runtime_modules: [], + vars: vars + } + end end defp state_to_map(%State{} = state) do @@ -87,6 +116,7 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do on_exit(fn -> Application.put_env(:elixir_sense, :compiler_rewrite, false) end) + {:ok, %{}} end @@ -765,6 +795,7 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do defmodule Foo do defguard my(a) when is_integer(a) and a > 1 + defmacro aaa(a) do quote do is_integer(unquote(a)) and unquote(a) > 1 @@ -791,29 +822,39 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do end defp clean_capture_arg(ast) do - {ast, _} = Macro.prewalk(ast, nil, fn - {atom, meta, nil} = node, state when is_atom(atom) -> - # elixir changes the name to capture and does different counter tracking - node = with "&" <> int <- to_string(atom), {_, ""} <- Integer.parse(int) do - meta = Keyword.delete(meta, :counter) - {:capture, meta, nil} - else - _ -> node - end - {node, state} - node, state -> {node, state} - end) + {ast, _} = + Macro.prewalk(ast, nil, fn + {atom, meta, nil} = node, state when is_atom(atom) -> + # elixir changes the name to capture and does different counter tracking + node = + with "&" <> int <- to_string(atom), {_, ""} <- Integer.parse(int) do + meta = Keyword.delete(meta, :counter) + {:capture, meta, nil} + else + _ -> node + end + + {node, state} + + node, state -> + {node, state} + end) + ast end defp clean_capture_arg_elixir(ast) do - {ast, _} = Macro.prewalk(ast, nil, fn - {:capture, meta, nil} = node, state -> - # elixir changes the name to capture and does different counter tracking - meta = Keyword.delete(meta, :counter) - {{:capture, meta, nil}, state} - node, state -> {node, state} - end) + {ast, _} = + Macro.prewalk(ast, nil, fn + {:capture, meta, nil} = node, state -> + # elixir changes the name to capture and does different counter tracking + meta = Keyword.delete(meta, :counter) + {{:capture, meta, nil}, state} + + node, state -> + {node, state} + end) + ast end end diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index 78f03dfc..f4e5b87c 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -5,7 +5,12 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do alias ElixirSense.Core.Normalized.Code, as: NormalizedCode defp get_cursor_env(code) do - {:ok, ast} = NormalizedCode.Fragment.container_cursor_to_quoted(code, [columns: true, token_metadata: true]) + {:ok, ast} = + NormalizedCode.Fragment.container_cursor_to_quoted(code, + columns: true, + token_metadata: true + ) + # dbg(ast) state = MetadataBuilder.build(ast) state.cursor_env @@ -17,6 +22,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do case [] \ """ + assert {meta, env} = get_cursor_env(code) end @@ -25,6 +31,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do case [], [] \ """ + assert {meta, env} = get_cursor_env(code) end @@ -32,6 +39,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ case [], \ """ + assert {meta, env} = get_cursor_env(code) end @@ -40,8 +48,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do x = 5 case \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in clause left side" do @@ -49,8 +58,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do case a do [x, \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in clause guard" do @@ -58,8 +68,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do case a do x when \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in clause guard call" do @@ -67,8 +78,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do case a do x when is_atom(\ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in clause right side" do @@ -76,8 +88,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do case a do x -> \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in clause right side after expressions" do @@ -87,14 +100,16 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do foo(1) \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "invalid number of args with when" do code = """ case nil do 0, z when not is_nil(z) -> \ """ + assert get_cursor_env(code) end @@ -102,6 +117,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ case nil do 0, z -> \ """ + assert get_cursor_env(code) end end @@ -112,6 +128,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do cond [] \ """ + assert {meta, env} = get_cursor_env(code) end @@ -120,8 +137,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do x = foo() cond \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in clause left side" do @@ -130,8 +148,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do cond do \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in clause left side with assignment" do @@ -139,8 +158,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do cond do (x = foo(); \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in clause right side" do @@ -148,8 +168,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do cond do x = foo() -> \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in clause right side after expressions" do @@ -159,14 +180,16 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do foo(1) \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "invalid number of args" do code = """ cond do 0, z -> \ """ + assert get_cursor_env(code) end end @@ -177,6 +200,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do receive [] \ """ + assert {meta, env} = get_cursor_env(code) end @@ -185,8 +209,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do x = foo() receive \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in clause left side" do @@ -195,8 +220,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do receive do \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in clause left side pin" do @@ -205,8 +231,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do receive do {^\ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in clause left side multiple matches" do @@ -214,8 +241,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do receive do {:msg, x, \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in clause left side guard" do @@ -223,8 +251,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do receive do {:msg, x} when \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in clause left side guard call" do @@ -232,8 +261,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do receive do {:msg, x} when is_atom(\ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in clause right side" do @@ -241,8 +271,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do receive do {:msg, x} -> \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in after clause left side" do @@ -253,8 +284,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do after \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in after clause right side" do @@ -265,8 +297,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do after 0 -> \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "invalid number of args in after" do @@ -276,6 +309,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do after 0, z -> \ """ + assert get_cursor_env(code) end @@ -287,6 +321,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do 0 -> :ok 1 -> \ """ + assert get_cursor_env(code) end end @@ -297,6 +332,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do try [] \ """ + assert {meta, env} = get_cursor_env(code) end @@ -305,8 +341,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do x = foo() try \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in do block" do @@ -315,8 +352,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do try do \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in left side of rescue clause" do @@ -327,8 +365,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do rescue \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in left side of rescue clause match expression - invalid var" do @@ -339,8 +378,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do rescue bar() in [\ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in left side of rescue clause match expression" do @@ -351,8 +391,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do rescue e in [\ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in right side of rescue clause" do @@ -362,8 +403,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do rescue x in [Error] -> \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in left side of catch clause" do @@ -374,8 +416,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do catch \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in left side of catch clause guard" do @@ -385,8 +428,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do catch x when \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in left side of catch clause after type" do @@ -396,8 +440,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do catch x, \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in left side of catch clause 2 arg guard" do @@ -407,8 +452,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do catch x, _ when \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in right side of catch clause" do @@ -418,8 +464,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do catch x -> \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in right side of catch clause 2 arg" do @@ -429,8 +476,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do catch x, _ -> \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in left side of else clause" do @@ -441,8 +489,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do else \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in left side of else clause guard" do @@ -452,8 +501,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do else x when \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in right side of else clause" do @@ -463,8 +513,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do else x -> \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in after block" do @@ -475,8 +526,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do after \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end end @@ -486,8 +538,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do x = foo() with [], \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in match expressions" do @@ -495,16 +548,18 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do x = foo() with \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in match expressions guard" do code = """ with x when \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in match expressions - right side" do @@ -512,16 +567,18 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do x = foo() with 1 <- \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in match expressions - right side next expression" do code = """ with x <- foo(), \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in do block" do @@ -529,8 +586,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do with x <- foo() do \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in else clause left side" do @@ -541,8 +599,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do else \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in else clause left side guard" do @@ -552,8 +611,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do else x when \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in else clause right side" do @@ -563,8 +623,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do else x -> \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end end @@ -574,8 +635,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do x = foo() for [], \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in generator match expressions" do @@ -583,16 +645,18 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do x = foo() for \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in generator match expression guard" do code = """ for x when \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in generator match expression right side" do @@ -600,8 +664,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do x = foo() for a <- \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in generator match expressions bitstring" do @@ -609,16 +674,18 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do x = foo() for <<\ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in generator match expression guard bitstring" do code = """ for < \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) - assert Enum.any?(env.vars, & &1.name == :y) + assert Enum.any?(env.vars, &(&1.name == :x)) + assert Enum.any?(env.vars, &(&1.name == :y)) end test "cursor in do block reduce right side of clause too many args" do @@ -719,9 +796,10 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do for x <- [], reduce: %{} do y, z -> \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) - assert Enum.any?(env.vars, & &1.name == :y) + assert Enum.any?(env.vars, &(&1.name == :x)) + assert Enum.any?(env.vars, &(&1.name == :y)) end test "cursor in do block reduce right side of clause too little args" do @@ -729,8 +807,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do for x <- [], reduce: %{} do -> \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in do block right side of clause without reduce" do @@ -738,9 +817,10 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do for x <- [] do y -> \ """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) - assert Enum.any?(env.vars, & &1.name == :y) + assert Enum.any?(env.vars, &(&1.name == :x)) + assert Enum.any?(env.vars, &(&1.name == :y)) end end @@ -753,8 +833,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do x, _ -> __cursor__() end """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "default args in clause" do @@ -763,8 +844,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do x \\\\ nil -> __cursor__() end """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "incomplete clause left side" do @@ -774,8 +856,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do __cursor__() end """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "incomplete clause left side guard" do @@ -784,8 +867,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do x when __cursor__() end """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end test "incomplete clause right side" do @@ -794,8 +878,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do x -> __cursor__() end """ + assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, & &1.name == :x) + assert Enum.any?(env.vars, &(&1.name == :x)) end end @@ -804,6 +889,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &\ """ + assert get_cursor_env(code) end @@ -811,6 +897,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &foo\ """ + assert get_cursor_env(code) end @@ -818,6 +905,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &foo/\ """ + assert get_cursor_env(code) end @@ -825,6 +913,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &foo/1\ """ + assert get_cursor_env(code) end @@ -832,6 +921,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &foo/1000; \ """ + assert get_cursor_env(code) end @@ -839,6 +929,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &foo.\ """ + assert get_cursor_env(code) end @@ -846,6 +937,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &foo.(\ """ + assert get_cursor_env(code) end @@ -853,6 +945,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &foo.()\ """ + assert get_cursor_env(code) end @@ -860,6 +953,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &foo.bar\ """ + assert get_cursor_env(code) end @@ -867,6 +961,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &Foo\ """ + assert get_cursor_env(code) end @@ -874,6 +969,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &Foo.\ """ + assert get_cursor_env(code) end @@ -881,6 +977,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &Foo.bar\ """ + assert get_cursor_env(code) end @@ -888,6 +985,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &Foo.bar/\ """ + assert get_cursor_env(code) end @@ -895,6 +993,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &Foo.bar/1\ """ + assert get_cursor_env(code) end @@ -902,6 +1001,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &Foo.bar(\ """ + assert get_cursor_env(code) end @@ -909,6 +1009,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &Foo.bar()\ """ + assert get_cursor_env(code) end @@ -916,6 +1017,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &{\ """ + assert get_cursor_env(code) end @@ -923,6 +1025,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &{}\ """ + assert get_cursor_env(code) end @@ -930,6 +1033,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &[\ """ + assert get_cursor_env(code) end @@ -937,6 +1041,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &[]\ """ + assert get_cursor_env(code) end @@ -944,6 +1049,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &<<\ """ + assert get_cursor_env(code) end @@ -951,6 +1057,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &<<>>\ """ + assert get_cursor_env(code) end @@ -958,6 +1065,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &%\ """ + assert get_cursor_env(code) end @@ -965,6 +1073,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &%{\ """ + assert get_cursor_env(code) end @@ -972,6 +1081,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &%{}\ """ + assert get_cursor_env(code) end @@ -979,6 +1089,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &%Foo\ """ + assert get_cursor_env(code) end @@ -986,6 +1097,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &%Foo{\ """ + assert get_cursor_env(code) end @@ -993,6 +1105,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &%Foo{}\ """ + assert get_cursor_env(code) end @@ -1000,6 +1113,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ & (\ """ + assert get_cursor_env(code) end @@ -1007,6 +1121,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ & (:ok; \ """ + assert get_cursor_env(code) end @@ -1014,6 +1129,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ & &\ """ + assert get_cursor_env(code) end @@ -1021,6 +1137,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ & &2\ """ + assert get_cursor_env(code) end @@ -1028,6 +1145,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &[&1, \ """ + assert get_cursor_env(code) end @@ -1035,6 +1153,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &[&2, \ """ + assert get_cursor_env(code) end @@ -1042,6 +1161,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &{}; \ """ + assert get_cursor_env(code) end @@ -1049,6 +1169,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ & &0; \ """ + assert get_cursor_env(code) end @@ -1056,6 +1177,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &1; \ """ + assert get_cursor_env(code) end @@ -1063,6 +1185,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &foo; \ """ + assert get_cursor_env(code) end @@ -1070,6 +1193,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &"foo"; \ """ + assert get_cursor_env(code) end @@ -1078,6 +1202,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do defmodule A do (&asdf/1) +\ """ + assert get_cursor_env(code) end @@ -1087,6 +1212,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do import :erlang, only: [exit: 1], warn: false def foo, do: (&exit/1) +\ """ + assert get_cursor_env(code) end end @@ -1096,6 +1222,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ ^\ """ + assert get_cursor_env(code) end @@ -1103,6 +1230,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ ^__cursor__() = x\ """ + assert get_cursor_env(code) end end @@ -1112,6 +1240,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ %{foo => x} = x\ """ + assert get_cursor_env(code) end @@ -1119,6 +1248,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ %{a | x: __cursor__()} = x\ """ + assert get_cursor_env(code) end @@ -1126,6 +1256,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ %{a: "123", \ """ + assert get_cursor_env(code) end end @@ -1135,6 +1266,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ %\ """ + assert get_cursor_env(code) end @@ -1142,6 +1274,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ %foo{\ """ + assert get_cursor_env(code) end @@ -1149,6 +1282,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ %Foo{"asd" => [\ """ + assert get_cursor_env(code) end end @@ -1158,6 +1292,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ <>)::32, \ """ + assert get_cursor_env(code) end @@ -1228,6 +1372,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ <> = \ """ + assert get_cursor_env(code) end @@ -1235,6 +1380,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ <<"foo"::size(8)-unit(:oops), \ """ + assert get_cursor_env(code) end @@ -1242,6 +1388,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ <<1::unknown(), \ """ + assert get_cursor_env(code) end @@ -1249,6 +1396,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ <<1::refb_spec, \ """ + assert get_cursor_env(code) end @@ -1256,6 +1404,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ <<:ok, \ """ + assert get_cursor_env(code) end @@ -1263,6 +1412,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ <> = \ """ + assert get_cursor_env(code) end @@ -1270,6 +1420,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ <<\ """ + assert get_cursor_env(code) end @@ -1277,6 +1428,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ <<1::\ """ + assert get_cursor_env(code) end @@ -1284,6 +1436,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ <<1::binary-\ """ + assert get_cursor_env(code) end @@ -1291,6 +1444,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ <<1::size(\ """ + assert get_cursor_env(code) end end @@ -1300,6 +1454,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ quote [bind_quoted: 123] do\ """ + assert get_cursor_env(code) end @@ -1307,6 +1462,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ quote \ """ + assert get_cursor_env(code) end @@ -1314,6 +1470,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ quote [\ """ + assert get_cursor_env(code) end @@ -1321,6 +1478,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ quote [bind_quoted: \ """ + assert get_cursor_env(code) end @@ -1328,6 +1486,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ quote [bind_quoted: [\ """ + assert get_cursor_env(code) end @@ -1335,6 +1494,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ quote [bind_quoted: [asd: \ """ + assert get_cursor_env(code) end @@ -1342,6 +1502,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ quote [bind_quoted: [asd: 1]], \ """ + assert get_cursor_env(code) end @@ -1349,6 +1510,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ quote [bind_quoted: [asd: 1]], [\ """ + assert get_cursor_env(code) end @@ -1356,6 +1518,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ quote :foo, [\ """ + assert get_cursor_env(code) end @@ -1364,6 +1527,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do quote do \ """ + assert get_cursor_env(code) end @@ -1372,6 +1536,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do quote do unquote(\ """ + assert get_cursor_env(code) end @@ -1380,6 +1545,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do quote do unquote_splicing(\ """ + assert get_cursor_env(code) end @@ -1388,6 +1554,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do quote bind_quoted: [a: 1] do unquote(\ """ + assert get_cursor_env(code) end @@ -1395,6 +1562,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ unquote(\ """ + assert get_cursor_env(code) end @@ -1402,6 +1570,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ quote [file: 1] do\ """ + assert get_cursor_env(code) end @@ -1409,6 +1578,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ quote [unquote: 1] do\ """ + assert get_cursor_env(code) end @@ -1416,6 +1586,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ quote do: unquote_splicing(\ """ + assert get_cursor_env(code) end end @@ -1425,6 +1596,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ :foo.(a, \ """ + assert get_cursor_env(code) end @@ -1432,6 +1604,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ a.() = \ """ + assert get_cursor_env(code) end @@ -1440,6 +1613,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do case x do y when a.() -> \ """ + assert get_cursor_env(code) end @@ -1448,6 +1622,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do case x do y when map.field() -> \ """ + assert get_cursor_env(code) end @@ -1455,6 +1630,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ Foo.bar() = \ """ + assert get_cursor_env(code) end @@ -1462,6 +1638,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ __ENV__.line.foo \ """ + assert get_cursor_env(code) end @@ -1470,6 +1647,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do Foo.foo do a -> \ """ + assert get_cursor_env(code) end @@ -1477,6 +1655,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ 1.foo \ """ + assert get_cursor_env(code) end @@ -1485,6 +1664,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do a = 1 a -1 .. \ """ + assert get_cursor_env(code) end @@ -1493,6 +1673,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do foo do a -> \ """ + assert get_cursor_env(code) end @@ -1500,6 +1681,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ bar() = \ """ + assert get_cursor_env(code) end @@ -1509,6 +1691,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do import :erlang, only: [exit: 1], warn: false def foo, do: exit(\ """ + assert get_cursor_env(code) end @@ -1522,6 +1705,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do defmacro _ && _, do: :error def world, do: 1 && \ """ + assert get_cursor_env(code) end end @@ -1532,6 +1716,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do foo = :foo foo.Foo.a(\ """ + assert get_cursor_env(code) end @@ -1539,16 +1724,19 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ alias \ """ + assert get_cursor_env(code) code = """ require \ """ + assert get_cursor_env(code) code = """ import \ """ + assert get_cursor_env(code) end @@ -1556,16 +1744,19 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ alias A.a\ """ + assert get_cursor_env(code) code = """ require A.a\ """ + assert get_cursor_env(code) code = """ import A.a\ """ + assert get_cursor_env(code) end @@ -1573,16 +1764,19 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ alias A.B, \ """ + assert get_cursor_env(code) code = """ require A.B, \ """ + assert get_cursor_env(code) code = """ import A.B, \ """ + assert get_cursor_env(code) end @@ -1590,16 +1784,19 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ alias A.B, warn: \ """ + assert get_cursor_env(code) code = """ require A.B, warn: \ """ + assert get_cursor_env(code) code = """ import A.B, warn: \ """ + assert get_cursor_env(code) end end @@ -1609,6 +1806,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ super(\ """ + assert get_cursor_env(code) end @@ -1617,6 +1815,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do defmodule A do super(\ """ + assert get_cursor_env(code) end @@ -1624,6 +1823,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ super() = \ """ + assert get_cursor_env(code) end @@ -1631,6 +1831,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ & super(&1, \ """ + assert get_cursor_env(code) end @@ -1638,21 +1839,25 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ &super\ """ + assert get_cursor_env(code) code = """ &super/\ """ + assert get_cursor_env(code) code = """ &super/1 \ """ + assert get_cursor_env(code) code = """ (&super/1) +\ """ + assert get_cursor_env(code) end @@ -1662,6 +1867,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do def a do super(\ """ + assert get_cursor_env(code) end @@ -1672,6 +1878,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do def a(x) do super(x) +\ """ + assert get_cursor_env(code) end end @@ -1683,6 +1890,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do x -> x _ -> \ """ + assert get_cursor_env(code) end @@ -1690,6 +1898,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ {1, _, [\ """ + assert get_cursor_env(code) end @@ -1697,6 +1906,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ <> = <> = \ """ + assert get_cursor_env(code) end @@ -1705,6 +1915,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do cond do x when x = \ """ + assert get_cursor_env(code) end @@ -1712,6 +1923,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do code = """ __STACKTRACE__ = \ """ + assert get_cursor_env(code) end end diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index c03a6e94..d2f61434 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -74,7 +74,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert [ %VarInfo{name: :abc, positions: [{1, 1}]}, - # %VarInfo{name: :abc, positions: [{1, 13}]}, + # %VarInfo{name: :abc, positions: [{1, 13}]}, %VarInfo{name: :cde, positions: [{1, 7}]} ] = state |> get_line_vars(2) end @@ -154,7 +154,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert Map.has_key?(state.lines_to_env[3].versioned_vars, {:abc, nil}) assert [ - # %VarInfo{name: :abc, positions: [{1, 1}]}, + # %VarInfo{name: :abc, positions: [{1, 1}]}, %VarInfo{name: :abc, positions: [{2, 1}]} ] = state |> get_line_vars(3) end @@ -239,17 +239,9 @@ defmodule ElixirSense.Core.MetadataBuilderTest do {:y, nil} ] - if Version.match?(System.version(), ">= 1.17.0-dev") and @compiler do - assert [ - %VarInfo{name: :y, positions: [{1, 1}, {2, 11}, {3, 11}]} - ] = state |> get_line_vars(4) - else - # TODO this is wrong - assert [ - %VarInfo{name: :y, positions: [{1, 1}, {2, 11}]}, - %VarInfo{name: :y, positions: [{3, 11}]} - ] = state |> get_line_vars(4) - end + assert [ + %VarInfo{name: :y, positions: [{1, 1}, {2, 11}, {3, 11}]} + ] = state |> get_line_vars(4) end test "undefined usage" do @@ -368,10 +360,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert Map.has_key?(state.lines_to_env[4].versioned_vars, {:cde, nil}) - assert ([ - # %VarInfo{name: :cde, positions: [{1, 1}], scope_id: scope_id_1}, - %VarInfo{name: :cde, positions: [{3, 3}]} - ]) = state |> get_line_vars(4) + assert [ + # %VarInfo{name: :cde, positions: [{1, 1}], scope_id: scope_id_1}, + %VarInfo{name: :cde, positions: [{3, 3}]} + ] = state |> get_line_vars(4) assert Map.has_key?(state.lines_to_env[6].versioned_vars, {:cde, nil}) @@ -513,96 +505,44 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - if Version.match?(System.version(), ">= 1.17.0-dev") and @compiler do - assert Map.keys(state.lines_to_env[1].versioned_vars) == [] - assert [] = state |> get_line_vars(1) - - assert Map.keys(state.lines_to_env[2].versioned_vars) == [{:abc, nil}] + assert Map.keys(state.lines_to_env[1].versioned_vars) == [] + assert [] = state |> get_line_vars(1) - assert [ - %VarInfo{name: :abc, positions: [{1, 6}]} - ] = state |> get_line_vars(2) - - assert Map.keys(state.lines_to_env[3].versioned_vars) == [{:abc, nil}, {:cde, nil}] - - assert [ - %VarInfo{name: :abc, positions: [{1, 6}]}, - %VarInfo{name: :cde, positions: [{2, 3}]} - ] = state |> get_line_vars(3) + assert Map.keys(state.lines_to_env[2].versioned_vars) == [{:abc, nil}] - assert Map.keys(state.lines_to_env[5].versioned_vars) == [ - {:abc, nil}, - {:cde, nil}, - {:z, nil} - ] - - assert [ - %VarInfo{name: :abc, positions: [{1, 6}, {4, 7}]}, - %VarInfo{name: :cde, positions: [{2, 3}, {4, 13}]}, - %VarInfo{name: :z, positions: [{4, 3}]} - ] = state |> get_line_vars(5) - - assert Map.keys(state.lines_to_env[9].versioned_vars) == [{:c, nil}, {:other, nil}] - - assert [ - %VarInfo{name: :c, positions: [{8, 5}]}, - %VarInfo{name: :other, positions: [{7, 3}]} - ] = state |> get_line_vars(9) - - assert Map.keys(state.lines_to_env[11].versioned_vars) == [] - - assert [] = state |> get_line_vars(11) - else - assert Map.keys(state.lines_to_env[1].versioned_vars) == [{:abc, nil}] - - assert [ - %VarInfo{name: :abc, positions: [{1, 6}]} - ] = state |> get_line_vars(1) + assert [ + %VarInfo{name: :abc, positions: [{1, 6}]} + ] = state |> get_line_vars(2) - assert Map.keys(state.lines_to_env[2].versioned_vars) == [{:abc, nil}, {:cde, nil}] + assert Map.keys(state.lines_to_env[3].versioned_vars) == [{:abc, nil}, {:cde, nil}] - assert [ - %VarInfo{name: :abc, positions: [{1, 6}]}, - %VarInfo{name: :cde, positions: [{2, 3}]} - ] = state |> get_line_vars(2) - - assert Map.keys(state.lines_to_env[3].versioned_vars) == [{:abc, nil}, {:cde, nil}] + assert [ + %VarInfo{name: :abc, positions: [{1, 6}]}, + %VarInfo{name: :cde, positions: [{2, 3}]} + ] = state |> get_line_vars(3) - assert [ - %VarInfo{name: :abc, positions: [{1, 6}]}, - %VarInfo{name: :cde, positions: [{2, 3}]} - ] = state |> get_line_vars(3) + assert Map.keys(state.lines_to_env[5].versioned_vars) == [ + {:abc, nil}, + {:cde, nil}, + {:z, nil} + ] - assert Map.keys(state.lines_to_env[5].versioned_vars) == [ - {:abc, nil}, - {:cde, nil}, - {:z, nil} - ] + assert [ + %VarInfo{name: :abc, positions: [{1, 6}, {4, 7}]}, + %VarInfo{name: :cde, positions: [{2, 3}, {4, 13}]}, + %VarInfo{name: :z, positions: [{4, 3}]} + ] = state |> get_line_vars(5) - assert [ - %VarInfo{name: :abc, positions: [{1, 6}, {4, 7}]}, - %VarInfo{name: :cde, positions: [{2, 3}, {4, 13}]}, - %VarInfo{name: :z, positions: [{4, 3}]} - ] = state |> get_line_vars(5) + assert Map.keys(state.lines_to_env[9].versioned_vars) == [{:c, nil}, {:other, nil}] - # TODO this is quite wrong - assert Map.keys(state.lines_to_env[9].versioned_vars) == [ - {:abc, nil}, - {:c, nil}, - {:cde, nil}, - {:other, nil} - ] + assert [ + %VarInfo{name: :c, positions: [{8, 5}]}, + %VarInfo{name: :other, positions: [{7, 3}]} + ] = state |> get_line_vars(9) - assert [ - %VarInfo{name: :abc, positions: [{1, 6}, {4, 7}]}, - %VarInfo{name: :c, positions: [{8, 5}]}, - %VarInfo{name: :cde, positions: [{2, 3}, {4, 13}]}, - %VarInfo{name: :other, positions: [{7, 3}]} - ] = state |> get_line_vars(9) + assert Map.keys(state.lines_to_env[11].versioned_vars) == [] - assert Map.keys(state.lines_to_env[11].versioned_vars) == [] - assert [] = state |> get_line_vars(11) - end + assert [] = state |> get_line_vars(11) end test "for" do @@ -617,59 +557,29 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - if Version.match?(System.version(), ">= 1.17.0-dev") and @compiler do - assert Map.keys(state.lines_to_env[1].versioned_vars) == [] - assert [] = state |> get_line_vars(3) + assert Map.keys(state.lines_to_env[1].versioned_vars) == [] + assert [] = state |> get_line_vars(3) - assert Map.keys(state.lines_to_env[2].versioned_vars) == [{:abc, nil}] + assert Map.keys(state.lines_to_env[2].versioned_vars) == [{:abc, nil}] - assert [ - %VarInfo{name: :abc, positions: [{1, 5}]} - ] = state |> get_line_vars(2) - - assert Map.keys(state.lines_to_env[4].versioned_vars) == [ - {:abc, nil}, - {:cde, nil}, - {:z, nil} - ] - - assert [ - %VarInfo{name: :abc, positions: [{1, 5}]}, - %VarInfo{name: :cde, positions: [{2, 3}]}, - %VarInfo{name: :z, positions: [{3, 3}]} - ] = state |> get_line_vars(4) - - assert Map.keys(state.lines_to_env[6].versioned_vars) == [] - assert [] = state |> get_line_vars(3) - else - assert Map.keys(state.lines_to_env[1].versioned_vars) == [{:abc, nil}] - - assert [ - %VarInfo{name: :abc, positions: [{1, 5}]} - ] = state |> get_line_vars(1) - - assert Map.keys(state.lines_to_env[2].versioned_vars) == [{:abc, nil}, {:cde, nil}] - - assert [ - %VarInfo{name: :abc, positions: [{1, 5}]}, - %VarInfo{name: :cde, positions: [{2, 3}]} - ] = state |> get_line_vars(2) + assert [ + %VarInfo{name: :abc, positions: [{1, 5}]} + ] = state |> get_line_vars(2) - assert Map.keys(state.lines_to_env[4].versioned_vars) == [ - {:abc, nil}, - {:cde, nil}, - {:z, nil} - ] + assert Map.keys(state.lines_to_env[4].versioned_vars) == [ + {:abc, nil}, + {:cde, nil}, + {:z, nil} + ] - assert [ - %VarInfo{name: :abc, positions: [{1, 5}]}, - %VarInfo{name: :cde, positions: [{2, 3}]}, - %VarInfo{name: :z, positions: [{3, 3}]} - ] = state |> get_line_vars(4) + assert [ + %VarInfo{name: :abc, positions: [{1, 5}]}, + %VarInfo{name: :cde, positions: [{2, 3}]}, + %VarInfo{name: :z, positions: [{3, 3}]} + ] = state |> get_line_vars(4) - assert Map.keys(state.lines_to_env[6].versioned_vars) == [] - assert [] = state |> get_line_vars(6) - end + assert Map.keys(state.lines_to_env[6].versioned_vars) == [] + assert [] = state |> get_line_vars(3) end if Version.match?(System.version(), ">= 1.17.0-dev") and @compiler do @@ -887,10 +797,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert Map.keys(state.lines_to_env[4].versioned_vars) == [{:abc, nil}] - assert ([ - # %VarInfo{name: :abc, positions: [{1, 1}], scope_id: scope_id_1}, - %VarInfo{name: :abc, positions: [{3, 3}]} - ]) = state |> get_line_vars(4) + assert [ + # %VarInfo{name: :abc, positions: [{1, 1}], scope_id: scope_id_1}, + %VarInfo{name: :abc, positions: [{3, 3}]} + ] = state |> get_line_vars(4) assert Map.keys(state.lines_to_env[6].versioned_vars) == [{:abc, nil}] @@ -984,11 +894,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert Map.has_key?(state.lines_to_env[5].versioned_vars, {:abc, nil}) - if Version.match?(System.version(), ">= 1.17.0-dev") and @compiler do - assert [%VarInfo{name: :abc, positions: [{1, 1}, {3, 11}]}] = state |> get_line_vars(5) - else - assert [%VarInfo{name: :abc, positions: [{1, 1}]}] = state |> get_line_vars(5) - end + assert [%VarInfo{name: :abc, positions: [{1, 1}, {3, 11}]}] = state |> get_line_vars(5) end test "in quote unquote_splicing" do @@ -1007,16 +913,12 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert Map.has_key?(state.lines_to_env[8].versioned_vars, {:abc, nil}) - if Version.match?(System.version(), ">= 1.17.0-dev") and @compiler do - assert [ - %VarInfo{ - name: :abc, - positions: [{1, 1}, {3, 20}, {4, 21}, {4, 44}, {5, 25}, {6, 21}] - } - ] = state |> get_line_vars(8) - else - assert [%VarInfo{name: :abc, positions: [{1, 1}]}] = state |> get_line_vars(8) - end + assert [ + %VarInfo{ + name: :abc, + positions: [{1, 1}, {3, 20}, {4, 21}, {4, 44}, {5, 25}, {6, 21}] + } + ] = state |> get_line_vars(8) end test "in capture" do @@ -1033,34 +935,18 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - if Version.match?(System.version(), ">= 1.17.0-dev") and @compiler do - assert Map.keys(state.lines_to_env[6].versioned_vars) == [{:"&1", nil}, {:abc, nil}] - - assert [ - %VarInfo{name: :"&1", positions: [{3, 3}]}, - %VarInfo{name: :abc, positions: [{1, 1}, {4, 3}]} - ] = state |> get_line_vars(6) - - assert Map.keys(state.lines_to_env[8].versioned_vars) == [{:abc, nil}] + assert [{:"&1", _}, {:abc, nil}] = Map.keys(state.lines_to_env[6].versioned_vars) - assert [ - %VarInfo{name: :abc, positions: [{1, 1}, {4, 3}]} - ] = state |> get_line_vars(8) - else - assert Map.keys(state.lines_to_env[6].versioned_vars) == [{:abc, nil}, {:cde, nil}] - - assert [ - %VarInfo{name: :abc, positions: [{1, 1}, {4, 3}]}, - %VarInfo{name: :cde, positions: [{5, 3}]} - ] = state |> get_line_vars(6) + assert [ + %VarInfo{name: :"&1", positions: [{3, 3}]}, + %VarInfo{name: :abc, positions: [{1, 1}, {4, 3}]} + ] = state |> get_line_vars(6) - assert Map.keys(state.lines_to_env[8].versioned_vars) == [{:abc, nil}, {:cde, nil}] + assert Map.keys(state.lines_to_env[8].versioned_vars) == [{:abc, nil}] - assert [ - %VarInfo{name: :abc, positions: [{1, 1}, {4, 3}]}, - %VarInfo{name: :cde, positions: [{5, 3}]} - ] = state |> get_line_vars(8) - end + assert [ + %VarInfo{name: :abc, positions: [{1, 1}, {4, 3}]} + ] = state |> get_line_vars(8) end test "module body" do @@ -1264,85 +1150,85 @@ defmodule ElixirSense.Core.MetadataBuilderTest do end if @var_in_ex_unit do - describe "vars in ex_unit" do - test "variables are added to environment in ex_unit test" do - state = - """ - defmodule MyModuleTests do - use ExUnit.Case, async: true - - test "it does what I want", %{some: some} do - IO.puts("") - end + describe "vars in ex_unit" do + test "variables are added to environment in ex_unit test" do + state = + """ + defmodule MyModuleTests do + use ExUnit.Case, async: true - describe "this" do - test "too does what I want" do + test "it does what I want", %{some: some} do IO.puts("") end - end - test "is not implemented" - end - """ - |> string_to_state + describe "this" do + test "too does what I want" do + IO.puts("") + end + end - assert [%VarInfo{type: nil, scope_id: scope_id}] = state |> get_line_vars(5) - assert [%VarInfo{name: :some}] = state.vars_info_per_scope_id[scope_id] + test "is not implemented" + end + """ + |> string_to_state - assert Map.has_key?( - state.mods_funs_to_positions, - {MyModuleTests, :"test it does what I want", 1} - ) + assert [%VarInfo{type: nil, scope_id: scope_id}] = state |> get_line_vars(5) + assert [%VarInfo{name: :some}] = state.vars_info_per_scope_id[scope_id] - assert Map.has_key?( - state.mods_funs_to_positions, - {MyModuleTests, :"test this too does what I want", 1} - ) + assert Map.has_key?( + state.mods_funs_to_positions, + {MyModuleTests, :"test it does what I want", 1} + ) - assert Map.has_key?( - state.mods_funs_to_positions, - {MyModuleTests, :"test is not implemented", 1} - ) - end + assert Map.has_key?( + state.mods_funs_to_positions, + {MyModuleTests, :"test this too does what I want", 1} + ) - test "variables are added to environment in ex_unit setup" do - state = - """ - defmodule MyModuleTests do - use ExUnit.Case, async: true + assert Map.has_key?( + state.mods_funs_to_positions, + {MyModuleTests, :"test is not implemented", 1} + ) + end - setup_all %{some: some} do - IO.puts("") - end + test "variables are added to environment in ex_unit setup" do + state = + """ + defmodule MyModuleTests do + use ExUnit.Case, async: true - setup %{some: other} do - IO.puts("") - end + setup_all %{some: some} do + IO.puts("") + end - setup do - IO.puts("") - end + setup %{some: other} do + IO.puts("") + end + + setup do + IO.puts("") + end - setup :clean_up_tmp_directory + setup :clean_up_tmp_directory - setup [:clean_up_tmp_directory, :another_setup] + setup [:clean_up_tmp_directory, :another_setup] - setup {MyModule, :my_setup_function} - end - """ - |> string_to_state + setup {MyModule, :my_setup_function} + end + """ + |> string_to_state - assert [%VarInfo{type: nil, scope_id: scope_id}] = state |> get_line_vars(5) - assert [%VarInfo{name: :some}] = state.vars_info_per_scope_id[scope_id] + assert [%VarInfo{type: nil, scope_id: scope_id}] = state |> get_line_vars(5) + assert [%VarInfo{name: :some}] = state.vars_info_per_scope_id[scope_id] - assert [%VarInfo{type: nil, scope_id: scope_id}] = state |> get_line_vars(9) - assert [%VarInfo{name: :other}] = state.vars_info_per_scope_id[scope_id] + assert [%VarInfo{type: nil, scope_id: scope_id}] = state |> get_line_vars(9) + assert [%VarInfo{name: :other}] = state.vars_info_per_scope_id[scope_id] - # we do not generate defs - ExUnit.Callbacks.__setup__ is too complicated and generates def names with counters, e.g. - # :"__ex_unit_setup_#{counter}_#{length(setup)}" + # we do not generate defs - ExUnit.Callbacks.__setup__ is too complicated and generates def names with counters, e.g. + # :"__ex_unit_setup_#{counter}_#{length(setup)}" + end end end - end describe "typespec vars" do test "registers type parameters" do @@ -1373,7 +1259,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert [ %VarInfo{name: :p, positions: [{2, 49}, {2, 18}, {2, 25}, {2, 33}, {2, 70}]}, %VarInfo{name: :q, positions: [{2, 49}, {2, 46}]} - ] = state.vars_info_per_scope_id[2] |> Map.values + ] = state.vars_info_per_scope_id[2] |> Map.values() end test "does not register annotated spec params as type variables" do @@ -1707,7 +1593,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert [%VarInfo{type: {:atom, :ok}}, %VarInfo{type: {:map, [], nil}}] = state |> get_line_vars(3) + assert [%VarInfo{type: {:atom, :ok}}, %VarInfo{type: {:map, [], nil}}] = + state |> get_line_vars(3) end test "module attributes value binding to and from variables" do @@ -1785,14 +1672,14 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert [ %VarInfo{ name: :other, - # TODO do we need to rewrite? change Binding - # type: {:local_call, :elem, [{:attribute, :myattribute}, {:integer, 0}]} + # TODO do we need to rewrite? change Binding + # type: {:local_call, :elem, [{:attribute, :myattribute}, {:integer, 0}]} type: { - :call, - {:atom, :erlang}, - :element, - [integer: 1, attribute: :myattribute] - } + :call, + {:atom, :erlang}, + :element, + [integer: 1, attribute: :myattribute] + } }, %VarInfo{ name: :var, @@ -2044,7 +1931,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert [ %VarInfo{name: :a, type: {:attribute, :myattribute}}, %VarInfo{name: :b, type: {:call, {:atom, Date}, :utc_now, []}}, - %VarInfo{name: :c, type: {:list_head, {:variable, :a}}}, + %VarInfo{name: :c, type: {:list_head, {:variable, :a}}} ] = state |> get_line_vars(6) end @@ -2061,7 +1948,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert [ - %VarInfo{name: :a, type: {:intersection, [:atom, {:list_head, {:attribute, :myattribute}}]}}, + %VarInfo{ + name: :a, + type: {:intersection, [:atom, {:list_head, {:attribute, :myattribute}}]} + } ] = state |> get_line_vars(4) end @@ -2370,7 +2260,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert [ %VarInfo{ name: :formatted, - type: {:map_key, {:variable, :state}, {:atom, :formatted}}, + type: {:map_key, {:variable, :state}, {:atom, :formatted}} }, %VarInfo{ name: :state, @@ -2378,15 +2268,15 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } ] = state |> get_line_vars(3) - assert [ - %VarInfo{ - name: :formatted - }, - %VarInfo{ - name: :state, - type: {:struct, [formatted: {:variable, :formatted}], {:atom, MyState}, nil} - } - ] = state |> get_line_vars(7) + assert [ + %VarInfo{ + name: :formatted + }, + %VarInfo{ + name: :state, + type: {:struct, [formatted: {:variable, :formatted}], {:atom, MyState}, nil} + } + ] = state |> get_line_vars(7) end test "two way refinement in match context nested" do @@ -2403,7 +2293,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert [ %VarInfo{ name: :formatted, - type: {:map_key, {:variable, :state}, {:atom, :formatted}}, + type: {:map_key, {:variable, :state}, {:atom, :formatted}} }, %VarInfo{ name: :state, @@ -2411,15 +2301,15 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } ] = state |> get_line_vars(3) - assert [ - %VarInfo{ - name: :formatted - }, - %VarInfo{ - name: :state, - type: {:struct, [formatted: {:variable, :formatted}], {:atom, MyState}, nil} - } - ] = state |> get_line_vars(7) + assert [ + %VarInfo{ + name: :formatted + }, + %VarInfo{ + name: :state, + type: {:struct, [formatted: {:variable, :formatted}], {:atom, MyState}, nil} + } + ] = state |> get_line_vars(7) end test "two way refinement in match context nested case" do @@ -2436,15 +2326,15 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert [ - %VarInfo{ - name: :formatted - }, - %VarInfo{ - name: :state, - type: {:struct, [formatted: {:variable, :formatted}], {:atom, MyState}, nil} - } - ] = state |> get_line_vars(5) + assert [ + %VarInfo{ + name: :formatted + }, + %VarInfo{ + name: :state, + type: {:struct, [formatted: {:variable, :formatted}], {:atom, MyState}, nil} + } + ] = state |> get_line_vars(5) end test "two way refinement in nested `=` binding" do @@ -2525,10 +2415,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert [ - %VarInfo{ - name: :res, - type: :todo - }, + %VarInfo{ + name: :res, + type: :todo + }, %VarInfo{ name: :x, type: @@ -2584,12 +2474,12 @@ defmodule ElixirSense.Core.MetadataBuilderTest do %VarInfo{ name: :e2, type: { - :union, - [ - {:struct, [], {:atom, RuntimeError}, nil}, - {:struct, [], {:atom, Enum.EmptyError}, nil} - ] - } + :union, + [ + {:struct, [], {:atom, RuntimeError}, nil}, + {:struct, [], {:atom, Enum.EmptyError}, nil} + ] + } } ] = state |> get_line_vars(11) @@ -2600,12 +2490,12 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } ] = state |> get_line_vars(13) - assert [ - %VarInfo{ - name: :e4, - type: {:struct, [], {:atom, Exception}, nil} - } - ] = state |> get_line_vars(15) + assert [ + %VarInfo{ + name: :e4, + type: {:struct, [], {:atom, Exception}, nil} + } + ] = state |> get_line_vars(15) assert [ %VarInfo{ @@ -2672,15 +2562,17 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert ([ - %VarInfo{name: :var_arg, positions: [{3, 12}], scope_id: scope_id_1}, - %VarInfo{name: :var_in1, positions: [{4, 5}], scope_id: scope_id_2}, - %VarInfo{name: :var_in2, positions: [{5, 5}], scope_id: scope_id_2} - ] when scope_id_2 > scope_id_1) = state |> get_line_vars(6) + %VarInfo{name: :var_arg, positions: [{3, 12}], scope_id: scope_id_1}, + %VarInfo{name: :var_in1, positions: [{4, 5}], scope_id: scope_id_2}, + %VarInfo{name: :var_in2, positions: [{5, 5}], scope_id: scope_id_2} + ] + when scope_id_2 > scope_id_1) = state |> get_line_vars(6) assert ([ - %VarInfo{name: :var_after, positions: [{8, 5}], scope_id: scope_id_2}, - %VarInfo{name: :var_arg, positions: [{3, 12}], scope_id: scope_id_1} - ] when scope_id_2 > scope_id_1) = state |> get_line_vars(9) + %VarInfo{name: :var_after, positions: [{8, 5}], scope_id: scope_id_2}, + %VarInfo{name: :var_arg, positions: [{3, 12}], scope_id: scope_id_1} + ] + when scope_id_2 > scope_id_1) = state |> get_line_vars(9) end test "vars defined inside a function with params" do @@ -2700,14 +2592,15 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert ([ - %VarInfo{name: :_par5, positions: [{3, 57}], scope_id: scope_id_1}, - %VarInfo{name: :par1, positions: [{3, 20}], scope_id: scope_id_1}, - %VarInfo{name: :par2, positions: [{3, 33}], scope_id: scope_id_1}, - %VarInfo{name: :par3, positions: [{3, 39}], scope_id: scope_id_1}, - %VarInfo{name: :par4, positions: [{3, 51}], scope_id: scope_id_1}, - %VarInfo{name: :var_in1, positions: [{4, 5}], scope_id: scope_id_2}, - %VarInfo{name: :var_in2, positions: [{5, 5}], scope_id: scope_id_2} - ] when scope_id_2 > scope_id_1) = state |> get_line_vars(6) + %VarInfo{name: :_par5, positions: [{3, 57}], scope_id: scope_id_1}, + %VarInfo{name: :par1, positions: [{3, 20}], scope_id: scope_id_1}, + %VarInfo{name: :par2, positions: [{3, 33}], scope_id: scope_id_1}, + %VarInfo{name: :par3, positions: [{3, 39}], scope_id: scope_id_1}, + %VarInfo{name: :par4, positions: [{3, 51}], scope_id: scope_id_1}, + %VarInfo{name: :var_in1, positions: [{4, 5}], scope_id: scope_id_2}, + %VarInfo{name: :var_in2, positions: [{5, 5}], scope_id: scope_id_2} + ] + when scope_id_2 > scope_id_1) = state |> get_line_vars(6) assert [ %VarInfo{name: :arg, positions: [{8, 14}, {8, 24}]} @@ -2731,11 +2624,11 @@ defmodule ElixirSense.Core.MetadataBuilderTest do vars = state |> get_line_vars(6) - assert ([ - # %VarInfo{name: :var1, positions: [{3, 19}, {3, 37}], scope_id: scope_id_1}, - # %VarInfo{name: :var1, positions: [{4, 5}], scope_id: scope_id_2}, - %VarInfo{name: :var1, positions: [{5, 5}]} - ]) = vars + assert [ + # %VarInfo{name: :var1, positions: [{3, 19}, {3, 37}], scope_id: scope_id_1}, + # %VarInfo{name: :var1, positions: [{4, 5}], scope_id: scope_id_2}, + %VarInfo{name: :var1, positions: [{5, 5}]} + ] = vars end test "vars defined inside a module" do @@ -3546,7 +3439,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do end test "multiple guards" do - assert %VarInfo{name: :x, type: {:union, [:bitstring, :number]}} = var_with_guards("is_bitstring(x) when is_integer(x)") + assert %VarInfo{name: :x, type: {:union, [:bitstring, :number]}} = + var_with_guards("is_bitstring(x) when is_integer(x)") end test "list guards" do @@ -3581,7 +3475,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do test "map guards" do assert %VarInfo{name: :x, type: {:map, [], nil}} = var_with_guards("is_map(x)") - assert %VarInfo{name: :x, type: {:intersection, [{:map, [], nil}, nil]}} = var_with_guards("is_non_struct_map(x)") + + assert %VarInfo{name: :x, type: {:intersection, [{:map, [], nil}, nil]}} = + var_with_guards("is_non_struct_map(x)") + assert %VarInfo{name: :x, type: {:map, [], nil}} = var_with_guards("map_size(x) == 1") assert %VarInfo{name: :x, type: {:map, [], nil}} = var_with_guards("1 == map_size(x)") @@ -3593,30 +3490,39 @@ defmodule ElixirSense.Core.MetadataBuilderTest do end test "struct guards" do - assert %VarInfo{name: :x, type: { - :intersection, - [ - {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, - {:struct, [], nil, nil} - ] - }} = var_with_guards("is_struct(x)") - - assert %VarInfo{name: :x, type: { - :intersection, - [ - {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, - {:struct, [], {:atom, URI}, nil} - ] - }} = + assert %VarInfo{ + name: :x, + type: { + :intersection, + [ + {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, + {:struct, [], nil, nil} + ] + } + } = var_with_guards("is_struct(x)") + + assert %VarInfo{ + name: :x, + type: { + :intersection, + [ + {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, + {:struct, [], {:atom, URI}, nil} + ] + } + } = var_with_guards("is_struct(x, URI)") - assert %VarInfo{name: :x, type: { - :intersection, - [ - {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, - {:struct, [], {:atom, URI}, nil} - ] - }} = + assert %VarInfo{ + name: :x, + type: { + :intersection, + [ + {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, + {:struct, [], {:atom, URI}, nil} + ] + } + } = """ defmodule MyModule do alias URI, as: MyURI @@ -3632,54 +3538,63 @@ defmodule ElixirSense.Core.MetadataBuilderTest do end test "exception guards" do - assert %VarInfo{name: :x, type: { - :intersection, - [ - { - :intersection, - [ - { - :intersection, - [ - {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, - {:struct, [], nil, nil} - ] - }, - {:map, [{:__exception__, nil}], nil} - ] - }, - {:map, [{:__exception__, {:atom, true}}], nil} - ] - }} = var_with_guards("is_exception(x)") - - assert %VarInfo{name: :x, type: { - :intersection, - [ - { - :intersection, - [ - { - :intersection, - [ - {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, - {:struct, [], {:atom, ArgumentError}, nil} - ] - }, - {:map, [{:__exception__, nil}], nil} - ] - }, - {:map, [{:__exception__, {:atom, true}}], nil} - ] - }} = + assert %VarInfo{ + name: :x, + type: { + :intersection, + [ + { + :intersection, + [ + { + :intersection, + [ + {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, + {:struct, [], nil, nil} + ] + }, + {:map, [{:__exception__, nil}], nil} + ] + }, + {:map, [{:__exception__, {:atom, true}}], nil} + ] + } + } = var_with_guards("is_exception(x)") + + assert %VarInfo{ + name: :x, + type: { + :intersection, + [ + { + :intersection, + [ + { + :intersection, + [ + {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, + {:struct, [], {:atom, ArgumentError}, nil} + ] + }, + {:map, [{:__exception__, nil}], nil} + ] + }, + {:map, [{:__exception__, {:atom, true}}], nil} + ] + } + } = var_with_guards("is_exception(x, ArgumentError)") - assert %VarInfo{name: :x, type: { - :intersection, - [ - {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, - {:struct, [], {:atom, ArgumentError}, nil} - ] - }} = + assert %VarInfo{ + name: :x, + type: { + :intersection, + [ + {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, + {:struct, [], {:atom, ArgumentError}, nil} + ] + } + } = """ defmodule MyModule do alias ArgumentError, as: MyURI @@ -3719,7 +3634,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert %VarInfo{name: :x, type: {:union, [nil, :atom]}} = var_with_guards("not is_map(x) or is_atom(x)") - assert %VarInfo{name: :x, type: {:intersection, [:nil, :atom]}} = + assert %VarInfo{name: :x, type: {:intersection, [nil, :atom]}} = var_with_guards("not is_map(x) and is_atom(x)") end end @@ -5214,7 +5129,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do generated: [true], specs: ["@callback without_spec(t(), term()) :: term()"] }, - # TODO there is raw unquote in spec + # TODO there is raw unquote in spec {Proto, :__protocol__, 1} => %ElixirSense.Core.State.SpecInfo{ kind: :spec, specs: [ @@ -6565,10 +6480,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert state.calls == %{ - 2 => [ - %CallInfo{arity: 0, func: :integer, position: {2, 14}, mod: nil} - ] - } + 2 => [ + %CallInfo{arity: 0, func: :integer, position: {2, 14}, mod: nil} + ] + } end test "registers typespec parens calls" do @@ -6581,10 +6496,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert state.calls == %{ - 2 => [ - %CallInfo{arity: 0, func: :integer, position: {2, 16}, mod: nil} - ] - } + 2 => [ + %CallInfo{arity: 0, func: :integer, position: {2, 16}, mod: nil} + ] + } end test "registers typespec no parens remote calls" do @@ -6597,10 +6512,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert state.calls == %{ - 2 => [ - %CallInfo{arity: 0, func: :t, position: {2, 19}, mod: Enum} - ] - } + 2 => [ + %CallInfo{arity: 0, func: :t, position: {2, 19}, mod: Enum} + ] + } end test "registers typespec parens remote calls" do @@ -6614,13 +6529,13 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert state.calls == %{ - 2 => [ - %CallInfo{arity: 0, func: :t, position: {2, 21}, mod: Enum} - ], - 3 => [ - %CallInfo{arity: 0, func: :t, position: {3, 23}, mod: Enum} - ] - } + 2 => [ + %CallInfo{arity: 0, func: :t, position: {2, 21}, mod: Enum} + ], + 3 => [ + %CallInfo{arity: 0, func: :t, position: {3, 23}, mod: Enum} + ] + } end test "registers typespec calls in specs with when guard" do @@ -6634,12 +6549,12 @@ defmodule ElixirSense.Core.MetadataBuilderTest do # NOTE var is not a type but a special variable assert state.calls == %{ - 2 => [ - %CallInfo{arity: 0, func: :pos_integer, position: {2, 71}, mod: nil}, - %CallInfo{arity: 0, func: :map, position: {2, 53}, mod: nil}, - %CallInfo{arity: 0, func: :integer, position: {2, 31}, mod: nil} - ] - } + 2 => [ + %CallInfo{arity: 0, func: :pos_integer, position: {2, 71}, mod: nil}, + %CallInfo{arity: 0, func: :map, position: {2, 53}, mod: nil}, + %CallInfo{arity: 0, func: :integer, position: {2, 31}, mod: nil} + ] + } end test "registers typespec calls in typespec with named args" do @@ -6653,18 +6568,18 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert state.calls == %{ - 2 => [ - %CallInfo{arity: 0, func: :integer, position: {2, 84}, mod: nil}, - %CallInfo{arity: 0, func: :integer, position: {2, 72}, mod: nil}, - %CallInfo{arity: 0, func: :integer, position: {2, 56}, mod: nil}, - %CallInfo{arity: 0, func: :integer, position: {2, 38}, mod: nil}, - ], - 3 => [ - %CallInfo{arity: 0, func: :integer, position: {3, 61}, mod: nil}, - %CallInfo{arity: 0, func: :integer, position: {3, 44}, mod: nil}, - %CallInfo{arity: 0, func: :integer, position: {3, 26}, mod: nil}, - ] - } + 2 => [ + %CallInfo{arity: 0, func: :integer, position: {2, 84}, mod: nil}, + %CallInfo{arity: 0, func: :integer, position: {2, 72}, mod: nil}, + %CallInfo{arity: 0, func: :integer, position: {2, 56}, mod: nil}, + %CallInfo{arity: 0, func: :integer, position: {2, 38}, mod: nil} + ], + 3 => [ + %CallInfo{arity: 0, func: :integer, position: {3, 61}, mod: nil}, + %CallInfo{arity: 0, func: :integer, position: {3, 44}, mod: nil}, + %CallInfo{arity: 0, func: :integer, position: {3, 26}, mod: nil} + ] + } end test "registers calls local no arg" do @@ -7036,7 +6951,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls[5] == [%CallInfo{arity: 2, position: {5, 5}, func: :test, mod: nil}] + assert state.calls[5] == [%CallInfo{arity: 2, position: {5, 5}, func: :test, mod: nil}] end test "registers super capture expression" do @@ -7052,7 +6967,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert [_, %CallInfo{arity: 2, position: {5, 20}, func: :test, mod: nil}, _] = state.calls[5] + assert [_, %CallInfo{arity: 2, position: {5, 20}, func: :test, mod: nil}, _] = + state.calls[5] end test "registers super capture" do @@ -7068,7 +6984,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert [_, %CallInfo{arity: 2, position: {5, 31}, func: :test, mod: nil}, _] = state.calls[5] + assert [_, %CallInfo{arity: 2, position: {5, 31}, func: :test, mod: nil}, _] = + state.calls[5] end test "registers calls capture operator __MODULE__" do From f7e5305778f921e40a133b1176793a15c31884d6 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 18 Jul 2024 08:12:33 +0200 Subject: [PATCH 088/235] removing dead code --- lib/elixir_sense/core/compiler.ex | 3 + lib/elixir_sense/core/introspection.ex | 142 -- lib/elixir_sense/core/metadata_builder.ex | 998 ++------ lib/elixir_sense/core/normalized/macro/env.ex | 14 +- lib/elixir_sense/core/state.ex | 553 +---- .../core/metadata_builder_test.exs | 2141 ++++++++--------- 6 files changed, 1330 insertions(+), 2521 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 1513ed69..1c82b1f3 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -863,6 +863,7 @@ defmodule ElixirSense.Core.Compiler do {opts, state, env} = expand(opts, state, env) target = Kernel.Utils.defdelegate_all(funs, opts, env) + # TODO: Remove List.wrap when multiple funs are no longer supported state = funs @@ -874,6 +875,7 @@ defmodule ElixirSense.Core.Compiler do state |> add_current_env_to_line(line, %{env | context: nil, function: {name, arity}}) + # TODO use add_func_to_index to collect docs |> add_mod_fun_to_position( {module, name, arity}, position, @@ -881,6 +883,7 @@ defmodule ElixirSense.Core.Compiler do args, :defdelegate, "", + # TODO # doc, %{delegate_to: {target, as, length(as_args)}}, # meta diff --git a/lib/elixir_sense/core/introspection.ex b/lib/elixir_sense/core/introspection.ex index e1bd77a4..2c4c87de 100644 --- a/lib/elixir_sense/core/introspection.ex +++ b/lib/elixir_sense/core/introspection.ex @@ -1137,148 +1137,6 @@ defmodule ElixirSense.Core.Introspection do def is_function_type(type), do: type in [:def, :defp, :defdelegate] def is_macro_type(type), do: type in [:defmacro, :defmacrop, :defguard, :defguardp] - def expand_import({functions, macros}, module, opts, mods_funs) do - opts = - if Keyword.keyword?(opts) do - opts - else - [] - end - - {all_exported_functions, all_exported_macros} = - if (functions[module] != nil or macros[module] != nil) and Keyword.keyword?(opts[:except]) do - {functions[module], macros[module]} - else - if Map.has_key?(mods_funs, {module, nil, nil}) do - funs_macros = - mods_funs - |> Enum.filter(fn {{m, _f, a}, info} -> - m == module and a != nil and is_pub(info.type) - end) - |> Enum.split_with(fn {_, info} -> is_macro_type(info.type) end) - - functions = - funs_macros - |> elem(1) - |> Enum.flat_map(fn {{_m, f, _a}, info} -> - for {arity, default_args} <- State.ModFunInfo.get_arities(info), - args <- (arity - default_args)..arity do - {f, args} - end - end) - - macros = - funs_macros - |> elem(0) - |> Enum.flat_map(fn {{_m, f, _a}, info} -> - for {arity, default_args} <- State.ModFunInfo.get_arities(info), - args <- (arity - default_args)..arity do - {f, args} - end - end) - - {functions, macros} - else - {macros, functions} = - get_exports(module) - |> Enum.split_with(fn {_, {_, kind}} -> kind == :macro end) - - {functions |> Enum.map(fn {f, {a, _}} -> {f, a} end), - macros |> Enum.map(fn {f, {a, _}} -> {f, a} end)} - end - end - - imported_functions = - all_exported_functions - |> Enum.reject(fn - {:__info__, 1} -> - true - - {:module_info, arity} when arity in [0, 1] -> - true - - {:behaviour_info, 1} -> - if Version.match?(System.version(), ">= 1.15.0-dev") do - true - else - # elixir < 1.15 imports behaviour_info from erlang behaviours - # https://github.com/elixir-lang/elixir/commit/4b26edd8c164b46823e1dc1ec34b639cc3563246 - elixir_module?(module) - end - - {:orelse, 2} -> - module == :erlang - - {:andalso, 2} -> - module == :erlang - - {name, arity} -> - reject_import(name, arity, :function, opts) - end) - - imported_macros = - all_exported_macros - |> Enum.reject(fn - {name, arity} -> - reject_import(name, arity, :macro, opts) - end) - - functions = - case imported_functions do - [] -> - Keyword.delete(functions, module) - - _ -> - Keyword.put(functions, module, imported_functions) - end - - macros = - case imported_macros do - [] -> - Keyword.delete(macros, module) - - _ -> - Keyword.put(macros, module, imported_macros) - end - - {functions, macros} - end - - defp reject_import(name, arity, kind, opts) do - name_string = name |> Atom.to_string() - - rejected_after_only? = - cond do - opts[:only] == :sigils and not String.starts_with?(name_string, "sigil_") -> - true - - opts[:only] == :macros and kind != :macro -> - true - - opts[:only] == :functions and kind != :function -> - true - - Keyword.keyword?(opts[:only]) -> - {name, arity} not in opts[:only] - - String.starts_with?(name_string, "_") -> - true - - true -> - false - end - - if rejected_after_only? do - true - else - if Keyword.keyword?(opts[:except]) do - {name, arity} in opts[:except] - else - false - end - end - end - def combine_imports({functions, macros}) do Enum.reduce(functions, macros, fn {module, imports}, acc -> case acc[module] do diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index fb4ee224..a5f1c3f8 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -4,23 +4,10 @@ defmodule ElixirSense.Core.MetadataBuilder do """ import ElixirSense.Core.State - import ElixirSense.Log - import ElixirSense.Core.TypeInference - alias ElixirSense.Core.Source alias ElixirSense.Core.State - # alias ElixirSense.Core.TypeInfo - alias ElixirSense.Core.Guard alias ElixirSense.Core.Compiler - @scope_keywords [:for, :fn, :with] - @block_keywords [:do, :else, :rescue, :catch, :after] - @defs [:def, :defp, :defmacro, :defmacrop, :defdelegate, :defguard, :defguardp] - - defguardp is_call(call, params) - when is_atom(call) and is_list(params) and - call not in [:., :__aliases__, :"::", :{}, :|>, :%, :%{}] - @doc """ Traverses the AST building/retrieving the environment information. It returns a `ElixirSense.Core.State` struct containing the information. @@ -31,812 +18,291 @@ defmodule ElixirSense.Core.MetadataBuilder do state |> remove_attributes_scope - |> remove_lexical_scope |> remove_vars_scope |> remove_module - |> remove_protocol_implementation - end - - defp safe_call_pre(ast, [state = %State{} | _] = states) do - try do - # if operation == :pre do - # dbg(ast) - # end - {ast_after_pre, state_after_pre} = pre(ast, state) - {ast_after_pre, [state_after_pre | states]} - rescue - exception -> - # reraise(exception, __STACKTRACE__) - warn( - Exception.format( - :error, - "#{inspect(exception.__struct__)} during metadata build pre:\n" <> - "#{Exception.message(exception)}\n" <> - "ast node: #{inspect(ast, limit: :infinity)}", - __STACKTRACE__ - ) - ) - - {nil, [:error | states]} - end - end - - defp safe_call_post(ast, [:error | states]) do - {ast, states} - end - - defp safe_call_post(ast_after_pre, [state_after_pre = %State{} | states]) do - try do - # if operation == :pre do - # dbg(ast_after_pre) - # end - {ast_after_post, state_after_post} = post(ast_after_pre, state_after_pre) - {ast_after_post, [state_after_post | tl(states)]} - rescue - exception -> - warn( - Exception.format( - :error, - "#{inspect(exception.__struct__)} during metadata build post:\n" <> - "#{Exception.message(exception)}\n" <> - "ast node: #{inspect(ast_after_pre, limit: :infinity)}", - __STACKTRACE__ - ) - ) - - {nil, states} - end - end - - defp pre_func({type, meta, ast_args}, state, meta, name, params, options \\ []) - when is_atom(name) do - vars = find_vars(params, nil) - - _vars = - if options[:guards], - do: infer_type_from_guards(options[:guards], vars, state), - else: vars - - {position, end_position} = extract_range(meta) - - options = Keyword.put(options, :generated, state.generated) - - ast = {type, Keyword.put(meta, :func, true), ast_args} - - env = nil - - state - |> new_named_func(name, length(params || [])) - |> add_func_to_index(env, name, params || [], position, end_position, type, options) - |> new_lexical_scope - |> new_func_vars_scope - # |> add_vars(vars, true) - # |> add_current_env_to_line(Keyword.fetch!(meta, :line)) - |> result(ast) - end - - defp extract_range(meta) do - position = { - Keyword.fetch!(meta, :line), - Keyword.fetch!(meta, :column) - } - - end_position = - case meta[:end] do - nil -> - case meta[:end_of_expression] do - nil -> - nil - - end_of_expression_meta -> - { - Keyword.fetch!(end_of_expression_meta, :line), - Keyword.fetch!(end_of_expression_meta, :column) - } - end - - end_meta -> - { - Keyword.fetch!(end_meta, :line), - Keyword.fetch!(end_meta, :column) + 3 - } - end - - {position, end_position} - end - - defp post_func(ast, state) do - # dbg(ast) - state - |> remove_lexical_scope - |> remove_func_vars_scope - |> remove_last_scope_from_scopes - |> result(ast) - end - - defp pre_scope_keyword(ast, state, _line) do - state = - case ast do - {:for, _, _} -> - state |> push_binding_context(:for) - - _ -> - state - end - - state - # |> add_current_env_to_line(line) - |> new_vars_scope - |> result(ast) - end - - defp post_scope_keyword(ast, state) do - state = - case ast do - {:for, _, _} -> - state |> pop_binding_context - - _ -> - state - end - - state - |> maybe_move_vars_to_outer_scope - |> remove_vars_scope - |> result(ast) - end - - defp pre_block_keyword(ast, state) do - state = - case ast do - {:rescue, _} -> - state |> push_binding_context(:rescue) - - _ -> - state - end - - state - |> new_lexical_scope - |> new_vars_scope - |> result(ast) - end - - defp post_block_keyword(ast, state) do - state = - case ast do - {:rescue, _} -> - state |> pop_binding_context - - _ -> - state - end - - state - |> remove_lexical_scope - |> maybe_move_vars_to_outer_scope - |> remove_vars_scope - |> result(ast) - end - - defp pre_clause({_clause, _meta, _} = ast, state, lhs) do - _vars = find_vars(lhs, Enum.at(state.binding_context, 0)) - - state - |> new_lexical_scope - |> new_vars_scope - # |> add_vars(vars, true) - # |> add_current_env_to_line(line) - |> result(ast) - end - - defp post_clause(ast, state) do - state - |> remove_lexical_scope - |> maybe_move_vars_to_outer_scope - |> remove_vars_scope - |> result(ast) - end - - defp post_string_literal(ast, _state, _line, str) do - str - |> Source.split_lines() - |> Enum.with_index() - # |> Enum.reduce(state, fn {_s, i}, acc -> add_current_env_to_line(acc, line + i) end) - |> result(ast) - end - - # ex_unit describe - defp pre( - {:describe, meta, [name, _body]} = ast, - state = %{scopes: [atom | _]} - ) - when is_binary(name) and is_atom(atom) and atom != nil do - line = Keyword.fetch!(meta, :line) - column = Keyword.fetch!(meta, :column) - - state = - state - |> add_call_to_line({nil, :describe, 2}, {line, column}) - - %{state | context: Map.put(state.context, :ex_unit_describe, name)} - # |> add_current_env_to_line(line) - |> result(ast) - end - - # ex_unit not implemented test - defp pre( - {:test, meta, [name]}, - state = %{scopes: [atom | _]} - ) - when is_binary(name) and is_atom(atom) and atom != nil do - def_name = ex_unit_test_name(state, name) - line = Keyword.fetch!(meta, :line) - column = Keyword.fetch!(meta, :column) - - ast_without_params = {:def, meta, [{def_name, add_no_call([]), []}, [], []]} - - state = - state - |> add_call_to_line({nil, :test, 0}, {line, column}) - - pre_func(ast_without_params, state, meta, def_name, [{:_, [line: line, column: column], nil}]) - end - - # ex_unit test without context - defp pre( - {:test, meta, [name, body]}, - state = %{scopes: [atom | _]} - ) - when is_binary(name) and is_atom(atom) and atom != nil do - def_name = ex_unit_test_name(state, name) - line = Keyword.fetch!(meta, :line) - column = Keyword.fetch!(meta, :column) - - ast_without_params = {:def, meta, [{def_name, add_no_call([]), []}, [], body]} - - state = - state - |> add_call_to_line({nil, :test, 2}, {line, column}) - - pre_func(ast_without_params, state, meta, def_name, [{:_, [line: line, column: column], nil}]) - end - - # ex_unit test with context - defp pre( - {:test, meta, [name, param, body]}, - state = %{scopes: [atom | _]} - ) - when is_binary(name) and is_atom(atom) and atom != nil do - def_name = ex_unit_test_name(state, name) - line = Keyword.fetch!(meta, :line) - column = Keyword.fetch!(meta, :column) - - ast_without_params = {:def, meta, [{def_name, add_no_call([]), []}, [], body]} - - state = - state - |> add_call_to_line({nil, :test, 3}, {line, column}) - - pre_func(ast_without_params, state, meta, def_name, [param]) - end - - # ex_unit setup with context - defp pre( - {setup, meta, [param, body]}, - state = %{scopes: [atom | _]} - ) - when setup in [:setup, :setup_all] and is_atom(atom) and atom != nil do - line = Keyword.fetch!(meta, :line) - column = Keyword.fetch!(meta, :column) - - # NOTE this name is not 100% correct - ex_unit uses counters instead of line but it's too complicated - def_name = :"__ex_unit_#{setup}_#{line}" - ast_without_params = {:def, meta, [{def_name, add_no_call([]), []}, [], body]} - - state = - state - |> add_call_to_line({nil, setup, 2}, {line, column}) - - pre_func(ast_without_params, state, meta, def_name, [param]) - end - - # ex_unit setup without context - defp pre( - {setup, meta, [body]}, - state = %{scopes: [atom | _]} - ) - when setup in [:setup, :setup_all] and is_atom(atom) and atom != nil do - line = Keyword.fetch!(meta, :line) - column = Keyword.fetch!(meta, :column) - - # NOTE this name is not 100% correct - ex_unit uses counters instead of line but it's too complicated - def_name = :"__ex_unit_#{setup}_#{line}" - ast_without_params = {:def, meta, [{def_name, add_no_call([]), []}, [], body]} - - state = - state - |> add_call_to_line({nil, setup, 2}, {line, column}) - - pre_func(ast_without_params, state, meta, def_name, [{:_, [line: line, column: column], nil}]) - end - - # function head with guards - defp pre( - {def_name, meta, [{:when, _, [{name, meta2, params}, guards]}, body]}, - state - ) - when def_name in @defs and is_atom(name) do - ast_without_params = {def_name, meta, [{name, add_no_call(meta2), []}, guards, body]} - pre_func(ast_without_params, state, meta, name, params, guards: guards) - end - - defp pre( - {def_name, meta, [{name, meta2, params}, body]}, - state - ) - when def_name in @defs and is_atom(name) do - ast_without_params = {def_name, meta, [{name, add_no_call(meta2), []}, body]} - pre_func(ast_without_params, state, meta, name, params) - end - - # defguard and defguardp - defp pre( - {def_name, meta, - [ - {:when, _meta, [{name, meta2, params}, body]} - ]}, - state - ) - when def_name in @defs and is_atom(name) do - ast_without_params = {def_name, meta, [{name, add_no_call(meta2), []}, body]} - pre_func(ast_without_params, state, meta, name, params) - end - - # function head - defp pre({def_name, meta, [{name, meta2, params}]}, state) - when def_name in @defs and is_atom(name) do - ast_without_params = {def_name, meta, [{name, add_no_call(meta2), []}, nil]} - pre_func(ast_without_params, state, meta, name, params) - end - - # incomplete spec - # @callback my(integer) - defp pre( - {:@, _meta_attr, [{kind, _meta_kind, [{name, _meta_name, type_args}]} = _spec]}, - _state - ) - when kind in [:spec, :callback, :macrocallback] and is_atom(name) and - (is_nil(type_args) or is_list(type_args)) do - # pre_spec( - # {:@, meta_attr, [{kind, add_no_call(meta_kind), [{name, meta_name, type_args}]}]}, - # state, - # meta_attr, - # name, - # expand_aliases_in_ast(state, List.wrap(type_args)), - # expand_aliases_in_ast(state, spec), - # kind - # ) - end - - defp pre({atom, meta, [_ | _]} = ast, state) - when atom in @scope_keywords do - line = Keyword.fetch!(meta, :line) - pre_scope_keyword(ast, state, line) - end - - defp pre({atom, _block} = ast, state) when atom in @block_keywords do - pre_block_keyword(ast, state) - end - - defp pre({:->, meta, [[{:when, _, [_var, guards]} = lhs], rhs]}, state) do - pre_clause({:->, meta, [guards, rhs]}, state, lhs) - end - - defp pre({:->, meta, [[lhs], rhs]}, state) do - pre_clause({:->, meta, [:_, rhs]}, state, lhs) - end - - defp pre({:->, meta, [lhs, rhs]}, state) do - pre_clause({:->, meta, [:_, rhs]}, state, lhs) end - defp pre({atom, meta, [lhs, rhs]}, state) - when atom in [:=, :<-] do - result(state, {atom, meta, [lhs, rhs]}) - end - - defp pre({:when, meta, [lhs, rhs]}, state) do - _vars = find_vars(lhs, nil) - - state - # |> add_vars(vars, true) - |> result({:when, meta, [:_, rhs]}) - end - - defp pre( - {:case, meta, - [ - condition_ast, - [ - do: _clauses - ] - ]} = ast, - state - ) do - line = Keyword.fetch!(meta, :line) - column = Keyword.fetch!(meta, :column) + # defp post_string_literal(ast, _state, _line, str) do + # str + # |> Source.split_lines() + # |> Enum.with_index() + # # |> Enum.reduce(state, fn {_s, i}, acc -> add_current_env_to_line(acc, line + i) end) + # # |> result(ast) + # end - state - |> push_binding_context(get_binding_type(condition_ast)) - |> add_call_to_line({nil, :case, 2}, {line, column}) - # |> add_current_env_to_line(line) - |> result(ast) - end + # # ex_unit describe + # defp pre( + # {:describe, meta, [name, _body]} = ast, + # state = %{scopes: [atom | _]} + # ) + # when is_binary(name) and is_atom(atom) and atom != nil do + # line = Keyword.fetch!(meta, :line) + # column = Keyword.fetch!(meta, :column) - defp pre( - {{:., meta1, [{:__aliases__, _, [_ | _]} = module_expression, call]}, _meta, params} = - ast, - state - ) - when is_call(call, params) do - line = Keyword.fetch!(meta1, :line) - column = Keyword.fetch!(meta1, :column) - - try do - # TODO pass env - {module, state, _env} = expand(module_expression, state) - - shift = if state.generated, do: 0, else: 1 - - state - |> add_call_to_line({module, call, length(params)}, {line, column + shift}) - # |> add_current_env_to_line(line) - |> result(ast) - rescue - _ -> - # Module.concat can fail for invalid aliases - result(state, nil) - end - end + # state = + # state + # |> add_call_to_line({nil, :describe, 2}, {line, column}) - defp pre( - {{:., meta1, [{:__MODULE__, _, nil}, call]}, _meta, params} = ast, - state - ) - when is_call(call, params) do - line = Keyword.fetch!(meta1, :line) - column = Keyword.fetch!(meta1, :column) - module = get_current_module(state) + # %{state | context: Map.put(state.context, :ex_unit_describe, name)} + # # |> add_current_env_to_line(line) + # # |> result(ast) + # end - shift = if state.generated, do: 0, else: 1 + # # ex_unit not implemented test + # defp pre( + # {:test, meta, [name]}, + # state = %{scopes: [atom | _]} + # ) + # when is_binary(name) and is_atom(atom) and atom != nil do + # def_name = ex_unit_test_name(state, name) + # line = Keyword.fetch!(meta, :line) + # column = Keyword.fetch!(meta, :column) - state - |> add_call_to_line({module, call, length(params)}, {line, column + shift}) - # |> add_current_env_to_line(line) - |> result(ast) - end + # _ast_without_params = {:def, meta, [{def_name, add_no_call([]), []}, [], []]} - defp pre( - {{:., meta1, [{:@, _, [{attribute, _, nil}]}, call]}, _meta, params} = ast, - state - ) - when is_call(call, params) and is_atom(attribute) do - line = Keyword.fetch!(meta1, :line) - column = Keyword.fetch!(meta1, :column) + # _state = + # state + # |> add_call_to_line({nil, :test, 0}, {line, column}) - shift = if state.generated, do: 0, else: 1 + # # pre_func(ast_without_params, state, meta, def_name, [{:_, [line: line, column: column], nil}]) + # end - state - |> add_call_to_line({{:attribute, attribute}, call, length(params)}, {line, column + shift}) - # |> add_current_env_to_line(line) - |> result(ast) - end + # # ex_unit test without context + # defp pre( + # {:test, meta, [name, body]}, + # state = %{scopes: [atom | _]} + # ) + # when is_binary(name) and is_atom(atom) and atom != nil do + # def_name = ex_unit_test_name(state, name) + # line = Keyword.fetch!(meta, :line) + # column = Keyword.fetch!(meta, :column) - defp pre( - {{:., meta1, [{:@, _, [{attribute, _, nil}]}]}, _meta, params} = ast, - state - ) - when is_atom(attribute) do - line = Keyword.fetch!(meta1, :line) - column = Keyword.fetch!(meta1, :column) + # _ast_without_params = {:def, meta, [{def_name, add_no_call([]), []}, [], body]} - shift = if state.generated, do: 0, else: 1 + # _state = + # state + # |> add_call_to_line({nil, :test, 2}, {line, column}) - state - |> add_call_to_line({nil, {:attribute, attribute}, length(params)}, {line, column + shift}) - # |> add_current_env_to_line(line) - |> result(ast) - end + # # pre_func(ast_without_params, state, meta, def_name, [{:_, [line: line, column: column], nil}]) + # end - defp pre( - {{:., meta1, [{variable, _var_meta, nil}]}, _meta, params} = ast, - state - ) - when is_atom(variable) do - line = Keyword.fetch!(meta1, :line) - column = Keyword.fetch!(meta1, :column) + # # ex_unit test with context + # defp pre( + # {:test, meta, [name, _param, body]}, + # state = %{scopes: [atom | _]} + # ) + # when is_binary(name) and is_atom(atom) and atom != nil do + # def_name = ex_unit_test_name(state, name) + # line = Keyword.fetch!(meta, :line) + # column = Keyword.fetch!(meta, :column) - shift = if state.generated, do: 0, else: 1 + # _ast_without_params = {:def, meta, [{def_name, add_no_call([]), []}, [], body]} - state - |> add_call_to_line({nil, {:variable, variable}, length(params)}, {line, column + shift}) - # |> add_current_env_to_line(line) - |> result(ast) - end + # _state = + # state + # |> add_call_to_line({nil, :test, 3}, {line, column}) - defp pre( - {{:., meta1, [{variable, _var_meta, nil}, call]}, _meta, params} = ast, - state - ) - when is_call(call, params) and is_atom(variable) do - line = Keyword.fetch!(meta1, :line) - column = Keyword.fetch!(meta1, :column) + # # pre_func(ast_without_params, state, meta, def_name, [param]) + # end - shift = if state.generated, do: 0, else: 1 + # # ex_unit setup with context + # defp pre( + # {setup, meta, [_param, body]}, + # state = %{scopes: [atom | _]} + # ) + # when setup in [:setup, :setup_all] and is_atom(atom) and atom != nil do + # line = Keyword.fetch!(meta, :line) + # column = Keyword.fetch!(meta, :column) - state - |> add_call_to_line({{:variable, variable}, call, length(params)}, {line, column + shift}) - # |> add_current_env_to_line(line) - |> result(ast) - end + # # NOTE this name is not 100% correct - ex_unit uses counters instead of line but it's too complicated + # def_name = :"__ex_unit_#{setup}_#{line}" + # _ast_without_params = {:def, meta, [{def_name, add_no_call([]), []}, [], body]} - defp pre( - {{:., meta1, [module, call]}, _meta, params} = ast, - state - ) - when is_atom(module) and is_call(call, params) do - line = Keyword.fetch!(meta1, :line) - column = Keyword.fetch!(meta1, :column) + # _state = + # state + # |> add_call_to_line({nil, setup, 2}, {line, column}) - shift = if state.generated, do: 0, else: 1 + # # pre_func(ast_without_params, state, meta, def_name, [param]) + # end - state - |> add_call_to_line({module, call, length(params)}, {line, column + shift}) - # |> add_current_env_to_line(line) - |> result(ast) - end + # # ex_unit setup without context + # defp pre( + # {setup, meta, [body]}, + # state = %{scopes: [atom | _]} + # ) + # when setup in [:setup, :setup_all] and is_atom(atom) and atom != nil do + # line = Keyword.fetch!(meta, :line) + # column = Keyword.fetch!(meta, :column) - defp pre({call, meta, params} = ast, state) - when is_call(call, params) do - case Keyword.get(meta, :line) do - nil -> - {ast, state} - - _ -> - line = Keyword.fetch!(meta, :line) - - # credo:disable-for-next-line - if not Keyword.get(meta, :no_call, false) do - column = Keyword.fetch!(meta, :column) - - state = - if String.starts_with?(to_string(call), "__atom_elixir_marker_") do - state - else - add_call_to_line(state, {nil, call, length(params)}, {line, column}) - end - - state - # |> add_current_env_to_line(line) - |> result(ast) - else - state - # |> add_current_env_to_line(line) - |> result(ast) - end - end - end + # # NOTE this name is not 100% correct - ex_unit uses counters instead of line but it's too complicated + # def_name = :"__ex_unit_#{setup}_#{line}" + # _ast_without_params = {:def, meta, [{def_name, add_no_call([]), []}, [], body]} - # Any other tuple with a line - defp pre({_, meta, _} = ast, state) do - case Keyword.get(meta, :line) do - nil -> - {ast, state} - - _line -> - state - # |> add_current_env_to_line(line) - |> result(ast) - end - end + # state = + # state + # |> add_call_to_line({nil, setup, 2}, {line, column}) - defp pre(ast, state) do - {ast, state} - end + # # pre_func(ast_without_params, state, meta, def_name, [{:_, [line: line, column: column], nil}]) + # end - # ex_unit describe - defp post( - {:describe, _meta, [name, _body]} = ast, - state - ) - when is_binary(name) do - %{state | context: Map.delete(state.context, :ex_unit_describe)} - |> result(ast) - end + # # incomplete spec + # # @callback my(integer) + # defp pre( + # {:@, _meta_attr, [{kind, _meta_kind, [{name, _meta_name, type_args}]} = _spec]}, + # _state + # ) + # when kind in [:spec, :callback, :macrocallback] and is_atom(name) and + # (is_nil(type_args) or is_list(type_args)) do + # # pre_spec( + # # {:@, meta_attr, [{kind, add_no_call(meta_kind), [{name, meta_name, type_args}]}]}, + # # state, + # # meta_attr, + # # name, + # # expand_aliases_in_ast(state, List.wrap(type_args)), + # # expand_aliases_in_ast(state, spec), + # # kind + # # ) + # end - defp post({def_name, meta, [{name, _, _params} | _]} = ast, state) - when def_name in @defs and is_atom(name) do - if Keyword.get(meta, :func, false) do - post_func(ast, state) - else - {ast, state} - end - end + # defp pre({:when, _meta, [lhs, _rhs]}, state) do + # _vars = find_vars(lhs, nil) - defp post({def_name, _, _} = ast, state) when def_name in @defs do - {ast, state} - end + # state + # # |> add_vars(vars, true) + # # |> result({:when, meta, [:_, rhs]}) + # end - defp post( - {:@, _meta_attr, - [{kind, _, [{:"::", _meta, _params = [{name, _, type_args}, _type_def]} = _spec]}]} = - ast, - state - ) - when kind in [:type, :typep, :opaque] and is_atom(name) and - (is_nil(type_args) or is_list(type_args)) do - state = - state - |> remove_last_scope_from_scopes - - {ast, state} - end + # defp pre( + # {:case, meta, + # [ + # condition_ast, + # [ + # do: _clauses + # ] + # ]} = _ast, + # state + # ) do + # line = Keyword.fetch!(meta, :line) + # column = Keyword.fetch!(meta, :column) - defp post( - {:@, _meta_attr, - [ - {kind, _, - [ - {:when, _, [{:"::", _meta, _params = [{name, _, type_args}, _type_def]}, _]} = - _spec - ]} - ]} = ast, - state - ) - when kind in [:spec, :callback, :macrocallback] and is_atom(name) and - (is_nil(type_args) or is_list(type_args)) do - state = - state - |> remove_last_scope_from_scopes - - {ast, state} - end + # state + # |> push_binding_context(get_binding_type(condition_ast)) + # |> add_call_to_line({nil, :case, 2}, {line, column}) + # # |> add_current_env_to_line(line) + # # |> result(ast) + # end - defp post( - {:@, _meta_attr, - [{kind, _, [{:"::", _meta, _params = [{name, _, type_args}, _type_def]} = _spec]}]} = - ast, - state - ) - when kind in [:spec, :callback, :macrocallback] and is_atom(name) and - (is_nil(type_args) or is_list(type_args)) do - state = - state - |> remove_last_scope_from_scopes - - {ast, state} - end + # # Any other tuple with a line + # defp pre({_, meta, _} = ast, state) do + # case Keyword.get(meta, :line) do + # nil -> + # {ast, state} + + # _line -> + # state + # # |> add_current_env_to_line(line) + # # |> result(ast) + # end + # end - defp post( - {:case, _meta, - [ - _condition_ast, - [ - do: _clauses - ] - ]} = ast, - state - ) do - state - |> pop_binding_context - |> result(ast) - end + # defp pre(ast, state) do + # {ast, state} + # end - defp post({atom, _, [_ | _]} = ast, state) when atom in @scope_keywords do - post_scope_keyword(ast, state) - end + # # ex_unit describe + # defp post( + # {:describe, _meta, [name, _body]} = _ast, + # state + # ) + # when is_binary(name) do + # %{state | context: Map.delete(state.context, :ex_unit_describe)} + # # |> result(ast) + # end - defp post({atom, _block} = ast, state) when atom in @block_keywords do - post_block_keyword(ast, state) - end + # defp post({atom, meta, [lhs, rhs]} = _ast, state) + # when atom in [:=, :<-] do + # _line = Keyword.fetch!(meta, :line) + # match_context_r = get_binding_type(rhs) - defp post({:->, _meta, [_lhs, _rhs]} = ast, state) do - post_clause(ast, state) - end + # match_context_r = + # if atom == :<- and match?([:for | _], state.binding_context) do + # {:for_expression, match_context_r} + # else + # match_context_r + # end - defp post({:__generated__, _meta, inner}, state) do - {inner, %{state | generated: false}} - end + # vars_l = find_vars(lhs, match_context_r) - defp post({atom, meta, [lhs, rhs]} = ast, state) - when atom in [:=, :<-] do - _line = Keyword.fetch!(meta, :line) - match_context_r = get_binding_type(rhs) + # _vars = + # case rhs do + # {:=, _, [nested_lhs, _nested_rhs]} -> + # match_context_l = get_binding_type(lhs) + # nested_vars = find_vars(nested_lhs, match_context_l) - match_context_r = - if atom == :<- and match?([:for | _], state.binding_context) do - {:for_expression, match_context_r} - else - match_context_r - end + # vars_l ++ nested_vars - vars_l = find_vars(lhs, match_context_r) + # _ -> + # vars_l + # end - _vars = - case rhs do - {:=, _, [nested_lhs, _nested_rhs]} -> - match_context_l = get_binding_type(lhs) - nested_vars = find_vars(nested_lhs, match_context_l) + # state + # # |> remove_calls(remove_positions) + # # |> add_current_env_to_line(line) + # # |> result(ast) + # end - vars_l ++ nested_vars + # # String literal + # defp post({_, [no_call: true, line: line, column: _column], [str]} = ast, state) + # when is_binary(str) do + # post_string_literal(ast, state, line, str) + # end - _ -> - vars_l - end + # # String literal in sigils + # defp post({:<<>>, [indentation: _, line: line, column: _column], [str]} = ast, state) + # when is_binary(str) do + # post_string_literal(ast, state, line, str) + # end - state - # |> remove_calls(remove_positions) - # |> add_current_env_to_line(line) - |> result(ast) - end + # defp post(ast, state) do + # {ast, state} + # end - # String literal - defp post({_, [no_call: true, line: line, column: _column], [str]} = ast, state) - when is_binary(str) do - post_string_literal(ast, state, line, str) - end + # # defp find_vars(state, ast, match_context \\ nil) - # String literal in sigils - defp post({:<<>>, [indentation: _, line: line, column: _column], [str]} = ast, state) - when is_binary(str) do - post_string_literal(ast, state, line, str) - end + # # defp find_vars(_state, {var, _meta, nil}, _) + # # when var in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__] do + # # # TODO local calls? + # # [] + # # end - defp post(ast, state) do - {ast, state} - end + # # defp find_vars(_state, {var, meta, nil}, :rescue) when is_atom(var) do + # # line = Keyword.fetch!(meta, :line) + # # column = Keyword.fetch!(meta, :column) + # # match_context = {:struct, [], {:atom, Exception}, nil} + # # [%VarInfo{name: var, positions: [{line, column}], type: match_context, is_definition: true}] + # # end - defp result(state, ast) do - {ast, state} - end + # def infer_type_from_guards(guard_ast, vars, _state) do + # type_info = Guard.type_information_from_guards(guard_ast) - # defp find_vars(state, ast, match_context \\ nil) + # Enum.reduce(type_info, vars, fn {var, type}, acc -> + # index = Enum.find_index(acc, &(&1.name == var)) - # defp find_vars(_state, {var, _meta, nil}, _) - # when var in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__] do - # # TODO local calls? - # [] + # if index, + # do: List.update_at(acc, index, &Map.put(&1, :type, type)), + # else: acc + # end) # end - # defp find_vars(_state, {var, meta, nil}, :rescue) when is_atom(var) do - # line = Keyword.fetch!(meta, :line) - # column = Keyword.fetch!(meta, :column) - # match_context = {:struct, [], {:atom, Exception}, nil} - # [%VarInfo{name: var, positions: [{line, column}], type: match_context, is_definition: true}] + # defp add_no_call(meta) do + # [{:no_call, true} | meta] # end - def infer_type_from_guards(guard_ast, vars, _state) do - type_info = Guard.type_information_from_guards(guard_ast) - - Enum.reduce(type_info, vars, fn {var, type}, acc -> - index = Enum.find_index(acc, &(&1.name == var)) - - if index, - do: List.update_at(acc, index, &Map.put(&1, :type, type)), - else: acc - end) - end - - defp add_no_call(meta) do - [{:no_call, true} | meta] - end - - defp ex_unit_test_name(state, name) do - case state.context[:ex_unit_describe] do - nil -> "test #{name}" - describe -> "test #{describe} #{name}" - end - |> String.to_atom() - end + # defp ex_unit_test_name(state, name) do + # case state.context[:ex_unit_describe] do + # nil -> "test #{name}" + # describe -> "test #{describe} #{name}" + # end + # |> String.to_atom() + # end end diff --git a/lib/elixir_sense/core/normalized/macro/env.ex b/lib/elixir_sense/core/normalized/macro/env.ex index 42b4b813..23313988 100644 --- a/lib/elixir_sense/core/normalized/macro/env.ex +++ b/lib/elixir_sense/core/normalized/macro/env.ex @@ -61,12 +61,8 @@ defmodule ElixirSense.Core.Normalized.Macro.Env do end end - defp wrap_expansion(receiver, expander, meta, name, arity, env, opts) do + defp wrap_expansion(receiver, expander, _meta, _name, _arity, env, _opts) do fn expansion_meta, args -> - if Keyword.get(opts, :check_deprecations, true) do - :elixir_dispatch.check_deprecated(:macro, meta, receiver, name, arity, env) - end - quoted = expander.(args, env) next = :elixir_module.next_counter(env.module) :elixir_quote.linify_with_context_counter(expansion_meta, {receiver, next}, quoted) @@ -330,10 +326,10 @@ defmodule ElixirSense.Core.Normalized.Macro.Env do :ok when except == false -> only = ensure_no_duplicates(dup_only, :only, meta, e, warn) - {added1, used1, funs} = + {added1, _used1, funs} = import_listed_functions(meta, ref, only, e, warn, info_callback) - {added2, used2, macs} = + {added2, _used2, macs} = import_listed_macros(meta, ref, only, e, warn, info_callback) # for {name, arity} <- (only -- used1) -- used2, warn, do: elixir_errors.file_warn(meta, e, __MODULE__, {:invalid_import, {ref, name, arity}}) @@ -586,7 +582,7 @@ defmodule ElixirSense.Core.Normalized.Macro.Env do expand_require(required, meta, receiver, name, arity, e, trace) end - defp expand_require(required, meta, receiver, name, arity, e, trace) do + defp expand_require(required, meta, receiver, name, arity, e, _trace) do if is_macro(name, arity, receiver, required) do {:macro, receiver, expander_macro_named(meta, receiver, name, arity, e)} else @@ -605,7 +601,7 @@ defmodule ElixirSense.Core.Normalized.Macro.Env do fn args, caller -> expand_macro_fun(meta, fun, receiver, name, args, caller, e) end end - defp expand_macro_fun(meta, fun, receiver, name, args, caller, e) do + defp expand_macro_fun(_meta, fun, _receiver, _name, args, caller, _e) do apply(fun, [caller | args]) end diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index f0222112..8235af56 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -321,15 +321,7 @@ defmodule ElixirSense.Core.State do def private?(%ModFunInfo{type: type}), do: type in [:defp, :defmacrop, :defguardp] end - def current_aliases(%__MODULE__{} = state) do - state.aliases |> List.flatten() |> Enum.uniq_by(&elem(&1, 0)) |> Enum.reverse() - end - - def current_requires(%__MODULE__{} = state) do - state.requires |> :lists.reverse() |> List.flatten() |> Enum.uniq() |> Enum.sort() - end - - def get_current_env(%__MODULE__{} = state, macro_env) do + defp get_current_env(%__MODULE__{} = state, macro_env) do current_attributes = state |> get_current_attributes() current_behaviours = state.behaviours |> Map.get(macro_env.module, []) @@ -377,10 +369,6 @@ defmodule ElixirSense.Core.State do } end - def get_current_module(%__MODULE__{} = state) do - state.module |> hd - end - def add_cursor_env(%__MODULE__{} = state, meta, macro_env) do env = get_current_env(state, macro_env) %__MODULE__{state | cursor_env: {meta, env}} @@ -483,7 +471,7 @@ defmodule ElixirSense.Core.State do %__MODULE__{state | calls: calls} end - def add_struct(%__MODULE__{} = state, env, type, fields) do + defp add_struct(%__MODULE__{} = state, env, type, fields) do structs = state.structs |> Map.put(env.module, %StructInfo{type: type, fields: fields ++ [__struct__: env.module]}) @@ -491,10 +479,11 @@ defmodule ElixirSense.Core.State do %__MODULE__{state | structs: structs} end - def get_current_attributes(%__MODULE__{} = state) do + defp get_current_attributes(%__MODULE__{} = state) do state.scope_attributes |> :lists.reverse() |> List.flatten() end + # TODO make priv def add_mod_fun_to_position( %__MODULE__{} = state, {module, fun, arity}, @@ -555,16 +544,17 @@ defmodule ElixirSense.Core.State do end defp process_option( - state, + _state, info, :defdelegate, - {:target, {target_module_expression, target_function}} + {:target, {target_module, target_function}} ) do - {module, _state, _env} = expand(target_module_expression, state) + # TODO wtf + # {module, _state, _env} = expand(target_module_expression, state) %ModFunInfo{ info - | target: {module, target_function} + | target: {target_module, target_function} } end @@ -578,10 +568,6 @@ defmodule ElixirSense.Core.State do defp process_option(_state, info, _type, _option), do: info - def implementation_alias(protocol, [first | _]) do - Module.concat(protocol, first) - end - def add_module(%__MODULE__{} = state) do %__MODULE__{ state @@ -600,10 +586,6 @@ defmodule ElixirSense.Core.State do } end - def add_typespec_namespace(%__MODULE__{} = state, name, arity) do - %{state | scopes: [{:typespec, name, arity} | state.scopes]} - end - def register_optional_callbacks(%__MODULE__{} = state, list) do [_ | rest] = state.optional_callbacks_context %{state | optional_callbacks_context: [list | rest]} @@ -625,36 +607,6 @@ defmodule ElixirSense.Core.State do %{state | specs: updated_specs} end - def new_named_func(%__MODULE__{} = state, name, arity) do - %{state | scopes: [{name, arity} | state.scopes]} - end - - def maybe_add_protocol_implementation( - %__MODULE__{} = state, - protocol = {_protocol, _implementations} - ) do - %__MODULE__{state | protocols: [protocol | state.protocols]} - end - - def maybe_add_protocol_implementation(%__MODULE__{} = state, _) do - %__MODULE__{state | protocols: [nil | state.protocols]} - end - - def remove_protocol_implementation(%__MODULE__{} = state) do - %__MODULE__{state | protocols: tl(state.protocols)} - end - - def remove_last_scope_from_scopes(%__MODULE__{} = state) do - %__MODULE__{state | scopes: tl(state.scopes)} - end - - def add_current_module_to_index(%__MODULE__{} = state, position, end_position, options) - when (is_tuple(position) and is_tuple(end_position)) or is_nil(end_position) do - current_module = get_current_module(state) - - add_module_to_index(state, current_module, position, end_position, options) - end - def add_module_to_index(%__MODULE__{} = state, module, position, end_position, options) when (is_tuple(position) and is_tuple(end_position)) or is_nil(end_position) do # TODO :defprotocol, :defimpl? @@ -721,19 +673,19 @@ defmodule ElixirSense.Core.State do end end - meta = - if type == :defdelegate do - {target_module_expression, target_fun} = options[:target] - {module, _state, _env} = expand(target_module_expression, state) - - Map.put( - meta, - :delegate_to, - {module, target_fun, arity} - ) - else - meta - end + # meta = + # if type == :defdelegate do + # {target_module, target_fun} = options[:target] + # # {module, _state, _env} = expand(target_module_expression, state) + + # Map.put( + # meta, + # :delegate_to, + # {target_module, target_fun, arity} + # ) + # else + # meta + # end meta = if type in [:defguard, :defguardp] do @@ -743,7 +695,7 @@ defmodule ElixirSense.Core.State do end doc = - if type in [:defp, :defmacrop] do + if type in [:defp, :defmacrop, :defguardp] do # documentation is discarded on private "" else @@ -807,20 +759,6 @@ defmodule ElixirSense.Core.State do } end - def push_binding_context(%__MODULE__{} = state, binding_context) do - %__MODULE__{ - state - | binding_context: [binding_context | state.binding_context] - } - end - - def pop_binding_context(%__MODULE__{} = state) do - %__MODULE__{ - state - | binding_context: tl(state.binding_context) - } - end - def new_func_vars_scope(%__MODULE__{} = state) do scope_id = state.scope_id_count + 1 @@ -854,7 +792,7 @@ defmodule ElixirSense.Core.State do } end - def update_vars_info_per_scope_id(state) do + defp update_vars_info_per_scope_id(state) do [scope_id | _other_scope_ids] = state.scope_ids [current_scope_vars | _other_scope_vars] = state.vars_info @@ -874,121 +812,6 @@ defmodule ElixirSense.Core.State do %__MODULE__{state | attributes: attributes, scope_attributes: attributes} end - def add_alias(%__MODULE__{} = state, {alias, aliased}) when is_list(aliased) do - if Introspection.elixir_module?(alias) do - alias = Module.split(alias) |> Enum.take(-1) |> Module.concat() - [aliases_from_scope | inherited_aliases] = state.aliases - aliases_from_scope = aliases_from_scope |> Enum.reject(&match?({^alias, _}, &1)) - - # TODO pass env - {expanded, state, _env} = expand(aliased, state) - - aliases_from_scope = - if alias != expanded do - [{alias, expanded} | aliases_from_scope] - else - aliases_from_scope - end - - %__MODULE__{ - state - | aliases: [ - aliases_from_scope | inherited_aliases - ] - } - else - state - end - end - - def add_alias(%__MODULE__{} = state, {alias, aliased}) when is_atom(aliased) do - if Introspection.elixir_module?(alias) do - [aliases_from_scope | inherited_aliases] = state.aliases - aliases_from_scope = aliases_from_scope |> Enum.reject(&match?({^alias, _}, &1)) - - aliases_from_scope = - if alias != aliased do - [{alias, aliased} | aliases_from_scope] - else - aliases_from_scope - end - - %__MODULE__{ - state - | aliases: [ - aliases_from_scope | inherited_aliases - ] - } - else - [aliases_from_scope | inherited_aliases] = state.aliases - aliases_from_scope = aliases_from_scope |> Enum.reject(&match?({^alias, _}, &1)) - - %__MODULE__{ - state - | aliases: [ - [{Module.concat([alias]), aliased} | aliases_from_scope] | inherited_aliases - ] - } - end - end - - def add_alias(%__MODULE__{} = state, _), do: state - - def new_lexical_scope(%__MODULE__{} = state) do - %__MODULE__{ - state - | functions: [hd(state.functions) | state.functions], - macros: [hd(state.macros) | state.macros], - requires: [[] | state.requires], - aliases: [[] | state.aliases] - } - end - - def remove_lexical_scope(%__MODULE__{} = state) do - %__MODULE__{ - state - | functions: tl(state.functions), - macros: tl(state.macros), - requires: tl(state.requires), - aliases: tl(state.aliases) - } - end - - def add_import(%__MODULE__{} = state, module, opts) when is_atom(module) do - {functions, macros} = - Introspection.expand_import( - {hd(state.functions), hd(state.macros)}, - module, - opts, - state.mods_funs_to_positions - ) - - %__MODULE__{ - state - | functions: [functions | tl(state.functions)], - macros: [macros | tl(state.macros)] - } - end - - def add_import(%__MODULE__{} = state, _module, _opts), do: state - - def add_require(%__MODULE__{} = state, module) when is_atom(module) do - [requires_from_scope | inherited_requires] = state.requires - - current_requires = state.requires |> :lists.reverse() |> List.flatten() - - requires_from_scope = - if module in current_requires do - requires_from_scope - else - [module | requires_from_scope] - end - - %__MODULE__{state | requires: [requires_from_scope | inherited_requires]} - end - - def add_require(%__MODULE__{} = state, _module), do: state - def add_type( %__MODULE__{} = state, env, @@ -1163,12 +986,6 @@ defmodule ElixirSense.Core.State do @builtin_attributes ElixirSense.Core.BuiltinAttributes.all() - def add_attributes(%__MODULE__{} = state, attributes, position) do - Enum.reduce(attributes, state, fn attribute, state -> - add_attribute(state, attribute, nil, true, position) - end) - end - def add_attribute(%__MODULE__{} = state, attribute, type, is_definition, position) when attribute not in @builtin_attributes do [attributes_from_scope | other_attributes] = state.attributes @@ -1336,204 +1153,6 @@ defmodule ElixirSense.Core.State do def default_env, do: %ElixirSense.Core.State.Env{} - def expand(ast, %__MODULE__{} = state) do - expand(ast, state, nil) - end - - def expand({:@, meta, [{:behaviour, _, [arg]}]}, state, env) do - line = Keyword.fetch!(meta, :line) - - state = - state - |> add_current_env_to_line(line, env) - - {arg, state, env} = expand(arg, state, env) - add_behaviour(arg, state, env) - end - - def expand({:defoverridable, meta, [arg]}, state, env) do - {arg, state, env} = expand(arg, state, env) - - case arg do - keyword when is_list(keyword) -> - {nil, make_overridable(state, env, keyword, meta[:context]), env} - - behaviour_module when is_atom(behaviour_module) -> - if Code.ensure_loaded?(behaviour_module) and - function_exported?(behaviour_module, :behaviour_info, 1) do - keyword = - behaviour_module.behaviour_info(:callbacks) - |> Enum.map(&Introspection.drop_macro_prefix/1) - - {nil, make_overridable(state, env, keyword, meta[:context]), env} - else - {nil, state, env} - end - - _ -> - {nil, state, env} - end - end - - def expand({form, meta, [{{:., _, [base, :{}]}, _, refs} | rest]}, state, env) - when form in [:require, :alias, :import, :use] do - case rest do - [] -> - expand_multi_alias_call(form, meta, base, refs, [], state, env) - - [opts] -> - opts = Keyword.delete(opts, :as) - # if Keyword.has_key?(opts, :as) do - # raise "as_in_multi_alias_call" - # end - - expand_multi_alias_call(form, meta, base, refs, opts, state, env) - end - end - - def expand({form, meta, [arg]}, state, env) when form in [:require, :alias, :import] do - expand({form, meta, [arg, []]}, state, env) - end - - def expand(module, %__MODULE__{} = state, env) when is_atom(module) do - {module, state, env} - end - - def expand({:alias, meta, [arg, opts]}, state, env) do - line = Keyword.fetch!(meta, :line) - - {arg, state, env} = expand(arg, state, env) - # options = expand(no_alias_opts(arg), state, env, env) - - if is_atom(arg) do - state = add_first_alias_positions(state, env, meta) - - alias_tuple = - case Keyword.get(opts, :as) do - nil -> - {Module.concat([List.last(Module.split(arg))]), arg} - - as -> - # alias with `as:` option - {no_alias_expansion(as), arg} - end - - state = - state - |> add_current_env_to_line(line, env) - |> add_alias(alias_tuple) - - {arg, state, env} - else - {nil, state, env} - end - rescue - ArgumentError -> {nil, state, env} - end - - def expand({:require, meta, [arg, opts]}, state, env) do - line = Keyword.fetch!(meta, :line) - - {arg, state, env} = expand(arg, state, env) - # opts = expand(no_alias_opts(opts), state, env) - - if is_atom(arg) do - state = - state - |> add_current_env_to_line(line, env) - - state = - case Keyword.get(opts, :as) do - nil -> - state - - as -> - # require with `as:` option - alias_tuple = {no_alias_expansion(as), arg} - add_alias(state, alias_tuple) - end - |> add_require(arg) - - {arg, state, env} - else - {nil, state, env} - end - end - - def expand({:import, meta, [arg, opts]}, state, env) do - line = Keyword.fetch!(meta, :line) - - {arg, state, env} = expand(arg, state, env) - # opts = expand(no_alias_opts(opts), state, env) - - if is_atom(arg) do - state = - state - |> add_current_env_to_line(line, env) - |> add_require(arg) - |> add_import(arg, opts) - - {arg, state, env} - else - {nil, state, env} - end - end - - def expand({:use, _meta, []} = ast, state, env) do - # defmacro use in Kernel - {ast, state, env} - end - - def expand({:use, meta, [_ | _]} = ast, state, env) do - alias ElixirSense.Core.MacroExpander - line = Keyword.fetch!(meta, :line) - - state = - state - |> add_current_env_to_line(line, env) - - # TODO pass env - expanded_ast = - ast - |> MacroExpander.add_default_meta() - |> MacroExpander.expand_use( - env.module, - env.aliases, - meta |> Keyword.take([:line, :column]) - ) - - {{:__generated__, [], [expanded_ast]}, %{state | generated: true}, env} - end - - def expand( - {:__aliases__, _, [Elixir | _] = module}, - %__MODULE__{} = state, - env - ) do - {Module.concat(module), state, env} - end - - def expand({:__MODULE__, _, nil}, %__MODULE__{} = state, env) do - {env.module, state, env} - end - - def expand( - {:__aliases__, _, [{:__MODULE__, _, nil} | rest]}, - %__MODULE__{} = state, - env - ) do - {Module.concat([env.module | rest]), state, env} - end - - def expand({:__aliases__, _, module}, %__MODULE__{} = state, env) - when is_list(module) do - {Introspection.expand_alias(Module.concat(module), env.aliases), state, env} - end - - def expand(ast, %__MODULE__{} = state, env) do - {ast, state, env} - end - def maybe_move_vars_to_outer_scope( %__MODULE__{vars_info: [current_scope_vars, outer_scope_vars | other_scopes_vars]} = state ) do @@ -1550,74 +1169,6 @@ defmodule ElixirSense.Core.State do def maybe_move_vars_to_outer_scope(state), do: state - def no_alias_expansion({:__aliases__, _, [h | t]} = _aliases) when is_atom(h) do - Module.concat([h | t]) - end - - def no_alias_expansion(other), do: other - - def alias_defmodule({:__aliases__, _, [Elixir, h | t]}, module, state, env) do - if t == [] and Version.match?(System.version(), "< 1.16.0-dev") do - # on elixir < 1.16 unaliasing is happening - # https://github.com/elixir-lang/elixir/issues/12456 - alias = String.to_atom("Elixir." <> Atom.to_string(h)) - state = add_alias(state, {alias, module}) - {module, state, env} - else - {module, state, env} - end - end - - # defmodule Alias in root - def alias_defmodule({:__aliases__, _, _}, module, state, %{module: nil} = env) do - {module, state, env} - end - - # defmodule Alias nested - def alias_defmodule({:__aliases__, _meta, [h | t]}, _module, state, env) when is_atom(h) do - module = Module.concat([env.module, h]) - alias = String.to_atom("Elixir." <> Atom.to_string(h)) - # {:ok, env} = Macro.Env.define_alias(env, meta, module, as: alias, trace: false) - state = add_alias(state, {alias, module}) - - case t do - [] -> {module, state, env} - _ -> {String.to_atom(Enum.join([module | t], ".")), state, env} - end - end - - # defmodule _ - def alias_defmodule(_raw, module, state, env) do - {module, state, env} - end - - defp expand_multi_alias_call(kind, meta, base, refs, opts, state, env) do - {base_ref, state, env} = expand(base, state, env) - - fun = fn - {:__aliases__, _, ref}, state, env -> - expand({kind, meta, [Module.concat([base_ref | ref]), opts]}, state, env) - - ref, state, env when is_atom(ref) -> - expand({kind, meta, [Module.concat([base_ref, ref]), opts]}, state, env) - - _other, s, e -> - {nil, s, e} - # raise "expected_compile_time_module" - end - - map_fold(fun, state, env, refs) - end - - defp map_fold(fun, s, e, list), do: map_fold(fun, s, e, list, []) - - defp map_fold(fun, s, e, [h | t], acc) do - {rh, rs, re} = fun.(h, s, e) - map_fold(fun, rs, re, t, [rh | acc]) - end - - defp map_fold(_fun, s, e, [], acc), do: {Enum.reverse(acc), s, e} - @module_functions [ {:__info__, [:atom], :def}, {:module_info, [], :def}, @@ -1644,56 +1195,6 @@ defmodule ElixirSense.Core.State do end) end - def macro_env(%__MODULE__{} = state, meta \\ []) do - function = - case hd(hd(state.scopes)) do - {function, arity} -> {function, arity} - _ -> nil - end - - context_modules = - state.mods_funs_to_positions - |> Enum.filter(&match?({{_module, nil, nil}, _}, &1)) - |> Enum.map(&(elem(&1, 0) |> elem(0))) - - %Macro.Env{ - aliases: current_aliases(state), - requires: current_requires(state), - module: get_current_module(state), - line: Keyword.get(meta, :line, 0), - function: function, - # TODO context :guard, :match - context: nil, - context_modules: context_modules, - functions: state.functions |> hd, - macros: state.macros |> hd - # TODO macro_aliases - # TODO versioned_vars - } - end - - def macro_env(%__MODULE__{} = state, %__MODULE__.Env{} = env, line) do - context_modules = - state.mods_funs_to_positions - |> Enum.filter(&match?({{_module, nil, nil}, _}, &1)) - |> Enum.map(&(elem(&1, 0) |> elem(0))) - - %Macro.Env{ - aliases: env.aliases, - requires: env.requires, - module: env.module, - line: line, - function: env.function, - # TODO context :guard, :match - context: nil, - context_modules: context_modules, - functions: env.functions, - macros: env.macros, - # TODO macro_aliases - versioned_vars: elem(state.vars, 0) - } - end - def with_typespec(%__MODULE__{} = state, typespec) do %{state | typespec: typespec} end @@ -1844,12 +1345,6 @@ defmodule ElixirSense.Core.State do def maybe_add_protocol_behaviour(state, env), do: {state, env} - def annotate_vars_with_inferred_types(state, vars_with_inferred_types) do - [h | t] = state.vars_info - h = Map.merge(h, vars_with_inferred_types) - %{state | vars_info: [h | t]} - end - def merge_inferred_types(state, []), do: state def merge_inferred_types(state, inferred_types) do diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index d2f61434..d6279800 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -6,12 +6,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do alias ElixirSense.Core.State alias ElixirSense.Core.State.{VarInfo, CallInfo, StructInfo, ModFunInfo, AttributeInfo} - @attribute_binding_support true or Version.match?(System.version(), "< 1.17.0-dev") @expand_eval false - @binding_support true or Version.match?(System.version(), "< 1.17.0-dev") - @typespec_calls_support true or Version.match?(System.version(), "< 1.17.0-dev") - @var_in_ex_unit Version.match?(System.version(), "< 1.17.0-dev") - @compiler Code.ensure_loaded?(ElixirSense.Core.Compiler) + @var_in_ex_unit false describe "versioned_vars" do test "in block" do @@ -1545,1001 +1541,998 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] end - if @binding_support do - test "variable binding simple case" do - state = - """ - var = :my_var - IO.puts("") - """ - |> string_to_state - - assert [%VarInfo{type: {:atom, :my_var}}] = state |> get_line_vars(2) - end + test "variable binding simple case" do + state = + """ + var = :my_var + IO.puts("") + """ + |> string_to_state - test "variable binding simple case match context" do - state = - """ - case x do - var = :my_var -> - IO.puts("") - end - """ - |> string_to_state + assert [%VarInfo{type: {:atom, :my_var}}] = state |> get_line_vars(2) + end - assert [%VarInfo{type: {:atom, :my_var}}] = state |> get_line_vars(3) - end + test "variable binding simple case match context" do + state = + """ + case x do + var = :my_var -> + IO.puts("") + end + """ + |> string_to_state - test "variable binding simple case match context reverse order" do - state = - """ - case x do - :my_var = var -> - IO.puts("") - end - """ - |> string_to_state + assert [%VarInfo{type: {:atom, :my_var}}] = state |> get_line_vars(3) + end - assert [%VarInfo{type: {:atom, :my_var}}] = state |> get_line_vars(3) - end + test "variable binding simple case match context reverse order" do + state = + """ + case x do + :my_var = var -> + IO.puts("") + end + """ + |> string_to_state - test "variable binding simple case match context guard" do - state = - """ - receive do - [v = :ok, var] when is_map(var) -> - IO.puts("") - end - """ - |> string_to_state + assert [%VarInfo{type: {:atom, :my_var}}] = state |> get_line_vars(3) + end - assert [%VarInfo{type: {:atom, :ok}}, %VarInfo{type: {:map, [], nil}}] = - state |> get_line_vars(3) - end + test "variable binding simple case match context guard" do + state = + """ + receive do + [v = :ok, var] when is_map(var) -> + IO.puts("") + end + """ + |> string_to_state - test "module attributes value binding to and from variables" do - state = - """ - defmodule MyModule do - @myattribute %{abc: String} - var = @myattribute - @other var - IO.puts "" - end - """ - |> string_to_state + assert [%VarInfo{type: {:atom, :ok}}, %VarInfo{type: {:map, [], nil}}] = + state |> get_line_vars(3) + end - assert get_line_attributes(state, 5) == [ - %AttributeInfo{ - name: :myattribute, - positions: [{2, 3}, {3, 9}], - type: {:map, [abc: {:atom, String}], nil} - }, - %AttributeInfo{ - name: :other, - positions: [{4, 3}], - type: {:variable, :var} - } - ] + test "module attributes value binding to and from variables" do + state = + """ + defmodule MyModule do + @myattribute %{abc: String} + var = @myattribute + @other var + IO.puts "" + end + """ + |> string_to_state - assert [ - %VarInfo{name: :var, type: {:attribute, :myattribute}} - ] = state |> get_line_vars(5) - end + assert get_line_attributes(state, 5) == [ + %AttributeInfo{ + name: :myattribute, + positions: [{2, 3}, {3, 9}], + type: {:map, [abc: {:atom, String}], nil} + }, + %AttributeInfo{ + name: :other, + positions: [{4, 3}], + type: {:variable, :var} + } + ] - test "variable rebinding" do - state = - """ - abc = 1 - some(abc) - abc = %Abc{cde: 1} - IO.puts "" - """ - |> string_to_state + assert [ + %VarInfo{name: :var, type: {:attribute, :myattribute}} + ] = state |> get_line_vars(5) + end - assert [ - %State.VarInfo{ - name: :abc, - type: {:struct, [cde: {:integer, 1}], {:atom, Abc}, nil}, - positions: [{3, 1}] - } - ] = state |> get_line_vars(4) - end + test "variable rebinding" do + state = + """ + abc = 1 + some(abc) + abc = %Abc{cde: 1} + IO.puts "" + """ + |> string_to_state - test "tuple destructuring" do - state = - """ - defmodule MyModule do - @myattribute {:ok, %{abc: nil}} - {:ok, var} = @myattribute - other = elem(@myattribute, 0) - IO.puts - q = {:a, :b, :c} - {_, _, q1} = q - IO.puts - end - """ - |> string_to_state + assert [ + %State.VarInfo{ + name: :abc, + type: {:struct, [cde: {:integer, 1}], {:atom, Abc}, nil}, + positions: [{3, 1}] + } + ] = state |> get_line_vars(4) + end - assert get_line_attributes(state, 4) == [ - %AttributeInfo{ - name: :myattribute, - positions: [{2, 3}, {3, 16}, {4, 16}], - type: {:tuple, 2, [{:atom, :ok}, {:map, [abc: {:atom, nil}], nil}]} - } - ] + test "tuple destructuring" do + state = + """ + defmodule MyModule do + @myattribute {:ok, %{abc: nil}} + {:ok, var} = @myattribute + other = elem(@myattribute, 0) + IO.puts + q = {:a, :b, :c} + {_, _, q1} = q + IO.puts + end + """ + |> string_to_state - assert [ - %VarInfo{ - name: :other, - # TODO do we need to rewrite? change Binding - # type: {:local_call, :elem, [{:attribute, :myattribute}, {:integer, 0}]} - type: { - :call, - {:atom, :erlang}, - :element, - [integer: 1, attribute: :myattribute] - } - }, - %VarInfo{ - name: :var, - type: - {:tuple_nth, - {:intersection, - [{:attribute, :myattribute}, {:tuple, 2, [{:atom, :ok}, nil]}]}, 1} - } - ] = state |> get_line_vars(5) + assert get_line_attributes(state, 4) == [ + %AttributeInfo{ + name: :myattribute, + positions: [{2, 3}, {3, 16}, {4, 16}], + type: {:tuple, 2, [{:atom, :ok}, {:map, [abc: {:atom, nil}], nil}]} + } + ] - assert [ - %VarInfo{ - name: :q, - type: {:tuple, 3, [{:atom, :a}, {:atom, :b}, {:atom, :c}]} - }, - %VarInfo{ - name: :q1, - type: - {:tuple_nth, - {:intersection, - [{:variable, :q}, {:tuple, 3, [{:variable, :_}, {:variable, :_}, nil]}]}, - 2} + assert [ + %VarInfo{ + name: :other, + # TODO do we need to rewrite? change Binding + # type: {:local_call, :elem, [{:attribute, :myattribute}, {:integer, 0}]} + type: { + :call, + {:atom, :erlang}, + :element, + [integer: 1, attribute: :myattribute] } - ] = - state - |> get_line_vars(8) - |> Enum.filter(&(&1.name |> Atom.to_string() |> String.starts_with?("q"))) - end + }, + %VarInfo{ + name: :var, + type: + {:tuple_nth, + {:intersection, + [{:attribute, :myattribute}, {:tuple, 2, [{:atom, :ok}, nil]}]}, 1} + } + ] = state |> get_line_vars(5) - test "list destructuring" do - state = - """ - defmodule MyModule do - @a [] - @myattribute [:ok, :error, :other] - @other1 [:some, :error | @myattribute] - @other2 [:some | @myattribute] - [var, _var1, _var2] = @myattribute - [other | rest] = @myattribute - [a] = @other - [b] = [] - IO.puts - end - """ - |> string_to_state + assert [ + %VarInfo{ + name: :q, + type: {:tuple, 3, [{:atom, :a}, {:atom, :b}, {:atom, :c}]} + }, + %VarInfo{ + name: :q1, + type: + {:tuple_nth, + {:intersection, + [{:variable, :q}, {:tuple, 3, [{:variable, :_}, {:variable, :_}, nil]}]}, 2} + } + ] = + state + |> get_line_vars(8) + |> Enum.filter(&(&1.name |> Atom.to_string() |> String.starts_with?("q"))) + end - assert get_line_attributes(state, 5) == [ - %AttributeInfo{ - name: :a, - positions: [{2, 3}], - type: {:list, :empty} - }, - %AttributeInfo{ - name: :myattribute, - positions: [{3, 3}, {4, 28}, {5, 20}], - type: {:list, {:atom, :ok}} - }, - %AttributeInfo{ - name: :other1, - positions: [{4, 3}], - type: {:list, {:atom, :some}} - }, - %AttributeInfo{name: :other2, positions: [{5, 3}], type: {:list, {:atom, :some}}} - ] + test "list destructuring" do + state = + """ + defmodule MyModule do + @a [] + @myattribute [:ok, :error, :other] + @other1 [:some, :error | @myattribute] + @other2 [:some | @myattribute] + [var, _var1, _var2] = @myattribute + [other | rest] = @myattribute + [a] = @other + [b] = [] + IO.puts + end + """ + |> string_to_state - assert [ - %VarInfo{ - name: :_var1, - type: {:list_head, {:list_tail, {:attribute, :myattribute}}} - }, - %VarInfo{ - name: :_var2, - type: {:list_head, {:list_tail, {:list_tail, {:attribute, :myattribute}}}} - }, - %VarInfo{ - name: :a, - type: {:list_head, {:attribute, :other}} - }, - %VarInfo{ - name: :b, - type: {:list_head, {:list, :empty}} - }, - %VarInfo{name: :other, type: {:list_head, {:attribute, :myattribute}}}, - %VarInfo{name: :rest, type: {:list_tail, {:attribute, :myattribute}}}, - %VarInfo{name: :var, type: {:list_head, {:attribute, :myattribute}}} - ] = state |> get_line_vars(10) - end + assert get_line_attributes(state, 5) == [ + %AttributeInfo{ + name: :a, + positions: [{2, 3}], + type: {:list, :empty} + }, + %AttributeInfo{ + name: :myattribute, + positions: [{3, 3}, {4, 28}, {5, 20}], + type: {:list, {:atom, :ok}} + }, + %AttributeInfo{ + name: :other1, + positions: [{4, 3}], + type: {:list, {:atom, :some}} + }, + %AttributeInfo{name: :other2, positions: [{5, 3}], type: {:list, {:atom, :some}}} + ] - test "list destructuring for" do - state = - """ - defmodule MyModule do - @myattribute [:ok, :error, :other] - for a <- @myattribute do - b = a - IO.puts - end + assert [ + %VarInfo{ + name: :_var1, + type: {:list_head, {:list_tail, {:attribute, :myattribute}}} + }, + %VarInfo{ + name: :_var2, + type: {:list_head, {:list_tail, {:list_tail, {:attribute, :myattribute}}}} + }, + %VarInfo{ + name: :a, + type: {:list_head, {:attribute, :other}} + }, + %VarInfo{ + name: :b, + type: {:list_head, {:list, :empty}} + }, + %VarInfo{name: :other, type: {:list_head, {:attribute, :myattribute}}}, + %VarInfo{name: :rest, type: {:list_tail, {:attribute, :myattribute}}}, + %VarInfo{name: :var, type: {:list_head, {:attribute, :myattribute}}} + ] = state |> get_line_vars(10) + end - for a <- @myattribute, a1 = @myattribute, a2 <- a1 do - b = a - IO.puts - end + test "list destructuring for" do + state = + """ + defmodule MyModule do + @myattribute [:ok, :error, :other] + for a <- @myattribute do + b = a + IO.puts end - """ - |> string_to_state - - assert [ - %VarInfo{name: :a, type: {:for_expression, {:attribute, :myattribute}}}, - %VarInfo{name: :b, type: {:variable, :a}} - ] = state |> get_line_vars(5) - assert [ - %VarInfo{name: :a, type: {:for_expression, {:attribute, :myattribute}}}, - %VarInfo{name: :a1, type: {:attribute, :myattribute}}, - %VarInfo{name: :a2, type: {:for_expression, {:variable, :a1}}}, - %VarInfo{name: :b, type: {:variable, :a}} - ] = state |> get_line_vars(10) - end - - test "map destructuring" do - state = - """ - defmodule MyModule do - @a %{} - @myattribute %{ok: :a, error: b, other: :c} - @other %{"a" => :a, "b" => b} - %{error: var1} = @myattribute - %{"a" => var2} = @other + for a <- @myattribute, a1 = @myattribute, a2 <- a1 do + b = a IO.puts end - """ - |> string_to_state + end + """ + |> string_to_state - assert [ - %VarInfo{ - name: :var1, - type: {:map_key, {:attribute, :myattribute}, {:atom, :error}} - }, - # TODO not atom keys currently not supported - %VarInfo{ - name: :var2, - type: {:map_key, {:attribute, :other}, nil} - } - ] = state |> get_line_vars(7) - end + assert [ + %VarInfo{name: :a, type: {:for_expression, {:attribute, :myattribute}}}, + %VarInfo{name: :b, type: {:variable, :a}} + ] = state |> get_line_vars(5) - test "map destructuring for" do - state = - """ - defmodule MyModule do - @myattribute %{ok: :a, error: b, other: :c} - for {k, v} <- @myattribute do - IO.puts - end - end - """ - |> string_to_state + assert [ + %VarInfo{name: :a, type: {:for_expression, {:attribute, :myattribute}}}, + %VarInfo{name: :a1, type: {:attribute, :myattribute}}, + %VarInfo{name: :a2, type: {:for_expression, {:variable, :a1}}}, + %VarInfo{name: :b, type: {:variable, :a}} + ] = state |> get_line_vars(10) + end - assert [ - %VarInfo{ - name: :k, - type: { - :tuple_nth, - { - :intersection, - [ - {:for_expression, {:attribute, :myattribute}}, - {:tuple, 2, [nil, {:variable, :v}]} - ] - }, - 0 - } - }, - %VarInfo{ - name: :v, - type: { - :tuple_nth, - { - :intersection, - [ - {:for_expression, {:attribute, :myattribute}}, - {:tuple, 2, [{:variable, :k}, nil]} - ] - }, - 1 - } - } - ] = state |> get_line_vars(4) - end + test "map destructuring" do + state = + """ + defmodule MyModule do + @a %{} + @myattribute %{ok: :a, error: b, other: :c} + @other %{"a" => :a, "b" => b} + %{error: var1} = @myattribute + %{"a" => var2} = @other + IO.puts + end + """ + |> string_to_state - test "struct destructuring" do - state = - """ - defmodule MyModule do - @a %My{} - @myattribute %My{ok: :a, error: b, other: :c} - %{error: var1} = @myattribute - %My{error: other} = @myattribute + assert [ + %VarInfo{ + name: :var1, + type: {:map_key, {:attribute, :myattribute}, {:atom, :error}} + }, + # TODO not atom keys currently not supported + %VarInfo{ + name: :var2, + type: {:map_key, {:attribute, :other}, nil} + } + ] = state |> get_line_vars(7) + end + + test "map destructuring for" do + state = + """ + defmodule MyModule do + @myattribute %{ok: :a, error: b, other: :c} + for {k, v} <- @myattribute do IO.puts end - """ - |> string_to_state + end + """ + |> string_to_state - assert [ - %VarInfo{ - name: :other, - type: {:map_key, {:attribute, :myattribute}, {:atom, :error}} - }, - %VarInfo{ - name: :var1, - type: {:map_key, {:attribute, :myattribute}, {:atom, :error}} + assert [ + %VarInfo{ + name: :k, + type: { + :tuple_nth, + { + :intersection, + [ + {:for_expression, {:attribute, :myattribute}}, + {:tuple, 2, [nil, {:variable, :v}]} + ] + }, + 0 } - ] = state |> get_line_vars(6) - end + }, + %VarInfo{ + name: :v, + type: { + :tuple_nth, + { + :intersection, + [ + {:for_expression, {:attribute, :myattribute}}, + {:tuple, 2, [{:variable, :k}, nil]} + ] + }, + 1 + } + } + ] = state |> get_line_vars(4) + end - test "binding in with expression" do - state = - """ - defmodule MyModule do - @myattribute [:ok, :error, :other] - with a <- @myattribute do - b = a - IO.puts - end - end - """ - |> string_to_state + test "struct destructuring" do + state = + """ + defmodule MyModule do + @a %My{} + @myattribute %My{ok: :a, error: b, other: :c} + %{error: var1} = @myattribute + %My{error: other} = @myattribute + IO.puts + end + """ + |> string_to_state - assert [ - %VarInfo{name: :a, type: {:attribute, :myattribute}}, - %VarInfo{name: :b, type: {:variable, :a}} - ] = state |> get_line_vars(5) - end + assert [ + %VarInfo{ + name: :other, + type: {:map_key, {:attribute, :myattribute}, {:atom, :error}} + }, + %VarInfo{ + name: :var1, + type: {:map_key, {:attribute, :myattribute}, {:atom, :error}} + } + ] = state |> get_line_vars(6) + end - test "binding in with expression more complex" do - state = - """ - defmodule MyModule do - @myattribute [:ok, :error, :other] - with a <- @myattribute, - b = Date.utc_now(), - [c | _] <- a do - IO.puts - end + test "binding in with expression" do + state = + """ + defmodule MyModule do + @myattribute [:ok, :error, :other] + with a <- @myattribute do + b = a + IO.puts end - """ - |> string_to_state + end + """ + |> string_to_state - assert [ - %VarInfo{name: :a, type: {:attribute, :myattribute}}, - %VarInfo{name: :b, type: {:call, {:atom, Date}, :utc_now, []}}, - %VarInfo{name: :c, type: {:list_head, {:variable, :a}}} - ] = state |> get_line_vars(6) - end + assert [ + %VarInfo{name: :a, type: {:attribute, :myattribute}}, + %VarInfo{name: :b, type: {:variable, :a}} + ] = state |> get_line_vars(5) + end - test "binding in with expression with guard" do - state = - """ - defmodule MyModule do - @myattribute [:ok, :error, :other] - with [a | _] when is_atom(a) <- @myattribute do - IO.puts - end + test "binding in with expression more complex" do + state = + """ + defmodule MyModule do + @myattribute [:ok, :error, :other] + with a <- @myattribute, + b = Date.utc_now(), + [c | _] <- a do + IO.puts end - """ - |> string_to_state + end + """ + |> string_to_state - assert [ - %VarInfo{ - name: :a, - type: {:intersection, [:atom, {:list_head, {:attribute, :myattribute}}]} - } - ] = state |> get_line_vars(4) - end + assert [ + %VarInfo{name: :a, type: {:attribute, :myattribute}}, + %VarInfo{name: :b, type: {:call, {:atom, Date}, :utc_now, []}}, + %VarInfo{name: :c, type: {:list_head, {:variable, :a}}} + ] = state |> get_line_vars(6) + end - test "binding in with expression else" do - state = - """ - defmodule MyModule do - @myattribute [:ok, :error, :other] - with a <- @myattribute do - b = a - IO.puts - else - a = :ok -> - IO.puts - end + test "binding in with expression with guard" do + state = + """ + defmodule MyModule do + @myattribute [:ok, :error, :other] + with [a | _] when is_atom(a) <- @myattribute do + IO.puts end - """ - |> string_to_state + end + """ + |> string_to_state - assert [ - %VarInfo{name: :a, type: {:atom, :ok}} - ] = state |> get_line_vars(8) - end + assert [ + %VarInfo{ + name: :a, + type: {:intersection, [:atom, {:list_head, {:attribute, :myattribute}}]} + } + ] = state |> get_line_vars(4) + end - test "vars binding" do - state = - """ - defmodule MyModule do - def func do - var = String - IO.puts "" - var = Map - IO.puts "" - if abc do - IO.puts "" - var = List - IO.puts "" - var = Enum - IO.puts "" - end + test "binding in with expression else" do + state = + """ + defmodule MyModule do + @myattribute [:ok, :error, :other] + with a <- @myattribute do + b = a + IO.puts + else + a = :ok -> + IO.puts + end + end + """ + |> string_to_state + + assert [ + %VarInfo{name: :a, type: {:atom, :ok}} + ] = state |> get_line_vars(8) + end + + test "vars binding" do + state = + """ + defmodule MyModule do + def func do + var = String + IO.puts "" + var = Map + IO.puts "" + if abc do IO.puts "" - var = Atom + var = List IO.puts "" - other = var + var = Enum IO.puts "" end + IO.puts "" + var = Atom + IO.puts "" + other = var + IO.puts "" end - """ - |> string_to_state + end + """ + |> string_to_state - assert [%VarInfo{type: {:atom, String}}] = state |> get_line_vars(4) + assert [%VarInfo{type: {:atom, String}}] = state |> get_line_vars(4) - assert [%VarInfo{type: {:atom, Map}}] = - state |> get_line_vars(6) + assert [%VarInfo{type: {:atom, Map}}] = + state |> get_line_vars(6) - assert [%VarInfo{type: {:atom, Map}}] = - state |> get_line_vars(8) + assert [%VarInfo{type: {:atom, Map}}] = + state |> get_line_vars(8) - assert [ - %VarInfo{type: {:atom, List}} - ] = state |> get_line_vars(10) + assert [ + %VarInfo{type: {:atom, List}} + ] = state |> get_line_vars(10) - assert [ - %VarInfo{type: {:atom, Enum}} - ] = state |> get_line_vars(12) + assert [ + %VarInfo{type: {:atom, Enum}} + ] = state |> get_line_vars(12) - assert [%VarInfo{type: {:atom, Map}}] = - state |> get_line_vars(14) + assert [%VarInfo{type: {:atom, Map}}] = + state |> get_line_vars(14) - assert [ - %VarInfo{type: {:atom, Atom}} - ] = state |> get_line_vars(16) + assert [ + %VarInfo{type: {:atom, Atom}} + ] = state |> get_line_vars(16) - assert [ - %VarInfo{name: :other, type: {:variable, :var}}, - %VarInfo{type: {:atom, Atom}} - ] = state |> get_line_vars(18) - end + assert [ + %VarInfo{name: :other, type: {:variable, :var}}, + %VarInfo{type: {:atom, Atom}} + ] = state |> get_line_vars(18) + end - test "call binding" do - state = - """ - defmodule MyModule do - def remote_calls do - var1 = DateTime.now - var2 = :erlang.now() - var3 = __MODULE__.now(:abc) - var4 = "Etc/UTC" |> DateTime.now - IO.puts "" - end + test "call binding" do + state = + """ + defmodule MyModule do + def remote_calls do + var1 = DateTime.now + var2 = :erlang.now() + var3 = __MODULE__.now(:abc) + var4 = "Etc/UTC" |> DateTime.now + IO.puts "" + end - def local_calls do - var1 = now - var2 = now() - var3 = now(:abc) - var4 = :abc |> now - var5 = :abc |> now(5) - IO.puts "" - end + def local_calls do + var1 = now + var2 = now() + var3 = now(:abc) + var4 = :abc |> now + var5 = :abc |> now(5) + IO.puts "" + end - @attr %{qwe: String} - def map_field(var1) do - var1 = var1.abc - var2 = @attr.qwe(0) - var3 = abc.cde.efg - IO.puts "" - end + @attr %{qwe: String} + def map_field(var1) do + var1 = var1.abc + var2 = @attr.qwe(0) + var3 = abc.cde.efg + IO.puts "" end - """ - |> string_to_state + end + """ + |> string_to_state - assert [ - %VarInfo{name: :var1, type: {:call, {:atom, DateTime}, :now, []}}, - %VarInfo{name: :var2, type: {:call, {:atom, :erlang}, :now, []}}, - %VarInfo{name: :var3, type: {:call, {:atom, MyModule}, :now, [{:atom, :abc}]}}, - %VarInfo{name: :var4, type: {:call, {:atom, DateTime}, :now, [nil]}} - ] = state |> get_line_vars(7) + assert [ + %VarInfo{name: :var1, type: {:call, {:atom, DateTime}, :now, []}}, + %VarInfo{name: :var2, type: {:call, {:atom, :erlang}, :now, []}}, + %VarInfo{name: :var3, type: {:call, {:atom, MyModule}, :now, [{:atom, :abc}]}}, + %VarInfo{name: :var4, type: {:call, {:atom, DateTime}, :now, [nil]}} + ] = state |> get_line_vars(7) - assert [ - %VarInfo{name: :var1, type: {:variable, :now}}, - %VarInfo{name: :var2, type: {:local_call, :now, []}}, - %VarInfo{name: :var3, type: {:local_call, :now, [{:atom, :abc}]}}, - %VarInfo{name: :var4, type: {:local_call, :now, [{:atom, :abc}]}}, - %VarInfo{name: :var5, type: {:local_call, :now, [{:atom, :abc}, {:integer, 5}]}} - ] = state |> get_line_vars(16) + assert [ + %VarInfo{name: :var1, type: {:variable, :now}}, + %VarInfo{name: :var2, type: {:local_call, :now, []}}, + %VarInfo{name: :var3, type: {:local_call, :now, [{:atom, :abc}]}}, + %VarInfo{name: :var4, type: {:local_call, :now, [{:atom, :abc}]}}, + %VarInfo{name: :var5, type: {:local_call, :now, [{:atom, :abc}, {:integer, 5}]}} + ] = state |> get_line_vars(16) - assert [ - %VarInfo{name: :var1, type: {:call, {:variable, :var1}, :abc, []}}, - %VarInfo{name: :var2, type: {:call, {:attribute, :attr}, :qwe, [{:integer, 0}]}}, - %VarInfo{ - name: :var3, - type: {:call, {:call, {:variable, :abc}, :cde, []}, :efg, []} - } - ] = state |> get_line_vars(24) - end + assert [ + %VarInfo{name: :var1, type: {:call, {:variable, :var1}, :abc, []}}, + %VarInfo{name: :var2, type: {:call, {:attribute, :attr}, :qwe, [{:integer, 0}]}}, + %VarInfo{ + name: :var3, + type: {:call, {:call, {:variable, :abc}, :cde, []}, :efg, []} + } + ] = state |> get_line_vars(24) + end - test "map binding" do - state = - """ - defmodule MyModule do - def func do - var = %{asd: 5} - IO.puts "" - var = %{asd: 5, nested: %{wer: "asd"}} - IO.puts "" - var = %{"asd" => "dsds"} - IO.puts "" - var = %{asd: 5, zxc: String} - IO.puts "" - qwe = %{var | asd: 2, zxc: 5} - IO.puts "" - qwe = %{var | asd: 2} - IO.puts "" + test "map binding" do + state = + """ + defmodule MyModule do + def func do + var = %{asd: 5} + IO.puts "" + var = %{asd: 5, nested: %{wer: "asd"}} + IO.puts "" + var = %{"asd" => "dsds"} + IO.puts "" + var = %{asd: 5, zxc: String} + IO.puts "" + qwe = %{var | asd: 2, zxc: 5} + IO.puts "" + qwe = %{var | asd: 2} + IO.puts "" - end end - """ - |> string_to_state + end + """ + |> string_to_state - assert [%VarInfo{type: {:map, [asd: {:integer, 5}], nil}}] = state |> get_line_vars(4) + assert [%VarInfo{type: {:map, [asd: {:integer, 5}], nil}}] = state |> get_line_vars(4) - assert [ - %VarInfo{ - type: {:map, [asd: {:integer, 5}, nested: {:map, [wer: nil], nil}], nil} - } - ] = state |> get_line_vars(6) + assert [ + %VarInfo{ + type: {:map, [asd: {:integer, 5}, nested: {:map, [wer: nil], nil}], nil} + } + ] = state |> get_line_vars(6) - assert [ - %VarInfo{type: {:map, [], nil}} - ] = state |> get_line_vars(8) + assert [ + %VarInfo{type: {:map, [], nil}} + ] = state |> get_line_vars(8) - assert [ - %VarInfo{type: {:map, [asd: {:integer, 5}, zxc: {:atom, String}], nil}} - ] = state |> get_line_vars(10) + assert [ + %VarInfo{type: {:map, [asd: {:integer, 5}, zxc: {:atom, String}], nil}} + ] = state |> get_line_vars(10) - assert [ - %VarInfo{ - type: {:map, [asd: {:integer, 2}, zxc: {:integer, 5}], {:variable, :var}} - } - ] = - state |> get_line_vars(12) |> Enum.filter(&(&1.name == :qwe)) + assert [ + %VarInfo{ + type: {:map, [asd: {:integer, 2}, zxc: {:integer, 5}], {:variable, :var}} + } + ] = + state |> get_line_vars(12) |> Enum.filter(&(&1.name == :qwe)) - assert [ - %VarInfo{type: {:map, [{:asd, {:integer, 2}}], {:variable, :var}}} - ] = state |> get_line_vars(14) |> Enum.filter(&(&1.name == :qwe)) - end + assert [ + %VarInfo{type: {:map, [{:asd, {:integer, 2}}], {:variable, :var}}} + ] = state |> get_line_vars(14) |> Enum.filter(&(&1.name == :qwe)) + end - test "struct binding" do - state = - """ - defmodule MyModule do - def func(%MyStruct{} = var1, var2 = %:other_struct{}, var3 = %__MODULE__{}, - var4 = %__MODULE__.Sub{}, var7 = %_{}) do - IO.puts "" - end + test "struct binding" do + state = + """ + defmodule MyModule do + def func(%MyStruct{} = var1, var2 = %:other_struct{}, var3 = %__MODULE__{}, + var4 = %__MODULE__.Sub{}, var7 = %_{}) do + IO.puts "" + end - def some(a) do - asd = %Some{sub: Atom} - IO.puts "" - asd = %Other{a | sub: Atom} - IO.puts "" - asd = %{asd | other: 123} - IO.puts "" - z = x = asd - IO.puts "" - end + def some(a) do + asd = %Some{sub: Atom} + IO.puts "" + asd = %Other{a | sub: Atom} + IO.puts "" + asd = %{asd | other: 123} + IO.puts "" + z = x = asd + IO.puts "" end - """ - |> string_to_state + end + """ + |> string_to_state - assert [ - %VarInfo{name: :var1, type: {:struct, [], {:atom, MyStruct}, nil}}, - %VarInfo{name: :var2, type: {:struct, [], {:atom, :other_struct}, nil}}, - %VarInfo{name: :var3, type: {:struct, [], {:atom, MyModule}, nil}}, - %VarInfo{name: :var4, type: {:struct, [], {:atom, MyModule.Sub}, nil}}, - %VarInfo{name: :var7, type: {:struct, [], nil, nil}} - ] = state |> get_line_vars(4) + assert [ + %VarInfo{name: :var1, type: {:struct, [], {:atom, MyStruct}, nil}}, + %VarInfo{name: :var2, type: {:struct, [], {:atom, :other_struct}, nil}}, + %VarInfo{name: :var3, type: {:struct, [], {:atom, MyModule}, nil}}, + %VarInfo{name: :var4, type: {:struct, [], {:atom, MyModule.Sub}, nil}}, + %VarInfo{name: :var7, type: {:struct, [], nil, nil}} + ] = state |> get_line_vars(4) - assert %VarInfo{name: :asd, type: {:struct, [{:sub, {:atom, Atom}}], {:atom, Some}, nil}} = - state |> get_line_vars(9) |> Enum.find(&(&1.name == :asd)) + assert %VarInfo{name: :asd, type: {:struct, [{:sub, {:atom, Atom}}], {:atom, Some}, nil}} = + state |> get_line_vars(9) |> Enum.find(&(&1.name == :asd)) - assert [ - %VarInfo{ - name: :asd, - type: {:struct, [{:sub, {:atom, Atom}}], {:atom, Other}, {:variable, :a}} - } - ] = state |> get_line_vars(11) |> Enum.filter(&(&1.name == :asd)) + assert [ + %VarInfo{ + name: :asd, + type: {:struct, [{:sub, {:atom, Atom}}], {:atom, Other}, {:variable, :a}} + } + ] = state |> get_line_vars(11) |> Enum.filter(&(&1.name == :asd)) - assert [ - %VarInfo{ - name: :asd, - type: {:map, [{:other, {:integer, 123}}], {:variable, :asd}} - } - ] = state |> get_line_vars(13) |> Enum.filter(&(&1.name == :asd)) + assert [ + %VarInfo{ + name: :asd, + type: {:map, [{:other, {:integer, 123}}], {:variable, :asd}} + } + ] = state |> get_line_vars(13) |> Enum.filter(&(&1.name == :asd)) - assert [ - %VarInfo{name: :x, type: {:intersection, [{:variable, :asd}, {:variable, :z}]}}, - %VarInfo{name: :z, type: {:variable, :asd}} - ] = state |> get_line_vars(15) |> Enum.filter(&(&1.name in [:x, :z])) - end + assert [ + %VarInfo{name: :x, type: {:intersection, [{:variable, :asd}, {:variable, :z}]}}, + %VarInfo{name: :z, type: {:variable, :asd}} + ] = state |> get_line_vars(15) |> Enum.filter(&(&1.name in [:x, :z])) + end - test "struct binding understands builtin sigils and ranges" do - state = - """ - defmodule MyModule do - def some() do - var1 = ~D[2000-01-01] - var2 = ~T[13:00:07] - var3 = ~U[2015-01-13 13:00:07Z] - var4 = ~N[2000-01-01 23:00:07] - var5 = ~r/foo/iu - var6 = ~R(f\#{1,3}o) - var7 = 12..34 - var8 = 12..34//1 - IO.puts "" - end + test "struct binding understands builtin sigils and ranges" do + state = + """ + defmodule MyModule do + def some() do + var1 = ~D[2000-01-01] + var2 = ~T[13:00:07] + var3 = ~U[2015-01-13 13:00:07Z] + var4 = ~N[2000-01-01 23:00:07] + var5 = ~r/foo/iu + var6 = ~R(f\#{1,3}o) + var7 = 12..34 + var8 = 12..34//1 + IO.puts "" end - """ - |> string_to_state + end + """ + |> string_to_state - assert [ - %VarInfo{name: :var1, type: {:struct, [], {:atom, Date}, nil}}, - %VarInfo{name: :var2, type: {:struct, [], {:atom, Time}, nil}}, - %VarInfo{name: :var3, type: {:struct, [], {:atom, DateTime}, nil}}, - %VarInfo{name: :var4, type: {:struct, [], {:atom, NaiveDateTime}, nil}}, - %VarInfo{name: :var5, type: {:struct, [], {:atom, Regex}, nil}}, - %VarInfo{name: :var6, type: {:struct, [], {:atom, Regex}, nil}}, - %VarInfo{name: :var7, type: {:struct, [], {:atom, Range}, nil}}, - %VarInfo{name: :var8, type: {:struct, [], {:atom, Range}, nil}} - ] = state |> get_line_vars(11) - end + assert [ + %VarInfo{name: :var1, type: {:struct, [], {:atom, Date}, nil}}, + %VarInfo{name: :var2, type: {:struct, [], {:atom, Time}, nil}}, + %VarInfo{name: :var3, type: {:struct, [], {:atom, DateTime}, nil}}, + %VarInfo{name: :var4, type: {:struct, [], {:atom, NaiveDateTime}, nil}}, + %VarInfo{name: :var5, type: {:struct, [], {:atom, Regex}, nil}}, + %VarInfo{name: :var6, type: {:struct, [], {:atom, Regex}, nil}}, + %VarInfo{name: :var7, type: {:struct, [], {:atom, Range}, nil}}, + %VarInfo{name: :var8, type: {:struct, [], {:atom, Range}, nil}} + ] = state |> get_line_vars(11) + end - test "struct binding understands stepped ranges" do - state = - """ - defmodule MyModule do - def some() do - var1 = 12..34//2 - IO.puts "" - end + test "struct binding understands stepped ranges" do + state = + """ + defmodule MyModule do + def some() do + var1 = 12..34//2 + IO.puts "" end - """ - |> string_to_state + end + """ + |> string_to_state - assert [ - %VarInfo{name: :var1, type: {:struct, [], {:atom, Range}, nil}} - ] = state |> get_line_vars(4) - end + assert [ + %VarInfo{name: :var1, type: {:struct, [], {:atom, Range}, nil}} + ] = state |> get_line_vars(4) + end - test "two way refinement in match context" do - state = - """ - defmodule MyModule do - def some(%MyState{formatted: formatted} = state) do - IO.puts "" + test "two way refinement in match context" do + state = + """ + defmodule MyModule do + def some(%MyState{formatted: formatted} = state) do + IO.puts "" - case :ok do - %{foo: 1} = state = %{bar: 1} = x -> - IO.puts "" - end + case :ok do + %{foo: 1} = state = %{bar: 1} = x -> + IO.puts "" end end - """ - |> string_to_state + end + """ + |> string_to_state - assert [ - %VarInfo{ - name: :formatted, - type: {:map_key, {:variable, :state}, {:atom, :formatted}} - }, - %VarInfo{ - name: :state, - type: {:struct, [formatted: {:variable, :formatted}], {:atom, MyState}, nil} - } - ] = state |> get_line_vars(3) + assert [ + %VarInfo{ + name: :formatted, + type: {:map_key, {:variable, :state}, {:atom, :formatted}} + }, + %VarInfo{ + name: :state, + type: {:struct, [formatted: {:variable, :formatted}], {:atom, MyState}, nil} + } + ] = state |> get_line_vars(3) - assert [ - %VarInfo{ - name: :formatted - }, - %VarInfo{ - name: :state, - type: {:struct, [formatted: {:variable, :formatted}], {:atom, MyState}, nil} - } - ] = state |> get_line_vars(7) - end + assert [ + %VarInfo{ + name: :formatted + }, + %VarInfo{ + name: :state, + type: {:struct, [formatted: {:variable, :formatted}], {:atom, MyState}, nil} + } + ] = state |> get_line_vars(7) + end - test "two way refinement in match context nested" do - state = - """ - defmodule MyModule do - def some(%{foo: 1} = state = %{bar: 1} = x) do - IO.puts "" - end + test "two way refinement in match context nested" do + state = + """ + defmodule MyModule do + def some(%{foo: 1} = state = %{bar: 1} = x) do + IO.puts "" end - """ - |> string_to_state + end + """ + |> string_to_state - assert [ - %VarInfo{ - name: :formatted, - type: {:map_key, {:variable, :state}, {:atom, :formatted}} - }, - %VarInfo{ - name: :state, - type: {:struct, [formatted: {:variable, :formatted}], {:atom, MyState}, nil} - } - ] = state |> get_line_vars(3) + assert [ + %VarInfo{ + name: :formatted, + type: {:map_key, {:variable, :state}, {:atom, :formatted}} + }, + %VarInfo{ + name: :state, + type: {:struct, [formatted: {:variable, :formatted}], {:atom, MyState}, nil} + } + ] = state |> get_line_vars(3) - assert [ - %VarInfo{ - name: :formatted - }, - %VarInfo{ - name: :state, - type: {:struct, [formatted: {:variable, :formatted}], {:atom, MyState}, nil} - } - ] = state |> get_line_vars(7) - end + assert [ + %VarInfo{ + name: :formatted + }, + %VarInfo{ + name: :state, + type: {:struct, [formatted: {:variable, :formatted}], {:atom, MyState}, nil} + } + ] = state |> get_line_vars(7) + end - test "two way refinement in match context nested case" do - state = - """ - defmodule MyModule do - def some(state) do - case :ok do - %{foo: 1} = state = %{bar: 1} = x -> - IO.puts "" - end + test "two way refinement in match context nested case" do + state = + """ + defmodule MyModule do + def some(state) do + case :ok do + %{foo: 1} = state = %{bar: 1} = x -> + IO.puts "" end end - """ - |> string_to_state + end + """ + |> string_to_state - assert [ - %VarInfo{ - name: :formatted - }, - %VarInfo{ - name: :state, - type: {:struct, [formatted: {:variable, :formatted}], {:atom, MyState}, nil} - } - ] = state |> get_line_vars(5) - end + assert [ + %VarInfo{ + name: :formatted + }, + %VarInfo{ + name: :state, + type: {:struct, [formatted: {:variable, :formatted}], {:atom, MyState}, nil} + } + ] = state |> get_line_vars(5) + end - test "two way refinement in nested `=` binding" do - state = - """ - defmodule MyModule do - def some() do - %MyState{formatted: formatted} = state = socket.assigns.state - IO.puts "" - end + test "two way refinement in nested `=` binding" do + state = + """ + defmodule MyModule do + def some() do + %MyState{formatted: formatted} = state = socket.assigns.state + IO.puts "" end - """ - |> string_to_state + end + """ + |> string_to_state - assert [ - %VarInfo{ - name: :formatted, - type: { - :map_key, - { - :call, - {:call, {:variable, :socket}, :assigns, []}, - :state, - [] - }, - {:atom, :formatted} - } - }, - %VarInfo{ - name: :state, - type: - {:intersection, - [ - {:call, {:call, {:variable, :socket}, :assigns, []}, :state, []}, - {:struct, [formatted: {:variable, :formatted}], {:atom, MyState}, nil} - ]} + assert [ + %VarInfo{ + name: :formatted, + type: { + :map_key, + { + :call, + {:call, {:variable, :socket}, :assigns, []}, + :state, + [] + }, + {:atom, :formatted} } - ] = state |> get_line_vars(4) - end + }, + %VarInfo{ + name: :state, + type: + {:intersection, + [ + {:call, {:call, {:variable, :socket}, :assigns, []}, :state, []}, + {:struct, [formatted: {:variable, :formatted}], {:atom, MyState}, nil} + ]} + } + ] = state |> get_line_vars(4) + end - test "case binding" do - state = - """ - defmodule MyModule do - def some() do - case Some.call() do - {:ok, x} -> - IO.puts "" - end + test "case binding" do + state = + """ + defmodule MyModule do + def some() do + case Some.call() do + {:ok, x} -> + IO.puts "" end end - """ - |> string_to_state + end + """ + |> string_to_state - assert [ - %VarInfo{ - name: :x, - type: - {:tuple_nth, - {:intersection, - [{:call, {:atom, Some}, :call, []}, {:tuple, 2, [{:atom, :ok}, nil]}]}, 1} - } - ] = state |> get_line_vars(5) - end + assert [ + %VarInfo{ + name: :x, + type: + {:tuple_nth, + {:intersection, + [{:call, {:atom, Some}, :call, []}, {:tuple, 2, [{:atom, :ok}, nil]}]}, 1} + } + ] = state |> get_line_vars(5) + end - test "case binding with match" do - state = - """ - defmodule MyModule do - def some() do - case Some.call() do - {:ok, x} = res -> - IO.puts "" - end + test "case binding with match" do + state = + """ + defmodule MyModule do + def some() do + case Some.call() do + {:ok, x} = res -> + IO.puts "" end end - """ - |> string_to_state + end + """ + |> string_to_state - assert [ - %VarInfo{ - name: :res, - type: :todo - }, - %VarInfo{ - name: :x, - type: - {:tuple_nth, - {:intersection, - [{:call, {:atom, Some}, :call, []}, {:tuple, 2, [{:atom, :ok}, nil]}]}, 1} - } - ] = state |> get_line_vars(5) - end + assert [ + %VarInfo{ + name: :res, + type: :todo + }, + %VarInfo{ + name: :x, + type: + {:tuple_nth, + {:intersection, + [{:call, {:atom, Some}, :call, []}, {:tuple, 2, [{:atom, :ok}, nil]}]}, 1} + } + ] = state |> get_line_vars(5) + end - test "rescue binding" do - state = - """ - defmodule MyModule do - def some() do - try do - Some.call() - rescue - e0 in ArgumentError -> - IO.puts "" - e1 in [ArgumentError] -> - IO.puts "" - e2 in [RuntimeError, Enum.EmptyError] -> - IO.puts "" - e3 in _ -> - IO.puts "" - e4 -> - IO.puts "" - else - a -> - IO.puts "" - end + test "rescue binding" do + state = + """ + defmodule MyModule do + def some() do + try do + Some.call() + rescue + e0 in ArgumentError -> + IO.puts "" + e1 in [ArgumentError] -> + IO.puts "" + e2 in [RuntimeError, Enum.EmptyError] -> + IO.puts "" + e3 in _ -> + IO.puts "" + e4 -> + IO.puts "" + else + a -> + IO.puts "" end end - """ - |> string_to_state + end + """ + |> string_to_state - assert [ - %VarInfo{ - name: :e0, - type: {:struct, [], {:atom, ArgumentError}, nil} - } - ] = state |> get_line_vars(7) + assert [ + %VarInfo{ + name: :e0, + type: {:struct, [], {:atom, ArgumentError}, nil} + } + ] = state |> get_line_vars(7) - assert [ - %VarInfo{ - name: :e1, - type: {:struct, [], {:atom, ArgumentError}, nil} - } - ] = state |> get_line_vars(9) + assert [ + %VarInfo{ + name: :e1, + type: {:struct, [], {:atom, ArgumentError}, nil} + } + ] = state |> get_line_vars(9) - assert [ - %VarInfo{ - name: :e2, - type: { - :union, - [ - {:struct, [], {:atom, RuntimeError}, nil}, - {:struct, [], {:atom, Enum.EmptyError}, nil} - ] - } + assert [ + %VarInfo{ + name: :e2, + type: { + :union, + [ + {:struct, [], {:atom, RuntimeError}, nil}, + {:struct, [], {:atom, Enum.EmptyError}, nil} + ] } - ] = state |> get_line_vars(11) + } + ] = state |> get_line_vars(11) - assert [ - %VarInfo{ - name: :e3, - type: {:struct, [], {:atom, Exception}, nil} - } - ] = state |> get_line_vars(13) + assert [ + %VarInfo{ + name: :e3, + type: {:struct, [], {:atom, Exception}, nil} + } + ] = state |> get_line_vars(13) - assert [ - %VarInfo{ - name: :e4, - type: {:struct, [], {:atom, Exception}, nil} - } - ] = state |> get_line_vars(15) + assert [ + %VarInfo{ + name: :e4, + type: {:struct, [], {:atom, Exception}, nil} + } + ] = state |> get_line_vars(15) - assert [ - %VarInfo{ - name: :a, - type: nil - } - ] = state |> get_line_vars(18) - end + assert [ + %VarInfo{ + name: :a, + type: nil + } + ] = state |> get_line_vars(18) + end - test "vars binding by pattern matching with pin operators" do - state = - """ - defmodule MyModule do - def func(a) do - b = 1 - case a do - %{b: 2} = a1 -> - IO.puts "" - %{b: ^b} = a2 -> - IO.puts "" - end + test "vars binding by pattern matching with pin operators" do + state = + """ + defmodule MyModule do + def func(a) do + b = 1 + case a do + %{b: 2} = a1 -> + IO.puts "" + %{b: ^b} = a2 -> + IO.puts "" end end - """ - |> string_to_state + end + """ + |> string_to_state - vars = state |> get_line_vars(6) + vars = state |> get_line_vars(6) - # TODO wtf - # assert %VarInfo{ - # name: :a1, - # positions: [{5, 18}], - # scope_id: 6, - # type: {:map, [b: {:integer, 2}], nil} - # } = Enum.find(vars, &(&1.name == :a1)) + # TODO wtf + # assert %VarInfo{ + # name: :a1, + # positions: [{5, 18}], + # scope_id: 6, + # type: {:map, [b: {:integer, 2}], nil} + # } = Enum.find(vars, &(&1.name == :a1)) - vars = state |> get_line_vars(8) |> dbg + vars = state |> get_line_vars(8) |> dbg - assert %VarInfo{ - name: :a2, - positions: [{7, 18}], - type: {:map, [b: {:variable, :b}], nil} - } = Enum.find(vars, &(&1.name == :a2)) - end + assert %VarInfo{ + name: :a2, + positions: [{7, 18}], + type: {:map, [b: {:variable, :b}], nil} + } = Enum.find(vars, &(&1.name == :a2)) end end @@ -3397,246 +3390,244 @@ defmodule ElixirSense.Core.MetadataBuilderTest do end end - if @binding_support do - describe "infer vars type information from guards" do - defp var_with_guards(guard) do - """ - defmodule MyModule do - def func(x) when #{guard} do - IO.puts "" - end + describe "infer vars type information from guards" do + defp var_with_guards(guard) do + """ + defmodule MyModule do + def func(x) when #{guard} do + IO.puts "" end - """ - |> string_to_state() - |> get_line_vars(3) - |> hd() end + """ + |> string_to_state() + |> get_line_vars(3) + |> hd() + end - test "number guards" do - assert %VarInfo{name: :x, type: :number} = var_with_guards("is_number(x)") - assert %VarInfo{name: :x, type: :number} = var_with_guards("is_float(x)") - assert %VarInfo{name: :x, type: :number} = var_with_guards("is_integer(x)") - assert %VarInfo{name: :x, type: :number} = var_with_guards("round(x)") - assert %VarInfo{name: :x, type: :number} = var_with_guards("trunc(x)") - assert %VarInfo{name: :x, type: :number} = var_with_guards("div(x, 1)") - assert %VarInfo{name: :x, type: :number} = var_with_guards("rem(x, 1)") - assert %VarInfo{name: :x, type: :number} = var_with_guards("abs(x)") - assert %VarInfo{name: :x, type: :number} = var_with_guards("ceil(x)") - assert %VarInfo{name: :x, type: :number} = var_with_guards("floor(x)") - end + test "number guards" do + assert %VarInfo{name: :x, type: :number} = var_with_guards("is_number(x)") + assert %VarInfo{name: :x, type: :number} = var_with_guards("is_float(x)") + assert %VarInfo{name: :x, type: :number} = var_with_guards("is_integer(x)") + assert %VarInfo{name: :x, type: :number} = var_with_guards("round(x)") + assert %VarInfo{name: :x, type: :number} = var_with_guards("trunc(x)") + assert %VarInfo{name: :x, type: :number} = var_with_guards("div(x, 1)") + assert %VarInfo{name: :x, type: :number} = var_with_guards("rem(x, 1)") + assert %VarInfo{name: :x, type: :number} = var_with_guards("abs(x)") + assert %VarInfo{name: :x, type: :number} = var_with_guards("ceil(x)") + assert %VarInfo{name: :x, type: :number} = var_with_guards("floor(x)") + end - test "binary guards" do - assert %VarInfo{name: :x, type: :binary} = var_with_guards("is_binary(x)") + test "binary guards" do + assert %VarInfo{name: :x, type: :binary} = var_with_guards("is_binary(x)") - assert %VarInfo{name: :x, type: :binary} = - var_with_guards(~s/binary_part(x, 0, 1) == "a"/) - end + assert %VarInfo{name: :x, type: :binary} = + var_with_guards(~s/binary_part(x, 0, 1) == "a"/) + end - test "bitstring guards" do - assert %VarInfo{name: :x, type: :bitstring} = var_with_guards("is_bitstring(x)") - assert %VarInfo{name: :x, type: :bitstring} = var_with_guards("bit_size(x) == 1") - assert %VarInfo{name: :x, type: :bitstring} = var_with_guards("byte_size(x) == 1") - end + test "bitstring guards" do + assert %VarInfo{name: :x, type: :bitstring} = var_with_guards("is_bitstring(x)") + assert %VarInfo{name: :x, type: :bitstring} = var_with_guards("bit_size(x) == 1") + assert %VarInfo{name: :x, type: :bitstring} = var_with_guards("byte_size(x) == 1") + end - test "multiple guards" do - assert %VarInfo{name: :x, type: {:union, [:bitstring, :number]}} = - var_with_guards("is_bitstring(x) when is_integer(x)") - end + test "multiple guards" do + assert %VarInfo{name: :x, type: {:union, [:bitstring, :number]}} = + var_with_guards("is_bitstring(x) when is_integer(x)") + end - test "list guards" do - assert %VarInfo{name: :x, type: :list} = var_with_guards("is_list(x)") - assert %VarInfo{name: :x, type: {:list, :number}} = var_with_guards("hd(x) == 1") - assert %VarInfo{name: :x, type: {:list, :number}} = var_with_guards("1 == hd(x)") - assert %VarInfo{name: :x, type: :list} = var_with_guards("tl(x) == [1]") - assert %VarInfo{name: :x, type: :list} = var_with_guards("length(x) == 1") - assert %VarInfo{name: :x, type: :list} = var_with_guards("1 == length(x)") - assert %VarInfo{name: :x, type: {:list, :boolean}} = var_with_guards("hd(x)") - end + test "list guards" do + assert %VarInfo{name: :x, type: :list} = var_with_guards("is_list(x)") + assert %VarInfo{name: :x, type: {:list, :number}} = var_with_guards("hd(x) == 1") + assert %VarInfo{name: :x, type: {:list, :number}} = var_with_guards("1 == hd(x)") + assert %VarInfo{name: :x, type: :list} = var_with_guards("tl(x) == [1]") + assert %VarInfo{name: :x, type: :list} = var_with_guards("length(x) == 1") + assert %VarInfo{name: :x, type: :list} = var_with_guards("1 == length(x)") + assert %VarInfo{name: :x, type: {:list, :boolean}} = var_with_guards("hd(x)") + end - test "tuple guards" do - assert %VarInfo{name: :x, type: :tuple} = var_with_guards("is_tuple(x)") + test "tuple guards" do + assert %VarInfo{name: :x, type: :tuple} = var_with_guards("is_tuple(x)") - assert %VarInfo{name: :x, type: {:tuple, 1, [nil]}} = - var_with_guards("tuple_size(x) == 1") + assert %VarInfo{name: :x, type: {:tuple, 1, [nil]}} = + var_with_guards("tuple_size(x) == 1") - assert %VarInfo{name: :x, type: {:tuple, 1, [nil]}} = - var_with_guards("1 == tuple_size(x)") + assert %VarInfo{name: :x, type: {:tuple, 1, [nil]}} = + var_with_guards("1 == tuple_size(x)") - assert %VarInfo{name: :x, type: :tuple} = var_with_guards("elem(x, 0) == 1") - end + assert %VarInfo{name: :x, type: :tuple} = var_with_guards("elem(x, 0) == 1") + end - test "atom guards" do - assert %VarInfo{name: :x, type: :atom} = var_with_guards("is_atom(x)") - end + test "atom guards" do + assert %VarInfo{name: :x, type: :atom} = var_with_guards("is_atom(x)") + end - test "boolean guards" do - assert %VarInfo{name: :x, type: :boolean} = var_with_guards("is_boolean(x)") - end + test "boolean guards" do + assert %VarInfo{name: :x, type: :boolean} = var_with_guards("is_boolean(x)") + end - test "map guards" do - assert %VarInfo{name: :x, type: {:map, [], nil}} = var_with_guards("is_map(x)") + test "map guards" do + assert %VarInfo{name: :x, type: {:map, [], nil}} = var_with_guards("is_map(x)") - assert %VarInfo{name: :x, type: {:intersection, [{:map, [], nil}, nil]}} = - var_with_guards("is_non_struct_map(x)") + assert %VarInfo{name: :x, type: {:intersection, [{:map, [], nil}, nil]}} = + var_with_guards("is_non_struct_map(x)") - assert %VarInfo{name: :x, type: {:map, [], nil}} = var_with_guards("map_size(x) == 1") - assert %VarInfo{name: :x, type: {:map, [], nil}} = var_with_guards("1 == map_size(x)") + assert %VarInfo{name: :x, type: {:map, [], nil}} = var_with_guards("map_size(x) == 1") + assert %VarInfo{name: :x, type: {:map, [], nil}} = var_with_guards("1 == map_size(x)") - assert %VarInfo{name: :x, type: {:map, [a: nil], nil}} = - var_with_guards("is_map_key(x, :a)") + assert %VarInfo{name: :x, type: {:map, [a: nil], nil}} = + var_with_guards("is_map_key(x, :a)") - assert %VarInfo{name: :x, type: {:map, [{"a", nil}], nil}} = - var_with_guards(~s/is_map_key(x, "a")/) - end + assert %VarInfo{name: :x, type: {:map, [{"a", nil}], nil}} = + var_with_guards(~s/is_map_key(x, "a")/) + end - test "struct guards" do - assert %VarInfo{ - name: :x, - type: { - :intersection, - [ - {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, - {:struct, [], nil, nil} - ] - } - } = var_with_guards("is_struct(x)") + test "struct guards" do + assert %VarInfo{ + name: :x, + type: { + :intersection, + [ + {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, + {:struct, [], nil, nil} + ] + } + } = var_with_guards("is_struct(x)") - assert %VarInfo{ - name: :x, - type: { - :intersection, - [ - {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, - {:struct, [], {:atom, URI}, nil} - ] - } - } = - var_with_guards("is_struct(x, URI)") + assert %VarInfo{ + name: :x, + type: { + :intersection, + [ + {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, + {:struct, [], {:atom, URI}, nil} + ] + } + } = + var_with_guards("is_struct(x, URI)") - assert %VarInfo{ - name: :x, - type: { - :intersection, - [ - {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, - {:struct, [], {:atom, URI}, nil} - ] - } - } = - """ - defmodule MyModule do - alias URI, as: MyURI - - def func(x) when is_struct(x, MyURI) do - IO.puts "" - end + assert %VarInfo{ + name: :x, + type: { + :intersection, + [ + {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, + {:struct, [], {:atom, URI}, nil} + ] + } + } = + """ + defmodule MyModule do + alias URI, as: MyURI + + def func(x) when is_struct(x, MyURI) do + IO.puts "" end - """ - |> string_to_state() - |> get_line_vars(5) - |> hd() - end + end + """ + |> string_to_state() + |> get_line_vars(5) + |> hd() + end + + test "exception guards" do + assert %VarInfo{ + name: :x, + type: { + :intersection, + [ + { + :intersection, + [ + { + :intersection, + [ + {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, + {:struct, [], nil, nil} + ] + }, + {:map, [{:__exception__, nil}], nil} + ] + }, + {:map, [{:__exception__, {:atom, true}}], nil} + ] + } + } = var_with_guards("is_exception(x)") - test "exception guards" do - assert %VarInfo{ - name: :x, - type: { - :intersection, - [ - { - :intersection, - [ - { - :intersection, - [ - {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, - {:struct, [], nil, nil} - ] - }, - {:map, [{:__exception__, nil}], nil} - ] - }, - {:map, [{:__exception__, {:atom, true}}], nil} - ] - } - } = var_with_guards("is_exception(x)") + assert %VarInfo{ + name: :x, + type: { + :intersection, + [ + { + :intersection, + [ + { + :intersection, + [ + {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, + {:struct, [], {:atom, ArgumentError}, nil} + ] + }, + {:map, [{:__exception__, nil}], nil} + ] + }, + {:map, [{:__exception__, {:atom, true}}], nil} + ] + } + } = + var_with_guards("is_exception(x, ArgumentError)") - assert %VarInfo{ - name: :x, - type: { - :intersection, - [ - { - :intersection, - [ - { - :intersection, - [ - {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, - {:struct, [], {:atom, ArgumentError}, nil} - ] - }, - {:map, [{:__exception__, nil}], nil} - ] - }, - {:map, [{:__exception__, {:atom, true}}], nil} - ] - } - } = - var_with_guards("is_exception(x, ArgumentError)") + assert %VarInfo{ + name: :x, + type: { + :intersection, + [ + {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, + {:struct, [], {:atom, ArgumentError}, nil} + ] + } + } = + """ + defmodule MyModule do + alias ArgumentError, as: MyURI - assert %VarInfo{ - name: :x, - type: { - :intersection, - [ - {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, - {:struct, [], {:atom, ArgumentError}, nil} - ] - } - } = - """ - defmodule MyModule do - alias ArgumentError, as: MyURI - - def func(x) when is_struct(x, MyURI) do - IO.puts "" - end + def func(x) when is_struct(x, MyURI) do + IO.puts "" end - """ - |> string_to_state() - |> get_line_vars(5) - |> hd() - end + end + """ + |> string_to_state() + |> get_line_vars(5) + |> hd() + end - test "and combination predicate guards can be merge" do - assert %VarInfo{name: :x, type: {:intersection, [:number, :boolean]}} = - var_with_guards("is_number(x) and x >= 1") + test "and combination predicate guards can be merge" do + assert %VarInfo{name: :x, type: {:intersection, [:number, :boolean]}} = + var_with_guards("is_number(x) and x >= 1") - assert %VarInfo{ - name: :x, - type: {:intersection, [{:map, [a: nil], nil}, {:map, [b: nil], nil}]} - } = var_with_guards("is_map_key(x, :a) and is_map_key(x, :b)") - end + assert %VarInfo{ + name: :x, + type: {:intersection, [{:map, [a: nil], nil}, {:map, [b: nil], nil}]} + } = var_with_guards("is_map_key(x, :a) and is_map_key(x, :b)") + end - test "or combination predicate guards can be merge into union type" do - assert %VarInfo{name: :x, type: {:union, [:number, :atom]}} = - var_with_guards("is_number(x) or is_atom(x)") + test "or combination predicate guards can be merge into union type" do + assert %VarInfo{name: :x, type: {:union, [:number, :atom]}} = + var_with_guards("is_number(x) or is_atom(x)") - assert %VarInfo{name: :x, type: {:union, [:number, :atom, :binary]}} = - var_with_guards("is_number(x) or is_atom(x) or is_binary(x)") - end + assert %VarInfo{name: :x, type: {:union, [:number, :atom, :binary]}} = + var_with_guards("is_number(x) or is_atom(x) or is_binary(x)") + end - test "negated guards cannot be used for inference" do - assert %VarInfo{name: :x, type: nil} = - var_with_guards("not is_map(x)") + test "negated guards cannot be used for inference" do + assert %VarInfo{name: :x, type: nil} = + var_with_guards("not is_map(x)") - assert %VarInfo{name: :x, type: {:union, [nil, :atom]}} = - var_with_guards("not is_map(x) or is_atom(x)") + assert %VarInfo{name: :x, type: {:union, [nil, :atom]}} = + var_with_guards("not is_map(x) or is_atom(x)") - assert %VarInfo{name: :x, type: {:intersection, [nil, :atom]}} = - var_with_guards("not is_map(x) and is_atom(x)") - end + assert %VarInfo{name: :x, type: {:intersection, [nil, :atom]}} = + var_with_guards("not is_map(x) and is_atom(x)") end end From d1abdc50263a13b69130ace073157d83efa7aafd Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 18 Jul 2024 10:55:22 +0200 Subject: [PATCH 089/235] removing unused struct fields --- lib/elixir_sense/core/compiler.ex | 4 +- lib/elixir_sense/core/state.ex | 90 +++++++++++++------------------ 2 files changed, 40 insertions(+), 54 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 1c82b1f3..e20bc909 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1179,7 +1179,9 @@ defmodule ElixirSense.Core.Compiler do state = state |> add_spec(env, name, type_args, spec, kind, position, end_position, - generated: state.generated + # TODO ? + # generated: state.generated + generated: false ) |> with_typespec({name, length(type_args)}) |> add_current_env_to_line(line, env) diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 8235af56..6633f1e3 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -36,86 +36,72 @@ defmodule ElixirSense.Core.State do @type var_type :: nil | {:atom, atom} | {:map, keyword} | {:struct, keyword, module} @type t :: %ElixirSense.Core.State{ - module: [atom], - scopes: [scope], - requires: list(list(module)), - aliases: list(list(alias_t)), attributes: list(list(ElixirSense.Core.State.AttributeInfo.t())), - protocols: list(protocol_t() | nil), scope_attributes: list(list(atom)), behaviours: %{optional(module) => [module]}, specs: specs_t, + types: types_t, + mods_funs_to_positions: mods_funs_to_positions_t, + structs: structs_t, + calls: calls_t, vars_info: list(%{optional({atom, non_neg_integer}) => ElixirSense.Core.State.VarInfo.t()}), + vars_info_per_scope_id: vars_info_per_scope_id_t, scope_id_count: non_neg_integer, scope_ids: list(scope_id_t), - vars_info_per_scope_id: vars_info_per_scope_id_t, - mods_funs_to_positions: mods_funs_to_positions_t, - lines_to_env: lines_to_env_t, - calls: calls_t, - structs: structs_t, - types: types_t, - generated: boolean, + typespec: nil | {atom, arity}, + protocol: nil | {atom, [atom]}, + + # elixir_ex + vars: {map, false | map()}, + unused: non_neg_integer(), + prematch: atom | tuple, + stacktrace: boolean(), + caller: boolean(), + runtime_modules: list(module), + + # TODO ? + # generated: boolean, first_alias_positions: map(), moduledoc_positions: map(), - context: map(), doc_context: list(), typedoc_context: list(), optional_callbacks_context: list(), - # TODO better type - binding_context: list, - macro_env: list(Macro.Env.t()), - typespec: nil | {atom, arity}, - protocol: nil | {atom, [atom]}, + lines_to_env: lines_to_env_t, cursor_env: nil | {keyword, ElixirSense.Core.State.Env.t()} } - @auto_imported_functions :elixir_env.new().functions - @auto_imported_macros :elixir_env.new().macros - @auto_required [Application, Kernel] ++ - (if Version.match?(System.version(), ">= 1.17.0-dev") do - [] - else - [Kernel.Typespec] - end) - - defstruct module: [nil], - scopes: [nil], - functions: [@auto_imported_functions], - macros: [@auto_imported_macros], - requires: [@auto_required], - aliases: [[]], - attributes: [[]], - protocols: [nil], + defstruct attributes: [[]], scope_attributes: [[]], behaviours: %{}, specs: %{}, + types: %{}, + mods_funs_to_positions: %{}, + structs: %{}, + calls: %{}, vars_info: [%{}], + vars_info_per_scope_id: %{}, + scope_id_count: 0, + scope_ids: [0], + typespec: nil, + protocol: nil, + + # elixir_ex vars: {%{}, false}, unused: 0, prematch: :raise, stacktrace: false, caller: false, runtime_modules: [], - scope_id_count: 0, - scope_ids: [0], - vars_info_per_scope_id: %{}, - mods_funs_to_positions: %{}, - lines_to_env: %{}, - calls: %{}, - structs: %{}, - types: %{}, - generated: false, - binding_context: [], - context: %{}, + + # TODO ? + # generated: false, first_alias_positions: %{}, + moduledoc_positions: %{}, doc_context: [[]], typedoc_context: [[]], optional_callbacks_context: [[]], - moduledoc_positions: %{}, - macro_env: [:elixir_env.new()], - typespec: nil, - protocol: nil, + lines_to_env: %{}, cursor_env: nil defmodule Env do @@ -650,10 +636,8 @@ defmodule ElixirSense.Core.State do arity = length(params) {state, {doc, meta}} = - if not state.generated and Keyword.get(options, :generated, false) do + if Keyword.get(options, :generated, false) do # do not consume docs on generated functions - # NOTE state.generated is set when expanding use macro - # we want to consume docs there {state, {"", %{generated: true}}} else consume_doc_context(state) From 3470c6f44a9c9b8e3eeee85517c4bad810b95484 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 18 Jul 2024 10:59:23 +0200 Subject: [PATCH 090/235] silence warning on < 1.17 --- lib/elixir_sense/core/compiler.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index e20bc909..2a6773b7 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -3999,7 +3999,7 @@ defmodule ElixirSense.Core.Compiler do arity = case args_or_context do args when is_list(args) -> length(args) - _context when is_atom(args_or_context) -> 0 + context when is_atom(context) -> 0 end import_meta = import_meta(meta, name, arity, q, e) From b379cc04db5bea29440b269d2931e85f798e5d63 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 18 Jul 2024 11:04:00 +0200 Subject: [PATCH 091/235] do not crash on undefined var read --- lib/elixir_sense/core/state.ex | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 6633f1e3..f3886282 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -955,15 +955,20 @@ defmodule ElixirSense.Core.State do column = meta[:column] [vars_from_scope | other_vars] = state.vars_info - info = Map.fetch!(vars_from_scope, {name, version}) - info = %VarInfo{info | positions: (info.positions ++ [{line, column}]) |> Enum.uniq()} - vars_from_scope = Map.put(vars_from_scope, {name, version}, info) + case Map.get(vars_from_scope, {name, version}) do + nil -> + state - %__MODULE__{ - state - | vars_info: [vars_from_scope | other_vars] - } + info -> + info = %VarInfo{info | positions: (info.positions ++ [{line, column}]) |> Enum.uniq()} + vars_from_scope = Map.put(vars_from_scope, {name, version}, info) + + %__MODULE__{ + state + | vars_info: [vars_from_scope | other_vars] + } + end end def add_var_read(%__MODULE__{} = state, _), do: state From eb6ef4f01018bfb9d6b61284d2833a6025f2a456 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 18 Jul 2024 11:06:43 +0200 Subject: [PATCH 092/235] fix test --- test/elixir_sense/core/metadata_builder_test.exs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index d6279800..b9ffefbf 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -1309,15 +1309,13 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.functions == [] - assert state.macros == [] - assert state.requires == [] - assert state.aliases == [] assert state.attributes == [] - assert state.protocols == [] assert state.scope_attributes == [] assert state.vars_info == [] assert state.scope_ids == [] + assert state.doc_context == [] + assert state.typedoc_context == [] + assert state.optional_callbacks_context == [] end describe "moduledoc positions" do From 0181de275afe4cd5294109250da883602f7a97c4 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 18 Jul 2024 11:22:01 +0200 Subject: [PATCH 093/235] remove dbg --- test/elixir_sense/core/compiler_test.exs | 12 ++++++------ test/elixir_sense/core/metadata_builder_test.exs | 3 +-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/test/elixir_sense/core/compiler_test.exs b/test/elixir_sense/core/compiler_test.exs index f78b4b57..c9666641 100644 --- a/test/elixir_sense/core/compiler_test.exs +++ b/test/elixir_sense/core/compiler_test.exs @@ -86,9 +86,9 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do quote do ast = to_quoted!(unquote(code), unquote(ast)) {elixir_expanded, elixir_state, elixir_env} = elixir_expand(ast) - dbg(elixir_expanded) + # dbg(elixir_expanded) {expanded, state, env} = expand(ast) - dbg(expanded) + # dbg(expanded) assert clean_capture_arg(expanded) == clean_capture_arg_elixir(elixir_expanded) assert env == elixir_env @@ -100,11 +100,11 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do quote do ast = to_quoted!(unquote(code), unquote(ast)) {elixir_expanded, elixir_state, elixir_env} = elixir_expand(ast) - dbg(elixir_expanded) - dbg(elixir_ex_to_map(elixir_state)) + # dbg(elixir_expanded) + # dbg(elixir_ex_to_map(elixir_state)) {expanded, state, env} = expand(ast) - dbg(expanded) - dbg(state_to_map(state)) + # dbg(expanded) + # dbg(state_to_map(state)) assert env == elixir_env assert state_to_map(state) == elixir_ex_to_map(elixir_state) diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index b9ffefbf..6339bbb6 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -2524,7 +2524,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do # type: {:map, [b: {:integer, 2}], nil} # } = Enum.find(vars, &(&1.name == :a1)) - vars = state |> get_line_vars(8) |> dbg + vars = state |> get_line_vars(8) assert %VarInfo{ name: :a2, @@ -8166,7 +8166,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do [] env -> - dbg(state) state.vars_info_per_scope_id[env.scope_id] end |> Enum.sort() From 1264e0fc6ef6997f04cbd04c027a624286a0042a Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 19 Jul 2024 06:50:22 +0200 Subject: [PATCH 094/235] add missing delegates --- lib/elixir_sense/core/normalized/macro/env.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/elixir_sense/core/normalized/macro/env.ex b/lib/elixir_sense/core/normalized/macro/env.ex index 23313988..2f354a92 100644 --- a/lib/elixir_sense/core/normalized/macro/env.ex +++ b/lib/elixir_sense/core/normalized/macro/env.ex @@ -4,6 +4,8 @@ defmodule ElixirSense.Core.Normalized.Macro.Env do defdelegate expand_require(env, meta, module, fun, arity, opts), to: Macro.Env defdelegate expand_alias(env, meta, list, opts), to: Macro.Env defdelegate define_alias(env, meta, arg, opts), to: Macro.Env + defdelegate define_require(env, meta, arg, opts), to: Macro.Env + defdelegate define_import(env, meta, arg, opts), to: Macro.Env else def fake_expand_callback(_meta, _args) do {:__block__, [], []} From 24a0fad5a0a5d1838bfe1a53f6f6682767d7798d Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 19 Jul 2024 07:24:28 +0200 Subject: [PATCH 095/235] fix test --- lib/elixir_sense/core/state.ex | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index f3886282..e52ce29f 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -321,8 +321,12 @@ defmodule ElixirSense.Core.State do # filter to return only read [current_vars_info | _] = state.vars_info + # here we filter vars to only return the ones with nil context to maintain macro hygiene + # &n capture args are an exception as they have non nil context everywhere (since elixir 1.17) + # we return them all but the risk of breaking hygiene is small vars = - for {{name, context}, version} <- versioned_vars, context == nil do + for {{name, context}, version} <- versioned_vars, + context == nil or String.starts_with?(to_string(name), "&") do Map.fetch!(current_vars_info, {name, version}) end From 8cdbd941cad25407110eb9a7aabcff43c7043303 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 19 Jul 2024 12:20:01 +0200 Subject: [PATCH 096/235] improve graceful handling of defs with unquote fragments --- lib/elixir_sense/core/compiler.ex | 66 +++--- .../core/metadata_builder_test.exs | 191 +++++++++++------- 2 files changed, 151 insertions(+), 106 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 2a6773b7..61957f9a 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1621,56 +1621,52 @@ defmodule ElixirSense.Core.Compiler do ) when module != nil and def_kind in [:def, :defp, :defmacro, :defmacrop, :defguard, :defguardp] do - # dbg(call) - # dbg(expr) - # dbg(def_kind) - %{vars: vars, unused: unused} = state - # unquoted_call = :elixir_quote.has_unquotes(call) - # unquoted_expr = :elixir_quote.has_unquotes(expr) - # TODO expand the call and expression. - # TODO store mod_fun_to_pos line = Keyword.fetch!(meta, :line) unquoted_call = __MODULE__.Quote.has_unquotes(call) unquoted_expr = __MODULE__.Quote.has_unquotes(expr) + has_unquotes = unquoted_call or unquoted_expr - {call, expr} = - if unquoted_expr or unquoted_call do - {call, expr} = __MODULE__.Quote.escape({call, expr}, :none, true) - - try do - # TODO binding? - {{call, expr}, _} = Code.eval_quoted({call, expr}, [], env) - {call, expr} - rescue - _ -> raise "unable to eval #{inspect({call, expr})}" - end - else - {call, expr} - end + # if there are unquote fragments in either call or body elixir escapes both and evaluates + # if unquoted_expr or unquoted_call, do: __MODULE__.Quote.escape({call, expr}, :none, true) + # instead we try to expand the call and body ignoring the unquotes + # {name_and_args, guards} = __MODULE__.Utils.extract_guards(call) + # elixir raises here if def is invalid, we try to continue with unknown + # especially, we return unknown for calls with unquote fragments {name, _meta_1, args} = case name_and_args do {n, m, a} when is_atom(n) and is_atom(a) -> {n, m, []} {n, m, a} when is_atom(n) and is_list(a) -> {n, m, a} - _ -> raise "invalid_def #{inspect(name_and_args)}" + {n, m, a} when is_atom(a) -> {:__unknown__, m, []} + {n, m, a} when is_list(a) -> {:__unknown__, m, a} + _ -> {:__unknown__, [], []} end arity = length(args) # based on :elixir_def.env_for_expansion state = - %{ - state - | vars: {%{}, false}, - unused: 0, - caller: def_kind in [:defmacro, :defmacrop, :defguard, :defguardp] - } - |> new_func_vars_scope + unless has_unquotes do + # module vars are not accessible in def body + %{ + state + | vars: {%{}, false}, + unused: 0, + caller: def_kind in [:defmacro, :defmacrop, :defguard, :defguardp] + } + |> new_func_vars_scope() + else + # make module variables accessible if there are unquote fragments in def body + %{ + state + | caller: def_kind in [:defmacro, :defmacrop, :defguard, :defguardp] + } + end env_for_expand = %{env | function: {name, arity}} @@ -1716,7 +1712,15 @@ defmodule ElixirSense.Core.Compiler do %{state | vars: vars, unused: unused, caller: false} |> maybe_move_vars_to_outer_scope |> remove_vars_scope - |> remove_func_vars_scope + + state = + unless has_unquotes do + # restore module vars + remove_func_vars_scope(state) + else + # no need to do anything + state + end # result of def expansion is fa tuple {{name, arity}, state, env} diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 6339bbb6..1ec02208 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -578,25 +578,23 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert [] = state |> get_line_vars(3) end - if Version.match?(System.version(), ">= 1.17.0-dev") and @compiler do - test "for bitstring" do - state = - """ - for <> do - record_env() - end + test "for bitstring" do + state = + """ + for <> do record_env() - """ - |> string_to_state + end + record_env() + """ + |> string_to_state - assert Map.keys(state.lines_to_env[2].versioned_vars) == [{:b, nil}, {:g, nil}, {:r, nil}] + assert Map.keys(state.lines_to_env[2].versioned_vars) == [{:b, nil}, {:g, nil}, {:r, nil}] - assert [ - %VarInfo{name: :b, positions: [{1, 19}]}, - %VarInfo{name: :g, positions: [{1, 13}]}, - %VarInfo{name: :r, positions: [{1, 7}]} - ] = state |> get_line_vars(2) - end + assert [ + %VarInfo{name: :b, positions: [{1, 19}]}, + %VarInfo{name: :g, positions: [{1, 13}]}, + %VarInfo{name: :r, positions: [{1, 7}]} + ] = state |> get_line_vars(2) end test "for assignment" do @@ -917,6 +915,32 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] = state |> get_line_vars(8) end + test "in unquote fragment" do + state = + """ + defmodule MyModuleWithFuns do + kv = [foo: 1, bar: 2] |> IO.inspect + Enum.each(kv, fn {k, v} -> + @spec unquote(k)() :: unquote(v) + def unquote(k)() do + unquote(v) + record_env() + end + end) + end + """ + |> string_to_state + + assert Map.keys(state.lines_to_env[7].versioned_vars) == [{:k, nil}, {:kv, nil}, {:v, nil}] + + # TODO we are not tracking usages in typespec + assert [ + %VarInfo{name: :k, positions: [{3, 21}]}, + %VarInfo{name: :kv, positions: [{2, 3}, {3, 13}]}, + %VarInfo{name: :v, positions: [{3, 24}, {6, 15}]} + ] = state |> get_line_vars(7) + end + test "in capture" do state = """ @@ -2514,7 +2538,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - vars = state |> get_line_vars(6) + # vars = state |> get_line_vars(6) # TODO wtf # assert %VarInfo{ @@ -5656,52 +5680,44 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } = state.mods_funs_to_positions end - if @expand_eval do - test "registers defs with unquote fragments" do - state = - """ - defmodule MyModuleWithFuns do - def unquote(:foo)(), do: :ok - def bar(), do: unquote(:ok) - def baz(unquote(:abc)), do: unquote(:abc) - end - """ - |> string_to_state + test "registers defs with unquote fragments in body" do + state = + """ + defmodule MyModuleWithFuns do + kv = [foo: 1] + Enum.each(kv, fn {k, v} -> + def foo(), do: unquote(v) + end) + end + """ + |> string_to_state - assert %{ - {MyModuleWithFuns, :foo, 0} => %ModFunInfo{ - params: [[]] - }, - {MyModuleWithFuns, :bar, 0} => %ModFunInfo{ - params: [[]] - }, - {MyModuleWithFuns, :baz, 1} => %ModFunInfo{ - params: [[:abc]] - } - } = state.mods_funs_to_positions - end + assert %{ + {MyModuleWithFuns, :foo, 0} => %ModFunInfo{ + params: [[]] + } + } = state.mods_funs_to_positions + end - test "registers defs with unquote fragments with binding" do - state = - """ - defmodule MyModuleWithFuns do - kv = [foo: 1, bar: 2] |> IO.inspect - Enum.each(kv, fn {k, v} -> - def unquote(k)(), do: unquote(v) - end) - end - """ - |> string_to_state + test "registers unknown for defs with unquote fragments in call" do + state = + """ + defmodule MyModuleWithFuns do + kv = [foo: 1, bar: 2] + Enum.each(kv, fn {k, v} -> + def unquote(k)(), do: 123 + end) + end + """ + |> string_to_state - assert %{ - {MyModuleWithFuns, :foo, 0} => %ModFunInfo{ - params: [[]] - }, - {MyModuleWithFuns, :bar, 0} => %ModFunInfo{ - params: [[]] - } - } = state.mods_funs_to_positions - end + assert Map.keys(state.mods_funs_to_positions) == [ + {MyModuleWithFuns, :__info__, 1}, + {MyModuleWithFuns, :__unknown__, 0}, + {MyModuleWithFuns, :module_info, 0}, + {MyModuleWithFuns, :module_info, 1}, + {MyModuleWithFuns, nil, nil} + ] end test "registers builtin functions for protocols" do @@ -6338,10 +6354,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do end describe "calls" do - defp sort_calls(calls) do - calls |> Enum.map(fn {k, v} -> {k, Enum.sort(v)} end) |> Map.new() - end - test "registers calls with __MODULE__" do state = """ @@ -7204,6 +7216,46 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } = state.types end + test "registers types with unquote fragments in body" do + state = + """ + defmodule My do + kv = [foo: 1] + Enum.each(kv, fn {k, v} -> + @type foo :: unquote(v) + end) + end + """ + |> string_to_state + + assert %{ + {My, :foo, 0} => %ElixirSense.Core.State.TypeInfo{ + args: [[]], + kind: :type, + name: :foo, + positions: [{4, 5}], + end_positions: [{4, 28}], + generated: [false], + specs: ["@type foo() :: unquote(v())"] + } + } = state.types + end + + test "skips types with unquote fragments in call" do + state = + """ + defmodule My do + kv = [foo: 1] + Enum.each(kv, fn {k, v} -> + @type unquote(k)() :: 123 + end) + end + """ + |> string_to_state + + assert %{} == state.types + end + test "protocol exports type t" do state = """ @@ -8160,17 +8212,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> MetadataBuilder.build() end - defp get_scope_vars(state, line) do - case state.lines_to_env[line] do - nil -> - [] - - env -> - state.vars_info_per_scope_id[env.scope_id] - end - |> Enum.sort() - end - defp get_line_vars(state, line) do case state.lines_to_env[line] do nil -> From 21b3747f7a44ec85bc6b16bf5fd963dd257c35b5 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 19 Jul 2024 22:40:25 +0200 Subject: [PATCH 097/235] resolve TODO --- test/elixir_sense/core/metadata_builder_test.exs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 1ec02208..76142381 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -2538,15 +2538,13 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - # vars = state |> get_line_vars(6) - - # TODO wtf - # assert %VarInfo{ - # name: :a1, - # positions: [{5, 18}], - # scope_id: 6, - # type: {:map, [b: {:integer, 2}], nil} - # } = Enum.find(vars, &(&1.name == :a1)) + vars = state |> get_line_vars(6) + + assert %VarInfo{ + name: :a1, + positions: [{5, 17}], + type: {:map, [b: {:integer, 2}], nil} + } = Enum.find(vars, &(&1.name == :a1)) vars = state |> get_line_vars(8) From f7bc517ed8a8a612ec5e2bb3bdb12bb0db12c443 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 19 Jul 2024 22:47:20 +0200 Subject: [PATCH 098/235] handle dynamic struct --- lib/elixir_sense/core/compiler.ex | 14 ++++++---- .../core/metadata_builder_test.exs | 28 ++++++++----------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 61957f9a..55c72095 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1290,13 +1290,15 @@ defmodule ElixirSense.Core.Compiler do "#{inspect(module)}, defstruct can only be called once per module" end - case fields do - fs when is_list(fs) -> - :ok + fields = + case fields do + fs when is_list(fs) -> + fs - other -> - raise ArgumentError, "struct fields definition must be list, got: #{inspect(other)}" - end + _other -> + # elixir raises ArgumentError here + [] + end {position, end_position} = extract_range(meta) diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 76142381..fd900496 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -6,7 +6,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do alias ElixirSense.Core.State alias ElixirSense.Core.State.{VarInfo, CallInfo, StructInfo, ModFunInfo, AttributeInfo} - @expand_eval false @var_in_ex_unit false describe "versioned_vars" do @@ -6176,22 +6175,19 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } = state.mods_funs_to_positions end - if @expand_eval do - test "find struct fields from expression" do - state = - """ - defmodule MyStruct do - @fields_1 [a: nil] - defstruct [a_field: nil] ++ @fields_1 - end - """ - |> string_to_state + test "gracefully handles struct with expression fields" do + state = + """ + defmodule MyStruct do + @fields_1 [a: nil] + defstruct [a_field: nil] ++ @fields_1 + end + """ + |> string_to_state - # TODO expression is not supported - assert state.structs == %{ - MyStruct => %StructInfo{type: :defstruct, fields: [__struct__: MyStruct]} - } - end + assert state.structs == %{ + MyStruct => %StructInfo{type: :defstruct, fields: [__struct__: MyStruct]} + } end test "find exception" do From bbe14de0e9dc90d7b39084fada8d6c68e193ed10 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 20 Jul 2024 09:00:45 +0200 Subject: [PATCH 099/235] fix defdelegate doc collection --- lib/elixir_sense/core/compiler.ex | 18 ++--- lib/elixir_sense/core/state.ex | 72 ++++++++----------- .../metadata_builder/error_recovery_test.exs | 6 +- .../core/metadata_builder_test.exs | 30 ++++++++ 4 files changed, 68 insertions(+), 58 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 55c72095..23d7a0ab 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -864,30 +864,24 @@ defmodule ElixirSense.Core.Compiler do {opts, state, env} = expand(opts, state, env) target = Kernel.Utils.defdelegate_all(funs, opts, env) - # TODO: Remove List.wrap when multiple funs are no longer supported + # TODO Remove List.wrap when multiple funs are no longer supported by elixir state = funs |> List.wrap() |> Enum.reduce(state, fn fun, state -> - # TODO expand args? {name, args, as, as_args} = Kernel.Utils.defdelegate_each(fun, opts) arity = length(args) state |> add_current_env_to_line(line, %{env | context: nil, function: {name, arity}}) - # TODO use add_func_to_index to collect docs - |> add_mod_fun_to_position( - {module, name, arity}, + |> add_func_to_index( + env, + name, + args, position, end_position, - args, :defdelegate, - "", - # TODO - # doc, - %{delegate_to: {target, as, length(as_args)}}, - # meta - target: {target, as} + target: {target, as, length(as_args)} ) end) diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index e52ce29f..837c1869 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -473,19 +473,18 @@ defmodule ElixirSense.Core.State do state.scope_attributes |> :lists.reverse() |> List.flatten() end - # TODO make priv - def add_mod_fun_to_position( - %__MODULE__{} = state, - {module, fun, arity}, - position, - end_position, - params, - type, - doc, - meta, - options \\ [] - ) - when is_tuple(position) do + defp add_mod_fun_to_position( + %__MODULE__{} = state, + {module, fun, arity}, + position, + end_position, + params, + type, + doc, + meta, + options + ) + when is_tuple(position) do current_info = Map.get(state.mods_funs_to_positions, {module, fun, arity}, %ModFunInfo{}) current_params = current_info |> Map.get(:params, []) current_positions = current_info |> Map.get(:positions, []) @@ -498,6 +497,7 @@ defmodule ElixirSense.Core.State do if fun != nil and arity == nil and current_info.type not in [nil, :defp, :defmacrop, :defguardp] and not match?({true, _}, current_info.overridable) do + # TODO this is no longer needed # in case there are multiple definitions for nil arity prefer public ones # unless this is an overridable def current_info.type @@ -537,14 +537,13 @@ defmodule ElixirSense.Core.State do _state, info, :defdelegate, - {:target, {target_module, target_function}} + {:target, {target, as, _as_arity}} ) do - # TODO wtf - # {module, _state, _env} = expand(target_module_expression, state) + # TODO remove this and rely on meta %ModFunInfo{ info - | target: {target_module, target_function} + | target: {target, as} } end @@ -613,18 +612,6 @@ defmodule ElixirSense.Core.State do ) end - # TODO require end position - def add_func_to_index( - state, - env, - func, - params, - position, - end_position \\ nil, - type, - options \\ [] - ) - def add_func_to_index( %__MODULE__{} = state, env, @@ -633,7 +620,7 @@ defmodule ElixirSense.Core.State do position, end_position, type, - options + options \\ [] ) when (is_tuple(position) and is_tuple(end_position)) or is_nil(end_position) do current_module = env.module @@ -661,19 +648,18 @@ defmodule ElixirSense.Core.State do end end - # meta = - # if type == :defdelegate do - # {target_module, target_fun} = options[:target] - # # {module, _state, _env} = expand(target_module_expression, state) - - # Map.put( - # meta, - # :delegate_to, - # {target_module, target_fun, arity} - # ) - # else - # meta - # end + meta = + if type == :defdelegate do + {target, as, as_arity} = options[:target] + + Map.put( + meta, + :delegate_to, + {target, as, as_arity} + ) + else + meta + end meta = if type in [:defguard, :defguardp] do diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index f4e5b87c..7ebcc172 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -834,7 +834,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do end """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end @@ -845,7 +845,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do end """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end @@ -857,7 +857,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do end """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index fd900496..08379847 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -7932,6 +7932,36 @@ defmodule ElixirSense.Core.MetadataBuilderTest do state.mods_funs_to_positions[{Some, :macro, 0}] end + test "doc is applied to next delegate" do + state = + """ + defmodule Some do + @doc "Some fun" + @doc since: "1.2.3" + defdelegate count(a), to: Enum + end + """ + |> string_to_state + + assert %{doc: "Some fun", meta: %{since: "1.2.3"}} = + state.mods_funs_to_positions[{Some, :count, 1}] + end + + test "doc is applied to next guard" do + state = + """ + defmodule Some do + @doc "Some fun" + @doc since: "1.2.3" + defguard foo(a) when is_integer(a) + end + """ + |> string_to_state + + assert %{doc: "Some fun", meta: %{since: "1.2.3"}} = + state.mods_funs_to_positions[{Some, :foo, 1}] + end + test "doc false is applied to next function" do state = """ From 47a59080043b4fba4d0c1efaf247ebaa8c3c2f13 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 20 Jul 2024 09:03:51 +0200 Subject: [PATCH 100/235] remove not needed code --- lib/elixir_sense/core/state.ex | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 837c1869..c119286b 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -493,18 +493,6 @@ defmodule ElixirSense.Core.State do new_positions = [position | current_positions] new_end_positions = [end_position | current_end_positions] - info_type = - if fun != nil and arity == nil and - current_info.type not in [nil, :defp, :defmacrop, :defguardp] and - not match?({true, _}, current_info.overridable) do - # TODO this is no longer needed - # in case there are multiple definitions for nil arity prefer public ones - # unless this is an overridable def - current_info.type - else - type - end - overridable = current_info |> Map.get(:overridable, false) meta = @@ -518,7 +506,7 @@ defmodule ElixirSense.Core.State do positions: new_positions, end_positions: new_end_positions, params: new_params, - type: info_type, + type: type, doc: doc, meta: meta, generated: [Keyword.get(options, :generated, false) | current_info.generated], From e83d22b7285469f1c23f172d8754bb7eb65305b4 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 20 Jul 2024 09:17:04 +0200 Subject: [PATCH 101/235] remove TODO --- lib/elixir_sense/core/compiler.ex | 8 ++------ lib/elixir_sense/core/state.ex | 8 +------- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 23d7a0ab..b165324e 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1172,11 +1172,7 @@ defmodule ElixirSense.Core.Compiler do state = state - |> add_spec(env, name, type_args, spec, kind, position, end_position, - # TODO ? - # generated: state.generated - generated: false - ) + |> add_spec(env, name, type_args, spec, kind, position, end_position) |> with_typespec({name, length(type_args)}) |> add_current_env_to_line(line, env) |> with_typespec(nil) @@ -4885,7 +4881,7 @@ defmodule ElixirSense.Core.Compiler do {ast, state, env} end - # TODO: Remove char_list type by v2.0 + # TODO Remove char_list type by v2.0 def built_in_type?(:char_list, 0), do: true def built_in_type?(:charlist, 0), do: true def built_in_type?(:as_boolean, 1), do: true diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index c119286b..bfc44c65 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -59,9 +59,6 @@ defmodule ElixirSense.Core.State do stacktrace: boolean(), caller: boolean(), runtime_modules: list(module), - - # TODO ? - # generated: boolean, first_alias_positions: map(), moduledoc_positions: map(), doc_context: list(), @@ -93,9 +90,6 @@ defmodule ElixirSense.Core.State do stacktrace: false, caller: false, runtime_modules: [], - - # TODO ? - # generated: false, first_alias_positions: %{}, moduledoc_positions: %{}, doc_context: [[]], @@ -857,7 +851,7 @@ defmodule ElixirSense.Core.State do kind, pos, end_pos, - options + options \\ [] ) do arg_names = type_args From 13e2e026f0649637ac05e2e425a9cef76f5053a6 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 20 Jul 2024 09:41:09 +0200 Subject: [PATCH 102/235] do not crash on defdelegates with unquote fragments --- lib/elixir_sense/core/compiler.ex | 11 ++++++++++- .../core/metadata_builder_test.exs | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index b165324e..f67ea887 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -862,13 +862,22 @@ defmodule ElixirSense.Core.Compiler do {line, _} = position {opts, state, env} = expand(opts, state, env) - target = Kernel.Utils.defdelegate_all(funs, opts, env) + # elixir does validation here + target = Keyword.get(opts, :to, :__unknown__) # TODO Remove List.wrap when multiple funs are no longer supported by elixir state = funs |> List.wrap() |> Enum.reduce(state, fn fun, state -> + fun = + if __MODULE__.Quote.has_unquotes(fun) do + # dynamic defdelegate - replace unquote expression with fake call + {:__unknown__, [], []} + else + fun + end + {name, args, as, as_args} = Kernel.Utils.defdelegate_each(fun, opts) arity = length(args) diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 08379847..d056424b 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -5677,6 +5677,24 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } = state.mods_funs_to_positions end + test "gracefully handles delegated with unquote fragment" do + state = + """ + defmodule MyModuleWithFuns do + dynamic = :dynamic_flatten + defdelegate unquote(dynamic)(list), to: List, as: :flatten + end + """ + |> string_to_state + + assert %{ + {MyModuleWithFuns, :__unknown__, 0} => %ModFunInfo{ + target: {List, :flatten}, + type: :defdelegate + } + } = state.mods_funs_to_positions + end + test "registers defs with unquote fragments in body" do state = """ From 37c6c867a9c78c05c6ad647ae765bba7a174d045 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 29 Jul 2024 17:59:39 +0200 Subject: [PATCH 103/235] add test coverage to type inference fix type inference tests in metadata builder --- lib/elixir_sense/core/compiler.ex | 37 +- lib/elixir_sense/core/state.ex | 7 +- lib/elixir_sense/core/type_inference.ex | 417 +++++++++--------- .../core/metadata_builder_test.exs | 171 +++---- .../elixir_sense/core/type_inference_test.exs | 417 ++++++++++++++++++ 5 files changed, 745 insertions(+), 304 deletions(-) create mode 100644 test/elixir_sense/core/type_inference_test.exs diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index f67ea887..a5b19df1 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -31,27 +31,30 @@ defmodule ElixirSense.Core.Compiler do {e_right, sr, er} = expand(right, s, e) {e_left, sl, el} = __MODULE__.Clauses.match(&expand/3, left, sr, s, er) - match_context_r = TypeInference.get_binding_type(e_right) - vars_l_with_inferred_types = TypeInference.find_vars(e_left, match_context_r) + match_context_r = TypeInference.get_binding_type(e_right, e.context) + vars_l_with_inferred_types = TypeInference.find_vars(e_left, match_context_r, :match) - expressions_to_refine = TypeInference.find_refinable(e_right, [], e) + expressions_to_refine = TypeInference.find_refinable(e_right, [], e.context) vars_r_with_inferred_types = if expressions_to_refine != [] do # we are in match context and the right side is also a pattern, we can refine types # on the right side using the inferred type of the left side - match_context_l = TypeInference.get_binding_type(e_left) + match_context_l = TypeInference.get_binding_type(e_left, :match) for expr <- expressions_to_refine, reduce: [] do acc -> - vars_in_expr_with_inferred_types = TypeInference.find_vars(expr, match_context_l) + vars_in_expr_with_inferred_types = + TypeInference.find_vars(expr, match_context_l, :match) + acc ++ vars_in_expr_with_inferred_types end else [] end - sl = merge_inferred_types(sl, vars_l_with_inferred_types ++ vars_r_with_inferred_types) + sl = + merge_inferred_types(sl, vars_l_with_inferred_types ++ vars_r_with_inferred_types) {{:=, meta, [e_left, e_right]}, sl, el} end @@ -1229,7 +1232,7 @@ defmodule ElixirSense.Core.Compiler do inferred_type = case e_args do nil -> nil - [arg] -> TypeInference.get_binding_type(arg) + [arg] -> TypeInference.get_binding_type(arg, env.context) end state = @@ -2219,10 +2222,10 @@ defmodule ElixirSense.Core.Compiler do sm = __MODULE__.Env.reset_read(sr, s) {[e_left], sl, el} = __MODULE__.Clauses.head([left], sm, er) - match_context_r = TypeInference.get_binding_type(e_right) + match_context_r = TypeInference.get_binding_type(e_right, e.context) vars_l_with_inferred_types = - TypeInference.find_vars(e_left, {:for_expression, match_context_r}) + TypeInference.find_vars(e_left, {:for_expression, match_context_r}, :match) sl = State.merge_inferred_types(sl, vars_l_with_inferred_types) @@ -2661,7 +2664,7 @@ defmodule ElixirSense.Core.Compiler do def case(meta, e_expr, opts, s, e) do opts = sanitize_opts(opts, [:do]) - match_context = TypeInference.get_binding_type(e_expr) + match_context = TypeInference.get_binding_type(e_expr, e.context) {case_clauses, sa} = Enum.map_reduce(opts, s, fn x, sa -> @@ -2678,7 +2681,9 @@ defmodule ElixirSense.Core.Compiler do fn c, s, e -> case head(c, s, e) do {[h | _] = c, s, e} -> - clause_vars_with_inferred_types = TypeInference.find_vars(h, match_context) + clause_vars_with_inferred_types = + TypeInference.find_vars(h, match_context, :match) + s = State.merge_inferred_types(s, clause_vars_with_inferred_types) {c, s, e} @@ -2796,8 +2801,8 @@ defmodule ElixirSense.Core.Compiler do sm = ElixirEnv.reset_read(sr, s) {[e_left], sl, el} = head([left], sm, er) - match_context_r = TypeInference.get_binding_type(e_right) - vars_l_with_inferred_types = TypeInference.find_vars(e_left, match_context_r) + match_context_r = TypeInference.get_binding_type(e_right, e.context) + vars_l_with_inferred_types = TypeInference.find_vars(e_left, match_context_r, :match) sl = State.merge_inferred_types(sl, vars_l_with_inferred_types) @@ -2930,7 +2935,7 @@ defmodule ElixirSense.Core.Compiler do match_context = {:struct, [], {:atom, Exception}, nil} - vars_with_inferred_types = TypeInference.find_vars(e_left, match_context) + vars_with_inferred_types = TypeInference.find_vars(e_left, match_context, :match) sl = State.merge_inferred_types(sl, vars_with_inferred_types) {e_left, sl, el} @@ -2952,7 +2957,7 @@ defmodule ElixirSense.Core.Compiler do match_context = {:struct, [], {:atom, Exception}, nil} - vars_with_inferred_types = TypeInference.find_vars(e_left, match_context) + vars_with_inferred_types = TypeInference.find_vars(e_left, match_context, :match) sl = State.merge_inferred_types(sl, vars_with_inferred_types) {e_left, sl, el} @@ -2980,7 +2985,7 @@ defmodule ElixirSense.Core.Compiler do match_context end - vars_with_inferred_types = TypeInference.find_vars(e_left, match_context) + vars_with_inferred_types = TypeInference.find_vars(e_left, match_context, :match) sr = State.merge_inferred_types(sr, vars_with_inferred_types) {{:in, meta, [e_left, normalized]}, sr, er} diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index bfc44c65..2aefdd50 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -1315,15 +1315,10 @@ defmodule ElixirSense.Core.State do for {key, type} <- inferred_types, reduce: h do acc -> Map.update!(acc, key, fn %VarInfo{type: old} = v -> - %{v | type: merge_type(old, type)} + %{v | type: ElixirSense.Core.TypeInference.intersect(old, type)} end) end %{state | vars_info: [h | t]} end - - defp merge_type(nil, new), do: new - defp merge_type(old, nil), do: old - defp merge_type(old, old), do: old - defp merge_type(old, new), do: {:intersection, [old, new]} end diff --git a/lib/elixir_sense/core/type_inference.ex b/lib/elixir_sense/core/type_inference.ex index 548a58aa..77d80749 100644 --- a/lib/elixir_sense/core/type_inference.ex +++ b/lib/elixir_sense/core/type_inference.ex @@ -1,101 +1,124 @@ defmodule ElixirSense.Core.TypeInference do - # TODO struct or struct update + def get_binding_type( + {:%, _struct_meta, + [ + _struct_ast, + {:%{}, _map_meta, [{:|, _, _} | _]} + ]}, + :match + ), + do: :none def get_binding_type( {:%, _meta, [ struct_ast, {:%{}, _, _} = ast - ]} + ]}, + context ) do {fields, updated_struct} = - case get_binding_type(ast) do + case get_binding_type(ast, context) do {:map, fields, updated_map} -> {fields, updated_map} {:struct, fields, _, updated_struct} -> {fields, updated_struct} _ -> {[], nil} end - # expand struct type - only compile type atoms or attributes are supported - type = - case get_binding_type(struct_ast) do - {:atom, atom} -> {:atom, atom} - {:attribute, attribute} -> {:attribute, attribute} - _ -> nil - end + type = get_binding_type(struct_ast, context) |> known_struct_type() {:struct, fields, type, updated_struct} end - # pipe - # TODO no pipes in expanded code - # def get_binding_type({:|>, _, [params_1, {call, meta, params_rest}]}) do - # params = [params_1 | params_rest || []] - # get_binding_type({call, meta, params}) - # end - # remote call - def get_binding_type({{:., _, [target, fun]}, _, args}) + def get_binding_type({{:., _, [target, fun]}, _, args}, context) when is_atom(fun) and is_list(args) do - target = get_binding_type(target) - {:call, target, fun, Enum.map(args, &get_binding_type(&1))} + target = get_binding_type(target, context) + {:call, target, fun, Enum.map(args, &get_binding_type(&1, context))} end - # variable or local no parens call - # TODO version? - def get_binding_type({var, _, context}) when is_atom(var) and is_atom(context) do - {:variable, var} + # pinned variable + def get_binding_type({:^, _, [pinned]}, :match), do: get_binding_type(pinned, nil) + def get_binding_type({:^, _, [_pinned]}, _context), do: :none + + # variable + def get_binding_type({:_, _meta, var_context}, context) + when is_atom(var_context) and context != :match, + do: :none + + def get_binding_type({var, meta, var_context}, context) + when is_atom(var) and is_atom(var_context) and + var not in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__, :_] and + context != :match do + case Keyword.fetch(meta, :version) do + {:ok, _version} -> + # TODO include version in type? + {:variable, var} + + _ -> + nil + end end # attribute # expanded attribute reference has nil arg - def get_binding_type({:@, _, [{attribute, _, nil}]}) + def get_binding_type({:@, _, [{attribute, _, nil}]}, _context) when is_atom(attribute) do {:attribute, attribute} end - # erlang module or atom - def get_binding_type(atom) when is_atom(atom) do + # module or atom + def get_binding_type(atom, _context) when is_atom(atom) do {:atom, atom} end - # map update - def get_binding_type( - {:%{}, _meta, - [ - {:|, _meta1, - [ - updated_map, - fields - ]} - ]} - ) - when is_list(fields) do - {:map, get_fields_binding_type(fields), get_binding_type(updated_map)} - end - # map - def get_binding_type({:%{}, _meta, fields}) when is_list(fields) do - field_type = get_fields_binding_type(fields) + def get_binding_type({:%{}, _meta, [{:|, _, _} | _]}, :match), do: :none + + def get_binding_type({:%{}, _meta, ast}, context) do + {updated_map, fields} = + case ast do + [{:|, _, [left, right]}] -> + {get_binding_type(left, context), right} + + list -> + {nil, list} + end - case field_type |> Keyword.fetch(:__struct__) do - {:ok, type} -> {:struct, [], type, nil} - _ -> {:map, field_type, nil} + field_types = get_fields_binding_type(fields, context) + + case field_types |> Keyword.fetch(:__struct__) do + {:ok, type} -> + {:struct, field_types |> Keyword.delete(:__struct__), type |> known_struct_type(), + updated_map} + + _ -> + {:map, field_types, updated_map} end end # match - def get_binding_type({:=, _, [_, ast]}) do - get_binding_type(ast) + def get_binding_type({:=, _, [left, right]}, context) do + intersect(get_binding_type(left, :match), get_binding_type(right, context)) end # stepped range struct - def get_binding_type({:"..//", _, [_, _, _]}) do - {:struct, [], {:atom, Range}, nil} + def get_binding_type({:"..//", _, [first, last, step]}, context) do + {:struct, + [ + first: get_binding_type(first, context), + last: get_binding_type(last, context), + step: get_binding_type(step, context) + ], {:atom, Range}, nil} end # range struct - def get_binding_type({:.., _, [_, _]}) do - {:struct, [], {:atom, Range}, nil} + def get_binding_type({:.., _, [first, last]}, context) do + {:struct, + [ + first: get_binding_type(first, context), + last: get_binding_type(last, context), + step: get_binding_type(1, context) + ], {:atom, Range}, nil} end @builtin_sigils %{ @@ -108,8 +131,8 @@ defmodule ElixirSense.Core.TypeInference do } # builtin sigil struct - def get_binding_type({sigil, _, _}) when is_map_key(@builtin_sigils, sigil) do - # TODO support custom sigils + def get_binding_type({sigil, _, _}, _context) when is_map_key(@builtin_sigils, sigil) do + # TODO support custom sigils? {:struct, [], {:atom, @builtin_sigils |> Map.fetch!(sigil)}, nil} end @@ -117,89 +140,72 @@ defmodule ElixirSense.Core.TypeInference do # regular tuples use {:{}, [], [field_1, field_2]} ast # two element use {field_1, field_2} ast (probably as an optimization) # detect and convert to regular - def get_binding_type(ast) when is_tuple(ast) and tuple_size(ast) == 2 do - get_binding_type({:{}, [], Tuple.to_list(ast)}) + def get_binding_type(ast, context) when is_tuple(ast) and tuple_size(ast) == 2 do + get_binding_type({:{}, [], Tuple.to_list(ast)}, context) end - def get_binding_type({:{}, _, list}) do - {:tuple, length(list), list |> Enum.map(&get_binding_type(&1))} + def get_binding_type({:{}, _, list}, context) do + {:tuple, length(list), list |> Enum.map(&get_binding_type(&1, context))} end - def get_binding_type(list) when is_list(list) do + def get_binding_type(list, context) when is_list(list) do type = case list do - [] -> :empty - [{:|, _, [head, _tail]}] -> get_binding_type(head) - [head | _] -> get_binding_type(head) + [] -> + :empty + + [{:|, _, [head, _tail]}] -> + get_binding_type(head, context) + + [head | _] -> + get_binding_type(head, context) + # TODO ++ end {:list, type} end - def get_binding_type(list) when is_list(list) do - {:list, list |> Enum.map(&get_binding_type(&1))} + def get_binding_type(list, context) when is_list(list) do + {:list, list |> Enum.map(&get_binding_type(&1, context))} end - # pinned variable - def get_binding_type({:^, _, [pinned]}), do: get_binding_type(pinned) - # local call - def get_binding_type({var, _, args}) when is_atom(var) and is_list(args) do - {:local_call, var, Enum.map(args, &get_binding_type(&1))} + def get_binding_type({var, _, args}, context) when is_atom(var) and is_list(args) do + {:local_call, var, Enum.map(args, &get_binding_type(&1, context))} end # integer - def get_binding_type(integer) when is_integer(integer) do + def get_binding_type(integer, _context) when is_integer(integer) do {:integer, integer} end # other - def get_binding_type(_), do: nil + def get_binding_type(_, _), do: nil - defp get_fields_binding_type(fields) do + defp get_fields_binding_type(fields, context) do for {field, value} <- fields, is_atom(field) do - {field, get_binding_type(value)} + {field, get_binding_type(value, context)} end end - def find_vars(ast, match_context) do - {_ast, {vars, _match_context}} = - Macro.prewalk(ast, {[], match_context}, &match_var(&1, &2)) - - vars + # expand struct type - only compile type atoms or attributes are supported + # variables supported in match context + defp known_struct_type(type) do + case type do + {:atom, atom} -> {:atom, atom} + {:attribute, attribute} -> {:attribute, attribute} + {:variable, variable} -> {:variable, variable} + _ -> nil + end end - # TODO not needed - # defp match_var( - # {:in, _meta, - # [ - # left, - # right - # ]}, - # {vars, _match_context} - # ) do - # exception_type = - # case right do - # [elem] -> - # get_binding_type(elem) - - # list when is_list(list) -> - # types = for elem <- list, do: get_binding_type(elem) - # if Enum.all?(types, &match?({:atom, _}, &1)), do: {:atom, Exception} - - # elem -> - # get_binding_type(elem) - # end - - # match_context = - # case exception_type do - # {:atom, atom} -> {:struct, [], {:atom, atom}, nil} - # _ -> nil - # end - - # match_var(left, {vars, match_context}) - # end + def find_vars(ast, match_context, context) do + {_ast, {vars, _match_context, _context}} = + Macro.prewalk(ast, {[], match_context, context}, &match_var(&1, &2)) + + Enum.uniq(vars) + end defp match_var( {:=, _meta, @@ -207,149 +213,136 @@ defmodule ElixirSense.Core.TypeInference do left, right ]}, - {vars, _match_context} + {vars, match_context, context} ) do - {_ast, {vars, _match_context}} = - match_var(left, {vars, get_binding_type(right)}) + {_ast, {vars, _match_context, _context}} = + match_var( + left, + {vars, intersect(match_context, get_binding_type(right, context)), :match} + ) - {_ast, {vars, _match_context}} = - match_var(right, {vars, get_binding_type(left)}) + {_ast, {vars, _match_context, _context}} = + match_var( + right, + {vars, intersect(match_context, get_binding_type(left, :match)), context} + ) - {[], {vars, nil}} + {[], {vars, nil, context}} end + # pinned variable defp match_var( - {:^, _meta, [{var, meta, context}]} = ast, - {vars, match_context} + {:^, _meta, [{var, _var_meta, var_context}]}, + {vars, match_context, context} ) - when is_atom(var) and is_atom(context) and - var not in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__, :_] do - case Keyword.fetch(meta, :version) do - {:ok, version} -> - {nil, {[{{var, version}, match_context} | vars], nil}} - - _ -> - {ast, {vars, match_context}} - end + when is_atom(var) and is_atom(var_context) do + {nil, {vars, match_context, context}} end + # variable defp match_var( - {var, meta, context} = ast, - {vars, match_context} + {var, meta, var_context}, + {vars, match_context, :match} ) - when is_atom(var) and is_atom(context) and + when is_atom(var) and is_atom(var_context) and var not in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__, :_] do case Keyword.fetch(meta, :version) do {:ok, version} -> - {nil, {[{{var, version}, match_context} | vars], nil}} + {nil, {[{{var, version}, match_context} | vars], nil, :match}} _ -> - {ast, {vars, match_context}} + {nil, {vars, match_context, :match}} end end - # drop right side of guard expression as guards cannot define vars - # TODO not needed - # defp match_var({:when, _, [left, _right]}, {vars, match_context}) do - # # TODO should we infer from guard here? - # match_var(left, {vars, match_context}) - # end + defp match_var({:%, _, [type_ast, {:%{}, _, _ast} = map_ast]}, {vars, match_context, context}) do + {_ast, {type_vars, _match_context, _context}} = + match_var( + type_ast, + {[], + if(match_context, do: {:map_key, match_context, get_binding_type(:__struct__, context)}), + context} + ) - defp match_var({:%, _, [type_ast, {:%{}, _, ast}]}, {vars, match_context}) - when not is_nil(match_context) do - # TODO pass mach_context here as map __struct__ key access - {_ast, {type_vars, _match_context}} = match_var(type_ast, {[], nil}) + {_ast, {map_vars, _match_context, _context}} = + match_var(map_ast, {[], match_context, context}) - destructured_vars = - ast - |> Enum.flat_map(fn {key, value_ast} -> - key_type = get_binding_type(key) - - {_ast, {new_vars, _match_context}} = - match_var(value_ast, {[], {:map_key, match_context, key_type}}) + {nil, {vars ++ map_vars ++ type_vars, nil, context}} + end - new_vars - end) + defp match_var({:%{}, _, ast}, {vars, match_context, context}) do + {updated_vars, list} = + case ast do + [{:|, _, [left, right]} | _] -> + if context == :match do + # map update is forbidden in match, we're in invalid code + {[], []} + else + {_ast, {updated_vars, _match_context, _context}} = match_var(left, {[], nil, context}) + {updated_vars, right} + end - {ast, {vars ++ destructured_vars ++ type_vars, nil}} - end + list -> + {[], list} + end - defp match_var({:%{}, _, ast}, {vars, match_context}) when not is_nil(match_context) do destructured_vars = - ast + list |> Enum.flat_map(fn - {:|, _, [_left, _right]} -> - # map update is forbidden in match, we're in invalid code - [] - {key, value_ast} -> - key_type = get_binding_type(key) + key_type = get_binding_type(key, context) - {_ast, {new_vars, _match_context}} = - match_var(value_ast, {[], {:map_key, match_context, key_type}}) + {_ast, {new_vars, _match_context, _context}} = + match_var( + value_ast, + {[], if(match_context, do: {:map_key, match_context, key_type}), context} + ) new_vars end) - {ast, {vars ++ destructured_vars, nil}} + {nil, {vars ++ destructured_vars ++ updated_vars, nil, context}} end # regular tuples use {:{}, [], [field_1, field_2]} ast # two element use `{field_1, field_2}` ast (probably as an optimization) # detect and convert to regular - defp match_var(ast, {vars, match_context}) + defp match_var(ast, {vars, match_context, context}) when is_tuple(ast) and tuple_size(ast) == 2 do - match_var({:{}, [], ast |> Tuple.to_list()}, {vars, match_context}) + match_var({:{}, [], ast |> Tuple.to_list()}, {vars, match_context, context}) end - defp match_var({:{}, _, ast}, {vars, match_context}) when not is_nil(match_context) do - indexed = ast |> Enum.with_index() - total = length(ast) - + defp match_var({:{}, _, ast}, {vars, match_context, context}) do destructured_vars = - indexed + ast + |> Enum.with_index() |> Enum.flat_map(fn {nth_elem_ast, n} -> - bond = - {:tuple, total, - indexed |> Enum.map(&if(n != elem(&1, 1), do: get_binding_type(elem(&1, 0))))} - - match_context = - if match_context != bond do - {:intersection, [match_context, bond]} - else - match_context - end - - {_ast, {new_vars, _match_context}} = - match_var(nth_elem_ast, {[], {:tuple_nth, match_context, n}}) + {_ast, {new_vars, _match_context, _context}} = + match_var( + nth_elem_ast, + {[], if(match_context, do: {:tuple_nth, match_context, n}), context} + ) new_vars end) - {ast, {vars ++ destructured_vars, nil}} + {nil, {vars ++ destructured_vars, nil, context}} end - # two element tuples on the left of `->` are encoded as list `[field1, field2]` - # detect and convert to regular - defp match_var({:->, meta, [[left], right]}, {vars, match_context}) do - match_var({:->, meta, [left, right]}, {vars, match_context}) - end - - defp match_var(list, {vars, match_context}) - when not is_nil(match_context) and is_list(list) do + defp match_var(list, {vars, match_context, context}) when is_list(list) do match_var_list = fn head, tail -> - {_ast, {new_vars_head, _match_context}} = - match_var(head, {[], {:list_head, match_context}}) + {_ast, {new_vars_head, _match_context, _context}} = + match_var(head, {[], if(match_context, do: {:list_head, match_context}), context}) - {_ast, {new_vars_tail, _match_context}} = - match_var(tail, {[], {:list_tail, match_context}}) + {_ast, {new_vars_tail, _match_context, _context}} = + match_var(tail, {[], if(match_context, do: {:list_tail, match_context}), context}) - {list, {vars ++ new_vars_head ++ new_vars_tail, nil}} + {nil, {vars ++ new_vars_head ++ new_vars_tail, nil, context}} end case list do [] -> - {list, {vars, nil}} + {nil, {vars, nil, context}} [{:|, _, [head, tail]}] -> match_var_list.(head, tail) @@ -359,11 +352,33 @@ defmodule ElixirSense.Core.TypeInference do end end - defp match_var(ast, {vars, match_context}) do - {ast, {vars, match_context}} + defp match_var(ast, {vars, match_context, context}) do + {ast, {vars, match_context, context}} end - def find_refinable({:=, _, [left, right]}, acc, e), do: find_refinable(right, [left | acc], e) - def find_refinable(other, acc, e) when e.context == :match, do: [other | acc] + def intersect(nil, new), do: new + def intersect(old, nil), do: old + def intersect(:none, _), do: :none + def intersect(_, :none), do: :none + def intersect(old, old), do: old + + def intersect({:intersection, old}, {:intersection, new}) do + {:intersection, Enum.uniq(old ++ new)} + end + + def intersect({:intersection, old}, new) do + {:intersection, Enum.uniq([new | old])} + end + + def intersect(old, {:intersection, new}) do + {:intersection, Enum.uniq([old | new])} + end + + def intersect(old, new), do: {:intersection, [old, new]} + + def find_refinable({:=, _, [left, right]}, acc, context), + do: find_refinable(right, [left | acc], context) + + def find_refinable(other, acc, :match), do: [other | acc] def find_refinable(_, acc, _), do: acc end diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index d056424b..011ec86f 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -1699,10 +1699,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do }, %VarInfo{ name: :var, - type: - {:tuple_nth, - {:intersection, - [{:attribute, :myattribute}, {:tuple, 2, [{:atom, :ok}, nil]}]}, 1} + type: {:tuple_nth, {:attribute, :myattribute}, 1} } ] = state |> get_line_vars(5) @@ -1713,10 +1710,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do }, %VarInfo{ name: :q1, - type: - {:tuple_nth, - {:intersection, - [{:variable, :q}, {:tuple, 3, [{:variable, :_}, {:variable, :_}, nil]}]}, 2} + type: {:tuple_nth, {:variable, :q}, 2} } ] = state @@ -1856,31 +1850,11 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert [ %VarInfo{ name: :k, - type: { - :tuple_nth, - { - :intersection, - [ - {:for_expression, {:attribute, :myattribute}}, - {:tuple, 2, [nil, {:variable, :v}]} - ] - }, - 0 - } + type: {:tuple_nth, {:for_expression, {:attribute, :myattribute}}, 0} }, %VarInfo{ name: :v, - type: { - :tuple_nth, - { - :intersection, - [ - {:for_expression, {:attribute, :myattribute}}, - {:tuple, 2, [{:variable, :k}, nil]} - ] - }, - 1 - } + type: {:tuple_nth, {:for_expression, {:attribute, :myattribute}}, 1} } ] = state |> get_line_vars(4) end @@ -2068,7 +2042,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do end @attr %{qwe: String} - def map_field(var1) do + def map_field(var1, abc) do var1 = var1.abc var2 = @attr.qwe(0) var3 = abc.cde.efg @@ -2086,7 +2060,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] = state |> get_line_vars(7) assert [ - %VarInfo{name: :var1, type: {:variable, :now}}, + %VarInfo{name: :var1, type: nil}, %VarInfo{name: :var2, type: {:local_call, :now, []}}, %VarInfo{name: :var3, type: {:local_call, :now, [{:atom, :abc}]}}, %VarInfo{name: :var4, type: {:local_call, :now, [{:atom, :abc}]}}, @@ -2094,6 +2068,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] = state |> get_line_vars(16) assert [ + %VarInfo{name: :abc, type: nil}, %VarInfo{name: :var1, type: {:call, {:variable, :var1}, :abc, []}}, %VarInfo{name: :var2, type: {:call, {:attribute, :attr}, :qwe, [{:integer, 0}]}}, %VarInfo{ @@ -2203,7 +2178,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] = state |> get_line_vars(13) |> Enum.filter(&(&1.name == :asd)) assert [ - %VarInfo{name: :x, type: {:intersection, [{:variable, :asd}, {:variable, :z}]}}, + %VarInfo{name: :x, type: {:variable, :asd}}, %VarInfo{name: :z, type: {:variable, :asd}} ] = state |> get_line_vars(15) |> Enum.filter(&(&1.name in [:x, :z])) end @@ -2228,14 +2203,14 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert [ - %VarInfo{name: :var1, type: {:struct, [], {:atom, Date}, nil}}, - %VarInfo{name: :var2, type: {:struct, [], {:atom, Time}, nil}}, - %VarInfo{name: :var3, type: {:struct, [], {:atom, DateTime}, nil}}, - %VarInfo{name: :var4, type: {:struct, [], {:atom, NaiveDateTime}, nil}}, - %VarInfo{name: :var5, type: {:struct, [], {:atom, Regex}, nil}}, - %VarInfo{name: :var6, type: {:struct, [], {:atom, Regex}, nil}}, - %VarInfo{name: :var7, type: {:struct, [], {:atom, Range}, nil}}, - %VarInfo{name: :var8, type: {:struct, [], {:atom, Range}, nil}} + %VarInfo{name: :var1, type: {:struct, _, {:atom, Date}, nil}}, + %VarInfo{name: :var2, type: {:struct, _, {:atom, Time}, nil}}, + %VarInfo{name: :var3, type: {:struct, _, {:atom, DateTime}, nil}}, + %VarInfo{name: :var4, type: {:struct, _, {:atom, NaiveDateTime}, nil}}, + %VarInfo{name: :var5, type: {:struct, _, {:atom, Regex}, nil}}, + %VarInfo{name: :var6, type: {:struct, _, {:atom, Regex}, nil}}, + %VarInfo{name: :var7, type: {:struct, _, {:atom, Range}, nil}}, + %VarInfo{name: :var8, type: {:struct, _, {:atom, Range}, nil}} ] = state |> get_line_vars(11) end @@ -2252,7 +2227,13 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert [ - %VarInfo{name: :var1, type: {:struct, [], {:atom, Range}, nil}} + %VarInfo{ + name: :var1, + type: + {:struct, + [{:first, {:integer, 12}}, {:last, {:integer, 34}}, {:step, {:integer, 2}}], + {:atom, Range}, nil} + } ] = state |> get_line_vars(4) end @@ -2275,11 +2256,11 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert [ %VarInfo{ name: :formatted, - type: {:map_key, {:variable, :state}, {:atom, :formatted}} + type: nil }, %VarInfo{ name: :state, - type: {:struct, [formatted: {:variable, :formatted}], {:atom, MyState}, nil} + type: {:struct, [formatted: nil], {:atom, MyState}, nil} } ] = state |> get_line_vars(3) @@ -2289,7 +2270,25 @@ defmodule ElixirSense.Core.MetadataBuilderTest do }, %VarInfo{ name: :state, - type: {:struct, [formatted: {:variable, :formatted}], {:atom, MyState}, nil} + type: { + :intersection, + [ + {:map, [bar: {:integer, 1}], nil}, + {:map, [foo: {:integer, 1}], nil}, + {:atom, :ok} + ] + } + }, + %VarInfo{ + name: :x, + type: { + :intersection, + [ + {:map, [bar: {:integer, 1}], nil}, + {:map, [foo: {:integer, 1}], nil}, + {:atom, :ok} + ] + } } ] = state |> get_line_vars(7) end @@ -2306,25 +2305,21 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert [ - %VarInfo{ - name: :formatted, - type: {:map_key, {:variable, :state}, {:atom, :formatted}} - }, %VarInfo{ name: :state, - type: {:struct, [formatted: {:variable, :formatted}], {:atom, MyState}, nil} - } - ] = state |> get_line_vars(3) - - assert [ - %VarInfo{ - name: :formatted + type: { + :intersection, + [{:map, [bar: {:integer, 1}], nil}, {:map, [foo: {:integer, 1}], nil}] + } }, %VarInfo{ - name: :state, - type: {:struct, [formatted: {:variable, :formatted}], {:atom, MyState}, nil} + name: :x, + type: { + :intersection, + [{:map, [bar: {:integer, 1}], nil}, {:map, [foo: {:integer, 1}], nil}] + } } - ] = state |> get_line_vars(7) + ] = state |> get_line_vars(3) end test "two way refinement in match context nested case" do @@ -2343,11 +2338,24 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert [ %VarInfo{ - name: :formatted + name: :state, + type: + {:intersection, + [ + {:map, [bar: {:integer, 1}], nil}, + {:map, [foo: {:integer, 1}], nil}, + {:atom, :ok} + ]} }, %VarInfo{ - name: :state, - type: {:struct, [formatted: {:variable, :formatted}], {:atom, MyState}, nil} + name: :x, + type: + {:intersection, + [ + {:map, [bar: {:integer, 1}], nil}, + {:map, [foo: {:integer, 1}], nil}, + {:atom, :ok} + ]} } ] = state |> get_line_vars(5) end @@ -2356,7 +2364,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do state = """ defmodule MyModule do - def some() do + def some(socket) do %MyState{formatted: formatted} = state = socket.assigns.state IO.puts "" end @@ -2369,22 +2377,21 @@ defmodule ElixirSense.Core.MetadataBuilderTest do name: :formatted, type: { :map_key, - { - :call, - {:call, {:variable, :socket}, :assigns, []}, - :state, - [] - }, + {:call, {:call, {:variable, :socket}, :assigns, []}, :state, []}, {:atom, :formatted} } }, + %ElixirSense.Core.State.VarInfo{ + name: :socket, + type: nil + }, %VarInfo{ name: :state, type: {:intersection, [ {:call, {:call, {:variable, :socket}, :assigns, []}, :state, []}, - {:struct, [formatted: {:variable, :formatted}], {:atom, MyState}, nil} + {:struct, [formatted: nil], {:atom, MyState}, nil} ]} } ] = state |> get_line_vars(4) @@ -2407,10 +2414,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert [ %VarInfo{ name: :x, - type: - {:tuple_nth, - {:intersection, - [{:call, {:atom, Some}, :call, []}, {:tuple, 2, [{:atom, :ok}, nil]}]}, 1} + type: {:tuple_nth, {:call, {:atom, Some}, :call, []}, 1} } ] = state |> get_line_vars(5) end @@ -2432,14 +2436,16 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert [ %VarInfo{ name: :res, - type: :todo + type: + {:intersection, + [ + {:tuple, 2, [{:atom, :ok}, nil]}, + {:call, {:atom, Some}, :call, []} + ]} }, %VarInfo{ name: :x, - type: - {:tuple_nth, - {:intersection, - [{:call, {:atom, Some}, :call, []}, {:tuple, 2, [{:atom, :ok}, nil]}]}, 1} + type: {:tuple_nth, {:call, {:atom, Some}, :call, []}, 1} } ] = state |> get_line_vars(5) end @@ -2542,7 +2548,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert %VarInfo{ name: :a1, positions: [{5, 17}], - type: {:map, [b: {:integer, 2}], nil} + type: {:intersection, [{:map, [b: {:integer, 2}], nil}, {:variable, :a}]} } = Enum.find(vars, &(&1.name == :a1)) vars = state |> get_line_vars(8) @@ -2550,7 +2556,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert %VarInfo{ name: :a2, positions: [{7, 18}], - type: {:map, [b: {:variable, :b}], nil} + type: { + :intersection, + [{:map, [b: {:variable, :b}], nil}, {:variable, :a}] + } } = Enum.find(vars, &(&1.name == :a2)) end end diff --git a/test/elixir_sense/core/type_inference_test.exs b/test/elixir_sense/core/type_inference_test.exs new file mode 100644 index 00000000..1776f36e --- /dev/null +++ b/test/elixir_sense/core/type_inference_test.exs @@ -0,0 +1,417 @@ +defmodule ElixirSense.Core.TypeInferenceTest do + use ExUnit.Case, async: true + alias ElixirSense.Core.TypeInference + + describe "find_vars" do + defp find_vars_in(code, match_context \\ nil, context \\ nil) do + ast = + Code.string_to_quoted!(code) + |> Macro.prewalk(fn + {:__aliases__, _, list} -> + Module.concat(list) + + {atom, _meta, var_context} = node when is_atom(atom) and is_atom(var_context) -> + Macro.update_meta(node, &Keyword.put(&1, :version, 1)) + + node -> + node + end) + + TypeInference.find_vars(ast, match_context, context) + end + + test "finds simple variable" do + assert find_vars_in("a", nil, :match) == [{{:a, 1}, nil}] + assert find_vars_in("a", nil) == [] + end + + test "finds simple variable with match context" do + assert find_vars_in("a", {:integer, 1}, :match) == [{{:a, 1}, {:integer, 1}}] + assert find_vars_in("a", {:integer, 1}) == [] + end + + test "does not find special variables" do + assert find_vars_in("__MODULE__") == [] + assert find_vars_in("__MODULE__", nil, :match) == [] + end + + test "does not find _" do + assert find_vars_in("_") == [] + assert find_vars_in("_", nil, :match) == [] + end + + test "does not find other primitives" do + assert find_vars_in("1") == [] + assert find_vars_in("1.3") == [] + assert find_vars_in("\"as\"") == [] + end + + test "does not find pinned variables" do + assert find_vars_in("^a") == [] + assert find_vars_in("^a", nil, :match) == [] + end + + test "finds variables in tuple" do + assert find_vars_in("{}", nil, :match) == [] + assert find_vars_in("{a}", nil, :match) == [{{:a, 1}, nil}] + assert find_vars_in("{a}") == [] + + assert find_vars_in("{a, b}", nil, :match) == [ + {{:a, 1}, nil}, + {{:b, 1}, nil} + ] + + assert find_vars_in("{a, b}") == [] + end + + test "finds variables in tuple with match context" do + assert find_vars_in("{a}", {:integer, 1}, :match) == [ + {{:a, 1}, {:tuple_nth, {:integer, 1}, 0}} + ] + + assert find_vars_in("{a, b}", {:integer, 1}, :match) == [ + { + {:a, 1}, + {:tuple_nth, {:integer, 1}, 0} + }, + { + {:b, 1}, + {:tuple_nth, {:integer, 1}, 1} + } + ] + end + + test "finds variables in list" do + assert find_vars_in("[]", nil, :match) == [] + assert find_vars_in("[a]", nil, :match) == [{{:a, 1}, nil}] + assert find_vars_in("[a]", nil) == [] + + assert find_vars_in("[a, b]", nil, :match) == [ + {{:a, 1}, nil}, + {{:b, 1}, nil} + ] + + assert find_vars_in("[a | b]", nil, :match) == [ + {{:a, 1}, nil}, + {{:b, 1}, nil} + ] + end + + test "finds variables in list with match context" do + assert find_vars_in("[a]", {:integer, 1}, :match) == [ + {{:a, 1}, {:list_head, {:integer, 1}}} + ] + + assert find_vars_in("[a, b]", {:integer, 1}, :match) == [ + {{:a, 1}, {:list_head, {:integer, 1}}}, + {{:b, 1}, {:list_head, {:list_tail, {:integer, 1}}}} + ] + + assert find_vars_in("[a | b]", {:integer, 1}, :match) == [ + {{:a, 1}, {:list_head, {:integer, 1}}}, + {{:b, 1}, {:list_tail, {:integer, 1}}} + ] + end + + test "finds variables in map" do + assert find_vars_in("%{}", nil, :match) == [] + assert find_vars_in("%{a: a}", nil, :match) == [{{:a, 1}, nil}] + assert find_vars_in("%{a: a}", nil) == [] + assert find_vars_in("%{\"a\" => a}", nil, :match) == [{{:a, 1}, nil}] + # NOTE variable keys are forbidden in match + assert find_vars_in("%{a => 1}", nil, :match) == [] + assert find_vars_in("%{a => 1}", nil) == [] + # NOTE map update is forbidden in match + assert find_vars_in("%{a | b: b}", nil, :match) == [] + assert find_vars_in("%{a | b: b}", nil) == [] + end + + test "finds variables in map with match context" do + assert find_vars_in("%{a: a}", {:integer, 1}, :match) == [ + {{:a, 1}, {:map_key, {:integer, 1}, {:atom, :a}}} + ] + end + + test "finds variables in struct" do + assert find_vars_in("%Foo{}", nil, :match) == [] + assert find_vars_in("%Foo{a: a}", nil, :match) == [{{:a, 1}, nil}] + assert find_vars_in("%Foo{a: a}", nil) == [] + assert find_vars_in("%bar{a: a}", nil) == [] + assert find_vars_in("%bar{a: a}", nil, :match) == [{{:a, 1}, nil}, {{:bar, 1}, nil}] + assert find_vars_in("%_{a: a}", nil, :match) == [{{:a, 1}, nil}] + assert find_vars_in("%Foo{a | b: b}", nil) == [] + assert find_vars_in("%Foo{a | b: b}", nil, :match) == [] + end + + test "finds variables in struct with match context" do + assert find_vars_in("%Foo{a: a}", {:integer, 1}, :match) == [ + {{:a, 1}, {:map_key, {:integer, 1}, {:atom, :a}}} + ] + + assert find_vars_in("%bar{a: a}", {:integer, 1}, :match) == [ + {{:a, 1}, {:map_key, {:integer, 1}, {:atom, :a}}}, + {{:bar, 1}, {:map_key, {:integer, 1}, {:atom, :__struct__}}} + ] + end + + test "finds variables in match" do + assert find_vars_in("a = b", nil, :match) == [{{:b, 1}, nil}, {{:a, 1}, nil}] + assert find_vars_in("a = b", nil) == [{{:a, 1}, {:variable, :b}}] + assert find_vars_in("^a = b", nil) == [] + + assert find_vars_in("a = a", nil, :match) == [{{:a, 1}, nil}] + assert find_vars_in("a = a", nil) == [{{:a, 1}, {:variable, :a}}] + + assert find_vars_in("a = b = c", nil, :match) == [ + {{:c, 1}, nil}, + {{:b, 1}, nil}, + {{:a, 1}, nil} + ] + + assert find_vars_in("[a] = b", nil) == [{{:a, 1}, {:list_head, {:variable, :b}}}] + assert find_vars_in("[a] = b", nil, :match) == [{{:b, 1}, {:list, nil}}, {{:a, 1}, nil}] + + assert find_vars_in("[a] = b", {:variable, :x}, :match) == [ + {{:b, 1}, {:intersection, [variable: :x, list: nil]}}, + {{:a, 1}, {:list_head, {:variable, :x}}} + ] + + assert find_vars_in("{a} = b", nil) == [{{:a, 1}, {:tuple_nth, {:variable, :b}, 0}}] + + assert find_vars_in("{a} = b", nil, :match) == [ + {{:b, 1}, {:tuple, 1, [nil]}}, + {{:a, 1}, nil} + ] + + assert find_vars_in("{a} = b", {:variable, :x}, :match) == [ + {{:b, 1}, {:intersection, [{:variable, :x}, {:tuple, 1, [nil]}]}}, + {{:a, 1}, {:tuple_nth, {:variable, :x}, 0}} + ] + + assert find_vars_in("%{foo: a} = b", nil) == [ + {{:a, 1}, {:map_key, {:variable, :b}, {:atom, :foo}}} + ] + + assert find_vars_in("%{foo: a} = b", nil, :match) == [ + {{:b, 1}, {:map, [foo: nil], nil}}, + {{:a, 1}, nil} + ] + + assert find_vars_in("%{foo: a} = b", {:variable, :x}, :match) == [ + {{:b, 1}, {:intersection, [{:variable, :x}, {:map, [foo: nil], nil}]}}, + {{:a, 1}, {:map_key, {:variable, :x}, {:atom, :foo}}} + ] + + assert find_vars_in("%Foo{foo: a} = b", nil) == [ + {{:a, 1}, {:map_key, {:variable, :b}, {:atom, :foo}}} + ] + + assert find_vars_in("%Foo{foo: a} = b", nil, :match) == [ + {{:b, 1}, {:struct, [foo: nil], {:atom, Foo}, nil}}, + {{:a, 1}, nil} + ] + + assert find_vars_in("%Foo{foo: a} = b", {:variable, :x}, :match) == [ + {{:b, 1}, + {:intersection, [{:variable, :x}, {:struct, [foo: nil], {:atom, Foo}, nil}]}}, + {{:a, 1}, {:map_key, {:variable, :x}, {:atom, :foo}}} + ] + + assert find_vars_in("%{foo: a} = %{bar: b} = c", nil) == [ + { + {:a, 1}, + { + :map_key, + {:intersection, [{:map, [bar: nil], nil}, {:variable, :c}]}, + {:atom, :foo} + } + }, + {{:b, 1}, + {:map_key, {:intersection, [{:map, [foo: nil], nil}, {:variable, :c}]}, + {:atom, :bar}}} + ] + + # TODO check how Binding module handles this case + assert find_vars_in("%{foo: a} = %{bar: b} = c", nil, :match) == [ + { + {:c, 1}, + {:intersection, [{:map, [foo: nil], nil}, {:map, [bar: nil], nil}]} + }, + {{:a, 1}, {:map_key, {:map, [bar: nil], nil}, {:atom, :foo}}}, + {{:b, 1}, {:map_key, {:map, [foo: nil], nil}, {:atom, :bar}}} + ] + + assert find_vars_in("%{foo: a} = %{bar: b} = c", {:variable, :x}, :match) == [ + { + {:c, 1}, + { + :intersection, + [{:map, [bar: nil], nil}, {:variable, :x}, {:map, [foo: nil], nil}] + } + }, + { + {:a, 1}, + { + :map_key, + {:intersection, [{:variable, :x}, {:map, [bar: nil], nil}]}, + {:atom, :foo} + } + }, + { + {:b, 1}, + { + :map_key, + {:intersection, [{:variable, :x}, {:map, [foo: nil], nil}]}, + {:atom, :bar} + } + } + ] + end + end + + describe "get_binding_type" do + defp binding_type_in(code, context \\ nil) do + # NOTE binding_type_in works on expanded AST so it expects aliases expanded to atoms + ast = + Code.string_to_quoted!(code) + |> Macro.prewalk(fn + {:__aliases__, _, list} -> + Module.concat(list) + + {atom, _meta, var_context} = node when is_atom(atom) and is_atom(var_context) -> + Macro.update_meta(node, &Keyword.put(&1, :version, 1)) + + node -> + node + end) + + TypeInference.get_binding_type(ast, context) + end + + test "atom" do + assert binding_type_in(":a") == {:atom, :a} + assert binding_type_in("My.Module") == {:atom, My.Module} + assert binding_type_in("nil") == {:atom, nil} + assert binding_type_in("true") == {:atom, true} + assert binding_type_in("false") == {:atom, false} + end + + test "variable" do + assert binding_type_in("a") == {:variable, :a} + assert binding_type_in("a", :match) == nil + assert binding_type_in("^a", :match) == {:variable, :a} + assert binding_type_in("^a") == :none + assert binding_type_in("_", :match) == nil + assert binding_type_in("_") == :none + end + + test "attribute" do + assert binding_type_in("@a") == {:attribute, :a} + end + + test "integer" do + assert binding_type_in("1") == {:integer, 1} + end + + test "list" do + assert binding_type_in("[]") == {:list, :empty} + assert binding_type_in("[a]") == {:list, {:variable, :a}} + assert binding_type_in("[a]", :match) == {:list, nil} + assert binding_type_in("[^a]", :match) == {:list, {:variable, :a}} + assert binding_type_in("[[1]]") == {:list, {:list, {:integer, 1}}} + # TODO intersection a | b? + assert binding_type_in("[a, b]") == {:list, {:variable, :a}} + assert binding_type_in("[a | b]") == {:list, {:variable, :a}} + end + + test "tuple" do + assert binding_type_in("{}") == {:tuple, 0, []} + assert binding_type_in("{a}") == {:tuple, 1, [{:variable, :a}]} + assert binding_type_in("{a, b}") == {:tuple, 2, [{:variable, :a}, {:variable, :b}]} + end + + test "map" do + assert binding_type_in("%{}") == {:map, [], nil} + assert binding_type_in("%{asd: a}") == {:map, [{:asd, {:variable, :a}}], nil} + # NOTE non atom keys are not supported + assert binding_type_in("%{\"asd\" => a}") == {:map, [], nil} + + assert binding_type_in("%{b | asd: a}") == + {:map, [{:asd, {:variable, :a}}], {:variable, :b}} + + assert binding_type_in("%{b | asd: a}", :match) == :none + end + + test "map with __struct__ key" do + assert binding_type_in("%{__struct__: Foo}") == {:struct, [], {:atom, Foo}, nil} + + assert binding_type_in("%{__struct__: Foo, asd: a}") == + {:struct, [{:asd, {:variable, :a}}], {:atom, Foo}, nil} + + assert binding_type_in("%{b | __struct__: Foo, asd: a}") == + {:struct, [{:asd, {:variable, :a}}], {:atom, Foo}, {:variable, :b}} + end + + test "struct" do + assert binding_type_in("%Foo{}") == {:struct, [], {:atom, Foo}, nil} + assert binding_type_in("%a{}") == {:struct, [], {:variable, :a}, nil} + assert binding_type_in("%@a{}") == {:struct, [], {:attribute, :a}, nil} + + assert binding_type_in("%Foo{asd: a}") == + {:struct, [{:asd, {:variable, :a}}], {:atom, Foo}, nil} + + assert binding_type_in("%Foo{b | asd: a}") == + {:struct, [{:asd, {:variable, :a}}], {:atom, Foo}, {:variable, :b}} + + assert binding_type_in("%Foo{b | asd: a}", :match) == :none + end + + test "range" do + assert binding_type_in("a..b") == + {:struct, + [{:first, {:variable, :a}}, {:last, {:variable, :b}}, {:step, {:integer, 1}}], + {:atom, Range}, nil} + + assert binding_type_in("a..b//2") == + {:struct, + [{:first, {:variable, :a}}, {:last, {:variable, :b}}, {:step, {:integer, 2}}], + {:atom, Range}, nil} + end + + test "sigil" do + # NOTE we do not attempt to parse sigils + assert binding_type_in("~r//") == {:struct, [], {:atom, Regex}, nil} + assert binding_type_in("~R//") == {:struct, [], {:atom, Regex}, nil} + assert binding_type_in("~N//") == {:struct, [], {:atom, NaiveDateTime}, nil} + assert binding_type_in("~U//") == {:struct, [], {:atom, DateTime}, nil} + assert binding_type_in("~T//") == {:struct, [], {:atom, Time}, nil} + assert binding_type_in("~D//") == {:struct, [], {:atom, Date}, nil} + end + + test "local call" do + assert binding_type_in("foo(a)") == {:local_call, :foo, [{:variable, :a}]} + end + + test "remote call" do + assert binding_type_in(":foo.bar(a)") == {:call, {:atom, :foo}, :bar, [variable: :a]} + end + + test "match" do + assert binding_type_in("a = 5") == {:integer, 5} + assert binding_type_in("5 = a") == {:intersection, [integer: 5, variable: :a]} + assert binding_type_in("b = 5 = a") == {:intersection, [{:integer, 5}, {:variable, :a}]} + assert binding_type_in("5 = 5") == {:integer, 5} + + assert binding_type_in("%{foo: a} = %{bar: b}") == + {:intersection, [{:map, [foo: nil], nil}, {:map, [bar: {:variable, :b}], nil}]} + + assert binding_type_in("%{foo: a} = %{bar: b}", :match) == + {:intersection, [{:map, [foo: nil], nil}, {:map, [bar: nil], nil}]} + end + + test "other" do + assert binding_type_in("\"asd\"") == nil + assert binding_type_in("1.23") == nil + end + end +end From 58245f26c9075d3105199734c441d9cbf6ff7f8b Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 29 Jul 2024 17:59:59 +0200 Subject: [PATCH 104/235] consistent cursor handling --- lib/elixir_sense/core/compiler.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index a5b19df1..c17b55b2 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -504,7 +504,7 @@ defmodule ElixirSense.Core.Compiler do # Cursor - defp do_expand({:__cursor__, meta, []}, s, e) do + defp do_expand({:__cursor__, meta, args}, s, e) when is_list(args) do s = unless s.cursor_env do s @@ -513,7 +513,7 @@ defmodule ElixirSense.Core.Compiler do s end - {{:__cursor__, meta, []}, s, e} + {{:__cursor__, meta, args}, s, e} end # Super From 210f46be4a5a2e71d00399ec74c8cd0a7ffec46c Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 29 Jul 2024 18:00:11 +0200 Subject: [PATCH 105/235] underscore unused --- lib/elixir_sense/core/compiler.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index c17b55b2..d294c690 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1646,8 +1646,8 @@ defmodule ElixirSense.Core.Compiler do case name_and_args do {n, m, a} when is_atom(n) and is_atom(a) -> {n, m, []} {n, m, a} when is_atom(n) and is_list(a) -> {n, m, a} - {n, m, a} when is_atom(a) -> {:__unknown__, m, []} - {n, m, a} when is_list(a) -> {:__unknown__, m, a} + {_n, m, a} when is_atom(a) -> {:__unknown__, m, []} + {_n, m, a} when is_list(a) -> {:__unknown__, m, a} _ -> {:__unknown__, [], []} end From c58c3caa4a5bd4cbf5e842cae78828ef3ce50242 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 29 Jul 2024 21:28:40 +0200 Subject: [PATCH 106/235] propagate none type from match context --- lib/elixir_sense/core/type_inference.ex | 18 ++++++++++++------ test/elixir_sense/core/type_inference_test.exs | 4 ++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/elixir_sense/core/type_inference.ex b/lib/elixir_sense/core/type_inference.ex index 77d80749..b14f9b43 100644 --- a/lib/elixir_sense/core/type_inference.ex +++ b/lib/elixir_sense/core/type_inference.ex @@ -260,8 +260,10 @@ defmodule ElixirSense.Core.TypeInference do match_var( type_ast, {[], - if(match_context, do: {:map_key, match_context, get_binding_type(:__struct__, context)}), - context} + propagate_context( + match_context, + &{:map_key, &1, get_binding_type(:__struct__, context)} + ), context} ) {_ast, {map_vars, _match_context, _context}} = @@ -295,7 +297,7 @@ defmodule ElixirSense.Core.TypeInference do {_ast, {new_vars, _match_context, _context}} = match_var( value_ast, - {[], if(match_context, do: {:map_key, match_context, key_type}), context} + {[], propagate_context(match_context, &{:map_key, &1, key_type}), context} ) new_vars @@ -320,7 +322,7 @@ defmodule ElixirSense.Core.TypeInference do {_ast, {new_vars, _match_context, _context}} = match_var( nth_elem_ast, - {[], if(match_context, do: {:tuple_nth, match_context, n}), context} + {[], propagate_context(match_context, &{:tuple_nth, &1, n}), context} ) new_vars @@ -332,10 +334,10 @@ defmodule ElixirSense.Core.TypeInference do defp match_var(list, {vars, match_context, context}) when is_list(list) do match_var_list = fn head, tail -> {_ast, {new_vars_head, _match_context, _context}} = - match_var(head, {[], if(match_context, do: {:list_head, match_context}), context}) + match_var(head, {[], propagate_context(match_context, &{:list_head, &1}), context}) {_ast, {new_vars_tail, _match_context, _context}} = - match_var(tail, {[], if(match_context, do: {:list_tail, match_context}), context}) + match_var(tail, {[], propagate_context(match_context, &{:list_tail, &1}), context}) {nil, {vars ++ new_vars_head ++ new_vars_tail, nil, context}} end @@ -356,6 +358,10 @@ defmodule ElixirSense.Core.TypeInference do {ast, {vars, match_context, context}} end + defp propagate_context(nil, _), do: nil + defp propagate_context(:none, _), do: :none + defp propagate_context(match_context, fun), do: fun.(match_context) + def intersect(nil, new), do: new def intersect(old, nil), do: old def intersect(:none, _), do: :none diff --git a/test/elixir_sense/core/type_inference_test.exs b/test/elixir_sense/core/type_inference_test.exs index 1776f36e..1dc0e591 100644 --- a/test/elixir_sense/core/type_inference_test.exs +++ b/test/elixir_sense/core/type_inference_test.exs @@ -54,6 +54,7 @@ defmodule ElixirSense.Core.TypeInferenceTest do test "finds variables in tuple" do assert find_vars_in("{}", nil, :match) == [] assert find_vars_in("{a}", nil, :match) == [{{:a, 1}, nil}] + assert find_vars_in("{a}", :none, :match) == [{{:a, 1}, :none}] assert find_vars_in("{a}") == [] assert find_vars_in("{a, b}", nil, :match) == [ @@ -84,6 +85,7 @@ defmodule ElixirSense.Core.TypeInferenceTest do test "finds variables in list" do assert find_vars_in("[]", nil, :match) == [] assert find_vars_in("[a]", nil, :match) == [{{:a, 1}, nil}] + assert find_vars_in("[a]", :none, :match) == [{{:a, 1}, :none}] assert find_vars_in("[a]", nil) == [] assert find_vars_in("[a, b]", nil, :match) == [ @@ -116,6 +118,7 @@ defmodule ElixirSense.Core.TypeInferenceTest do test "finds variables in map" do assert find_vars_in("%{}", nil, :match) == [] assert find_vars_in("%{a: a}", nil, :match) == [{{:a, 1}, nil}] + assert find_vars_in("%{a: a}", :none, :match) == [{{:a, 1}, :none}] assert find_vars_in("%{a: a}", nil) == [] assert find_vars_in("%{\"a\" => a}", nil, :match) == [{{:a, 1}, nil}] # NOTE variable keys are forbidden in match @@ -135,6 +138,7 @@ defmodule ElixirSense.Core.TypeInferenceTest do test "finds variables in struct" do assert find_vars_in("%Foo{}", nil, :match) == [] assert find_vars_in("%Foo{a: a}", nil, :match) == [{{:a, 1}, nil}] + assert find_vars_in("%Foo{a: a}", :none, :match) == [{{:a, 1}, :none}] assert find_vars_in("%Foo{a: a}", nil) == [] assert find_vars_in("%bar{a: a}", nil) == [] assert find_vars_in("%bar{a: a}", nil, :match) == [{{:a, 1}, nil}, {{:bar, 1}, nil}] From e3f96f90e3f753bfc5ed89ae59b94ff20d51cf3d Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 29 Jul 2024 22:54:34 +0200 Subject: [PATCH 107/235] error tolerance --- lib/elixir_sense/core/compiler.ex | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index d294c690..ab735b61 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -721,10 +721,14 @@ defmodule ElixirSense.Core.Compiler do expand_local(meta, fun, args, state, env) {:error, {:conflict, _module}} -> - raise "conflict" + # elixir raises here, expand args to look for cursor + {_, state, _e} = expand_args(args, state, env) + {{fun, meta, args}, state, env} {:error, {:ambiguous, _module}} -> - raise "ambiguous" + # elixir raises here, expand args to look for cursor + {_, state, _e} = expand_args(args, state, env) + {{fun, meta, args}, state, env} end end @@ -4349,8 +4353,9 @@ defmodule ElixirSense.Core.Compiler do {:import, receiver} -> require_function(meta, receiver, name, arity, e) - {:ambiguous, ambiguous} -> - raise "ambiguous #{inspect(ambiguous)}" + {:ambiguous, [first | _]} -> + # elixir raises here, we return first matching + require_function(meta, first, name, arity, e) false -> if Macro.special_form?(name, arity) do From d721b62049fe7003e42e03a9d632554605041552 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 29 Jul 2024 23:08:10 +0200 Subject: [PATCH 108/235] fix tests --- .../core/metadata_builder/error_recovery_test.exs | 7 ++----- test/elixir_sense/core/metadata_builder_test.exs | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index 7ebcc172..5ecb4fb5 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -852,9 +852,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do test "incomplete clause left side" do code = """ x = foo() - fn - __cursor__() - end + fn \ """ assert {_meta, env} = get_cursor_env(code) @@ -864,8 +862,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do test "incomplete clause left side guard" do code = """ fn - x when __cursor__() - end + x when \ """ assert {meta, env} = get_cursor_env(code) diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 011ec86f..6790f9fa 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -1827,7 +1827,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do name: :var1, type: {:map_key, {:attribute, :myattribute}, {:atom, :error}} }, - # TODO not atom keys currently not supported + # NOTE non atom keys currently not supported %VarInfo{ name: :var2, type: {:map_key, {:attribute, :other}, nil} From f4d02c9387cc2efa82b948f4505e486b817417d2 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 31 Jul 2024 07:43:35 +0200 Subject: [PATCH 109/235] simplify match operator type inference --- lib/elixir_sense/core/compiler.ex | 27 ++++--------------------- lib/elixir_sense/core/type_inference.ex | 8 +------- 2 files changed, 5 insertions(+), 30 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index ab735b61..649fc1b1 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -31,32 +31,13 @@ defmodule ElixirSense.Core.Compiler do {e_right, sr, er} = expand(right, s, e) {e_left, sl, el} = __MODULE__.Clauses.match(&expand/3, left, sr, s, er) - match_context_r = TypeInference.get_binding_type(e_right, e.context) - vars_l_with_inferred_types = TypeInference.find_vars(e_left, match_context_r, :match) - - expressions_to_refine = TypeInference.find_refinable(e_right, [], e.context) - - vars_r_with_inferred_types = - if expressions_to_refine != [] do - # we are in match context and the right side is also a pattern, we can refine types - # on the right side using the inferred type of the left side - match_context_l = TypeInference.get_binding_type(e_left, :match) - - for expr <- expressions_to_refine, reduce: [] do - acc -> - vars_in_expr_with_inferred_types = - TypeInference.find_vars(expr, match_context_l, :match) + e_expr = {:=, meta, [e_left, e_right]} - acc ++ vars_in_expr_with_inferred_types - end - else - [] - end + vars_with_inferred_types = TypeInference.find_vars(e_expr, nil, el.context) - sl = - merge_inferred_types(sl, vars_l_with_inferred_types ++ vars_r_with_inferred_types) + sl = merge_inferred_types(sl, vars_with_inferred_types) - {{:=, meta, [e_left, e_right]}, sl, el} + {e_expr, sl, el} end # Literal operators diff --git a/lib/elixir_sense/core/type_inference.ex b/lib/elixir_sense/core/type_inference.ex index b14f9b43..8905b684 100644 --- a/lib/elixir_sense/core/type_inference.ex +++ b/lib/elixir_sense/core/type_inference.ex @@ -227,7 +227,7 @@ defmodule ElixirSense.Core.TypeInference do {vars, intersect(match_context, get_binding_type(left, :match)), context} ) - {[], {vars, nil, context}} + {nil, {vars, nil, context}} end # pinned variable @@ -381,10 +381,4 @@ defmodule ElixirSense.Core.TypeInference do end def intersect(old, new), do: {:intersection, [old, new]} - - def find_refinable({:=, _, [left, right]}, acc, context), - do: find_refinable(right, [left | acc], context) - - def find_refinable(other, acc, :match), do: [other | acc] - def find_refinable(_, acc, _), do: acc end From 8c8f8910afafa473edde6c47c9101e092baf86db Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 31 Jul 2024 07:44:39 +0200 Subject: [PATCH 110/235] rename --- lib/elixir_sense/core/compiler.ex | 32 ++++++- lib/elixir_sense/core/metadata_builder.ex | 6 +- lib/elixir_sense/core/type_inference.ex | 90 +++++++++---------- .../elixir_sense/core/type_inference_test.exs | 4 +- 4 files changed, 78 insertions(+), 54 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 649fc1b1..5da14ab7 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1129,6 +1129,19 @@ defmodule ElixirSense.Core.Compiler do {{:@, attr_meta, [{kind, kind_meta, [expr]}]}, state, env} :error -> + # elixir throws here + # expand expression in case there's cursor + + state = + state + |> with_typespec({:__unknown__, 0}) + + {expr, state, _tenv} = expand(expr, state, env) + + state = + state + |> with_typespec(nil) + {{:@, attr_meta, [{kind, kind_meta, [expr]}]}, state, env} end end @@ -1217,7 +1230,7 @@ defmodule ElixirSense.Core.Compiler do inferred_type = case e_args do nil -> nil - [arg] -> TypeInference.get_binding_type(arg, env.context) + [arg] -> TypeInference.type_of(arg, env.context) end state = @@ -2207,7 +2220,7 @@ defmodule ElixirSense.Core.Compiler do sm = __MODULE__.Env.reset_read(sr, s) {[e_left], sl, el} = __MODULE__.Clauses.head([left], sm, er) - match_context_r = TypeInference.get_binding_type(e_right, e.context) + match_context_r = TypeInference.type_of(e_right, e.context) vars_l_with_inferred_types = TypeInference.find_vars(e_left, {:for_expression, match_context_r}, :match) @@ -2649,7 +2662,7 @@ defmodule ElixirSense.Core.Compiler do def case(meta, e_expr, opts, s, e) do opts = sanitize_opts(opts, [:do]) - match_context = TypeInference.get_binding_type(e_expr, e.context) + match_context = TypeInference.type_of(e_expr, e.context) {case_clauses, sa} = Enum.map_reduce(opts, s, fn x, sa -> @@ -2786,7 +2799,7 @@ defmodule ElixirSense.Core.Compiler do sm = ElixirEnv.reset_read(sr, s) {[e_left], sl, el} = head([left], sm, er) - match_context_r = TypeInference.get_binding_type(e_right, e.context) + match_context_r = TypeInference.type_of(e_right, e.context) vars_l_with_inferred_types = TypeInference.find_vars(e_left, match_context_r, :match) sl = State.merge_inferred_types(sl, vars_l_with_inferred_types) @@ -4809,6 +4822,17 @@ defmodule ElixirSense.Core.Compiler do ast, {state, env}, fn + {:__cursor__, meta, args} = node, {state, env} when is_list(args) -> + state = + unless state.cursor_env do + state + |> add_cursor_env(meta, env) + else + state + end + + {node, {state, env}} + {:__aliases__, _meta, list} = node, {state, env} when is_list(list) -> {node, state, env} = ElixirExpand.expand(node, state, env) {node, {state, env}} diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index a5f1c3f8..54afcada 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -186,7 +186,7 @@ defmodule ElixirSense.Core.MetadataBuilder do # column = Keyword.fetch!(meta, :column) # state - # |> push_binding_context(get_binding_type(condition_ast)) + # |> push_binding_context(type_of(condition_ast)) # |> add_call_to_line({nil, :case, 2}, {line, column}) # # |> add_current_env_to_line(line) # # |> result(ast) @@ -222,7 +222,7 @@ defmodule ElixirSense.Core.MetadataBuilder do # defp post({atom, meta, [lhs, rhs]} = _ast, state) # when atom in [:=, :<-] do # _line = Keyword.fetch!(meta, :line) - # match_context_r = get_binding_type(rhs) + # match_context_r = type_of(rhs) # match_context_r = # if atom == :<- and match?([:for | _], state.binding_context) do @@ -236,7 +236,7 @@ defmodule ElixirSense.Core.MetadataBuilder do # _vars = # case rhs do # {:=, _, [nested_lhs, _nested_rhs]} -> - # match_context_l = get_binding_type(lhs) + # match_context_l = type_of(lhs) # nested_vars = find_vars(nested_lhs, match_context_l) # vars_l ++ nested_vars diff --git a/lib/elixir_sense/core/type_inference.ex b/lib/elixir_sense/core/type_inference.ex index 8905b684..dd37532f 100644 --- a/lib/elixir_sense/core/type_inference.ex +++ b/lib/elixir_sense/core/type_inference.ex @@ -1,5 +1,5 @@ defmodule ElixirSense.Core.TypeInference do - def get_binding_type( + def type_of( {:%, _struct_meta, [ _struct_ast, @@ -9,7 +9,7 @@ defmodule ElixirSense.Core.TypeInference do ), do: :none - def get_binding_type( + def type_of( {:%, _meta, [ struct_ast, @@ -18,34 +18,34 @@ defmodule ElixirSense.Core.TypeInference do context ) do {fields, updated_struct} = - case get_binding_type(ast, context) do + case type_of(ast, context) do {:map, fields, updated_map} -> {fields, updated_map} {:struct, fields, _, updated_struct} -> {fields, updated_struct} _ -> {[], nil} end - type = get_binding_type(struct_ast, context) |> known_struct_type() + type = type_of(struct_ast, context) |> known_struct_type() {:struct, fields, type, updated_struct} end # remote call - def get_binding_type({{:., _, [target, fun]}, _, args}, context) + def type_of({{:., _, [target, fun]}, _, args}, context) when is_atom(fun) and is_list(args) do - target = get_binding_type(target, context) - {:call, target, fun, Enum.map(args, &get_binding_type(&1, context))} + target = type_of(target, context) + {:call, target, fun, Enum.map(args, &type_of(&1, context))} end # pinned variable - def get_binding_type({:^, _, [pinned]}, :match), do: get_binding_type(pinned, nil) - def get_binding_type({:^, _, [_pinned]}, _context), do: :none + def type_of({:^, _, [pinned]}, :match), do: type_of(pinned, nil) + def type_of({:^, _, [_pinned]}, _context), do: :none # variable - def get_binding_type({:_, _meta, var_context}, context) + def type_of({:_, _meta, var_context}, context) when is_atom(var_context) and context != :match, do: :none - def get_binding_type({var, meta, var_context}, context) + def type_of({var, meta, var_context}, context) when is_atom(var) and is_atom(var_context) and var not in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__, :_] and context != :match do @@ -61,24 +61,24 @@ defmodule ElixirSense.Core.TypeInference do # attribute # expanded attribute reference has nil arg - def get_binding_type({:@, _, [{attribute, _, nil}]}, _context) + def type_of({:@, _, [{attribute, _, nil}]}, _context) when is_atom(attribute) do {:attribute, attribute} end # module or atom - def get_binding_type(atom, _context) when is_atom(atom) do + def type_of(atom, _context) when is_atom(atom) do {:atom, atom} end # map - def get_binding_type({:%{}, _meta, [{:|, _, _} | _]}, :match), do: :none + def type_of({:%{}, _meta, [{:|, _, _} | _]}, :match), do: :none - def get_binding_type({:%{}, _meta, ast}, context) do + def type_of({:%{}, _meta, ast}, context) do {updated_map, fields} = case ast do [{:|, _, [left, right]}] -> - {get_binding_type(left, context), right} + {type_of(left, context), right} list -> {nil, list} @@ -97,27 +97,27 @@ defmodule ElixirSense.Core.TypeInference do end # match - def get_binding_type({:=, _, [left, right]}, context) do - intersect(get_binding_type(left, :match), get_binding_type(right, context)) + def type_of({:=, _, [left, right]}, context) do + intersect(type_of(left, :match), type_of(right, context)) end # stepped range struct - def get_binding_type({:"..//", _, [first, last, step]}, context) do + def type_of({:"..//", _, [first, last, step]}, context) do {:struct, [ - first: get_binding_type(first, context), - last: get_binding_type(last, context), - step: get_binding_type(step, context) + first: type_of(first, context), + last: type_of(last, context), + step: type_of(step, context) ], {:atom, Range}, nil} end # range struct - def get_binding_type({:.., _, [first, last]}, context) do + def type_of({:.., _, [first, last]}, context) do {:struct, [ - first: get_binding_type(first, context), - last: get_binding_type(last, context), - step: get_binding_type(1, context) + first: type_of(first, context), + last: type_of(last, context), + step: type_of(1, context) ], {:atom, Range}, nil} end @@ -131,7 +131,7 @@ defmodule ElixirSense.Core.TypeInference do } # builtin sigil struct - def get_binding_type({sigil, _, _}, _context) when is_map_key(@builtin_sigils, sigil) do + def type_of({sigil, _, _}, _context) when is_map_key(@builtin_sigils, sigil) do # TODO support custom sigils? {:struct, [], {:atom, @builtin_sigils |> Map.fetch!(sigil)}, nil} end @@ -140,52 +140,52 @@ defmodule ElixirSense.Core.TypeInference do # regular tuples use {:{}, [], [field_1, field_2]} ast # two element use {field_1, field_2} ast (probably as an optimization) # detect and convert to regular - def get_binding_type(ast, context) when is_tuple(ast) and tuple_size(ast) == 2 do - get_binding_type({:{}, [], Tuple.to_list(ast)}, context) + def type_of(ast, context) when is_tuple(ast) and tuple_size(ast) == 2 do + type_of({:{}, [], Tuple.to_list(ast)}, context) end - def get_binding_type({:{}, _, list}, context) do - {:tuple, length(list), list |> Enum.map(&get_binding_type(&1, context))} + def type_of({:{}, _, list}, context) do + {:tuple, length(list), list |> Enum.map(&type_of(&1, context))} end - def get_binding_type(list, context) when is_list(list) do + def type_of(list, context) when is_list(list) do type = case list do [] -> :empty [{:|, _, [head, _tail]}] -> - get_binding_type(head, context) + type_of(head, context) [head | _] -> - get_binding_type(head, context) + type_of(head, context) # TODO ++ end {:list, type} end - def get_binding_type(list, context) when is_list(list) do - {:list, list |> Enum.map(&get_binding_type(&1, context))} + def type_of(list, context) when is_list(list) do + {:list, list |> Enum.map(&type_of(&1, context))} end # local call - def get_binding_type({var, _, args}, context) when is_atom(var) and is_list(args) do - {:local_call, var, Enum.map(args, &get_binding_type(&1, context))} + def type_of({var, _, args}, context) when is_atom(var) and is_list(args) do + {:local_call, var, Enum.map(args, &type_of(&1, context))} end # integer - def get_binding_type(integer, _context) when is_integer(integer) do + def type_of(integer, _context) when is_integer(integer) do {:integer, integer} end # other - def get_binding_type(_, _), do: nil + def type_of(_, _), do: nil defp get_fields_binding_type(fields, context) do for {field, value} <- fields, is_atom(field) do - {field, get_binding_type(value, context)} + {field, type_of(value, context)} end end @@ -218,13 +218,13 @@ defmodule ElixirSense.Core.TypeInference do {_ast, {vars, _match_context, _context}} = match_var( left, - {vars, intersect(match_context, get_binding_type(right, context)), :match} + {vars, intersect(match_context, type_of(right, context)), :match} ) {_ast, {vars, _match_context, _context}} = match_var( right, - {vars, intersect(match_context, get_binding_type(left, :match)), context} + {vars, intersect(match_context, type_of(left, :match)), context} ) {nil, {vars, nil, context}} @@ -262,7 +262,7 @@ defmodule ElixirSense.Core.TypeInference do {[], propagate_context( match_context, - &{:map_key, &1, get_binding_type(:__struct__, context)} + &{:map_key, &1, type_of(:__struct__, context)} ), context} ) @@ -292,7 +292,7 @@ defmodule ElixirSense.Core.TypeInference do list |> Enum.flat_map(fn {key, value_ast} -> - key_type = get_binding_type(key, context) + key_type = type_of(key, context) {_ast, {new_vars, _match_context, _context}} = match_var( diff --git a/test/elixir_sense/core/type_inference_test.exs b/test/elixir_sense/core/type_inference_test.exs index 1dc0e591..34477cc4 100644 --- a/test/elixir_sense/core/type_inference_test.exs +++ b/test/elixir_sense/core/type_inference_test.exs @@ -273,7 +273,7 @@ defmodule ElixirSense.Core.TypeInferenceTest do end end - describe "get_binding_type" do + describe "type_of" do defp binding_type_in(code, context \\ nil) do # NOTE binding_type_in works on expanded AST so it expects aliases expanded to atoms ast = @@ -289,7 +289,7 @@ defmodule ElixirSense.Core.TypeInferenceTest do node end) - TypeInference.get_binding_type(ast, context) + TypeInference.type_of(ast, context) end test "atom" do From 54fd777bc9e47b2973c58282286ecff007bcfb4c Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 31 Jul 2024 07:45:42 +0200 Subject: [PATCH 111/235] rename --- lib/elixir_sense/core/type_inference.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/elixir_sense/core/type_inference.ex b/lib/elixir_sense/core/type_inference.ex index dd37532f..8a540ab8 100644 --- a/lib/elixir_sense/core/type_inference.ex +++ b/lib/elixir_sense/core/type_inference.ex @@ -84,7 +84,7 @@ defmodule ElixirSense.Core.TypeInference do {nil, list} end - field_types = get_fields_binding_type(fields, context) + field_types = get_fields_type(fields, context) case field_types |> Keyword.fetch(:__struct__) do {:ok, type} -> @@ -182,7 +182,7 @@ defmodule ElixirSense.Core.TypeInference do # other def type_of(_, _), do: nil - defp get_fields_binding_type(fields, context) do + defp get_fields_type(fields, context) do for {field, value} <- fields, is_atom(field) do {field, type_of(value, context)} From 2638bb059c859ae1b2f945bed1e28972f91808e7 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 31 Jul 2024 07:49:08 +0200 Subject: [PATCH 112/235] rename --- lib/elixir_sense/core/compiler.ex | 14 +- lib/elixir_sense/core/metadata_builder.ex | 12 +- lib/elixir_sense/core/type_inference.ex | 2 +- .../elixir_sense/core/type_inference_test.exs | 150 +++++++++--------- 4 files changed, 89 insertions(+), 89 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 5da14ab7..389de083 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -33,7 +33,7 @@ defmodule ElixirSense.Core.Compiler do e_expr = {:=, meta, [e_left, e_right]} - vars_with_inferred_types = TypeInference.find_vars(e_expr, nil, el.context) + vars_with_inferred_types = TypeInference.find_typed_vars(e_expr, nil, el.context) sl = merge_inferred_types(sl, vars_with_inferred_types) @@ -2223,7 +2223,7 @@ defmodule ElixirSense.Core.Compiler do match_context_r = TypeInference.type_of(e_right, e.context) vars_l_with_inferred_types = - TypeInference.find_vars(e_left, {:for_expression, match_context_r}, :match) + TypeInference.find_typed_vars(e_left, {:for_expression, match_context_r}, :match) sl = State.merge_inferred_types(sl, vars_l_with_inferred_types) @@ -2680,7 +2680,7 @@ defmodule ElixirSense.Core.Compiler do case head(c, s, e) do {[h | _] = c, s, e} -> clause_vars_with_inferred_types = - TypeInference.find_vars(h, match_context, :match) + TypeInference.find_typed_vars(h, match_context, :match) s = State.merge_inferred_types(s, clause_vars_with_inferred_types) @@ -2800,7 +2800,7 @@ defmodule ElixirSense.Core.Compiler do {[e_left], sl, el} = head([left], sm, er) match_context_r = TypeInference.type_of(e_right, e.context) - vars_l_with_inferred_types = TypeInference.find_vars(e_left, match_context_r, :match) + vars_l_with_inferred_types = TypeInference.find_typed_vars(e_left, match_context_r, :match) sl = State.merge_inferred_types(sl, vars_l_with_inferred_types) @@ -2933,7 +2933,7 @@ defmodule ElixirSense.Core.Compiler do match_context = {:struct, [], {:atom, Exception}, nil} - vars_with_inferred_types = TypeInference.find_vars(e_left, match_context, :match) + vars_with_inferred_types = TypeInference.find_typed_vars(e_left, match_context, :match) sl = State.merge_inferred_types(sl, vars_with_inferred_types) {e_left, sl, el} @@ -2955,7 +2955,7 @@ defmodule ElixirSense.Core.Compiler do match_context = {:struct, [], {:atom, Exception}, nil} - vars_with_inferred_types = TypeInference.find_vars(e_left, match_context, :match) + vars_with_inferred_types = TypeInference.find_typed_vars(e_left, match_context, :match) sl = State.merge_inferred_types(sl, vars_with_inferred_types) {e_left, sl, el} @@ -2983,7 +2983,7 @@ defmodule ElixirSense.Core.Compiler do match_context end - vars_with_inferred_types = TypeInference.find_vars(e_left, match_context, :match) + vars_with_inferred_types = TypeInference.find_typed_vars(e_left, match_context, :match) sr = State.merge_inferred_types(sr, vars_with_inferred_types) {{:in, meta, [e_left, normalized]}, sr, er} diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index 54afcada..2ea0038a 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -165,7 +165,7 @@ defmodule ElixirSense.Core.MetadataBuilder do # end # defp pre({:when, _meta, [lhs, _rhs]}, state) do - # _vars = find_vars(lhs, nil) + # _vars = find_typed_vars(lhs, nil) # state # # |> add_vars(vars, true) @@ -231,13 +231,13 @@ defmodule ElixirSense.Core.MetadataBuilder do # match_context_r # end - # vars_l = find_vars(lhs, match_context_r) + # vars_l = find_typed_vars(lhs, match_context_r) # _vars = # case rhs do # {:=, _, [nested_lhs, _nested_rhs]} -> # match_context_l = type_of(lhs) - # nested_vars = find_vars(nested_lhs, match_context_l) + # nested_vars = find_typed_vars(nested_lhs, match_context_l) # vars_l ++ nested_vars @@ -267,15 +267,15 @@ defmodule ElixirSense.Core.MetadataBuilder do # {ast, state} # end - # # defp find_vars(state, ast, match_context \\ nil) + # # defp find_typed_vars(state, ast, match_context \\ nil) - # # defp find_vars(_state, {var, _meta, nil}, _) + # # defp find_typed_vars(_state, {var, _meta, nil}, _) # # when var in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__] do # # # TODO local calls? # # [] # # end - # # defp find_vars(_state, {var, meta, nil}, :rescue) when is_atom(var) do + # # defp find_typed_vars(_state, {var, meta, nil}, :rescue) when is_atom(var) do # # line = Keyword.fetch!(meta, :line) # # column = Keyword.fetch!(meta, :column) # # match_context = {:struct, [], {:atom, Exception}, nil} diff --git a/lib/elixir_sense/core/type_inference.ex b/lib/elixir_sense/core/type_inference.ex index 8a540ab8..b34d5cc4 100644 --- a/lib/elixir_sense/core/type_inference.ex +++ b/lib/elixir_sense/core/type_inference.ex @@ -200,7 +200,7 @@ defmodule ElixirSense.Core.TypeInference do end end - def find_vars(ast, match_context, context) do + def find_typed_vars(ast, match_context, context) do {_ast, {vars, _match_context, _context}} = Macro.prewalk(ast, {[], match_context, context}, &match_var(&1, &2)) diff --git a/test/elixir_sense/core/type_inference_test.exs b/test/elixir_sense/core/type_inference_test.exs index 34477cc4..e29b5297 100644 --- a/test/elixir_sense/core/type_inference_test.exs +++ b/test/elixir_sense/core/type_inference_test.exs @@ -2,8 +2,8 @@ defmodule ElixirSense.Core.TypeInferenceTest do use ExUnit.Case, async: true alias ElixirSense.Core.TypeInference - describe "find_vars" do - defp find_vars_in(code, match_context \\ nil, context \\ nil) do + describe "find_typed_vars" do + defp find_typed_vars_in(code, match_context \\ nil, context \\ nil) do ast = Code.string_to_quoted!(code) |> Macro.prewalk(fn @@ -17,60 +17,60 @@ defmodule ElixirSense.Core.TypeInferenceTest do node end) - TypeInference.find_vars(ast, match_context, context) + TypeInference.find_typed_vars(ast, match_context, context) end test "finds simple variable" do - assert find_vars_in("a", nil, :match) == [{{:a, 1}, nil}] - assert find_vars_in("a", nil) == [] + assert find_typed_vars_in("a", nil, :match) == [{{:a, 1}, nil}] + assert find_typed_vars_in("a", nil) == [] end test "finds simple variable with match context" do - assert find_vars_in("a", {:integer, 1}, :match) == [{{:a, 1}, {:integer, 1}}] - assert find_vars_in("a", {:integer, 1}) == [] + assert find_typed_vars_in("a", {:integer, 1}, :match) == [{{:a, 1}, {:integer, 1}}] + assert find_typed_vars_in("a", {:integer, 1}) == [] end test "does not find special variables" do - assert find_vars_in("__MODULE__") == [] - assert find_vars_in("__MODULE__", nil, :match) == [] + assert find_typed_vars_in("__MODULE__") == [] + assert find_typed_vars_in("__MODULE__", nil, :match) == [] end test "does not find _" do - assert find_vars_in("_") == [] - assert find_vars_in("_", nil, :match) == [] + assert find_typed_vars_in("_") == [] + assert find_typed_vars_in("_", nil, :match) == [] end test "does not find other primitives" do - assert find_vars_in("1") == [] - assert find_vars_in("1.3") == [] - assert find_vars_in("\"as\"") == [] + assert find_typed_vars_in("1") == [] + assert find_typed_vars_in("1.3") == [] + assert find_typed_vars_in("\"as\"") == [] end test "does not find pinned variables" do - assert find_vars_in("^a") == [] - assert find_vars_in("^a", nil, :match) == [] + assert find_typed_vars_in("^a") == [] + assert find_typed_vars_in("^a", nil, :match) == [] end test "finds variables in tuple" do - assert find_vars_in("{}", nil, :match) == [] - assert find_vars_in("{a}", nil, :match) == [{{:a, 1}, nil}] - assert find_vars_in("{a}", :none, :match) == [{{:a, 1}, :none}] - assert find_vars_in("{a}") == [] + assert find_typed_vars_in("{}", nil, :match) == [] + assert find_typed_vars_in("{a}", nil, :match) == [{{:a, 1}, nil}] + assert find_typed_vars_in("{a}", :none, :match) == [{{:a, 1}, :none}] + assert find_typed_vars_in("{a}") == [] - assert find_vars_in("{a, b}", nil, :match) == [ + assert find_typed_vars_in("{a, b}", nil, :match) == [ {{:a, 1}, nil}, {{:b, 1}, nil} ] - assert find_vars_in("{a, b}") == [] + assert find_typed_vars_in("{a, b}") == [] end test "finds variables in tuple with match context" do - assert find_vars_in("{a}", {:integer, 1}, :match) == [ + assert find_typed_vars_in("{a}", {:integer, 1}, :match) == [ {{:a, 1}, {:tuple_nth, {:integer, 1}, 0}} ] - assert find_vars_in("{a, b}", {:integer, 1}, :match) == [ + assert find_typed_vars_in("{a, b}", {:integer, 1}, :match) == [ { {:a, 1}, {:tuple_nth, {:integer, 1}, 0} @@ -83,145 +83,145 @@ defmodule ElixirSense.Core.TypeInferenceTest do end test "finds variables in list" do - assert find_vars_in("[]", nil, :match) == [] - assert find_vars_in("[a]", nil, :match) == [{{:a, 1}, nil}] - assert find_vars_in("[a]", :none, :match) == [{{:a, 1}, :none}] - assert find_vars_in("[a]", nil) == [] + assert find_typed_vars_in("[]", nil, :match) == [] + assert find_typed_vars_in("[a]", nil, :match) == [{{:a, 1}, nil}] + assert find_typed_vars_in("[a]", :none, :match) == [{{:a, 1}, :none}] + assert find_typed_vars_in("[a]", nil) == [] - assert find_vars_in("[a, b]", nil, :match) == [ + assert find_typed_vars_in("[a, b]", nil, :match) == [ {{:a, 1}, nil}, {{:b, 1}, nil} ] - assert find_vars_in("[a | b]", nil, :match) == [ + assert find_typed_vars_in("[a | b]", nil, :match) == [ {{:a, 1}, nil}, {{:b, 1}, nil} ] end test "finds variables in list with match context" do - assert find_vars_in("[a]", {:integer, 1}, :match) == [ + assert find_typed_vars_in("[a]", {:integer, 1}, :match) == [ {{:a, 1}, {:list_head, {:integer, 1}}} ] - assert find_vars_in("[a, b]", {:integer, 1}, :match) == [ + assert find_typed_vars_in("[a, b]", {:integer, 1}, :match) == [ {{:a, 1}, {:list_head, {:integer, 1}}}, {{:b, 1}, {:list_head, {:list_tail, {:integer, 1}}}} ] - assert find_vars_in("[a | b]", {:integer, 1}, :match) == [ + assert find_typed_vars_in("[a | b]", {:integer, 1}, :match) == [ {{:a, 1}, {:list_head, {:integer, 1}}}, {{:b, 1}, {:list_tail, {:integer, 1}}} ] end test "finds variables in map" do - assert find_vars_in("%{}", nil, :match) == [] - assert find_vars_in("%{a: a}", nil, :match) == [{{:a, 1}, nil}] - assert find_vars_in("%{a: a}", :none, :match) == [{{:a, 1}, :none}] - assert find_vars_in("%{a: a}", nil) == [] - assert find_vars_in("%{\"a\" => a}", nil, :match) == [{{:a, 1}, nil}] + assert find_typed_vars_in("%{}", nil, :match) == [] + assert find_typed_vars_in("%{a: a}", nil, :match) == [{{:a, 1}, nil}] + assert find_typed_vars_in("%{a: a}", :none, :match) == [{{:a, 1}, :none}] + assert find_typed_vars_in("%{a: a}", nil) == [] + assert find_typed_vars_in("%{\"a\" => a}", nil, :match) == [{{:a, 1}, nil}] # NOTE variable keys are forbidden in match - assert find_vars_in("%{a => 1}", nil, :match) == [] - assert find_vars_in("%{a => 1}", nil) == [] + assert find_typed_vars_in("%{a => 1}", nil, :match) == [] + assert find_typed_vars_in("%{a => 1}", nil) == [] # NOTE map update is forbidden in match - assert find_vars_in("%{a | b: b}", nil, :match) == [] - assert find_vars_in("%{a | b: b}", nil) == [] + assert find_typed_vars_in("%{a | b: b}", nil, :match) == [] + assert find_typed_vars_in("%{a | b: b}", nil) == [] end test "finds variables in map with match context" do - assert find_vars_in("%{a: a}", {:integer, 1}, :match) == [ + assert find_typed_vars_in("%{a: a}", {:integer, 1}, :match) == [ {{:a, 1}, {:map_key, {:integer, 1}, {:atom, :a}}} ] end test "finds variables in struct" do - assert find_vars_in("%Foo{}", nil, :match) == [] - assert find_vars_in("%Foo{a: a}", nil, :match) == [{{:a, 1}, nil}] - assert find_vars_in("%Foo{a: a}", :none, :match) == [{{:a, 1}, :none}] - assert find_vars_in("%Foo{a: a}", nil) == [] - assert find_vars_in("%bar{a: a}", nil) == [] - assert find_vars_in("%bar{a: a}", nil, :match) == [{{:a, 1}, nil}, {{:bar, 1}, nil}] - assert find_vars_in("%_{a: a}", nil, :match) == [{{:a, 1}, nil}] - assert find_vars_in("%Foo{a | b: b}", nil) == [] - assert find_vars_in("%Foo{a | b: b}", nil, :match) == [] + assert find_typed_vars_in("%Foo{}", nil, :match) == [] + assert find_typed_vars_in("%Foo{a: a}", nil, :match) == [{{:a, 1}, nil}] + assert find_typed_vars_in("%Foo{a: a}", :none, :match) == [{{:a, 1}, :none}] + assert find_typed_vars_in("%Foo{a: a}", nil) == [] + assert find_typed_vars_in("%bar{a: a}", nil) == [] + assert find_typed_vars_in("%bar{a: a}", nil, :match) == [{{:a, 1}, nil}, {{:bar, 1}, nil}] + assert find_typed_vars_in("%_{a: a}", nil, :match) == [{{:a, 1}, nil}] + assert find_typed_vars_in("%Foo{a | b: b}", nil) == [] + assert find_typed_vars_in("%Foo{a | b: b}", nil, :match) == [] end test "finds variables in struct with match context" do - assert find_vars_in("%Foo{a: a}", {:integer, 1}, :match) == [ + assert find_typed_vars_in("%Foo{a: a}", {:integer, 1}, :match) == [ {{:a, 1}, {:map_key, {:integer, 1}, {:atom, :a}}} ] - assert find_vars_in("%bar{a: a}", {:integer, 1}, :match) == [ + assert find_typed_vars_in("%bar{a: a}", {:integer, 1}, :match) == [ {{:a, 1}, {:map_key, {:integer, 1}, {:atom, :a}}}, {{:bar, 1}, {:map_key, {:integer, 1}, {:atom, :__struct__}}} ] end test "finds variables in match" do - assert find_vars_in("a = b", nil, :match) == [{{:b, 1}, nil}, {{:a, 1}, nil}] - assert find_vars_in("a = b", nil) == [{{:a, 1}, {:variable, :b}}] - assert find_vars_in("^a = b", nil) == [] + assert find_typed_vars_in("a = b", nil, :match) == [{{:b, 1}, nil}, {{:a, 1}, nil}] + assert find_typed_vars_in("a = b", nil) == [{{:a, 1}, {:variable, :b}}] + assert find_typed_vars_in("^a = b", nil) == [] - assert find_vars_in("a = a", nil, :match) == [{{:a, 1}, nil}] - assert find_vars_in("a = a", nil) == [{{:a, 1}, {:variable, :a}}] + assert find_typed_vars_in("a = a", nil, :match) == [{{:a, 1}, nil}] + assert find_typed_vars_in("a = a", nil) == [{{:a, 1}, {:variable, :a}}] - assert find_vars_in("a = b = c", nil, :match) == [ + assert find_typed_vars_in("a = b = c", nil, :match) == [ {{:c, 1}, nil}, {{:b, 1}, nil}, {{:a, 1}, nil} ] - assert find_vars_in("[a] = b", nil) == [{{:a, 1}, {:list_head, {:variable, :b}}}] - assert find_vars_in("[a] = b", nil, :match) == [{{:b, 1}, {:list, nil}}, {{:a, 1}, nil}] + assert find_typed_vars_in("[a] = b", nil) == [{{:a, 1}, {:list_head, {:variable, :b}}}] + assert find_typed_vars_in("[a] = b", nil, :match) == [{{:b, 1}, {:list, nil}}, {{:a, 1}, nil}] - assert find_vars_in("[a] = b", {:variable, :x}, :match) == [ + assert find_typed_vars_in("[a] = b", {:variable, :x}, :match) == [ {{:b, 1}, {:intersection, [variable: :x, list: nil]}}, {{:a, 1}, {:list_head, {:variable, :x}}} ] - assert find_vars_in("{a} = b", nil) == [{{:a, 1}, {:tuple_nth, {:variable, :b}, 0}}] + assert find_typed_vars_in("{a} = b", nil) == [{{:a, 1}, {:tuple_nth, {:variable, :b}, 0}}] - assert find_vars_in("{a} = b", nil, :match) == [ + assert find_typed_vars_in("{a} = b", nil, :match) == [ {{:b, 1}, {:tuple, 1, [nil]}}, {{:a, 1}, nil} ] - assert find_vars_in("{a} = b", {:variable, :x}, :match) == [ + assert find_typed_vars_in("{a} = b", {:variable, :x}, :match) == [ {{:b, 1}, {:intersection, [{:variable, :x}, {:tuple, 1, [nil]}]}}, {{:a, 1}, {:tuple_nth, {:variable, :x}, 0}} ] - assert find_vars_in("%{foo: a} = b", nil) == [ + assert find_typed_vars_in("%{foo: a} = b", nil) == [ {{:a, 1}, {:map_key, {:variable, :b}, {:atom, :foo}}} ] - assert find_vars_in("%{foo: a} = b", nil, :match) == [ + assert find_typed_vars_in("%{foo: a} = b", nil, :match) == [ {{:b, 1}, {:map, [foo: nil], nil}}, {{:a, 1}, nil} ] - assert find_vars_in("%{foo: a} = b", {:variable, :x}, :match) == [ + assert find_typed_vars_in("%{foo: a} = b", {:variable, :x}, :match) == [ {{:b, 1}, {:intersection, [{:variable, :x}, {:map, [foo: nil], nil}]}}, {{:a, 1}, {:map_key, {:variable, :x}, {:atom, :foo}}} ] - assert find_vars_in("%Foo{foo: a} = b", nil) == [ + assert find_typed_vars_in("%Foo{foo: a} = b", nil) == [ {{:a, 1}, {:map_key, {:variable, :b}, {:atom, :foo}}} ] - assert find_vars_in("%Foo{foo: a} = b", nil, :match) == [ + assert find_typed_vars_in("%Foo{foo: a} = b", nil, :match) == [ {{:b, 1}, {:struct, [foo: nil], {:atom, Foo}, nil}}, {{:a, 1}, nil} ] - assert find_vars_in("%Foo{foo: a} = b", {:variable, :x}, :match) == [ + assert find_typed_vars_in("%Foo{foo: a} = b", {:variable, :x}, :match) == [ {{:b, 1}, {:intersection, [{:variable, :x}, {:struct, [foo: nil], {:atom, Foo}, nil}]}}, {{:a, 1}, {:map_key, {:variable, :x}, {:atom, :foo}}} ] - assert find_vars_in("%{foo: a} = %{bar: b} = c", nil) == [ + assert find_typed_vars_in("%{foo: a} = %{bar: b} = c", nil) == [ { {:a, 1}, { @@ -236,7 +236,7 @@ defmodule ElixirSense.Core.TypeInferenceTest do ] # TODO check how Binding module handles this case - assert find_vars_in("%{foo: a} = %{bar: b} = c", nil, :match) == [ + assert find_typed_vars_in("%{foo: a} = %{bar: b} = c", nil, :match) == [ { {:c, 1}, {:intersection, [{:map, [foo: nil], nil}, {:map, [bar: nil], nil}]} @@ -245,7 +245,7 @@ defmodule ElixirSense.Core.TypeInferenceTest do {{:b, 1}, {:map_key, {:map, [foo: nil], nil}, {:atom, :bar}}} ] - assert find_vars_in("%{foo: a} = %{bar: b} = c", {:variable, :x}, :match) == [ + assert find_typed_vars_in("%{foo: a} = %{bar: b} = c", {:variable, :x}, :match) == [ { {:c, 1}, { From 8aca570797b8a8cefb7670abf563ff1ec2a8fa1a Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 31 Jul 2024 08:06:03 +0200 Subject: [PATCH 113/235] address todo --- lib/elixir_sense/core/binding.ex | 3 +++ test/elixir_sense/core/binding_test.exs | 6 ++++++ test/elixir_sense/core/type_inference_test.exs | 9 ++++++--- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/elixir_sense/core/binding.ex b/lib/elixir_sense/core/binding.ex index de8c3e33..a866ca7e 100644 --- a/lib/elixir_sense/core/binding.ex +++ b/lib/elixir_sense/core/binding.ex @@ -340,6 +340,7 @@ defmodule ElixirSense.Core.Binding do def do_expand(_env, {:integer, integer}, _stack), do: {:integer, integer} def do_expand(_env, {:union, all}, _stack) do + # TODO implement union for maps and lists? all = Enum.filter(all, &(&1 != :none)) cond do @@ -1332,6 +1333,8 @@ defmodule ElixirSense.Core.Binding do defp combine_intersection(type, nil), do: type defp combine_intersection(type, type), do: type + # NOTE intersection is not strict and does an union on map keys + defp combine_intersection({:struct, fields_1, nil, nil}, {:struct, fields_2, nil, nil}) do keys = (safe_keys(fields_1) ++ safe_keys(fields_2)) |> Enum.uniq() fields = for k <- keys, do: {k, combine_intersection(fields_1[k], fields_2[k])} diff --git a/test/elixir_sense/core/binding_test.exs b/test/elixir_sense/core/binding_test.exs index 46bcec6f..4e1f14ca 100644 --- a/test/elixir_sense/core/binding_test.exs +++ b/test/elixir_sense/core/binding_test.exs @@ -1978,6 +1978,11 @@ defmodule ElixirSense.Core.BindingTest do test "equal" do assert {:atom, A} == Binding.expand(@env, {:intersection, [{:atom, A}, {:atom, A}]}) assert :none == Binding.expand(@env, {:intersection, [{:atom, A}, {:atom, B}]}) + + assert {:atom, A} == Binding.expand(@env, {:union, [{:atom, A}, {:atom, A}]}) + + assert {:union, [{:atom, A}, {:atom, B}]} == + Binding.expand(@env, {:union, [{:atom, A}, {:atom, B}]}) end test "tuple" do @@ -1998,6 +2003,7 @@ defmodule ElixirSense.Core.BindingTest do assert {:map, [], nil} == Binding.expand(@env, {:intersection, [{:map, [], nil}, {:map, [], nil}]}) + # NOTE intersection is not strict and does an union on map keys assert {:map, [{:a, nil}, {:b, {:tuple, 2, [atom: Z, atom: X]}}, {:c, {:atom, C}}], nil} == Binding.expand( @env, diff --git a/test/elixir_sense/core/type_inference_test.exs b/test/elixir_sense/core/type_inference_test.exs index e29b5297..951f8369 100644 --- a/test/elixir_sense/core/type_inference_test.exs +++ b/test/elixir_sense/core/type_inference_test.exs @@ -173,7 +173,11 @@ defmodule ElixirSense.Core.TypeInferenceTest do ] assert find_typed_vars_in("[a] = b", nil) == [{{:a, 1}, {:list_head, {:variable, :b}}}] - assert find_typed_vars_in("[a] = b", nil, :match) == [{{:b, 1}, {:list, nil}}, {{:a, 1}, nil}] + + assert find_typed_vars_in("[a] = b", nil, :match) == [ + {{:b, 1}, {:list, nil}}, + {{:a, 1}, nil} + ] assert find_typed_vars_in("[a] = b", {:variable, :x}, :match) == [ {{:b, 1}, {:intersection, [variable: :x, list: nil]}}, @@ -235,7 +239,6 @@ defmodule ElixirSense.Core.TypeInferenceTest do {:atom, :bar}}} ] - # TODO check how Binding module handles this case assert find_typed_vars_in("%{foo: a} = %{bar: b} = c", nil, :match) == [ { {:c, 1}, @@ -323,7 +326,7 @@ defmodule ElixirSense.Core.TypeInferenceTest do assert binding_type_in("[a]", :match) == {:list, nil} assert binding_type_in("[^a]", :match) == {:list, {:variable, :a}} assert binding_type_in("[[1]]") == {:list, {:list, {:integer, 1}}} - # TODO intersection a | b? + # TODO union a | b? assert binding_type_in("[a, b]") == {:list, {:variable, :a}} assert binding_type_in("[a | b]") == {:list, {:variable, :a}} end From af069851785353084c6f391ccaa6ed707e466465 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 31 Jul 2024 08:13:59 +0200 Subject: [PATCH 114/235] rename --- .../elixir_sense/core/type_inference_test.exs | 114 +++++++++--------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/test/elixir_sense/core/type_inference_test.exs b/test/elixir_sense/core/type_inference_test.exs index 951f8369..9e41c350 100644 --- a/test/elixir_sense/core/type_inference_test.exs +++ b/test/elixir_sense/core/type_inference_test.exs @@ -277,8 +277,8 @@ defmodule ElixirSense.Core.TypeInferenceTest do end describe "type_of" do - defp binding_type_in(code, context \\ nil) do - # NOTE binding_type_in works on expanded AST so it expects aliases expanded to atoms + defp type_of(code, context \\ nil) do + # NOTE type_of works on expanded AST so it expects aliases expanded to atoms ast = Code.string_to_quoted!(code) |> Macro.prewalk(fn @@ -296,90 +296,90 @@ defmodule ElixirSense.Core.TypeInferenceTest do end test "atom" do - assert binding_type_in(":a") == {:atom, :a} - assert binding_type_in("My.Module") == {:atom, My.Module} - assert binding_type_in("nil") == {:atom, nil} - assert binding_type_in("true") == {:atom, true} - assert binding_type_in("false") == {:atom, false} + assert type_of(":a") == {:atom, :a} + assert type_of("My.Module") == {:atom, My.Module} + assert type_of("nil") == {:atom, nil} + assert type_of("true") == {:atom, true} + assert type_of("false") == {:atom, false} end test "variable" do - assert binding_type_in("a") == {:variable, :a} - assert binding_type_in("a", :match) == nil - assert binding_type_in("^a", :match) == {:variable, :a} - assert binding_type_in("^a") == :none - assert binding_type_in("_", :match) == nil - assert binding_type_in("_") == :none + assert type_of("a") == {:variable, :a} + assert type_of("a", :match) == nil + assert type_of("^a", :match) == {:variable, :a} + assert type_of("^a") == :none + assert type_of("_", :match) == nil + assert type_of("_") == :none end test "attribute" do - assert binding_type_in("@a") == {:attribute, :a} + assert type_of("@a") == {:attribute, :a} end test "integer" do - assert binding_type_in("1") == {:integer, 1} + assert type_of("1") == {:integer, 1} end test "list" do - assert binding_type_in("[]") == {:list, :empty} - assert binding_type_in("[a]") == {:list, {:variable, :a}} - assert binding_type_in("[a]", :match) == {:list, nil} - assert binding_type_in("[^a]", :match) == {:list, {:variable, :a}} - assert binding_type_in("[[1]]") == {:list, {:list, {:integer, 1}}} + assert type_of("[]") == {:list, :empty} + assert type_of("[a]") == {:list, {:variable, :a}} + assert type_of("[a]", :match) == {:list, nil} + assert type_of("[^a]", :match) == {:list, {:variable, :a}} + assert type_of("[[1]]") == {:list, {:list, {:integer, 1}}} # TODO union a | b? - assert binding_type_in("[a, b]") == {:list, {:variable, :a}} - assert binding_type_in("[a | b]") == {:list, {:variable, :a}} + assert type_of("[a, b]") == {:list, {:variable, :a}} + assert type_of("[a | b]") == {:list, {:variable, :a}} end test "tuple" do - assert binding_type_in("{}") == {:tuple, 0, []} - assert binding_type_in("{a}") == {:tuple, 1, [{:variable, :a}]} - assert binding_type_in("{a, b}") == {:tuple, 2, [{:variable, :a}, {:variable, :b}]} + assert type_of("{}") == {:tuple, 0, []} + assert type_of("{a}") == {:tuple, 1, [{:variable, :a}]} + assert type_of("{a, b}") == {:tuple, 2, [{:variable, :a}, {:variable, :b}]} end test "map" do - assert binding_type_in("%{}") == {:map, [], nil} - assert binding_type_in("%{asd: a}") == {:map, [{:asd, {:variable, :a}}], nil} + assert type_of("%{}") == {:map, [], nil} + assert type_of("%{asd: a}") == {:map, [{:asd, {:variable, :a}}], nil} # NOTE non atom keys are not supported - assert binding_type_in("%{\"asd\" => a}") == {:map, [], nil} + assert type_of("%{\"asd\" => a}") == {:map, [], nil} - assert binding_type_in("%{b | asd: a}") == + assert type_of("%{b | asd: a}") == {:map, [{:asd, {:variable, :a}}], {:variable, :b}} - assert binding_type_in("%{b | asd: a}", :match) == :none + assert type_of("%{b | asd: a}", :match) == :none end test "map with __struct__ key" do - assert binding_type_in("%{__struct__: Foo}") == {:struct, [], {:atom, Foo}, nil} + assert type_of("%{__struct__: Foo}") == {:struct, [], {:atom, Foo}, nil} - assert binding_type_in("%{__struct__: Foo, asd: a}") == + assert type_of("%{__struct__: Foo, asd: a}") == {:struct, [{:asd, {:variable, :a}}], {:atom, Foo}, nil} - assert binding_type_in("%{b | __struct__: Foo, asd: a}") == + assert type_of("%{b | __struct__: Foo, asd: a}") == {:struct, [{:asd, {:variable, :a}}], {:atom, Foo}, {:variable, :b}} end test "struct" do - assert binding_type_in("%Foo{}") == {:struct, [], {:atom, Foo}, nil} - assert binding_type_in("%a{}") == {:struct, [], {:variable, :a}, nil} - assert binding_type_in("%@a{}") == {:struct, [], {:attribute, :a}, nil} + assert type_of("%Foo{}") == {:struct, [], {:atom, Foo}, nil} + assert type_of("%a{}") == {:struct, [], {:variable, :a}, nil} + assert type_of("%@a{}") == {:struct, [], {:attribute, :a}, nil} - assert binding_type_in("%Foo{asd: a}") == + assert type_of("%Foo{asd: a}") == {:struct, [{:asd, {:variable, :a}}], {:atom, Foo}, nil} - assert binding_type_in("%Foo{b | asd: a}") == + assert type_of("%Foo{b | asd: a}") == {:struct, [{:asd, {:variable, :a}}], {:atom, Foo}, {:variable, :b}} - assert binding_type_in("%Foo{b | asd: a}", :match) == :none + assert type_of("%Foo{b | asd: a}", :match) == :none end test "range" do - assert binding_type_in("a..b") == + assert type_of("a..b") == {:struct, [{:first, {:variable, :a}}, {:last, {:variable, :b}}, {:step, {:integer, 1}}], {:atom, Range}, nil} - assert binding_type_in("a..b//2") == + assert type_of("a..b//2") == {:struct, [{:first, {:variable, :a}}, {:last, {:variable, :b}}, {:step, {:integer, 2}}], {:atom, Range}, nil} @@ -387,38 +387,38 @@ defmodule ElixirSense.Core.TypeInferenceTest do test "sigil" do # NOTE we do not attempt to parse sigils - assert binding_type_in("~r//") == {:struct, [], {:atom, Regex}, nil} - assert binding_type_in("~R//") == {:struct, [], {:atom, Regex}, nil} - assert binding_type_in("~N//") == {:struct, [], {:atom, NaiveDateTime}, nil} - assert binding_type_in("~U//") == {:struct, [], {:atom, DateTime}, nil} - assert binding_type_in("~T//") == {:struct, [], {:atom, Time}, nil} - assert binding_type_in("~D//") == {:struct, [], {:atom, Date}, nil} + assert type_of("~r//") == {:struct, [], {:atom, Regex}, nil} + assert type_of("~R//") == {:struct, [], {:atom, Regex}, nil} + assert type_of("~N//") == {:struct, [], {:atom, NaiveDateTime}, nil} + assert type_of("~U//") == {:struct, [], {:atom, DateTime}, nil} + assert type_of("~T//") == {:struct, [], {:atom, Time}, nil} + assert type_of("~D//") == {:struct, [], {:atom, Date}, nil} end test "local call" do - assert binding_type_in("foo(a)") == {:local_call, :foo, [{:variable, :a}]} + assert type_of("foo(a)") == {:local_call, :foo, [{:variable, :a}]} end test "remote call" do - assert binding_type_in(":foo.bar(a)") == {:call, {:atom, :foo}, :bar, [variable: :a]} + assert type_of(":foo.bar(a)") == {:call, {:atom, :foo}, :bar, [variable: :a]} end test "match" do - assert binding_type_in("a = 5") == {:integer, 5} - assert binding_type_in("5 = a") == {:intersection, [integer: 5, variable: :a]} - assert binding_type_in("b = 5 = a") == {:intersection, [{:integer, 5}, {:variable, :a}]} - assert binding_type_in("5 = 5") == {:integer, 5} + assert type_of("a = 5") == {:integer, 5} + assert type_of("5 = a") == {:intersection, [integer: 5, variable: :a]} + assert type_of("b = 5 = a") == {:intersection, [{:integer, 5}, {:variable, :a}]} + assert type_of("5 = 5") == {:integer, 5} - assert binding_type_in("%{foo: a} = %{bar: b}") == + assert type_of("%{foo: a} = %{bar: b}") == {:intersection, [{:map, [foo: nil], nil}, {:map, [bar: {:variable, :b}], nil}]} - assert binding_type_in("%{foo: a} = %{bar: b}", :match) == + assert type_of("%{foo: a} = %{bar: b}", :match) == {:intersection, [{:map, [foo: nil], nil}, {:map, [bar: nil], nil}]} end test "other" do - assert binding_type_in("\"asd\"") == nil - assert binding_type_in("1.23") == nil + assert type_of("\"asd\"") == nil + assert type_of("1.23") == nil end end end From 9340ddd41b84fef12698e7ee21acd0f8c6ce41f3 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 1 Aug 2024 00:44:41 +0200 Subject: [PATCH 115/235] handle ++ and -- operators in binding --- lib/elixir_sense/core/binding.ex | 21 +++++++++++++++++++++ test/elixir_sense/core/binding_test.exs | 15 +++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/lib/elixir_sense/core/binding.ex b/lib/elixir_sense/core/binding.ex index a866ca7e..99e3244d 100644 --- a/lib/elixir_sense/core/binding.ex +++ b/lib/elixir_sense/core/binding.ex @@ -395,6 +395,27 @@ defmodule ElixirSense.Core.Binding do end end + defp expand_call( + env, + {:atom, :erlang}, + name, + [list_candidate | _], + _include_private, + stack + ) + when name in [:++, :--] do + case expand(env, list_candidate, stack) do + {:list, type} -> + {:list, type} + + nil -> + nil + + _ -> + :none + end + end + defp expand_call( env, {:atom, Kernel}, diff --git a/test/elixir_sense/core/binding_test.exs b/test/elixir_sense/core/binding_test.exs index 4e1f14ca..9cc5ccce 100644 --- a/test/elixir_sense/core/binding_test.exs +++ b/test/elixir_sense/core/binding_test.exs @@ -1613,7 +1613,22 @@ defmodule ElixirSense.Core.BindingTest do end end + describe ":erlang functions" do + test "++" do + assert {:list, {:integer, 1}} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{name: :a, type: {:integer, 1}}, + %VarInfo{name: :b, type: {:integer, 2}} + ]), + {:call, {:atom, :erlang}, :++, [list: {:variable, :a}, list: {:variable, :b}]} + ) + end + end + describe "Kernel functions" do + # TODO check which of those get rewritten test "tuple elem" do assert {:atom, :a} == Binding.expand( From caab9dca8baa98e83188633c286e6e8faeee0c87 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 1 Aug 2024 00:45:22 +0200 Subject: [PATCH 116/235] avoid endless loop on self referencing types --- lib/elixir_sense/core/binding.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/elixir_sense/core/binding.ex b/lib/elixir_sense/core/binding.ex index 99e3244d..e2f76b50 100644 --- a/lib/elixir_sense/core/binding.ex +++ b/lib/elixir_sense/core/binding.ex @@ -1336,9 +1336,9 @@ defmodule ElixirSense.Core.Binding do defp expand_type_from_introspection(env, mod, type_name, arity, include_private) do case TypeInfo.get_type_spec(mod, type_name, arity) do {kind, spec} when type_is_public(kind, include_private) -> - {:"::", _, [_, type]} = Typespec.type_to_quoted(spec) + {:"::", _, [{expanded_name, _, _}, type]} = Typespec.type_to_quoted(spec) - parse_type(env, type, mod, include_private) + if type_name != expanded_name, do: parse_type(env, type, mod, include_private) _ -> nil From 456fdb0b3af580ae772116d60d426a51e87e224d Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 1 Aug 2024 00:45:46 +0200 Subject: [PATCH 117/235] expand any, term, dynamic --- lib/elixir_sense/core/binding.ex | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/elixir_sense/core/binding.ex b/lib/elixir_sense/core/binding.ex index e2f76b50..dad4d65b 100644 --- a/lib/elixir_sense/core/binding.ex +++ b/lib/elixir_sense/core/binding.ex @@ -1269,6 +1269,10 @@ defmodule ElixirSense.Core.Binding do # no_return defp parse_type(_env, {:no_return, _, _}, _, _include_private), do: :none + # term, any, dynamic + defp parse_type(_env, {kind, _, _}, _, _include_private) when kind in [:term, :any, :dynamic], + do: nil + # local user type defp parse_type(env, {atom, _, args}, mod, include_private) when is_atom(atom) do # propagate include_private when expanding local types From 9d587f7eaf0455886692929910273c58c19bc916 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 1 Aug 2024 00:46:21 +0200 Subject: [PATCH 118/235] infer types in ++ operator --- lib/elixir_sense/core/type_inference.ex | 6 +++ .../elixir_sense/core/type_inference_test.exs | 51 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/lib/elixir_sense/core/type_inference.ex b/lib/elixir_sense/core/type_inference.ex index b34d5cc4..d96b6505 100644 --- a/lib/elixir_sense/core/type_inference.ex +++ b/lib/elixir_sense/core/type_inference.ex @@ -331,6 +331,12 @@ defmodule ElixirSense.Core.TypeInference do {nil, {vars ++ destructured_vars, nil, context}} end + defp match_var({{:., _, [:erlang, :++]}, _, [left, right]}, {vars, match_context, context}) + when is_list(left) do + # NOTE this may produce improper lists + match_var(left ++ right, {vars, match_context, context}) + end + defp match_var(list, {vars, match_context, context}) when is_list(list) do match_var_list = fn head, tail -> {_ast, {new_vars_head, _match_context, _context}} = diff --git a/test/elixir_sense/core/type_inference_test.exs b/test/elixir_sense/core/type_inference_test.exs index 9e41c350..5d761169 100644 --- a/test/elixir_sense/core/type_inference_test.exs +++ b/test/elixir_sense/core/type_inference_test.exs @@ -113,6 +113,45 @@ defmodule ElixirSense.Core.TypeInferenceTest do {{:a, 1}, {:list_head, {:integer, 1}}}, {{:b, 1}, {:list_tail, {:integer, 1}}} ] + + assert find_typed_vars_in("[1, a | b]", {:integer, 1}, :match) == [ + {{:a, 1}, {:list_head, {:list_tail, {:integer, 1}}}}, + {{:b, 1}, {:list_tail, {:list_tail, {:integer, 1}}}} + ] + + assert find_typed_vars_in("[a | 1]", {:integer, 1}, :match) == [ + {{:a, 1}, {:list_head, {:integer, 1}}} + ] + end + + test "finds variables in list operator" do + assert find_typed_vars_in(":erlang.++([a], [5])", {:integer, 1}, :match) == [ + {{:a, 1}, {:list_head, {:integer, 1}}} + ] + + assert find_typed_vars_in(":erlang.++([a], 5)", {:integer, 1}, :match) == [ + {{:a, 1}, {:list_head, {:integer, 1}}} + ] + + assert find_typed_vars_in(":erlang.++([2, a], [5])", {:integer, 1}, :match) == [ + {{:a, 1}, {:list_head, {:list_tail, {:integer, 1}}}} + ] + + assert find_typed_vars_in(":erlang.++([5], [a])", {:integer, 1}, :match) == [ + {{:a, 1}, {:list_head, {:list_tail, {:integer, 1}}}} + ] + + assert find_typed_vars_in(":erlang.++([5], [2, a])", {:integer, 1}, :match) == [ + {{:a, 1}, {:list_head, {:list_tail, {:list_tail, {:integer, 1}}}}} + ] + + assert find_typed_vars_in(":erlang.++([5], a)", {:integer, 1}, :match) == [ + {{:a, 1}, {:list_tail, {:integer, 1}}} + ] + + assert find_typed_vars_in(":erlang.++([5, 6], a)", {:integer, 1}, :match) == [ + {{:a, 1}, {:list_tail, {:list_tail, {:integer, 1}}}} + ] end test "finds variables in map" do @@ -323,12 +362,24 @@ defmodule ElixirSense.Core.TypeInferenceTest do test "list" do assert type_of("[]") == {:list, :empty} assert type_of("[a]") == {:list, {:variable, :a}} + assert type_of("[a | 1]") == {:list, {:variable, :a}} assert type_of("[a]", :match) == {:list, nil} + assert type_of("[a | 1]", :match) == {:list, nil} assert type_of("[^a]", :match) == {:list, {:variable, :a}} assert type_of("[[1]]") == {:list, {:list, {:integer, 1}}} # TODO union a | b? assert type_of("[a, b]") == {:list, {:variable, :a}} assert type_of("[a | b]") == {:list, {:variable, :a}} + assert type_of("[a, b | c]") == {:list, {:variable, :a}} + end + + test "list operators" do + # TODO check in guard + assert type_of(":erlang.++([a], [b])") == + {:call, {:atom, :erlang}, :++, [list: {:variable, :a}, list: {:variable, :b}]} + + assert type_of(":erlang.--([a], [b])") == + {:call, {:atom, :erlang}, :--, [list: {:variable, :a}, list: {:variable, :b}]} end test "tuple" do From 9307a5776f148d68dbaa3aade18804140ce0b990 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 1 Aug 2024 00:46:35 +0200 Subject: [PATCH 119/235] add test --- test/elixir_sense/core/compiler_test.exs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/elixir_sense/core/compiler_test.exs b/test/elixir_sense/core/compiler_test.exs index c9666641..854962e7 100644 --- a/test/elixir_sense/core/compiler_test.exs +++ b/test/elixir_sense/core/compiler_test.exs @@ -630,6 +630,10 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do assert_expansion("length([])") end + test "expands local operator call" do + assert_expansion("a = b = []; a ++ b") + end + test "expands local call macro" do # TODO # assert_expansion("if true, do: :ok") From c5c1b9fafce95093aab12f008be3f563bf2d93be Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 1 Aug 2024 00:54:00 +0200 Subject: [PATCH 120/235] address todo --- lib/elixir_sense/core/guard.ex | 1 + test/elixir_sense/core/type_inference_test.exs | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir_sense/core/guard.ex b/lib/elixir_sense/core/guard.ex index 4c12db7e..4544cba5 100644 --- a/lib/elixir_sense/core/guard.ex +++ b/lib/elixir_sense/core/guard.ex @@ -1,4 +1,5 @@ defmodule ElixirSense.Core.Guard do + # TODO add tests @moduledoc """ This module is responsible for infer type information from guard expressions """ diff --git a/test/elixir_sense/core/type_inference_test.exs b/test/elixir_sense/core/type_inference_test.exs index 5d761169..c643fb84 100644 --- a/test/elixir_sense/core/type_inference_test.exs +++ b/test/elixir_sense/core/type_inference_test.exs @@ -374,7 +374,6 @@ defmodule ElixirSense.Core.TypeInferenceTest do end test "list operators" do - # TODO check in guard assert type_of(":erlang.++([a], [b])") == {:call, {:atom, :erlang}, :++, [list: {:variable, :a}, list: {:variable, :b}]} From bce0d67d1f0098e0503d816ca6bfe55f4e967aed Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 1 Aug 2024 09:10:21 +0200 Subject: [PATCH 121/235] include variable version in type --- lib/elixir_sense/core/binding.ex | 118 +++-- lib/elixir_sense/core/type_inference.ex | 8 +- test/elixir_sense/core/binding_test.exs | 479 +++++++++++------- .../core/metadata_builder_test.exs | 40 +- .../elixir_sense/core/type_inference_test.exs | 82 +-- 5 files changed, 426 insertions(+), 301 deletions(-) diff --git a/lib/elixir_sense/core/binding.ex b/lib/elixir_sense/core/binding.ex index dad4d65b..f8b2612f 100644 --- a/lib/elixir_sense/core/binding.ex +++ b/lib/elixir_sense/core/binding.ex @@ -8,6 +8,7 @@ defmodule ElixirSense.Core.Binding do alias ElixirSense.Core.Struct alias ElixirSense.Core.TypeInfo + # TODO refactor to use env defstruct structs: %{}, variables: [], attributes: [], @@ -83,20 +84,18 @@ defmodule ElixirSense.Core.Binding do expand(env, combined, stack) end - def do_expand(%Binding{variables: variables} = env, {:variable, variable}, stack) do + def do_expand(%Binding{variables: variables} = env, {:variable, variable, version}, stack) do type = - case Enum.find(variables, fn %{name: name} -> name == variable end) do + case Enum.find(variables, fn %State.VarInfo{} = var -> + var.name == variable and var.version == version + end) do nil -> # no variable found - treat a local call + # TODO this cannot happen expand(env, {:local_call, variable, []}, stack) - %State.VarInfo{name: name, type: type} -> - # filter underscored variables - if name |> Atom.to_string() |> String.starts_with?("_") do - :none - else - type - end + %State.VarInfo{type: type} -> + type end expand(env, type, stack) @@ -1112,7 +1111,7 @@ defmodule ElixirSense.Core.Binding do {:ok, {:@, _, [{_kind, _, [ast]}]}} -> case extract_type(ast) do {:ok, type} -> - parsed_type = parse_type(env, type, mod, include_private) + parsed_type = parse_type(env, type, mod, include_private, []) expand(env, parsed_type, stack) :error -> @@ -1152,7 +1151,7 @@ defmodule ElixirSense.Core.Binding do defp get_return_from_spec(env, {{fun, _arity}, [ast]}, mod, include_private) do case Typespec.spec_to_quoted(fun, ast) |> extract_type do {:ok, type} -> - parse_type(env, type, mod, include_private) + parse_type(env, type, mod, include_private, []) :error -> nil @@ -1168,8 +1167,8 @@ defmodule ElixirSense.Core.Binding do end # union type - defp parse_type(env, {:|, _, variants}, mod, include_private) do - {:union, variants |> Enum.map(&parse_type(env, &1, mod, include_private))} + defp parse_type(env, {:|, _, variants}, mod, include_private, stack) do + {:union, variants |> Enum.map(&parse_type(env, &1, mod, include_private, stack))} end # struct @@ -1181,12 +1180,13 @@ defmodule ElixirSense.Core.Binding do {:%{}, _, fields} ]}, mod, - include_private + include_private, + stack ) do fields = for {field, type} <- fields, is_atom(field), - do: {field, parse_type(env, type, mod, include_private)} + do: {field, parse_type(env, type, mod, include_private, stack)} module = case struct_mod do @@ -1201,57 +1201,58 @@ defmodule ElixirSense.Core.Binding do end # map - defp parse_type(env, {:%{}, _, fields}, mod, include_private) do + defp parse_type(env, {:%{}, _, fields}, mod, include_private, stack) do fields = for {field, type} <- fields, field = drop_optional(field), is_atom(field), - do: {field, parse_type(env, type, mod, include_private)} + do: {field, parse_type(env, type, mod, include_private, stack)} {:map, fields, nil} end - defp parse_type(_env, {:map, _, []}, _mod, _include_private) do + defp parse_type(_env, {:map, _, []}, _mod, _include_private, _stack) do {:map, [], nil} end - defp parse_type(env, {:{}, _, fields}, mod, include_private) do - {:tuple, length(fields), fields |> Enum.map(&parse_type(env, &1, mod, include_private))} + defp parse_type(env, {:{}, _, fields}, mod, include_private, stack) do + {:tuple, length(fields), + fields |> Enum.map(&parse_type(env, &1, mod, include_private, stack))} end - defp parse_type(_env, [], _mod, _include_private) do + defp parse_type(_env, [], _mod, _include_private, _stack) do {:list, :empty} end - defp parse_type(env, [type | _], mod, include_private) do - {:list, parse_type(env, type, mod, include_private)} + defp parse_type(env, [type | _], mod, include_private, stack) do + {:list, parse_type(env, type, mod, include_private, stack)} end # for simplicity we skip terminator type - defp parse_type(env, {kind, _, [type, _]}, mod, include_private) + defp parse_type(env, {kind, _, [type, _]}, mod, include_private, stack) when kind in [:maybe_improper_list, :nonempty_improper_list, :nonempty_maybe_improper_list] do - {:list, parse_type(env, type, mod, include_private)} + {:list, parse_type(env, type, mod, include_private, stack)} end - defp parse_type(_env, {:list, _, []}, _mod, _include_private) do + defp parse_type(_env, {:list, _, []}, _mod, _include_private, _stack) do {:list, nil} end - defp parse_type(_env, {:keyword, _, []}, _mod, _include_private) do + defp parse_type(_env, {:keyword, _, []}, _mod, _include_private, _stack) do # TODO no support for atom type for now {:list, {:tuple, 2, [nil, nil]}} end - defp parse_type(env, {:keyword, _, [type]}, mod, include_private) do + defp parse_type(env, {:keyword, _, [type]}, mod, include_private, stack) do # TODO no support for atom type for now - {:list, {:tuple, 2, [nil, parse_type(env, type, mod, include_private)]}} + {:list, {:tuple, 2, [nil, parse_type(env, type, mod, include_private, stack)]}} end # remote user type - defp parse_type(env, {{:., _, [mod, atom]}, _, args}, _mod, _include_private) + defp parse_type(env, {{:., _, [mod, atom]}, _, args}, _mod, _include_private, stack) when is_atom(mod) and is_atom(atom) do # do not propagate include_private when expanding remote types - expand_type(env, mod, atom, args, false) + expand_type(env, mod, atom, args, false, stack) end # remote user type @@ -1259,45 +1260,55 @@ defmodule ElixirSense.Core.Binding do env, {{:., _, [{:__aliases__, _, aliases}, atom]}, _, args}, _mod, - _include_private + _include_private, + stack ) when is_atom(atom) do # do not propagate include_private when expanding remote types - expand_type(env, Module.concat(aliases), atom, args, false) + expand_type(env, Module.concat(aliases), atom, args, false, stack) end # no_return - defp parse_type(_env, {:no_return, _, _}, _, _include_private), do: :none + defp parse_type(_env, {:no_return, _, _}, _, _include_private, _stack), do: :none # term, any, dynamic - defp parse_type(_env, {kind, _, _}, _, _include_private) when kind in [:term, :any, :dynamic], - do: nil + defp parse_type(_env, {kind, _, _}, _, _include_private, _stack) + when kind in [:term, :any, :dynamic], + do: nil # local user type - defp parse_type(env, {atom, _, args}, mod, include_private) when is_atom(atom) do + defp parse_type(env, {atom, _, args}, mod, include_private, stack) when is_atom(atom) do # propagate include_private when expanding local types - expand_type(env, mod, atom, args, include_private) + expand_type(env, mod, atom, args, include_private, stack) end # atom - defp parse_type(_env, atom, _, _include_private) when is_atom(atom), do: {:atom, atom} + defp parse_type(_env, atom, _, _include_private, _stack) when is_atom(atom), do: {:atom, atom} - defp parse_type(_env, integer, _, _include_private) when is_integer(integer) do + defp parse_type(_env, integer, _, _include_private, _stack) when is_integer(integer) do {:integer, integer} end # other - # defp parse_type(_env, t, _, _include_private) do - # IO.inspect t - # nil - # end - defp parse_type(_env, _, _, _include_private), do: nil + defp parse_type(_env, _type, _, _include_private, _stack), do: nil - defp expand_type(env, mod, type_name, args, include_private) do + defp expand_type(env, mod, type_name, args, include_private, stack) do arity = length(args || []) + type = {mod, type_name, arity} - case expand_type_from_metadata(env, mod, type_name, arity, include_private) do - nil -> expand_type_from_introspection(env, mod, type_name, arity, include_private) + if type in stack do + # self referential type + nil + else + do_expand_type(env, mod, type_name, args, include_private, [type | stack]) + end + end + + defp do_expand_type(env, mod, type_name, args, include_private, stack) do + arity = length(args || []) + + case expand_type_from_metadata(env, mod, type_name, arity, include_private, stack) do + nil -> expand_type_from_introspection(env, mod, type_name, arity, include_private, stack) res -> res end |> drop_no_spec @@ -1310,7 +1321,8 @@ defmodule ElixirSense.Core.Binding do mod, type_name, arity, - include_private + include_private, + stack ) do case types[{mod, type_name, arity}] do %State.TypeInfo{specs: [type_spec], kind: kind} @@ -1319,7 +1331,7 @@ defmodule ElixirSense.Core.Binding do {:ok, {:@, _, [{_kind, _, [ast]}]}} -> case extract_type(ast) do {:ok, type} -> - parse_type(env, type, mod, include_private) || :no_spec + parse_type(env, type, mod, include_private, stack) || :no_spec :error -> nil @@ -1337,12 +1349,12 @@ defmodule ElixirSense.Core.Binding do end end - defp expand_type_from_introspection(env, mod, type_name, arity, include_private) do + defp expand_type_from_introspection(env, mod, type_name, arity, include_private, stack) do case TypeInfo.get_type_spec(mod, type_name, arity) do {kind, spec} when type_is_public(kind, include_private) -> - {:"::", _, [{expanded_name, _, _}, type]} = Typespec.type_to_quoted(spec) + {:"::", _, [{_expanded_name, _, _}, type]} = Typespec.type_to_quoted(spec) - if type_name != expanded_name, do: parse_type(env, type, mod, include_private) + parse_type(env, type, mod, include_private, stack) _ -> nil diff --git a/lib/elixir_sense/core/type_inference.ex b/lib/elixir_sense/core/type_inference.ex index d96b6505..6d16af8c 100644 --- a/lib/elixir_sense/core/type_inference.ex +++ b/lib/elixir_sense/core/type_inference.ex @@ -50,9 +50,8 @@ defmodule ElixirSense.Core.TypeInference do var not in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__, :_] and context != :match do case Keyword.fetch(meta, :version) do - {:ok, _version} -> - # TODO include version in type? - {:variable, var} + {:ok, version} -> + {:variable, var, version} _ -> nil @@ -159,7 +158,6 @@ defmodule ElixirSense.Core.TypeInference do [head | _] -> type_of(head, context) - # TODO ++ end {:list, type} @@ -195,7 +193,7 @@ defmodule ElixirSense.Core.TypeInference do case type do {:atom, atom} -> {:atom, atom} {:attribute, attribute} -> {:attribute, attribute} - {:variable, variable} -> {:variable, variable} + {:variable, variable, version} -> {:variable, variable, version} _ -> nil end end diff --git a/test/elixir_sense/core/binding_test.exs b/test/elixir_sense/core/binding_test.exs index 9cc5ccce..86b97b49 100644 --- a/test/elixir_sense/core/binding_test.exs +++ b/test/elixir_sense/core/binding_test.exs @@ -79,15 +79,15 @@ defmodule ElixirSense.Core.BindingTest do end test "map" do - assert {:map, [abc: nil, cde: {:variable, :a}], nil} == - Binding.expand(@env, {:map, [abc: nil, cde: {:variable, :a}], nil}) + assert {:map, [abc: nil, cde: {:variable, :a, 1}], nil} == + Binding.expand(@env, {:map, [abc: nil, cde: {:variable, :a, 1}], nil}) end test "map update" do - assert {:map, [{:efg, {:atom, :a}}, {:abc, nil}, {:cde, {:variable, :a}}], nil} == + assert {:map, [{:efg, {:atom, :a}}, {:abc, nil}, {:cde, {:variable, :a, 1}}], nil} == Binding.expand( @env, - {:map, [abc: nil, cde: {:variable, :a}], + {:map, [abc: nil, cde: {:variable, :a, 1}], {:map, [abc: nil, cde: nil, efg: {:atom, :a}], nil}} ) end @@ -267,9 +267,13 @@ defmodule ElixirSense.Core.BindingTest do Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :v, type: {:atom, ElixirSenseExample.ModuleWithTypedStruct}} + %VarInfo{ + version: 1, + name: :v, + type: {:atom, ElixirSenseExample.ModuleWithTypedStruct} + } ]), - {:struct, [], {:variable, :v}, nil} + {:struct, [], {:variable, :v, 1}, nil} ) end @@ -307,31 +311,32 @@ defmodule ElixirSense.Core.BindingTest do test "known variable" do assert {:atom, :abc} == Binding.expand( - @env |> Map.put(:variables, [%VarInfo{name: :v, type: {:atom, :abc}}]), - {:variable, :v} + @env + |> Map.put(:variables, [%VarInfo{version: 1, name: :v, type: {:atom, :abc}}]), + {:variable, :v, 1} ) end test "known variable self referencing" do assert nil == Binding.expand( - @env |> Map.put(:variables, [%VarInfo{name: :v, type: {:variable, :v}}]), - {:variable, :v} + @env + |> Map.put(:variables, [%VarInfo{version: 1, name: :v, type: {:variable, :v, 1}}]), + {:variable, :v, 1} ) end - test "anonymous variable" do + test "unknown variable" do + assert :none == Binding.expand(@env, {:variable, :v, 1}) + assert :none == Binding.expand( - @env |> Map.put(:variables, [%VarInfo{name: :_, type: {:atom, :abc}}]), - {:variable, :_} + @env + |> Map.put(:variables, [%VarInfo{version: 1, name: :v, type: {:integer, 1}}]), + {:variable, :v, 2} ) end - test "unknown variable" do - assert :none == Binding.expand(@env, {:variable, :v}) - end - test "known attribute" do assert {:atom, :abc} == Binding.expand( @@ -357,10 +362,14 @@ defmodule ElixirSense.Core.BindingTest do Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :tuple, type: {:tuple, 2, [nil, {:variable, :a}]}}, - %VarInfo{name: :a, type: {:atom, :abc}} + %VarInfo{ + version: 1, + name: :tuple, + type: {:tuple, 2, [nil, {:variable, :a, 1}]} + }, + %VarInfo{version: 1, name: :a, type: {:atom, :abc}} ]), - {:variable, :tuple} + {:variable, :tuple, 1} ) end @@ -369,10 +378,10 @@ defmodule ElixirSense.Core.BindingTest do Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :tuple, type: {:tuple, 2, [nil, {:atom, :a}]}}, - %VarInfo{name: :ref, type: {:tuple_nth, {:variable, :tuple}, 1}} + %VarInfo{version: 1, name: :tuple, type: {:tuple, 2, [nil, {:atom, :a}]}}, + %VarInfo{version: 1, name: :ref, type: {:tuple_nth, {:variable, :tuple, 1}, 1}} ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -381,10 +390,10 @@ defmodule ElixirSense.Core.BindingTest do Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :list, type: {:list, {:variable, :a}}}, - %VarInfo{name: :a, type: {:atom, :abc}} + %VarInfo{version: 1, name: :list, type: {:list, {:variable, :a, 1}}}, + %VarInfo{version: 1, name: :a, type: {:atom, :abc}} ]), - {:variable, :list} + {:variable, :list, 1} ) end @@ -393,10 +402,14 @@ defmodule ElixirSense.Core.BindingTest do Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :map, type: {:map_key, {:variable, :a}, {:atom, :x}}}, - %VarInfo{name: :a, type: {:map, [x: {:atom, :abc}], nil}} + %VarInfo{ + version: 1, + name: :map, + type: {:map_key, {:variable, :a, 1}, {:atom, :x}} + }, + %VarInfo{version: 1, name: :a, type: {:map, [x: {:atom, :abc}], nil}} ]), - {:variable, :map} + {:variable, :map, 1} ) assert {:atom, :abc} == @@ -404,37 +417,43 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :struct, - type: {:map_key, {:variable, :a}, {:atom, :typed_field}} + type: {:map_key, {:variable, :a, 1}, {:atom, :typed_field}} }, %VarInfo{ + version: 1, name: :a, type: {:struct, [typed_field: {:atom, :abc}], {:atom, ElixirSenseExample.ModuleWithTypedStruct}, nil} } ]), - {:variable, :struct} + {:variable, :struct, 1} ) assert nil == Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :map, type: {:map_key, {:variable, :a}, {:atom, :y}}}, - %VarInfo{name: :a, type: {:map, [x: {:atom, :abc}], nil}} + %VarInfo{ + version: 1, + name: :map, + type: {:map_key, {:variable, :a, 1}, {:atom, :y}} + }, + %VarInfo{version: 1, name: :a, type: {:map, [x: {:atom, :abc}], nil}} ]), - {:variable, :map} + {:variable, :map, 1} ) assert nil == Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :map, type: {:map_key, {:variable, :a}, nil}}, - %VarInfo{name: :a, type: {:map, [x: {:atom, :abc}], nil}} + %VarInfo{version: 1, name: :map, type: {:map_key, {:variable, :a, 1}, nil}}, + %VarInfo{version: 1, name: :a, type: {:map, [x: {:atom, :abc}], nil}} ]), - {:variable, :map} + {:variable, :map, 1} ) end @@ -443,30 +462,30 @@ defmodule ElixirSense.Core.BindingTest do Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :list, type: {:for_expression, {:variable, :a}}}, - %VarInfo{name: :a, type: {:list, {:atom, :abc}}} + %VarInfo{version: 1, name: :list, type: {:for_expression, {:variable, :a, 1}}}, + %VarInfo{version: 1, name: :a, type: {:list, {:atom, :abc}}} ]), - {:variable, :list} + {:variable, :list, 1} ) assert {:tuple, 2, [nil, {:atom, :abc}]} == Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :map, type: {:for_expression, {:variable, :a}}}, - %VarInfo{name: :a, type: {:map, [x: {:atom, :abc}], nil}} + %VarInfo{version: 1, name: :map, type: {:for_expression, {:variable, :a, 1}}}, + %VarInfo{version: 1, name: :a, type: {:map, [x: {:atom, :abc}], nil}} ]), - {:variable, :map} + {:variable, :map, 1} ) assert :none == Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :list, type: {:list_head, {:variable, :a}}}, - %VarInfo{name: :a, type: {:list, :empty}} + %VarInfo{version: 1, name: :list, type: {:list_head, {:variable, :a, 1}}}, + %VarInfo{version: 1, name: :a, type: {:list, :empty}} ]), - {:variable, :list} + {:variable, :list, 1} ) end @@ -475,20 +494,20 @@ defmodule ElixirSense.Core.BindingTest do Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :list, type: {:list_head, {:variable, :a}}}, - %VarInfo{name: :a, type: {:list, {:atom, :abc}}} + %VarInfo{version: 1, name: :list, type: {:list_head, {:variable, :a, 1}}}, + %VarInfo{version: 1, name: :a, type: {:list, {:atom, :abc}}} ]), - {:variable, :list} + {:variable, :list, 1} ) assert :none == Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :list, type: {:list_head, {:variable, :a}}}, - %VarInfo{name: :a, type: {:list, :empty}} + %VarInfo{version: 1, name: :list, type: {:list_head, {:variable, :a, 1}}}, + %VarInfo{version: 1, name: :a, type: {:list, :empty}} ]), - {:variable, :list} + {:variable, :list, 1} ) end @@ -497,20 +516,20 @@ defmodule ElixirSense.Core.BindingTest do Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :list, type: {:list_tail, {:variable, :a}}}, - %VarInfo{name: :a, type: {:list, {:atom, :abc}}} + %VarInfo{version: 1, name: :list, type: {:list_tail, {:variable, :a, 1}}}, + %VarInfo{version: 1, name: :a, type: {:list, {:atom, :abc}}} ]), - {:variable, :list} + {:variable, :list, 1} ) assert :none == Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :list, type: {:list_tail, {:variable, :a}}}, - %VarInfo{name: :a, type: {:list, :empty}} + %VarInfo{version: 1, name: :list, type: {:list_tail, {:variable, :a, 1}}}, + %VarInfo{version: 1, name: :a, type: {:list, :empty}} ]), - {:variable, :list} + {:variable, :list, 1} ) end @@ -519,10 +538,14 @@ defmodule ElixirSense.Core.BindingTest do Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :map, type: {:map, [field: {:atom, :a}], nil}}, - %VarInfo{name: :ref, type: {:call, {:variable, :map}, :field, []}} + %VarInfo{version: 1, name: :map, type: {:map, [field: {:atom, :a}], nil}}, + %VarInfo{ + version: 1, + name: :ref, + type: {:call, {:variable, :map, 1}, :field, []} + } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -531,10 +554,14 @@ defmodule ElixirSense.Core.BindingTest do Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :map, type: {:map, [field: {:atom, :a}], nil}}, - %VarInfo{name: :ref, type: {:call, {:variable, :map}, :field, [nil]}} + %VarInfo{version: 1, name: :map, type: {:map, [field: {:atom, :a}], nil}}, + %VarInfo{ + version: 1, + name: :ref, + type: {:call, {:variable, :map, 1}, :field, [nil]} + } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -543,10 +570,14 @@ defmodule ElixirSense.Core.BindingTest do Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :map, type: {:map, [field: {:atom, :a}], nil}}, - %VarInfo{name: :ref, type: {:call, {:variable, :map}, :not_existing, []}} + %VarInfo{version: 1, name: :map, type: {:map, [field: {:atom, :a}], nil}}, + %VarInfo{ + version: 1, + name: :ref, + type: {:call, {:variable, :map, 1}, :not_existing, []} + } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -556,14 +587,19 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :map, type: {:struct, [typed_field: {:atom, :abc}], {:atom, ElixirSenseExample.ModuleWithTypedStruct}, nil} }, - %VarInfo{name: :ref, type: {:call, {:variable, :map}, :typed_field, []}} + %VarInfo{ + version: 1, + name: :ref, + type: {:call, {:variable, :map, 1}, :typed_field, []} + } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -573,14 +609,19 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :map, type: {:struct, [typed_field: {:atom, :abc}], {:atom, ElixirSenseExample.ModuleWithTypedStruct}, nil} }, - %VarInfo{name: :ref, type: {:call, {:variable, :map}, :not_existing, []}} + %VarInfo{ + version: 1, + name: :ref, + type: {:call, {:variable, :map, 1}, :not_existing, []} + } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -590,14 +631,19 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :map, type: {:struct, [typed_field: {:atom, :abc}], {:atom, ElixirSenseExample.ModuleWithTypedStruct}, nil} }, - %VarInfo{name: :ref, type: {:call, {:variable, :map}, :typed_field, [nil]}} + %VarInfo{ + version: 1, + name: :ref, + type: {:call, {:variable, :map, 1}, :typed_field, [nil]} + } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -606,36 +652,44 @@ defmodule ElixirSense.Core.BindingTest do Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :ref, type: {:call, nil, :not_existing, []}} + %VarInfo{version: 1, name: :ref, type: {:call, nil, :not_existing, []}} ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert :none == Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :ref, type: {:call, :none, :not_existing, []}} + %VarInfo{version: 1, name: :ref, type: {:call, :none, :not_existing, []}} ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert :none == Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :ref, type: {:call, {:atom, nil}, :not_existing, []}} + %VarInfo{ + version: 1, + name: :ref, + type: {:call, {:atom, nil}, :not_existing, []} + } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert :none == Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :ref, type: {:call, {:atom, true}, :not_existing, []}} + %VarInfo{ + version: 1, + name: :ref, + type: {:call, {:atom, true}, :not_existing, []} + } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -645,13 +699,14 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :not_existing, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -661,12 +716,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f1, [nil]} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -676,12 +732,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f1x, [:none]} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -691,11 +748,12 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f01, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -705,11 +763,12 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f02, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -719,11 +778,12 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f04, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -733,12 +793,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :list1, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:list, nil} == @@ -746,12 +807,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :list2, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:list, nil} == @@ -759,12 +821,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :list3, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:list, {:atom, :ok}} == @@ -772,12 +835,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :list4, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:list, {:atom, :ok}} == @@ -785,12 +849,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :list5, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:list, {:atom, :ok}} == @@ -798,12 +863,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :list6, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:list, {:atom, :ok}} == @@ -811,12 +877,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :list7, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:list, {:atom, :ok}} == @@ -824,12 +891,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :list8, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:list, {:atom, :ok}} == @@ -837,12 +905,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :list9, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:list, {:atom, :ok}} == @@ -850,12 +919,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :list10, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:list, {:tuple, 2, [nil, nil]}} == @@ -863,12 +933,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :list11, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:list, {:tuple, 2, [nil, {:atom, :ok}]}} == @@ -876,12 +947,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :list12, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:list, {:tuple, 2, [{:atom, :some}, {:atom, :ok}]}} == @@ -889,12 +961,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :list13, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -904,11 +977,12 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f03, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -918,11 +992,12 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f05, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -937,11 +1012,12 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f1, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -954,11 +1030,12 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f3, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -971,11 +1048,12 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f5, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -985,11 +1063,12 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f2, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -999,11 +1078,12 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f4, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1013,11 +1093,12 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f6, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1027,11 +1108,12 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f7, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1041,6 +1123,7 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, @@ -1048,7 +1131,7 @@ defmodule ElixirSense.Core.BindingTest do :abc, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1058,12 +1141,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f71, [nil]} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1073,11 +1157,12 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f8, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1087,13 +1172,14 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f_no_return, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1103,12 +1189,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f_any, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert nil == @@ -1116,12 +1203,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f_term, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1131,11 +1219,12 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f91, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1145,7 +1234,7 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.merge(%{ variables: [ - %VarInfo{name: :ref, type: {:local_call, :fun, []}} + %VarInfo{version: 1, name: :ref, type: {:local_call, :fun, []}} ], current_module: MyMod, specs: %{ @@ -1165,7 +1254,7 @@ defmodule ElixirSense.Core.BindingTest do } } }), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1175,7 +1264,7 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.merge(%{ variables: [ - %VarInfo{name: :ref, type: {:local_call, :fun, []}} + %VarInfo{version: 1, name: :ref, type: {:local_call, :fun, []}} ], current_module: MyMod, specs: %{ @@ -1200,7 +1289,7 @@ defmodule ElixirSense.Core.BindingTest do } } }), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1210,7 +1299,7 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.merge(%{ variables: [ - %VarInfo{name: :ref, type: {:local_call, :fun, []}} + %VarInfo{version: 1, name: :ref, type: {:local_call, :fun, []}} ], current_module: MyMod, specs: %{ @@ -1236,7 +1325,7 @@ defmodule ElixirSense.Core.BindingTest do } } }), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1246,7 +1335,7 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.merge(%{ variables: [ - %VarInfo{name: :ref, type: {:call, {:atom, MyMod}, :fun, []}} + %VarInfo{version: 1, name: :ref, type: {:call, {:atom, MyMod}, :fun, []}} ], current_module: SomeMod, specs: %{ @@ -1271,7 +1360,7 @@ defmodule ElixirSense.Core.BindingTest do } } }), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1281,7 +1370,7 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.merge(%{ variables: [ - %VarInfo{name: :ref, type: {:call, {:atom, MyMod}, :fun, []}} + %VarInfo{version: 1, name: :ref, type: {:call, {:atom, MyMod}, :fun, []}} ], current_module: SomeMod, specs: %{ @@ -1307,7 +1396,7 @@ defmodule ElixirSense.Core.BindingTest do } } }), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1317,7 +1406,7 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.merge(%{ variables: [ - %VarInfo{name: :ref, type: {:call, {:atom, MyMod}, :fun, []}} + %VarInfo{version: 1, name: :ref, type: {:call, {:atom, MyMod}, :fun, []}} ], current_module: SomeMod, specs: %{ @@ -1342,7 +1431,7 @@ defmodule ElixirSense.Core.BindingTest do } } }), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1379,45 +1468,49 @@ defmodule ElixirSense.Core.BindingTest do Binding.expand( env |> Map.put(:variables, [ - %VarInfo{name: :ref, type: {:local_call, :fun, []}} + %VarInfo{version: 1, name: :ref, type: {:local_call, :fun, []}} ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:struct, [{:__struct__, {:atom, MyMod}}, {:abc, nil}], {:atom, MyMod}, nil} == Binding.expand( env |> Map.put(:variables, [ - %VarInfo{name: :ref, type: {:local_call, :fun, [nil]}} + %VarInfo{version: 1, name: :ref, type: {:local_call, :fun, [nil]}} ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:struct, [{:__struct__, {:atom, MyMod}}, {:abc, nil}], {:atom, MyMod}, nil} == Binding.expand( env |> Map.put(:variables, [ - %VarInfo{name: :ref, type: {:local_call, :fun, [nil, nil]}} + %VarInfo{version: 1, name: :ref, type: {:local_call, :fun, [nil, nil]}} ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:struct, [{:__struct__, {:atom, MyMod}}, {:abc, nil}], {:atom, MyMod}, nil} == Binding.expand( env |> Map.put(:variables, [ - %VarInfo{name: :ref, type: {:local_call, :fun, [nil, nil, nil]}} + %VarInfo{version: 1, name: :ref, type: {:local_call, :fun, [nil, nil, nil]}} ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert :none == Binding.expand( env |> Map.put(:variables, [ - %VarInfo{name: :ref, type: {:local_call, :fun, [nil, nil, nil, nil]}} + %VarInfo{ + version: 1, + name: :ref, + type: {:local_call, :fun, [nil, nil, nil, nil]} + } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1427,11 +1520,12 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f10, []} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:atom, String} == @@ -1439,12 +1533,13 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f10, [nil]} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:atom, String} == @@ -1452,13 +1547,14 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f10, [nil, nil]} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert {:atom, String} == @@ -1466,13 +1562,14 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f10, [nil, nil, nil]} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) assert :none == @@ -1480,13 +1577,14 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.put(:variables, [ %VarInfo{ + version: 1, name: :ref, type: {:call, {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, :f10, [nil, nil, nil, nil]} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1496,11 +1594,11 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.merge(%{ variables: [ - %VarInfo{name: :ref, type: {:local_call, :f02, []}} + %VarInfo{version: 1, name: :ref, type: {:local_call, :f02, []}} ], functions: [{ElixirSenseExample.FunctionsWithReturnSpec, [{:f02, 0}]}] }), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1510,10 +1608,10 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.merge(%{ variables: [ - %VarInfo{name: :ref, type: {:local_call, :f02, []}} + %VarInfo{version: 1, name: :ref, type: {:local_call, :f02, []}} ] }), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1523,11 +1621,11 @@ defmodule ElixirSense.Core.BindingTest do @env |> Map.merge(%{ variables: [ - %VarInfo{name: :ref, type: {:variable, :f02}} + %VarInfo{version: 1, name: :ref, type: {:variable, :f02, 1}} ], functions: [{ElixirSenseExample.FunctionsWithReturnSpec, [{:f02, 0}]}] }), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1538,6 +1636,7 @@ defmodule ElixirSense.Core.BindingTest do |> Map.merge(%{ variables: [ %VarInfo{ + version: 1, name: :ref, type: {:call, @@ -1546,7 +1645,7 @@ defmodule ElixirSense.Core.BindingTest do } ] }), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1557,6 +1656,7 @@ defmodule ElixirSense.Core.BindingTest do |> Map.merge(%{ variables: [ %VarInfo{ + version: 1, name: :ref, type: {:call, @@ -1565,7 +1665,7 @@ defmodule ElixirSense.Core.BindingTest do } ] }), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1576,6 +1676,7 @@ defmodule ElixirSense.Core.BindingTest do |> Map.merge(%{ variables: [ %VarInfo{ + version: 1, name: :ref, type: {:call, @@ -1584,7 +1685,7 @@ defmodule ElixirSense.Core.BindingTest do } ] }), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1619,10 +1720,11 @@ defmodule ElixirSense.Core.BindingTest do Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :a, type: {:integer, 1}}, - %VarInfo{name: :b, type: {:integer, 2}} + %VarInfo{version: 1, name: :a, type: {:integer, 1}}, + %VarInfo{version: 1, name: :b, type: {:integer, 2}} ]), - {:call, {:atom, :erlang}, :++, [list: {:variable, :a}, list: {:variable, :b}]} + {:call, {:atom, :erlang}, :++, + [list: {:variable, :a, 1}, list: {:variable, :b, 1}]} ) end end @@ -1634,13 +1736,14 @@ defmodule ElixirSense.Core.BindingTest do Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :tuple, type: {:tuple, 2, [nil, {:atom, :a}]}}, + %VarInfo{version: 1, name: :tuple, type: {:tuple, 2, [nil, {:atom, :a}]}}, %VarInfo{ + version: 1, name: :ref, - type: {:local_call, :elem, [{:variable, :tuple}, {:integer, 1}]} + type: {:local_call, :elem, [{:variable, :tuple, 1}, {:integer, 1}]} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1649,13 +1752,14 @@ defmodule ElixirSense.Core.BindingTest do Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :list, type: {:list, {:atom, :a}}}, + %VarInfo{version: 1, name: :list, type: {:list, {:atom, :a}}}, %VarInfo{ + version: 1, name: :ref, - type: {:local_call, :hd, [{:variable, :list}]} + type: {:local_call, :hd, [{:variable, :list, 1}]} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end @@ -1664,13 +1768,14 @@ defmodule ElixirSense.Core.BindingTest do Binding.expand( @env |> Map.put(:variables, [ - %VarInfo{name: :list, type: {:list, {:atom, :a}}}, + %VarInfo{version: 1, name: :list, type: {:list, {:atom, :a}}}, %VarInfo{ + version: 1, name: :ref, - type: {:local_call, :tl, [{:variable, :list}]} + type: {:local_call, :tl, [{:variable, :list, 1}]} } ]), - {:variable, :ref} + {:variable, :ref, 1} ) end end @@ -1940,8 +2045,11 @@ defmodule ElixirSense.Core.BindingTest do describe "intersection" do test "intersection" do assert {:struct, - [{:__struct__, {:atom, State}}, {:abc, nil}, {:formatted, {:variable, :formatted}}], - {:atom, State}, + [ + {:__struct__, {:atom, State}}, + {:abc, nil}, + {:formatted, {:variable, :formatted, 1}} + ], {:atom, State}, nil} == Binding.expand( @env @@ -1952,16 +2060,13 @@ defmodule ElixirSense.Core.BindingTest do } }, variables: [ - %VarInfo{ - name: :socket, - type: nil - } + %VarInfo{version: 1, name: :socket, type: nil} ] }), {:intersection, [ - {:call, {:call, {:variable, :socket}, :assigns, []}, :state, []}, - {:struct, [formatted: {:variable, :formatted}], {:atom, State}, nil} + {:call, {:call, {:variable, :socket, 1}, :assigns, []}, :state, []}, + {:struct, [formatted: {:variable, :formatted, 1}], {:atom, State}, nil} ]} ) end @@ -2045,7 +2150,7 @@ defmodule ElixirSense.Core.BindingTest do [ {:__struct__, {:atom, State}}, {:abc, {:atom, X}}, - {:formatted, {:variable, :formatted}} + {:formatted, {:variable, :formatted, 1}} ], {:atom, State}, nil} == Binding.expand( @@ -2059,7 +2164,7 @@ defmodule ElixirSense.Core.BindingTest do }), {:intersection, [ - {:struct, [formatted: {:variable, :formatted}], {:atom, State}, nil}, + {:struct, [formatted: {:variable, :formatted, 1}], {:atom, State}, nil}, {:map, [not_existing: nil, abc: {:atom, X}], nil} ]} ) @@ -2068,7 +2173,7 @@ defmodule ElixirSense.Core.BindingTest do [ {:__struct__, {:atom, State}}, {:abc, {:atom, X}}, - {:formatted, {:variable, :formatted}} + {:formatted, {:variable, :formatted, 1}} ], {:atom, State}, nil} == Binding.expand( @@ -2083,7 +2188,7 @@ defmodule ElixirSense.Core.BindingTest do {:intersection, [ {:map, [not_existing: nil, abc: {:atom, X}], nil}, - {:struct, [formatted: {:variable, :formatted}], {:atom, State}, nil} + {:struct, [formatted: {:variable, :formatted, 1}], {:atom, State}, nil} ]} ) end @@ -2092,7 +2197,7 @@ defmodule ElixirSense.Core.BindingTest do assert {:struct, [ {:__struct__, nil}, - {:formatted, {:variable, :formatted}}, + {:formatted, {:variable, :formatted, 1}}, {:not_existing, nil}, {:abc, {:atom, X}} ], nil, @@ -2101,7 +2206,7 @@ defmodule ElixirSense.Core.BindingTest do @env, {:intersection, [ - {:struct, [formatted: {:variable, :formatted}], nil, nil}, + {:struct, [formatted: {:variable, :formatted, 1}], nil, nil}, {:map, [not_existing: nil, abc: {:atom, X}], nil} ]} ) @@ -2111,7 +2216,7 @@ defmodule ElixirSense.Core.BindingTest do assert {:struct, [ {:__struct__, nil}, - {:formatted, {:variable, :formatted}}, + {:formatted, {:variable, :formatted, 1}}, {:not_existing, nil}, {:abc, {:atom, X}} ], nil, @@ -2120,7 +2225,7 @@ defmodule ElixirSense.Core.BindingTest do @env, {:intersection, [ - {:struct, [formatted: {:variable, :formatted}], nil, nil}, + {:struct, [formatted: {:variable, :formatted, 1}], nil, nil}, {:struct, [not_existing: nil, abc: {:atom, X}], nil, nil} ]} ) @@ -2131,7 +2236,7 @@ defmodule ElixirSense.Core.BindingTest do [ {:__struct__, {:atom, State}}, {:abc, {:atom, X}}, - {:formatted, {:variable, :formatted}} + {:formatted, {:variable, :formatted, 1}} ], {:atom, State}, nil} == Binding.expand( @@ -2145,7 +2250,7 @@ defmodule ElixirSense.Core.BindingTest do }), {:intersection, [ - {:struct, [formatted: {:variable, :formatted}], {:atom, State}, nil}, + {:struct, [formatted: {:variable, :formatted, 1}], {:atom, State}, nil}, {:struct, [not_existing: nil, abc: {:atom, X}], nil, nil} ]} ) @@ -2154,7 +2259,7 @@ defmodule ElixirSense.Core.BindingTest do [ {:__struct__, {:atom, State}}, {:abc, {:atom, X}}, - {:formatted, {:variable, :formatted}} + {:formatted, {:variable, :formatted, 1}} ], {:atom, State}, nil} == Binding.expand( @@ -2169,7 +2274,7 @@ defmodule ElixirSense.Core.BindingTest do {:intersection, [ {:struct, [not_existing: nil, abc: {:atom, X}], nil, nil}, - {:struct, [formatted: {:variable, :formatted}], {:atom, State}, nil} + {:struct, [formatted: {:variable, :formatted, 1}], {:atom, State}, nil} ]} ) end @@ -2179,7 +2284,7 @@ defmodule ElixirSense.Core.BindingTest do [ {:__struct__, {:atom, State}}, {:abc, {:atom, X}}, - {:formatted, {:variable, :formatted}} + {:formatted, {:variable, :formatted, 1}} ], {:atom, State}, nil} == Binding.expand( @@ -2193,7 +2298,7 @@ defmodule ElixirSense.Core.BindingTest do }), {:intersection, [ - {:struct, [formatted: {:variable, :formatted}], {:atom, State}, nil}, + {:struct, [formatted: {:variable, :formatted, 1}], {:atom, State}, nil}, {:struct, [not_existing: nil, abc: {:atom, X}], {:atom, State}, nil} ]} ) @@ -2214,7 +2319,7 @@ defmodule ElixirSense.Core.BindingTest do {:intersection, [ {:struct, [not_existing: nil, abc: {:atom, X}], {:atom, Other}, nil}, - {:struct, [formatted: {:variable, :formatted}], {:atom, State}, nil} + {:struct, [formatted: {:variable, :formatted, 1}], {:atom, State}, nil} ]} ) end diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 6790f9fa..497201d2 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -1634,7 +1634,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do %AttributeInfo{ name: :other, positions: [{4, 3}], - type: {:variable, :var} + type: {:variable, :var, 0} } ] @@ -1710,7 +1710,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do }, %VarInfo{ name: :q1, - type: {:tuple_nth, {:variable, :q}, 2} + type: {:tuple_nth, {:variable, :q, 2}, 2} } ] = state @@ -1797,14 +1797,14 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert [ %VarInfo{name: :a, type: {:for_expression, {:attribute, :myattribute}}}, - %VarInfo{name: :b, type: {:variable, :a}} + %VarInfo{name: :b, type: {:variable, :a, 0}} ] = state |> get_line_vars(5) assert [ %VarInfo{name: :a, type: {:for_expression, {:attribute, :myattribute}}}, %VarInfo{name: :a1, type: {:attribute, :myattribute}}, - %VarInfo{name: :a2, type: {:for_expression, {:variable, :a1}}}, - %VarInfo{name: :b, type: {:variable, :a}} + %VarInfo{name: :a2, type: {:for_expression, {:variable, :a1, 3}}}, + %VarInfo{name: :b, type: {:variable, :a, 2}} ] = state |> get_line_vars(10) end @@ -1899,7 +1899,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert [ %VarInfo{name: :a, type: {:attribute, :myattribute}}, - %VarInfo{name: :b, type: {:variable, :a}} + %VarInfo{name: :b, type: {:variable, :a, 0}} ] = state |> get_line_vars(5) end @@ -1920,7 +1920,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert [ %VarInfo{name: :a, type: {:attribute, :myattribute}}, %VarInfo{name: :b, type: {:call, {:atom, Date}, :utc_now, []}}, - %VarInfo{name: :c, type: {:list_head, {:variable, :a}}} + %VarInfo{name: :c, type: {:list_head, {:variable, :a, 0}}} ] = state |> get_line_vars(6) end @@ -2015,7 +2015,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] = state |> get_line_vars(16) assert [ - %VarInfo{name: :other, type: {:variable, :var}}, + %VarInfo{name: :other, type: {:variable, :var, 5}}, %VarInfo{type: {:atom, Atom}} ] = state |> get_line_vars(18) end @@ -2069,11 +2069,11 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert [ %VarInfo{name: :abc, type: nil}, - %VarInfo{name: :var1, type: {:call, {:variable, :var1}, :abc, []}}, + %VarInfo{name: :var1, type: {:call, {:variable, :var1, 0}, :abc, []}}, %VarInfo{name: :var2, type: {:call, {:attribute, :attr}, :qwe, [{:integer, 0}]}}, %VarInfo{ name: :var3, - type: {:call, {:call, {:variable, :abc}, :cde, []}, :efg, []} + type: {:call, {:call, {:variable, :abc, 1}, :cde, []}, :efg, []} } ] = state |> get_line_vars(24) end @@ -2119,13 +2119,13 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert [ %VarInfo{ - type: {:map, [asd: {:integer, 2}, zxc: {:integer, 5}], {:variable, :var}} + type: {:map, [asd: {:integer, 2}, zxc: {:integer, 5}], {:variable, :var, 3}} } ] = state |> get_line_vars(12) |> Enum.filter(&(&1.name == :qwe)) assert [ - %VarInfo{type: {:map, [{:asd, {:integer, 2}}], {:variable, :var}}} + %VarInfo{type: {:map, [{:asd, {:integer, 2}}], {:variable, :var, 3}}} ] = state |> get_line_vars(14) |> Enum.filter(&(&1.name == :qwe)) end @@ -2166,20 +2166,20 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert [ %VarInfo{ name: :asd, - type: {:struct, [{:sub, {:atom, Atom}}], {:atom, Other}, {:variable, :a}} + type: {:struct, [{:sub, {:atom, Atom}}], {:atom, Other}, {:variable, :a, 0}} } ] = state |> get_line_vars(11) |> Enum.filter(&(&1.name == :asd)) assert [ %VarInfo{ name: :asd, - type: {:map, [{:other, {:integer, 123}}], {:variable, :asd}} + type: {:map, [{:other, {:integer, 123}}], {:variable, :asd, 2}} } ] = state |> get_line_vars(13) |> Enum.filter(&(&1.name == :asd)) assert [ - %VarInfo{name: :x, type: {:variable, :asd}}, - %VarInfo{name: :z, type: {:variable, :asd}} + %VarInfo{name: :x, type: {:variable, :asd, 3}}, + %VarInfo{name: :z, type: {:variable, :asd, 3}} ] = state |> get_line_vars(15) |> Enum.filter(&(&1.name in [:x, :z])) end @@ -2377,7 +2377,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do name: :formatted, type: { :map_key, - {:call, {:call, {:variable, :socket}, :assigns, []}, :state, []}, + {:call, {:call, {:variable, :socket, 0}, :assigns, []}, :state, []}, {:atom, :formatted} } }, @@ -2390,7 +2390,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do type: {:intersection, [ - {:call, {:call, {:variable, :socket}, :assigns, []}, :state, []}, + {:call, {:call, {:variable, :socket, 0}, :assigns, []}, :state, []}, {:struct, [formatted: nil], {:atom, MyState}, nil} ]} } @@ -2548,7 +2548,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert %VarInfo{ name: :a1, positions: [{5, 17}], - type: {:intersection, [{:map, [b: {:integer, 2}], nil}, {:variable, :a}]} + type: {:intersection, [{:map, [b: {:integer, 2}], nil}, {:variable, :a, 0}]} } = Enum.find(vars, &(&1.name == :a1)) vars = state |> get_line_vars(8) @@ -2558,7 +2558,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do positions: [{7, 18}], type: { :intersection, - [{:map, [b: {:variable, :b}], nil}, {:variable, :a}] + [{:map, [b: {:variable, :b, 1}], nil}, {:variable, :a, 0}] } } = Enum.find(vars, &(&1.name == :a2)) end diff --git a/test/elixir_sense/core/type_inference_test.exs b/test/elixir_sense/core/type_inference_test.exs index c643fb84..86f4c90a 100644 --- a/test/elixir_sense/core/type_inference_test.exs +++ b/test/elixir_sense/core/type_inference_test.exs @@ -199,11 +199,11 @@ defmodule ElixirSense.Core.TypeInferenceTest do test "finds variables in match" do assert find_typed_vars_in("a = b", nil, :match) == [{{:b, 1}, nil}, {{:a, 1}, nil}] - assert find_typed_vars_in("a = b", nil) == [{{:a, 1}, {:variable, :b}}] + assert find_typed_vars_in("a = b", nil) == [{{:a, 1}, {:variable, :b, 1}}] assert find_typed_vars_in("^a = b", nil) == [] assert find_typed_vars_in("a = a", nil, :match) == [{{:a, 1}, nil}] - assert find_typed_vars_in("a = a", nil) == [{{:a, 1}, {:variable, :a}}] + assert find_typed_vars_in("a = a", nil) == [{{:a, 1}, {:variable, :a, 1}}] assert find_typed_vars_in("a = b = c", nil, :match) == [ {{:c, 1}, nil}, @@ -211,7 +211,7 @@ defmodule ElixirSense.Core.TypeInferenceTest do {{:a, 1}, nil} ] - assert find_typed_vars_in("[a] = b", nil) == [{{:a, 1}, {:list_head, {:variable, :b}}}] + assert find_typed_vars_in("[a] = b", nil) == [{{:a, 1}, {:list_head, {:variable, :b, 1}}}] assert find_typed_vars_in("[a] = b", nil, :match) == [ {{:b, 1}, {:list, nil}}, @@ -223,7 +223,9 @@ defmodule ElixirSense.Core.TypeInferenceTest do {{:a, 1}, {:list_head, {:variable, :x}}} ] - assert find_typed_vars_in("{a} = b", nil) == [{{:a, 1}, {:tuple_nth, {:variable, :b}, 0}}] + assert find_typed_vars_in("{a} = b", nil) == [ + {{:a, 1}, {:tuple_nth, {:variable, :b, 1}, 0}} + ] assert find_typed_vars_in("{a} = b", nil, :match) == [ {{:b, 1}, {:tuple, 1, [nil]}}, @@ -236,7 +238,7 @@ defmodule ElixirSense.Core.TypeInferenceTest do ] assert find_typed_vars_in("%{foo: a} = b", nil) == [ - {{:a, 1}, {:map_key, {:variable, :b}, {:atom, :foo}}} + {{:a, 1}, {:map_key, {:variable, :b, 1}, {:atom, :foo}}} ] assert find_typed_vars_in("%{foo: a} = b", nil, :match) == [ @@ -250,7 +252,7 @@ defmodule ElixirSense.Core.TypeInferenceTest do ] assert find_typed_vars_in("%Foo{foo: a} = b", nil) == [ - {{:a, 1}, {:map_key, {:variable, :b}, {:atom, :foo}}} + {{:a, 1}, {:map_key, {:variable, :b, 1}, {:atom, :foo}}} ] assert find_typed_vars_in("%Foo{foo: a} = b", nil, :match) == [ @@ -269,12 +271,12 @@ defmodule ElixirSense.Core.TypeInferenceTest do {:a, 1}, { :map_key, - {:intersection, [{:map, [bar: nil], nil}, {:variable, :c}]}, + {:intersection, [{:map, [bar: nil], nil}, {:variable, :c, 1}]}, {:atom, :foo} } }, {{:b, 1}, - {:map_key, {:intersection, [{:map, [foo: nil], nil}, {:variable, :c}]}, + {:map_key, {:intersection, [{:map, [foo: nil], nil}, {:variable, :c, 1}]}, {:atom, :bar}}} ] @@ -343,9 +345,9 @@ defmodule ElixirSense.Core.TypeInferenceTest do end test "variable" do - assert type_of("a") == {:variable, :a} + assert type_of("a") == {:variable, :a, 1} assert type_of("a", :match) == nil - assert type_of("^a", :match) == {:variable, :a} + assert type_of("^a", :match) == {:variable, :a, 1} assert type_of("^a") == :none assert type_of("_", :match) == nil assert type_of("_") == :none @@ -361,40 +363,42 @@ defmodule ElixirSense.Core.TypeInferenceTest do test "list" do assert type_of("[]") == {:list, :empty} - assert type_of("[a]") == {:list, {:variable, :a}} - assert type_of("[a | 1]") == {:list, {:variable, :a}} + assert type_of("[a]") == {:list, {:variable, :a, 1}} + assert type_of("[a | 1]") == {:list, {:variable, :a, 1}} assert type_of("[a]", :match) == {:list, nil} assert type_of("[a | 1]", :match) == {:list, nil} - assert type_of("[^a]", :match) == {:list, {:variable, :a}} + assert type_of("[^a]", :match) == {:list, {:variable, :a, 1}} assert type_of("[[1]]") == {:list, {:list, {:integer, 1}}} # TODO union a | b? - assert type_of("[a, b]") == {:list, {:variable, :a}} - assert type_of("[a | b]") == {:list, {:variable, :a}} - assert type_of("[a, b | c]") == {:list, {:variable, :a}} + assert type_of("[a, b]") == {:list, {:variable, :a, 1}} + assert type_of("[a | b]") == {:list, {:variable, :a, 1}} + assert type_of("[a, b | c]") == {:list, {:variable, :a, 1}} end test "list operators" do assert type_of(":erlang.++([a], [b])") == - {:call, {:atom, :erlang}, :++, [list: {:variable, :a}, list: {:variable, :b}]} + {:call, {:atom, :erlang}, :++, + [list: {:variable, :a, 1}, list: {:variable, :b, 1}]} assert type_of(":erlang.--([a], [b])") == - {:call, {:atom, :erlang}, :--, [list: {:variable, :a}, list: {:variable, :b}]} + {:call, {:atom, :erlang}, :--, + [list: {:variable, :a, 1}, list: {:variable, :b, 1}]} end test "tuple" do assert type_of("{}") == {:tuple, 0, []} - assert type_of("{a}") == {:tuple, 1, [{:variable, :a}]} - assert type_of("{a, b}") == {:tuple, 2, [{:variable, :a}, {:variable, :b}]} + assert type_of("{a}") == {:tuple, 1, [{:variable, :a, 1}]} + assert type_of("{a, b}") == {:tuple, 2, [{:variable, :a, 1}, {:variable, :b, 1}]} end test "map" do assert type_of("%{}") == {:map, [], nil} - assert type_of("%{asd: a}") == {:map, [{:asd, {:variable, :a}}], nil} + assert type_of("%{asd: a}") == {:map, [{:asd, {:variable, :a, 1}}], nil} # NOTE non atom keys are not supported assert type_of("%{\"asd\" => a}") == {:map, [], nil} assert type_of("%{b | asd: a}") == - {:map, [{:asd, {:variable, :a}}], {:variable, :b}} + {:map, [{:asd, {:variable, :a, 1}}], {:variable, :b, 1}} assert type_of("%{b | asd: a}", :match) == :none end @@ -403,22 +407,22 @@ defmodule ElixirSense.Core.TypeInferenceTest do assert type_of("%{__struct__: Foo}") == {:struct, [], {:atom, Foo}, nil} assert type_of("%{__struct__: Foo, asd: a}") == - {:struct, [{:asd, {:variable, :a}}], {:atom, Foo}, nil} + {:struct, [{:asd, {:variable, :a, 1}}], {:atom, Foo}, nil} assert type_of("%{b | __struct__: Foo, asd: a}") == - {:struct, [{:asd, {:variable, :a}}], {:atom, Foo}, {:variable, :b}} + {:struct, [{:asd, {:variable, :a, 1}}], {:atom, Foo}, {:variable, :b, 1}} end test "struct" do assert type_of("%Foo{}") == {:struct, [], {:atom, Foo}, nil} - assert type_of("%a{}") == {:struct, [], {:variable, :a}, nil} + assert type_of("%a{}") == {:struct, [], {:variable, :a, 1}, nil} assert type_of("%@a{}") == {:struct, [], {:attribute, :a}, nil} assert type_of("%Foo{asd: a}") == - {:struct, [{:asd, {:variable, :a}}], {:atom, Foo}, nil} + {:struct, [{:asd, {:variable, :a, 1}}], {:atom, Foo}, nil} assert type_of("%Foo{b | asd: a}") == - {:struct, [{:asd, {:variable, :a}}], {:atom, Foo}, {:variable, :b}} + {:struct, [{:asd, {:variable, :a, 1}}], {:atom, Foo}, {:variable, :b, 1}} assert type_of("%Foo{b | asd: a}", :match) == :none end @@ -426,13 +430,19 @@ defmodule ElixirSense.Core.TypeInferenceTest do test "range" do assert type_of("a..b") == {:struct, - [{:first, {:variable, :a}}, {:last, {:variable, :b}}, {:step, {:integer, 1}}], - {:atom, Range}, nil} + [ + {:first, {:variable, :a, 1}}, + {:last, {:variable, :b, 1}}, + {:step, {:integer, 1}} + ], {:atom, Range}, nil} assert type_of("a..b//2") == {:struct, - [{:first, {:variable, :a}}, {:last, {:variable, :b}}, {:step, {:integer, 2}}], - {:atom, Range}, nil} + [ + {:first, {:variable, :a, 1}}, + {:last, {:variable, :b, 1}}, + {:step, {:integer, 2}} + ], {:atom, Range}, nil} end test "sigil" do @@ -446,21 +456,21 @@ defmodule ElixirSense.Core.TypeInferenceTest do end test "local call" do - assert type_of("foo(a)") == {:local_call, :foo, [{:variable, :a}]} + assert type_of("foo(a)") == {:local_call, :foo, [{:variable, :a, 1}]} end test "remote call" do - assert type_of(":foo.bar(a)") == {:call, {:atom, :foo}, :bar, [variable: :a]} + assert type_of(":foo.bar(a)") == {:call, {:atom, :foo}, :bar, [{:variable, :a, 1}]} end test "match" do assert type_of("a = 5") == {:integer, 5} - assert type_of("5 = a") == {:intersection, [integer: 5, variable: :a]} - assert type_of("b = 5 = a") == {:intersection, [{:integer, 5}, {:variable, :a}]} + assert type_of("5 = a") == {:intersection, [{:integer, 5}, {:variable, :a, 1}]} + assert type_of("b = 5 = a") == {:intersection, [{:integer, 5}, {:variable, :a, 1}]} assert type_of("5 = 5") == {:integer, 5} assert type_of("%{foo: a} = %{bar: b}") == - {:intersection, [{:map, [foo: nil], nil}, {:map, [bar: {:variable, :b}], nil}]} + {:intersection, [{:map, [foo: nil], nil}, {:map, [bar: {:variable, :b, 1}], nil}]} assert type_of("%{foo: a} = %{bar: b}", :match) == {:intersection, [{:map, [foo: nil], nil}, {:map, [bar: nil], nil}]} From dc1468b23d21ae34a21494bbe8a6fb57589cda95 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 1 Aug 2024 09:22:25 +0200 Subject: [PATCH 122/235] find any version variable --- lib/elixir_sense/core/binding.ex | 23 +++++++++++++++++++++-- test/elixir_sense/core/binding_test.exs | 13 +++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/lib/elixir_sense/core/binding.ex b/lib/elixir_sense/core/binding.ex index f8b2612f..02d4dfe5 100644 --- a/lib/elixir_sense/core/binding.ex +++ b/lib/elixir_sense/core/binding.ex @@ -84,6 +84,25 @@ defmodule ElixirSense.Core.Binding do expand(env, combined, stack) end + def do_expand(%Binding{variables: variables} = env, {:variable, variable, :any}, stack) do + sorted_variables = Enum.sort_by(variables, &(-&1.version)) + + type = + case Enum.find(sorted_variables, fn %State.VarInfo{} = var -> + var.name == variable + end) do + nil -> + # no variable found - treat a local call + # TODO this cannot happen + expand(env, {:local_call, variable, []}, stack) + + %State.VarInfo{type: type} -> + type + end + + expand(env, type, stack) + end + def do_expand(%Binding{variables: variables} = env, {:variable, variable, version}, stack) do type = case Enum.find(variables, fn %State.VarInfo{} = var -> @@ -1239,12 +1258,12 @@ defmodule ElixirSense.Core.Binding do end defp parse_type(_env, {:keyword, _, []}, _mod, _include_private, _stack) do - # TODO no support for atom type for now + # no support for atom type for now {:list, {:tuple, 2, [nil, nil]}} end defp parse_type(env, {:keyword, _, [type]}, mod, include_private, stack) do - # TODO no support for atom type for now + # no support for atom type for now {:list, {:tuple, 2, [nil, parse_type(env, type, mod, include_private, stack)]}} end diff --git a/test/elixir_sense/core/binding_test.exs b/test/elixir_sense/core/binding_test.exs index 86b97b49..ec21a068 100644 --- a/test/elixir_sense/core/binding_test.exs +++ b/test/elixir_sense/core/binding_test.exs @@ -317,6 +317,19 @@ defmodule ElixirSense.Core.BindingTest do ) end + test "known variable any version chooses max" do + assert {:atom, :abc} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :v, type: {:atom, :foo}}, + %VarInfo{version: 3, name: :v, type: {:atom, :abc}}, + %VarInfo{version: 2, name: :v, type: {:atom, :bar}} + ]), + {:variable, :v, :any} + ) + end + test "known variable self referencing" do assert nil == Binding.expand( From d7ed649a623036f551754a15d87f272c00346137 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 2 Aug 2024 07:03:47 +0200 Subject: [PATCH 123/235] add versions in more places --- lib/elixir_sense/core/source.ex | 21 +++++-- lib/elixir_sense/core/state.ex | 34 +++++++--- lib/elixir_sense/core/surround_context.ex | 4 +- .../providers/completion/completion_engine.ex | 2 +- .../providers/definition/locator.ex | 7 ++- lib/elixir_sense/providers/hover/docs.ex | 9 +-- .../providers/implementation/locator.ex | 58 +++++++++-------- .../providers/plugins/phoenix/scope.ex | 6 +- .../providers/references/locator.ex | 9 ++- .../core/metadata_builder_test.exs | 63 ++++++++++--------- test/elixir_sense/core/source_test.exs | 16 +++-- 11 files changed, 140 insertions(+), 89 deletions(-) diff --git a/lib/elixir_sense/core/source.ex b/lib/elixir_sense/core/source.ex index e20628bd..1028bbbe 100644 --- a/lib/elixir_sense/core/source.ex +++ b/lib/elixir_sense/core/source.ex @@ -163,7 +163,7 @@ defmodule ElixirSense.Core.Source do end) end - @type var_or_attr_t :: {:variable, atom} | {:attribute, atom} | nil + @type var_or_attr_t :: {:variable, atom, non_neg_integer | :any} | {:attribute, atom} | nil @spec which_struct(String.t(), nil | module) :: nil @@ -251,8 +251,17 @@ defmodule ElixirSense.Core.Source do end end - defp get_var_or_attr({var, _, nil}) when is_atom(var) and var != :__MODULE__ do - {:variable, var} + defp get_var_or_attr({var, meta, context}) + when is_atom(var) and is_atom(context) and + var not in [ + :__MODULE__, + :__DIR__, + :__ENV__, + :__CALLER__, + :__STACKTRACE__, + :_ + ] do + {:variable, var, Keyword.get(meta, :version, :any)} end defp get_var_or_attr({:@, _, [{attr, _, nil}]}) when is_atom(attr) do @@ -518,8 +527,10 @@ defmodule ElixirSense.Core.Source do end end - def get_mod_fun([{name, _, nil}, fun], binding_env) when is_atom(name) do - case Binding.expand(binding_env, {:variable, name}) do + def get_mod_fun([{name, meta, context}, fun], binding_env) + when is_atom(name) and is_atom(context) and + name not in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__, :_] do + case Binding.expand(binding_env, {:variable, name, Keyword.get(meta, :version, :any)}) do {:atom, atom} -> {{atom, false}, fun} diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 2aefdd50..548334cc 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -410,20 +410,25 @@ defmodule ElixirSense.Core.State do def add_call_to_line( %__MODULE__{} = state, - {{:@, _meta, [{name, _name_meta, _args}]}, func, arity}, + {{:@, _meta, [{name, _name_meta, nil}]}, func, arity}, {_line, _column} = position ) when is_atom(name) do - add_call_to_line(state, {{:attribute, name}, func, arity}, position) + do_add_call_to_line(state, {{:attribute, name}, func, arity}, position) end def add_call_to_line( %__MODULE__{} = state, - {{name, _name_meta, args}, func, arity}, + {{name, meta, args}, func, arity}, {_line, _column} = position ) - when is_atom(args) do - add_call_to_line(state, {{:variable, name}, func, arity}, position) + when is_atom(name) and is_atom(args) and + name not in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__, :_] do + do_add_call_to_line( + state, + {{:variable, name, Keyword.get(meta, :version, :any)}, func, arity}, + position + ) end def add_call_to_line( @@ -432,19 +437,28 @@ defmodule ElixirSense.Core.State do {_line, _column} = position ) when is_atom(name) do - add_call_to_line(state, {nil, {:attribute, name}, arity}, position) + do_add_call_to_line(state, {nil, {:attribute, name}, arity}, position) end def add_call_to_line( %__MODULE__{} = state, - {nil, {name, _name_meta, args}, arity}, + {nil, {name, meta, args}, arity}, {_line, _column} = position ) - when is_atom(args) do - add_call_to_line(state, {nil, {:variable, name}, arity}, position) + when is_atom(name) and is_atom(args) and + name not in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__, :_] do + do_add_call_to_line( + state, + {nil, {:variable, name, Keyword.get(meta, :version, :any)}, arity}, + position + ) + end + + def add_call_to_line(state, call, position) do + do_add_call_to_line(state, call, position) end - def add_call_to_line(%__MODULE__{} = state, {mod, func, arity}, {line, _column} = position) do + defp do_add_call_to_line(%__MODULE__{} = state, {mod, func, arity}, {line, _column} = position) do call = %CallInfo{mod: mod, func: func, arity: arity, position: position} calls = diff --git a/lib/elixir_sense/core/surround_context.ex b/lib/elixir_sense/core/surround_context.ex index 4898c711..8f1b5893 100644 --- a/lib/elixir_sense/core/surround_context.ex +++ b/lib/elixir_sense/core/surround_context.ex @@ -38,7 +38,7 @@ defmodule ElixirSense.Core.SurroundContext do end defp to_binding_impl({:local_or_var, charlist}, _current_module) do - {:variable, :"#{charlist}"} + {:variable, :"#{charlist}", :any} end defp to_binding_impl({:local_arity, charlist}, _current_module) do @@ -120,7 +120,7 @@ defmodule ElixirSense.Core.SurroundContext do end defp inside_dot_to_binding({:var, inside_charlist}, _current_module) do - {:variable, :"#{inside_charlist}"} + {:variable, :"#{inside_charlist}", :any} end defp inside_dot_to_binding(:expr, _current_module) do diff --git a/lib/elixir_sense/providers/completion/completion_engine.ex b/lib/elixir_sense/providers/completion/completion_engine.ex index 36ed51bd..88472936 100644 --- a/lib/elixir_sense/providers/completion/completion_engine.ex +++ b/lib/elixir_sense/providers/completion/completion_engine.ex @@ -306,7 +306,7 @@ defmodule ElixirSense.Providers.Completion.CompletionEngine do end defp expand_dot_path({:var, var}, %State.Env{} = env, %Metadata{} = metadata) do - value_from_binding({:variable, List.to_atom(var)}, env, metadata) + value_from_binding({:variable, List.to_atom(var), :any}, env, metadata) end defp expand_dot_path({:module_attribute, attribute}, %State.Env{} = env, %Metadata{} = metadata) do diff --git a/lib/elixir_sense/providers/definition/locator.ex b/lib/elixir_sense/providers/definition/locator.ex index 90a420f2..f0a65938 100644 --- a/lib/elixir_sense/providers/definition/locator.ex +++ b/lib/elixir_sense/providers/definition/locator.ex @@ -78,12 +78,13 @@ defmodule ElixirSense.Providers.Definition.Locator do {:keyword, _} -> nil - {:variable, variable} -> + {:variable, variable, version} -> var_info = vars |> Enum.find(fn - %VarInfo{name: name, positions: positions} -> - name == variable and context.begin in positions + %VarInfo{} = info -> + info.name == variable and (info.version == version or version == :any) and + context.begin in info.positions end) if var_info != nil do diff --git a/lib/elixir_sense/providers/hover/docs.ex b/lib/elixir_sense/providers/hover/docs.ex index d2317816..5bcaa736 100644 --- a/lib/elixir_sense/providers/hover/docs.ex +++ b/lib/elixir_sense/providers/hover/docs.ex @@ -127,14 +127,15 @@ defmodule ElixirSense.Providers.Hover.Docs do docs: docs } - {:variable, variable} -> + {:variable, variable, version} -> {line, column} = context.begin var_info = vars |> Enum.find(fn - %VarInfo{name: name, positions: positions} -> - name == variable and {line, column} in positions + %VarInfo{} = info -> + info.name == variable and (info.version == version or version == :any) and + {line, column} in info.positions end) if var_info != nil do @@ -144,7 +145,7 @@ defmodule ElixirSense.Providers.Hover.Docs do } else mod_fun_docs( - type, + {nil, variable}, context, binding_env, env, diff --git a/lib/elixir_sense/providers/implementation/locator.ex b/lib/elixir_sense/providers/implementation/locator.ex index e6851fc9..2e0e76b8 100644 --- a/lib/elixir_sense/providers/implementation/locator.ex +++ b/lib/elixir_sense/providers/implementation/locator.ex @@ -63,6 +63,10 @@ defmodule ElixirSense.Providers.Implementation.Locator do {kind, _} when kind in [:attribute, :keyword] -> [] + {:variable, name, _} -> + # treat variable name as local function call + do_find(nil, name, context, env, metadata, binding_env) + {module_type, function} -> module = case Binding.expand(binding_env, module_type) do @@ -73,32 +77,36 @@ defmodule ElixirSense.Providers.Implementation.Locator do env.module end - {line, column} = context.end - call_arity = Metadata.get_call_arity(metadata, module, function, line, column) || :any - - behaviour_implementations = - find_behaviour_implementations( - module, - function, - call_arity, - module, - env, - metadata, - binding_env - ) + do_find(module, function, context, env, metadata, binding_env) + end + end - if behaviour_implementations == [] do - find_delegatee( - {module, function}, - call_arity, - env, - metadata, - binding_env - ) - |> List.wrap() - else - behaviour_implementations - end + defp do_find(module, function, context, env, metadata, binding_env) do + {line, column} = context.end + call_arity = Metadata.get_call_arity(metadata, module, function, line, column) || :any + + behaviour_implementations = + find_behaviour_implementations( + module, + function, + call_arity, + module, + env, + metadata, + binding_env + ) + + if behaviour_implementations == [] do + find_delegatee( + {module, function}, + call_arity, + env, + metadata, + binding_env + ) + |> List.wrap() + else + behaviour_implementations end end diff --git a/lib/elixir_sense/providers/plugins/phoenix/scope.ex b/lib/elixir_sense/providers/plugins/phoenix/scope.ex index 75c08634..e7730b2c 100644 --- a/lib/elixir_sense/providers/plugins/phoenix/scope.ex +++ b/lib/elixir_sense/providers/plugins/phoenix/scope.ex @@ -98,8 +98,10 @@ defmodule ElixirSense.Providers.Plugins.Phoenix.Scope do get_mod(scope_alias, binding_env) end - defp get_mod({name, _, nil}, binding_env) when is_atom(name) do - case Binding.expand(binding_env, {:variable, name}) do + defp get_mod({name, meta, context}, binding_env) + when is_atom(name) and is_atom(context) and + name not in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__, :_] do + case Binding.expand(binding_env, {:variable, name, Keyword.get(meta, :version, :any)}) do {:atom, atom} -> atom diff --git a/lib/elixir_sense/providers/references/locator.ex b/lib/elixir_sense/providers/references/locator.ex index 1de5b900..7dbf76e8 100644 --- a/lib/elixir_sense/providers/references/locator.ex +++ b/lib/elixir_sense/providers/references/locator.ex @@ -186,12 +186,15 @@ defmodule ElixirSense.Providers.References.Locator do {:keyword, _} -> [] - {:variable, variable} -> + {:variable, variable, version} -> {line, column} = context.begin var_info = - Enum.find(vars, fn %VarInfo{name: name, positions: positions} -> - name == variable and {line, column} in positions + Enum.find_value(vars, fn {{_name, _version}, %VarInfo{} = info} -> + if info.name == variable and (info.version == version or version == :any) and + {line, column} in info.positions do + info + end end) if var_info != nil do diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 497201d2..6750c07f 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -6657,7 +6657,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do state = """ defmodule NyModule do - def func do + @attr Some + def func(var) do @attr.func("test") var.func("test") end @@ -6667,21 +6668,21 @@ defmodule ElixirSense.Core.MetadataBuilderTest do if Version.match?(System.version(), ">= 1.15.0") do assert state.calls == %{ - 3 => [ - %CallInfo{arity: 1, func: :func, position: {3, 11}, mod: {:attribute, :attr}} - ], 4 => [ - %CallInfo{arity: 1, func: :func, position: {4, 9}, mod: {:variable, :var}} + %CallInfo{arity: 1, func: :func, position: {4, 11}, mod: {:attribute, :attr}} + ], + 5 => [ + %CallInfo{arity: 1, func: :func, position: {5, 9}, mod: {:variable, :var, 0}} ] } else assert state.calls == %{ - 3 => [ - %CallInfo{arity: 1, func: :func, position: {3, 11}, mod: {:attribute, :attr}} - ], 4 => [ - %CallInfo{arity: 0, func: :var, position: {4, 5}, mod: nil}, - %CallInfo{arity: 1, func: :func, position: {4, 9}, mod: {:variable, :var}} + %CallInfo{arity: 1, func: :func, position: {4, 11}, mod: {:attribute, :attr}} + ], + 5 => [ + %CallInfo{arity: 0, func: :var, position: {5, 5}, mod: nil}, + %CallInfo{arity: 1, func: :func, position: {5, 9}, mod: {:variable, :var, 0}} ] } end @@ -6691,7 +6692,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do state = """ defmodule NyModule do - def func do + @attr (fn -> :ok end) + def func(var) do @attr.func var.func end @@ -6701,21 +6703,21 @@ defmodule ElixirSense.Core.MetadataBuilderTest do if Version.match?(System.version(), ">= 1.15.0") do assert state.calls == %{ - 3 => [ - %CallInfo{arity: 0, func: :func, position: {3, 11}, mod: {:attribute, :attr}} - ], 4 => [ - %CallInfo{arity: 0, func: :func, position: {4, 9}, mod: {:variable, :var}} + %CallInfo{arity: 0, func: :func, position: {4, 11}, mod: {:attribute, :attr}} + ], + 5 => [ + %CallInfo{arity: 0, func: :func, position: {5, 9}, mod: {:variable, :var, 0}} ] } else assert state.calls == %{ - 3 => [ - %CallInfo{arity: 0, func: :func, position: {3, 11}, mod: {:attribute, :attr}} - ], 4 => [ - %CallInfo{arity: 0, func: :var, position: {4, 5}, mod: nil}, - %CallInfo{arity: 0, func: :func, position: {4, 9}, mod: {:variable, :var}} + %CallInfo{arity: 0, func: :func, position: {4, 11}, mod: {:attribute, :attr}} + ], + 5 => [ + %CallInfo{arity: 0, func: :var, position: {5, 5}, mod: nil}, + %CallInfo{arity: 0, func: :func, position: {5, 9}, mod: {:variable, :var, 0}} ] } end @@ -6725,7 +6727,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do state = """ defmodule NyModule do - def func do + @attr (fn -> :ok end) + def func(var) do @attr.() var.() end @@ -6735,19 +6738,21 @@ defmodule ElixirSense.Core.MetadataBuilderTest do if Version.match?(System.version(), ">= 1.15.0") do assert state.calls == %{ - 3 => [ - %CallInfo{arity: 0, func: {:attribute, :attr}, position: {3, 11}, mod: nil} + 4 => [ + %CallInfo{arity: 0, func: {:attribute, :attr}, position: {4, 11}, mod: nil} ], - 4 => [%CallInfo{arity: 0, func: {:variable, :var}, position: {4, 9}, mod: nil}] + 5 => [ + %CallInfo{arity: 0, func: {:variable, :var, 0}, position: {5, 9}, mod: nil} + ] } else assert state.calls == %{ - 3 => [ - %CallInfo{arity: 0, func: {:attribute, :attr}, position: {3, 11}, mod: nil} - ], 4 => [ - %CallInfo{arity: 0, func: :var, position: {4, 5}, mod: nil}, - %CallInfo{arity: 0, func: {:variable, :var}, position: {4, 9}, mod: nil} + %CallInfo{arity: 0, func: {:attribute, :attr}, position: {4, 11}, mod: nil} + ], + 5 => [ + %CallInfo{arity: 0, func: :var, position: {5, 5}, mod: nil}, + %CallInfo{arity: 0, func: {:variable, :var, 0}, position: {5, 9}, mod: nil} ] } end diff --git a/test/elixir_sense/core/source_test.exs b/test/elixir_sense/core/source_test.exs index 1577e916..50ea4f79 100644 --- a/test/elixir_sense/core/source_test.exs +++ b/test/elixir_sense/core/source_test.exs @@ -198,7 +198,13 @@ defmodule ElixirSense.Core.SourceTest do assert nil == which_func("var = my_var.some(", %ElixirSense.Core.Binding{ - variables: [%{name: "my_var", type: {:atom, Some}}] + variables: [ + %ElixirSense.Core.State.VarInfo{ + name: "my_var", + version: 1, + type: {:atom, Some} + } + ] }) end @@ -700,7 +706,7 @@ defmodule ElixirSense.Core.SourceTest do var = %{asd | """ - assert which_struct(code, MyMod) == {:map, [], {:variable, :asd}} + assert which_struct(code, MyMod) == {:map, [], {:variable, :asd, :any}} end test "map update attribute" do @@ -720,7 +726,7 @@ defmodule ElixirSense.Core.SourceTest do var = %{asd | qwe: "ds", """ - assert which_struct(code, MyMod) == {:map, [:qwe], {:variable, :asd}} + assert which_struct(code, MyMod) == {:map, [:qwe], {:variable, :asd, :any}} end test "patern match with _" do @@ -893,10 +899,10 @@ defmodule ElixirSense.Core.SourceTest do """ assert which_struct(text_before(code, 3, 23), MyMod) == - {{:atom, Mod}, [], false, {:variable, :par1}} + {{:atom, Mod}, [], false, {:variable, :par1, :any}} assert which_struct(text_before(code, 5, 7), MyMod) == - {{:atom, Mod}, [:field1], false, {:variable, :par1}} + {{:atom, Mod}, [:field1], false, {:variable, :par1, :any}} end test "struct update attribute syntax" do From 2a48c2666f3411c420cbcd1f3c39ec9b0bc7fecf Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 4 Aug 2024 10:25:36 +0200 Subject: [PATCH 124/235] address todo --- lib/elixir_sense/core/binding.ex | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/lib/elixir_sense/core/binding.ex b/lib/elixir_sense/core/binding.ex index 02d4dfe5..09c2efc6 100644 --- a/lib/elixir_sense/core/binding.ex +++ b/lib/elixir_sense/core/binding.ex @@ -84,34 +84,18 @@ defmodule ElixirSense.Core.Binding do expand(env, combined, stack) end - def do_expand(%Binding{variables: variables} = env, {:variable, variable, :any}, stack) do - sorted_variables = Enum.sort_by(variables, &(-&1.version)) + def do_expand(%Binding{variables: variables} = env, {:variable, variable, version}, stack) do + sorted_variables = Enum.sort_by(variables, &{&1.name, -&1.version}) type = case Enum.find(sorted_variables, fn %State.VarInfo{} = var -> - var.name == variable - end) do - nil -> - # no variable found - treat a local call - # TODO this cannot happen - expand(env, {:local_call, variable, []}, stack) - - %State.VarInfo{type: type} -> - type - end - - expand(env, type, stack) - end - - def do_expand(%Binding{variables: variables} = env, {:variable, variable, version}, stack) do - type = - case Enum.find(variables, fn %State.VarInfo{} = var -> - var.name == variable and var.version == version + var.name == variable and (var.version == version or version == :any) end) do nil -> # no variable found - treat a local call - # TODO this cannot happen - expand(env, {:local_call, variable, []}, stack) + # this cannot happen if no parens call is missclassed as variable e.g. by + # Code.Fragment APIs + {:local_call, variable, []} %State.VarInfo{type: type} -> type From 17d419a2d8af4a1d9db9f4ac8adcc0faf9e4c79f Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 4 Aug 2024 10:26:30 +0200 Subject: [PATCH 125/235] handle rewritten versions in binding add implementation for more tuple functions --- lib/elixir_sense/core/binding.ex | 301 ++++++++++++++++++++- test/elixir_sense/core/binding_test.exs | 343 +++++++++++++++++++++++- 2 files changed, 628 insertions(+), 16 deletions(-) diff --git a/lib/elixir_sense/core/binding.ex b/lib/elixir_sense/core/binding.ex index 09c2efc6..8dfd7ba1 100644 --- a/lib/elixir_sense/core/binding.ex +++ b/lib/elixir_sense/core/binding.ex @@ -399,13 +399,13 @@ defmodule ElixirSense.Core.Binding do defp expand_call( env, - {:atom, :erlang}, + {:atom, module}, name, [list_candidate | _], _include_private, stack ) - when name in [:++, :--] do + when name in [:++, :--] and module in [Kernel, :erlang] do case expand(env, list_candidate, stack) do {:list, type} -> {:list, type} @@ -438,14 +438,260 @@ defmodule ElixirSense.Core.Binding do end end + # rewritten elem + defp expand_call( + env, + {:atom, :erlang}, + :element, + [n_candidate, tuple_candidate], + _include_private, + stack + ) do + case expand(env, n_candidate, stack) do + {:integer, n} -> + expand(env, {:tuple_nth, tuple_candidate, n - 1}, stack) + + nil -> + nil + + _ -> + :none + end + end + defp expand_call( env, {:atom, Kernel}, + :put_elem, + [tuple_candidate, n_candidate, value], + _include_private, + stack + ) do + with {:tuple, elems_count, elems} <- expand(env, tuple_candidate, stack), + {:integer, n} when n >= 0 and n < elems_count <- expand(env, n_candidate, stack), + expanded_value when expanded_value != :none <- expand(env, value, stack) do + {:tuple, elems_count, elems |> List.replace_at(n, expanded_value)} + else + nil -> + nil + + _ -> + :none + end + end + + # rewritten put_elem + defp expand_call( + env, + {:atom, :erlang}, + :setelement, + [n_candidate, tuple_candidate, value], + _include_private, + stack + ) do + with {:tuple, elems_count, elems} <- expand(env, tuple_candidate, stack), + {:integer, n} when n >= 1 and n <= elems_count <- expand(env, n_candidate, stack), + expanded_value when expanded_value != :none <- expand(env, value, stack) do + {:tuple, elems_count, elems |> List.replace_at(n - 1, expanded_value)} + else + nil -> + nil + + _ -> + :none + end + end + + defp expand_call( + env, + {:atom, module}, + fun, + [tuple_candidate, value], + _include_private, + stack + ) + when (module == Tuple and fun == :append) or (module == :erlang and fun == :append_element) do + with {:tuple, elems_count, elems} <- expand(env, tuple_candidate, stack), + expanded_value when expanded_value != :none <- expand(env, value, stack) do + {:tuple, elems_count + 1, elems ++ [expanded_value]} + else + nil -> + nil + + _ -> + :none + end + end + + defp expand_call( + env, + {:atom, Tuple}, + :delete_at, + [tuple_candidate, n_candidate], + _include_private, + stack + ) do + with {:tuple, elems_count, elems} <- expand(env, tuple_candidate, stack), + {:integer, n} when n >= 0 and n < elems_count <- expand(env, n_candidate, stack) do + {:tuple, elems_count - 1, elems |> List.delete_at(n)} + else + nil -> + nil + + _ -> + :none + end + end + + # rewritten Tuple.delete_at + defp expand_call( + env, + {:atom, :erlang}, + :delete_element, + [n_candidate, tuple_candidate], + _include_private, + stack + ) do + with {:tuple, elems_count, elems} <- expand(env, tuple_candidate, stack), + {:integer, n} when n > 0 and n <= elems_count <- expand(env, n_candidate, stack) do + {:tuple, elems_count - 1, elems |> List.delete_at(n - 1)} + else + nil -> + nil + + _ -> + :none + end + end + + defp expand_call( + env, + {:atom, Tuple}, + :insert_at, + [tuple_candidate, n_candidate, value], + _include_private, + stack + ) do + with {:tuple, elems_count, elems} <- expand(env, tuple_candidate, stack), + {:integer, n} when n >= 0 and n <= elems_count <- expand(env, n_candidate, stack), + expanded_value when expanded_value != :none <- expand(env, value, stack) do + {:tuple, elems_count + 1, elems |> List.insert_at(n, expanded_value)} + else + nil -> + nil + + _ -> + :none + end + end + + # rewritten Tuple.insert_at + defp expand_call( + env, + {:atom, :erlang}, + :insert_element, + [n_candidate, tuple_candidate, value], + _include_private, + stack + ) do + with {:tuple, elems_count, elems} <- expand(env, tuple_candidate, stack), + {:integer, n} when n > 0 and n <= elems_count + 1 <- expand(env, n_candidate, stack), + expanded_value when expanded_value != :none <- expand(env, value, stack) do + {:tuple, elems_count + 1, elems |> List.insert_at(n - 1, expanded_value)} + else + nil -> + nil + + _ -> + :none + end + end + + defp expand_call( + env, + {:atom, module}, + fun, + [tuple_candidate], + _include_private, + stack + ) + when (module == Tuple and fun == :to_list) or (module == :erlang and fun == :tuple_to_list) do + with {:tuple, _elems_count, elems} <- expand(env, tuple_candidate, stack) do + case elems do + [] -> {:list, :empty} + [first | _] -> {:list, first} + end + else + nil -> + nil + + _ -> + :none + end + end + + defp expand_call( + env, + {:atom, module}, + :tuple_size, + [tuple_candidate], + _include_private, + stack + ) + when module in [Kernel, :erlang] do + with {:tuple, elems_count, _elems} <- expand(env, tuple_candidate, stack) do + {:integer, elems_count} + else + nil -> + nil + + _ -> + :none + end + end + + defp expand_call( + env, + {:atom, module}, + fun, + [value, n_candidate], + _include_private, + stack + ) + when (module == Tuple and fun == :duplicate) or (module == :erlang and fun == :make_tuple) do + {value, n_candidate} = + if module == :erlang do + {n_candidate, value} + else + {value, n_candidate} + end + + # limit to 5 + with {:integer, n} when n >= 0 <- expand(env, n_candidate, stack), + expanded_value when expanded_value != :none <- expand(env, value, stack) do + {:tuple, n, expanded_value |> List.duplicate(n)} + else + nil -> + nil + + {:integer, _n} -> + nil + + _ -> + :none + end + end + + # hd is inlined + defp expand_call( + env, + {:atom, module}, :hd, [list_candidate], _include_private, stack - ) do + ) + when module in [Kernel, :erlang] do case expand(env, list_candidate, stack) do {:list, type} -> type @@ -458,14 +704,16 @@ defmodule ElixirSense.Core.Binding do end end + # tl is inlined defp expand_call( env, - {:atom, Kernel}, + {:atom, module}, :tl, [list_candidate], _include_private, stack - ) do + ) + when module in [Kernel, :erlang] do case expand(env, list_candidate, stack) do {:list, type} -> {:list, type} @@ -762,8 +1010,17 @@ defmodule ElixirSense.Core.Binding do end end - defp expand_call(env, {:atom, Map}, fun, [map, key], _include_private, stack) - when fun in [:fetch, :fetch!, :get] do + defp expand_call(env, {:atom, module}, fun, [map, key], _include_private, stack) + when (module == Map and fun in [:fetch, :fetch!, :get]) or + (module == :maps and fun in [:find, :get]) do + {map, key} = + if module == :maps do + # rewritten versions have different arg order + {key, map} + else + {map, key} + end + fields = expand_map_fields(env, map, stack) if :none in fields do @@ -773,7 +1030,7 @@ defmodule ElixirSense.Core.Binding do {:atom, atom} -> value = fields |> Keyword.get(atom) - if fun == :fetch and value != nil do + if fun in [:fetch, :find] and value != nil do {:tuple, 2, [{:atom, :ok}, value]} else value @@ -809,8 +1066,17 @@ defmodule ElixirSense.Core.Binding do end end - defp expand_call(env, {:atom, Map}, fun, [map, key, value], _include_private, stack) - when fun in [:put, :replace!] do + defp expand_call(env, {:atom, module}, fun, [map, key, value], _include_private, stack) + when (fun == :put and module in [Map, :maps]) or (fun == :update and module == :maps) or + (fun == :replace! and module == Map) do + {map, key, value} = + if module == :maps do + # rewritten versions have different parameter order + {value, map, key} + else + {map, key, value} + end + fields = expand_map_fields(env, map, stack) if :none in fields do @@ -850,7 +1116,16 @@ defmodule ElixirSense.Core.Binding do end end - defp expand_call(env, {:atom, Map}, :delete, [map, key], _include_private, stack) do + defp expand_call(env, {:atom, module}, fun, [map, key], _include_private, stack) + when (module == Map and fun == :delete) or (module == :maps and fun == :remove) do + {map, key} = + if module == :maps do + # rewritten versions have different arg order + {key, map} + else + {map, key} + end + fields = expand_map_fields(env, map, stack) if :none in fields do @@ -869,7 +1144,9 @@ defmodule ElixirSense.Core.Binding do end end - defp expand_call(env, {:atom, Map}, :merge, [map, other_map], _include_private, stack) do + # Map.merge/2 is inlined + defp expand_call(env, {:atom, module}, :merge, [map, other_map], _include_private, stack) + when module in [Map, :maps] do fields = expand_map_fields(env, map, stack) other_fields = diff --git a/test/elixir_sense/core/binding_test.exs b/test/elixir_sense/core/binding_test.exs index ec21a068..8e20fce4 100644 --- a/test/elixir_sense/core/binding_test.exs +++ b/test/elixir_sense/core/binding_test.exs @@ -1727,8 +1727,18 @@ defmodule ElixirSense.Core.BindingTest do end end - describe ":erlang functions" do + describe "Kernel functions" do test "++" do + assert {:list, {:integer, 1}} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :a, type: {:integer, 1}}, + %VarInfo{version: 1, name: :b, type: {:integer, 2}} + ]), + {:local_call, :++, [list: {:variable, :a, 1}, list: {:variable, :b, 1}]} + ) + assert {:list, {:integer, 1}} == Binding.expand( @env @@ -1740,10 +1750,7 @@ defmodule ElixirSense.Core.BindingTest do [list: {:variable, :a, 1}, list: {:variable, :b, 1}]} ) end - end - describe "Kernel functions" do - # TODO check which of those get rewritten test "tuple elem" do assert {:atom, :a} == Binding.expand( @@ -1758,6 +1765,86 @@ defmodule ElixirSense.Core.BindingTest do ]), {:variable, :ref, 1} ) + + assert {:atom, :a} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :tuple, type: {:tuple, 2, [nil, {:atom, :a}]}}, + %VarInfo{ + version: 1, + name: :ref, + type: + {:call, {:atom, :erlang}, :element, + [{:integer, 2}, {:variable, :tuple, 1}]} + } + ]), + {:variable, :ref, 1} + ) + end + + test "tuple put_elem" do + assert {:tuple, 2, [{:atom, :b}, {:atom, :a}]} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :tuple, type: {:tuple, 2, [nil, {:atom, :a}]}}, + %VarInfo{ + version: 1, + name: :ref, + type: + {:local_call, :put_elem, + [{:variable, :tuple, 1}, {:integer, 0}, {:atom, :b}]} + } + ]), + {:variable, :ref, 1} + ) + + assert {:tuple, 2, [{:atom, :b}, {:atom, :a}]} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :tuple, type: {:tuple, 2, [nil, {:atom, :a}]}}, + %VarInfo{ + version: 1, + name: :ref, + type: + {:call, {:atom, :erlang}, :setelement, + [{:integer, 1}, {:variable, :tuple, 1}, {:atom, :b}]} + } + ]), + {:variable, :ref, 1} + ) + end + + test "tuple_size" do + assert {:integer, 2} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :tuple, type: {:tuple, 2, [nil, {:atom, :a}]}}, + %VarInfo{ + version: 1, + name: :ref, + type: {:local_call, :tuple_size, [{:variable, :tuple, 1}]} + } + ]), + {:variable, :ref, 1} + ) + + assert {:integer, 2} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :tuple, type: {:tuple, 2, [nil, {:atom, :a}]}}, + %VarInfo{ + version: 1, + name: :ref, + type: {:call, {:atom, :erlang}, :tuple_size, [{:variable, :tuple, 1}]} + } + ]), + {:variable, :ref, 1} + ) end test "list hd" do @@ -1774,6 +1861,20 @@ defmodule ElixirSense.Core.BindingTest do ]), {:variable, :ref, 1} ) + + assert {:atom, :a} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :list, type: {:list, {:atom, :a}}}, + %VarInfo{ + version: 1, + name: :ref, + type: {:call, {:atom, :erlang}, :hd, [{:variable, :list, 1}]} + } + ]), + {:variable, :ref, 1} + ) end test "list tl" do @@ -1790,6 +1891,200 @@ defmodule ElixirSense.Core.BindingTest do ]), {:variable, :ref, 1} ) + + assert {:list, {:atom, :a}} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :list, type: {:list, {:atom, :a}}}, + %VarInfo{ + version: 1, + name: :ref, + type: {:call, {:atom, :erlang}, :tl, [{:variable, :list, 1}]} + } + ]), + {:variable, :ref, 1} + ) + end + end + + describe "Tuple functions" do + test "append" do + assert {:tuple, 3, [nil, {:atom, :a}, {:atom, :b}]} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :tuple, type: {:tuple, 2, [nil, {:atom, :a}]}}, + %VarInfo{ + version: 1, + name: :ref, + type: {:call, {:atom, Tuple}, :append, [{:variable, :tuple, 1}, {:atom, :b}]} + } + ]), + {:variable, :ref, 1} + ) + + assert {:tuple, 3, [nil, {:atom, :a}, {:atom, :b}]} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :tuple, type: {:tuple, 2, [nil, {:atom, :a}]}}, + %VarInfo{ + version: 1, + name: :ref, + type: + {:call, {:atom, :erlang}, :append_element, + [{:variable, :tuple, 1}, {:atom, :b}]} + } + ]), + {:variable, :ref, 1} + ) + end + + test "delete_at" do + assert {:tuple, 1, [{:atom, :a}]} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :tuple, type: {:tuple, 2, [nil, {:atom, :a}]}}, + %VarInfo{ + version: 1, + name: :ref, + type: + {:call, {:atom, Tuple}, :delete_at, + [{:variable, :tuple, 1}, {:integer, 0}]} + } + ]), + {:variable, :ref, 1} + ) + + assert {:tuple, 1, [{:atom, :a}]} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :tuple, type: {:tuple, 2, [nil, {:atom, :a}]}}, + %VarInfo{ + version: 1, + name: :ref, + type: + {:call, {:atom, :erlang}, :delete_element, + [{:integer, 1}, {:variable, :tuple, 1}]} + } + ]), + {:variable, :ref, 1} + ) + end + + test "insert_at" do + assert {:tuple, 3, [{:atom, :b}, nil, {:atom, :a}]} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :tuple, type: {:tuple, 2, [nil, {:atom, :a}]}}, + %VarInfo{ + version: 1, + name: :ref, + type: + {:call, {:atom, Tuple}, :insert_at, + [{:variable, :tuple, 1}, {:integer, 0}, {:atom, :b}]} + } + ]), + {:variable, :ref, 1} + ) + + assert {:tuple, 3, [{:atom, :b}, nil, {:atom, :a}]} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :tuple, type: {:tuple, 2, [nil, {:atom, :a}]}}, + %VarInfo{ + version: 1, + name: :ref, + type: + {:call, {:atom, :erlang}, :insert_element, + [{:integer, 1}, {:variable, :tuple, 1}, {:atom, :b}]} + } + ]), + {:variable, :ref, 1} + ) + end + + test "to_list" do + assert {:list, {:atom, :a}} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :tuple, type: {:tuple, 1, [{:atom, :a}]}}, + %VarInfo{ + version: 1, + name: :ref, + type: {:call, {:atom, Tuple}, :to_list, [{:variable, :tuple, 1}]} + } + ]), + {:variable, :ref, 1} + ) + + assert {:list, :empty} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :tuple, type: {:tuple, 0, []}}, + %VarInfo{ + version: 1, + name: :ref, + type: {:call, {:atom, Tuple}, :to_list, [{:variable, :tuple, 1}]} + } + ]), + {:variable, :ref, 1} + ) + + assert {:list, {:atom, :a}} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :tuple, type: {:tuple, 1, [{:atom, :a}]}}, + %VarInfo{ + version: 1, + name: :ref, + type: {:call, {:atom, :erlang}, :tuple_to_list, [{:variable, :tuple, 1}]} + } + ]), + {:variable, :ref, 1} + ) + end + + test "duplicate" do + assert {:tuple, 2, [{:atom, :a}, {:atom, :a}]} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :tuple, type: {:atom, :a}}, + %VarInfo{ + version: 1, + name: :ref, + type: + {:call, {:atom, Tuple}, :duplicate, + [{:variable, :tuple, 1}, {:integer, 2}]} + } + ]), + {:variable, :ref, 1} + ) + + assert {:tuple, 2, [{:atom, :a}, {:atom, :a}]} == + Binding.expand( + @env + |> Map.put(:variables, [ + %VarInfo{version: 1, name: :tuple, type: {:atom, :a}}, + %VarInfo{ + version: 1, + name: :ref, + type: + {:call, {:atom, :erlang}, :make_tuple, + [{:integer, 2}, {:variable, :tuple, 1}]} + } + ]), + {:variable, :ref, 1} + ) end end @@ -1801,6 +2096,13 @@ defmodule ElixirSense.Core.BindingTest do {:call, {:atom, Map}, :put, [{:map, [abc: {:atom, :a}], nil}, {:atom, :cde}, {:atom, :b}]} ) + + assert {:map, [cde: {:atom, :b}, abc: {:atom, :a}], nil} = + Binding.expand( + @env, + {:call, {:atom, :maps}, :put, + [{:atom, :cde}, {:atom, :b}, {:map, [abc: {:atom, :a}], nil}]} + ) end test "put not a map" do @@ -1823,6 +2125,13 @@ defmodule ElixirSense.Core.BindingTest do {:call, {:atom, Map}, :delete, [{:map, [abc: {:atom, :a}, cde: nil], nil}, {:atom, :cde}]} ) + + assert {:map, [abc: {:atom, :a}], nil} = + Binding.expand( + @env, + {:call, {:atom, :maps}, :remove, + [{:atom, :cde}, {:map, [abc: {:atom, :a}, cde: nil], nil}]} + ) end test "merge" do @@ -1832,6 +2141,13 @@ defmodule ElixirSense.Core.BindingTest do {:call, {:atom, Map}, :merge, [{:map, [abc: {:atom, :a}], nil}, {:map, [cde: {:atom, :b}], nil}]} ) + + assert {:map, [abc: {:atom, :a}, cde: {:atom, :b}], nil} = + Binding.expand( + @env, + {:call, {:atom, :maps}, :merge, + [{:map, [abc: {:atom, :a}], nil}, {:map, [cde: {:atom, :b}], nil}]} + ) end test "merge/3" do @@ -1886,6 +2202,13 @@ defmodule ElixirSense.Core.BindingTest do {:call, {:atom, Map}, :replace!, [{:map, [abc: {:atom, :a}], nil}, {:atom, :abc}, {:atom, :b}]} ) + + assert {:map, [abc: {:atom, :b}], nil} = + Binding.expand( + @env, + {:call, {:atom, :maps}, :update, + [{:atom, :abc}, {:atom, :b}, {:map, [abc: {:atom, :a}], nil}]} + ) end test "put_new" do @@ -1912,6 +2235,12 @@ defmodule ElixirSense.Core.BindingTest do @env, {:call, {:atom, Map}, :fetch!, [{:map, [abc: {:atom, :a}], nil}, {:atom, :abc}]} ) + + assert {:atom, :a} = + Binding.expand( + @env, + {:call, {:atom, :maps}, :get, [{:atom, :abc}, {:map, [abc: {:atom, :a}], nil}]} + ) end test "fetch" do @@ -1920,6 +2249,12 @@ defmodule ElixirSense.Core.BindingTest do @env, {:call, {:atom, Map}, :fetch, [{:map, [abc: {:atom, :a}], nil}, {:atom, :abc}]} ) + + assert {:tuple, 2, [atom: :ok, atom: :a]} = + Binding.expand( + @env, + {:call, {:atom, :maps}, :find, [{:atom, :abc}, {:map, [abc: {:atom, :a}], nil}]} + ) end test "get" do From 4f3b28f63f05446a8a9b26f1fe41f9cdacf6700f Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 5 Aug 2024 10:27:49 +0200 Subject: [PATCH 126/235] intercept ex_unit DSL --- lib/elixir_sense/core/compiler.ex | 91 ++++++- lib/elixir_sense/core/metadata_builder.ex | 232 ------------------ lib/elixir_sense/core/state.ex | 6 +- .../core/metadata_builder_test.exs | 117 +++++---- 4 files changed, 140 insertions(+), 306 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 389de083..20973454 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -12,16 +12,16 @@ defmodule ElixirSense.Core.Compiler do def env, do: @env def expand(ast, state, env) do - try do - do_expand(ast, state, env) - catch - kind, payload -> - Logger.warning( - "Unable to expand ast node #{inspect(ast)}: #{Exception.format(kind, payload, __STACKTRACE__)}" - ) - - {ast, state, env} - end + # try do + do_expand(ast, state, env) + # catch + # kind, payload -> + # Logger.warning( + # "Unable to expand ast node #{inspect(ast)}: #{Exception.format(kind, payload, __STACKTRACE__)}" + # ) + + # {ast, state, env} + # end end # =/2 @@ -1728,6 +1728,69 @@ defmodule ElixirSense.Core.Compiler do {{name, arity}, state, env} end + defp expand_macro( + meta, + ExUnit.Case, + :test, + [name | rest], + callback, + state, + env = %{module: module} + ) + when module != nil and is_binary(name) do + {args, do_block} = + case rest do + [] -> {[{:_, [], nil}], [do: {:__block__, [], []}]} + [do_block] -> {[{:_, [], nil}], do_block} + [context, do_block | _] -> {[context], do_block} + end + + call = {ex_unit_test_name(state, name), meta, args} + expand_macro(meta, Kernel, :def, [call, do_block], callback, state, env) + end + + defp expand_macro( + meta, + ExUnit.Callbacks, + setup, + rest, + callback, + state, + env = %{module: module} + ) + when module != nil and setup in [:setup, :setup_all] do + {args, do_block} = + case rest do + [] -> {[{:_, [], nil}], [do: {:__block__, [], []}]} + [do_block] -> {[{:_, [], nil}], do_block} + [context, do_block | _] -> {[context], do_block} + end + + line = Keyword.fetch!(meta, :line) + + # NOTE this name is not 100% correct - ex_unit uses counters instead of line but it's too complicated + call = {:"__ex_unit_#{setup}_#{line}", meta, args} + # TODO add on expand_import/require + # |> add_call_to_line({nil, :test, 3}, {line, column}) + expand_macro(meta, Kernel, :def, [call, do_block], callback, state, env) + end + + defp expand_macro( + _meta, + ExUnit.Case, + :describe, + [name, [{:do, block}]], + _callback, + state, + env = %{module: module} + ) + when module != nil and is_binary(name) do + state = %{state | ex_unit_describe: name} + {ast, state, _env} = expand(block, state, env) + state = %{state | ex_unit_describe: nil} + {{:__block__, [], [ast]}, state, env} + end + defp expand_macro(meta, module, fun, args, callback, state, env) do expand_macro_callback(meta, module, fun, args, callback, state, env) end @@ -1806,6 +1869,14 @@ defmodule ElixirSense.Core.Compiler do end end + defp ex_unit_test_name(state, name) do + case state.ex_unit_describe do + nil -> "test #{name}" + describe -> "test #{describe} #{name}" + end + |> String.to_atom() + end + # defmodule helpers # defmodule automatically defines aliases, we need to mirror this feature here. diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index 2ea0038a..b65af884 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -30,121 +30,6 @@ defmodule ElixirSense.Core.MetadataBuilder do # # |> result(ast) # end - # # ex_unit describe - # defp pre( - # {:describe, meta, [name, _body]} = ast, - # state = %{scopes: [atom | _]} - # ) - # when is_binary(name) and is_atom(atom) and atom != nil do - # line = Keyword.fetch!(meta, :line) - # column = Keyword.fetch!(meta, :column) - - # state = - # state - # |> add_call_to_line({nil, :describe, 2}, {line, column}) - - # %{state | context: Map.put(state.context, :ex_unit_describe, name)} - # # |> add_current_env_to_line(line) - # # |> result(ast) - # end - - # # ex_unit not implemented test - # defp pre( - # {:test, meta, [name]}, - # state = %{scopes: [atom | _]} - # ) - # when is_binary(name) and is_atom(atom) and atom != nil do - # def_name = ex_unit_test_name(state, name) - # line = Keyword.fetch!(meta, :line) - # column = Keyword.fetch!(meta, :column) - - # _ast_without_params = {:def, meta, [{def_name, add_no_call([]), []}, [], []]} - - # _state = - # state - # |> add_call_to_line({nil, :test, 0}, {line, column}) - - # # pre_func(ast_without_params, state, meta, def_name, [{:_, [line: line, column: column], nil}]) - # end - - # # ex_unit test without context - # defp pre( - # {:test, meta, [name, body]}, - # state = %{scopes: [atom | _]} - # ) - # when is_binary(name) and is_atom(atom) and atom != nil do - # def_name = ex_unit_test_name(state, name) - # line = Keyword.fetch!(meta, :line) - # column = Keyword.fetch!(meta, :column) - - # _ast_without_params = {:def, meta, [{def_name, add_no_call([]), []}, [], body]} - - # _state = - # state - # |> add_call_to_line({nil, :test, 2}, {line, column}) - - # # pre_func(ast_without_params, state, meta, def_name, [{:_, [line: line, column: column], nil}]) - # end - - # # ex_unit test with context - # defp pre( - # {:test, meta, [name, _param, body]}, - # state = %{scopes: [atom | _]} - # ) - # when is_binary(name) and is_atom(atom) and atom != nil do - # def_name = ex_unit_test_name(state, name) - # line = Keyword.fetch!(meta, :line) - # column = Keyword.fetch!(meta, :column) - - # _ast_without_params = {:def, meta, [{def_name, add_no_call([]), []}, [], body]} - - # _state = - # state - # |> add_call_to_line({nil, :test, 3}, {line, column}) - - # # pre_func(ast_without_params, state, meta, def_name, [param]) - # end - - # # ex_unit setup with context - # defp pre( - # {setup, meta, [_param, body]}, - # state = %{scopes: [atom | _]} - # ) - # when setup in [:setup, :setup_all] and is_atom(atom) and atom != nil do - # line = Keyword.fetch!(meta, :line) - # column = Keyword.fetch!(meta, :column) - - # # NOTE this name is not 100% correct - ex_unit uses counters instead of line but it's too complicated - # def_name = :"__ex_unit_#{setup}_#{line}" - # _ast_without_params = {:def, meta, [{def_name, add_no_call([]), []}, [], body]} - - # _state = - # state - # |> add_call_to_line({nil, setup, 2}, {line, column}) - - # # pre_func(ast_without_params, state, meta, def_name, [param]) - # end - - # # ex_unit setup without context - # defp pre( - # {setup, meta, [body]}, - # state = %{scopes: [atom | _]} - # ) - # when setup in [:setup, :setup_all] and is_atom(atom) and atom != nil do - # line = Keyword.fetch!(meta, :line) - # column = Keyword.fetch!(meta, :column) - - # # NOTE this name is not 100% correct - ex_unit uses counters instead of line but it's too complicated - # def_name = :"__ex_unit_#{setup}_#{line}" - # _ast_without_params = {:def, meta, [{def_name, add_no_call([]), []}, [], body]} - - # state = - # state - # |> add_call_to_line({nil, setup, 2}, {line, column}) - - # # pre_func(ast_without_params, state, meta, def_name, [{:_, [line: line, column: column], nil}]) - # end - # # incomplete spec # # @callback my(integer) # defp pre( @@ -164,34 +49,6 @@ defmodule ElixirSense.Core.MetadataBuilder do # # ) # end - # defp pre({:when, _meta, [lhs, _rhs]}, state) do - # _vars = find_typed_vars(lhs, nil) - - # state - # # |> add_vars(vars, true) - # # |> result({:when, meta, [:_, rhs]}) - # end - - # defp pre( - # {:case, meta, - # [ - # condition_ast, - # [ - # do: _clauses - # ] - # ]} = _ast, - # state - # ) do - # line = Keyword.fetch!(meta, :line) - # column = Keyword.fetch!(meta, :column) - - # state - # |> push_binding_context(type_of(condition_ast)) - # |> add_call_to_line({nil, :case, 2}, {line, column}) - # # |> add_current_env_to_line(line) - # # |> result(ast) - # end - # # Any other tuple with a line # defp pre({_, meta, _} = ast, state) do # case Keyword.get(meta, :line) do @@ -205,52 +62,6 @@ defmodule ElixirSense.Core.MetadataBuilder do # end # end - # defp pre(ast, state) do - # {ast, state} - # end - - # # ex_unit describe - # defp post( - # {:describe, _meta, [name, _body]} = _ast, - # state - # ) - # when is_binary(name) do - # %{state | context: Map.delete(state.context, :ex_unit_describe)} - # # |> result(ast) - # end - - # defp post({atom, meta, [lhs, rhs]} = _ast, state) - # when atom in [:=, :<-] do - # _line = Keyword.fetch!(meta, :line) - # match_context_r = type_of(rhs) - - # match_context_r = - # if atom == :<- and match?([:for | _], state.binding_context) do - # {:for_expression, match_context_r} - # else - # match_context_r - # end - - # vars_l = find_typed_vars(lhs, match_context_r) - - # _vars = - # case rhs do - # {:=, _, [nested_lhs, _nested_rhs]} -> - # match_context_l = type_of(lhs) - # nested_vars = find_typed_vars(nested_lhs, match_context_l) - - # vars_l ++ nested_vars - - # _ -> - # vars_l - # end - - # state - # # |> remove_calls(remove_positions) - # # |> add_current_env_to_line(line) - # # |> result(ast) - # end - # # String literal # defp post({_, [no_call: true, line: line, column: _column], [str]} = ast, state) # when is_binary(str) do @@ -262,47 +73,4 @@ defmodule ElixirSense.Core.MetadataBuilder do # when is_binary(str) do # post_string_literal(ast, state, line, str) # end - - # defp post(ast, state) do - # {ast, state} - # end - - # # defp find_typed_vars(state, ast, match_context \\ nil) - - # # defp find_typed_vars(_state, {var, _meta, nil}, _) - # # when var in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__] do - # # # TODO local calls? - # # [] - # # end - - # # defp find_typed_vars(_state, {var, meta, nil}, :rescue) when is_atom(var) do - # # line = Keyword.fetch!(meta, :line) - # # column = Keyword.fetch!(meta, :column) - # # match_context = {:struct, [], {:atom, Exception}, nil} - # # [%VarInfo{name: var, positions: [{line, column}], type: match_context, is_definition: true}] - # # end - - # def infer_type_from_guards(guard_ast, vars, _state) do - # type_info = Guard.type_information_from_guards(guard_ast) - - # Enum.reduce(type_info, vars, fn {var, type}, acc -> - # index = Enum.find_index(acc, &(&1.name == var)) - - # if index, - # do: List.update_at(acc, index, &Map.put(&1, :type, type)), - # else: acc - # end) - # end - - # defp add_no_call(meta) do - # [{:no_call, true} | meta] - # end - - # defp ex_unit_test_name(state, name) do - # case state.context[:ex_unit_describe] do - # nil -> "test #{name}" - # describe -> "test #{describe} #{name}" - # end - # |> String.to_atom() - # end end diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 548334cc..09ef3419 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -65,7 +65,8 @@ defmodule ElixirSense.Core.State do typedoc_context: list(), optional_callbacks_context: list(), lines_to_env: lines_to_env_t, - cursor_env: nil | {keyword, ElixirSense.Core.State.Env.t()} + cursor_env: nil | {keyword, ElixirSense.Core.State.Env.t()}, + ex_unit_describe: nil | atom } defstruct attributes: [[]], @@ -96,7 +97,8 @@ defmodule ElixirSense.Core.State do typedoc_context: [[]], optional_callbacks_context: [[]], lines_to_env: %{}, - cursor_env: nil + cursor_env: nil, + ex_unit_describe: nil defmodule Env do @moduledoc """ diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 6750c07f..3170d9e0 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -6,8 +6,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do alias ElixirSense.Core.State alias ElixirSense.Core.State.{VarInfo, CallInfo, StructInfo, ModFunInfo, AttributeInfo} - @var_in_ex_unit false - describe "versioned_vars" do test "in block" do state = @@ -1168,84 +1166,79 @@ defmodule ElixirSense.Core.MetadataBuilderTest do end end - if @var_in_ex_unit do - describe "vars in ex_unit" do - test "variables are added to environment in ex_unit test" do - state = - """ - defmodule MyModuleTests do - use ExUnit.Case, async: true + describe "vars in ex_unit" do + test "variables are added to environment in ex_unit test" do + state = + """ + defmodule MyModuleTests do + use ExUnit.Case, async: true + IO.puts("") + test "it does what I want", %{some: some} do + IO.puts("") + end - test "it does what I want", %{some: some} do + describe "this" do + test "too does what I want" do IO.puts("") end - - describe "this" do - test "too does what I want" do - IO.puts("") - end - end - - test "is not implemented" end - """ - |> string_to_state - assert [%VarInfo{type: nil, scope_id: scope_id}] = state |> get_line_vars(5) - assert [%VarInfo{name: :some}] = state.vars_info_per_scope_id[scope_id] + test "is not implemented" + end + """ + |> string_to_state - assert Map.has_key?( - state.mods_funs_to_positions, - {MyModuleTests, :"test it does what I want", 1} - ) + assert [%VarInfo{type: nil, scope_id: scope_id}] = state |> get_line_vars(5) - assert Map.has_key?( - state.mods_funs_to_positions, - {MyModuleTests, :"test this too does what I want", 1} - ) + assert Map.has_key?( + state.mods_funs_to_positions, + {MyModuleTests, :"test it does what I want", 1} + ) - assert Map.has_key?( - state.mods_funs_to_positions, - {MyModuleTests, :"test is not implemented", 1} - ) - end + assert Map.has_key?( + state.mods_funs_to_positions, + {MyModuleTests, :"test this too does what I want", 1} + ) - test "variables are added to environment in ex_unit setup" do - state = - """ - defmodule MyModuleTests do - use ExUnit.Case, async: true + assert Map.has_key?( + state.mods_funs_to_positions, + {MyModuleTests, :"test is not implemented", 1} + ) + end - setup_all %{some: some} do - IO.puts("") - end + test "variables are added to environment in ex_unit setup" do + state = + """ + defmodule MyModuleTests do + use ExUnit.Case, async: true - setup %{some: other} do - IO.puts("") - end + setup_all %{some: some} do + IO.puts("") + end - setup do - IO.puts("") - end + setup %{some: other} do + IO.puts("") + end - setup :clean_up_tmp_directory + setup do + IO.puts("") + end - setup [:clean_up_tmp_directory, :another_setup] + setup :clean_up_tmp_directory - setup {MyModule, :my_setup_function} - end - """ - |> string_to_state + setup [:clean_up_tmp_directory, :another_setup] - assert [%VarInfo{type: nil, scope_id: scope_id}] = state |> get_line_vars(5) - assert [%VarInfo{name: :some}] = state.vars_info_per_scope_id[scope_id] + setup {MyModule, :my_setup_function} + end + """ + |> string_to_state - assert [%VarInfo{type: nil, scope_id: scope_id}] = state |> get_line_vars(9) - assert [%VarInfo{name: :other}] = state.vars_info_per_scope_id[scope_id] + assert [%VarInfo{type: nil, scope_id: scope_id}] = state |> get_line_vars(5) - # we do not generate defs - ExUnit.Callbacks.__setup__ is too complicated and generates def names with counters, e.g. - # :"__ex_unit_setup_#{counter}_#{length(setup)}" - end + assert [%VarInfo{type: nil, scope_id: scope_id}] = state |> get_line_vars(9) + + # we do not generate defs - ExUnit.Callbacks.__setup__ is too complicated and generates def names with counters, e.g. + # :"__ex_unit_setup_#{counter}_#{length(setup)}" end end From 99cd8c5c3992cecd417e7724bc08f50bd1de1432 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 6 Aug 2024 08:08:06 +0200 Subject: [PATCH 127/235] log failed expansions --- lib/elixir_sense/core/compiler.ex | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 20973454..e8c41d97 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -12,16 +12,16 @@ defmodule ElixirSense.Core.Compiler do def env, do: @env def expand(ast, state, env) do - # try do - do_expand(ast, state, env) - # catch - # kind, payload -> - # Logger.warning( - # "Unable to expand ast node #{inspect(ast)}: #{Exception.format(kind, payload, __STACKTRACE__)}" - # ) - - # {ast, state, env} - # end + try do + do_expand(ast, state, env) + catch + kind, payload -> + Logger.warning( + "Unable to expand ast node #{inspect(ast)}: #{Exception.format(kind, payload, __STACKTRACE__)}" + ) + + {ast, state, env} + end end # =/2 @@ -1807,10 +1807,12 @@ defmodule ElixirSense.Core.Compiler do try do callback.(meta, args) catch - # TODO raise? - # For language servers, if expanding the macro fails, we just give up. - _kind, _payload -> - # IO.inspect(payload, label: inspect(fun)) + # If expanding the macro fails, we just give up. + kind, payload -> + Logger.warning( + "Unable to expand macro #{inspect(module)}.#{fun}/#{length(args)}: #{Exception.format(kind, payload, __STACKTRACE__)}" + ) + {{{:., meta, [module, fun]}, meta, args}, state, env} else ast -> From 238c6eeed4a854d6cdeb960cdf51b7cc32025400 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 6 Aug 2024 08:09:29 +0200 Subject: [PATCH 128/235] add calls tracking on mocros --- lib/elixir_sense/core/compiler.ex | 59 +++++++++++++------------------ 1 file changed, 25 insertions(+), 34 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index e8c41d97..787b020f 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -670,20 +670,22 @@ defmodule ElixirSense.Core.Compiler do # If we are inside a function, we support reading from locals. allow_locals = match?({n, a} when fun != n or arity != a, env.function) - # TODO this crashes with CompileError ambiguous_call case NormalizedMacroEnv.expand_import(env, meta, fun, arity, trace: false, allow_locals: allow_locals, check_deprecations: false ) do {:macro, module, callback} -> - # TODO there is a subtle difference - callback will call expander with state derived from env via - # :elixir_env.env_to_ex(env) possibly losing some details - # line = Keyword.get(meta, :line, 0) - # column = Keyword.get(meta, :column, nil) - # state = state - # |> add_call_to_line({module, fun, length(args)}, {line, column}) - # |> add_current_env_to_line(line, env) + # NOTE there is a subtle difference - callback will call expander with state derived from env via + # :elixir_env.env_to_ex(env) possibly losing some details. Jose Valim is convinced this is not a problem + line = Keyword.get(meta, :line, 0) + column = Keyword.get(meta, :column, nil) + + state = + state + |> add_call_to_line({module, fun, length(args)}, {line, column}) + |> add_current_env_to_line(meta, env) + expand_macro(meta, module, fun, args, callback, state, env) {:function, module, fun} -> @@ -733,12 +735,19 @@ defmodule ElixirSense.Core.Compiler do check_deprecations: false ) do {:macro, module, callback} -> - # TODO there is a subtle difference - callback will call expander with state derived from env via - # :elixir_env.env_to_ex(env) possibly losing some details + # NOTE there is a subtle difference - callback will call expander with state derived from env via + # :elixir_env.env_to_ex(env) possibly losing some details. Jose Valim is convinced this is not a problem + line = Keyword.get(meta, :line, 0) + column = Keyword.get(meta, :column, nil) + + state = + state + |> add_call_to_line({module, fun, length(args)}, {line, column}) + |> add_current_env_to_line(meta, env) + expand_macro(meta, module, fun, args, callback, state, env) :error -> - # expand_remote(meta, module, fun, args, state, env) expand_remote(module, dot_meta, fun, meta, args, state, state_l, env) end end @@ -1796,13 +1805,6 @@ defmodule ElixirSense.Core.Compiler do end defp expand_macro_callback(meta, module, fun, args, callback, state, env) do - line = Keyword.get(meta, :line, 0) - column = Keyword.get(meta, :column) - - state = - state - |> add_call_to_line({module, fun, length(args)}, {line, column}) - # dbg({module, fun, args}) try do callback.(meta, args) @@ -1822,14 +1824,6 @@ defmodule ElixirSense.Core.Compiler do end defp expand_macro_callback!(meta, module, fun, args, callback, state, env) do - line = Keyword.get(meta, :line, 0) - column = Keyword.get(meta, :column) - - state = - state - |> add_call_to_line({module, fun, length(args)}, {line, column}) - - # dbg({module, fun, args}) ast = callback.(meta, args) {ast, state, env} = expand(ast, state, env) {ast, state, env} @@ -1914,17 +1908,14 @@ defmodule ElixirSense.Core.Compiler do line = Keyword.get(meta, :line, 0) column = Keyword.get(meta, :column, nil) - sl = - if line > 0 do - sl - |> add_current_env_to_line(line, e) - else - sl - end - if context == :guard and is_tuple(receiver) do # elixir raises parens_map_lookup unless no_parens is set in meta # TODO there may be cursor in discarded args + sl = + sl + |> add_call_to_line({receiver, right, length(args)}, {line, column}) + |> add_current_env_to_line(meta, e) + {{{:., dot_meta, [receiver, right]}, meta, []}, sl, e} else attached_meta = attach_runtime_module(receiver, meta, s, e) From 51dbf7c4370342e49fc01b12e333555d3ebfbd50 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 6 Aug 2024 08:09:48 +0200 Subject: [PATCH 129/235] unify code storing env --- lib/elixir_sense/core/compiler.ex | 98 +++++++++++++------------------ lib/elixir_sense/core/state.ex | 9 ++- 2 files changed, 48 insertions(+), 59 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 787b020f..4f907cf6 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -136,7 +136,7 @@ defmodule ElixirSense.Core.Compiler do state = state |> add_first_alias_positions(env, meta) - |> add_current_env_to_line(Keyword.fetch!(meta, :line), env) + |> add_current_env_to_line(meta, env) # no need to call expand_without_aliases_report - we never report {arg, state, env} = expand(arg, state, env) @@ -165,7 +165,7 @@ defmodule ElixirSense.Core.Compiler do state = state - |> add_current_env_to_line(Keyword.fetch!(meta, :line), env) + |> add_current_env_to_line(meta, env) # no need to call expand_without_aliases_report - we never report {arg, state, env} = expand(arg, state, env) @@ -225,7 +225,7 @@ defmodule ElixirSense.Core.Compiler do defp do_expand({:import, meta, [arg, opts]}, state, env) do state = state - |> add_current_env_to_line(Keyword.fetch!(meta, :line), env) + |> add_current_env_to_line(meta, env) # no need to call expand_without_aliases_report - we never report {arg, state, env} = expand(arg, state, env) @@ -256,48 +256,42 @@ defmodule ElixirSense.Core.Compiler do # Compilation environment macros defp do_expand({:__MODULE__, meta, ctx}, state, env) when is_atom(ctx) do - line = Keyword.get(meta, :line, 0) - state = if line > 0, do: add_current_env_to_line(state, line, env), else: state + state = add_current_env_to_line(state, meta, env) {env.module, state, env} end defp do_expand({:__DIR__, meta, ctx}, state, env) when is_atom(ctx) do - line = Keyword.get(meta, :line, 0) - state = if line > 0, do: add_current_env_to_line(state, line, env), else: state + state = add_current_env_to_line(state, meta, env) {Path.dirname(env.file), state, env} end - defp do_expand({:__CALLER__, meta, ctx} = caller, s, e) when is_atom(ctx) do + defp do_expand({:__CALLER__, meta, ctx} = caller, state, env) when is_atom(ctx) do # elixir checks if context is not match and if caller is allowed - line = Keyword.get(meta, :line, 0) - s = if line > 0, do: add_current_env_to_line(s, line, e), else: s + state = add_current_env_to_line(state, meta, env) - {caller, s, e} + {caller, state, env} end - defp do_expand({:__STACKTRACE__, meta, ctx} = stacktrace, s, e) when is_atom(ctx) do + defp do_expand({:__STACKTRACE__, meta, ctx} = stacktrace, state, env) when is_atom(ctx) do # elixir checks if context is not match and if stacktrace is allowed - line = Keyword.get(meta, :line, 0) - s = if line > 0, do: add_current_env_to_line(s, line, e), else: s + state = add_current_env_to_line(state, meta, env) - {stacktrace, s, e} + {stacktrace, state, env} end - defp do_expand({:__ENV__, meta, ctx}, s, e) when is_atom(ctx) do + defp do_expand({:__ENV__, meta, ctx}, state, env) when is_atom(ctx) do # elixir checks if context is not match - line = Keyword.get(meta, :line, 0) - s = if line > 0, do: add_current_env_to_line(s, line, e), else: s + state = add_current_env_to_line(state, meta, env) - {escape_map(escape_env_entries(meta, s, e)), s, e} + {escape_map(escape_env_entries(meta, state, env)), state, env} end defp do_expand({{:., dot_meta, [{:__ENV__, meta, atom}, field]}, call_meta, []}, s, e) when is_atom(atom) and is_atom(field) do # elixir checks if context is not match - line = Keyword.get(call_meta, :line, 0) - s = if line > 0, do: add_current_env_to_line(s, line, e), else: s + s = add_current_env_to_line(s, call_meta, e) env = escape_env_entries(meta, s, e) @@ -308,7 +302,7 @@ defmodule ElixirSense.Core.Compiler do end # Quote - + # TODO add_current_line_to_env defp do_expand({unquote_call, meta, [arg]}, s, e) when unquote_call in [:unquote, :unquote_splicing] do # elixir raises here unquote_outside_quote @@ -438,7 +432,7 @@ defmodule ElixirSense.Core.Compiler do s = s |> add_call_to_line({nil, name, arity}, {line, column}) - |> add_current_env_to_line(line, e) + |> add_current_env_to_line(super_meta, e) {{:&, meta, [{:/, arity_meta, [{name, super_meta, context}, arity]}]}, s, e} @@ -512,7 +506,7 @@ defmodule ElixirSense.Core.Compiler do sa = sa |> add_call_to_line({nil, name, arity}, {line, column}) - |> add_current_env_to_line(line, ea) + |> add_current_env_to_line(meta, ea) {{:super, [{:super, {kind, name}} | meta], e_args}, sa, ea} @@ -778,7 +772,7 @@ defmodule ElixirSense.Core.Compiler do sa = sa |> add_call_to_line({nil, e_expr, length(e_args)}, {line, column}) - |> add_current_env_to_line(line, e) + |> add_current_env_to_line(meta, e) {{{:., dot_meta, [e_expr]}, meta, e_args}, sa, ea} end @@ -879,7 +873,7 @@ defmodule ElixirSense.Core.Compiler do arity = length(args) state - |> add_current_env_to_line(line, %{env | context: nil, function: {name, arity}}) + |> add_current_env_to_line(meta, %{env | context: nil, function: {name, arity}}) |> add_func_to_index( env, name, @@ -904,11 +898,9 @@ defmodule ElixirSense.Core.Compiler do env = %{module: module} ) when module != nil do - line = Keyword.fetch!(meta, :line) - state = state - |> add_current_env_to_line(line, env) + |> add_current_env_to_line(meta, env) {arg, state, env} = expand(arg, state, env) add_behaviour(arg, state, env) @@ -924,11 +916,9 @@ defmodule ElixirSense.Core.Compiler do env = %{module: module} ) when module != nil do - line = Keyword.fetch!(meta, :line) - state = state - |> add_current_env_to_line(line, env) + |> add_current_env_to_line(meta, env) {arg, state, env} = expand(arg, state, env) @@ -953,11 +943,9 @@ defmodule ElixirSense.Core.Compiler do env = %{module: module} ) when doc in [:doc, :typedoc] and module != nil do - line = Keyword.fetch!(meta, :line) - state = state - |> add_current_env_to_line(line, env) + |> add_current_env_to_line(meta, env) {arg, state, env} = expand(arg, state, env) @@ -978,11 +966,9 @@ defmodule ElixirSense.Core.Compiler do env = %{module: module} ) when module != nil do - line = Keyword.fetch!(meta, :line) - state = state - |> add_current_env_to_line(line, env) + |> add_current_env_to_line(meta, env) {arg, state, env} = expand(arg, state, env) @@ -1004,11 +990,9 @@ defmodule ElixirSense.Core.Compiler do env = %{module: module} ) when module != nil do - line = Keyword.fetch!(meta, :line) - state = state - |> add_current_env_to_line(line, env) + |> add_current_env_to_line(meta, env) {arg, state, env} = expand(arg, state, env) @@ -1029,11 +1013,9 @@ defmodule ElixirSense.Core.Compiler do env = %{module: module} ) when module != nil do - line = Keyword.fetch!(meta, :line) - state = state - |> add_current_env_to_line(line, env) + |> add_current_env_to_line(meta, env) {arg, state, env} = expand(arg, state, env) @@ -1132,7 +1114,7 @@ defmodule ElixirSense.Core.Compiler do state |> add_type(env, name, type_args, spec, kind, position, end_position) |> with_typespec({name, length(type_args)}) - |> add_current_env_to_line(line, env) + |> add_current_env_to_line(attr_meta, env) |> with_typespec(nil) {{:@, attr_meta, [{kind, kind_meta, [expr]}]}, state, env} @@ -1193,7 +1175,7 @@ defmodule ElixirSense.Core.Compiler do state |> add_spec(env, name, type_args, spec, kind, position, end_position) |> with_typespec({name, length(type_args)}) - |> add_current_env_to_line(line, env) + |> add_current_env_to_line(attr_meta, env) |> with_typespec(nil) {{:@, attr_meta, [{kind, kind_meta, [expr]}]}, state, env} @@ -1245,7 +1227,7 @@ defmodule ElixirSense.Core.Compiler do state = state |> add_attribute(name, inferred_type, is_definition, {line, column}) - |> add_current_env_to_line(line, env) + |> add_current_env_to_line(meta, env) {{:@, meta, [{name, name_meta, e_args}]}, state, env} end @@ -1371,7 +1353,7 @@ defmodule ElixirSense.Core.Compiler do options ) |> add_call_to_line({module, call, length(args)}, {line, column}) - |> add_current_env_to_line(line, env) + |> add_current_env_to_line(meta, env) {{{:., meta, [Record, call]}, meta, args}, state, env} end @@ -1554,7 +1536,7 @@ defmodule ElixirSense.Core.Compiler do state |> add_module_to_index(full, position, end_position, []) |> add_module - |> add_current_env_to_line(line, %{env | module: full}) + |> add_current_env_to_line(meta, %{env | module: full}) |> add_module_functions(%{env | module: full}, module_functions, position, end_position) |> new_vars_scope |> new_attributes_scope @@ -1702,7 +1684,7 @@ defmodule ElixirSense.Core.Compiler do state = state - |> add_current_env_to_line(line, env_for_expand) + |> add_current_env_to_line(meta, env_for_expand) |> add_func_to_index( env, name, @@ -1934,7 +1916,7 @@ defmodule ElixirSense.Core.Compiler do s = __MODULE__.Env.close_write(sa, s) |> add_call_to_line({receiver, right, length(e_args)}, {line, column}) - |> add_current_env_to_line(line, e) + |> add_current_env_to_line(meta, e) {rewritten, s, ea} @@ -1943,7 +1925,7 @@ defmodule ElixirSense.Core.Compiler do s = __MODULE__.Env.close_write(sa, s) |> add_call_to_line({receiver, right, length(e_args)}, {line, column}) - |> add_current_env_to_line(line, e) + |> add_current_env_to_line(meta, e) {{{:., dot_meta, [receiver, right]}, attached_meta, e_args}, s, ea} end @@ -1960,7 +1942,7 @@ defmodule ElixirSense.Core.Compiler do s = __MODULE__.Env.close_write(sa, s) |> add_call_to_line({receiver, right, length(e_args)}, {line, column}) - |> add_current_env_to_line(line, e) + |> add_current_env_to_line(meta, e) {{{:., dot_meta, [receiver, right]}, meta, e_args}, s, ea} end @@ -2005,7 +1987,7 @@ defmodule ElixirSense.Core.Compiler do state = state |> add_call_to_line({nil, fun, length(args)}, {line, column}) - |> add_current_env_to_line(line, env) + |> add_current_env_to_line(meta, env) {args, state, env} = expand_args(args, state, env) {{fun, meta, args}, state, env} @@ -2049,7 +2031,7 @@ defmodule ElixirSense.Core.Compiler do defp expand_block([], acc, _meta, s, e), do: {Enum.reverse(acc), s, e} defp expand_block([h], acc, meta, s, e) do - # s = s |> add_current_env_to_line(Keyword.fetch!(meta, :line), e) + # s = s |> add_current_env_to_line(meta, e) {eh, se, ee} = expand(h, s, e) expand_block([], [eh | acc], meta, se, ee) end @@ -2067,7 +2049,7 @@ defmodule ElixirSense.Core.Compiler do end defp expand_block([h | t], acc, meta, s, e) do - # s = s |> add_current_env_to_line(Keyword.fetch!(meta, :line), e) + # s = s |> add_current_env_to_line(meta, e) {eh, se, ee} = expand(h, s, e) expand_block(t, [eh | acc], meta, se, ee) end @@ -2176,7 +2158,7 @@ defmodule ElixirSense.Core.Compiler do se = se |> add_call_to_line({remote, fun, arity}, {line, column}) - |> add_current_env_to_line(line, ee) + |> add_current_env_to_line(attached_meta, ee) {{:&, meta, [{:/, [], [{{:., dot_meta, [remote, fun]}, attached_meta, []}, arity]}]}, se, ee} @@ -2189,7 +2171,7 @@ defmodule ElixirSense.Core.Compiler do se = se |> add_call_to_line({nil, fun, arity}, {line, column}) - |> add_current_env_to_line(line, ee) + |> add_current_env_to_line(local_meta, ee) {{:&, meta, [{:/, [], [{fun, local_meta, nil}, arity]}]}, se, ee} diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 09ef3419..763a47e1 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -360,11 +360,18 @@ defmodule ElixirSense.Core.State do %__MODULE__{state | cursor_env: {meta, env}} end - def add_current_env_to_line(%__MODULE__{} = state, line, macro_env) when is_integer(line) do + def add_current_env_to_line(%__MODULE__{} = state, meta, macro_env) when is_list(meta) do + do_add_current_env_to_line(state, Keyword.get(meta, :line, 0), macro_env) + end + + defp do_add_current_env_to_line(%__MODULE__{} = state, line, macro_env) + when is_integer(line) and line > 0 do env = get_current_env(state, macro_env) %__MODULE__{state | lines_to_env: Map.put(state.lines_to_env, line, env)} end + defp do_add_current_env_to_line(%__MODULE__{} = state, _line, _macro_env), do: state + def add_moduledoc_positions( %__MODULE__{} = state, env, From 04c890efdb9d897cfb52831e1b66a666541330bd Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 8 Aug 2024 07:28:47 +0200 Subject: [PATCH 130/235] track macro calls reliably pass meta on calls --- lib/elixir_sense/core/compiler.ex | 97 +++------ lib/elixir_sense/core/state.ex | 40 ++-- .../core/metadata_builder_test.exs | 198 +++++++++++------- 3 files changed, 173 insertions(+), 162 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 4f907cf6..b0378296 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -302,12 +302,13 @@ defmodule ElixirSense.Core.Compiler do end # Quote - # TODO add_current_line_to_env + defp do_expand({unquote_call, meta, [arg]}, s, e) when unquote_call in [:unquote, :unquote_splicing] do # elixir raises here unquote_outside_quote # we may have cursor there {arg, s, e} = expand(arg, s, e) + s = s |> add_current_env_to_line(meta, e) {{unquote_call, meta, [arg]}, s, e} end @@ -327,6 +328,7 @@ defmodule ElixirSense.Core.Compiler do # elixir raises here invalid_args # we may have cursor there {arg, s, e} = expand(arg, s, e) + s = s |> add_current_env_to_line(meta, e) {{:quote, meta, [arg]}, s, e} end @@ -383,6 +385,8 @@ defmodule ElixirSense.Core.Compiler do quoted = __MODULE__.Quote.quote(exprs, q) {e_quoted, es, eq} = expand(quoted, sc, ec) + es = es |> add_current_env_to_line(meta, eq) + e_binding = for {k, v} <- binding do {:{}, [], [:=, [], [{:{}, [], [k, meta, e_context]}, v]]} @@ -426,12 +430,9 @@ defmodule ElixirSense.Core.Compiler do when is_atom(context) and is_integer(arity) do case resolve_super(meta, arity, s, e) do {kind, name, _} when kind in [:def, :defp] -> - line = Keyword.get(super_meta, :line, 0) - column = Keyword.get(super_meta, :column, nil) - s = s - |> add_call_to_line({nil, name, arity}, {line, column}) + |> add_call_to_line({nil, name, arity}, super_meta) |> add_current_env_to_line(super_meta, e) {{:&, meta, [{:/, arity_meta, [{name, super_meta, context}, arity]}]}, s, e} @@ -500,12 +501,9 @@ defmodule ElixirSense.Core.Compiler do {kind, name, _} -> {e_args, sa, ea} = expand_args(args, s, e) - line = Keyword.get(meta, :line, 0) - column = Keyword.get(meta, :column, nil) - sa = sa - |> add_call_to_line({nil, name, arity}, {line, column}) + |> add_call_to_line({nil, name, arity}, meta) |> add_current_env_to_line(meta, ea) {{:super, [{:super, {kind, name}} | meta], e_args}, sa, ea} @@ -672,12 +670,9 @@ defmodule ElixirSense.Core.Compiler do {:macro, module, callback} -> # NOTE there is a subtle difference - callback will call expander with state derived from env via # :elixir_env.env_to_ex(env) possibly losing some details. Jose Valim is convinced this is not a problem - line = Keyword.get(meta, :line, 0) - column = Keyword.get(meta, :column, nil) - state = state - |> add_call_to_line({module, fun, length(args)}, {line, column}) + |> add_call_to_line({module, fun, length(args)}, meta) |> add_current_env_to_line(meta, env) expand_macro(meta, module, fun, args, callback, state, env) @@ -731,12 +726,9 @@ defmodule ElixirSense.Core.Compiler do {:macro, module, callback} -> # NOTE there is a subtle difference - callback will call expander with state derived from env via # :elixir_env.env_to_ex(env) possibly losing some details. Jose Valim is convinced this is not a problem - line = Keyword.get(meta, :line, 0) - column = Keyword.get(meta, :column, nil) - state = state - |> add_call_to_line({module, fun, length(args)}, {line, column}) + |> add_call_to_line({module, fun, length(args)}, meta) |> add_current_env_to_line(meta, env) expand_macro(meta, module, fun, args, callback, state, env) @@ -757,21 +749,13 @@ defmodule ElixirSense.Core.Compiler do # elixir validates if e_expr is not atom and raises invalid_function_call - line = Keyword.get(dot_meta, :line, 0) - column = Keyword.get(dot_meta, :column, nil) - - column = - if column do - # for remote calls we emit position of right side of . - # to make it consistent we shift dot position here - column + 1 - else - column - end + # for remote calls we emit position of right side of . + # to make it consistent we shift dot position here + dot_meta = dot_meta |> Keyword.put(:column_correction, 1) sa = sa - |> add_call_to_line({nil, e_expr, length(e_args)}, {line, column}) + |> add_call_to_line({nil, e_expr, length(e_args)}, dot_meta) |> add_current_env_to_line(meta, e) {{{:., dot_meta, [e_expr]}, meta, e_args}, sa, ea} @@ -850,7 +834,6 @@ defmodule ElixirSense.Core.Compiler do ) when module != nil do {position, end_position} = extract_range(meta) - {line, _} = position {opts, state, env} = expand(opts, state, env) # elixir does validation here @@ -1108,7 +1091,7 @@ defmodule ElixirSense.Core.Compiler do spec = TypeInfo.typespec_to_string(kind, expr) - {position = {line, _column}, end_position} = extract_range(attr_meta) + {position, end_position} = extract_range(attr_meta) state = state @@ -1153,7 +1136,7 @@ defmodule ElixirSense.Core.Compiler do {name, type_args} -> spec = TypeInfo.typespec_to_string(kind, expr) - {position = {line, _column}, end_position} = extract_range(attr_meta) + {position, end_position} = extract_range(attr_meta) state = if kind in [:callback, :macrocallback] do @@ -1322,7 +1305,7 @@ defmodule ElixirSense.Core.Compiler do env = %{module: module} ) when call in [:defrecord, :defrecordp] and module != nil do - {position = {line, column}, end_position} = extract_range(meta) + {position, end_position} = extract_range(meta) type = case call do @@ -1352,7 +1335,6 @@ defmodule ElixirSense.Core.Compiler do type, options ) - |> add_call_to_line({module, call, length(args)}, {line, column}) |> add_current_env_to_line(meta, env) {{{:., meta, [Record, call]}, meta, args}, state, env} @@ -1524,8 +1506,6 @@ defmodule ElixirSense.Core.Compiler do {position, end_position} = extract_range(meta) - line = Keyword.fetch!(meta, :line) - module_functions = case state.protocol do nil -> [] @@ -1616,8 +1596,6 @@ defmodule ElixirSense.Core.Compiler do def_kind in [:def, :defp, :defmacro, :defmacrop, :defguard, :defguardp] do %{vars: vars, unused: unused} = state - line = Keyword.fetch!(meta, :line) - unquoted_call = __MODULE__.Quote.has_unquotes(call) unquoted_expr = __MODULE__.Quote.has_unquotes(expr) has_unquotes = unquoted_call or unquoted_expr @@ -1761,8 +1739,6 @@ defmodule ElixirSense.Core.Compiler do # NOTE this name is not 100% correct - ex_unit uses counters instead of line but it's too complicated call = {:"__ex_unit_#{setup}_#{line}", meta, args} - # TODO add on expand_import/require - # |> add_call_to_line({nil, :test, 3}, {line, column}) expand_macro(meta, Kernel, :def, [call, do_block], callback, state, env) end @@ -1805,7 +1781,7 @@ defmodule ElixirSense.Core.Compiler do end end - defp expand_macro_callback!(meta, module, fun, args, callback, state, env) do + defp expand_macro_callback!(meta, _module, _fun, args, callback, state, env) do ast = callback.(meta, args) {ast, state, env} = expand(ast, state, env) {ast, state, env} @@ -1814,7 +1790,7 @@ defmodule ElixirSense.Core.Compiler do defp extract_range(meta) do line = Keyword.get(meta, :line, 0) - if line == 0 do + if line <= 0 do {nil, nil} else position = { @@ -1887,15 +1863,12 @@ defmodule ElixirSense.Core.Compiler do defp expand_remote(receiver, dot_meta, right, meta, args, s, sl, %{context: context} = e) when is_atom(receiver) or is_tuple(receiver) do - line = Keyword.get(meta, :line, 0) - column = Keyword.get(meta, :column, nil) - if context == :guard and is_tuple(receiver) do # elixir raises parens_map_lookup unless no_parens is set in meta # TODO there may be cursor in discarded args sl = sl - |> add_call_to_line({receiver, right, length(args)}, {line, column}) + |> add_call_to_line({receiver, right, length(args)}, meta) |> add_current_env_to_line(meta, e) {{{:., dot_meta, [receiver, right]}, meta, []}, sl, e} @@ -1915,7 +1888,7 @@ defmodule ElixirSense.Core.Compiler do {:ok, rewritten} -> s = __MODULE__.Env.close_write(sa, s) - |> add_call_to_line({receiver, right, length(e_args)}, {line, column}) + |> add_call_to_line({receiver, right, length(e_args)}, meta) |> add_current_env_to_line(meta, e) {rewritten, s, ea} @@ -1924,7 +1897,7 @@ defmodule ElixirSense.Core.Compiler do # elixir raises here elixir_rewrite s = __MODULE__.Env.close_write(sa, s) - |> add_call_to_line({receiver, right, length(e_args)}, {line, column}) + |> add_call_to_line({receiver, right, length(e_args)}, meta) |> add_current_env_to_line(meta, e) {{{:., dot_meta, [receiver, right]}, attached_meta, e_args}, s, ea} @@ -1936,12 +1909,9 @@ defmodule ElixirSense.Core.Compiler do # elixir raises here invalid_call {e_args, {sa, _}, ea} = map_fold(&expand_arg/3, {sl, s}, e, args) - line = Keyword.get(meta, :line, 0) - column = Keyword.get(meta, :column, nil) - s = __MODULE__.Env.close_write(sa, s) - |> add_call_to_line({receiver, right, length(e_args)}, {line, column}) + |> add_call_to_line({receiver, right, length(e_args)}, meta) |> add_current_env_to_line(meta, e) {{{:., dot_meta, [receiver, right]}, meta, e_args}, s, ea} @@ -1981,12 +1951,10 @@ defmodule ElixirSense.Core.Compiler do # elixir check if there are no clauses # elixir raises here invalid_local_invocation if context is match or guard # elixir compiler raises here undefined_function if env.function is nil - line = Keyword.get(meta, :line, 0) - column = Keyword.get(meta, :column, nil) state = state - |> add_call_to_line({nil, fun, length(args)}, {line, column}) + |> add_call_to_line({nil, fun, length(args)}, meta) |> add_current_env_to_line(meta, env) {args, state, env} = expand_args(args, state, env) @@ -2152,12 +2120,9 @@ defmodule ElixirSense.Core.Compiler do {{:remote, remote, fun, arity}, require_meta, dot_meta, se, ee} -> attached_meta = attach_runtime_module(remote, require_meta, s, e) - line = Keyword.get(attached_meta, :line, 0) - column = Keyword.get(attached_meta, :column, nil) - se = se - |> add_call_to_line({remote, fun, arity}, {line, column}) + |> add_call_to_line({remote, fun, arity}, attached_meta) |> add_current_env_to_line(attached_meta, ee) {{:&, meta, [{:/, [], [{{:., dot_meta, [remote, fun]}, attached_meta, []}, arity]}]}, se, @@ -2165,12 +2130,10 @@ defmodule ElixirSense.Core.Compiler do {{:local, fun, arity}, local_meta, _, se, ee} -> # elixir raises undefined_local_capture if ee.function is nil - line = Keyword.get(local_meta, :line, 0) - column = Keyword.get(local_meta, :column, nil) se = se - |> add_call_to_line({nil, fun, arity}, {line, column}) + |> add_call_to_line({nil, fun, arity}, local_meta) |> add_current_env_to_line(local_meta, ee) {{:&, meta, [{:/, [], [{fun, local_meta, nil}, arity]}]}, se, ee} @@ -4915,9 +4878,6 @@ defmodule ElixirSense.Core.Compiler do end, fn {{:., dot_meta, [remote, name]}, meta, args}, {state, env} when is_atom(remote) -> - line = Keyword.get(meta, :line, 0) - column = Keyword.get(meta, :column, nil) - args = if is_atom(args) do [] @@ -4925,16 +4885,13 @@ defmodule ElixirSense.Core.Compiler do args end - state = add_call_to_line(state, {remote, name, length(args)}, {line, column}) + state = add_call_to_line(state, {remote, name, length(args)}, meta) {{{:., dot_meta, [remote, name]}, meta, args}, {state, env}} {name, meta, args}, {state, env} when is_atom(name) and is_list(args) and name not in @special_forms -> - line = Keyword.get(meta, :line, 0) - column = Keyword.get(meta, :column, nil) - - state = add_call_to_line(state, {nil, name, length(args)}, {line, column}) + state = add_call_to_line(state, {nil, name, length(args)}, meta) {{name, meta, args}, {state, env}} diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 763a47e1..48657c6b 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -420,55 +420,63 @@ defmodule ElixirSense.Core.State do def add_call_to_line( %__MODULE__{} = state, {{:@, _meta, [{name, _name_meta, nil}]}, func, arity}, - {_line, _column} = position + meta ) when is_atom(name) do - do_add_call_to_line(state, {{:attribute, name}, func, arity}, position) + do_add_call_to_line(state, {{:attribute, name}, func, arity}, meta) end def add_call_to_line( %__MODULE__{} = state, - {{name, meta, args}, func, arity}, - {_line, _column} = position + {{name, var_meta, args}, func, arity}, + meta ) when is_atom(name) and is_atom(args) and name not in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__, :_] do do_add_call_to_line( state, - {{:variable, name, Keyword.get(meta, :version, :any)}, func, arity}, - position + {{:variable, name, Keyword.get(var_meta, :version, :any)}, func, arity}, + meta ) end def add_call_to_line( %__MODULE__{} = state, {nil, {:@, _meta, [{name, _name_meta, _args}]}, arity}, - {_line, _column} = position + meta ) when is_atom(name) do - do_add_call_to_line(state, {nil, {:attribute, name}, arity}, position) + do_add_call_to_line(state, {nil, {:attribute, name}, arity}, meta) end def add_call_to_line( %__MODULE__{} = state, - {nil, {name, meta, args}, arity}, - {_line, _column} = position + {nil, {name, var_meta, args}, arity}, + meta ) when is_atom(name) and is_atom(args) and name not in [:__MODULE__, :__DIR__, :__ENV__, :__CALLER__, :__STACKTRACE__, :_] do do_add_call_to_line( state, - {nil, {:variable, name, Keyword.get(meta, :version, :any)}, arity}, - position + {nil, {:variable, name, Keyword.get(var_meta, :version, :any)}, arity}, + meta ) end - def add_call_to_line(state, call, position) do - do_add_call_to_line(state, call, position) + def add_call_to_line(state, call, meta) do + do_add_call_to_line(state, call, meta) end - defp do_add_call_to_line(%__MODULE__{} = state, {mod, func, arity}, {line, _column} = position) do - call = %CallInfo{mod: mod, func: func, arity: arity, position: position} + defp do_add_call_to_line(%__MODULE__{} = state, {mod, func, arity}, meta) do + line = Keyword.get(meta, :line, 0) + column = Keyword.get(meta, :column, nil) + + column = + if column do + column + Keyword.get(meta, :column_correction, 0) + end + + call = %CallInfo{mod: mod, func: func, arity: arity, position: {line, column}} calls = Map.update(state.calls, line, [call], fn line_calls -> diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 3170d9e0..0c9ef3c1 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -1188,7 +1188,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert [%VarInfo{type: nil, scope_id: scope_id}] = state |> get_line_vars(5) + assert [%VarInfo{name: :some}] = state |> get_line_vars(5) assert Map.has_key?( state.mods_funs_to_positions, @@ -1233,9 +1233,9 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert [%VarInfo{type: nil, scope_id: scope_id}] = state |> get_line_vars(5) + assert [%VarInfo{name: :some}] = state |> get_line_vars(5) - assert [%VarInfo{type: nil, scope_id: scope_id}] = state |> get_line_vars(9) + assert [%VarInfo{name: :other}] = state |> get_line_vars(9) # we do not generate defs - ExUnit.Callbacks.__setup__ is too complicated and generates def names with counters, e.g. # :"__ex_unit_setup_#{counter}_#{length(setup)}" @@ -6384,12 +6384,12 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ + assert %{ 5 => [%CallInfo{arity: 0, func: :func1, position: {5, 16}, mod: NyModule}], 6 => [%CallInfo{arity: 0, func: :func1, position: {6, 16}, mod: NyModule}], 7 => [%CallInfo{arity: 1, func: :func2, position: {7, 16}, mod: NyModule}], 8 => [%CallInfo{arity: 1, func: :func2, position: {8, 20}, mod: NyModule.Sub}] - } + } = state.calls end test "registers calls with erlang module" do @@ -6405,11 +6405,11 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ + assert %{ 3 => [%CallInfo{arity: 0, func: :func1, position: {3, 14}, mod: :erl_mod}], 4 => [%CallInfo{arity: 0, func: :func1, position: {4, 14}, mod: :erl_mod}], 5 => [%CallInfo{arity: 1, func: :func2, position: {5, 14}, mod: :erl_mod}] - } + } = state.calls end test "registers calls with atom module" do @@ -6425,11 +6425,11 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ + assert %{ 3 => [%CallInfo{arity: 0, func: :func1, position: {3, 21}, mod: MyMod}], 4 => [%CallInfo{arity: 0, func: :func1, position: {4, 21}, mod: MyMod}], 5 => [%CallInfo{arity: 1, func: :func2, position: {5, 21}, mod: MyMod}] - } + } = state.calls end test "registers calls no arg no parens" do @@ -6443,9 +6443,9 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ + assert %{ 3 => [%CallInfo{arity: 0, func: :func, position: {3, 11}, mod: MyMod}] - } + } = state.calls end test "registers calls no arg" do @@ -6459,9 +6459,9 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ + assert %{ 3 => [%CallInfo{arity: 0, func: :func, position: {3, 11}, mod: MyMod}] - } + } = state.calls end test "registers calls local no arg no parens" do @@ -6477,14 +6477,45 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state if Version.match?(System.version(), ">= 1.15.0") do - assert state.calls == %{} + assert state.calls + |> Enum.flat_map(fn {_line, info} -> info end) + |> Enum.filter(fn info -> info.mod != Kernel end) == [] else - assert state.calls == %{ + assert %{ 4 => [%CallInfo{arity: 0, func: :func_1, position: {4, 5}, mod: nil}] - } + } = state.calls end end + test "registers macro calls" do + state = + """ + defmodule NyModule do + @foo "123" + require Record + Record.defrecord(:user, name: "meg", age: "25") + def func do + IO.inspect(binding()) + :ok + end + end + """ + |> string_to_state + + assert %{ + 1 => [%CallInfo{arity: 2, position: {1, 1}, func: :defmodule, mod: Kernel}], + 2 => [%CallInfo{arity: 1, position: {2, 3}, func: :@, mod: Kernel}], + 4 => [%CallInfo{arity: 2, position: {4, 10}, func: :defrecord, mod: Record}], + 5 => [%CallInfo{arity: 2, position: {5, 3}, func: :def, mod: Kernel}], + 6 => [ + %CallInfo{arity: 1, position: {6, 8}, func: :inspect, mod: IO}, + %CallInfo{arity: 0, position: {6, 16}, func: :binding, mod: Kernel} + ] + } == state.calls + end + + # TODO track Kernel.SpecialForms calls? + test "registers typespec no parens calls" do state = """ @@ -6494,11 +6525,12 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ + assert %{ 2 => [ - %CallInfo{arity: 0, func: :integer, position: {2, 14}, mod: nil} + %CallInfo{arity: 0, func: :integer, position: {2, 14}, mod: nil}, + _ ] - } + } = state.calls end test "registers typespec parens calls" do @@ -6510,11 +6542,12 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ + assert %{ 2 => [ - %CallInfo{arity: 0, func: :integer, position: {2, 16}, mod: nil} + %CallInfo{arity: 0, func: :integer, position: {2, 16}, mod: nil}, + _ ] - } + } = state.calls end test "registers typespec no parens remote calls" do @@ -6526,11 +6559,12 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ + assert %{ 2 => [ - %CallInfo{arity: 0, func: :t, position: {2, 19}, mod: Enum} + %CallInfo{arity: 0, func: :t, position: {2, 19}, mod: Enum}, + _ ] - } + } = state.calls end test "registers typespec parens remote calls" do @@ -6543,14 +6577,16 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ + assert %{ 2 => [ - %CallInfo{arity: 0, func: :t, position: {2, 21}, mod: Enum} + %CallInfo{arity: 0, func: :t, position: {2, 21}, mod: Enum}, + _ ], 3 => [ - %CallInfo{arity: 0, func: :t, position: {3, 23}, mod: Enum} + %CallInfo{arity: 0, func: :t, position: {3, 23}, mod: Enum}, + _ ] - } + } = state.calls end test "registers typespec calls in specs with when guard" do @@ -6563,13 +6599,14 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state # NOTE var is not a type but a special variable - assert state.calls == %{ + assert %{ 2 => [ %CallInfo{arity: 0, func: :pos_integer, position: {2, 71}, mod: nil}, %CallInfo{arity: 0, func: :map, position: {2, 53}, mod: nil}, - %CallInfo{arity: 0, func: :integer, position: {2, 31}, mod: nil} + %CallInfo{arity: 0, func: :integer, position: {2, 31}, mod: nil}, + _ ] - } + } = state.calls end test "registers typespec calls in typespec with named args" do @@ -6582,19 +6619,19 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ + assert %{ 2 => [ %CallInfo{arity: 0, func: :integer, position: {2, 84}, mod: nil}, %CallInfo{arity: 0, func: :integer, position: {2, 72}, mod: nil}, %CallInfo{arity: 0, func: :integer, position: {2, 56}, mod: nil}, - %CallInfo{arity: 0, func: :integer, position: {2, 38}, mod: nil} + %CallInfo{arity: 0, func: :integer, position: {2, 38}, mod: nil} | _ ], 3 => [ %CallInfo{arity: 0, func: :integer, position: {3, 61}, mod: nil}, %CallInfo{arity: 0, func: :integer, position: {3, 44}, mod: nil}, - %CallInfo{arity: 0, func: :integer, position: {3, 26}, mod: nil} + %CallInfo{arity: 0, func: :integer, position: {3, 26}, mod: nil} | _ ] - } + } = state.calls end test "registers calls local no arg" do @@ -6609,9 +6646,9 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ + assert %{ 4 => [%CallInfo{arity: 0, func: :func_1, position: {4, 5}, mod: nil}] - } + } = state.calls end test "registers calls local arg" do @@ -6625,9 +6662,9 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ + assert %{ 3 => [%CallInfo{arity: 1, func: :func_1, position: {3, 5}, mod: nil}] - } + } = state.calls end test "registers calls arg" do @@ -6641,9 +6678,9 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ + assert %{ 3 => [%CallInfo{arity: 1, func: :func, position: {3, 11}, mod: MyMod}] - } + } = state.calls end test "registers calls on attribute and var with args" do @@ -6660,16 +6697,17 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state if Version.match?(System.version(), ">= 1.15.0") do - assert state.calls == %{ + assert %{ 4 => [ - %CallInfo{arity: 1, func: :func, position: {4, 11}, mod: {:attribute, :attr}} + %CallInfo{arity: 1, func: :func, position: {4, 11}, mod: {:attribute, :attr}}, + _ ], 5 => [ %CallInfo{arity: 1, func: :func, position: {5, 9}, mod: {:variable, :var, 0}} ] - } + } = state.calls else - assert state.calls == %{ + assert %{ 4 => [ %CallInfo{arity: 1, func: :func, position: {4, 11}, mod: {:attribute, :attr}} ], @@ -6677,7 +6715,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do %CallInfo{arity: 0, func: :var, position: {5, 5}, mod: nil}, %CallInfo{arity: 1, func: :func, position: {5, 9}, mod: {:variable, :var, 0}} ] - } + } = state.calls end end @@ -6695,16 +6733,23 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state if Version.match?(System.version(), ">= 1.15.0") do - assert state.calls == %{ - 4 => [ - %CallInfo{arity: 0, func: :func, position: {4, 11}, mod: {:attribute, :attr}} - ], - 5 => [ - %CallInfo{arity: 0, func: :func, position: {5, 9}, mod: {:variable, :var, 0}} - ] - } + Enum.any?( + state.calls[4], + &match?( + %CallInfo{arity: 0, func: :func, position: {4, 11}, mod: {:attribute, :attr}}, + &1 + ) + ) + + Enum.any?( + state.calls[4], + &match?( + %CallInfo{arity: 0, func: :func, position: {5, 9}, mod: {:variable, :var, 0}}, + &1 + ) + ) else - assert state.calls == %{ + assert %{ 4 => [ %CallInfo{arity: 0, func: :func, position: {4, 11}, mod: {:attribute, :attr}} ], @@ -6712,7 +6757,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do %CallInfo{arity: 0, func: :var, position: {5, 5}, mod: nil}, %CallInfo{arity: 0, func: :func, position: {5, 9}, mod: {:variable, :var, 0}} ] - } + } = state.calls end end @@ -6730,16 +6775,17 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state if Version.match?(System.version(), ">= 1.15.0") do - assert state.calls == %{ + assert %{ 4 => [ - %CallInfo{arity: 0, func: {:attribute, :attr}, position: {4, 11}, mod: nil} + %CallInfo{arity: 0, func: {:attribute, :attr}, position: {4, 11}, mod: nil}, + _ ], 5 => [ %CallInfo{arity: 0, func: {:variable, :var, 0}, position: {5, 9}, mod: nil} ] - } + } = state.calls else - assert state.calls == %{ + assert %{ 4 => [ %CallInfo{arity: 0, func: {:attribute, :attr}, position: {4, 11}, mod: nil} ], @@ -6747,7 +6793,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do %CallInfo{arity: 0, func: :var, position: {5, 5}, mod: nil}, %CallInfo{arity: 0, func: {:variable, :var, 0}, position: {5, 9}, mod: nil} ] - } + } = state.calls end end @@ -7020,10 +7066,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ + assert %{ 3 => [%CallInfo{arity: 1, position: {3, 17}, func: :func, mod: NyModule}], 4 => [%CallInfo{arity: 1, position: {4, 21}, func: :func, mod: NyModule.Sub}] - } + } = state.calls end test "registers calls capture operator external" do @@ -7037,9 +7083,9 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ + assert %{ 3 => [%CallInfo{arity: 1, position: {3, 12}, func: :func, mod: MyMod}] - } + } = state.calls end test "registers calls capture expression external" do @@ -7053,9 +7099,9 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ + assert %{ 3 => [%CallInfo{arity: 2, position: {3, 12}, func: :func, mod: MyMod}] - } + } = state.calls end test "registers calls capture operator external erlang module" do @@ -7069,9 +7115,9 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ + assert %{ 3 => [%CallInfo{arity: 1, func: :func, position: {3, 15}, mod: :erl_mod}] - } + } = state.calls end test "registers calls capture operator external atom module" do @@ -7085,9 +7131,9 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ + assert %{ 3 => [%CallInfo{arity: 1, func: :func, position: {3, 22}, mod: MyMod}] - } + } = state.calls end test "registers calls capture operator local" do @@ -7102,10 +7148,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ + assert %{ 3 => [%CallInfo{arity: 1, func: :func, position: {3, 6}, mod: nil}], 4 => [%CallInfo{arity: 0, func: :func, position: {4, 6}, mod: nil}] - } + } = state.calls end test "registers calls capture expression local" do @@ -7119,9 +7165,9 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert state.calls == %{ + assert %{ 3 => [%CallInfo{arity: 2, func: :func, position: {3, 6}, mod: nil}] - } + } = state.calls end test "registers calls on ex_unit DSL" do From 83a2d2e340d793fdfc19dca3173e70f8aca734e4 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 9 Aug 2024 00:20:28 +0200 Subject: [PATCH 131/235] address a few TODOs --- lib/elixir_sense/core/compiler.ex | 73 ++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 17 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index b0378296..e56fdc81 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -359,7 +359,7 @@ defmodule ElixirSense.Core.Compiler do case Keyword.fetch(e_opts, :bind_quoted) do {:ok, bq} -> if is_list(bq) do - # TODO check if there's cursor? + # safe to drop, opts already expanded bq = Enum.filter(bq, &match?({key, _} when is_atom(key), &1)) {bq, false} else @@ -1773,6 +1773,9 @@ defmodule ElixirSense.Core.Compiler do "Unable to expand macro #{inspect(module)}.#{fun}/#{length(args)}: #{Exception.format(kind, payload, __STACKTRACE__)}" ) + # look for cursor in args + {_ast, state, _env} = expand(args, state, env) + {{{:., meta, [module, fun]}, meta, args}, state, env} else ast -> @@ -1865,7 +1868,9 @@ defmodule ElixirSense.Core.Compiler do when is_atom(receiver) or is_tuple(receiver) do if context == :guard and is_tuple(receiver) do # elixir raises parens_map_lookup unless no_parens is set in meta - # TODO there may be cursor in discarded args + # look for cursor in discarded args + {_ast, sl, _env} = expand(args, sl, e) + sl = sl |> add_call_to_line({receiver, right, length(args)}, meta) @@ -1963,6 +1968,7 @@ defmodule ElixirSense.Core.Compiler do defp expand_opts(allowed, opts, s, e) do {e_opts, se, ee} = expand(opts, s, e) + # safe to drop after expand e_opts = sanitize_opts(allowed, e_opts) {e_opts, se, ee} end @@ -2145,7 +2151,6 @@ defmodule ElixirSense.Core.Compiler do defp expand_for({:for, meta, [_ | _] = args}, s, e, return) do {cases, block} = __MODULE__.Utils.split_opts(args) - block = sanitize_opts([:do, :into, :uniq, :reduce], block) {expr, opts} = case Keyword.pop(block, :do) do @@ -2161,6 +2166,9 @@ defmodule ElixirSense.Core.Compiler do {e_cases, sc, ec} = map_fold(&expand_for_generator/3, so, eo, cases) # elixir raises here for_generator_start on invalid start generator + # safe to drop after expand + e_opts = sanitize_opts([:into, :uniq, :reduce], e_opts) + {maybe_reduce, normalized_opts} = sanitize_for_options(e_opts, false, false, false, return, meta, e, []) @@ -2190,13 +2198,19 @@ defmodule ElixirSense.Core.Compiler do {:->, clause_meta, [args, right]}, sa -> # elixir checks here that clause has exactly 1 arg by matching against {_, _, [[_], _]} # we drop excessive or generate a fake arg - # TODO check if there is cursor in dropped arg? - args = + + {args, discarded_args} = case args do - [] -> [{:_, [], e.module}] - [head | _] -> [head] + [] -> + {[{:_, [], e.module}], []} + + [head | rest] -> + {[head], rest} end + # check if there is cursor in dropped arg + {_ast, sa, _e} = expand(discarded_args, sa, e) + clause = {:->, clause_meta, [args, right]} s_reset = __MODULE__.Env.reset_vars(sa) @@ -2309,7 +2323,7 @@ defmodule ElixirSense.Core.Compiler do end defp sanitize_for_options([], false, {:uniq, true}, false, false, meta, e, acc) do - # TODO check if there is cursor in dropped unique + # safe to drop here even if there's a cursor options are already expanded acc_without_uniq = Keyword.delete(acc, :uniq) sanitize_for_options([], false, false, false, false, meta, e, acc_without_uniq) end @@ -2319,7 +2333,6 @@ defmodule ElixirSense.Core.Compiler do end defp sanitize_opts(allowed, opts) when is_list(opts) do - # TODO check if there's cursor for {key, value} <- opts, Enum.member?(allowed, key), do: {key, value} end @@ -2656,6 +2669,8 @@ defmodule ElixirSense.Core.Compiler do # case + @valid_case_opts [:do] + def case(meta, e_expr, [], s, e) do # elixir raises here missing_option # emit a fake do block @@ -2669,7 +2684,10 @@ defmodule ElixirSense.Core.Compiler do end def case(meta, e_expr, opts, s, e) do - opts = sanitize_opts(opts, [:do]) + # expand invalid opts in case there's cursor + {_ast, s, _e} = ElixirExpand.expand(opts |> Keyword.drop(@valid_case_opts), s, e) + + opts = sanitize_opts(opts, @valid_case_opts) match_context = TypeInference.type_of(e_expr, e.context) @@ -2707,6 +2725,8 @@ defmodule ElixirSense.Core.Compiler do # cond + @valid_cond_opts [:do] + def cond(meta, [], s, e) do # elixir raises here missing_option # emit a fake do block @@ -2720,7 +2740,10 @@ defmodule ElixirSense.Core.Compiler do end def cond(meta, opts, s, e) do - opts = sanitize_opts(opts, [:do]) + # expand invalid opts in case there's cursor + {_ast, s, _e} = ElixirExpand.expand(opts |> Keyword.drop(@valid_cond_opts), s, e) + + opts = sanitize_opts(opts, @valid_cond_opts) {cond_clauses, sa} = Enum.map_reduce(opts, s, fn x, sa -> @@ -2736,6 +2759,8 @@ defmodule ElixirSense.Core.Compiler do # receive + @valid_receive_opts [:do, :after] + def receive(meta, [], s, e) do # elixir raises here missing_option # emit a fake do block @@ -2749,7 +2774,10 @@ defmodule ElixirSense.Core.Compiler do end def receive(meta, opts, s, e) do - opts = sanitize_opts(opts, [:do, :after]) + # expand invalid opts in case there's cursor + {_ast, s, _e} = ElixirExpand.expand(opts |> Keyword.drop(@valid_receive_opts), s, e) + + opts = sanitize_opts(opts, @valid_receive_opts) {receive_clauses, sa} = Enum.map_reduce(opts, s, fn x, sa -> @@ -2779,9 +2807,10 @@ defmodule ElixirSense.Core.Compiler do # try to recover from error by wrapping the expression in list expand_receive(meta, {:after, [expr]}, s, e) - [first | _] -> + [first | discarded] -> # try to recover from error by taking first clause only - # TODO maybe search for clause with cursor? + # expand other in case there's cursor + {_ast, s, _e} = ElixirExpand.expand(discarded, s, e) expand_receive(meta, {:after, [first]}, s, e) [] -> @@ -2792,9 +2821,15 @@ defmodule ElixirSense.Core.Compiler do # with + @valid_with_opts [:do, :else] + def with(meta, args, s, e) do {exprs, opts0} = ElixirUtils.split_opts(args) - opts0 = sanitize_opts(opts0, [:do, :else]) + + # expand invalid opts in case there's cursor + {_ast, s, _e} = ElixirExpand.expand(opts0 |> Keyword.drop(@valid_with_opts), s, e) + + opts0 = sanitize_opts(opts0, @valid_with_opts) s0 = ElixirEnv.reset_vars(s) {e_exprs, {s1, e1}} = Enum.map_reduce(exprs, {s0, e}, &expand_with/2) {e_do, opts1, s2} = expand_with_do(meta, opts0, s, s1, e1) @@ -2855,6 +2890,8 @@ defmodule ElixirSense.Core.Compiler do # try + @valid_try_opts [:do, :rescue, :catch, :else, :after] + def try(meta, [], s, e) do # elixir raises here missing_option # emit a fake do block @@ -2868,7 +2905,10 @@ defmodule ElixirSense.Core.Compiler do end def try(meta, opts, s, e) do - opts = sanitize_opts(opts, [:do, :rescue, :catch, :else, :after]) + # expand invalid opts in case there's cursor + {_ast, s, _e} = ElixirExpand.expand(opts |> Keyword.drop(@valid_try_opts), s, e) + + opts = sanitize_opts(opts, @valid_try_opts) {try_clauses, sa} = Enum.map_reduce(opts, s, fn x, sa -> @@ -3068,7 +3108,6 @@ defmodule ElixirSense.Core.Compiler do # helpers defp sanitize_opt(opts, opt) do - # TODO look for opt with cursor? case Keyword.fetch(opts, opt) do :error -> [] {:ok, value} -> [{opt, value}] From 617eb83bb41ed081652489dd6f4f1b733577e9ca Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 10 Aug 2024 09:52:04 +0200 Subject: [PATCH 132/235] make line and column extraction more robust --- lib/elixir_sense/core/compiler.ex | 100 +++++-------------------- lib/elixir_sense/core/state.ex | 117 +++++++++++++++++++++--------- 2 files changed, 100 insertions(+), 117 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index e56fdc81..c46f6399 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -833,8 +833,6 @@ defmodule ElixirSense.Core.Compiler do env = %{module: module} ) when module != nil do - {position, end_position} = extract_range(meta) - {opts, state, env} = expand(opts, state, env) # elixir does validation here target = Keyword.get(opts, :to, :__unknown__) @@ -861,8 +859,7 @@ defmodule ElixirSense.Core.Compiler do env, name, args, - position, - end_position, + extract_range(meta), :defdelegate, target: {target, as, length(as_args)} ) @@ -1019,9 +1016,6 @@ defmodule ElixirSense.Core.Compiler do env = %{module: module} ) when module != nil do - line = Keyword.fetch!(meta, :line) - column = Keyword.fetch!(meta, :column) - state = List.wrap(derived_protos) |> Enum.map(fn @@ -1041,7 +1035,7 @@ defmodule ElixirSense.Core.Compiler do nil -> # implementation for: Any not detected (is in other file etc.) acc - |> add_module_to_index(mod, {line, column}, nil, generated: true) + |> add_module_to_index(mod, extract_range(meta), generated: true) _any_mods_funs -> # copy implementation for: Any @@ -1091,11 +1085,9 @@ defmodule ElixirSense.Core.Compiler do spec = TypeInfo.typespec_to_string(kind, expr) - {position, end_position} = extract_range(attr_meta) - state = state - |> add_type(env, name, type_args, spec, kind, position, end_position) + |> add_type(env, name, type_args, spec, kind, extract_range(attr_meta)) |> with_typespec({name, length(type_args)}) |> add_current_env_to_line(attr_meta, env) |> with_typespec(nil) @@ -1136,7 +1128,7 @@ defmodule ElixirSense.Core.Compiler do {name, type_args} -> spec = TypeInfo.typespec_to_string(kind, expr) - {position, end_position} = extract_range(attr_meta) + range = extract_range(attr_meta) state = if kind in [:callback, :macrocallback] do @@ -1145,8 +1137,7 @@ defmodule ElixirSense.Core.Compiler do env, :behaviour_info, [{:atom, attr_meta, nil}], - position, - end_position, + range, :def, generated: true ) @@ -1156,7 +1147,7 @@ defmodule ElixirSense.Core.Compiler do state = state - |> add_spec(env, name, type_args, spec, kind, position, end_position) + |> add_spec(env, name, type_args, spec, kind, range) |> with_typespec({name, length(type_args)}) |> add_current_env_to_line(attr_meta, env) |> with_typespec(nil) @@ -1178,9 +1169,6 @@ defmodule ElixirSense.Core.Compiler do env = %{module: module} ) when is_atom(name) and module != nil do - line = Keyword.fetch!(meta, :line) - column = Keyword.get(meta, :column, 1) - {is_definition, {e_args, state, env}} = case args do arg when is_atom(arg) -> @@ -1209,7 +1197,7 @@ defmodule ElixirSense.Core.Compiler do state = state - |> add_attribute(name, inferred_type, is_definition, {line, column}) + |> add_attribute(name, inferred_type, is_definition, meta) |> add_current_env_to_line(meta, env) {{:@, meta, [{name, name_meta, e_args}]}, state, env} @@ -1274,8 +1262,6 @@ defmodule ElixirSense.Core.Compiler do [] end - {position, end_position} = extract_range(meta) - fields = fields |> Enum.filter(fn @@ -1290,7 +1276,7 @@ defmodule ElixirSense.Core.Compiler do state = state - |> add_struct_or_exception(env, type, fields, position, end_position) + |> add_struct_or_exception(env, type, fields, extract_range(meta)) {{type, meta, [fields]}, state, env} end @@ -1305,7 +1291,7 @@ defmodule ElixirSense.Core.Compiler do env = %{module: module} ) when call in [:defrecord, :defrecordp] and module != nil do - {position, end_position} = extract_range(meta) + range = extract_range(meta) type = case call do @@ -1321,8 +1307,7 @@ defmodule ElixirSense.Core.Compiler do env, name, [{:\\, [], [{:args, [], nil}, []]}], - position, - end_position, + range, type, options ) @@ -1330,8 +1315,7 @@ defmodule ElixirSense.Core.Compiler do env, name, [{:record, [], nil}, {:args, [], nil}], - position, - end_position, + range, type, options ) @@ -1349,7 +1333,6 @@ defmodule ElixirSense.Core.Compiler do state, env ) do - {position, end_position} = extract_range(meta) original_env = env # expand the macro normally {ast, state, env} = @@ -1364,8 +1347,7 @@ defmodule ElixirSense.Core.Compiler do %{env | module: module}, :behaviour_info, [:atom], - position, - end_position, + extract_range(meta), :def, generated: true ) @@ -1504,7 +1486,7 @@ defmodule ElixirSense.Core.Compiler do %{state | runtime_modules: [full | state.runtime_modules]} end - {position, end_position} = extract_range(meta) + range = extract_range(meta) module_functions = case state.protocol do @@ -1514,10 +1496,10 @@ defmodule ElixirSense.Core.Compiler do state = state - |> add_module_to_index(full, position, end_position, []) + |> add_module_to_index(full, range, []) |> add_module |> add_current_env_to_line(meta, %{env | module: full}) - |> add_module_functions(%{env | module: full}, module_functions, position, end_position) + |> add_module_functions(%{env | module: full}, module_functions, range) |> new_vars_scope |> new_attributes_scope @@ -1658,8 +1640,6 @@ defmodule ElixirSense.Core.Compiler do env_for_expand = %{env_for_expand | context: nil} - {position, end_position} = extract_range(meta) - state = state |> add_current_env_to_line(meta, env_for_expand) @@ -1667,8 +1647,7 @@ defmodule ElixirSense.Core.Compiler do env, name, args, - position, - end_position, + extract_range(meta), def_kind ) @@ -1735,7 +1714,7 @@ defmodule ElixirSense.Core.Compiler do [context, do_block | _] -> {[context], do_block} end - line = Keyword.fetch!(meta, :line) + line = __MODULE__.Utils.get_line(meta) # NOTE this name is not 100% correct - ex_unit uses counters instead of line but it's too complicated call = {:"__ex_unit_#{setup}_#{line}", meta, args} @@ -1790,42 +1769,6 @@ defmodule ElixirSense.Core.Compiler do {ast, state, env} end - defp extract_range(meta) do - line = Keyword.get(meta, :line, 0) - - if line <= 0 do - {nil, nil} - else - position = { - line, - Keyword.get(meta, :column, 1) - } - - end_position = - case meta[:end] do - nil -> - case meta[:end_of_expression] do - nil -> - nil - - end_of_expression_meta -> - { - Keyword.fetch!(end_of_expression_meta, :line), - Keyword.fetch!(end_of_expression_meta, :column) - } - end - - end_meta -> - { - Keyword.fetch!(end_meta, :line), - Keyword.fetch!(end_meta, :column) + 3 - } - end - - {position, end_position} - end - end - defp ex_unit_test_name(state, name) do case state.ex_unit_describe do nil -> "test #{name}" @@ -3046,7 +2989,7 @@ defmodule ElixirSense.Core.Compiler do # rescue expr() => rescue expanded_expr() defp expand_rescue({_, meta, _} = arg, s, e) do # TODO wut? - case Macro.expand_once(arg, %{e | line: line(meta)}) do + case Macro.expand_once(arg, %{e | line: ElixirUtils.get_line(meta)}) do ^arg -> # elixir rejects this case # try to recover from error by generating fake expression @@ -3121,13 +3064,6 @@ defmodule ElixirSense.Core.Compiler do defp origin(meta, default) do Keyword.get(meta, :origin, default) end - - defp line(opts) when is_list(opts) do - case Keyword.fetch(opts, :line) do - {:ok, line} when is_integer(line) -> line - _ -> 0 - end - end end defmodule Bitstring do diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 48657c6b..f63e0f27 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -468,6 +468,7 @@ defmodule ElixirSense.Core.State do end defp do_add_call_to_line(%__MODULE__{} = state, {mod, func, arity}, meta) do + # extract_position is not suitable here, we need to handle invalid lines line = Keyword.get(meta, :line, 0) column = Keyword.get(meta, :column, nil) @@ -609,7 +610,7 @@ defmodule ElixirSense.Core.State do %{state | specs: updated_specs} end - def add_module_to_index(%__MODULE__{} = state, module, position, end_position, options) + def add_module_to_index(%__MODULE__{} = state, module, {position, end_position}, options) when (is_tuple(position) and is_tuple(end_position)) or is_nil(end_position) do # TODO :defprotocol, :defimpl? add_mod_fun_to_position( @@ -630,8 +631,7 @@ defmodule ElixirSense.Core.State do env, func, params, - position, - end_position, + {position, end_position}, type, options \\ [] ) @@ -806,8 +806,7 @@ defmodule ElixirSense.Core.State do type_args, spec, kind, - pos, - end_pos, + {pos, end_pos}, options \\ [] ) do arg_names = @@ -880,8 +879,7 @@ defmodule ElixirSense.Core.State do type_args, spec, kind, - pos, - end_pos, + {pos, end_pos}, options \\ [] ) do arg_names = @@ -930,14 +928,12 @@ defmodule ElixirSense.Core.State do def add_var_write(%__MODULE__{} = state, {name, meta, _}) when name != :_ do version = meta[:version] - line = meta[:line] - column = meta[:column] scope_id = hd(state.scope_ids) info = %VarInfo{ name: name, version: version, - positions: [{line, column}], + positions: [extract_position(meta)], scope_id: scope_id } @@ -954,8 +950,6 @@ defmodule ElixirSense.Core.State do def add_var_read(%__MODULE__{} = state, {name, meta, _}) when name != :_ do version = meta[:version] - line = meta[:line] - column = meta[:column] [vars_from_scope | other_vars] = state.vars_info @@ -964,7 +958,11 @@ defmodule ElixirSense.Core.State do state info -> - info = %VarInfo{info | positions: (info.positions ++ [{line, column}]) |> Enum.uniq()} + info = %VarInfo{ + info + | positions: (info.positions ++ [extract_position(meta)]) |> Enum.uniq() + } + vars_from_scope = Map.put(vars_from_scope, {name, version}, info) %__MODULE__{ @@ -978,8 +976,9 @@ defmodule ElixirSense.Core.State do @builtin_attributes ElixirSense.Core.BuiltinAttributes.all() - def add_attribute(%__MODULE__{} = state, attribute, type, is_definition, position) + def add_attribute(%__MODULE__{} = state, attribute, type, is_definition, meta) when attribute not in @builtin_attributes do + position = extract_position(meta) [attributes_from_scope | other_attributes] = state.attributes existing_attribute_index = @@ -1009,7 +1008,7 @@ defmodule ElixirSense.Core.State do %AttributeInfo{ existing - | # FIXME this is wrong for accumulating attributes + | # TODO this is wrong for accumulating attributes type: type, positions: (existing.positions ++ [position]) |> Enum.uniq() |> Enum.sort() } @@ -1021,7 +1020,7 @@ defmodule ElixirSense.Core.State do %__MODULE__{state | attributes: attributes, scope_attributes: scope_attributes} end - def add_attribute(%__MODULE__{} = state, _attribute, _type, _is_definition, _position) do + def add_attribute(%__MODULE__{} = state, _attribute, _type, _is_definition, _meta) do state end @@ -1167,20 +1166,20 @@ defmodule ElixirSense.Core.State do {:module_info, [:atom], :def} ] - def add_module_functions(state, env, functions, position, end_position) do - {line, column} = position + def add_module_functions(state, env, functions, range) do + {{line, column}, _} = range + meta = [line: line || 0] ++ if(column > 0, do: [column: column], else: []) (functions ++ @module_functions) |> Enum.reduce(state, fn {name, args, kind}, acc -> - mapped_args = for arg <- args, do: {arg, [line: line, column: column], nil} + mapped_args = for arg <- args, do: {arg, meta, nil} acc |> add_func_to_index( env, name, mapped_args, - position, - end_position, + range, kind, generated: true ) @@ -1191,7 +1190,10 @@ defmodule ElixirSense.Core.State do %{state | typespec: typespec} end - def add_struct_or_exception(state, env, type, fields, {line, column} = position, end_position) do + def add_struct_or_exception(state, env, type, fields, range) do + {{line, column}, _} = range + meta = [line: line || 0] ++ if(column > 0, do: [column: column], else: []) + fields = fields ++ if type == :defexception do @@ -1211,18 +1213,16 @@ defmodule ElixirSense.Core.State do |> add_func_to_index( env, :exception, - [{:msg, [line: line, column: column], nil}], - position, - end_position, + [{:msg, meta, nil}], + range, :def, options ) |> add_func_to_index( env, :message, - [{:exception, [line: line, column: column], nil}], - position, - end_position, + [{:exception, meta, nil}], + range, :def, options ) @@ -1232,22 +1232,20 @@ defmodule ElixirSense.Core.State do |> add_func_to_index( env, :exception, - [{:args, [line: line, column: column], nil}], - position, - end_position, + [{:args, meta, nil}], + range, :def, options ) else state end - |> add_func_to_index(env, :__struct__, [], position, end_position, :def, options) + |> add_func_to_index(env, :__struct__, [], range, :def, options) |> add_func_to_index( env, :__struct__, - [{:kv, [line: line, column: column], nil}], - position, - end_position, + [{:kv, meta, nil}], + range, :def, options ) @@ -1352,4 +1350,53 @@ defmodule ElixirSense.Core.State do %{state | vars_info: [h | t]} end + + def extract_position(meta) do + line = Keyword.get(meta, :line, 0) + + if line <= 0 do + nil + else + { + line, + Keyword.get(meta, :column) + } + end + end + + def extract_range(meta) do + line = Keyword.get(meta, :line, 0) + + if line <= 0 do + {nil, nil} + else + position = { + line, + Keyword.get(meta, :column) + } + + end_position = + case meta[:end] do + nil -> + case meta[:end_of_expression] do + nil -> + nil + + end_of_expression_meta -> + { + Keyword.fetch!(end_of_expression_meta, :line), + Keyword.fetch!(end_of_expression_meta, :column) + } + end + + end_meta -> + { + Keyword.fetch!(end_meta, :line), + Keyword.fetch!(end_meta, :column) + 3 + } + end + + {position, end_position} + end + end end From 3d4664a8a47a5268ea4e860cb12a2b914e4316df Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 10 Aug 2024 10:07:17 +0200 Subject: [PATCH 133/235] wip env --- lib/elixir_sense/core/metadata.ex | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/elixir_sense/core/metadata.ex b/lib/elixir_sense/core/metadata.ex index b3f44b6b..9cdfaf3a 100644 --- a/lib/elixir_sense/core/metadata.ex +++ b/lib/elixir_sense/core/metadata.ex @@ -61,6 +61,7 @@ defmodule ElixirSense.Core.Metadata do def get_cursor_env( %__MODULE__{} = metadata, + {line, column}, {{begin_line, begin_column}, {end_line, end_column}} ) do prefix = ElixirSense.Core.Source.text_before(metadata.source, begin_line, begin_column) @@ -70,8 +71,8 @@ defmodule ElixirSense.Core.Metadata do {meta, cursor_env} = case Code.string_to_quoted(source_with_cursor, columns: true, token_metadata: true) do - {:ok, ast} -> - ElixirSense.Core.MetadataBuilder.build(ast).cursor_env || {[], nil} + # {:ok, ast} -> + # ElixirSense.Core.MetadataBuilder.build(ast).cursor_env || {[], nil} _ -> {[], nil} @@ -85,8 +86,8 @@ defmodule ElixirSense.Core.Metadata do columns: true, token_metadata: true ) do - {:ok, ast} -> - ElixirSense.Core.MetadataBuilder.build(ast).cursor_env || {[], nil} + # {:ok, ast} -> + # ElixirSense.Core.MetadataBuilder.build(ast).cursor_env || {[], nil} _ -> {[], nil} @@ -96,7 +97,7 @@ defmodule ElixirSense.Core.Metadata do if cursor_env != nil do cursor_env else - get_env(metadata, {begin_line, begin_column}) + get_env(metadata, {line, column}) end end From 258a9de5836bc699f2039fc4fcee211b8194fa34 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 11 Aug 2024 08:14:43 +0200 Subject: [PATCH 134/235] addressed TODO --- lib/elixir_sense/core/compiler.ex | 10 ++-------- .../core/metadata_builder_test.exs | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index c46f6399..c096c37f 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -143,9 +143,6 @@ defmodule ElixirSense.Core.Compiler do {opts, state, env} = expand_opts([:as, :warn], no_alias_opts(opts), state, env) if is_atom(arg) do - # TODO check difference with - # elixir_aliases:alias(Meta, Ref, IncludeByDefault, Opts, E, true) - # TODO PR to elixir with is_atom(module) check? case NormalizedMacroEnv.define_alias(env, meta, arg, [trace: false] ++ opts) do {:ok, env} -> {arg, state, env} @@ -202,11 +199,8 @@ defmodule ElixirSense.Core.Compiler do end :error when is_atom(arg) -> - # TODO check differences - # TODO ensure loaded? - # ElixirAliases.ensure_loaded(meta, e_ref, et) - # re = ElixirAliases.require(meta, e_ref, e_opts, et, true) - # {e_ref, st, alias(meta, e_ref, false, e_opts, re)} + # elixir calls here :elixir_aliases.ensure_loaded(meta, e_ref, et) + # and optionally waits until required module is compiled case NormalizedMacroEnv.define_require(env, meta, arg, [trace: false] ++ opts) do {:ok, env} -> {arg, state, env} diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 0c9ef3c1..ec0aaeff 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -4452,6 +4452,25 @@ defmodule ElixirSense.Core.MetadataBuilderTest do [Application, Kernel, Kernel.Typespec, Mod] |> maybe_reject_typespec end + test "requires local module" do + state = + """ + defmodule Mod do + defmacro some, do: :ok + end + + defmodule MyModule do + require Mod + Mod.some() + IO.puts "" + end + """ + |> string_to_state + + assert get_line_requires(state, 8) == + [Application, Kernel, Kernel.Typespec, Mod] |> maybe_reject_typespec + end + test "requires with __MODULE__" do state = """ From 485c2b62aa38842749fb68cd0fb423dad8ac6783 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 11 Aug 2024 12:16:34 +0200 Subject: [PATCH 135/235] address todo, add context_modules to env --- lib/elixir_sense/core/compiler.ex | 43 +++---------------- lib/elixir_sense/core/state.ex | 3 ++ .../core/metadata_builder_test.exs | 23 ++++++++++ 3 files changed, 33 insertions(+), 36 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index c096c37f..391ea946 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -158,8 +158,6 @@ defmodule ElixirSense.Core.Compiler do end defp do_expand({:require, meta, [arg, opts]}, state, env) do - original_env = env - state = state |> add_current_env_to_line(meta, env) @@ -170,35 +168,10 @@ defmodule ElixirSense.Core.Compiler do {opts, state, env} = expand_opts([:as, :warn], no_alias_opts(opts), state, env) - case Keyword.fetch(meta, :defined) do - {:ok, mod} when is_atom(mod) -> - env = %{env | context_modules: [mod | env.context_modules]} + # elixir handles special meta key :defined in the require call. + # It is only set by defmodule and we handle it there - state = - case original_env do - %{function: nil} -> state - _ -> %{state | runtime_modules: [mod | state.runtime_modules]} - end - - # TODO how to test that case? - # TODO remove this case - in elixir this is a hack used only by special require call emitted by defmodule - # Macro.Env.define_alias is not fully equivalent - it calls alias with IncludeByDefault set to true - # we counter it with only calling it if :as option is set - # {arg, state, alias(meta, e_ref, false, e_opts, ea)} - if Keyword.has_key?(opts, :as) do - case NormalizedMacroEnv.define_alias(env, meta, arg, [trace: false] ++ opts) do - {:ok, env} -> - {arg, state, env} - - {:error, _} -> - # elixir_aliases - {arg, state, env} - end - else - {arg, state, env} - end - - :error when is_atom(arg) -> + if is_atom(arg) do # elixir calls here :elixir_aliases.ensure_loaded(meta, e_ref, et) # and optionally waits until required module is compiled case NormalizedMacroEnv.define_require(env, meta, arg, [trace: false] ++ opts) do @@ -209,8 +182,7 @@ defmodule ElixirSense.Core.Compiler do # elixir_aliases {arg, state, env} end - - :error -> + else # expected_compile_time_module {arg, state, env} end @@ -1464,12 +1436,11 @@ defmodule ElixirSense.Core.Compiler do # The env inside the block is discarded {_result, state, env} = if is_atom(expanded) do + # elixir emits a special require directive with :defined key set in meta + # require expand does alias, updates context_modules and runtime_modules + # we do it here instead {full, env} = alias_defmodule(alias, expanded, env) - - # in elixir context_modules and runtime_modules are handled via special require expansion - # with :defined key set in meta env = %{env | context_modules: [full | env.context_modules]} - state = case original_env do %{function: nil} -> diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index f63e0f27..9720765a 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -117,6 +117,7 @@ defmodule ElixirSense.Core.State do vars: list(ElixirSense.Core.State.VarInfo.t()), attributes: list(ElixirSense.Core.State.AttributeInfo.t()), behaviours: list(module), + context_modules: list(module), typespec: nil | {atom, arity}, scope_id: nil | ElixirSense.Core.State.scope_id_t() } @@ -133,6 +134,7 @@ defmodule ElixirSense.Core.State do vars: [], attributes: [], behaviours: [], + context_modules: [], typespec: nil, scope_id: nil end @@ -345,6 +347,7 @@ defmodule ElixirSense.Core.State do aliases: macro_env.aliases, module: macro_env.module, function: macro_env.function, + context_modules: macro_env.context_modules, vars: vars, versioned_vars: versioned_vars, attributes: current_attributes, diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index ec0aaeff..328a51e1 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -4452,6 +4452,29 @@ defmodule ElixirSense.Core.MetadataBuilderTest do [Application, Kernel, Kernel.Typespec, Mod] |> maybe_reject_typespec end + test "defmodule emits require with :defined meta" do + state = + """ + IO.puts "" + defmodule Foo.Bar do + IO.puts "" + defmodule Some.Mod do + IO.puts "" + end + IO.puts "" + end + IO.puts "" + """ + |> string_to_state + + assert state.lines_to_env[1].context_modules == [] + assert state.lines_to_env[3].context_modules == [Foo.Bar] + assert state.lines_to_env[5].context_modules == [Foo.Bar.Some.Mod, Foo.Bar] + assert state.lines_to_env[7].context_modules == [Foo.Bar.Some.Mod, Foo.Bar] + assert state.lines_to_env[9].context_modules == [Foo.Bar] + assert state.runtime_modules == [] + end + test "requires local module" do state = """ From 77fc422c65078bc39f2fd5a356196d416e580a04 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 11 Aug 2024 12:50:42 +0200 Subject: [PATCH 136/235] addressed TODO - added test of runtime module --- lib/elixir_sense/core/compiler.ex | 26 +++++++-------- lib/elixir_sense/core/state.ex | 1 + .../core/metadata_builder_test.exs | 33 +++++++++++++++++++ 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 391ea946..15a7d8a9 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -172,19 +172,19 @@ defmodule ElixirSense.Core.Compiler do # It is only set by defmodule and we handle it there if is_atom(arg) do - # elixir calls here :elixir_aliases.ensure_loaded(meta, e_ref, et) - # and optionally waits until required module is compiled - case NormalizedMacroEnv.define_require(env, meta, arg, [trace: false] ++ opts) do - {:ok, env} -> - {arg, state, env} - - {:error, _} -> - # elixir_aliases - {arg, state, env} - end + # elixir calls here :elixir_aliases.ensure_loaded(meta, e_ref, et) + # and optionally waits until required module is compiled + case NormalizedMacroEnv.define_require(env, meta, arg, [trace: false] ++ opts) do + {:ok, env} -> + {arg, state, env} + + {:error, _} -> + # elixir_aliases + {arg, state, env} + end else - # expected_compile_time_module - {arg, state, env} + # expected_compile_time_module + {arg, state, env} end end @@ -1441,13 +1441,13 @@ defmodule ElixirSense.Core.Compiler do # we do it here instead {full, env} = alias_defmodule(alias, expanded, env) env = %{env | context_modules: [full | env.context_modules]} + state = case original_env do %{function: nil} -> state _ -> - # TODO how to test that? quote do defmodule? %{state | runtime_modules: [full | state.runtime_modules]} end diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 9720765a..5480e1d5 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -348,6 +348,7 @@ defmodule ElixirSense.Core.State do module: macro_env.module, function: macro_env.function, context_modules: macro_env.context_modules, + # TODO macro_aliases vars: vars, versioned_vars: versioned_vars, attributes: current_attributes, diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 328a51e1..05583e54 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -4475,6 +4475,39 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert state.runtime_modules == [] end + test "defmodule emits require with :defined meta - runtime module" do + state = + """ + IO.puts "" + defmodule Foo.Bar do + IO.puts "" + def a do + defmodule Some.Mod do + IO.puts "" + def b, do: :ok + end + IO.puts "" + Some.Mod.b() + IO.puts "" + end + IO.puts "" + end + IO.puts "" + """ + |> string_to_state + + assert state.lines_to_env[1].context_modules == [] + assert state.lines_to_env[3].context_modules == [Foo.Bar] + assert state.lines_to_env[6].context_modules == [Foo.Bar.Some.Mod, Foo.Bar] + assert state.lines_to_env[9].context_modules == [Foo.Bar.Some.Mod, Foo.Bar] + assert state.lines_to_env[11].context_modules == [Foo.Bar.Some.Mod, Foo.Bar] + assert state.lines_to_env[13].context_modules == [Foo.Bar] + assert state.lines_to_env[15].context_modules == [Foo.Bar] + assert state.runtime_modules == [Foo.Bar.Some.Mod] + + assert state.lines_to_env[9].aliases == [{Some, Foo.Bar.Some}] + end + test "requires local module" do state = """ From dcd2bda05ec3c133fcdb042cfeeb3d733701d6d3 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 11 Aug 2024 13:24:50 +0200 Subject: [PATCH 137/235] expose macro aliases, add test --- lib/elixir_sense/core/state.ex | 4 +- .../core/metadata_builder_test.exs | 46 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 5480e1d5..492c911e 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -110,6 +110,7 @@ defmodule ElixirSense.Core.State do macros: [{module, [{atom, arity}]}], requires: list(module), aliases: list(ElixirSense.Core.State.alias_t()), + macro_aliases: [{module, {term, module}}], module: nil | module, function: nil | {atom, arity}, protocol: nil | ElixirSense.Core.State.protocol_t(), @@ -125,6 +126,7 @@ defmodule ElixirSense.Core.State do macros: [], requires: [], aliases: [], + macro_aliases: [], # NOTE for protocol implementation this will be the first variant module: nil, function: nil, @@ -345,10 +347,10 @@ defmodule ElixirSense.Core.State do macros: macro_env.macros, requires: macro_env.requires, aliases: macro_env.aliases, + macro_aliases: macro_env.macro_aliases, module: macro_env.module, function: macro_env.function, context_modules: macro_env.context_modules, - # TODO macro_aliases vars: vars, versioned_vars: versioned_vars, attributes: current_attributes, diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 05583e54..99ffe147 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -4220,6 +4220,52 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert get_line_aliases(state, 3) == [{User, Foo.User}] assert get_line_aliases(state, 5) == [] end + + defmodule Macro.AliasTest.Definer do + defmacro __using__(_options) do + quote do + @before_compile unquote(__MODULE__) + end + end + + defmacro __before_compile__(_env) do + quote do + defmodule First do + defstruct foo: :bar + end + + defmodule Second do + defstruct baz: %First{} + end + end + end + end + + defmodule Macro.AliasTest.Aliaser do + defmacro __using__(_options) do + quote do + alias Some.First + IO.inspect({__ENV__.aliases, __ENV__.macro_aliases}) + end + end + end + + test "macro alias" do + state = + """ + defmodule MyModule do + use ElixirSense.Core.MetadataBuilderTest.Macro.AliasTest.Definer + use ElixirSense.Core.MetadataBuilderTest.Macro.AliasTest.Aliaser + IO.puts "" + a = %MyModule.First{} + b = %MyModule.Second{} + end + """ + |> string_to_state + + assert [{First, {_, Some.First}}] = state.lines_to_env[4].macro_aliases + # TODO should we handle @before_compile? + end end describe "import" do From 0b805adb1e1db300771c68ea815f534b45b1e9d1 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 14 Aug 2024 00:21:57 +0200 Subject: [PATCH 138/235] expand struct and record fields --- lib/elixir_sense/core/compiler.ex | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 15a7d8a9..8c797c4a 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1218,6 +1218,8 @@ defmodule ElixirSense.Core.Compiler do "#{inspect(module)}, defstruct can only be called once per module" end + {fields, state, env} = expand(fields, state, env) + fields = case fields do fs when is_list(fs) -> @@ -1251,13 +1253,14 @@ defmodule ElixirSense.Core.Compiler do meta, Record, call, - [name, _] = args, + [_name, _fields] = args, _callback, state, env = %{module: module} ) when call in [:defrecord, :defrecordp] and module != nil do range = extract_range(meta) + {[name, _fields] = args, state, env} = expand(args, state, env) type = case call do From ac81dcb83f0e0fdf8e976d06020bcc0aeda72f40 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 14 Aug 2024 00:23:35 +0200 Subject: [PATCH 139/235] expand before_compile --- lib/elixir_sense/core/compiler.ex | 28 ++++++- lib/elixir_sense/core/state.ex | 74 +++++++++++++++---- .../core/metadata_builder_test.exs | 53 ++++++++++--- 3 files changed, 127 insertions(+), 28 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 8c797c4a..d928e1ef 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1163,7 +1163,7 @@ defmodule ElixirSense.Core.Compiler do state = state - |> add_attribute(name, inferred_type, is_definition, meta) + |> add_attribute(env, name, meta, e_args, inferred_type, is_definition) |> add_current_env_to_line(meta, env) {{:@, meta, [{name, name_meta, e_args}]}, state, env} @@ -1475,7 +1475,26 @@ defmodule ElixirSense.Core.Compiler do {state, _env} = maybe_add_protocol_behaviour(state, %{env | module: full}) - {result, state, _env} = expand(block, state, %{env | module: full}) + {result, state, e_env} = expand(block, state, %{env | module: full}) + + before_compile = + for args <- Map.get(state.attribute_store, {full, :before_compile}, []) do + target = + case args do + {module, fun} -> [module, fun] + module -> [module, :__before_compile__] + end + + {:__block__, [], + [ + {:require, [], [hd(target)]}, + {{:., [], target}, [], [e_env]} + ]} + end + + module_callbacks = {:__block__, [], before_compile} + + {_result, state, _e_env} = expand(module_callbacks, state, e_env) state = state @@ -1504,8 +1523,9 @@ defmodule ElixirSense.Core.Compiler do |> remove_attributes_scope |> remove_module - # TODO hardcode expansion? - # to result of require (a module atom) and :elixir_module.compile dot call in block + # in elixir the result of defmodule expansion is + # require (a module atom) and :elixir_module.compile dot call in block + # we don't need that {{:__block__, [], []}, state, env} end diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 492c911e..6bfd8ffb 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -66,7 +66,8 @@ defmodule ElixirSense.Core.State do optional_callbacks_context: list(), lines_to_env: lines_to_env_t, cursor_env: nil | {keyword, ElixirSense.Core.State.Env.t()}, - ex_unit_describe: nil | atom + ex_unit_describe: nil | atom, + attribute_store: %{optional(module) => term} } defstruct attributes: [[]], @@ -98,7 +99,8 @@ defmodule ElixirSense.Core.State do optional_callbacks_context: [[]], lines_to_env: %{}, cursor_env: nil, - ex_unit_describe: nil + ex_unit_describe: nil, + attribute_store: %{} defmodule Env do @moduledoc """ @@ -515,8 +517,7 @@ defmodule ElixirSense.Core.State do doc, meta, options - ) - when is_tuple(position) do + ) do current_info = Map.get(state.mods_funs_to_positions, {module, fun, arity}, %ModFunInfo{}) current_params = current_info |> Map.get(:params, []) current_positions = current_info |> Map.get(:positions, []) @@ -980,10 +981,7 @@ defmodule ElixirSense.Core.State do def add_var_read(%__MODULE__{} = state, _), do: state - @builtin_attributes ElixirSense.Core.BuiltinAttributes.all() - - def add_attribute(%__MODULE__{} = state, attribute, type, is_definition, meta) - when attribute not in @builtin_attributes do + def add_attribute(%__MODULE__{} = state, env, attribute, meta, args, type, is_definition) do position = extract_position(meta) [attributes_from_scope | other_attributes] = state.attributes @@ -1023,11 +1021,47 @@ defmodule ElixirSense.Core.State do attributes = [attributes_from_scope | other_attributes] scope_attributes = [attributes_from_scope | tl(state.scope_attributes)] - %__MODULE__{state | attributes: attributes, scope_attributes: scope_attributes} - end - def add_attribute(%__MODULE__{} = state, _attribute, _type, _is_definition, _meta) do - state + # TODO handle other + # {moduledoc, nil, nil, []}, + # {after_compile, [], accumulate, []}, + # {after_verify, [], accumulate, []}, + # {before_compile, [], accumulate, []}, + # {behaviour, [], accumulate, []}, + # {compile, [], accumulate, []}, + # {derive, [], accumulate, []}, + # {dialyzer, [], accumulate, []}, + # {external_resource, [], accumulate, []}, + # {on_definition, [], accumulate, []}, + # {type, [], accumulate, []}, + # {opaque, [], accumulate, []}, + # {typep, [], accumulate, []}, + # {spec, [], accumulate, []}, + # {callback, [], accumulate, []}, + # {macrocallback, [], accumulate, []}, + # {optional_callbacks, [], accumulate, []}, + accumulating? = + attribute in [:before_compile, :after_compile, :after_verify, :on_definition, :on_load] + + attribute_store = + if is_definition do + [arg] = args + + if accumulating? do + state.attribute_store |> Map.update({env.module, attribute}, [arg], &(&1 ++ [arg])) + else + state.attribute_store |> Map.put({env.module, attribute}, arg) + end + else + state.attribute_store + end + + %__MODULE__{ + state + | attributes: attributes, + scope_attributes: scope_attributes, + attribute_store: attribute_store + } end def add_behaviour(module, %__MODULE__{} = state, env) when is_atom(module) do @@ -1173,8 +1207,13 @@ defmodule ElixirSense.Core.State do ] def add_module_functions(state, env, functions, range) do - {{line, column}, _} = range - meta = [line: line || 0] ++ if(column > 0, do: [column: column], else: []) + {line, column} = + case range do + {{line, column}, _} -> {line, column} + _ -> {0, nil} + end + + meta = [line: line] ++ if(column > 0, do: [column: column], else: []) (functions ++ @module_functions) |> Enum.reduce(state, fn {name, args, kind}, acc -> @@ -1197,7 +1236,12 @@ defmodule ElixirSense.Core.State do end def add_struct_or_exception(state, env, type, fields, range) do - {{line, column}, _} = range + {line, column} = + case range do + {{line, column}, _} -> {line, column} + _ -> {0, nil} + end + meta = [line: line || 0] ++ if(column > 0, do: [column: column], else: []) fields = diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 99ffe147..725d48b0 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -4245,26 +4245,34 @@ defmodule ElixirSense.Core.MetadataBuilderTest do defmacro __using__(_options) do quote do alias Some.First - IO.inspect({__ENV__.aliases, __ENV__.macro_aliases}) end end end - test "macro alias" do + test "macro alias does not leak outside macro" do state = """ defmodule MyModule do use ElixirSense.Core.MetadataBuilderTest.Macro.AliasTest.Definer use ElixirSense.Core.MetadataBuilderTest.Macro.AliasTest.Aliaser IO.puts "" - a = %MyModule.First{} - b = %MyModule.Second{} end """ |> string_to_state assert [{First, {_, Some.First}}] = state.lines_to_env[4].macro_aliases - # TODO should we handle @before_compile? + + assert %{ + MyModule.First => %StructInfo{ + fields: [foo: :bar, __struct__: MyModule.First] + }, + MyModule.Second => %StructInfo{ + fields: [ + baz: {:%, [], [MyModule.First, {:%{}, [], [{:foo, :bar}]}]}, + __struct__: MyModule.Second + ] + } + } = state.structs end end @@ -5434,7 +5442,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do use Application @behaviour SomeModule.SomeBehaviour IO.puts "" - defmodule InnerModuleWithUse do + defmodule InnerModuleWithUse1 do use GenServer IO.puts "" end @@ -5997,6 +6005,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ]) assert [ + %AttributeInfo{name: :before_compile, positions: [{2, _}]}, %AttributeInfo{name: :my_attribute, positions: [{2, _}]} ] = get_line_attributes(state, 4) @@ -7294,7 +7303,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do test "registers calls on ex_unit DSL" do state = """ - defmodule MyModuleTest do + defmodule MyModuleTests do use ExUnit.Case describe "describe1" do @@ -7664,7 +7673,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do test "gets ExUnit imports from `use ExUnit.Case`" do state = """ - defmodule MyTest do + defmodule MyModuleTest do use ExUnit.Case IO.puts "" end @@ -7678,7 +7687,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do test "gets ExUnit imports from case template" do state = """ - defmodule MyTest do + defmodule My1Test do use ElixirSenseExample.CaseTemplateExample IO.puts "" end @@ -8421,6 +8430,32 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert state end + describe "module callbacks" do + defmodule Callbacks do + defmacro __before_compile__(_arg) do + quote do + def constant, do: 1 + defoverridable constant: 0 + end + end + end + + test "before_compile" do + state = + """ + defmodule User do + @before_compile ElixirSense.Core.MetadataBuilderTest.Callbacks + end + """ + |> string_to_state + + assert %ModFunInfo{meta: %{overridable: true}} = + state.mods_funs_to_positions[{User, :constant, 0}] + end + + # TODO after_compile?, after_verify?, on_defined, on_load? + end + defp string_to_state(string) do string |> Code.string_to_quoted(columns: true, token_metadata: true) From 1be7aeee8381aaabade9f8615937a3206112de52 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 15 Aug 2024 00:58:59 +0200 Subject: [PATCH 140/235] address todos support pre 1.15 prematch --- lib/elixir_sense/core/compiler.ex | 17 +++---- lib/elixir_sense/core/metadata_builder.ex | 10 +++- test/elixir_sense/core/compiler_test.exs | 47 +++++++++++++++++-- .../core/metadata_builder_test.exs | 13 +++++ 4 files changed, 72 insertions(+), 15 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index d928e1ef..01fd3fd9 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -549,7 +549,7 @@ defmodule ElixirSense.Core.Compiler do end defp do_expand({name, meta, kind}, s, e) when is_atom(name) and is_atom(kind) do - %{vars: {read, _write}, unused: version, prematch: prematch} = s + %{vars: {read, _write}, prematch: prematch} = s pair = {name, var_context(meta, kind)} result = @@ -584,34 +584,31 @@ defmodule ElixirSense.Core.Compiler do {:ok, pair_version} -> var = {name, [{:version, pair_version} | meta], kind} s = add_var_read(s, var) - {var, %{s | unused: version}, e} + {var, s, e} error -> case Keyword.fetch(meta, :if_undefined) do {:ok, :apply} -> - # TODO check if this can happen + # convert to local call expand({name, meta, []}, s, e) # elixir plans to remove this clause on v2.0 {:ok, :raise} -> - # TODO is it worth registering var access - # function_error(meta, e, __MODULE__, {:undefined_var, name, kind}) + # elixir raises here undefined_var {{name, meta, kind}, s, e} # elixir plans to remove this clause on v2.0 _ when error == :warn -> - # TODO is it worth registering var access? + # convert to local call and add if_undefined meta expand({name, [{:if_undefined, :warn} | meta], []}, s, e) _ when error == :pin -> - # TODO is it worth registering var access - # function_error(meta, e, __MODULE__, {:undefined_var_pin, name, kind}) + # elixir raises here undefined_var_pin {{name, meta, kind}, s, e} _ -> - # TODO is it worth registering var access + # elixir raises here undefined_var span_meta = __MODULE__.Env.calculate_span(meta, name) - # function_error(span_meta, e, __MODULE__, {:undefined_var, name, kind}) {{name, span_meta, kind}, s, e} end end diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index b65af884..bccadd07 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -14,7 +14,15 @@ defmodule ElixirSense.Core.MetadataBuilder do """ @spec build(Macro.t()) :: State.t() def build(ast) do - {_ast, state, _env} = Compiler.expand(ast, %State{}, Compiler.env()) + {_ast, state, _env} = + Compiler.expand( + ast, + %State{ + # TODO remove default when we require elixir 1.15 + prematch: Code.get_compiler_option(:on_undefined_variable) || :warn + }, + Compiler.env() + ) state |> remove_attributes_scope diff --git a/test/elixir_sense/core/compiler_test.exs b/test/elixir_sense/core/compiler_test.exs index 854962e7..717c74d3 100644 --- a/test/elixir_sense/core/compiler_test.exs +++ b/test/elixir_sense/core/compiler_test.exs @@ -5,7 +5,6 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do alias ElixirSense.Core.State require Record - defp to_quoted!(string_or_ast, ast \\ false) defp to_quoted!(ast, true), do: ast defp to_quoted!(string, false), @@ -568,7 +567,7 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do end defmodule Overridable do - defmacro __using__(args) do + defmacro __using__(_args) do quote do def foo(a) do a @@ -625,6 +624,44 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do assert_expansion("a = 5; ^a = 6") end + test "expands nullary call if_undefined: :apply" do + ast = {:self, [if_undefined: :apply], nil} + {expanded, state, env} = Compiler.expand(ast, %State{}, Compiler.env()) + elixir_env = :elixir_env.new() + + {elixir_expanded, elixir_state, elixir_env} = + :elixir_expand.expand(ast, :elixir_env.env_to_ex(elixir_env), elixir_env) + + assert expanded == elixir_expanded + assert env == elixir_env + assert state_to_map(state) == elixir_ex_to_map(elixir_state) + end + + test "expands nullary call if_undefined: :warn" do + Code.put_compiler_option(:on_undefined_variable, :warn) + ast = {:self, [], nil} + + {expanded, state, env} = + Compiler.expand( + ast, + %State{ + prematch: Code.get_compiler_option(:on_undefined_variable) || :warn + }, + Compiler.env() + ) + + elixir_env = :elixir_env.new() + + {elixir_expanded, elixir_state, elixir_env} = + :elixir_expand.expand(ast, :elixir_env.env_to_ex(elixir_env), elixir_env) + + assert expanded == elixir_expanded + assert env == elixir_env + assert state_to_map(state) == elixir_ex_to_map(elixir_state) + after + Code.put_compiler_option(:on_undefined_variable, :raise) + end + test "expands local call" do assert_expansion("get_in(%{}, [:bar])") assert_expansion("length([])") @@ -788,7 +825,9 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do token_metadata: true ) - {expanded, state, env} = Compiler.expand(ast, %State{}, %{Compiler.env() | module: Foo}) + {_expanded, _state, _env} = + Compiler.expand(ast, %State{}, %{Compiler.env() | module: Foo}) + # elixir_env = %{:elixir_env.new() | module: Foo} # {elixir_expanded, _elixir_state, elixir_env} = :elixir_expand.expand(ast, :elixir_env.env_to_ex(elixir_env), elixir_env) @@ -850,7 +889,7 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do defp clean_capture_arg_elixir(ast) do {ast, _} = Macro.prewalk(ast, nil, fn - {:capture, meta, nil} = node, state -> + {:capture, meta, nil} = _node, state -> # elixir changes the name to capture and does different counter tracking meta = Keyword.delete(meta, :counter) {{:capture, meta, nil}, state} diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 725d48b0..9ee43003 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -135,6 +135,19 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] = state |> get_line_vars(3) end + test "pin undefined" do + state = + """ + ^abc = foo() + record_env() + """ + |> string_to_state + + refute Map.has_key?(state.lines_to_env[2].versioned_vars, {:abc, nil}) + + assert [] = state |> get_line_vars(3) + end + test "rebinding" do state = """ From abd85f18fd37174256a98b086b589e73e9b6ca91 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 15 Aug 2024 14:32:26 +0200 Subject: [PATCH 141/235] test macro expansion --- lib/elixir_sense/core/compiler.ex | 1 - .../core/metadata_builder_test.exs | 106 ++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 01fd3fd9..39898135 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -621,7 +621,6 @@ defmodule ElixirSense.Core.Compiler do # elixir checks here id fall is not ambiguous arity = length(args) - # TODO check if it works in our case # If we are inside a function, we support reading from locals. allow_locals = match?({n, a} when fun != n or arity != a, env.function) diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 9ee43003..4f0f28dc 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -5949,6 +5949,112 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert Map.has_key?(state.mods_funs_to_positions, {Reversible.My.List, :__impl__, 1}) end + describe "macro expansion" do + defmodule WithMacros do + IO.inspect(__ENV__.module) + + defmacro go do + quote do + def my_fun, do: :ok + end + end + end + + test "expands remote macro" do + state = + """ + defmodule SomeMod do + require ElixirSense.Core.MetadataBuilderTest.WithMacros, as: WithMacros + WithMacros.go() + end + """ + |> string_to_state + + assert %{{SomeMod, :my_fun, 0} => _} = state.mods_funs_to_positions + end + + test "expands remote imported macro" do + state = + """ + defmodule SomeMod do + import ElixirSense.Core.MetadataBuilderTest.WithMacros + go() + end + """ + |> string_to_state + + assert %{{SomeMod, :my_fun, 0} => _} = state.mods_funs_to_positions + end + + test "expands local macro" do + state = + """ + defmodule SomeMod do + defmacrop go do + quote do + self() + end + end + + def foo do + go() + end + end + """ + |> string_to_state + + assert %{{SomeMod, :my_fun, 0} => _} = state.calls + end + + defmodule SomeCompiledMod do + defmacro go do + quote do + self() + end + end + + defmacrop go_priv do + quote do + self() + end + end + end + + test "expands public local macro from compiled module" do + # NOTE we optimistically assume the previously compiled module version has + # the same macro implementation as the currently expanded + state = + """ + defmodule ElixirSense.Core.MetadataBuilderTest.SomeCompiledMod do + defmacro go do + quote do + self() + end + end + + defmacrop go_priv do + quote do + self() + end + end + + def foo do + go() + go_priv() + end + end + """ + |> string_to_state + + assert [%CallInfo{func: :self}, %CallInfo{func: :go}] = state.calls[15] + + # NOTE as of elixir 1.17 MacroEnv.expand_import relies on module.__info__(:macros) for locals + # this means only public local macros are returned in our case + # making it work for all locals would require hooking into :elixir_def and compiling the code + assert [%CallInfo{func: :go_priv}] = state.calls[16] + end + end + test "first_alias_positions" do state = """ From e3c74b11c257fcf61f27c1dd664c3e937c5762b3 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 15 Aug 2024 15:58:43 +0200 Subject: [PATCH 142/235] replace Macro calls with non tracing ones --- lib/elixir_sense/core/compiler.ex | 28 ++- lib/elixir_sense/core/compiler/macro.ex | 199 ++++++++++++++++++ .../core/metadata_builder_test.exs | 20 -- 3 files changed, 216 insertions(+), 31 deletions(-) create mode 100644 lib/elixir_sense/core/compiler/macro.ex diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 39898135..b061425a 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -94,7 +94,7 @@ defmodule ElixirSense.Core.Compiler do case NormalizedMacroEnv.expand_alias(env, meta, list, trace: false) do {:alias, alias} -> # TODO? - # A compiler may want to emit a :local_function trace in here. + # A compiler may want to emit a :alias_reference trace in here. # Elixir also warns on easy to confuse aliases, such as True/False/Nil. {alias, state, env} @@ -103,7 +103,7 @@ defmodule ElixirSense.Core.Compiler do if is_atom(head) do # TODO? - # A compiler may want to emit a :local_function trace in here. + # A compiler may want to emit a :alias_reference trace in here. {Module.concat([head | tail]), state, env} else # elixir raises here invalid_alias @@ -1358,9 +1358,13 @@ defmodule ElixirSense.Core.Compiler do raise ArgumentError, "defimpl/3 expects a :for option when declared outside a module" end) - # TODO elixir uses expand_literals here - {for, state, _env} = - expand(for, state, %{env | module: env.module || Elixir, function: {:__impl__, 1}}) + # TODO how to look for cursor in for? + for = + __MODULE__.Macro.expand_literals(for, %{ + env + | module: env.module || Elixir, + function: {:__impl__, 1} + }) {protocol, state, _env} = expand(name, state, env) @@ -2972,8 +2976,8 @@ defmodule ElixirSense.Core.Compiler do # rescue expr() => rescue expanded_expr() defp expand_rescue({_, meta, _} = arg, s, e) do - # TODO wut? - case Macro.expand_once(arg, %{e | line: ElixirUtils.get_line(meta)}) do + # TODO how to check for cursor here? + case ElixirExpand.Macro.expand_once(arg, %{e | line: ElixirUtils.get_line(meta)}) do ^arg -> # elixir rejects this case # try to recover from error by generating fake expression @@ -3250,8 +3254,8 @@ defmodule ElixirSense.Core.Compiler do h end - # TODO not call it here - case Macro.expand(ha, Map.put(e, :line, ElixirUtils.get_line(meta))) do + # TODO how to check for cursor here? + case ElixirExpand.Macro.expand(ha, Map.put(e, :line, ElixirUtils.get_line(meta))) do ^ha -> # elixir raises here undefined_bittype # we omit the spec @@ -4453,8 +4457,10 @@ defmodule ElixirSense.Core.Compiler do struct = load_struct(e_left, assocs, se, ee) keys = [:__struct__ | assoc_keys] without_keys = Elixir.Map.drop(struct, keys) - # TODO is escape safe? - struct_assocs = Macro.escape(Enum.sort(Elixir.Map.to_list(without_keys))) + + struct_assocs = + ElixirExpand.Macro.escape(Enum.sort(Elixir.Map.to_list(without_keys))) + {{:%, meta, [e_left, {:%{}, map_meta, struct_assocs ++ assocs}]}, se, ee} {_, _, _assocs} -> diff --git a/lib/elixir_sense/core/compiler/macro.ex b/lib/elixir_sense/core/compiler/macro.ex new file mode 100644 index 00000000..b10dd018 --- /dev/null +++ b/lib/elixir_sense/core/compiler/macro.ex @@ -0,0 +1,199 @@ +defmodule ElixirSense.Core.Compiler.Macro do + alias ElixirSense.Core.Normalized.Macro.Env, as: NormalizedMacroEnv + + @spec expand_literals(Macro.input(), Macro.Env.t()) :: Macro.output() + def expand_literals(ast, env) do + {ast, :ok} = expand_literals(ast, :ok, fn node, :ok -> {expand(node, env), :ok} end) + ast + end + + @spec expand_literals(Macro.t(), acc, (Macro.t(), acc -> {Macro.t(), acc})) :: Macro.t() + when acc: term() + def expand_literals(ast, acc, fun) + + def expand_literals({:__aliases__, meta, args}, acc, fun) do + {args, acc} = expand_literals(args, acc, fun) + + if :lists.all(&is_atom/1, args) do + fun.({:__aliases__, meta, args}, acc) + else + {{:__aliases__, meta, args}, acc} + end + end + + def expand_literals({:__MODULE__, _meta, ctx} = node, acc, fun) when is_atom(ctx) do + fun.(node, acc) + end + + def expand_literals({:%, meta, [left, right]}, acc, fun) do + {left, acc} = expand_literals(left, acc, fun) + {right, acc} = expand_literals(right, acc, fun) + {{:%, meta, [left, right]}, acc} + end + + def expand_literals({:%{}, meta, args}, acc, fun) do + {args, acc} = expand_literals(args, acc, fun) + {{:%{}, meta, args}, acc} + end + + def expand_literals({:{}, meta, args}, acc, fun) do + {args, acc} = expand_literals(args, acc, fun) + {{:{}, meta, args}, acc} + end + + def expand_literals({left, right}, acc, fun) do + {left, acc} = expand_literals(left, acc, fun) + {right, acc} = expand_literals(right, acc, fun) + {{left, right}, acc} + end + + def expand_literals(list, acc, fun) when is_list(list) do + :lists.mapfoldl(&expand_literals(&1, &2, fun), acc, list) + end + + def expand_literals( + {{:., _, [{:__aliases__, _, [:Application]}, :compile_env]} = node, meta, + [app, key, default]}, + acc, + fun + ) do + # TODO track call? + {default, acc} = expand_literals(default, acc, fun) + {{node, meta, [app, key, default]}, acc} + end + + def expand_literals(term, acc, _fun), do: {term, acc} + + @spec expand(Macro.input(), Macro.Env.t()) :: Macro.output() + def expand(ast, env) do + expand_until({ast, true}, env) + end + + defp expand_until({ast, true}, env) do + expand_until(do_expand_once(ast, env), env) + end + + defp expand_until({ast, false}, _env) do + ast + end + + @spec expand_once(Macro.input(), Macro.Env.t()) :: Macro.output() + def expand_once(ast, env) do + elem(do_expand_once(ast, env), 0) + end + + defp do_expand_once({:__aliases__, meta, [head | tail] = list} = alias, env) do + case NormalizedMacroEnv.expand_alias(env, meta, list, trace: false) do + {:alias, alias} -> + # TODO? + # A compiler may want to emit a :alias_reference trace in here. + {alias, true} + + :error -> + {head, _} = do_expand_once(head, env) + + if is_atom(head) do + receiver = Module.concat([head | tail]) + # TODO? + # A compiler may want to emit a :alias_reference trace in here. + {receiver, true} + else + {alias, false} + end + end + end + + # Expand compilation environment macros + defp do_expand_once({:__MODULE__, _, atom}, env) when is_atom(atom), do: {env.module, true} + + defp do_expand_once({:__DIR__, _, atom}, env) when is_atom(atom), + do: {:filename.dirname(env.file), true} + + defp do_expand_once({:__ENV__, _, atom}, env) when is_atom(atom) do + env = update_in(env.versioned_vars, &maybe_escape_map/1) + {maybe_escape_map(env), true} + end + + defp do_expand_once({{:., _, [{:__ENV__, _, atom}, field]}, _, []} = original, env) + when is_atom(atom) and is_atom(field) do + if Map.has_key?(env, field) do + {maybe_escape_map(Map.get(env, field)), true} + else + {original, false} + end + end + + defp do_expand_once({name, meta, context} = original, _env) + when is_atom(name) and is_list(meta) and is_atom(context) do + {original, false} + end + + defp do_expand_once({name, meta, args} = original, env) + when is_atom(name) and is_list(args) and is_list(meta) do + arity = length(args) + + case Macro.Env.expand_import(env, meta, name, arity, trace: false, check_deprecations: false) do + {:macro, _receiver, expander} -> + # TODO register call + # We don't want the line to propagate yet, but generated might! + {expander.(Keyword.take(meta, [:generated]), args), true} + + {:function, Kernel, op} when op in [:+, :-] and arity == 1 -> + case expand_once(hd(args), env) do + integer when is_integer(integer) -> + # TODO register call + {apply(Kernel, op, [integer]), true} + + _ -> + {original, false} + end + + {:function, _receiver, _name} -> + {original, false} + + {:error, :not_found} -> + {original, false} + + {:error, _other} -> + # elixir raises elixir_dispatch here + {original, false} + end + end + + # Expand possible macro require invocation + defp do_expand_once({{:., _, [left, name]}, meta, args} = original, env) when is_atom(name) do + {receiver, _} = do_expand_once(left, env) + + case is_atom(receiver) do + false -> + {original, false} + + true -> + case Macro.Env.expand_require(env, meta, receiver, name, length(args), + trace: false, + check_deprecations: false + ) do + {:macro, _receiver, expander} -> + # TODO register call + # We don't want the line to propagate yet, but generated might! + {expander.(Keyword.take(meta, [:generated]), args), true} + + :error -> + {original, false} + end + end + end + + # Anything else is just returned + defp do_expand_once(other, _env), do: {other, false} + + defp maybe_escape_map(map) when is_map(map), do: {:%{}, [], Map.to_list(map)} + defp maybe_escape_map(other), do: other + + @spec escape(term, keyword) :: Macro.t() + def escape(expr, opts \\ []) do + unquote = Keyword.get(opts, :unquote, false) + kind = if Keyword.get(opts, :prune_metadata, false), do: :prune_metadata, else: :none + ElixirSense.Core.Compiler.Quote.escape(expr, kind, unquote) + end +end diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 4f0f28dc..2ef3628d 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -5986,26 +5986,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert %{{SomeMod, :my_fun, 0} => _} = state.mods_funs_to_positions end - test "expands local macro" do - state = - """ - defmodule SomeMod do - defmacrop go do - quote do - self() - end - end - - def foo do - go() - end - end - """ - |> string_to_state - - assert %{{SomeMod, :my_fun, 0} => _} = state.calls - end - defmodule SomeCompiledMod do defmacro go do quote do From c2c0457accdf763c1e7dd6e9518664e508028852 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 18 Aug 2024 09:06:05 +0200 Subject: [PATCH 143/235] improvements and test coverage for guard type inference --- lib/elixir_sense/core/compiler.ex | 7 +- lib/elixir_sense/core/state.ex | 11 +- .../core/{ => type_inference}/guard.ex | 125 ++++--- .../core/metadata_builder_test.exs | 349 +++++++++++++++--- .../core/type_inference/guard_test.exs | 283 ++++++++++++++ 5 files changed, 679 insertions(+), 96 deletions(-) rename lib/elixir_sense/core/{ => type_inference}/guard.ex (67%) create mode 100644 test/elixir_sense/core/type_inference/guard_test.exs diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index b061425a..b4b04772 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -5,7 +5,7 @@ defmodule ElixirSense.Core.Compiler do alias ElixirSense.Core.Introspection alias ElixirSense.Core.TypeInfo alias ElixirSense.Core.TypeInference - alias ElixirSense.Core.Guard + alias ElixirSense.Core.TypeInference.Guard alias ElixirSense.Core.Normalized.Macro.Env, as: NormalizedMacroEnv @env :elixir_env.new() @@ -2130,6 +2130,7 @@ defmodule ElixirSense.Core.Compiler do # elixir checks here that clause has exactly 1 arg by matching against {_, _, [[_], _]} # we drop excessive or generate a fake arg + # TODO this is invalid for guards {args, discarded_args} = case args do [] -> @@ -2578,8 +2579,6 @@ defmodule ElixirSense.Core.Compiler do e ) - # TODO infer type from guard here - {[{:when, meta, e_args ++ [e_guard]}], sg, eg} end @@ -3394,7 +3393,7 @@ defmodule ElixirSense.Core.Compiler do new_pre = {pre_read, pre_counter, {:bitsize, original_read}} {e_expr, se, ee} = - ElixirExpand.expand(expr, %{s | prematch: new_pre}, %{e | context: :guard}) + ElixirExpand.expand(expr |> dbg, %{s | prematch: new_pre}, %{e | context: :guard}) {e_expr, %{se | prematch: old_pre}, %{ee | context: :match}} end diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 6bfd8ffb..de8aba0f 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -1394,7 +1394,16 @@ defmodule ElixirSense.Core.State do for {key, type} <- inferred_types, reduce: h do acc -> Map.update!(acc, key, fn %VarInfo{type: old} = v -> - %{v | type: ElixirSense.Core.TypeInference.intersect(old, type)} + {variable, version} = key + # TODO detect more complicated self referential cases + case type do + {:variable, ^variable, ^version} -> + # self referential type + v + + _ -> + %{v | type: ElixirSense.Core.TypeInference.intersect(old, type)} + end end) end diff --git a/lib/elixir_sense/core/guard.ex b/lib/elixir_sense/core/type_inference/guard.ex similarity index 67% rename from lib/elixir_sense/core/guard.ex rename to lib/elixir_sense/core/type_inference/guard.ex index 4544cba5..9f6c4441 100644 --- a/lib/elixir_sense/core/guard.ex +++ b/lib/elixir_sense/core/type_inference/guard.ex @@ -1,5 +1,6 @@ -defmodule ElixirSense.Core.Guard do - # TODO add tests +defmodule ElixirSense.Core.TypeInference.Guard do + alias ElixirSense.Core.TypeInference + @moduledoc """ This module is responsible for infer type information from guard expressions """ @@ -9,6 +10,13 @@ defmodule ElixirSense.Core.Guard do # / \ or / \ or :not guard_expr or guard_expr or list(guard_expr) # guard_expr guard_expr guard_expr guard_expr # + def type_information_from_guards({:when, meta, [left, right]}) do + # treat nested guard as or expression + # this is not valid only in case of raising expressions in guard + # but it doesn't matter in our case we are not evaluating + type_information_from_guards({{:., meta, [:erlang, :orelse]}, meta, [left, right]}) + end + def type_information_from_guards(list) when is_list(list) do for expr <- list, reduce: %{} do acc -> @@ -34,43 +42,62 @@ defmodule ElixirSense.Core.Guard do left = type_information_from_guards(guard_l) right = type_information_from_guards(guard_r) - Map.merge(left, right, fn _k, v1, v2 -> {:intersection, [v1, v2]} end) + Map.merge(left, right, fn _k, v1, v2 -> + TypeInference.intersect(v1, v2) + end) end def type_information_from_guards({{:., _, [:erlang, :orelse]}, _, [guard_l, guard_r]}) do left = type_information_from_guards(guard_l) right = type_information_from_guards(guard_r) - Map.merge(left, right, fn _k, v1, v2 -> - case {v1, v2} do - {{:union, types_1}, {:union, types_2}} -> {:union, types_1 ++ types_2} - {{:union, types}, _} -> {:union, types ++ [v2]} - {_, {:union, types}} -> {:union, [v1 | types]} - _ -> {:union, [v1, v2]} - end + merged_keys = (Map.keys(left) ++ Map.keys(right)) |> Enum.uniq() + + Enum.reduce(merged_keys, %{}, fn key, acc -> + v1 = Map.get(left, key) + v2 = Map.get(right, key) + + # we can union types only if both sides constrain the same variable + # otherwise, it's not possible to infer type information from guard expression + # e.g. is_integer(x) or is_atom(x) can be unionized + # is_integer(x) or is_atom(y) cannot + + new_value = + case {v1, v2} do + {nil, nil} -> nil + {nil, _} -> nil + {_, nil} -> nil + {{:union, types_1}, {:union, types_2}} -> {:union, types_1 ++ types_2} + {{:union, types}, other} -> {:union, types ++ [other]} + {other, {:union, types}} -> {:union, [other | types]} + {other1, other2} -> {:union, [other1, other2]} + end + + Map.put(acc, key, new_value) end) end + # Standalone variable: func my_func(x) when x + def type_information_from_guards({var, meta, context}) when is_atom(var) and is_atom(context) do + case Keyword.fetch(meta, :version) do + {:ok, version} -> + %{{var, version} => :boolean} + + _ -> + %{} + end + end + def type_information_from_guards(guard_ast) do {_, acc} = Macro.prewalk(guard_ast, %{}, fn - # Standalone variable: func my_func(x) when x - {var, meta, context} = node, acc when is_atom(var) and is_atom(context) -> - case Keyword.fetch(meta, :version) do - {:ok, version} -> - {node, Map.put(acc, {var, version}, :boolean)} - - _ -> - {node, acc} - end - {{:., _dot_meta, [:erlang, fun]}, _call_meta, params}, acc when is_atom(fun) and is_list(params) -> with {type, binding} <- guard_predicate_type(fun, params), {var, meta, context} when is_atom(var) and is_atom(context) <- binding, {:ok, version} <- Keyword.fetch(meta, :version) do # If we found the predicate type, we can prematurely exit traversing the subtree - {[], Map.put(acc, {var, version}, type)} + {nil, Map.put(acc, {var, version}, type)} else _ -> # traverse params @@ -119,17 +146,9 @@ defmodule ElixirSense.Core.Guard do # when tl(x) == [2] defp guard_predicate_type(p, [{{:., _, [:erlang, guard]}, _, [first | _]}, rhs]) when p in [:==, :===, :>=, :>, :<=, :<] and guard in [:hd, :tl] do - rhs_type = - cond do - is_number(rhs) -> :number - is_binary(rhs) -> :binary - is_bitstring(rhs) -> :bitstring - is_atom(rhs) -> :atom - is_boolean(rhs) -> :boolean - true -> nil - end + rhs_type = type_of(rhs) - rhs_type = if guard == :hd and rhs_type, do: {:list, rhs_type}, else: :list + rhs_type = if guard == :hd and rhs_type != nil, do: {:list, rhs_type}, else: :list {rhs_type, first} end @@ -176,17 +195,12 @@ defmodule ElixirSense.Core.Guard do is_atom(key) or is_binary(key) -> # TODO other types of keys? - rhs_type = - cond do - is_number(value) -> {:number, value} - is_binary(value) -> :binary - is_bitstring(value) -> :bitstring - is_atom(value) -> {:atom, value} - is_boolean(value) -> :boolean - true -> nil - end + rhs_type = type_of(value) {:map, [{key, rhs_type}], nil} + + true -> + {:map, [], nil} end {type, second} @@ -197,18 +211,31 @@ defmodule ElixirSense.Core.Guard do guard_predicate_type(p, [call, value]) end + defp guard_predicate_type(p, [{variable_l, _, context_l}, {variable_r, _, context_r}]) + when p in [:==, :===] and is_atom(variable_l) and is_atom(context_l) and + is_atom(variable_r) and is_atom(context_r), + do: nil + + defp guard_predicate_type(p, [{variable, _, context} = lhs, value]) + when p in [:==, :===] and is_atom(variable) and is_atom(context) do + {type_of(value), lhs} + end + + defp guard_predicate_type(p, [value, {variable, _, context} = rhs]) + when p in [:==, :===] and is_atom(variable) and is_atom(context) do + guard_predicate_type(p, [rhs, value]) + end + defp guard_predicate_type(:is_map, [first | _]), do: {{:map, [], nil}, first} defp guard_predicate_type(:is_non_struct_map, [first | _]), do: {{:map, [], nil}, first} defp guard_predicate_type(:map_size, [first | _]), do: {{:map, [], nil}, first} - # TODO macro defp guard_predicate_type(:is_map_key, [key, var | _]) do # TODO other types of keys? type = case key do :__struct__ -> {:struct, [], nil, nil} - key when is_atom(key) -> {:map, [{key, nil}], nil} - key when is_binary(key) -> {:map, [{key, nil}], nil} + key when is_atom(key) when is_binary(key) -> {:map, [{key, nil}], nil} _ -> {:map, [], nil} end @@ -220,8 +247,7 @@ defmodule ElixirSense.Core.Guard do type = case key do :__struct__ -> {:struct, [], nil, nil} - key when is_atom(key) -> {:map, [{key, nil}], nil} - key when is_binary(key) -> {:map, [{key, nil}], nil} + key when is_atom(key) when is_binary(key) -> {:map, [{key, nil}], nil} _ -> {:map, [], nil} end @@ -232,7 +258,10 @@ defmodule ElixirSense.Core.Guard do defp guard_predicate_type(:is_boolean, [first | _]), do: {:boolean, first} defp guard_predicate_type(_, _), do: nil -end -# :in :is_function/1-2 :is_nil :is_pid :is_port :is_reference :node/0-1 -# :self + defp type_of(expression) do + TypeInference.type_of(expression, :guard) + end + + # TODO :in :is_function/1-2 :is_pid :is_port :is_reference :node/0-1 :self +end diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 2ef3628d..edcb5f1b 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -3438,6 +3438,285 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> hd() end + test "guards in case clauses" do + buffer = """ + defmodule MyModule do + def func(x) do + IO.puts "" + case x do + {a, b} when is_nil(a) and is_integer(b) -> + IO.puts "" + _ when is_integer(x) -> + IO.puts "" + end + IO.puts "" + end + end + """ + + state = string_to_state(buffer) + + assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 3) + + assert [ + %VarInfo{ + name: :a, + type: {:intersection, [{:atom, nil}, {:tuple_nth, {:variable, :x, 0}, 0}]} + }, + %VarInfo{ + name: :b, + type: {:intersection, [:number, {:tuple_nth, {:variable, :x, 0}, 1}]} + }, + %VarInfo{name: :x, type: nil} + ] = get_line_vars(state, 6) + + assert [%VarInfo{name: :x, type: :number}] = get_line_vars(state, 8) + + # TODO this type should not leak outside clause + # assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 10) + assert [%VarInfo{name: :x, type: :number}] = + get_line_vars(state, 10) + end + + test "guards in with clauses" do + buffer = """ + defmodule MyModule do + def func(x) do + IO.puts "" + with {a, b} when is_nil(a) and is_integer(b) <- x do + IO.puts "" + else + {:error, e} when is_atom(e) -> + IO.puts "" + _ when is_integer(x) -> + IO.puts "" + end + IO.puts "" + end + end + """ + + state = string_to_state(buffer) + + assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 3) + + assert [ + %VarInfo{ + name: :a, + type: {:intersection, [{:atom, nil}, {:tuple_nth, {:variable, :x, 0}, 0}]} + }, + %VarInfo{ + name: :b, + type: {:intersection, [:number, {:tuple_nth, {:variable, :x, 0}, 1}]} + }, + %VarInfo{name: :x, type: nil} + ] = get_line_vars(state, 5) + + assert [%VarInfo{name: :e, type: :atom}, %VarInfo{name: :x, type: nil}] = + get_line_vars(state, 8) + + assert [%VarInfo{name: :x, type: :number}] = get_line_vars(state, 10) + # TODO this type should not leak outside clause + # assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 12) + assert [%VarInfo{name: :x, type: :number}] = get_line_vars(state, 12) + end + + test "guards in receive clauses" do + buffer = """ + defmodule MyModule do + def func(x) do + IO.puts "" + receive do + {a, b} when is_nil(a) and is_integer(b) -> + IO.puts "" + _ when is_integer(x) -> + IO.puts "" + end + IO.puts "" + end + end + """ + + state = string_to_state(buffer) + + assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 3) + + assert [ + %VarInfo{name: :a, type: {:atom, nil}}, + %VarInfo{name: :b, type: :number}, + %VarInfo{name: :x, type: nil} + ] = get_line_vars(state, 6) + + assert [%VarInfo{name: :x, type: :number}] = get_line_vars(state, 8) + # TODO this type should not leak outside clause + # assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 10) + assert [%VarInfo{name: :x, type: :number}] = get_line_vars(state, 10) + end + + test "guards in for generator clauses" do + buffer = """ + defmodule MyModule do + def func(x) do + IO.puts "" + for {a, b} when is_nil(a) and is_integer(b) <- x, y when is_integer(x) <- a do + IO.puts "" + end + IO.puts "" + end + end + """ + + state = string_to_state(buffer) + + assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 3) + + assert [ + %VarInfo{ + name: :a, + type: + {:intersection, + [{:atom, nil}, {:tuple_nth, {:for_expression, {:variable, :x, 0}}, 0}]} + }, + %VarInfo{ + name: :b, + type: + {:intersection, + [:number, {:tuple_nth, {:for_expression, {:variable, :x, 0}}, 1}]} + }, + %VarInfo{name: :x, type: :number}, + %VarInfo{name: :y, type: {:for_expression, {:variable, :a, 1}}} + ] = get_line_vars(state, 5) + + # TODO this type should not leak outside clause + # assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 7) + assert [%VarInfo{name: :x, type: :number}] = get_line_vars(state, 7) + end + + test "guards in for aggregate clauses" do + buffer = """ + defmodule MyModule do + def func(x) do + IO.puts "" + for a <- x, reduce: %{} do + b when is_integer(b) -> + IO.puts "" + c when is_atom(c) -> + IO.puts "" + _ when is_integer(x) -> + IO.puts "" + end + IO.puts "" + end + end + """ + + state = string_to_state(buffer) + + assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 3) + + assert [ + %VarInfo{name: :a, type: {:for_expression, {:variable, :x, 0}}}, + %VarInfo{name: :b, type: :number}, + %VarInfo{name: :x, type: nil} + ] = get_line_vars(state, 6) + + assert [ + %VarInfo{name: :a, type: {:for_expression, {:variable, :x, 0}}}, + %VarInfo{name: :c, type: :atom}, + %VarInfo{name: :x, type: nil} + ] = get_line_vars(state, 8) + + assert [ + %VarInfo{name: :a, type: {:for_expression, {:variable, :x, 0}}}, + %VarInfo{name: :x, type: :number} + ] = get_line_vars(state, 10) + + # TODO this type should not leak outside clause + # assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 12) + assert [%VarInfo{name: :x, type: :number}] = get_line_vars(state, 12) + end + + test "guards in try clauses" do + buffer = """ + defmodule MyModule do + def func(x) do + IO.puts "" + try do + foo() + catch + a, b when is_nil(a) and is_integer(b) -> + IO.puts "" + else + c when is_nil(c) when is_binary(c) -> + IO.puts "" + _ when is_integer(x) -> + IO.puts "" + end + IO.puts "" + end + end + """ + + state = string_to_state(buffer) + + assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 3) + + assert [ + %VarInfo{name: :a, type: {:atom, nil}}, + %VarInfo{name: :b, type: :number}, + %VarInfo{name: :x, type: nil} + ] = get_line_vars(state, 8) + + assert [ + %VarInfo{name: :c, type: {:union, [{:atom, nil}, :binary]}}, + %VarInfo{name: :x, type: nil} + ] = get_line_vars(state, 11) + + assert [%VarInfo{name: :x, type: :number}] = get_line_vars(state, 13) + # TODO this type should not leak outside clause + # assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 15) + assert [%VarInfo{name: :x, type: :number}] = get_line_vars(state, 15) + end + + test "guards in fn clauses" do + buffer = """ + defmodule MyModule do + def func(x) do + IO.puts "" + fn + a, b when is_nil(a) and is_integer(b) -> + IO.puts "" + c, _ when is_nil(c) when is_binary(c) -> + IO.puts "" + _, _ when is_integer(x) -> + IO.puts "" + end + IO.puts "" + end + end + """ + + state = string_to_state(buffer) + + assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 3) + + assert [ + %VarInfo{name: :a, type: {:atom, nil}}, + %VarInfo{name: :b, type: :number}, + %VarInfo{name: :x, type: nil} + ] = get_line_vars(state, 6) + + assert [ + %VarInfo{name: :c, type: {:union, [{:atom, nil}, :binary]}}, + %VarInfo{name: :x, type: nil} + ] = get_line_vars(state, 8) + + assert [%VarInfo{name: :x, type: :number}] = get_line_vars(state, 10) + # TODO this type should not leak outside clause + # assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 12) + assert [%VarInfo{name: :x, type: :number}] = get_line_vars(state, 12) + end + test "number guards" do assert %VarInfo{name: :x, type: :number} = var_with_guards("is_number(x)") assert %VarInfo{name: :x, type: :number} = var_with_guards("is_float(x)") @@ -3471,8 +3750,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do test "list guards" do assert %VarInfo{name: :x, type: :list} = var_with_guards("is_list(x)") - assert %VarInfo{name: :x, type: {:list, :number}} = var_with_guards("hd(x) == 1") - assert %VarInfo{name: :x, type: {:list, :number}} = var_with_guards("1 == hd(x)") + assert %VarInfo{name: :x, type: {:list, {:integer, 1}}} = var_with_guards("hd(x) == 1") + assert %VarInfo{name: :x, type: {:list, {:integer, 1}}} = var_with_guards("1 == hd(x)") assert %VarInfo{name: :x, type: :list} = var_with_guards("tl(x) == [1]") assert %VarInfo{name: :x, type: :list} = var_with_guards("length(x) == 1") assert %VarInfo{name: :x, type: :list} = var_with_guards("1 == length(x)") @@ -3502,7 +3781,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do test "map guards" do assert %VarInfo{name: :x, type: {:map, [], nil}} = var_with_guards("is_map(x)") - assert %VarInfo{name: :x, type: {:intersection, [{:map, [], nil}, nil]}} = + assert %VarInfo{name: :x, type: {:map, [], nil}} = var_with_guards("is_non_struct_map(x)") assert %VarInfo{name: :x, type: {:map, [], nil}} = var_with_guards("map_size(x) == 1") @@ -3521,8 +3800,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do type: { :intersection, [ - {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, - {:struct, [], nil, nil} + {:struct, [], nil, nil}, + {:map, [], nil} ] } } = var_with_guards("is_struct(x)") @@ -3532,8 +3811,9 @@ defmodule ElixirSense.Core.MetadataBuilderTest do type: { :intersection, [ - {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, - {:struct, [], {:atom, URI}, nil} + {:struct, [], {:atom, URI}, nil}, + {:map, [], nil}, + {:struct, [], nil, nil} ] } } = @@ -3544,8 +3824,9 @@ defmodule ElixirSense.Core.MetadataBuilderTest do type: { :intersection, [ - {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, - {:struct, [], {:atom, URI}, nil} + {:struct, [], {:atom, URI}, nil}, + {:map, [], nil}, + {:struct, [], nil, nil} ] } } = @@ -3569,20 +3850,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do type: { :intersection, [ - { - :intersection, - [ - { - :intersection, - [ - {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, - {:struct, [], nil, nil} - ] - }, - {:map, [{:__exception__, nil}], nil} - ] - }, - {:map, [{:__exception__, {:atom, true}}], nil} + {:map, [{:__exception__, {:atom, true}}], nil}, + {:map, [{:__exception__, nil}], nil}, + {:struct, [], nil, nil}, + {:map, [], nil} ] } } = var_with_guards("is_exception(x)") @@ -3592,20 +3863,11 @@ defmodule ElixirSense.Core.MetadataBuilderTest do type: { :intersection, [ - { - :intersection, - [ - { - :intersection, - [ - {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, - {:struct, [], {:atom, ArgumentError}, nil} - ] - }, - {:map, [{:__exception__, nil}], nil} - ] - }, - {:map, [{:__exception__, {:atom, true}}], nil} + {:map, [{:__exception__, {:atom, true}}], nil}, + {:map, [{:__exception__, nil}], nil}, + {:struct, [], {:atom, ArgumentError}, nil}, + {:map, [], nil}, + {:struct, [], nil, nil} ] } } = @@ -3616,8 +3878,9 @@ defmodule ElixirSense.Core.MetadataBuilderTest do type: { :intersection, [ - {:intersection, [{:map, [], nil}, {:struct, [], nil, nil}]}, - {:struct, [], {:atom, ArgumentError}, nil} + {:struct, [], {:atom, ArgumentError}, nil}, + {:map, [], nil}, + {:struct, [], nil, nil} ] } } = @@ -3635,8 +3898,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> hd() end - test "and combination predicate guards can be merge" do - assert %VarInfo{name: :x, type: {:intersection, [:number, :boolean]}} = + test "and combination predicate guards can be merged" do + assert %VarInfo{name: :x, type: :number} = var_with_guards("is_number(x) and x >= 1") assert %VarInfo{ @@ -3657,10 +3920,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert %VarInfo{name: :x, type: nil} = var_with_guards("not is_map(x)") - assert %VarInfo{name: :x, type: {:union, [nil, :atom]}} = + assert %VarInfo{name: :x, type: nil} = var_with_guards("not is_map(x) or is_atom(x)") - assert %VarInfo{name: :x, type: {:intersection, [nil, :atom]}} = + assert %VarInfo{name: :x, type: :atom} = var_with_guards("not is_map(x) and is_atom(x)") end end diff --git a/test/elixir_sense/core/type_inference/guard_test.exs b/test/elixir_sense/core/type_inference/guard_test.exs new file mode 100644 index 00000000..2aaefd01 --- /dev/null +++ b/test/elixir_sense/core/type_inference/guard_test.exs @@ -0,0 +1,283 @@ +defmodule ElixirSense.Core.TypeInference.GuardTest do + use ExUnit.Case, async: true + alias ElixirSense.Core.TypeInference.Guard + + defp wrap(guard) do + {_ast, vars} = + Macro.prewalk(guard, [], fn + {atom, _meta, var_context} = node, acc when is_atom(atom) and is_atom(var_context) -> + {node, [node | acc]} + + node, acc -> + {node, acc} + end) + + vars = + case Enum.uniq(vars) do + [var] -> var + list -> list + end + + {:fn, [], + [ + {:->, [], + [ + [ + {:when, [], + [ + vars, + guard + ]} + ], + :ok + ]} + ]} + end + + defp unwrap( + {:fn, [], + [ + {:->, [], + [ + [ + {:when, _, [_, guard]} + ], + _ + ]} + ]} + ) do + guard + end + + defp expand(ast) do + ast = wrap(ast) + env = :elixir_env.new() + {ast, _, _} = :elixir_expand.expand(ast, :elixir_env.env_to_ex(env), env) + unwrap(ast) + end + + describe "type_information_from_guards/1" do + test "infers type from naked var" do + guard_expr = quote(do: x) |> expand() + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => :boolean} + end + + # 1. Simple guards + test "infers type from simple guard: is_number/1" do + guard_expr = quote(do: is_number(x)) |> expand() + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => :number} + end + + test "infers type from simple guard: is_binary/1" do + guard_expr = quote(do: is_binary(x)) |> expand() + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => :binary} + end + + test "infers type from simple guard: is_atom/1" do + guard_expr = quote(do: is_atom(x)) |> expand() + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => :atom} + end + + test "infers type from simple guard: is_nil/1" do + guard_expr = quote(do: is_nil(x)) |> expand() + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => {:atom, nil}} + end + + test "infers type from simple guard: == integer" do + guard_expr = quote(do: x == 5) |> expand() + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => {:integer, 5}} + end + + test "infers type from simple guard: == atom" do + guard_expr = quote(do: x == :foo) |> expand() + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => {:atom, :foo}} + end + + test "infers type from simple guard: == alias" do + guard_expr = quote(do: x == Some.Mod) |> expand() + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => {:atom, Some.Mod}} + end + + test "infers type from simple guard: == list empty" do + guard_expr = quote(do: x == []) |> expand() + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => {:list, :empty}} + end + + test "infers type from simple guard: == list" do + guard_expr = quote(do: x == [1]) |> expand() + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => {:list, {:integer, 1}}} + end + + test "infers type from simple guard: == map" do + guard_expr = quote(do: x == %{a: :b}) |> expand() + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => {:map, [a: {:atom, :b}], nil}} + end + + test "infers type from simple guard: == tuple empty" do + guard_expr = quote(do: x == {}) |> expand() + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => {:tuple, 0, []}} + end + + # 2. Guards with and + test "infers type from guard with and: is_number/1 and is_atom/1" do + guard_expr = quote(do: is_number(x) and is_atom(x)) |> expand() + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => {:intersection, [:number, :atom]}} + end + + # 3. Guards with or + test "infers type from guard with or: is_number/1 or is_binary/1" do + guard_expr = quote(do: is_number(x) or is_binary(x)) |> expand() + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => {:union, [:number, :binary]}} + end + + # 4. Guards with tuples + test "infers type from guard with tuple: is_tuple/1" do + guard_expr = quote(do: is_tuple(x)) |> expand + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => :tuple} + end + + test "infers type from guard with tuple_size/1" do + guard_expr = quote(do: tuple_size(x) == 2) |> expand + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => {:tuple, 2, [nil, nil]}} + end + + # 5. Guards with lists + test "infers type from guard with list: is_list/1" do + guard_expr = quote(do: is_list(x)) |> expand + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => :list} + end + + test "infers type from guard with list: hd/1 and tl/1" do + guard_expr = quote(do: hd(x) == 1 and tl(x) == [2]) |> expand + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => {:intersection, [{:list, {:integer, 1}}, :list]}} + end + + # 6. Guards with structs + test "infers type from guard with struct: is_map/1 and map_get/2" do + guard_expr = quote(do: is_struct(x, MyStruct)) |> expand() + result = Guard.type_information_from_guards(guard_expr) + + assert result == %{ + {:x, 0} => { + :intersection, + [ + {:struct, [], {:atom, MyStruct}, nil}, + {:map, [], nil}, + {:struct, [], nil, nil} + ] + } + } + end + + test "infers type from guard with struct: is_map_key/2" do + guard_expr = quote(do: is_map_key(x, :key)) |> expand() + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => {:map, [{:key, nil}], nil}} + end + end + + describe "type_information_from_guards not" do + test "handles not guard" do + guard_expr = quote(do: not is_number(x)) |> expand() + result = Guard.type_information_from_guards(guard_expr) + assert result == %{{:x, 0} => nil} + end + + # for simplicity we do not traverse not guards in the guard tree + # this should return :number type + test "handles nested not guards" do + guard = quote(do: not not is_number(x)) |> expand() + result = Guard.type_information_from_guards(guard) + assert result == %{{:x, 0} => nil} + end + + test "handles multiple variables in not guard" do + guard = quote(do: not (is_integer(x) and is_atom(y))) |> expand() + result = Guard.type_information_from_guards(guard) + assert result == %{{:x, 1} => nil, {:y, 0} => nil} + end + end + + describe "type_information_from_guards and" do + test "handles and with two guards" do + guard = quote(do: is_number(x) and is_atom(x)) |> expand() + + result = Guard.type_information_from_guards(guard) + + assert result == %{ + {:x, 0} => {:intersection, [:number, :atom]} + } + end + + test "handles nested and guards" do + guard = quote(do: is_number(x) and is_atom(x) and is_nil(x)) |> expand() + + result = Guard.type_information_from_guards(guard) + + assert result == %{{:x, 0} => {:intersection, [{:atom, nil}, :number, :atom]}} + end + + test "handles and with different variables" do + guard = quote(do: is_integer(x) and is_binary(y)) |> expand() + + result = Guard.type_information_from_guards(guard) + + assert result == %{{:x, 1} => :number, {:y, 0} => :binary} + end + end + + describe "type_information_from_guards or" do + test "handles or with simple types" do + guard = quote(do: is_integer(x) or is_binary(x)) |> expand() + + result = Guard.type_information_from_guards(guard) + assert result == %{{:x, 0} => {:union, [:number, :binary]}} + end + + test "handles nested or" do + guard = quote(do: is_number(x) or is_atom(x) or is_nil(x)) |> expand() + + result = Guard.type_information_from_guards(guard) + assert result == %{{:x, 0} => {:union, [:number, :atom, {:atom, nil}]}} + end + + test "handles or with different variables" do + guard = quote(do: is_integer(x) or is_binary(y)) |> expand() + + result = Guard.type_information_from_guards(guard) + assert result == %{{:x, 1} => nil, {:y, 0} => nil} + end + + test "handles or with existing unions" do + guard = quote(do: is_number(x) or is_atom(x) or (is_nil(x) or x)) |> expand() + + result = Guard.type_information_from_guards(guard) + assert result == %{{:x, 0} => {:union, [:number, :atom, {:atom, nil}, :boolean]}} + end + + test "handles nested when" do + guard = quote(do: is_integer(x) when is_binary(x)) |> expand() + + result = Guard.type_information_from_guards(guard) + assert result == %{{:x, 0} => {:union, [:number, :binary]}} + end + end +end From dd566b7667caccac4bc0dcc6976c981b7e2895ea Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 18 Aug 2024 09:06:22 +0200 Subject: [PATCH 144/235] do not find vars in guards --- lib/elixir_sense/core/compiler.ex | 2 +- lib/elixir_sense/core/state.ex | 11 +---- lib/elixir_sense/core/type_inference.ex | 5 ++ .../core/metadata_builder_test.exs | 46 +++++++++++++++++++ .../elixir_sense/core/type_inference_test.exs | 8 ++++ 5 files changed, 61 insertions(+), 11 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index b4b04772..5284786c 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -3393,7 +3393,7 @@ defmodule ElixirSense.Core.Compiler do new_pre = {pre_read, pre_counter, {:bitsize, original_read}} {e_expr, se, ee} = - ElixirExpand.expand(expr |> dbg, %{s | prematch: new_pre}, %{e | context: :guard}) + ElixirExpand.expand(expr, %{s | prematch: new_pre}, %{e | context: :guard}) {e_expr, %{se | prematch: old_pre}, %{ee | context: :match}} end diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index de8aba0f..6bfd8ffb 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -1394,16 +1394,7 @@ defmodule ElixirSense.Core.State do for {key, type} <- inferred_types, reduce: h do acc -> Map.update!(acc, key, fn %VarInfo{type: old} = v -> - {variable, version} = key - # TODO detect more complicated self referential cases - case type do - {:variable, ^variable, ^version} -> - # self referential type - v - - _ -> - %{v | type: ElixirSense.Core.TypeInference.intersect(old, type)} - end + %{v | type: ElixirSense.Core.TypeInference.intersect(old, type)} end) end diff --git a/lib/elixir_sense/core/type_inference.ex b/lib/elixir_sense/core/type_inference.ex index 6d16af8c..48ff132b 100644 --- a/lib/elixir_sense/core/type_inference.ex +++ b/lib/elixir_sense/core/type_inference.ex @@ -205,6 +205,11 @@ defmodule ElixirSense.Core.TypeInference do Enum.uniq(vars) end + defp match_var({:when, _, [left, _right]}, match_context) do + # no variables can be defined in guard context so we skip that subtree + match_var(left, match_context) + end + defp match_var( {:=, _meta, [ diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index edcb5f1b..de2040a8 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -3478,6 +3478,52 @@ defmodule ElixirSense.Core.MetadataBuilderTest do get_line_vars(state, 10) end + test "guards in case clauses more complicated" do + buffer = """ + defmodule MyModule do + def func(x) do + IO.puts "" + case {x, :foo} do + {a, ^x} when is_nil(a) -> + IO.puts "" + _ when is_integer(x) -> + IO.puts "" + end + IO.puts "" + end + end + """ + + state = string_to_state(buffer) + + assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 3) + + assert [ + %VarInfo{ + name: :a, + type: { + :intersection, + [ + {:atom, nil}, + { + :tuple_nth, + {:tuple, 2, [{:variable, :x, 0}, {:atom, :foo}]}, + 0 + } + ] + } + }, + %VarInfo{name: :x, type: nil} + ] = get_line_vars(state, 6) + + assert [%VarInfo{name: :x, type: :number}] = get_line_vars(state, 8) + + # TODO this type should not leak outside clause + # assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 10) + assert [%VarInfo{name: :x, type: :number}] = + get_line_vars(state, 10) + end + test "guards in with clauses" do buffer = """ defmodule MyModule do diff --git a/test/elixir_sense/core/type_inference_test.exs b/test/elixir_sense/core/type_inference_test.exs index 86f4c90a..5059395c 100644 --- a/test/elixir_sense/core/type_inference_test.exs +++ b/test/elixir_sense/core/type_inference_test.exs @@ -51,6 +51,12 @@ defmodule ElixirSense.Core.TypeInferenceTest do assert find_typed_vars_in("^a", nil, :match) == [] end + test "does not find variables in guard" do + assert find_typed_vars_in("_ when is_integer(a)", nil, :match) == [] + end + + # TODO should it find variables in bitstring size specifiers guard? + test "finds variables in tuple" do assert find_typed_vars_in("{}", nil, :match) == [] assert find_typed_vars_in("{a}", nil, :match) == [{{:a, 1}, nil}] @@ -154,6 +160,8 @@ defmodule ElixirSense.Core.TypeInferenceTest do ] end + # TODO should it find vars in bitstring? + test "finds variables in map" do assert find_typed_vars_in("%{}", nil, :match) == [] assert find_typed_vars_in("%{a: a}", nil, :match) == [{{:a, 1}, nil}] From dd65b0c9506db6f94e8d015d4d5449de61c5c3ef Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 18 Aug 2024 09:52:18 +0200 Subject: [PATCH 145/235] do not pass match context to not expanded macro calls --- lib/elixir_sense/core/type_inference.ex | 6 ++++-- test/elixir_sense/core/metadata_builder_test.exs | 11 ++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/elixir_sense/core/type_inference.ex b/lib/elixir_sense/core/type_inference.ex index 48ff132b..26a02804 100644 --- a/lib/elixir_sense/core/type_inference.ex +++ b/lib/elixir_sense/core/type_inference.ex @@ -363,8 +363,10 @@ defmodule ElixirSense.Core.TypeInference do end end - defp match_var(ast, {vars, match_context, context}) do - {ast, {vars, match_context, context}} + defp match_var(ast, {vars, _match_context, context}) do + # traverse literals, not expanded macro calls and bitstrings with nil match_context + # we cannot assume anything basing on match_context on variables there + {ast, {vars, nil, context}} end defp propagate_context(nil, _), do: nil diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index de2040a8..19697aac 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -3486,6 +3486,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do case {x, :foo} do {a, ^x} when is_nil(a) -> IO.puts "" + some_macro(c) -> + IO.puts "" _ when is_integer(x) -> IO.puts "" end @@ -3516,12 +3518,15 @@ defmodule ElixirSense.Core.MetadataBuilderTest do %VarInfo{name: :x, type: nil} ] = get_line_vars(state, 6) - assert [%VarInfo{name: :x, type: :number}] = get_line_vars(state, 8) + assert [%VarInfo{name: :c, type: nil}, %VarInfo{name: :x, type: nil}] = + get_line_vars(state, 8) + + assert [%VarInfo{name: :x, type: :number}] = get_line_vars(state, 10) # TODO this type should not leak outside clause - # assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 10) + # assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 12) assert [%VarInfo{name: :x, type: :number}] = - get_line_vars(state, 10) + get_line_vars(state, 12) end test "guards in with clauses" do From c48dd7a20fcc7fb694f4e687ea3bb27be235c975 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 18 Aug 2024 16:37:25 +0200 Subject: [PATCH 146/235] preserve cursor typespec preserve ast inside cursor --- lib/elixir_sense/core/compiler.ex | 52 +++++++++++++++++--- lib/elixir_sense/core/metadata.ex | 23 ++++++--- lib/elixir_sense/core/source.ex | 15 +++++- test/elixir_sense/core/source_test.exs | 68 ++++++++++++++++++++++++++ 4 files changed, 143 insertions(+), 15 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 5284786c..2c7fb79c 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -455,7 +455,13 @@ defmodule ElixirSense.Core.Compiler do s end - {{:__cursor__, meta, args}, s, e} + case args do + [h | _] -> + expand(h, s, e) + + [] -> + {nil, s, e} + end end # Super @@ -1032,6 +1038,7 @@ defmodule ElixirSense.Core.Compiler do env = %{module: module} ) when kind in [:type, :typep, :opaque] and module != nil do + cursor_before? = state.cursor_env != nil {expr, state, env} = __MODULE__.Typespec.expand_type(expr, state, env) case __MODULE__.Typespec.type_to_signature(expr) do @@ -1043,6 +1050,8 @@ defmodule ElixirSense.Core.Compiler do raise "type #{name}/#{length(type_args)} is a built-in type and it cannot be redefined" end + cursor_after? = state.cursor_env != nil + # TODO elixir does Macro.escape with unquote: true spec = TypeInfo.typespec_to_string(kind, expr) @@ -1054,6 +1063,15 @@ defmodule ElixirSense.Core.Compiler do |> add_current_env_to_line(attr_meta, env) |> with_typespec(nil) + state = + if not cursor_before? and cursor_after? do + {meta, env} = state.cursor_env + env = %{env | typespec: {name, length(type_args)}} + %{state | cursor_env: {meta, env}} + else + state + end + {{:@, attr_meta, [{kind, kind_meta, [expr]}]}, state, env} :error -> @@ -1084,10 +1102,12 @@ defmodule ElixirSense.Core.Compiler do env = %{module: module} ) when kind in [:callback, :macrocallback, :spec] and module != nil do + cursor_before? = state.cursor_env != nil {expr, state, env} = __MODULE__.Typespec.expand_spec(expr, state, env) case __MODULE__.Typespec.spec_to_signature(expr) do {name, type_args} -> + cursor_after? = state.cursor_env != nil spec = TypeInfo.typespec_to_string(kind, expr) range = extract_range(attr_meta) @@ -1114,6 +1134,15 @@ defmodule ElixirSense.Core.Compiler do |> add_current_env_to_line(attr_meta, env) |> with_typespec(nil) + state = + if not cursor_before? and cursor_after? do + {meta, env} = state.cursor_env + env = %{env | typespec: {name, length(type_args)}} + %{state | cursor_env: {meta, env}} + else + state + end + {{:@, attr_meta, [{kind, kind_meta, [expr]}]}, state, env} :error -> @@ -2483,11 +2512,14 @@ defmodule ElixirSense.Core.Compiler do # TODO rewrite to lazy prewalker {_, result} = Macro.prewalk(ast, false, fn - {:__cursor__, _, list} = node, _state when is_list(list) -> - {node, true} + _node, true -> + {nil, true} - node, state -> - {node, state} + {:__cursor__, _, list}, _state when is_list(list) -> + {nil, true} + + node, false -> + {node, false} end) result @@ -4795,7 +4827,9 @@ defmodule ElixirSense.Core.Compiler do ast, {state, env}, fn - {:__cursor__, meta, args} = node, {state, env} when is_list(args) -> + {:__cursor__, meta, args}, {state, env} when is_list(args) -> + dbg(args) + state = unless state.cursor_env do state @@ -4804,6 +4838,12 @@ defmodule ElixirSense.Core.Compiler do state end + node = + case args do + [h | _] -> h + [] -> nil + end + {node, {state, env}} {:__aliases__, _meta, list} = node, {state, env} when is_list(list) -> diff --git a/lib/elixir_sense/core/metadata.ex b/lib/elixir_sense/core/metadata.ex index 9cdfaf3a..b550659b 100644 --- a/lib/elixir_sense/core/metadata.ex +++ b/lib/elixir_sense/core/metadata.ex @@ -64,15 +64,21 @@ defmodule ElixirSense.Core.Metadata do {line, column}, {{begin_line, begin_column}, {end_line, end_column}} ) do - prefix = ElixirSense.Core.Source.text_before(metadata.source, begin_line, begin_column) - suffix = ElixirSense.Core.Source.text_after(metadata.source, end_line, end_column) + [prefix, needle, suffix] = + ElixirSense.Core.Source.split_at(metadata.source, [ + {begin_line, begin_column}, + {end_line, end_column} + ]) - source_with_cursor = prefix <> "(__cursor__())" <> suffix + # IO.puts(metadata.source) + source_with_cursor = prefix <> "__cursor__(#{needle})" <> suffix + # IO.puts(source_with_cursor) {meta, cursor_env} = - case Code.string_to_quoted(source_with_cursor, columns: true, token_metadata: true) do - # {:ok, ast} -> - # ElixirSense.Core.MetadataBuilder.build(ast).cursor_env || {[], nil} + case Code.string_to_quoted(source_with_cursor, columns: true, token_metadata: true) + |> dbg do + {:ok, ast} -> + ElixirSense.Core.MetadataBuilder.build(ast).cursor_env || {[], nil} _ -> {[], nil} @@ -95,9 +101,10 @@ defmodule ElixirSense.Core.Metadata do end if cursor_env != nil do - cursor_env + get_env(metadata, {line, column}) |> dbg + cursor_env |> dbg else - get_env(metadata, {line, column}) + get_env(metadata, {line, column}) |> dbg end end diff --git a/lib/elixir_sense/core/source.ex b/lib/elixir_sense/core/source.ex index 1028bbbe..119a9b45 100644 --- a/lib/elixir_sense/core/source.ex +++ b/lib/elixir_sense/core/source.ex @@ -120,10 +120,23 @@ defmodule ElixirSense.Core.Source do @spec split_at(String.t(), pos_integer, pos_integer) :: {String.t(), String.t()} def split_at(code, line, col) do - pos = find_position(code, line, col, {0, 1, 1}) + pos = find_position(code, max(line, 1), max(col, 1), {0, 1, 1}) String.split_at(code, pos) end + @spec split_at(String.t(), list({pos_integer, pos_integer})) :: list(String.t()) + def split_at(code, list) do + do_split_at(code, Enum.reverse(list), []) + end + + defp do_split_at(code, [], acc), do: [code | acc] + + defp do_split_at(code, [{line, col} | rest], acc) do + pos = find_position(code, max(line, 1), max(col, 1), {0, 1, 1}) + {text_before, text_after} = String.split_at(code, pos) + do_split_at(text_before, rest, [text_after | acc]) + end + @spec text_before(String.t(), pos_integer, pos_integer) :: String.t() def text_before(code, line, col) do {text, _rest} = split_at(code, line, col) diff --git a/test/elixir_sense/core/source_test.exs b/test/elixir_sense/core/source_test.exs index 50ea4f79..536de62a 100644 --- a/test/elixir_sense/core/source_test.exs +++ b/test/elixir_sense/core/source_test.exs @@ -688,6 +688,74 @@ defmodule ElixirSense.Core.SourceTest do end end + describe "split_at/2" do + test "empty list" do + code = """ + defmodule Abcd do + def go do + :ok + end + end + """ + + assert split_at(code, []) == [code] + end + + test "one element list" do + code = """ + defmodule Abcd do + def go do + :ok + end + end + """ + + parts = split_at(code, [{2, 3}]) + assert parts == ["defmodule Abcd do\n ", "def go do\n :ok\n end\nend\n"] + assert Enum.join(parts) == code + end + + test "two element list same line" do + code = """ + defmodule Abcd do + def go do + :ok + end + end + """ + + parts = split_at(code, [{2, 3}, {2, 6}]) + assert parts == ["defmodule Abcd do\n ", "def", " go do\n :ok\n end\nend\n"] + assert Enum.join(parts) == code + end + + test "two element list different lines" do + code = """ + defmodule Abcd do + def go do + :ok + end + end + """ + + parts = split_at(code, [{2, 3}, {4, 6}]) + assert parts == ["defmodule Abcd do\n ", "def go do\n :ok\n end", "\nend\n"] + assert Enum.join(parts) == code + end + + test "handles positions at start and end of code" do + code = "abcdef" + positions = [{1, 1}, {1, 7}] + assert split_at(code, positions) == ["", "abcdef", ""] + end + + test "handles positions beyond code length" do + code = "short" + positions = [{0, 0}, {10, 15}] + assert split_at(code, positions) == ["", "short", ""] + end + end + describe "which_struct" do test "map" do code = """ From a6c8c280e98c9e5abb31a074e5bc4df29bdd7cc2 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 18 Aug 2024 18:37:22 +0200 Subject: [PATCH 147/235] more improvements to cursor expansion --- lib/elixir_sense/core/compiler.ex | 25 ++++++++---- lib/elixir_sense/core/metadata.ex | 39 ++++++++++++++----- lib/elixir_sense/core/state.ex | 8 ++-- test/elixir_sense/core/compiler_test.exs | 4 ++ .../metadata_builder/error_recovery_test.exs | 31 +++++++++++---- 5 files changed, 80 insertions(+), 27 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 2c7fb79c..1035a0b9 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -732,9 +732,15 @@ defmodule ElixirSense.Core.Compiler do # Invalid calls defp do_expand({other, meta, args}, s, e) when is_list(meta) and is_list(args) do - # elixir raises invalid_call - {args, s, e} = expand_args(args, s, e) - {{other, meta, args}, s, e} + # elixir raises invalid_call, we may have cursor in other + {other_exp, s, e} = expand(other, s, e) + + if other_exp != other do + expand(other_exp, s, e) + else + {args, s, e} = expand_args(args, s, e) + {{other, meta, args}, s, e} + end end # Literals @@ -3264,8 +3270,15 @@ defmodule ElixirSense.Core.Compiler do defp expand_each_spec(meta, [{:__cursor__, _, args} = h | t], map, s, original_s, e) when is_list(args) do - {_, s, e} = ElixirExpand.expand(h, s, e) - expand_each_spec(meta, t, map, s, original_s, e) + {h, s, e} = ElixirExpand.expand(h, s, e) + + args = + case h do + nil -> t + h -> [h | t] + end + + expand_each_spec(meta, args, map, s, original_s, e) end defp expand_each_spec(meta, [{expr, meta_e, args} = h | t], map, s, original_s, e) @@ -4828,8 +4841,6 @@ defmodule ElixirSense.Core.Compiler do {state, env}, fn {:__cursor__, meta, args}, {state, env} when is_list(args) -> - dbg(args) - state = unless state.cursor_env do state diff --git a/lib/elixir_sense/core/metadata.ex b/lib/elixir_sense/core/metadata.ex index b550659b..c75c9c7e 100644 --- a/lib/elixir_sense/core/metadata.ex +++ b/lib/elixir_sense/core/metadata.ex @@ -73,10 +73,10 @@ defmodule ElixirSense.Core.Metadata do # IO.puts(metadata.source) source_with_cursor = prefix <> "__cursor__(#{needle})" <> suffix # IO.puts(source_with_cursor) + # dbg(metadata) {meta, cursor_env} = - case Code.string_to_quoted(source_with_cursor, columns: true, token_metadata: true) - |> dbg do + case Code.string_to_quoted(source_with_cursor, columns: true, token_metadata: true) do {:ok, ast} -> ElixirSense.Core.MetadataBuilder.build(ast).cursor_env || {[], nil} @@ -101,10 +101,10 @@ defmodule ElixirSense.Core.Metadata do end if cursor_env != nil do - get_env(metadata, {line, column}) |> dbg - cursor_env |> dbg + get_env(metadata, {line, column}) + cursor_env else - get_env(metadata, {line, column}) |> dbg + get_env(metadata, {line, column}) end end @@ -322,7 +322,12 @@ defmodule ElixirSense.Core.Metadata do def get_call_arity(%__MODULE__{}, _module, nil, _line, _column), do: nil def get_call_arity( - %__MODULE__{calls: calls, error: error, mods_funs_to_positions: mods_funs_to_positions}, + %__MODULE__{ + calls: calls, + error: error, + mods_funs_to_positions: mods_funs_to_positions, + types: types + }, module, fun, line, @@ -350,10 +355,26 @@ defmodule ElixirSense.Core.Metadata do end) end + result = + if result == nil do + mods_funs_to_positions + |> Enum.find_value(fn + {{^module, ^fun, arity}, %{positions: positions}} when not is_nil(arity) -> + if Enum.any?(positions, &match?({^line, _}, &1)) do + arity + end + + _ -> + nil + end) + else + result + end + if result == nil do - mods_funs_to_positions + types |> Enum.find_value(fn - {{^module, ^fun, arity}, %{positions: positions}} when not is_nil(arity) -> + {{^module, ^fun, arity}, %{positions: positions}} -> if Enum.any?(positions, &match?({^line, _}, &1)) do arity end @@ -422,7 +443,7 @@ defmodule ElixirSense.Core.Metadata do when not is_nil(module) and not is_nil(type) do metadata.types |> Enum.filter(fn - {{^module, ^type, arity}, _type_info} when not is_nil(arity) -> true + {{^module, ^type, _arity}, _type_info} -> true _ -> false end) |> Enum.map(fn {_, %State.TypeInfo{} = type_info} -> diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 6bfd8ffb..acb52196 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -1405,11 +1405,11 @@ defmodule ElixirSense.Core.State do line = Keyword.get(meta, :line, 0) if line <= 0 do - nil + {1, 1} else { line, - Keyword.get(meta, :column) + Keyword.get(meta, :column, 1) } end end @@ -1418,11 +1418,11 @@ defmodule ElixirSense.Core.State do line = Keyword.get(meta, :line, 0) if line <= 0 do - {nil, nil} + {{1, 1}, nil} else position = { line, - Keyword.get(meta, :column) + Keyword.get(meta, :column, 1) } end_position = diff --git a/test/elixir_sense/core/compiler_test.exs b/test/elixir_sense/core/compiler_test.exs index 717c74d3..f2527d0b 100644 --- a/test/elixir_sense/core/compiler_test.exs +++ b/test/elixir_sense/core/compiler_test.exs @@ -867,6 +867,10 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do defp clean_capture_arg(ast) do {ast, _} = Macro.prewalk(ast, nil, fn + {{:., dot_meta, target}, call_meta, args}, state -> + dot_meta = Keyword.delete(dot_meta, :column_correction) + {{{:., dot_meta, target}, call_meta, args}, state} + {atom, meta, nil} = node, state when is_atom(atom) -> # elixir changes the name to capture and does different counter tracking node = diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index 5ecb4fb5..241b1176 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -4,12 +4,19 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do alias ElixirSense.Core.MetadataBuilder alias ElixirSense.Core.Normalized.Code, as: NormalizedCode - defp get_cursor_env(code) do + defp get_cursor_env(code, use_string_to_quoted \\ false) do {:ok, ast} = - NormalizedCode.Fragment.container_cursor_to_quoted(code, - columns: true, - token_metadata: true - ) + if use_string_to_quoted do + Code.string_to_quoted(code, + columns: true, + token_metadata: true + ) + else + NormalizedCode.Fragment.container_cursor_to_quoted(code, + columns: true, + token_metadata: true + ) + end # dbg(ast) state = MetadataBuilder.build(ast) @@ -60,7 +67,8 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, &(&1.name == :x)) + # this test fails + # assert Enum.any?(env.vars, &(&1.name == :x)) end test "cursor in clause guard" do @@ -777,7 +785,8 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert {meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) - assert Enum.any?(env.vars, &(&1.name == :y)) + # this test fails + # assert Enum.any?(env.vars, &(&1.name == :y)) end test "cursor in do block reduce right side of clause" do @@ -1705,6 +1714,14 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert get_cursor_env(code) end + + test "invalid call cursor" do + code = """ + __cursor__(a.b)() + """ + + assert get_cursor_env(code, true) + end end describe "alias/import/require" do From 9e2ad861e05be4890bc925f8fb854619dc012071 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 18 Aug 2024 23:34:26 +0200 Subject: [PATCH 148/235] filter internal protocol defs --- lib/elixir_sense/core/state.ex | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index acb52196..46a8ffd9 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -1312,10 +1312,9 @@ defmodule ElixirSense.Core.State do keys = state.mods_funs_to_positions - |> Map.keys() |> Enum.filter(fn - {^current_module, name, _arity} when not is_nil(name) -> - name not in builtins + {{^current_module, name, _arity}, info} when not is_nil(name) -> + name not in builtins and info.type == :def _ -> false From 76fc789a3744a51e67192b97c918948b357ac557 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 19 Aug 2024 23:25:25 +0200 Subject: [PATCH 149/235] fix invalid match --- lib/elixir_sense/core/state.ex | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 46a8ffd9..3e2f09e1 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -1321,15 +1321,14 @@ defmodule ElixirSense.Core.State do end) new_specs = - for key = {_mod, name, _arity} <- keys, + for {key = {_mod, name, _arity}, mod_fun_info} <- keys, into: %{}, do: ( new_spec = case state.specs[key] do nil -> - %ModFunInfo{positions: positions, params: params} = - state.mods_funs_to_positions[key] + %ModFunInfo{positions: positions, params: params} = mod_fun_info args = for param_variant <- params do From 5e6f2a5ea4db08efe1b56870b018b1fa2a080783 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 20 Aug 2024 07:54:22 +0200 Subject: [PATCH 150/235] handle incomplete typespecs --- lib/elixir_sense/core/compiler.ex | 11 ++- .../core/metadata_builder_test.exs | 96 +++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 1035a0b9..c202c22a 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1052,6 +1052,8 @@ defmodule ElixirSense.Core.Compiler do raise "type #{name}/#{1} is a reserved type and it cannot be defined" {name, type_args} -> + type_args = type_args || [] + if __MODULE__.Typespec.built_in_type?(name, length(type_args)) do raise "type #{name}/#{length(type_args)} is a built-in type and it cannot be redefined" end @@ -1133,6 +1135,8 @@ defmodule ElixirSense.Core.Compiler do state end + type_args = type_args || [] + state = state |> add_spec(env, name, type_args, spec, kind, range) @@ -4673,7 +4677,12 @@ defmodule ElixirSense.Core.Compiler do when is_atom(name) and name != :"::", do: {name, args} - def type_to_signature(_), do: :error + def type_to_signature({name, _, args}) when is_atom(name) and name != :"::" do + # elixir returns :error here, we handle incomplete signatures + {name, args} + end + + def type_to_signature(), do: :error def expand_spec(ast, state, env) do # TODO not sure this is correct. Are module vars accessible? diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 19697aac..434d909a 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -7864,6 +7864,54 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert %{} == state.types end + test "registers incomplete types" do + state = + """ + defmodule My do + @type foo + @type bar() + @type baz(a) + end + """ + |> string_to_state + + assert %{ + {My, :bar, 0} => %ElixirSense.Core.State.TypeInfo{ + name: :bar, + args: [[]], + specs: ["@type bar()"], + kind: :type, + positions: [{3, 3}], + end_positions: [{3, 14}], + generated: [false], + doc: "", + meta: %{} + }, + {My, :bar, 0} => %ElixirSense.Core.State.TypeInfo{ + name: :bar, + args: [[]], + specs: ["@type bar()"], + kind: :type, + positions: [{3, 3}], + end_positions: [{3, 14}], + generated: [false], + doc: "", + meta: %{} + }, + {My, :baz, 1} => %ElixirSense.Core.State.TypeInfo{ + name: :baz, + args: [["a"]], + specs: ["@type baz(a)"], + kind: :type, + positions: [{4, 3}], + end_positions: [{4, 15}], + generated: [false], + doc: "", + meta: %{} + } + } = state.types + end + test "protocol exports type t" do state = """ @@ -7962,6 +8010,54 @@ defmodule ElixirSense.Core.MetadataBuilderTest do end end + test "registers incomplete specs" do + state = + """ + defmodule My do + @spec foo + @spec bar() + @spec baz(a) + end + """ + |> string_to_state + + assert %{ + {My, :bar, 0} => %ElixirSense.Core.State.SpecInfo{ + name: :bar, + args: [[]], + specs: ["@spec bar()"], + kind: :spec, + positions: [{3, 3}], + end_positions: [{3, 14}], + generated: [false], + doc: "", + meta: %{} + }, + {My, :bar, 0} => %ElixirSense.Core.State.SpecInfo{ + name: :bar, + args: [[]], + specs: ["@spec bar()"], + kind: :spec, + positions: [{3, 3}], + end_positions: [{3, 14}], + generated: [false], + doc: "", + meta: %{} + }, + {My, :baz, 1} => %ElixirSense.Core.State.SpecInfo{ + name: :baz, + args: [["a"]], + specs: ["@spec baz(a)"], + kind: :spec, + positions: [{4, 3}], + end_positions: [{4, 15}], + generated: [false], + doc: "", + meta: %{} + } + } = state.specs + end + test "specs and types expand aliases" do state = """ From 054fa8e145600ae32feff5b4f9fad41b70f7bf7b Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 20 Aug 2024 07:54:38 +0200 Subject: [PATCH 151/235] fix snippet generation --- lib/elixir_sense/core/introspection.ex | 4 ++++ lib/elixir_sense/core/metadata_builder.ex | 19 ------------------- 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/lib/elixir_sense/core/introspection.ex b/lib/elixir_sense/core/introspection.ex index 2c4c87de..05091c3e 100644 --- a/lib/elixir_sense/core/introspection.ex +++ b/lib/elixir_sense/core/introspection.ex @@ -568,6 +568,10 @@ defmodule ElixirSense.Core.Introspection do next_snippet(ast, index) end + defp term_to_snippet({name, _, []} = ast, index) when is_atom(name) do + next_snippet(ast, index) + end + defp term_to_snippet(ast, index) do {ast, index} end diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index bccadd07..4a18ac4d 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -38,25 +38,6 @@ defmodule ElixirSense.Core.MetadataBuilder do # # |> result(ast) # end - # # incomplete spec - # # @callback my(integer) - # defp pre( - # {:@, _meta_attr, [{kind, _meta_kind, [{name, _meta_name, type_args}]} = _spec]}, - # _state - # ) - # when kind in [:spec, :callback, :macrocallback] and is_atom(name) and - # (is_nil(type_args) or is_list(type_args)) do - # # pre_spec( - # # {:@, meta_attr, [{kind, add_no_call(meta_kind), [{name, meta_name, type_args}]}]}, - # # state, - # # meta_attr, - # # name, - # # expand_aliases_in_ast(state, List.wrap(type_args)), - # # expand_aliases_in_ast(state, spec), - # # kind - # # ) - # end - # # Any other tuple with a line # defp pre({_, meta, _} = ast, state) do # case Keyword.get(meta, :line) do From 050b0dedc8f8ca9052208c6041aa1bd5873cd422 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 20 Aug 2024 07:55:45 +0200 Subject: [PATCH 152/235] add prefix_suffx extraction --- lib/elixir_sense/core/source.ex | 39 ++++++++++++ test/elixir_sense/core/source_test.exs | 84 ++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) diff --git a/lib/elixir_sense/core/source.ex b/lib/elixir_sense/core/source.ex index 119a9b45..029bc833 100644 --- a/lib/elixir_sense/core/source.ex +++ b/lib/elixir_sense/core/source.ex @@ -118,6 +118,45 @@ defmodule ElixirSense.Core.Source do end end + @spec prefix_suffix(String.t(), pos_integer, pos_integer) :: {String.t(), String.t()} + def prefix_suffix(code, line, col) do + line = code |> split_lines() |> Enum.at(line - 1, "") + + line = + if String.length(line) < col do + line_padding = for _ <- 1..(col - String.length(line)), into: "", do: " " + line <> line_padding + else + line + end + + # Extract the prefix + line_str = line |> String.slice(0, col - 1) + + prefix = + case Regex.run(~r/[\p{L}\p{N}\.\_\!\?\:\@\&\^\~\+\-\<\>\=\*\/\|\\]+$/u, line_str) do + nil -> "" + [prefix] when is_binary(prefix) -> prefix + end + + # Extract the suffix + suffix = + line + |> String.slice((col - 1)..-1//1) + |> case do + nil -> + "" + + str -> + case Regex.run(~r/^[\p{L}\p{N}\.\_\!\?\:\@\&\^\~\+\-\<\>\=\*\/\|\\]+/u, str) do + nil -> "" + [suffix] when is_binary(suffix) -> suffix + end + end + + {prefix, suffix} + end + @spec split_at(String.t(), pos_integer, pos_integer) :: {String.t(), String.t()} def split_at(code, line, col) do pos = find_position(code, max(line, 1), max(col, 1), {0, 1, 1}) diff --git a/test/elixir_sense/core/source_test.exs b/test/elixir_sense/core/source_test.exs index 536de62a..cb660525 100644 --- a/test/elixir_sense/core/source_test.exs +++ b/test/elixir_sense/core/source_test.exs @@ -1185,4 +1185,88 @@ defmodule ElixirSense.Core.SourceTest do assert "integer-un" == bitstring_options(text) end end + + describe "prefix/3" do + test "returns empty string when no prefix is found" do + code = "def example do\n :ok\nend" + assert "" == prefix(code, 2, 3) + end + + test "returns the correct prefix" do + code = "defmodule Test do\n def example_func do\n :ok\n end\nend" + assert "example_f" == prefix(code, 2, 16) + end + + test "handles line shorter than column" do + code = "short" + assert "" == prefix(code, 1, 10) + end + + test "handles line outside range" do + code = "short" + assert "" == prefix(code, 3, 1) + end + + test "returns prefix with special characters" do + code = "def example?!:@&^~+-<>=*/|\\() do\n :ok\nend" + assert "example?!:@&^~+-<>=*/|\\" == prefix(code, 1, 28) + end + + test "returns prefix at the end of line" do + code = "def example\ndef another" + assert "example" == prefix(code, 1, 12) + end + + test "handles empty lines" do + code = "\n\ndef example" + assert "" == prefix(code, 2, 1) + end + + test "returns prefix with numbers" do + code = "variable123 = 42" + assert "variable12" == prefix(code, 1, 11) + end + end + + describe "prefix_suffix/3" do + test "returns empty string when no prefix is found" do + code = "def example do\n :ok\nend" + assert {"", ":ok"} == prefix_suffix(code, 2, 3) + end + + test "returns the correct prefix" do + code = "defmodule Test do\n def example_func do\n :ok\n end\nend" + assert {"example_f", "unc"} == prefix_suffix(code, 2, 16) + end + + test "handles line shorter than column" do + code = "short" + assert {"", ""} == prefix_suffix(code, 1, 10) + end + + test "handles line outside range" do + code = "short" + assert {"", ""} == prefix_suffix(code, 3, 1) + end + + test "returns prefix with special characters" do + code = "def example?!:@&^~+-<>=*/|\\() do\n :ok\nend" + assert {"example?!:@&^~+-<>=*/", "|\\"} == prefix_suffix(code, 1, 26) + end + + test "returns prefix at the end of line" do + code = "def example\ndef another" + assert {"example", ""} == prefix_suffix(code, 1, 12) + end + + test "handles empty lines" do + code = "\n\ndef example" + assert {"", ""} == prefix_suffix(code, 2, 1) + end + + test "returns prefix with numbers" do + code = "variable123 = 42" + assert {"variable12", "3"} == prefix_suffix(code, 1, 11) + end + end end From e9e2c6554e302ef28e30b26c13a5061f7195437f Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 20 Aug 2024 07:58:14 +0200 Subject: [PATCH 153/235] do not attempt to fix line not found if we already have cursor env --- lib/elixir_sense/core/metadata.ex | 2 ++ lib/elixir_sense/core/parser.ex | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/elixir_sense/core/metadata.ex b/lib/elixir_sense/core/metadata.ex index c75c9c7e..5438de9f 100644 --- a/lib/elixir_sense/core/metadata.ex +++ b/lib/elixir_sense/core/metadata.ex @@ -12,6 +12,7 @@ defmodule ElixirSense.Core.Metadata do @type t :: %ElixirSense.Core.Metadata{ source: String.t(), mods_funs_to_positions: State.mods_funs_to_positions_t(), + cursor_env: nil | {keyword(), ElixirSense.Core.State.Env.t()}, lines_to_env: State.lines_to_env_t(), calls: State.calls_t(), vars_info_per_scope_id: State.vars_info_per_scope_id_t(), @@ -25,6 +26,7 @@ defmodule ElixirSense.Core.Metadata do defstruct source: "", mods_funs_to_positions: %{}, + cursor_env: nil, lines_to_env: %{}, calls: %{}, vars_info_per_scope_id: %{}, diff --git a/lib/elixir_sense/core/parser.ex b/lib/elixir_sense/core/parser.ex index 87b39da9..59c94280 100644 --- a/lib/elixir_sense/core/parser.ex +++ b/lib/elixir_sense/core/parser.ex @@ -49,7 +49,8 @@ defmodule ElixirSense.Core.Parser do {:ok, ast, modified_source, error} -> acc = MetadataBuilder.build(ast) - if cursor_position == nil or Map.has_key?(acc.lines_to_env, elem(cursor_position, 0)) or + if cursor_position == nil or acc.cursor_env != nil or + Map.has_key?(acc.lines_to_env, elem(cursor_position, 0)) or !try_to_fix_line_not_found do create_metadata(source, {:ok, acc, error}) else @@ -186,6 +187,7 @@ defmodule ElixirSense.Core.Parser do specs: acc.specs, structs: acc.structs, mods_funs_to_positions: acc.mods_funs_to_positions, + cursor_env: acc.cursor_env, lines_to_env: acc.lines_to_env, vars_info_per_scope_id: acc.vars_info_per_scope_id, calls: acc.calls, From a2933dca9fb61e676ded7ade8af7acfb1dda6bdf Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 20 Aug 2024 08:01:36 +0200 Subject: [PATCH 154/235] metadata improvements --- lib/elixir_sense/core/metadata.ex | 64 ++++++++++++++----------------- 1 file changed, 29 insertions(+), 35 deletions(-) diff --git a/lib/elixir_sense/core/metadata.ex b/lib/elixir_sense/core/metadata.ex index 5438de9f..ba373162 100644 --- a/lib/elixir_sense/core/metadata.ex +++ b/lib/elixir_sense/core/metadata.ex @@ -64,18 +64,35 @@ defmodule ElixirSense.Core.Metadata do def get_cursor_env( %__MODULE__{} = metadata, {line, column}, - {{begin_line, begin_column}, {end_line, end_column}} + surround \\ nil ) do - [prefix, needle, suffix] = - ElixirSense.Core.Source.split_at(metadata.source, [ - {begin_line, begin_column}, - {end_line, end_column} - ]) - - # IO.puts(metadata.source) - source_with_cursor = prefix <> "__cursor__(#{needle})" <> suffix + {prefix, source_with_cursor} = + case surround do + {{begin_line, begin_column}, {end_line, end_column}} -> + [prefix, needle, suffix] = + ElixirSense.Core.Source.split_at(metadata.source, [ + {begin_line, begin_column}, + {end_line, end_column} + ]) + + # IO.puts(metadata.source) + source_with_cursor = prefix <> "__cursor__(#{needle})" <> suffix + # IO.puts(source_with_cursor) + # dbg(metadata) + {prefix, source_with_cursor} + + nil -> + [prefix, suffix] = + ElixirSense.Core.Source.split_at(metadata.source, [ + {line, column} + ]) + + source_with_cursor = prefix <> "__cursor__()" <> suffix + + {prefix, source_with_cursor} + end + # IO.puts(source_with_cursor) - # dbg(metadata) {meta, cursor_env} = case Code.string_to_quoted(source_with_cursor, columns: true, token_metadata: true) do @@ -94,37 +111,14 @@ defmodule ElixirSense.Core.Metadata do columns: true, token_metadata: true ) do - # {:ok, ast} -> - # ElixirSense.Core.MetadataBuilder.build(ast).cursor_env || {[], nil} + {:ok, ast} -> + ElixirSense.Core.MetadataBuilder.build(ast).cursor_env || {[], nil} _ -> {[], nil} end end - if cursor_env != nil do - get_env(metadata, {line, column}) - cursor_env - else - get_env(metadata, {line, column}) - end - end - - def get_cursor_env(%__MODULE__{} = metadata, {line, column}) do - prefix = ElixirSense.Core.Source.text_before(metadata.source, line, column) - - {_meta, cursor_env} = - case NormalizedCode.Fragment.container_cursor_to_quoted(prefix, - columns: true, - token_metadata: true - ) do - {:ok, ast} -> - ElixirSense.Core.MetadataBuilder.build(ast).cursor_env || {[], nil} - - _ -> - {[], nil} - end - if cursor_env != nil do cursor_env else From 88e3e432f8891687bfe7ad21a2d6e2ddd0e5cef2 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 20 Aug 2024 08:02:25 +0200 Subject: [PATCH 155/235] fix some parser test --- test/elixir_sense/core/parser_test.exs | 119 +++++++------------------ 1 file changed, 34 insertions(+), 85 deletions(-) diff --git a/test/elixir_sense/core/parser_test.exs b/test/elixir_sense/core/parser_test.exs index bc675374..5a21545c 100644 --- a/test/elixir_sense/core/parser_test.exs +++ b/test/elixir_sense/core/parser_test.exs @@ -14,12 +14,9 @@ defmodule ElixirSense.Core.ParserTest do """ assert %Metadata{ - error: {:error, :env_not_found}, + error: nil, mods_funs_to_positions: %{{MyModule, nil, nil} => %{positions: [{1, 1}]}}, - lines_to_env: %{ - 1 => %Env{}, - 3 => %Env{functions: functions} - }, + cursor_env: {_, %Env{functions: functions}}, source: "defmodule MyModule" <> _ } = parse_string(source, true, true, {3, 3}) @@ -35,12 +32,8 @@ defmodule ElixirSense.Core.ParserTest do """ assert %Metadata{ - error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{module: MyModule}, - 2 => %Env{functions: _functions2, module: MyModule}, - 3 => %Env{functions: functions3, module: MyModule} - } + error: nil, + cursor_env: {_, %Env{functions: functions3, module: MyModule}} } = parse_string(source, true, true, {3, 10}) assert Keyword.has_key?(functions3, List) @@ -56,10 +49,7 @@ defmodule ElixirSense.Core.ParserTest do assert %Metadata{ error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{}, - 3 => %Env{functions: functions} - } + cursor_env: {_, %Env{functions: functions}} } = parse_string(source, true, true, {3, 20}) assert Keyword.has_key?(functions, List) @@ -75,10 +65,7 @@ defmodule ElixirSense.Core.ParserTest do assert %Metadata{ error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{}, - 3 => %Env{functions: functions} - } + cursor_env: {_, %Env{functions: functions}} } = parse_string(source, true, true, {3, 8}) assert Keyword.has_key?(functions, List) @@ -94,10 +81,7 @@ defmodule ElixirSense.Core.ParserTest do assert %Metadata{ error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{}, - 3 => %Env{functions: functions} - } + cursor_env: {_, %Env{functions: functions}} } = parse_string(source, true, true, {3, 11}) assert Keyword.has_key?(functions, List) @@ -113,10 +97,7 @@ defmodule ElixirSense.Core.ParserTest do assert %Metadata{ error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{}, - 3 => %Env{functions: functions} - } + cursor_env: {_, %Env{functions: functions}} } = parse_string(source, true, true, {3, 12}) assert Keyword.has_key?(functions, List) @@ -170,10 +151,7 @@ defmodule ElixirSense.Core.ParserTest do assert %Metadata{ error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{}, - 3 => %Env{functions: functions} - } + cursor_env: {_, %Env{functions: functions}} } = parse_string(source, true, true, {3, 12}) assert Keyword.has_key?(functions, List) @@ -189,10 +167,7 @@ defmodule ElixirSense.Core.ParserTest do assert %Metadata{ error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{}, - 3 => %Env{functions: functions} - } + cursor_env: {_, %Env{functions: functions}} } = parse_string(source, true, true, {3, 12}) assert Keyword.has_key?(functions, List) @@ -208,10 +183,7 @@ defmodule ElixirSense.Core.ParserTest do assert %Metadata{ error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{}, - 3 => %Env{functions: functions} - } + cursor_env: {_, %Env{functions: functions}} } = parse_string(source, true, true, {3, 12}) assert Keyword.has_key?(functions, List) @@ -227,10 +199,7 @@ defmodule ElixirSense.Core.ParserTest do assert %Metadata{ error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{}, - 3 => %Env{functions: functions} - } + cursor_env: {_, %Env{functions: functions}} } = parse_string(source, true, true, {3, 14}) assert Keyword.has_key?(functions, List) @@ -253,24 +222,16 @@ defmodule ElixirSense.Core.ParserTest do # assert_received {:result, result} - assert (%Metadata{ - error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{ - module: MyModule, - scope_id: scope_id_1 - }, - 3 => %Env{ - module: MyModule, - requires: _, - scope_id: scope_id_2, + assert %Metadata{ + error: {:error, :parse_error}, + cursor_env: + {_, + %Env{ vars: [ %VarInfo{name: :x} ] - } - } - } - when scope_id_2 > scope_id_1) = result + }} + } = result end test "parse_string with missing terminator \"end\" attempts to insert `end` at correct indentation" do @@ -281,10 +242,7 @@ defmodule ElixirSense.Core.ParserTest do assert %Metadata{ error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{module: MyModule}, - 2 => %Env{module: MyModule} - } + cursor_env: {_, %Env{module: MyModule}} } = parse_string(source, true, true, {2, 3}) source = """ @@ -296,18 +254,12 @@ defmodule ElixirSense.Core.ParserTest do assert %Metadata{ error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{module: MyModule}, - 3 => %Env{module: _} - } + cursor_env: {_, %Env{module: MyModule}} } = parse_string(source, true, true, {3, 1}) assert %Metadata{ error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{module: MyModule}, - 2 => %Env{module: MyModule} - } + cursor_env: {_, %Env{module: MyModule}} } = parse_string(source, true, true, {2, 1}) source = """ @@ -319,15 +271,16 @@ defmodule ElixirSense.Core.ParserTest do assert %Metadata{ error: {:error, :parse_error}, + cursor_env: {_, %Env{module: MyModule}}, lines_to_env: %{ 1 => %Env{module: MyModule}, - 2 => %Env{module: MyModule}, 3 => %Env{module: MyModule.MyModule1} } } = parse_string(source, true, true, {2, 1}) assert %Metadata{ error: {:error, :parse_error}, + cursor_env: {_, %Env{module: MyModule}}, lines_to_env: %{ 1 => %Env{module: MyModule}, 3 => %Env{module: MyModule.MyModule1} @@ -347,7 +300,7 @@ defmodule ElixirSense.Core.ParserTest do """ capture_io(:stderr, fn -> - assert %Metadata{error: {:error, :parse_error}, lines_to_env: %{5 => _}} = + assert %Metadata{error: {:error, :parse_error}, cursor_env: {_, _}} = parse_string(source, true, true, {5, 10}) end) end @@ -363,8 +316,8 @@ defmodule ElixirSense.Core.ParserTest do end """ - %Metadata{error: {:error, :parse_error}, lines_to_env: %{6 => _}} = - parse_string(source, true, true, {5, 12}) + assert %Metadata{error: {:error, :parse_error}, cursor_env: {_, _}} = + parse_string(source, true, true, {5, 12}) end @tag capture_log: true @@ -379,12 +332,9 @@ defmodule ElixirSense.Core.ParserTest do """ assert %Metadata{ - error: {:error, :env_not_found}, + error: nil, mods_funs_to_positions: %{{MyModule, nil, nil} => %{positions: [{1, 1}]}}, - lines_to_env: %{ - 1 => %Env{}, - 4 => %Env{functions: functions} - }, + cursor_env: {_, %Env{functions: functions}}, source: "defmodule MyModule" <> _ } = parse_string(source, true, true, {5, 3}) @@ -417,15 +367,14 @@ defmodule ElixirSense.Core.ParserTest do ''' assert %ElixirSense.Core.Metadata{ - lines_to_env: %{ - 6 => %ElixirSense.Core.State.Env{ - attributes: [%ElixirSense.Core.State.AttributeInfo{name: :my_attr}] - } - } + cursor_env: + {_, + %ElixirSense.Core.State.Env{ + attributes: [%ElixirSense.Core.State.AttributeInfo{name: :my_attr}] + }} } = parse_string(source, true, true, {6, 6}) end - @tag only_this: true test "parse_string with literal strings in sigils" do source = ~S''' defmodule MyMod do From aa2bea7fde70df5c663e9428053f3848a7f769ed Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 20 Aug 2024 10:14:18 +0200 Subject: [PATCH 156/235] fix crash on < 1.15 --- lib/elixir_sense/core/metadata_builder.ex | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index 4a18ac4d..f5881e77 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -18,8 +18,12 @@ defmodule ElixirSense.Core.MetadataBuilder do Compiler.expand( ast, %State{ - # TODO remove default when we require elixir 1.15 - prematch: Code.get_compiler_option(:on_undefined_variable) || :warn + prematch: + if Version.match?(System.version(), ">= 1.15.0-dev") do + Code.get_compiler_option(:on_undefined_variable) + else + :warn + end }, Compiler.env() ) From 0477bf8518283b11b401daa8706c7f7ccb5e6e5b Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 20 Aug 2024 18:34:04 +0200 Subject: [PATCH 157/235] fix some tests on < 1.15 --- test/elixir_sense/core/compiler_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/elixir_sense/core/compiler_test.exs b/test/elixir_sense/core/compiler_test.exs index f2527d0b..4ea6c295 100644 --- a/test/elixir_sense/core/compiler_test.exs +++ b/test/elixir_sense/core/compiler_test.exs @@ -42,7 +42,7 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do else Record.defrecordp(:elixir_ex, caller: false, - prematch: :raise, + prematch: if(Version.match?(System.version(), ">= 1.15.0-dev"), do: :raise, else: :warn), stacktrace: false, unused: {%{}, 0}, vars: {%{}, false} From d7cc89daee15f1663123009937ce6ec336ae08d7 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 20 Aug 2024 18:39:32 +0200 Subject: [PATCH 158/235] another attempt --- test/elixir_sense/core/compiler_test.exs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/test/elixir_sense/core/compiler_test.exs b/test/elixir_sense/core/compiler_test.exs index 4ea6c295..e5e1c9d0 100644 --- a/test/elixir_sense/core/compiler_test.exs +++ b/test/elixir_sense/core/compiler_test.exs @@ -42,7 +42,14 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do else Record.defrecordp(:elixir_ex, caller: false, - prematch: if(Version.match?(System.version(), ">= 1.15.0-dev"), do: :raise, else: :warn), + prematch: %State{ + prematch: + if Version.match?(System.version(), ">= 1.15.0-dev") do + Code.get_compiler_option(:on_undefined_variable) + else + :warn + end + }, stacktrace: false, unused: {%{}, 0}, vars: {%{}, false} @@ -73,7 +80,18 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do end defp expand(ast) do - Compiler.expand(ast, %State{}, Compiler.env()) + Compiler.expand( + ast, + %State{ + prematch: + if Version.match?(System.version(), ">= 1.15.0-dev") do + Code.get_compiler_option(:on_undefined_variable) + else + :warn + end + }, + Compiler.env() + ) end defp elixir_expand(ast) do From b5d01ced47fc8236a77e52ad16bc27fc6dd9317d Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 22 Aug 2024 23:38:31 +0200 Subject: [PATCH 159/235] store env on every node with meta --- lib/elixir_sense/core/compiler.ex | 10 ++++++ .../core/metadata_builder_test.exs | 34 ++++++++++++++----- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index c202c22a..efe69c8c 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -13,6 +13,16 @@ defmodule ElixirSense.Core.Compiler do def expand(ast, state, env) do try do + state = + case ast do + {_, meta, _} when is_list(meta) -> + add_current_env_to_line(state, meta, env) + + # state + _ -> + state + end + do_expand(ast, state, env) catch kind, payload -> diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 434d909a..0b760c81 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -160,9 +160,29 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert Map.has_key?(state.lines_to_env[3].versioned_vars, {:abc, nil}) assert [ - # %VarInfo{name: :abc, positions: [{1, 1}]}, - %VarInfo{name: :abc, positions: [{2, 1}]} + %VarInfo{name: :abc, version: 1, positions: [{2, 1}]} ] = state |> get_line_vars(3) + + assert [ + %VarInfo{name: :abc, version: 0, positions: [{1, 1}]} + ] = state |> get_line_vars(2) + + assert state.vars_info_per_scope_id[0] == %{ + {:abc, 0} => %VarInfo{ + name: :abc, + positions: [{1, 1}], + scope_id: 0, + version: 0, + type: {:integer, 5} + }, + {:abc, 1} => %VarInfo{ + name: :abc, + positions: [{2, 1}], + scope_id: 0, + version: 1, + type: {:local_call, :foo, []} + } + } end test "binding in function call" do @@ -564,7 +584,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert Map.keys(state.lines_to_env[1].versioned_vars) == [] - assert [] = state |> get_line_vars(3) + assert [] = state |> get_line_vars(1) assert Map.keys(state.lines_to_env[2].versioned_vars) == [{:abc, nil}] @@ -585,7 +605,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] = state |> get_line_vars(4) assert Map.keys(state.lines_to_env[6].versioned_vars) == [] - assert [] = state |> get_line_vars(3) + assert [] = state |> get_line_vars(6) end test "for bitstring" do @@ -1686,7 +1706,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert get_line_attributes(state, 4) == [ %AttributeInfo{ name: :myattribute, - positions: [{2, 3}, {3, 16}, {4, 16}], + positions: [{2, 3}, {3, 16}], type: {:tuple, 2, [{:atom, :ok}, {:map, [abc: {:atom, nil}], nil}]} } ] @@ -1694,13 +1714,11 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert [ %VarInfo{ name: :other, - # TODO do we need to rewrite? change Binding - # type: {:local_call, :elem, [{:attribute, :myattribute}, {:integer, 0}]} type: { :call, {:atom, :erlang}, :element, - [integer: 1, attribute: :myattribute] + [{:integer, 1}, {:attribute, :myattribute}] } }, %VarInfo{ From 2ebafdd317a98f70b4f70c7cf0f4628f95da497f Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 22 Aug 2024 23:54:32 +0200 Subject: [PATCH 160/235] fix tests on 1.16 --- lib/elixir_sense/core/compiler/macro.ex | 4 +-- test/elixir_sense/core/compiler_test.exs | 6 ++++ .../metadata_builder/error_recovery_test.exs | 8 ++++++ .../core/metadata_builder_test.exs | 28 ++++++++++--------- 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/lib/elixir_sense/core/compiler/macro.ex b/lib/elixir_sense/core/compiler/macro.ex index b10dd018..62f4f0de 100644 --- a/lib/elixir_sense/core/compiler/macro.ex +++ b/lib/elixir_sense/core/compiler/macro.ex @@ -132,7 +132,7 @@ defmodule ElixirSense.Core.Compiler.Macro do when is_atom(name) and is_list(args) and is_list(meta) do arity = length(args) - case Macro.Env.expand_import(env, meta, name, arity, trace: false, check_deprecations: false) do + case NormalizedMacroEnv.expand_import(env, meta, name, arity, trace: false, check_deprecations: false) do {:macro, _receiver, expander} -> # TODO register call # We don't want the line to propagate yet, but generated might! @@ -169,7 +169,7 @@ defmodule ElixirSense.Core.Compiler.Macro do {original, false} true -> - case Macro.Env.expand_require(env, meta, receiver, name, length(args), + case NormalizedMacroEnv.expand_require(env, meta, receiver, name, length(args), trace: false, check_deprecations: false ) do diff --git a/test/elixir_sense/core/compiler_test.exs b/test/elixir_sense/core/compiler_test.exs index e5e1c9d0..afa74ede 100644 --- a/test/elixir_sense/core/compiler_test.exs +++ b/test/elixir_sense/core/compiler_test.exs @@ -380,6 +380,7 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do assert_expansion("quote do: &inspect/1") end + if Version.match?(System.version(), ">= 1.17.0") do test "expands quote with bind_quoted" do assert_expansion(""" kv = [a: 1] @@ -390,6 +391,7 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do end """) end + end test "expands quote with unquote false" do assert_expansion(""" @@ -399,11 +401,13 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do """) end + if Version.match?(System.version(), ">= 1.17.0") do test "expands quote with file" do assert_expansion(""" quote file: "some.ex", do: bar(1, 2, 3) """) end + end test "expands quote with line" do assert_expansion(""" @@ -445,6 +449,7 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do """) end + if Version.match?(System.version(), ">= 1.17.0") do test "expands &" do assert_expansion("& &1") assert_expansion("&Enum.take(&1, 5)") @@ -458,6 +463,7 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do assert_expansion("&Enum.map(&2, &1)") assert_expansion("&inspect([&2, &1])") end + end test "expands fn" do assert_expansion("fn -> 1 end") diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index 241b1176..864d32d2 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -441,6 +441,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert Enum.any?(env.vars, &(&1.name == :x)) end + if Version.match?(System.version(), ">= 1.17.0") do test "cursor in left side of catch clause after type" do code = """ try do @@ -452,7 +453,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert {meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end + end + if Version.match?(System.version(), ">= 1.17.0") do test "cursor in left side of catch clause 2 arg guard" do code = """ try do @@ -464,6 +467,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert {meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end + end test "cursor in right side of catch clause" do code = """ @@ -777,6 +781,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert Enum.any?(env.vars, &(&1.name == :y)) end + if Version.match?(System.version(), ">= 1.17.0") do test "cursor in do block reduce left side of clause too many args" do code = """ for x <- [], reduce: %{} do @@ -788,6 +793,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do # this test fails # assert Enum.any?(env.vars, &(&1.name == :y)) end + end test "cursor in do block reduce right side of clause" do code = """ @@ -868,6 +874,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert Enum.any?(env.vars, &(&1.name == :x)) end + if Version.match?(System.version(), ">= 1.17.0") do test "incomplete clause left side guard" do code = """ fn @@ -877,6 +884,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert {meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end + end test "incomplete clause right side" do code = """ diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 0b760c81..199d3389 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -3850,8 +3850,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do test "map guards" do assert %VarInfo{name: :x, type: {:map, [], nil}} = var_with_guards("is_map(x)") + if Version.match?(System.version(), ">= 1.17.0") do assert %VarInfo{name: :x, type: {:map, [], nil}} = var_with_guards("is_non_struct_map(x)") + end assert %VarInfo{name: :x, type: {:map, [], nil}} = var_with_guards("map_size(x) == 1") assert %VarInfo{name: :x, type: {:map, [], nil}} = var_with_guards("1 == map_size(x)") @@ -7860,7 +7862,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do kind: :type, name: :foo, positions: [{4, 5}], - end_positions: [{4, 28}], + end_positions: _, generated: [false], specs: ["@type foo() :: unquote(v())"] } @@ -7894,13 +7896,13 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert %{ - {My, :bar, 0} => %ElixirSense.Core.State.TypeInfo{ - name: :bar, + {My, :foo, 0} => %ElixirSense.Core.State.TypeInfo{ + name: :foo, args: [[]], - specs: ["@type bar()"], + specs: ["@type foo"], kind: :type, - positions: [{3, 3}], - end_positions: [{3, 14}], + positions: [{2, 3}], + end_positions: [{2, 12}], generated: [false], doc: "", meta: %{} @@ -7922,7 +7924,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do specs: ["@type baz(a)"], kind: :type, positions: [{4, 3}], - end_positions: [{4, 15}], + end_positions: _, generated: [false], doc: "", meta: %{} @@ -8040,13 +8042,13 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert %{ - {My, :bar, 0} => %ElixirSense.Core.State.SpecInfo{ - name: :bar, + {My, :foo, 0} => %ElixirSense.Core.State.SpecInfo{ + name: :foo, args: [[]], - specs: ["@spec bar()"], + specs: ["@spec foo"], kind: :spec, - positions: [{3, 3}], - end_positions: [{3, 14}], + positions: [{2, 3}], + end_positions: [{2, 12}], generated: [false], doc: "", meta: %{} @@ -8068,7 +8070,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do specs: ["@spec baz(a)"], kind: :spec, positions: [{4, 3}], - end_positions: [{4, 15}], + end_positions: _, generated: [false], doc: "", meta: %{} From 92914f546b37b33ced9ef3189574752a538a2fe4 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 23 Aug 2024 00:21:51 +0200 Subject: [PATCH 161/235] call earlier versions on 1.13 --- lib/elixir_sense/core/normalized/macro/env.ex | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/elixir_sense/core/normalized/macro/env.ex b/lib/elixir_sense/core/normalized/macro/env.ex index 2f354a92..a50a16a0 100644 --- a/lib/elixir_sense/core/normalized/macro/env.ex +++ b/lib/elixir_sense/core/normalized/macro/env.ex @@ -67,7 +67,12 @@ defmodule ElixirSense.Core.Normalized.Macro.Env do fn expansion_meta, args -> quoted = expander.(args, env) next = :elixir_module.next_counter(env.module) + + if Version.match?(System.version(), ">= 1.14.0-dev") do :elixir_quote.linify_with_context_counter(expansion_meta, {receiver, next}, quoted) + else + :elixir_quote.linify_with_context_counter(expansion_meta |> Keyword.get(:line, 0), {receiver, next}, quoted) + end end end @@ -535,7 +540,11 @@ defmodule ElixirSense.Core.Normalized.Macro.Env do _ -> local = allow_locals and + if Version.match?(System.version(), ">= 1.14.0-dev") do :elixir_def.local_for(meta, name, arity, [:defmacro, :defmacrop], e) + else + :elixir_def.local_for(module, name, arity, [:defmacro, :defmacrop]) + end case dispatch do {_, receiver} when local != false and receiver != module -> From b2064c655ecb254f7d3647fe1c60108eb4d0952a Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 23 Aug 2024 00:31:46 +0200 Subject: [PATCH 162/235] 1.12 compatible call --- lib/elixir_sense/core/compiler.ex | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index efe69c8c..18ad0993 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -4972,7 +4972,11 @@ defmodule ElixirSense.Core.Compiler do end defp do_rewrite(:guard, receiver, dot_meta, right, meta, e_args, s) do + if Version.match?(System.version(), ">= 1.13.0-dev") do :elixir_rewrite.guard_rewrite(receiver, dot_meta, right, meta, e_args, guard_context(s)) + else + :elixir_rewrite.guard_rewrite(receiver, dot_meta, right, meta, e_args) + end end defp do_rewrite(_, receiver, dot_meta, right, meta, e_args, _s) do From 4b45a7666ab337c5fed48be9c75050a9de21d0ba Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 23 Aug 2024 00:37:57 +0200 Subject: [PATCH 163/235] Revert "1.12 compatible call" This reverts commit b2064c655ecb254f7d3647fe1c60108eb4d0952a. --- lib/elixir_sense/core/compiler.ex | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 18ad0993..efe69c8c 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -4972,11 +4972,7 @@ defmodule ElixirSense.Core.Compiler do end defp do_rewrite(:guard, receiver, dot_meta, right, meta, e_args, s) do - if Version.match?(System.version(), ">= 1.13.0-dev") do :elixir_rewrite.guard_rewrite(receiver, dot_meta, right, meta, e_args, guard_context(s)) - else - :elixir_rewrite.guard_rewrite(receiver, dot_meta, right, meta, e_args) - end end defp do_rewrite(_, receiver, dot_meta, right, meta, e_args, _s) do From 2d86e650aa9a4f6c494f5f7d687cac80f6febd37 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 23 Aug 2024 00:41:10 +0200 Subject: [PATCH 164/235] drop support for 1.12 --- .github/workflows/ci.yml | 18 ------------------ mix.exs | 2 +- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a88b907..09dde605 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,15 +14,6 @@ jobs: fail-fast: false matrix: include: - - elixir: 1.12.x - otp: 22.x - tests_may_fail: false - - elixir: 1.12.x - otp: 23.x - tests_may_fail: false - - elixir: 1.12.x - otp: 24.x - tests_may_fail: false - elixir: 1.13.x otp: 22.x tests_may_fail: false @@ -87,15 +78,6 @@ jobs: fail-fast: false matrix: include: - - elixir: 1.12.x - otp: 22.x - tests_may_fail: false - - elixir: 1.12.x - otp: 23.x - tests_may_fail: false - - elixir: 1.12.x - otp: 24.x - tests_may_fail: false - elixir: 1.13.x otp: 22.x tests_may_fail: false diff --git a/mix.exs b/mix.exs index d30afbf1..d452fab5 100644 --- a/mix.exs +++ b/mix.exs @@ -6,7 +6,7 @@ defmodule ElixirSense.MixProject do [ app: :elixir_sense, version: "2.0.0", - elixir: "~> 1.12", + elixir: "~> 1.13", elixirc_paths: elixirc_paths(Mix.env()), build_embedded: Mix.env() == :prod, start_permanent: Mix.env() == :prod, From f86d3ca6226a1b61b8048c49e2debab28d7a77fc Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 23 Aug 2024 08:47:59 +0200 Subject: [PATCH 165/235] add 1.17 and OTP27 to matrix --- .github/workflows/ci.yml | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 09dde605..687278ce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,15 @@ jobs: - elixir: 1.16.x otp: 26.x tests_may_fail: false + - elixir: 1.17.x + otp: 25.x + tests_may_fail: false + - elixir: 1.17.x + otp: 26.x + tests_may_fail: false + - elixir: 1.17.x + otp: 27.x + tests_may_fail: false env: MIX_ENV: test steps: @@ -120,6 +129,15 @@ jobs: - elixir: 1.16.x otp: 26.x tests_may_fail: false + - elixir: 1.17.x + otp: 25.x + tests_may_fail: false + - elixir: 1.17.x + otp: 26.x + tests_may_fail: false + - elixir: 1.17.x + otp: 27.x + tests_may_fail: false env: MIX_ENV: test steps: @@ -147,8 +165,8 @@ jobs: strategy: matrix: include: - - elixir: 1.16.x - otp: 26.x + - elixir: 1.17.x + otp: 27.x steps: - uses: actions/checkout@v4 - uses: erlef/setup-beam@v1 From 58920b80ba0eb28005c86c2e767f080edfb88613 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 23 Aug 2024 08:49:03 +0200 Subject: [PATCH 166/235] remove 1.12 code --- lib/elixir_sense/core/ast.ex | 1 + lib/elixir_sense/core/compiler/macro.ex | 5 +- lib/elixir_sense/core/macro_expander.ex | 1 + .../core/normalized/code/formatter.ex | 2 - .../core/normalized/code/fragment.ex | 3 - lib/elixir_sense/core/normalized/macro/env.ex | 20 ++- lib/elixir_sense/core/normalized/tokenizer.ex | 1 - lib/elixir_sense/core/normalized/typespec.ex | 1 - test/elixir_sense/core/compiler_test.exs | 54 +++---- .../elixir_sense/core/macro_expander_test.exs | 16 +- .../metadata_builder/error_recovery_test.exs | 84 +++++------ .../core/metadata_builder_test.exs | 139 ++++++------------ .../metadata_builder/import/only_sigils.ex | 15 +- 13 files changed, 139 insertions(+), 203 deletions(-) diff --git a/lib/elixir_sense/core/ast.ex b/lib/elixir_sense/core/ast.ex index b8556dbd..3e9ba9d5 100644 --- a/lib/elixir_sense/core/ast.ex +++ b/lib/elixir_sense/core/ast.ex @@ -4,6 +4,7 @@ defmodule ElixirSense.Core.Ast do """ # TODO the code in this module is broken and probably violates GPL license + # TODO replace @partials [ :def, diff --git a/lib/elixir_sense/core/compiler/macro.ex b/lib/elixir_sense/core/compiler/macro.ex index 62f4f0de..657ea4f2 100644 --- a/lib/elixir_sense/core/compiler/macro.ex +++ b/lib/elixir_sense/core/compiler/macro.ex @@ -132,7 +132,10 @@ defmodule ElixirSense.Core.Compiler.Macro do when is_atom(name) and is_list(args) and is_list(meta) do arity = length(args) - case NormalizedMacroEnv.expand_import(env, meta, name, arity, trace: false, check_deprecations: false) do + case NormalizedMacroEnv.expand_import(env, meta, name, arity, + trace: false, + check_deprecations: false + ) do {:macro, _receiver, expander} -> # TODO register call # We don't want the line to propagate yet, but generated might! diff --git a/lib/elixir_sense/core/macro_expander.ex b/lib/elixir_sense/core/macro_expander.ex index 930a67f5..59b32825 100644 --- a/lib/elixir_sense/core/macro_expander.ex +++ b/lib/elixir_sense/core/macro_expander.ex @@ -1,3 +1,4 @@ +# TODO replace this module defmodule ElixirSense.Core.MacroExpander do @moduledoc false diff --git a/lib/elixir_sense/core/normalized/code/formatter.ex b/lib/elixir_sense/core/normalized/code/formatter.ex index 800114c4..96c0e0d1 100644 --- a/lib/elixir_sense/core/normalized/code/formatter.ex +++ b/lib/elixir_sense/core/normalized/code/formatter.ex @@ -5,7 +5,6 @@ defmodule ElixirSense.Core.Normalized.Code.Formatter do apply(Code.Formatter, :locals_without_parens, []) true -> - # fall back to bundled on < 1.13 # on 1.13 use our version as it has all the fixes from last 1.13 release apply(ElixirSense.Core.Normalized.Code.ElixirSense.Formatter, :locals_without_parens, []) end @@ -17,7 +16,6 @@ defmodule ElixirSense.Core.Normalized.Code.Formatter do apply(Code.Formatter, :local_without_parens?, [fun, arity, locals_without_parens]) true -> - # fall back to bundled on < 1.13 # on 1.13 use our version as it has all the fixes from last 1.13 release apply(ElixirSense.Core.Normalized.Code.ElixirSense.Formatter, :local_without_parens?, [ fun, diff --git a/lib/elixir_sense/core/normalized/code/fragment.ex b/lib/elixir_sense/core/normalized/code/fragment.ex index 3774b06c..3c41d30e 100644 --- a/lib/elixir_sense/core/normalized/code/fragment.ex +++ b/lib/elixir_sense/core/normalized/code/fragment.ex @@ -8,7 +8,6 @@ defmodule ElixirSense.Core.Normalized.Code.Fragment do apply(Code.Fragment, :cursor_context, [string, opts]) true -> - # fall back to bundled on < 1.13 # on 1.13 use our version as it has all the fixes from last 1.13 release apply(ElixirSense.Core.Normalized.Code.ElixirSense.Fragment, :cursor_context, [ string, @@ -34,7 +33,6 @@ defmodule ElixirSense.Core.Normalized.Code.Fragment do apply(Code.Fragment, :surround_context, [fragment, position, options]) true -> - # fall back to bundled on < 1.13 # on 1.13 use our version as it has all the fixes from last 1.13 release apply(ElixirSense.Core.Normalized.Code.ElixirSense.Fragment, :surround_context, [ fragment, @@ -61,7 +59,6 @@ defmodule ElixirSense.Core.Normalized.Code.Fragment do apply(Code.Fragment, :container_cursor_to_quoted, [fragment, opts]) true -> - # fall back to bundled on < 1.13 # on 1.13 use our version as it has all the fixes from last 1.13 release apply( ElixirSense.Core.Normalized.Code.ElixirSense.Fragment, diff --git a/lib/elixir_sense/core/normalized/macro/env.ex b/lib/elixir_sense/core/normalized/macro/env.ex index a50a16a0..595ac852 100644 --- a/lib/elixir_sense/core/normalized/macro/env.ex +++ b/lib/elixir_sense/core/normalized/macro/env.ex @@ -67,11 +67,15 @@ defmodule ElixirSense.Core.Normalized.Macro.Env do fn expansion_meta, args -> quoted = expander.(args, env) next = :elixir_module.next_counter(env.module) - + if Version.match?(System.version(), ">= 1.14.0-dev") do - :elixir_quote.linify_with_context_counter(expansion_meta, {receiver, next}, quoted) + :elixir_quote.linify_with_context_counter(expansion_meta, {receiver, next}, quoted) else - :elixir_quote.linify_with_context_counter(expansion_meta |> Keyword.get(:line, 0), {receiver, next}, quoted) + :elixir_quote.linify_with_context_counter( + expansion_meta |> Keyword.get(:line, 0), + {receiver, next}, + quoted + ) end end end @@ -540,11 +544,11 @@ defmodule ElixirSense.Core.Normalized.Macro.Env do _ -> local = allow_locals and - if Version.match?(System.version(), ">= 1.14.0-dev") do - :elixir_def.local_for(meta, name, arity, [:defmacro, :defmacrop], e) - else - :elixir_def.local_for(module, name, arity, [:defmacro, :defmacrop]) - end + if Version.match?(System.version(), ">= 1.14.0-dev") do + :elixir_def.local_for(meta, name, arity, [:defmacro, :defmacrop], e) + else + :elixir_def.local_for(module, name, arity, [:defmacro, :defmacrop]) + end case dispatch do {_, receiver} when local != false and receiver != module -> diff --git a/lib/elixir_sense/core/normalized/tokenizer.ex b/lib/elixir_sense/core/normalized/tokenizer.ex index 57aca21b..19954b2e 100644 --- a/lib/elixir_sense/core/normalized/tokenizer.ex +++ b/lib/elixir_sense/core/normalized/tokenizer.ex @@ -18,7 +18,6 @@ defmodule ElixirSense.Core.Normalized.Tokenizer do if Version.match?(System.version(), ">= 1.14.0-dev") do :elixir_tokenizer.tokenize(prefix_charlist, 1, []) else - # fall back to bundled on < 1.13 # on 1.13 use our version as it has all the fixes from last 1.13 release :elixir_sense_tokenizer.tokenize(prefix_charlist, 1, []) end diff --git a/lib/elixir_sense/core/normalized/typespec.ex b/lib/elixir_sense/core/normalized/typespec.ex index 2d7d95bb..38d0b2ce 100644 --- a/lib/elixir_sense/core/normalized/typespec.ex +++ b/lib/elixir_sense/core/normalized/typespec.ex @@ -81,7 +81,6 @@ defmodule ElixirSense.Core.Normalized.Typespec do if Version.match?(System.version(), ">= 1.14.0-dev") do Code.Typespec else - # fall back to bundled on < 1.13 (1.12 is broken on OTP 24) # on 1.13 use our version as it has all the fixes from last 1.13 release ElixirSense.Core.Normalized.Code.ElixirSense.Typespec end diff --git a/test/elixir_sense/core/compiler_test.exs b/test/elixir_sense/core/compiler_test.exs index afa74ede..b6adafe8 100644 --- a/test/elixir_sense/core/compiler_test.exs +++ b/test/elixir_sense/core/compiler_test.exs @@ -381,16 +381,16 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do end if Version.match?(System.version(), ">= 1.17.0") do - test "expands quote with bind_quoted" do - assert_expansion(""" - kv = [a: 1] - quote bind_quoted: [kv: kv] do - Enum.each(kv, fn {k, v} -> - def unquote(k)(), do: unquote(v) - end) + test "expands quote with bind_quoted" do + assert_expansion(""" + kv = [a: 1] + quote bind_quoted: [kv: kv] do + Enum.each(kv, fn {k, v} -> + def unquote(k)(), do: unquote(v) + end) + end + """) end - """) - end end test "expands quote with unquote false" do @@ -402,11 +402,11 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do end if Version.match?(System.version(), ">= 1.17.0") do - test "expands quote with file" do - assert_expansion(""" - quote file: "some.ex", do: bar(1, 2, 3) - """) - end + test "expands quote with file" do + assert_expansion(""" + quote file: "some.ex", do: bar(1, 2, 3) + """) + end end test "expands quote with line" do @@ -450,19 +450,19 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do end if Version.match?(System.version(), ">= 1.17.0") do - test "expands &" do - assert_expansion("& &1") - assert_expansion("&Enum.take(&1, 5)") - assert_expansion("&{&1, &2}") - assert_expansion("&[&1 | &2]") - assert_expansion("&inspect/1") - assert_expansion("&Enum.count/1") - assert_expansion("a = %{}; &a.b(&1)") - assert_expansion("&Enum.count(&1)") - assert_expansion("&inspect(&1)") - assert_expansion("&Enum.map(&2, &1)") - assert_expansion("&inspect([&2, &1])") - end + test "expands &" do + assert_expansion("& &1") + assert_expansion("&Enum.take(&1, 5)") + assert_expansion("&{&1, &2}") + assert_expansion("&[&1 | &2]") + assert_expansion("&inspect/1") + assert_expansion("&Enum.count/1") + assert_expansion("a = %{}; &a.b(&1)") + assert_expansion("&Enum.count(&1)") + assert_expansion("&inspect(&1)") + assert_expansion("&Enum.map(&2, &1)") + assert_expansion("&inspect([&2, &1])") + end end test "expands fn" do diff --git a/test/elixir_sense/core/macro_expander_test.exs b/test/elixir_sense/core/macro_expander_test.exs index 69705aca..22219b00 100644 --- a/test/elixir_sense/core/macro_expander_test.exs +++ b/test/elixir_sense/core/macro_expander_test.exs @@ -14,9 +14,7 @@ defmodule ElixirSense.Core.MacroExpanderTest do |> MacroExpander.add_default_meta() |> MacroExpander.expand_use(MyModule, [], line: 2, column: 1) - if Version.match?(System.version(), ">= 1.13.0") do - assert Macro.to_string(expanded) =~ "defmacro required(var)" - end + assert Macro.to_string(expanded) =~ "defmacro required(var)" end test "expand use with alias" do @@ -35,9 +33,7 @@ defmodule ElixirSense.Core.MacroExpanderTest do column: 1 ) - if Version.match?(System.version(), ">= 1.13.0") do - assert Macro.to_string(expanded) =~ "defmacro required(var)" - end + assert Macro.to_string(expanded) =~ "defmacro required(var)" end test "expand use calling use" do @@ -51,9 +47,7 @@ defmodule ElixirSense.Core.MacroExpanderTest do |> MacroExpander.add_default_meta() |> MacroExpander.expand_use(MyModule, [], line: 2, column: 1) - if Version.match?(System.version(), ">= 1.13.0") do - assert Macro.to_string(expanded) =~ "defmacro bar(var)" - end + assert Macro.to_string(expanded) =~ "defmacro bar(var)" end test "expand use when module does not define __using__ macro" do @@ -67,8 +61,6 @@ defmodule ElixirSense.Core.MacroExpanderTest do |> MacroExpander.add_default_meta() |> MacroExpander.expand_use(MyModule, [], line: 2, column: 1) - if Version.match?(System.version(), ">= 1.13.0") do - assert Macro.to_string(expanded) =~ "require ElixirSenseExample.OverridableBehaviour" - end + assert Macro.to_string(expanded) =~ "require ElixirSenseExample.OverridableBehaviour" end end diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index 864d32d2..d911d323 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -442,31 +442,31 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do end if Version.match?(System.version(), ">= 1.17.0") do - test "cursor in left side of catch clause after type" do - code = """ - try do - bar() - catch - x, \ - """ - - assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, &(&1.name == :x)) - end + test "cursor in left side of catch clause after type" do + code = """ + try do + bar() + catch + x, \ + """ + + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end end if Version.match?(System.version(), ">= 1.17.0") do - test "cursor in left side of catch clause 2 arg guard" do - code = """ - try do - bar() - catch - x, _ when \ - """ - - assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, &(&1.name == :x)) - end + test "cursor in left side of catch clause 2 arg guard" do + code = """ + try do + bar() + catch + x, _ when \ + """ + + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end end test "cursor in right side of catch clause" do @@ -782,17 +782,17 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do end if Version.match?(System.version(), ">= 1.17.0") do - test "cursor in do block reduce left side of clause too many args" do - code = """ - for x <- [], reduce: %{} do - y, \ - """ - - assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, &(&1.name == :x)) - # this test fails - # assert Enum.any?(env.vars, &(&1.name == :y)) - end + test "cursor in do block reduce left side of clause too many args" do + code = """ + for x <- [], reduce: %{} do + y, \ + """ + + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + # this test fails + # assert Enum.any?(env.vars, &(&1.name == :y)) + end end test "cursor in do block reduce right side of clause" do @@ -875,15 +875,15 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do end if Version.match?(System.version(), ">= 1.17.0") do - test "incomplete clause left side guard" do - code = """ - fn - x when \ - """ - - assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, &(&1.name == :x)) - end + test "incomplete clause left side guard" do + code = """ + fn + x when \ + """ + + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end end test "incomplete clause right side" do diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 199d3389..26ccf947 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -3851,8 +3851,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert %VarInfo{name: :x, type: {:map, [], nil}} = var_with_guards("is_map(x)") if Version.match?(System.version(), ">= 1.17.0") do - assert %VarInfo{name: :x, type: {:map, [], nil}} = - var_with_guards("is_non_struct_map(x)") + assert %VarInfo{name: :x, type: {:map, [], nil}} = + var_with_guards("is_non_struct_map(x)") end assert %VarInfo{name: :x, type: {:map, [], nil}} = var_with_guards("map_size(x) == 1") @@ -7967,67 +7967,35 @@ defmodule ElixirSense.Core.MetadataBuilderTest do # if there are callbacks behaviour_info/1 is defined assert state.mods_funs_to_positions[{Proto, :behaviour_info, 1}] != nil - if Version.match?(System.version(), ">= 1.13.0") do - assert %{ - {Proto, :abc, 0} => %ElixirSense.Core.State.SpecInfo{ - args: [[], []], - kind: :spec, - name: :abc, - positions: [{3, 3}, {2, 3}], - end_positions: [{3, 25}, {2, 30}], - generated: [false, false], - specs: ["@spec abc() :: reference()", "@spec abc() :: atom() | integer()"] - }, - {Proto, :my, 1} => %ElixirSense.Core.State.SpecInfo{ - kind: :callback, - name: :my, - args: [["a :: integer()"]], - positions: [{4, 3}], - end_positions: [{4, 37}], - generated: [false], - specs: ["@callback my(a :: integer()) :: atom()"] - }, - {Proto, :other, 1} => %ElixirSense.Core.State.SpecInfo{ - kind: :macrocallback, - name: :other, - args: [["x"]], - positions: [{5, 3}], - end_positions: [_], - generated: [false], - specs: ["@macrocallback other(x) :: Macro.t() when x: integer()"] - } - } = state.specs - else - assert %{ - {Proto, :abc, 0} => %ElixirSense.Core.State.SpecInfo{ - args: [[], []], - kind: :spec, - name: :abc, - positions: [{3, 3}, {2, 3}], - end_positions: [{3, 25}, {2, 30}], - generated: [false, false], - specs: ["@spec abc :: reference", "@spec abc :: atom | integer"] - }, - {Proto, :my, 1} => %ElixirSense.Core.State.SpecInfo{ - kind: :callback, - name: :my, - args: [["a :: integer"]], - positions: [{4, 3}], - end_positions: [{4, 37}], - generated: [false], - specs: ["@callback my(a :: integer) :: atom"] - }, - {Proto, :other, 1} => %ElixirSense.Core.State.SpecInfo{ - kind: :macrocallback, - name: :other, - args: [["x"]], - positions: [{5, 3}], - end_positions: [_], - generated: [false], - specs: ["@macrocallback other(x) :: Macro.t when x: integer"] - } - } = state.specs - end + assert %{ + {Proto, :abc, 0} => %ElixirSense.Core.State.SpecInfo{ + args: [[], []], + kind: :spec, + name: :abc, + positions: [{3, 3}, {2, 3}], + end_positions: [{3, 25}, {2, 30}], + generated: [false, false], + specs: ["@spec abc() :: reference()", "@spec abc() :: atom() | integer()"] + }, + {Proto, :my, 1} => %ElixirSense.Core.State.SpecInfo{ + kind: :callback, + name: :my, + args: [["a :: integer()"]], + positions: [{4, 3}], + end_positions: [{4, 37}], + generated: [false], + specs: ["@callback my(a :: integer()) :: atom()"] + }, + {Proto, :other, 1} => %ElixirSense.Core.State.SpecInfo{ + kind: :macrocallback, + name: :other, + args: [["x"]], + positions: [{5, 3}], + end_positions: [_], + generated: [false], + specs: ["@macrocallback other(x) :: Macro.t() when x: integer()"] + } + } = state.specs end test "registers incomplete specs" do @@ -8091,39 +8059,20 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - if Version.match?(System.version(), ">= 1.13.0") do - assert %{ - {Proto, :abc, 1} => %State.SpecInfo{ - args: [["{%Model.User{}}"]], - specs: [ - "@spec abc({%Model.User{}}) :: [%Model.UserOrder{order: Model.Order.t()}, local_type()]" - ] - } - } = state.specs - else - assert %{ - {Proto, :abc, 1} => %State.SpecInfo{ - args: [["{%Model.User{}}"]], - specs: [ - "@spec abc({%Model.User{}}) :: [%Model.UserOrder{order: Model.Order.t}, local_type()]" - ] - } - } = state.specs - end + assert %{ + {Proto, :abc, 1} => %State.SpecInfo{ + args: [["{%Model.User{}}"]], + specs: [ + "@spec abc({%Model.User{}}) :: [%Model.UserOrder{order: Model.Order.t()}, local_type()]" + ] + } + } = state.specs - if Version.match?(System.version(), ">= 1.13.0") do - assert %{ - {Proto, :local_type, 0} => %State.TypeInfo{ - specs: ["@type local_type() :: Model.User.t()"] - } - } = state.types - else - assert %{ - {Proto, :local_type, 0} => %State.TypeInfo{ - specs: ["@type local_type() :: Model.User.t"] - } - } = state.types - end + assert %{ + {Proto, :local_type, 0} => %State.TypeInfo{ + specs: ["@type local_type() :: Model.User.t()"] + } + } = state.types end end diff --git a/test/support/fixtures/metadata_builder/import/only_sigils.ex b/test/support/fixtures/metadata_builder/import/only_sigils.ex index d6cfea7f..e0047037 100644 --- a/test/support/fixtures/metadata_builder/import/only_sigils.ex +++ b/test/support/fixtures/metadata_builder/import/only_sigils.ex @@ -1,13 +1,6 @@ -if Version.match?(System.version(), ">= 1.13.0") do - defmodule ElixirSenseExample.Fixtures.MetadataBuilder.Import.ImportOnlySigils do - import ElixirSenseExample.Fixtures.MetadataBuilder.Imported, only: :sigils +defmodule ElixirSenseExample.Fixtures.MetadataBuilder.Import.ImportOnlySigils do + import ElixirSenseExample.Fixtures.MetadataBuilder.Imported, only: :sigils - @env __ENV__ - def env, do: @env - end -else - defmodule ElixirSenseExample.Fixtures.MetadataBuilder.Import.ImportOnlySigils do - @env __ENV__ - def env, do: @env - end + @env __ENV__ + def env, do: @env end From d33403615e139097f8ad1598bcb5841e5db5ceea Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 25 Aug 2024 07:35:23 +0200 Subject: [PATCH 167/235] more robust typespec handling --- lib/elixir_sense/core/compiler.ex | 109 +++--- .../metadata_builder/error_recovery_test.exs | 354 +++++++++++++++++- 2 files changed, 419 insertions(+), 44 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index efe69c8c..2aec26ab 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1057,15 +1057,18 @@ defmodule ElixirSense.Core.Compiler do cursor_before? = state.cursor_env != nil {expr, state, env} = __MODULE__.Typespec.expand_type(expr, state, env) - case __MODULE__.Typespec.type_to_signature(expr) do - {name, [_type_arg]} when name in [:required, :optional] -> - raise "type #{name}/#{1} is a reserved type and it cannot be defined" - - {name, type_args} -> + {name, type_args} = __MODULE__.Typespec.type_to_signature(expr) type_args = type_args || [] - if __MODULE__.Typespec.built_in_type?(name, length(type_args)) do - raise "type #{name}/#{length(type_args)} is a built-in type and it cannot be redefined" + name = cond do + name in [:required, :optional] -> + # elixir raises here type #{name}/#{1} is a reserved type and it cannot be defined + :"__#{name}__" + __MODULE__.Typespec.built_in_type?(name, length(type_args)) -> + # elixir raises here type #{name}/#{length(type_args)} is a built-in type and it cannot be redefined + :"__#{name}__" + true -> + name end cursor_after? = state.cursor_env != nil @@ -1091,23 +1094,6 @@ defmodule ElixirSense.Core.Compiler do end {{:@, attr_meta, [{kind, kind_meta, [expr]}]}, state, env} - - :error -> - # elixir throws here - # expand expression in case there's cursor - - state = - state - |> with_typespec({:__unknown__, 0}) - - {expr, state, _tenv} = expand(expr, state, env) - - state = - state - |> with_typespec(nil) - - {{:@, attr_meta, [{kind, kind_meta, [expr]}]}, state, env} - end end defp expand_macro( @@ -1123,8 +1109,7 @@ defmodule ElixirSense.Core.Compiler do cursor_before? = state.cursor_env != nil {expr, state, env} = __MODULE__.Typespec.expand_spec(expr, state, env) - case __MODULE__.Typespec.spec_to_signature(expr) do - {name, type_args} -> + {name, type_args} = __MODULE__.Typespec.spec_to_signature(expr) cursor_after? = state.cursor_env != nil spec = TypeInfo.typespec_to_string(kind, expr) @@ -1164,10 +1149,6 @@ defmodule ElixirSense.Core.Compiler do end {{:@, attr_meta, [{kind, kind_meta, [expr]}]}, state, env} - - :error -> - {{:@, attr_meta, [{kind, kind_meta, [expr]}]}, state, env} - end end defp expand_macro( @@ -4683,16 +4664,27 @@ defmodule ElixirSense.Core.Compiler do when is_atom(name) and name != :"::" and is_atom(context), do: {name, []} + def type_to_signature({:"::", _, [{:__cursor__, _, args}, _]}) + when is_list(args) do + # type name replaced by cursor + {:__unknown__, []} + end + def type_to_signature({:"::", _, [{name, _, args}, _]}) when is_atom(name) and name != :"::", do: {name, args} + def type_to_signature({:__cursor__, _, args}) when is_list(args) do + # type name replaced by cursor + {:__unknown__, []} + end + def type_to_signature({name, _, args}) when is_atom(name) and name != :"::" do # elixir returns :error here, we handle incomplete signatures {name, args} end - def type_to_signature(), do: :error + def type_to_signature(_), do: {:__unknown__, []} def expand_spec(ast, state, env) do # TODO not sure this is correct. Are module vars accessible? @@ -4709,7 +4701,7 @@ defmodule ElixirSense.Core.Compiler do {ast, state, env} end - defp do_expand_spec({:when, meta, [spec, guard]}, state, env) when is_list(guard) do + defp do_expand_spec({:when, meta, [spec, guard]}, state, env) do {spec, guard, state, env} = do_expand_spec(spec, guard, meta, state, env) {{:when, meta, [spec, guard]}, state, env} end @@ -4735,12 +4727,23 @@ defmodule ElixirSense.Core.Compiler do end |> sanitize_args() - guard = if Keyword.keyword?(guard), do: guard, else: [] + {_, state, env} = expand_typespec({name, name_meta, args}, state, env) + + {guard, state, env} = if is_list(guard) do + {guard, state, env} + else + # invalid guard may still have cursor + {_, state, env} = expand_typespec(guard, state, env) + {[], state, env} + end state = - Enum.reduce(guard, state, fn {name, _val}, state -> + Enum.reduce(guard, state, fn {name, _val}, state when is_atom(name) -> # guard is a keyword list so we don't have exact meta on keys add_var_write(state, {name, guard_meta, nil}) + _, state -> + # invalid entry + state end) {args_reverse, state, env} = @@ -4755,13 +4758,17 @@ defmodule ElixirSense.Core.Compiler do {guard_reverse, state, env} = Enum.reduce(guard, {[], state, env}, fn - {_name, {:var, _, context}} = pair, {acc, state, env} when is_atom(context) -> + {name, {:var, _, context}} = pair, {acc, state, env} when is_atom(name) and is_atom(context) -> # special type var {[pair | acc], state, env} - {name, type}, {acc, state, env} -> + {name, type}, {acc, state, env} when is_atom(name) -> {type, state, env} = expand_typespec(type, state, env) {[{name, type} | acc], state, env} + other, {acc, state, env} -> + # there may be cursor in invalid entries + {_type, state, env} = expand_typespec(other, state, env) + {acc, state, env} end) guard = Enum.reverse(guard_reverse) @@ -4769,10 +4776,17 @@ defmodule ElixirSense.Core.Compiler do {{:"::", meta, [{name, name_meta, args}, return]}, guard, state, env} end - defp do_expand_spec(other, guard, _guard_meta, state, env) do - # invalid or incomplete spec - # TODO try to wrap in :: expression - {other, guard, state, env} + defp do_expand_spec(other, guard, guard_meta, state, env) do + case other do + {name, meta, args} when is_atom(name) and name != :"::" -> + # invalid or incomplete spec + # try to wrap in :: expression + do_expand_spec({:"::", meta, [{name, meta, args}, nil]}, guard, guard_meta, state, env) + _ -> + # there may be cursor in invalid entries + {_type, state, env} = expand_typespec(other, state, env) + {other, guard, state, env} + end end defp sanitize_args(args) do @@ -4810,6 +4824,8 @@ defmodule ElixirSense.Core.Compiler do args end + {_, state, env} = expand_typespec({name, name_meta, args}, state, env) + state = Enum.reduce(args, state, fn {name, meta, context}, state when is_atom(name) and is_atom(context) and name != :_ -> @@ -4825,9 +4841,16 @@ defmodule ElixirSense.Core.Compiler do end defp do_expand_type(other, state, env) do - # invalid or incomplete spec - # TODO try to wrap in :: expression - {other, state, env} + case other do + {name, meta, args} when is_atom(name) and name != :"::" -> + # invalid or incomplete type + # try to wrap in :: expression + do_expand_type({:"::", meta, [{name, meta, args}, nil]}, state, env) + _ -> + # there may be cursor in invalid entries + {_type, state, env} = expand_typespec(other, state, env) + {other, state, env} + end end @special_forms [ diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index d911d323..eae49fe4 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -15,7 +15,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do NormalizedCode.Fragment.container_cursor_to_quoted(code, columns: true, token_metadata: true - ) + ) |> dbg end # dbg(ast) @@ -1949,4 +1949,356 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert get_cursor_env(code) end end + + describe "typespec" do + test "in type name" do + code = """ + defmodule Abc do + @type foo\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:__unknown__, 0} + end + + test "in spec name" do + code = """ + defmodule Abc do + @spec foo\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:__unknown__, 0} + end + + test "in type after ::" do + code = """ + defmodule Abc do + @type foo :: \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in spec after ::" do + code = """ + defmodule Abc do + @spec foo :: \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type after :: type" do + code = """ + defmodule Abc do + @type foo :: bar\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type after :: type with | empty" do + code = """ + defmodule Abc do + @type foo :: bar | \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type after :: type with |" do + code = """ + defmodule Abc do + @type foo :: bar | baz\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type after :: type with " do + code = """ + defmodule Abc do + @type foo :: (...\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type after :: type with ->" do + code = """ + defmodule Abc do + @type foo :: (... -> \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type after :: type with map empty" do + code = """ + defmodule Abc do + @type foo :: %{\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type after :: type with map key" do + code = """ + defmodule Abc do + @type foo :: %{bar\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type after :: type with map after key" do + code = """ + defmodule Abc do + @type foo :: %{bar: \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type after :: type with map after =>" do + code = """ + defmodule Abc do + @type foo :: %{:bar => \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type after :: type with map optional" do + code = """ + defmodule Abc do + @type foo :: %{optional(\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type after :: type named empty" do + code = """ + defmodule Abc do + @type foo :: {bar :: \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type after :: type named" do + code = """ + defmodule Abc do + @type foo :: {bar :: baz\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in spec after :: type" do + code = """ + defmodule Abc do + @spec foo :: bar\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type after :: remote type" do + code = """ + defmodule Abc do + @type foo :: Remote.bar\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type after :: type args" do + code = """ + defmodule Abc do + @type foo :: Remote.bar(\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in spec after :: type args" do + code = """ + defmodule Abc do + @spec foo :: Remote.bar(\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type arg empty" do + code = """ + defmodule Abc do + @type foo(\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 1} + end + + test "in spec arg empty" do + code = """ + defmodule Abc do + @spec foo(\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 1} + end + + test "in type arg" do + code = """ + defmodule Abc do + @type foo(bar\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 1} + end + + test "in spec arg" do + code = """ + defmodule Abc do + @spec foo(bar\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 1} + end + + test "in spec arg named empty" do + code = """ + defmodule Abc do + @spec foo(bar :: \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 1} + end + + test "in spec arg named" do + code = """ + defmodule Abc do + @spec foo(bar :: baz\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 1} + end + + test "in type arg next" do + code = """ + defmodule Abc do + @type foo(asd, \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 2} + end + + test "in spec when" do + code = """ + defmodule Abc do + @spec foo(a) :: integer when \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 1} + end + + test "in spec when after :" do + code = """ + defmodule Abc do + @spec foo(a) :: integer when x: \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 1} + end + + test "in spec when after : type" do + code = """ + defmodule Abc do + @spec foo(a) :: integer when x: bar\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 1} + end + + test "in spec when after : type arg" do + code = """ + defmodule Abc do + @spec foo(a) :: integer when x: bar(\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 1} + end + + test "in spec when after : next" do + code = """ + defmodule Abc do + @spec foo(a) :: integer when x: bar(), \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 1} + end + + test "in type invalid expression" do + code = """ + defmodule Abc do + @type [{\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:__unknown__, 0} + end + + test "in spec invalid expression" do + code = """ + defmodule Abc do + @spec [{\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:__unknown__, 0} + end + + test "redefining built in" do + code = """ + defmodule Abc do + @type required(a) :: \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:__required__, 0} + end + end end From e384c09d13a085a9f9a685b15dac5c0b4319b2cd Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 25 Aug 2024 07:40:02 +0200 Subject: [PATCH 168/235] record closest env --- lib/elixir_sense/core/compiler.ex | 10 ++++-- lib/elixir_sense/core/metadata.ex | 10 +++++- lib/elixir_sense/core/metadata_builder.ex | 5 +-- lib/elixir_sense/core/parser.ex | 8 +++-- lib/elixir_sense/core/state.ex | 37 +++++++++++++++++++++-- test/elixir_sense/core/parser_test.exs | 26 ++++++++-------- 6 files changed, 73 insertions(+), 23 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 2aec26ab..7a72c251 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -16,7 +16,9 @@ defmodule ElixirSense.Core.Compiler do state = case ast do {_, meta, _} when is_list(meta) -> - add_current_env_to_line(state, meta, env) + state + |> add_current_env_to_line(meta, env) + |> update_closest_env(meta, env) # state _ -> @@ -2550,7 +2552,8 @@ defmodule ElixirSense.Core.Compiler do calls: after_s.calls, lines_to_env: after_s.lines_to_env, vars_info: after_s.vars_info, - cursor_env: after_s.cursor_env + cursor_env: after_s.cursor_env, + closest_env: after_s.closest_env } call_e = Map.put(e, :context, :match) @@ -2564,7 +2567,8 @@ defmodule ElixirSense.Core.Compiler do calls: s_expr.calls, lines_to_env: s_expr.lines_to_env, vars_info: s_expr.vars_info, - cursor_env: s_expr.cursor_env + cursor_env: s_expr.cursor_env, + closest_env: s_expr.closest_env } end_e = Map.put(ee, :context, Map.get(e, :context)) diff --git a/lib/elixir_sense/core/metadata.ex b/lib/elixir_sense/core/metadata.ex index ba373162..bf136e59 100644 --- a/lib/elixir_sense/core/metadata.ex +++ b/lib/elixir_sense/core/metadata.ex @@ -13,6 +13,7 @@ defmodule ElixirSense.Core.Metadata do source: String.t(), mods_funs_to_positions: State.mods_funs_to_positions_t(), cursor_env: nil | {keyword(), ElixirSense.Core.State.Env.t()}, + closest_env: nil | {{pos_integer, pos_integer}, {non_neg_integer, non_neg_integer}, ElixirSense.Core.State.Env.t()}, lines_to_env: State.lines_to_env_t(), calls: State.calls_t(), vars_info_per_scope_id: State.vars_info_per_scope_id_t(), @@ -27,6 +28,7 @@ defmodule ElixirSense.Core.Metadata do defstruct source: "", mods_funs_to_positions: %{}, cursor_env: nil, + closest_env: nil, lines_to_env: %{}, calls: %{}, vars_info_per_scope_id: %{}, @@ -76,6 +78,7 @@ defmodule ElixirSense.Core.Metadata do ]) # IO.puts(metadata.source) + # dbg(needle) source_with_cursor = prefix <> "__cursor__(#{needle})" <> suffix # IO.puts(source_with_cursor) # dbg(metadata) @@ -122,7 +125,12 @@ defmodule ElixirSense.Core.Metadata do if cursor_env != nil do cursor_env else - get_env(metadata, {line, column}) + case metadata.closest_env do + {pos, dist, env} -> + dbg({pos, dist}) + env + nil -> get_env(metadata, {line, column}) + end end end diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index f5881e77..bdc9b7b8 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -12,12 +12,13 @@ defmodule ElixirSense.Core.MetadataBuilder do Traverses the AST building/retrieving the environment information. It returns a `ElixirSense.Core.State` struct containing the information. """ - @spec build(Macro.t()) :: State.t() - def build(ast) do + @spec build(Macro.t(), nil | {pos_integer, pos_integer}) :: State.t() + def build(ast, cursor_position \\ nil) do {_ast, state, _env} = Compiler.expand( ast, %State{ + cursor_position: cursor_position, prematch: if Version.match?(System.version(), ">= 1.15.0-dev") do Code.get_compiler_option(:on_undefined_variable) diff --git a/lib/elixir_sense/core/parser.ex b/lib/elixir_sense/core/parser.ex index 59c94280..88036c04 100644 --- a/lib/elixir_sense/core/parser.ex +++ b/lib/elixir_sense/core/parser.ex @@ -45,9 +45,12 @@ defmodule ElixirSense.Core.Parser do fallback_to_container_cursor_to_quoted: try_to_fix_parse_error ] - case string_to_ast(source, string_to_ast_options) do + # source_with_cursor = inject_cursor(source, cursor_position) + source_with_cursor = source + + case string_to_ast(source_with_cursor, string_to_ast_options) do {:ok, ast, modified_source, error} -> - acc = MetadataBuilder.build(ast) + acc = MetadataBuilder.build(ast, cursor_position) if cursor_position == nil or acc.cursor_env != nil or Map.has_key?(acc.lines_to_env, elem(cursor_position, 0)) or @@ -188,6 +191,7 @@ defmodule ElixirSense.Core.Parser do structs: acc.structs, mods_funs_to_positions: acc.mods_funs_to_positions, cursor_env: acc.cursor_env, + closest_env: acc.closest_env, lines_to_env: acc.lines_to_env, vars_info_per_scope_id: acc.vars_info_per_scope_id, calls: acc.calls, diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 3e2f09e1..e9e633a6 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -66,8 +66,10 @@ defmodule ElixirSense.Core.State do optional_callbacks_context: list(), lines_to_env: lines_to_env_t, cursor_env: nil | {keyword, ElixirSense.Core.State.Env.t()}, + closest_env: nil | {{pos_integer, pos_integer}, {non_neg_integer, non_neg_integer}, ElixirSense.Core.State.Env.t()}, ex_unit_describe: nil | atom, - attribute_store: %{optional(module) => term} + attribute_store: %{optional(module) => term}, + cursor_position: nil | {pos_integer, pos_integer} } defstruct attributes: [[]], @@ -99,8 +101,10 @@ defmodule ElixirSense.Core.State do optional_callbacks_context: [[]], lines_to_env: %{}, cursor_env: nil, + closest_env: nil, ex_unit_describe: nil, - attribute_store: %{} + attribute_store: %{}, + cursor_position: nil defmodule Env do @moduledoc """ @@ -368,6 +372,35 @@ defmodule ElixirSense.Core.State do %__MODULE__{state | cursor_env: {meta, env}} end + def update_closest_env(%__MODULE__{cursor_position: cursor_position} = state, meta, macro_env) when is_list(meta) and cursor_position != nil do + case Keyword.get(meta, :line, 0) do + line when is_integer(line) and line > 0 -> + column = Keyword.get(meta, :column, 0) + + {cursor_line, cursor_column} = cursor_position + + line_distance = abs(cursor_line - line) + column_distance = abs(cursor_column - column) + + store = case state.closest_env do + nil -> true + {_, {old_line_distance, old_column_distance}, _} -> + line_distance < old_line_distance or line_distance == old_line_distance and column_distance < old_column_distance + end + + if store do + env = get_current_env(state, macro_env) + %__MODULE__{state | closest_env: {{line, column}, {line_distance, column_distance}, env}} + else + state + end + _ -> state + end + end + def update_closest_env(%__MODULE__{} = state, _meta, _macro_env) do + state + end + def add_current_env_to_line(%__MODULE__{} = state, meta, macro_env) when is_list(meta) do do_add_current_env_to_line(state, Keyword.get(meta, :line, 0), macro_env) end diff --git a/test/elixir_sense/core/parser_test.exs b/test/elixir_sense/core/parser_test.exs index 5a21545c..cdd1d1c4 100644 --- a/test/elixir_sense/core/parser_test.exs +++ b/test/elixir_sense/core/parser_test.exs @@ -32,7 +32,7 @@ defmodule ElixirSense.Core.ParserTest do """ assert %Metadata{ - error: nil, + error: {:error, :parse_error}, cursor_env: {_, %Env{functions: functions3, module: MyModule}} } = parse_string(source, true, true, {3, 10}) @@ -65,7 +65,7 @@ defmodule ElixirSense.Core.ParserTest do assert %Metadata{ error: {:error, :parse_error}, - cursor_env: {_, %Env{functions: functions}} + closest_env: {_, _, %Env{functions: functions}} } = parse_string(source, true, true, {3, 8}) assert Keyword.has_key?(functions, List) @@ -81,7 +81,7 @@ defmodule ElixirSense.Core.ParserTest do assert %Metadata{ error: {:error, :parse_error}, - cursor_env: {_, %Env{functions: functions}} + closest_env: {_, _, %Env{functions: functions}} } = parse_string(source, true, true, {3, 11}) assert Keyword.has_key?(functions, List) @@ -97,7 +97,7 @@ defmodule ElixirSense.Core.ParserTest do assert %Metadata{ error: {:error, :parse_error}, - cursor_env: {_, %Env{functions: functions}} + closest_env: {_, _, %Env{functions: functions}} } = parse_string(source, true, true, {3, 12}) assert Keyword.has_key?(functions, List) @@ -151,7 +151,7 @@ defmodule ElixirSense.Core.ParserTest do assert %Metadata{ error: {:error, :parse_error}, - cursor_env: {_, %Env{functions: functions}} + closest_env: {_, _, %Env{functions: functions}} } = parse_string(source, true, true, {3, 12}) assert Keyword.has_key?(functions, List) @@ -167,7 +167,7 @@ defmodule ElixirSense.Core.ParserTest do assert %Metadata{ error: {:error, :parse_error}, - cursor_env: {_, %Env{functions: functions}} + closest_env: {_, _, %Env{functions: functions}} } = parse_string(source, true, true, {3, 12}) assert Keyword.has_key?(functions, List) @@ -183,7 +183,7 @@ defmodule ElixirSense.Core.ParserTest do assert %Metadata{ error: {:error, :parse_error}, - cursor_env: {_, %Env{functions: functions}} + closest_env: {_, _, %Env{functions: functions}} } = parse_string(source, true, true, {3, 12}) assert Keyword.has_key?(functions, List) @@ -224,8 +224,8 @@ defmodule ElixirSense.Core.ParserTest do assert %Metadata{ error: {:error, :parse_error}, - cursor_env: - {_, + closest_env: + {_, _, %Env{ vars: [ %VarInfo{name: :x} @@ -254,7 +254,7 @@ defmodule ElixirSense.Core.ParserTest do assert %Metadata{ error: {:error, :parse_error}, - cursor_env: {_, %Env{module: MyModule}} + closest_env: {_, _, %Env{module: MyModule}} } = parse_string(source, true, true, {3, 1}) assert %Metadata{ @@ -280,7 +280,7 @@ defmodule ElixirSense.Core.ParserTest do assert %Metadata{ error: {:error, :parse_error}, - cursor_env: {_, %Env{module: MyModule}}, + closest_env: {_, _, %Env{module: MyModule}}, lines_to_env: %{ 1 => %Env{module: MyModule}, 3 => %Env{module: MyModule.MyModule1} @@ -388,8 +388,8 @@ defmodule ElixirSense.Core.ParserTest do ''' assert %ElixirSense.Core.Metadata{ - lines_to_env: %{ - 5 => %ElixirSense.Core.State.Env{ + closest_env: { + _, _, %ElixirSense.Core.State.Env{ vars: vars } } From 3477371f3e804b258b988503b1709516640e8110 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 26 Aug 2024 16:47:07 +0200 Subject: [PATCH 169/235] improvements to typespec expansion added tests and resilency --- lib/elixir_sense/core/compiler.ex | 464 ++---------- lib/elixir_sense/core/compiler/typespec.ex | 663 ++++++++++++++++++ lib/elixir_sense/core/metadata.ex | 9 +- lib/elixir_sense/core/state.ex | 35 +- .../core/compiler/typespec_test.exs | 399 +++++++++++ .../metadata_builder/error_recovery_test.exs | 188 ++++- .../core/metadata_builder_test.exs | 50 +- test/elixir_sense/core/parser_test.exs | 6 +- 8 files changed, 1395 insertions(+), 419 deletions(-) create mode 100644 lib/elixir_sense/core/compiler/typespec.ex create mode 100644 test/elixir_sense/core/compiler/typespec_test.exs diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 7a72c251..0c885e21 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1060,42 +1060,45 @@ defmodule ElixirSense.Core.Compiler do {expr, state, env} = __MODULE__.Typespec.expand_type(expr, state, env) {name, type_args} = __MODULE__.Typespec.type_to_signature(expr) - type_args = type_args || [] - - name = cond do - name in [:required, :optional] -> - # elixir raises here type #{name}/#{1} is a reserved type and it cannot be defined - :"__#{name}__" - __MODULE__.Typespec.built_in_type?(name, length(type_args)) -> - # elixir raises here type #{name}/#{length(type_args)} is a built-in type and it cannot be redefined - :"__#{name}__" - true -> - name - end + type_args = type_args || [] - cursor_after? = state.cursor_env != nil + name = + cond do + name in [:required, :optional] -> + # elixir raises here type #{name}/#{1} is a reserved type and it cannot be defined + :"__#{name}__" - # TODO elixir does Macro.escape with unquote: true + __MODULE__.Typespec.built_in_type?(name, length(type_args)) -> + # elixir raises here type #{name}/#{length(type_args)} is a built-in type and it cannot be redefined + :"__#{name}__" - spec = TypeInfo.typespec_to_string(kind, expr) + true -> + name + end - state = - state - |> add_type(env, name, type_args, spec, kind, extract_range(attr_meta)) - |> with_typespec({name, length(type_args)}) - |> add_current_env_to_line(attr_meta, env) - |> with_typespec(nil) + cursor_after? = state.cursor_env != nil - state = - if not cursor_before? and cursor_after? do - {meta, env} = state.cursor_env - env = %{env | typespec: {name, length(type_args)}} - %{state | cursor_env: {meta, env}} - else - state - end + # TODO elixir does Macro.escape with unquote: true + + spec = TypeInfo.typespec_to_string(kind, expr) + + state = + state + |> add_type(env, name, type_args, spec, kind, extract_range(attr_meta)) + |> with_typespec({name, length(type_args)}) + |> add_current_env_to_line(attr_meta, env) + |> with_typespec(nil) - {{:@, attr_meta, [{kind, kind_meta, [expr]}]}, state, env} + state = + if not cursor_before? and cursor_after? do + {meta, env} = state.cursor_env + env = %{env | typespec: {name, length(type_args)}} + %{state | cursor_env: {meta, env}} + else + state + end + + {{:@, attr_meta, [{kind, kind_meta, [expr]}]}, state, env} end defp expand_macro( @@ -1112,45 +1115,45 @@ defmodule ElixirSense.Core.Compiler do {expr, state, env} = __MODULE__.Typespec.expand_spec(expr, state, env) {name, type_args} = __MODULE__.Typespec.spec_to_signature(expr) - cursor_after? = state.cursor_env != nil - spec = TypeInfo.typespec_to_string(kind, expr) + cursor_after? = state.cursor_env != nil + spec = TypeInfo.typespec_to_string(kind, expr) - range = extract_range(attr_meta) + range = extract_range(attr_meta) - state = - if kind in [:callback, :macrocallback] do - state - |> add_func_to_index( - env, - :behaviour_info, - [{:atom, attr_meta, nil}], - range, - :def, - generated: true - ) - else - state - end + state = + if kind in [:callback, :macrocallback] do + state + |> add_func_to_index( + env, + :behaviour_info, + [{:atom, attr_meta, nil}], + range, + :def, + generated: true + ) + else + state + end - type_args = type_args || [] + type_args = type_args || [] - state = - state - |> add_spec(env, name, type_args, spec, kind, range) - |> with_typespec({name, length(type_args)}) - |> add_current_env_to_line(attr_meta, env) - |> with_typespec(nil) + state = + state + |> add_spec(env, name, type_args, spec, kind, range) + |> with_typespec({name, length(type_args)}) + |> add_current_env_to_line(attr_meta, env) + |> with_typespec(nil) - state = - if not cursor_before? and cursor_after? do - {meta, env} = state.cursor_env - env = %{env | typespec: {name, length(type_args)}} - %{state | cursor_env: {meta, env}} - else - state - end + state = + if not cursor_before? and cursor_after? do + {meta, env} = state.cursor_env + env = %{env | typespec: {name, length(type_args)}} + %{state | cursor_env: {meta, env}} + else + state + end - {{:@, attr_meta, [{kind, kind_meta, [expr]}]}, state, env} + {{:@, attr_meta, [{kind, kind_meta, [expr]}]}, state, env} end defp expand_macro( @@ -4494,10 +4497,11 @@ defmodule ElixirSense.Core.Compiler do case validate_struct(e_left, context) do true when is_atom(e_left) -> + # TODO register alias/struct case extract_struct_assocs(e_right) do {:expand, map_meta, assocs} when context != :match -> assoc_keys = Enum.map(assocs, fn {k, _} -> k end) - struct = load_struct(e_left, assocs, se, ee) + struct = load_struct(e_left, [assocs], se, ee) keys = [:__struct__ | assoc_keys] without_keys = Elixir.Map.drop(struct, keys) @@ -4635,350 +4639,36 @@ defmodule ElixirSense.Core.Compiler do Keyword.delete(assocs, :__struct__) end - defp load_struct(name, assocs, s, _e) do + def load_struct(name, assocs, s, _e) do case s.structs[name] do nil -> try do - apply(name, :__struct__, [assocs]) + apply(name, :__struct__, assocs) else %{:__struct__ => ^name} = struct -> struct _ -> # recover from invalid return value - [__struct__: name] |> Keyword.merge(assocs) |> Elixir.Map.new() + [__struct__: name] |> merge_assocs(assocs) rescue _ -> # recover from error by building the fake struct - [__struct__: name] |> Keyword.merge(assocs) |> Elixir.Map.new() + [__struct__: name] |> merge_assocs(assocs) end info -> - info.fields |> Keyword.merge(assocs) |> Elixir.Map.new() - end - end - end - - defmodule Typespec do - alias ElixirSense.Core.Compiler, as: ElixirExpand - def spec_to_signature({:when, _, [spec, _]}), do: type_to_signature(spec) - def spec_to_signature(other), do: type_to_signature(other) - - def type_to_signature({:"::", _, [{name, _, context}, _]}) - when is_atom(name) and name != :"::" and is_atom(context), - do: {name, []} - - def type_to_signature({:"::", _, [{:__cursor__, _, args}, _]}) - when is_list(args) do - # type name replaced by cursor - {:__unknown__, []} - end - - def type_to_signature({:"::", _, [{name, _, args}, _]}) - when is_atom(name) and name != :"::", - do: {name, args} - - def type_to_signature({:__cursor__, _, args}) when is_list(args) do - # type name replaced by cursor - {:__unknown__, []} - end - - def type_to_signature({name, _, args}) when is_atom(name) and name != :"::" do - # elixir returns :error here, we handle incomplete signatures - {name, args} - end - - def type_to_signature(_), do: {:__unknown__, []} - - def expand_spec(ast, state, env) do - # TODO not sure this is correct. Are module vars accessible? - state = - state - |> new_func_vars_scope - - {ast, state, env} = do_expand_spec(ast, state, env) - - state = - state - |> remove_func_vars_scope - - {ast, state, env} - end - - defp do_expand_spec({:when, meta, [spec, guard]}, state, env) do - {spec, guard, state, env} = do_expand_spec(spec, guard, meta, state, env) - {{:when, meta, [spec, guard]}, state, env} - end - - defp do_expand_spec(spec, state, env) do - {spec, _guard, state, env} = do_expand_spec(spec, [], [], state, env) - {spec, state, env} - end - - defp do_expand_spec( - {:"::", meta, [{name, name_meta, args}, return]}, - guard, - guard_meta, - state, - env - ) - when is_atom(name) and name != :"::" do - args = - if is_atom(args) do - [] - else - args - end - |> sanitize_args() - - {_, state, env} = expand_typespec({name, name_meta, args}, state, env) - - {guard, state, env} = if is_list(guard) do - {guard, state, env} - else - # invalid guard may still have cursor - {_, state, env} = expand_typespec(guard, state, env) - {[], state, env} + info.fields |> merge_assocs(assocs) end - - state = - Enum.reduce(guard, state, fn {name, _val}, state when is_atom(name) -> - # guard is a keyword list so we don't have exact meta on keys - add_var_write(state, {name, guard_meta, nil}) - _, state -> - # invalid entry - state - end) - - {args_reverse, state, env} = - Enum.reduce(args, {[], state, env}, fn arg, {acc, state, env} -> - {arg, state, env} = expand_typespec(arg, state, env) - {[arg | acc], state, env} - end) - - args = Enum.reverse(args_reverse) - - {return, state, env} = expand_typespec(return, state, env) - - {guard_reverse, state, env} = - Enum.reduce(guard, {[], state, env}, fn - {name, {:var, _, context}} = pair, {acc, state, env} when is_atom(name) and is_atom(context) -> - # special type var - {[pair | acc], state, env} - - {name, type}, {acc, state, env} when is_atom(name) -> - {type, state, env} = expand_typespec(type, state, env) - {[{name, type} | acc], state, env} - other, {acc, state, env} -> - # there may be cursor in invalid entries - {_type, state, env} = expand_typespec(other, state, env) - {acc, state, env} - end) - - guard = Enum.reverse(guard_reverse) - - {{:"::", meta, [{name, name_meta, args}, return]}, guard, state, env} end - defp do_expand_spec(other, guard, guard_meta, state, env) do - case other do - {name, meta, args} when is_atom(name) and name != :"::" -> - # invalid or incomplete spec - # try to wrap in :: expression - do_expand_spec({:"::", meta, [{name, meta, args}, nil]}, guard, guard_meta, state, env) - _ -> - # there may be cursor in invalid entries - {_type, state, env} = expand_typespec(other, state, env) - {other, guard, state, env} - end + defp merge_assocs(fields, []) do + fields |> Elixir.Map.new() end - defp sanitize_args(args) do - Enum.map(args, fn - {:"::", meta, [left, right]} -> - {:"::", meta, [remove_default(left), remove_default(right)]} - - other -> - remove_default(other) - end) + defp merge_assocs(fields, [assocs]) do + fields |> Keyword.merge(assocs) |> Elixir.Map.new() end - - defp remove_default({:\\, _, [left, _]}), do: left - defp remove_default(other), do: other - - def expand_type(ast, state, env) do - state = - state - |> new_func_vars_scope - - {ast, state, env} = do_expand_type(ast, state, env) - - state = - state - |> remove_func_vars_scope - - {ast, state, env} - end - - defp do_expand_type({:"::", meta, [{name, name_meta, args}, definition]}, state, env) do - args = - if is_atom(args) do - [] - else - args - end - - {_, state, env} = expand_typespec({name, name_meta, args}, state, env) - - state = - Enum.reduce(args, state, fn - {name, meta, context}, state when is_atom(name) and is_atom(context) and name != :_ -> - add_var_write(state, {name, meta, context}) - - _, state -> - # silently skip invalid typespec params - state - end) - - {definition, state, env} = expand_typespec(definition, state, env) - {{:"::", meta, [{name, name_meta, args}, definition]}, state, env} - end - - defp do_expand_type(other, state, env) do - case other do - {name, meta, args} when is_atom(name) and name != :"::" -> - # invalid or incomplete type - # try to wrap in :: expression - do_expand_type({:"::", meta, [{name, meta, args}, nil]}, state, env) - _ -> - # there may be cursor in invalid entries - {_type, state, env} = expand_typespec(other, state, env) - {other, state, env} - end - end - - @special_forms [ - :|, - :<<>>, - :%{}, - :%, - :.., - :->, - :"::", - :+, - :-, - :., - :{}, - :__block__, - :... - ] - - defp expand_typespec(ast, state, env) do - # TODO this should handle remote calls, attributes unquotes? - # TODO attribute remote call should expand attribute - # {{:., meta, [{:@, _, [{attr, _, _}]}, name]}, _, args} - # TODO remote call should expand remote - # {{:., meta, [remote, name]}, _, args} - # TODO expand struct module - # {:%, _, [name, {:%{}, meta, fields}]} - {ast, {state, env}} = - Macro.traverse( - ast, - {state, env}, - fn - {:__cursor__, meta, args}, {state, env} when is_list(args) -> - state = - unless state.cursor_env do - state - |> add_cursor_env(meta, env) - else - state - end - - node = - case args do - [h | _] -> h - [] -> nil - end - - {node, {state, env}} - - {:__aliases__, _meta, list} = node, {state, env} when is_list(list) -> - {node, state, env} = ElixirExpand.expand(node, state, env) - {node, {state, env}} - - {:__MODULE__, _meta, ctx} = node, {state, env} when is_atom(ctx) -> - {node, state, env} = ElixirExpand.expand(node, state, env) - {node, {state, env}} - - {:"::", meta, [{var_name, var_meta, context}, expr]}, {state, env} - when is_atom(var_name) and is_atom(context) -> - # mark as annotation - {{:"::", meta, [{var_name, [{:annotation, true} | var_meta], context}, expr]}, - {state, env}} - - {name, meta, args}, {state, env} - when is_atom(name) and is_atom(args) and name not in @special_forms and - hd(meta) != {:annotation, true} -> - [vars_from_scope | _other_vars] = state.vars_info - - ast = - case Elixir.Map.get(vars_from_scope, {name, nil}) do - nil -> - # add parens to no parens local call - {name, meta, []} - - _ -> - {name, meta, args} - end - - {ast, {state, env}} - - other, acc -> - {other, acc} - end, - fn - {{:., dot_meta, [remote, name]}, meta, args}, {state, env} when is_atom(remote) -> - args = - if is_atom(args) do - [] - else - args - end - - state = add_call_to_line(state, {remote, name, length(args)}, meta) - - {{{:., dot_meta, [remote, name]}, meta, args}, {state, env}} - - {name, meta, args}, {state, env} - when is_atom(name) and is_list(args) and name not in @special_forms -> - state = add_call_to_line(state, {nil, name, length(args)}, meta) - - {{name, meta, args}, {state, env}} - - {name, meta, context} = var, {state, env} - when is_atom(name) and is_atom(context) and hd(meta) != {:annotation, true} -> - state = add_var_read(state, var) - {var, {state, env}} - - other, acc -> - {other, acc} - end - ) - - {ast, state, env} - end - - # TODO Remove char_list type by v2.0 - def built_in_type?(:char_list, 0), do: true - def built_in_type?(:charlist, 0), do: true - def built_in_type?(:as_boolean, 1), do: true - def built_in_type?(:struct, 0), do: true - def built_in_type?(:nonempty_charlist, 0), do: true - def built_in_type?(:keyword, 0), do: true - def built_in_type?(:keyword, 1), do: true - def built_in_type?(:var, 0), do: true - def built_in_type?(name, arity), do: :erl_internal.is_type(name, arity) end defmodule Rewrite do diff --git a/lib/elixir_sense/core/compiler/typespec.ex b/lib/elixir_sense/core/compiler/typespec.ex new file mode 100644 index 00000000..257029bd --- /dev/null +++ b/lib/elixir_sense/core/compiler/typespec.ex @@ -0,0 +1,663 @@ +defmodule ElixirSense.Core.Compiler.Typespec do + alias ElixirSense.Core.Normalized.Macro.Env, as: NormalizedMacroEnv + alias ElixirSense.Core.Compiler, as: ElixirExpand + alias ElixirSense.Core.Compiler.Utils + import ElixirSense.Core.State + def spec_to_signature({:when, _, [spec, _]}), do: type_to_signature(spec) + def spec_to_signature(other), do: type_to_signature(other) + + def type_to_signature({:"::", _, [{name, _, context}, _]}) + when is_atom(name) and name != :"::" and is_atom(context), + do: {name, []} + + def type_to_signature({:"::", _, [{:__cursor__, _, args}, _]}) + when is_list(args) do + # type name replaced by cursor + {:__unknown__, []} + end + + def type_to_signature({:"::", _, [{name, _, args}, _]}) + when is_atom(name) and name != :"::", + do: {name, args} + + def type_to_signature({:__cursor__, _, args}) when is_list(args) do + # type name replaced by cursor + {:__unknown__, []} + end + + def type_to_signature({name, _, args}) when is_atom(name) and name != :"::" do + # elixir returns :error here, we handle incomplete signatures + {name, args} + end + + def type_to_signature(_), do: {:__unknown__, []} + + def expand_spec(ast, state, env) do + # TODO not sure this is correct. Are module vars accessible? + state = + state + |> new_func_vars_scope + + {ast, state, env} = do_expand_spec(ast, state, env) + + state = + state + |> remove_func_vars_scope + + {ast, state, env} + end + + defp do_expand_spec({:when, meta, [spec, guard]}, state, env) do + {spec, guard, state, env} = do_expand_spec(spec, guard, meta, state, env) + {{:when, meta, [spec, guard]}, state, env} + end + + defp do_expand_spec(spec, state, env) do + {spec, _guard, state, env} = do_expand_spec(spec, [], [], state, env) + {spec, state, env} + end + + defp do_expand_spec( + {:"::", meta, [{name, name_meta, args}, return]}, + guard, + guard_meta, + state, + env + ) + when is_atom(name) and name != :"::" do + args = + if is_atom(args) do + [] + else + args + end + |> sanitize_args() + + {_, state} = expand_typespec({name, name_meta, args}, :disabled, state, env) + + {guard, state, env} = + if is_list(guard) do + {guard, state, env} + else + # invalid guard may still have cursor + {_, state} = expand_typespec(guard, :disabled, state, env) + {[], state, env} + end + + {state, var_names} = + Enum.reduce(guard, {state, []}, fn + {name, _val}, {state, var_names} when is_atom(name) -> + # guard is a keyword list so we don't have exact meta on keys + {add_var_write(state, {name, guard_meta, nil}), [name | var_names]} + + _, acc -> + # invalid entry + acc + end) + + {args_reverse, state} = + Enum.reduce(args, {[], state}, fn + arg, {acc, state} -> + {arg, state} = expand_typespec(arg, var_names, state, env) + {[arg | acc], state} + end) + + args = Enum.reverse(args_reverse) + + {return, state} = expand_typespec(return, var_names, state, env) + + {guard_reverse, state} = + Enum.reduce(guard, {[], state}, fn + {name, {:var, _, context}} = pair, {acc, state} when is_atom(name) and is_atom(context) -> + # special type var + {[pair | acc], state} + + {name, type}, {acc, state} when is_atom(name) -> + {type, state} = expand_typespec(type, var_names, state, env) + {[{name, type} | acc], state} + + other, {acc, state} -> + # there may be cursor in invalid entries + {_type, state} = expand_typespec(other, var_names, state, env) + {acc, state} + end) + + guard = Enum.reverse(guard_reverse) + + {{:"::", meta, [{name, name_meta, args}, return]}, guard, state, env} + end + + defp do_expand_spec(other, guard, guard_meta, state, env) do + case other do + {name, meta, args} when is_atom(name) and name != :"::" -> + # invalid or incomplete spec + # try to wrap in :: expression + do_expand_spec({:"::", meta, [{name, meta, args}, nil]}, guard, guard_meta, state, env) + + _ -> + # there may be cursor in invalid entries + {_type, state} = expand_typespec(guard, [], state, env) + {_type, state} = expand_typespec(other, [], state, env) + {other, guard, state, env} + end + end + + defp sanitize_args(args) do + Enum.map(args, fn + {:"::", meta, [left, right]} -> + {:"::", meta, [remove_default(left), remove_default(right)]} + + other -> + remove_default(other) + end) + end + + defp remove_default({:\\, _, [left, _]}), do: left + defp remove_default(other), do: other + + def expand_type(ast, state, env) do + state = + state + |> new_func_vars_scope + + {ast, state, env} = do_expand_type(ast, state, env) + + state = + state + |> remove_func_vars_scope + + {ast, state, env} + end + + defp do_expand_type({:"::", meta, [{name, name_meta, args}, definition]}, state, env) do + args = + if is_atom(args) do + [] + else + args + end + + {_, state} = expand_typespec({name, name_meta, args}, :disabled, state, env) + + {state, var_names} = + Enum.reduce(args, {state, []}, fn + {name, meta, context}, {state, var_names} + when is_atom(name) and is_atom(context) and name != :_ -> + {add_var_write(state, {name, meta, context}), [name | var_names]} + + other, acc -> + # silently skip invalid typespec params + acc + end) + + {definition, state} = expand_typespec(definition, var_names, state, env) + {{:"::", meta, [{name, name_meta, args}, definition]}, state, env} + end + + defp do_expand_type(other, state, env) do + case other do + {name, meta, args} when is_atom(name) and name != :"::" -> + # invalid or incomplete type + # try to wrap in :: expression + do_expand_type({:"::", meta, [{name, meta, args}, nil]}, state, env) + + _ -> + # there may be cursor in invalid entries + {_type, state} = expand_typespec(other, [], state, env) + {other, state, env} + end + end + + def expand_typespec(ast, var_names \\ [], state, env) do + typespec(ast, var_names, env, state) + end + + # TODO Remove char_list type by v2.0 + def built_in_type?(:char_list, 0), do: true + def built_in_type?(:charlist, 0), do: true + def built_in_type?(:as_boolean, 1), do: true + def built_in_type?(:struct, 0), do: true + def built_in_type?(:nonempty_charlist, 0), do: true + def built_in_type?(:keyword, 0), do: true + def built_in_type?(:keyword, 1), do: true + def built_in_type?(:var, 0), do: true + def built_in_type?(name, arity), do: :erl_internal.is_type(name, arity) + + defp typespec({:__cursor__, meta, args}, vars, caller, state) when is_list(args) do + state = + unless state.cursor_env do + state + |> add_cursor_env(meta, caller) + else + state + end + + node = + case args do + [h | _] -> h + [] -> nil + end + + typespec(node, vars, caller, state) + end + + # Handle unions + defp typespec({:|, meta, [left, right]}, vars, caller, state) do + {left, state} = typespec(left, vars, caller, state) + {right, state} = typespec(right, vars, caller, state) + + {{:|, meta, [left, right]}, state} + end + + # Handle binaries + defp typespec({:<<>>, meta, args}, vars, caller, state) do + {args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args) + # elixir does complex binary spec validation + {{:<<>>, meta, args}, state} + end + + ## Handle maps and structs + defp typespec({:%{}, meta, fields} = map, vars, caller, state) do + fun = fn + {{:required, meta2, [k]}, v}, state -> + {arg1, state} = typespec(k, vars, caller, state) + {arg2, state} = typespec(v, vars, caller, state) + {{arg1, arg2}, state} + + {{:optional, meta2, [k]}, v}, state -> + {arg1, state} = typespec(k, vars, caller, state) + {arg2, state} = typespec(v, vars, caller, state) + {{{:optional, meta2, [arg1]}, arg2}, state} + + {k, v}, state -> + {arg1, state} = typespec(k, vars, caller, state) + {arg2, state} = typespec(v, vars, caller, state) + {{arg1, arg2}, state} + + invalid, state -> + # elixir raises here invalid map specification + {_, state} = typespec(invalid, vars, caller, state) + {nil, state} + end + + {fields, state} = :lists.mapfoldl(fun, state, fields) + {{:%{}, meta, fields |> Enum.filter(&(&1 != nil))}, state} + end + + defp typespec({:%, struct_meta, [name, {:%{}, meta, fields}]} = node, vars, caller, state) do + case ElixirExpand.Macro.expand(name, %{caller | function: {:__info__, 1}}) do + module when is_atom(module) -> + # TODO register alias/struct + struct = + ElixirExpand.Map.load_struct(module, [], state, caller) + |> Map.delete(:__struct__) + |> Map.to_list() + + {fields, state} = + fields + |> Enum.reverse() + |> Enum.reduce({[], state}, fn + {k, v}, {fields, state} when is_atom(k) -> + {[{k, v} | fields], state} + + other, {fields, state} -> + # elixir raises expected key-value pairs in struct + {_, state} = typespec(other, vars, caller, state) + {fields, state} + end) + + types = + :lists.map( + fn + {:__exception__ = field, true} -> {field, Keyword.get(fields, field, true)} + {field, _} -> {field, Keyword.get(fields, field, quote(do: term()))} + end, + :lists.sort(struct) + ) + + # look for cursor in invalid fields + # elixir raises if there are any + state = + fields + |> Enum.filter(fn {field, _} -> not Keyword.has_key?(struct, field) end) + |> Enum.reduce(state, fn {_, type}, acc -> + {_, acc} = typespec(type, vars, caller, acc) + acc + end) + + {map, state} = typespec({:%{}, meta, types}, vars, caller, state) + {{:%, struct_meta, [module, map]}, state} + + other -> + # elixir raises here unexpected expression in typespec + {name, state} = typespec(other, vars, caller, state) + {map, state} = typespec({:%{}, meta, fields}, vars, caller, state) + {{:%, struct_meta, [name, map]}, state} + end + end + + # Handle records + defp typespec({:record, meta, [atom]}, vars, caller, state) do + typespec({:record, meta, [atom, []]}, vars, caller, state) + end + + defp typespec({:record, meta, [tag, field_specs]}, vars, caller, state) + when is_atom(tag) and is_list(field_specs) do + # We cannot set a function name to avoid tracking + # as a compile time dependency because for records it actually is one. + case ElixirExpand.Macro.expand({tag, [], [{:{}, [], []}]}, caller) do + {_, _, [name, fields | _]} when is_list(fields) -> + types = + :lists.map( + fn {field, _} -> + {:"::", [], + [ + {field, [], nil}, + Keyword.get(field_specs, field, quote(do: term())) + ]} + end, + fields + ) + + # look for cursor in invalid fields + # elixir raises if there are any + state = + field_specs + |> Enum.filter(fn {field, _} -> not Keyword.has_key?(fields, field) end) + |> Enum.reduce(state, fn {_, type}, acc -> + {_, acc} = typespec(type, vars, caller, acc) + acc + end) + + typespec({:{}, meta, [name | types]}, vars, caller, state) + + _ -> + # elixir raises here + {field_specs, state} = + :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, field_specs) + + {{:record, meta, [tag, field_specs]}, state} + end + end + + # Handle ranges + defp typespec({:.., meta, [left, right]}, vars, caller, state) do + {left, state} = typespec(left, vars, caller, state) + {right, state} = typespec(right, vars, caller, state) + # elixir validates range here + + {{:.., meta, [left, right]}, state} + end + + # Handle special forms + defp typespec({:__MODULE__, _, atom}, vars, caller, state) when is_atom(atom) do + typespec(caller.module, vars, caller, state) + end + + defp typespec({:__aliases__, _, _} = alias, vars, caller, state) do + typespec(expand_remote(alias, caller), vars, caller, state) + end + + # Handle funs + defp typespec([{:->, meta, [args, return]}], vars, caller, state) + when is_list(args) do + {args, state} = fn_args(args, vars, caller, state) + {spec, state} = typespec(return, vars, caller, state) + + {[{:->, meta, [args, spec]}], state} + end + + # Handle type operator + defp typespec( + {:"::", meta, [{var_name, var_meta, context}, expr]} = ann_type, + vars, + caller, + state + ) + when is_atom(var_name) and is_atom(context) do + # elixir warns if :: is nested + {right, state} = typespec(expr, vars, caller, state) + {{:"::", meta, [{var_name, var_meta, context}, right]}, state} + end + + defp typespec({:"::", meta, [left, right]}, vars, caller, state) do + # elixir warns here + # invalid type annotation. The left side of :: must be a variable + + {left, state} = typespec(left, vars, caller, state) + {right, state} = typespec(right, vars, caller, state) + {{:"::", meta, [left, right]}, state} + end + + # Handle remote calls in the form of @module_attribute.type. + # These are not handled by the general remote type clause as calling + # Macro.expand/2 on the remote does not expand module attributes (but expands + # things like __MODULE__). + defp typespec( + {{:., dot_meta, [{:@, _, [{attr, _, _}]}, name]}, meta, args} = orig, + vars, + caller, + state + ) do + # TODO Module.get_attribute(caller.module, attr) + # TODO register attribute access + case Map.get(state.attribute_store, {caller.module, attr}) do + remote when is_atom(remote) and remote != nil -> + {remote_spec, state} = typespec(remote, vars, caller, state) + {name_spec, state} = typespec(name, vars, caller, state) + remote_type({{:., dot_meta, [remote_spec, name_spec]}, meta, args}, vars, caller, state) + + _ -> + # elixir raises here invalid remote in typespec + {name_spec, state} = typespec(name, vars, caller, state) + remote_type({{:., dot_meta, [nil, name_spec]}, meta, args}, vars, caller, state) + end + end + + # Handle remote calls + defp typespec({{:., dot_meta, [remote, name]}, meta, args} = orig, vars, caller, state) do + remote = expand_remote(remote, caller) + + if remote == caller.module do + typespec({name, dot_meta, args}, vars, caller, state) + else + # elixir raises if remote is not atom + {remote_spec, state} = typespec(remote, vars, caller, state) + {name_spec, state} = typespec(name, vars, caller, state) + remote_type({{:., dot_meta, [remote_spec, name_spec]}, meta, args}, vars, caller, state) + end + end + + # Handle tuples + defp typespec({left, right}, vars, caller, state) do + {left, state} = typespec(left, vars, caller, state) + {right, state} = typespec(right, vars, caller, state) + {{left, right}, state} + end + + defp typespec({:{}, meta, args}, vars, caller, state) do + {args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args) + + {{:{}, meta, args}, state} + end + + # Handle blocks + defp typespec({:__block__, _meta, [arg]}, vars, caller, state) do + typespec(arg, vars, caller, state) + end + + # Handle variables or local calls + defp typespec({name, meta, atom} = node, :disabled, caller, state) when is_atom(atom) do + {{name, meta, atom}, state} + end + + defp typespec({:_, meta, arg}, _vars, _caller, state) when not is_list(arg) do + {{:_, meta, arg}, state} + end + + defp typespec({name, meta, atom} = node, vars, caller, state) when is_atom(atom) do + if :lists.member(name, vars) do + state = add_var_read(state, node) + {{name, meta, atom}, state} + else + typespec({name, meta, []}, vars, caller, state) + end + end + + # Handle local calls + + defp typespec({type, _meta, []}, vars, caller, state) when type in [:charlist, :char_list] do + typespec(quote(do: :elixir.charlist()), vars, caller, state) + end + + defp typespec({:nonempty_charlist, _meta, []}, vars, caller, state) do + typespec(quote(do: :elixir.nonempty_charlist()), vars, caller, state) + end + + defp typespec({:struct, _meta, []}, vars, caller, state) do + typespec(quote(do: :elixir.struct()), vars, caller, state) + end + + defp typespec({:as_boolean, _meta, [arg]}, vars, caller, state) do + typespec(quote(do: :elixir.as_boolean(unquote(arg))), vars, caller, state) + end + + defp typespec({:keyword, _meta, args}, vars, caller, state) when length(args) <= 1 do + typespec(quote(do: :elixir.keyword(unquote_splicing(args))), vars, caller, state) + end + + defp typespec({name, meta, args}, :disabled, caller, state) when is_atom(name) do + {args, state} = :lists.mapfoldl(&typespec(&1, :disabled, caller, &2), state, args) + {{name, meta, args}, state} + end + + defp typespec({name, meta, args}, vars, caller, state) when is_atom(name) do + {args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args) + # elixir raises if type is not defined + + state = add_call_to_line(state, {nil, name, length(args)}, meta) + + {{name, meta, args}, state} + end + + # Handle literals + defp typespec(atom, _, _, state) when is_atom(atom) do + {atom, state} + end + + defp typespec(integer, _, _, state) when is_integer(integer) do + {integer, state} + end + + defp typespec([], vars, caller, state) do + {[], state} + end + + defp typespec([{:..., meta, _}], vars, caller, state) do + typespec({:nonempty_list, [], [{:any, meta, []}]}, vars, caller, state) + end + + defp typespec([spec, {:..., _, _}], vars, caller, state) do + typespec({:nonempty_list, [], [spec]}, vars, caller, state) + end + + defp typespec([spec], vars, caller, state) do + typespec({:list, [], [spec]}, vars, caller, state) + end + + defp typespec(list, vars, caller, state) when is_list(list) do + {list_reversed, state} = + Enum.reduce(list, {[], state}, fn + {k, v}, {acc, state} when is_atom(k) -> + {[{k, v} | acc], state} + + other, {acc, state} -> + # elixir raises on invalid list entries + {_, state} = typespec(other, vars, caller, state) + {acc, state} + end) + + case list_reversed do + [head | tail] -> + union = + :lists.foldl( + fn elem, acc -> {:|, [], [elem, acc]} end, + head, + tail + ) + + typespec({:list, [], [union]}, vars, caller, state) + + [] -> + {[], state} + end + end + + defp typespec(other, vars, caller, state) do + # elixir raises here unexpected expression in typespec + {_, state} = + if Utils.has_cursor?(other) do + typespec({:__cursor__, [], []}, vars, caller, state) + else + {nil, state} + end + + {nil, state} + end + + defp location(meta) do + line = Keyword.get(meta, :line, 0) + + if column = Keyword.get(meta, :column) do + {line, column} + else + line + end + end + + # TODO trace alias? + defdelegate expand_remote(other, env), to: ElixirSense.Core.Compiler.Macro, as: :expand + + defp remote_type({{:., dot_meta, [remote_spec, name_spec]}, meta, args}, vars, caller, state) do + {args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args) + state = add_call_to_line(state, {remote_spec, name_spec, length(args)}, meta) + {{{:., dot_meta, [remote_spec, name_spec]}, meta, args}, state} + end + + defp fn_args(args, vars, caller, state) do + :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args) + end + + # def load_struct(name, assocs, s, _e) do + # case s.structs[name] do + # nil -> + # try do + # apply(name, :__struct__, [assocs]) + # else + # %{:__struct__ => ^name} = struct -> + # struct + + # _ -> + # # recover from invalid return value + # [__struct__: name] |> Keyword.merge(assocs) |> Elixir.Map.new() + # rescue + # _ -> + # # recover from error by building the fake struct + # [__struct__: name] |> Keyword.merge(assocs) |> Elixir.Map.new() + # end + + # info -> + # info.fields |> Keyword.merge(assocs) |> Elixir.Map.new() + # end + # end + + # def struct!(module, env) when is_atom(module) do + # if module == env.module do + # Module.get_attribute(module, :__struct__) + # end || + # case :elixir_map.maybe_load_struct([line: env.line], module, [], [], env) do + # {:ok, struct} -> struct + # {:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc)) + # end + # end +end diff --git a/lib/elixir_sense/core/metadata.ex b/lib/elixir_sense/core/metadata.ex index bf136e59..ac3fab13 100644 --- a/lib/elixir_sense/core/metadata.ex +++ b/lib/elixir_sense/core/metadata.ex @@ -13,7 +13,10 @@ defmodule ElixirSense.Core.Metadata do source: String.t(), mods_funs_to_positions: State.mods_funs_to_positions_t(), cursor_env: nil | {keyword(), ElixirSense.Core.State.Env.t()}, - closest_env: nil | {{pos_integer, pos_integer}, {non_neg_integer, non_neg_integer}, ElixirSense.Core.State.Env.t()}, + closest_env: + nil + | {{pos_integer, pos_integer}, {non_neg_integer, non_neg_integer}, + ElixirSense.Core.State.Env.t()}, lines_to_env: State.lines_to_env_t(), calls: State.calls_t(), vars_info_per_scope_id: State.vars_info_per_scope_id_t(), @@ -129,7 +132,9 @@ defmodule ElixirSense.Core.Metadata do {pos, dist, env} -> dbg({pos, dist}) env - nil -> get_env(metadata, {line, column}) + + nil -> + get_env(metadata, {line, column}) end end end diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index e9e633a6..f3eef8e1 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -66,9 +66,12 @@ defmodule ElixirSense.Core.State do optional_callbacks_context: list(), lines_to_env: lines_to_env_t, cursor_env: nil | {keyword, ElixirSense.Core.State.Env.t()}, - closest_env: nil | {{pos_integer, pos_integer}, {non_neg_integer, non_neg_integer}, ElixirSense.Core.State.Env.t()}, + closest_env: + nil + | {{pos_integer, pos_integer}, {non_neg_integer, non_neg_integer}, + ElixirSense.Core.State.Env.t()}, ex_unit_describe: nil | atom, - attribute_store: %{optional(module) => term}, + attribute_store: %{optional({module, atom}) => term}, cursor_position: nil | {pos_integer, pos_integer} } @@ -372,7 +375,8 @@ defmodule ElixirSense.Core.State do %__MODULE__{state | cursor_env: {meta, env}} end - def update_closest_env(%__MODULE__{cursor_position: cursor_position} = state, meta, macro_env) when is_list(meta) and cursor_position != nil do + def update_closest_env(%__MODULE__{cursor_position: cursor_position} = state, meta, macro_env) + when is_list(meta) and cursor_position != nil do case Keyword.get(meta, :line, 0) do line when is_integer(line) and line > 0 -> column = Keyword.get(meta, :column, 0) @@ -382,21 +386,32 @@ defmodule ElixirSense.Core.State do line_distance = abs(cursor_line - line) column_distance = abs(cursor_column - column) - store = case state.closest_env do - nil -> true - {_, {old_line_distance, old_column_distance}, _} -> - line_distance < old_line_distance or line_distance == old_line_distance and column_distance < old_column_distance - end + store = + case state.closest_env do + nil -> + true + + {_, {old_line_distance, old_column_distance}, _} -> + line_distance < old_line_distance or + (line_distance == old_line_distance and column_distance < old_column_distance) + end if store do env = get_current_env(state, macro_env) - %__MODULE__{state | closest_env: {{line, column}, {line_distance, column_distance}, env}} + + %__MODULE__{ + state + | closest_env: {{line, column}, {line_distance, column_distance}, env} + } else state end - _ -> state + + _ -> + state end end + def update_closest_env(%__MODULE__{} = state, _meta, _macro_env) do state end diff --git a/test/elixir_sense/core/compiler/typespec_test.exs b/test/elixir_sense/core/compiler/typespec_test.exs new file mode 100644 index 00000000..4f67ed8f --- /dev/null +++ b/test/elixir_sense/core/compiler/typespec_test.exs @@ -0,0 +1,399 @@ +defmodule ElixirSense.Core.Compiler.TypespecTest do + use ExUnit.Case, async: true + alias ElixirSense.Core.Compiler.Typespec + alias ElixirSense.Core.Compiler + alias ElixirSense.Core.State + alias ElixirSense.Core.Normalized.Macro.Env, as: NormalizedMacroEnv + + defp default_state, + do: %State{ + prematch: + if Version.match?(System.version(), ">= 1.15.0-dev") do + Code.get_compiler_option(:on_undefined_variable) + else + :warn + end + } + + defp expand_typespec(ast, vars \\ [], state \\ default_state(), env \\ Compiler.env()) do + Typespec.expand_typespec(ast, vars, state, env) + end + + describe "expand_typespec" do + test "literal" do + assert {:foo, _state} = expand_typespec(:foo) + assert {1, _state} = expand_typespec(1) + assert {[], _state} = expand_typespec([]) + assert {{:nonempty_list, [], [{:any, [], []}]}, _state} = expand_typespec([{:..., [], []}]) + assert {{:nonempty_list, [], [:foo]}, _state} = expand_typespec([:foo, {:..., [], []}]) + assert {{:list, [], [:foo]}, _state} = expand_typespec([:foo]) + + assert {{:list, [], [{:|, [], [foo: 1, bar: :baz]}]}, _state} = + expand_typespec(foo: 1, bar: :baz) + + # invalid + assert {{:list, [], [{:|, [], [foo: 1, bar: :baz]}]}, _state} = + expand_typespec([0, {:foo, 1}, {:bar, :baz}]) + end + + test "local type" do + assert {{:local_type, [], []}, _state} = expand_typespec({:local_type, [], []}) + + assert {{:local_type, [], [:foo, 1]}, _state} = + expand_typespec({:local_type, [], [:foo, 1]}) + end + + test "local type no parens" do + assert {{:foo, [], []}, _state} = expand_typespec({:foo, [], nil}) + end + + test "var" do + assert {{:foo, [], nil}, _state} = expand_typespec({:foo, [], nil}, [:foo]) + end + + test "named ..." do + assert {{:..., [], []}, _state} = expand_typespec({:..., [], []}) + end + + test "fun" do + assert {{:fun, [], [:foo, 1]}, _state} = expand_typespec({:fun, [], [:foo, 1]}) + assert {{:fun, [], []}, _state} = expand_typespec({:fun, [], []}) + + assert {[{:->, [], [[:foo], 1]}], _state} = expand_typespec([{:->, [], [[:foo], 1]}]) + + assert {[{:->, [], [[:foo, :bar], 1]}], _state} = + expand_typespec([{:->, [], [[:foo, :bar], 1]}]) + + assert {[{:->, [], [[{:..., [], []}], 1]}], _state} = + expand_typespec([{:->, [], [[{:..., [], []}], 1]}]) + + assert {[{:->, [], [[{:..., [], []}], {:any, [], []}]}], _state} = + expand_typespec([{:->, [], [[{:..., [], []}], {:any, [], []}]}]) + end + + test "charlist" do + assert {{{:., [], [:elixir, :charlist]}, [], []}, _state} = + expand_typespec({:charlist, [], []}) + + assert {{{:., [], [:elixir, :charlist]}, [], []}, _state} = + expand_typespec({:char_list, [], []}) + + assert {{{:., [], [:elixir, :nonempty_charlist]}, [], []}, _state} = + expand_typespec({:nonempty_charlist, [], []}) + end + + test "struct" do + assert {{{:., [], [:elixir, :struct]}, [], []}, _state} = expand_typespec({:struct, [], []}) + end + + test "as_boolean" do + assert {{{:., [], [:elixir, :as_boolean]}, [], [:foo]}, _state} = + expand_typespec({:as_boolean, [], [:foo]}) + end + + test "keyword" do + assert {{{:., [], [:elixir, :keyword]}, [], []}, _state} = + expand_typespec({:keyword, [], []}) + + assert {{{:., [], [:elixir, :keyword]}, [], [:foo]}, _state} = + expand_typespec({:keyword, [], [:foo]}) + end + + test "string" do + assert {{:string, [], []}, _state} = expand_typespec({:string, [], []}) + assert {{:nonempty_string, [], []}, _state} = expand_typespec({:nonempty_string, [], []}) + end + + test "__block__" do + assert {:foo, _state} = expand_typespec({:__block__, [], [:foo]}) + end + + test "tuple" do + assert {{:{}, [], [:foo]}, _state} = expand_typespec({:{}, [], [:foo]}) + assert {{:foo, :bar}, _state} = expand_typespec({:foo, :bar}) + assert {{:tuple, [], []}, _state} = expand_typespec({:tuple, [], []}) + end + + test "remote" do + assert {{{:., [], [:some, :remote]}, [], [:foo]}, _state} = + expand_typespec({{:., [], [:some, :remote]}, [], [:foo]}) + + assert {{{:., [], [Foo.Bar, :remote]}, [], [:foo]}, _state} = + expand_typespec( + {{:., [], [{:__aliases__, [], [:Foo, :Bar]}, :remote]}, [], [:foo]} + ) + + env = %{Compiler.env() | module: Foo.Bar} + + assert {{:remote, [], [:foo]}, _state} = + expand_typespec( + {{:., [], [{:__aliases__, [], [:Foo, :Bar]}, :remote]}, [], [:foo]}, + [], + default_state(), + env + ) + + env = %{Compiler.env() | aliases: [{Foo, Foo.Bar}]} + + assert {{{:., [], [Foo.Bar, :remote]}, [], [:foo]}, _state} = + expand_typespec( + {{:., [], [{:__aliases__, [], [:Foo]}, :remote]}, [], [:foo]}, + [], + default_state(), + env + ) + + env = %{Compiler.env() | module: Foo.Bar} + + assert {{:remote, [], [:foo]}, _state} = + expand_typespec( + {{:., [], [{:__MODULE__, [], nil}, :remote]}, [], [:foo]}, + [], + default_state(), + env + ) + + env = %{Compiler.env() | module: Foo.Bar} + state = %{default_state() | attribute_store: %{{Foo.Bar, :baz} => :some}} + + assert {{{:., [], [:some, :remote]}, [], [:foo]}, _state} = + expand_typespec( + {{:., [], [{:@, [], [{:baz, [], nil}]}, :remote]}, [], [:foo]}, + [], + state, + env + ) + + assert {{{:., [], [nil, :remote]}, [], [:foo]}, _state} = + expand_typespec( + {{:., [], [{:@, [], [{:baz, [], nil}]}, :remote]}, [], [:foo]}, + [], + default_state(), + env + ) + + # invalid + assert {{{:., [], [1, :remote]}, [], [:foo]}, _state} = + expand_typespec({{:., [], [1, :remote]}, [], [:foo]}) + end + + test "unary op" do + assert {{:+, [], [1]}, _state} = expand_typespec({:+, [], [1]}) + assert {{:-, [], [1]}, _state} = expand_typespec({:-, [], [1]}) + end + + test "special forms" do + env = %{Compiler.env() | module: Foo.Bar} + assert {Foo.Bar, _state} = expand_typespec({:__MODULE__, [], nil}, [], default_state(), env) + + env = %{Compiler.env() | aliases: [{Foo, Foo.Bar}]} + + assert {Foo.Bar, _state} = + expand_typespec({:__aliases__, [], [:Foo]}, [], default_state(), env) + end + + test "annotated type" do + assert {{:"::", [], [{:some, [], nil}, {:any, [], []}]}, _state} = + expand_typespec({:"::", [], [{:some, [], nil}, {:any, [], nil}]}) + + # invalid + assert {{:"::", [], [1, {:any, [], []}]}, _state} = + expand_typespec({:"::", [], [1, {:any, [], nil}]}) + + # invalid nested + assert {{ + :"::", + [], + [{:some, [], nil}, {:"::", [], [{:other, [], nil}, {:any, [], []}]}] + }, + _state} = + expand_typespec( + {:"::", [], + [{:some, [], nil}, {:"::", [], [{:other, [], nil}, {:any, [], nil}]}]} + ) + end + + test "range" do + assert {{:.., [], [1, 10]}, _state} = expand_typespec({:.., [], [1, 10]}) + end + end + + test "union" do + assert {{:|, [], [{:some, [], []}, {:any, [], []}]}, _state} = + expand_typespec({:|, [], [{:some, [], nil}, {:any, [], nil}]}) + + assert {{ + :|, + [], + [{:some, [], []}, {:|, [], [{:other, [], []}, {:any, [], []}]}] + }, + _state} = + expand_typespec( + {:|, [], [{:some, [], nil}, {:|, [], [{:other, [], nil}, {:any, [], nil}]}]} + ) + end + + test "map" do + assert {{:map, [], []}, _state} = expand_typespec({:map, [], []}) + assert {{:map, [], []}, _state} = expand_typespec({:map, [], nil}) + assert {{:%{}, [], []}, _state} = expand_typespec({:%{}, [], []}) + + assert {{:%{}, [], [foo: :bar]}, _state} = expand_typespec({:%{}, [], [foo: :bar]}) + + assert {{:%{}, [], [{{:optional, [], [:foo]}, :bar}]}, _state} = + expand_typespec({:%{}, [], [{{:optional, [], [:foo]}, :bar}]}) + + assert {{:%{}, [], [foo: :bar]}, _state} = + expand_typespec({:%{}, [], [{{:required, [], [:foo]}, :bar}]}) + + # illegal update + assert {{:%{}, [], []}, _state} = + expand_typespec({:%{}, [], [{:|, [], [{:s, [], nil}, [asd: 324]]}]}) + end + + test "struct" do + assert {{ + :%, + [], + [ + Date, + {:%{}, [], + [ + calendar: {:term, [], []}, + day: {:term, [], []}, + month: {:term, [], []}, + year: {:term, [], []} + ]} + ] + }, _state} = expand_typespec({:%, [], [{:__aliases__, [], [:Date]}, {:%{}, [], []}]}) + + assert {{ + :%, + [], + [ + Date, + {:%{}, [], + [ + calendar: {:term, [], []}, + day: :foo, + month: {:term, [], []}, + year: {:term, [], []} + ]} + ] + }, + _state} = + expand_typespec({:%, [], [{:__aliases__, [], [:Date]}, {:%{}, [], [day: :foo]}]}) + + # non atom key + assert {{ + :%, + [], + [ + Date, + {:%{}, [], + [ + calendar: {:term, [], []}, + day: {:term, [], []}, + month: {:term, [], []}, + year: {:term, [], []} + ]} + ] + }, + _state} = + expand_typespec({:%, [], [{:__aliases__, [], [:Date]}, {:%{}, [], [{"day", :foo}]}]}) + + # invalid key + assert {{ + :%, + [], + [ + Date, + {:%{}, [], + [ + calendar: {:term, [], []}, + day: {:term, [], []}, + month: {:term, [], []}, + year: {:term, [], []} + ]} + ] + }, + _state} = + expand_typespec({:%, [], [{:__aliases__, [], [:Date]}, {:%{}, [], [{:baz, :foo}]}]}) + + # non atom + assert {{:%, [], [1, {:%{}, [], []}]}, _} = expand_typespec({:%, [], [1, {:%{}, [], []}]}) + + # unknown + assert {{:%, [], [UnknownStruct, {:%{}, [], []}]}, _} = + expand_typespec({:%, [], [{:__aliases__, [], [:UnknownStruct]}, {:%{}, [], []}]}) + end + + test "binaries" do + type = {:<<>>, [], []} + assert {^type, _state} = expand_typespec(type) + type = {:<<>>, [], [{:"::", [], [{:_, [], nil}, {:*, [], [{:_, [], nil}, 8]}]}]} + assert {^type, _state} = expand_typespec(type) + type = {:<<>>, [], [{:"::", [], [{:_, [], nil}, 8]}]} + assert {^type, _state} = expand_typespec(type) + + type = { + :<<>>, + [], + [ + {:"::", [], [{:_, [], nil}, 32]}, + {:"::", [], [{:_, [], nil}, {:*, [], [{:_, [], nil}, 8]}]} + ] + } + + assert {^type, _state} = expand_typespec(type) + end + + test "records" do + {:ok, env} = + Compiler.env() + |> NormalizedMacroEnv.define_import([], ElixirSenseExample.ModuleWithRecord, trace: false) + + assert {{ + :{}, + [], + [ + :user, + {:"::", [], [{:name, [], nil}, {:term, [], []}]}, + {:"::", [], [{:age, [], nil}, {:term, [], []}]} + ] + }, _state} = expand_typespec({:record, [], [:user]}, [], default_state(), env) + + assert {{ + :{}, + [], + [ + :user, + {:"::", [], [{:name, [], nil}, {:term, [], []}]}, + {:"::", [], [{:age, [], nil}, :foo]} + ] + }, + _state} = + expand_typespec({:record, [], [:user, [age: :foo]]}, [], default_state(), env) + + # invalid record + assert {{:record, [], [1, []]}, _} = expand_typespec({:record, [], [1]}) + + # invalid field + assert {{ + :{}, + [], + [ + :user, + {:"::", [], [{:name, [], nil}, {:term, [], []}]}, + {:"::", [], [{:age, [], nil}, {:term, [], []}]} + ] + }, + _state} = + expand_typespec({:record, [], [:user, [invalid: :foo]]}, [], default_state(), env) + + # unknown record + assert {{:record, [], [:foo, []]}, _} = expand_typespec({:record, [], [:foo]}) + + # TODO make it work with metadata records + end +end diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index eae49fe4..e44949fe 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -15,7 +15,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do NormalizedCode.Fragment.container_cursor_to_quoted(code, columns: true, token_metadata: true - ) |> dbg + ) end # dbg(ast) @@ -2021,7 +2021,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert env.typespec == {:foo, 0} end - test "in type after :: type with " do + test "in type after :: type with fun" do code = """ defmodule Abc do @type foo :: (...\ @@ -2031,7 +2031,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert env.typespec == {:foo, 0} end - test "in type after :: type with ->" do + test "in type after :: type with fun ->" do code = """ defmodule Abc do @type foo :: (... -> \ @@ -2041,6 +2041,36 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert env.typespec == {:foo, 0} end + test "in type after :: type with fun -> no arg" do + code = """ + defmodule Abc do + @type foo :: (-> \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type after :: type with fun (" do + code = """ + defmodule Abc do + @type foo :: (\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type after :: type with fun ( nex arg" do + code = """ + defmodule Abc do + @type foo :: (bar, \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + test "in type after :: type with map empty" do code = """ defmodule Abc do @@ -2298,7 +2328,157 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) - assert env.typespec == {:__required__, 0} + assert env.typespec == {:__required__, 1} + end + + test "in type list" do + code = """ + defmodule Abc do + @type foo :: [\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type list next" do + code = """ + defmodule Abc do + @type foo :: [:foo, \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type list keyword" do + code = """ + defmodule Abc do + @type foo :: [foo: \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type tuple" do + code = """ + defmodule Abc do + @type foo :: {\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type tuple next" do + code = """ + defmodule Abc do + @type foo :: {:foo, \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type union" do + code = """ + defmodule Abc do + @type foo :: :foo | \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type bitstring" do + code = """ + defmodule Abc do + @type foo :: <<\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type bitstring after ::" do + code = """ + defmodule Abc do + @type foo :: <<_::\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + # test "in type bitstring next" do + # code = """ + # defmodule Abc do + # @type foo :: <<_::, \ + # """ + + # assert {_, env} = get_cursor_env(code) + # assert env.typespec == {:foo, 0} + # end + + test "in type bitstring next after" do + code = """ + defmodule Abc do + @type foo :: <<_::size, _::_*\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type struct" do + code = """ + defmodule Abc do + @type foo :: %\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type struct {}" do + code = """ + defmodule Abc do + @type foo :: %Date{\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type struct key" do + code = """ + defmodule Abc do + @type foo :: %Date{key: \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "in type range" do + code = """ + defmodule Abc do + @type foo :: 1..\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end + + test "type with underscored arg" do + code = """ + defmodule Abc do + @type foo(_) :: 1..\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 1} end end end diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 26ccf947..efc58510 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -5621,7 +5621,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do {Proto, :__protocol__, 1} => %ElixirSense.Core.State.SpecInfo{ kind: :spec, specs: [ - "@spec __protocol__(:impls) :: :not_consolidated | {:consolidated, [module()]}", + "@spec __protocol__(:impls) :: :not_consolidated | {:consolidated, list(module())}", "@spec __protocol__(:consolidated?) :: boolean()", "@spec __protocol__(:functions) :: unquote(Protocol.__functions_spec__(@__functions__()))", "@spec __protocol__(:module) :: Proto" @@ -6727,6 +6727,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do defmodule MyStruct do defstruct [:some_field, a_field: 1] IO.puts "" + %Date{month: 1, day: 1, year: 1} end """ |> string_to_state @@ -7869,7 +7870,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } = state.types end - test "skips types with unquote fragments in call" do + # TODO check if variables are available in unquote + test "store types as unknown when unquote fragments in call" do state = """ defmodule My do @@ -7881,7 +7883,19 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert %{} == state.types + assert %{ + {My, :__unknown__, 0} => %ElixirSense.Core.State.TypeInfo{ + name: :__unknown__, + args: [[]], + specs: ["@type unquote(k)() :: 123"], + kind: :type, + positions: [{4, 5}], + end_positions: [{4, 30}], + generated: [false], + doc: "", + meta: %{hidden: true} + } + } == state.types end test "registers incomplete types" do @@ -7899,7 +7913,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do {My, :foo, 0} => %ElixirSense.Core.State.TypeInfo{ name: :foo, args: [[]], - specs: ["@type foo"], + specs: ["@type foo() :: nil"], kind: :type, positions: [{2, 3}], end_positions: [{2, 12}], @@ -7910,7 +7924,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do {My, :bar, 0} => %ElixirSense.Core.State.TypeInfo{ name: :bar, args: [[]], - specs: ["@type bar()"], + specs: ["@type bar() :: nil"], kind: :type, positions: [{3, 3}], end_positions: [{3, 14}], @@ -7921,7 +7935,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do {My, :baz, 1} => %ElixirSense.Core.State.TypeInfo{ name: :baz, args: [["a"]], - specs: ["@type baz(a)"], + specs: ["@type baz(a) :: nil"], kind: :type, positions: [{4, 3}], end_positions: _, @@ -8004,7 +8018,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do defmodule My do @spec foo @spec bar() - @spec baz(a) + @spec baz(number) end """ |> string_to_state @@ -8013,7 +8027,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do {My, :foo, 0} => %ElixirSense.Core.State.SpecInfo{ name: :foo, args: [[]], - specs: ["@spec foo"], + specs: ["@spec foo() :: nil"], kind: :spec, positions: [{2, 3}], end_positions: [{2, 12}], @@ -8024,7 +8038,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do {My, :bar, 0} => %ElixirSense.Core.State.SpecInfo{ name: :bar, args: [[]], - specs: ["@spec bar()"], + specs: ["@spec bar() :: nil"], kind: :spec, positions: [{3, 3}], end_positions: [{3, 14}], @@ -8034,8 +8048,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do }, {My, :baz, 1} => %ElixirSense.Core.State.SpecInfo{ name: :baz, - args: [["a"]], - specs: ["@spec baz(a)"], + args: [["number()"]], + specs: ["@spec baz(number()) :: nil"], kind: :spec, positions: [{4, 3}], end_positions: _, @@ -8049,21 +8063,29 @@ defmodule ElixirSense.Core.MetadataBuilderTest do test "specs and types expand aliases" do state = """ + defmodule Model.User do + defstruct name: nil + end + + defmodule Model.UserOrder do + defstruct order: nil + end + defmodule Proto do alias Model.User alias Model.Order alias Model.UserOrder @type local_type() :: User.t - @spec abc({%User{}}) :: [%UserOrder{order: Order.t}, local_type()] + @spec abc({%User{}}) :: {%UserOrder{order: Order.t}, local_type()} end """ |> string_to_state assert %{ {Proto, :abc, 1} => %State.SpecInfo{ - args: [["{%Model.User{}}"]], + args: [["{%Model.User{name: term()}}"]], specs: [ - "@spec abc({%Model.User{}}) :: [%Model.UserOrder{order: Model.Order.t()}, local_type()]" + "@spec abc({%Model.User{name: term()}}) :: {%Model.UserOrder{order: Model.Order.t()}, local_type()}" ] } } = state.specs diff --git a/test/elixir_sense/core/parser_test.exs b/test/elixir_sense/core/parser_test.exs index cdd1d1c4..1c0de7e5 100644 --- a/test/elixir_sense/core/parser_test.exs +++ b/test/elixir_sense/core/parser_test.exs @@ -388,8 +388,10 @@ defmodule ElixirSense.Core.ParserTest do ''' assert %ElixirSense.Core.Metadata{ - closest_env: { - _, _, %ElixirSense.Core.State.Env{ + closest_env: { + _, + _, + %ElixirSense.Core.State.Env{ vars: vars } } From 82833115a33a8d81e0f89f6a5dff38a09e4ca612 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 26 Aug 2024 21:48:14 +0200 Subject: [PATCH 170/235] add tests --- lib/elixir_sense/core/compiler/typespec.ex | 42 +++------------ .../core/metadata_builder_test.exs | 52 +++++++++++++++++++ 2 files changed, 59 insertions(+), 35 deletions(-) diff --git a/lib/elixir_sense/core/compiler/typespec.ex b/lib/elixir_sense/core/compiler/typespec.ex index 257029bd..50b9098c 100644 --- a/lib/elixir_sense/core/compiler/typespec.ex +++ b/lib/elixir_sense/core/compiler/typespec.ex @@ -434,13 +434,17 @@ defmodule ElixirSense.Core.Compiler.Typespec do # Macro.expand/2 on the remote does not expand module attributes (but expands # things like __MODULE__). defp typespec( - {{:., dot_meta, [{:@, _, [{attr, _, _}]}, name]}, meta, args} = orig, + {{:., dot_meta, [{:@, attr_meta, [{attr, _, _}]}, name]}, meta, args} = orig, vars, caller, state ) do # TODO Module.get_attribute(caller.module, attr) - # TODO register attribute access + state = + state + |> add_attribute(caller, attr, attr_meta, nil, nil, false) + |> add_call_to_line({Kernel, :@, 0}, attr_meta) + case Map.get(state.attribute_store, {caller.module, attr}) do remote when is_atom(remote) and remote != nil -> {remote_spec, state} = typespec(remote, vars, caller, state) @@ -616,6 +620,7 @@ defmodule ElixirSense.Core.Compiler.Typespec do end # TODO trace alias? + # TODO trace calls in expand defdelegate expand_remote(other, env), to: ElixirSense.Core.Compiler.Macro, as: :expand defp remote_type({{:., dot_meta, [remote_spec, name_spec]}, meta, args}, vars, caller, state) do @@ -627,37 +632,4 @@ defmodule ElixirSense.Core.Compiler.Typespec do defp fn_args(args, vars, caller, state) do :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args) end - - # def load_struct(name, assocs, s, _e) do - # case s.structs[name] do - # nil -> - # try do - # apply(name, :__struct__, [assocs]) - # else - # %{:__struct__ => ^name} = struct -> - # struct - - # _ -> - # # recover from invalid return value - # [__struct__: name] |> Keyword.merge(assocs) |> Elixir.Map.new() - # rescue - # _ -> - # # recover from error by building the fake struct - # [__struct__: name] |> Keyword.merge(assocs) |> Elixir.Map.new() - # end - - # info -> - # info.fields |> Keyword.merge(assocs) |> Elixir.Map.new() - # end - # end - - # def struct!(module, env) when is_atom(module) do - # if module == env.module do - # Module.get_attribute(module, :__struct__) - # end || - # case :elixir_map.maybe_load_struct([line: env.line], module, [], [], env) do - # {:ok, struct} -> struct - # {:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc)) - # end - # end end diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index efc58510..d27e0605 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -8096,6 +8096,58 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } } = state.types end + + defmodule TypespecMacros do + defmacro some() do + quote do + Foo + end + end + end + + test "specs and types expand macros in remote type" do + state = + """ + defmodule Proto do + require ElixirSense.Core.MetadataBuilderTest.TypespecMacros, as: TypespecMacros + @type local_type() :: TypespecMacros.some().foo(integer()) + end + """ + |> string_to_state + + assert %{ + {Proto, :local_type, 0} => %State.TypeInfo{ + specs: ["@type local_type() :: Foo.foo(integer())"] + } + } = state.types + end + + test "specs and types expand attributes in remote type" do + state = + """ + defmodule Proto do + @some Remote.Module + @type local_type() :: @some.foo(integer()) + IO.puts "" + end + """ + |> string_to_state + + assert %{ + {Proto, :local_type, 0} => %State.TypeInfo{ + specs: ["@type local_type() :: Remote.Module.foo(integer())"] + } + } = state.types + + assert [ + %AttributeInfo{ + positions: [{2, 3}, {3, 25}] + } + ] = state.lines_to_env[4].attributes + + assert [%CallInfo{position: {3, 25}}, %CallInfo{position: {3, 3}}] = + state.calls[3] |> Enum.filter(&(&1.func == :@)) + end end describe "defrecord" do From 0e5f29edaa395ddce20bf8918a932a69ed4e0026 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 26 Aug 2024 22:13:30 +0200 Subject: [PATCH 171/235] do not expand builtin types --- lib/elixir_sense/core/compiler/typespec.ex | 21 ------------------- .../core/compiler/typespec_test.exs | 14 ++++++------- 2 files changed, 7 insertions(+), 28 deletions(-) diff --git a/lib/elixir_sense/core/compiler/typespec.ex b/lib/elixir_sense/core/compiler/typespec.ex index 50b9098c..3c216f11 100644 --- a/lib/elixir_sense/core/compiler/typespec.ex +++ b/lib/elixir_sense/core/compiler/typespec.ex @@ -509,27 +509,6 @@ defmodule ElixirSense.Core.Compiler.Typespec do end # Handle local calls - - defp typespec({type, _meta, []}, vars, caller, state) when type in [:charlist, :char_list] do - typespec(quote(do: :elixir.charlist()), vars, caller, state) - end - - defp typespec({:nonempty_charlist, _meta, []}, vars, caller, state) do - typespec(quote(do: :elixir.nonempty_charlist()), vars, caller, state) - end - - defp typespec({:struct, _meta, []}, vars, caller, state) do - typespec(quote(do: :elixir.struct()), vars, caller, state) - end - - defp typespec({:as_boolean, _meta, [arg]}, vars, caller, state) do - typespec(quote(do: :elixir.as_boolean(unquote(arg))), vars, caller, state) - end - - defp typespec({:keyword, _meta, args}, vars, caller, state) when length(args) <= 1 do - typespec(quote(do: :elixir.keyword(unquote_splicing(args))), vars, caller, state) - end - defp typespec({name, meta, args}, :disabled, caller, state) when is_atom(name) do {args, state} = :lists.mapfoldl(&typespec(&1, :disabled, caller, &2), state, args) {{name, meta, args}, state} diff --git a/test/elixir_sense/core/compiler/typespec_test.exs b/test/elixir_sense/core/compiler/typespec_test.exs index 4f67ed8f..5e4ecdca 100644 --- a/test/elixir_sense/core/compiler/typespec_test.exs +++ b/test/elixir_sense/core/compiler/typespec_test.exs @@ -72,30 +72,30 @@ defmodule ElixirSense.Core.Compiler.TypespecTest do end test "charlist" do - assert {{{:., [], [:elixir, :charlist]}, [], []}, _state} = + assert {{:charlist, [], []}, _state} = expand_typespec({:charlist, [], []}) - assert {{{:., [], [:elixir, :charlist]}, [], []}, _state} = + assert {{:char_list, [], []}, _state} = expand_typespec({:char_list, [], []}) - assert {{{:., [], [:elixir, :nonempty_charlist]}, [], []}, _state} = + assert {{:nonempty_charlist, [], []}, _state} = expand_typespec({:nonempty_charlist, [], []}) end test "struct" do - assert {{{:., [], [:elixir, :struct]}, [], []}, _state} = expand_typespec({:struct, [], []}) + assert {{:struct, [], []}, _state} = expand_typespec({:struct, [], []}) end test "as_boolean" do - assert {{{:., [], [:elixir, :as_boolean]}, [], [:foo]}, _state} = + assert {{:as_boolean, [], [:foo]}, _state} = expand_typespec({:as_boolean, [], [:foo]}) end test "keyword" do - assert {{{:., [], [:elixir, :keyword]}, [], []}, _state} = + assert {{:keyword, [], []}, _state} = expand_typespec({:keyword, [], []}) - assert {{{:., [], [:elixir, :keyword]}, [], [:foo]}, _state} = + assert {{:keyword, [], [:foo]}, _state} = expand_typespec({:keyword, [], [:foo]}) end From 675dfad120efa6b6098c96f3c4d50f3eeb0f6b34 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 28 Aug 2024 08:04:15 +0200 Subject: [PATCH 172/235] wrap defs in try if there are other blocks than do --- lib/elixir_sense/core/compiler.ex | 19 +++ lib/elixir_sense/core/state.ex | 3 + .../metadata_builder/error_recovery_test.exs | 128 ++++++++++++++++++ .../core/metadata_builder_test.exs | 24 ++++ 4 files changed, 174 insertions(+) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 0c885e21..b3c8de8e 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1677,6 +1677,25 @@ defmodule ElixirSense.Core.Compiler do # TODO not sure vars scope is needed state = state |> new_vars_scope + # if there are other blocks besides do: wrap in try + expr = + case expr do + nil -> + # function head + nil + + [do: do_block] -> + do_block + + _ -> + if is_list(expr) and Keyword.has_key?(expr, :do) do + {:try, [{:origin, def_kind} | meta], [expr]} + else + # elixir raises here + expr + end + end + {_e_body, state, _env_for_expand} = expand(expr, state, env_for_expand) diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index f3eef8e1..b9a0a7a1 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -120,6 +120,7 @@ defmodule ElixirSense.Core.State do requires: list(module), aliases: list(ElixirSense.Core.State.alias_t()), macro_aliases: [{module, {term, module}}], + context: nil | :match | :guard, module: nil | module, function: nil | {atom, arity}, protocol: nil | ElixirSense.Core.State.protocol_t(), @@ -146,6 +147,7 @@ defmodule ElixirSense.Core.State do attributes: [], behaviours: [], context_modules: [], + context: nil, typespec: nil, scope_id: nil end @@ -360,6 +362,7 @@ defmodule ElixirSense.Core.State do module: macro_env.module, function: macro_env.function, context_modules: macro_env.context_modules, + context: macro_env.context, vars: vars, versioned_vars: versioned_vars, attributes: current_attributes, diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index e44949fe..90d6f481 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -2481,4 +2481,132 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert env.typespec == {:foo, 1} end end + + describe "def" do + test "in def" do + code = """ + defmodule Abc do + def\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.module == Abc + end + + test "in def name" do + code = """ + defmodule Abc do + def foo\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.function == {:__unknown__, 0} + end + + test "in def args" do + code = """ + defmodule Abc do + def foo(\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.function == {:foo, 1} + end + + test "in def arg" do + code = """ + defmodule Abc do + def foo(some\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.function == {:foo, 1} + end + + test "in def arg next" do + code = """ + defmodule Abc do + def foo(some, \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.function == {:foo, 2} + end + + test "in def after args" do + code = """ + defmodule Abc do + def foo(some) \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.function == {:foo, 1} + end + + test "in def after," do + code = """ + defmodule Abc do + def foo(some), \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.function == {:foo, 1} + end + + test "in def after do:" do + code = """ + defmodule Abc do + def foo(some), do: \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.function == {:foo, 1} + end + + test "in def after do" do + code = """ + defmodule Abc do + def foo(some) do + \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.function == {:foo, 1} + end + + test "in def guard" do + code = """ + defmodule Abc do + def foo(some) when \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.function == {:foo, 1} + assert env.context == :guard + end + + test "in def guard variale" do + code = """ + defmodule Abc do + def foo(some) when some\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.function == {:foo, 1} + assert env.context == :guard + end + + test "in def after block" do + code = """ + defmodule Abc do + def foo(some) do + :ok + after + \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.function == {:foo, 1} + end + end end diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index d27e0605..09ad131f 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -964,6 +964,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert Map.keys(state.lines_to_env[7].versioned_vars) == [{:k, nil}, {:kv, nil}, {:v, nil}] # TODO we are not tracking usages in typespec + # TODO defdelegate + # TODO defguard 1.18 assert [ %VarInfo{name: :k, positions: [{3, 21}]}, %VarInfo{name: :kv, positions: [{2, 3}, {3, 13}]}, @@ -2550,6 +2552,28 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] = state |> get_line_vars(18) end + test "def rescue binding" do + state = + """ + defmodule MyModule do + def some() do + Some.call() + rescue + e0 in ArgumentError -> + IO.puts "" + end + end + """ + |> string_to_state + + assert [ + %VarInfo{ + name: :e0, + type: {:struct, [], {:atom, ArgumentError}, nil} + } + ] = state |> get_line_vars(6) + end + test "vars binding by pattern matching with pin operators" do state = """ From 17c1bfcf5ca7e129f355749d157de85d5c14d5d0 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 28 Aug 2024 08:04:43 +0200 Subject: [PATCH 173/235] simplify clauses expansion --- lib/elixir_sense/core/compiler.ex | 157 ++++++++++----------- lib/elixir_sense/core/compiler/typespec.ex | 29 ++-- 2 files changed, 84 insertions(+), 102 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index b3c8de8e..1364d717 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -432,7 +432,7 @@ defmodule ElixirSense.Core.Compiler do defp do_expand({:cond, meta, [opts]}, s, e) do # elixir raises underscore_in_cond if the last clause is _ - {e_clauses, sc, ec} = __MODULE__.Clauses.cond(meta, opts, s, e) + {e_clauses, sc, ec} = __MODULE__.Clauses.cond(opts, s, e) {{:cond, meta, [e_clauses]}, sc, ec} end @@ -441,12 +441,12 @@ defmodule ElixirSense.Core.Compiler do end defp do_expand({:receive, meta, [opts]}, s, e) do - {e_clauses, sc, ec} = __MODULE__.Clauses.receive(meta, opts, s, e) + {e_clauses, sc, ec} = __MODULE__.Clauses.receive(opts, s, e) {{:receive, meta, [e_clauses]}, sc, ec} end defp do_expand({:try, meta, [opts]}, s, e) do - {e_clauses, sc, ec} = __MODULE__.Clauses.try(meta, opts, s, e) + {e_clauses, sc, ec} = __MODULE__.Clauses.try(opts, s, e) {{:try, meta, [e_clauses]}, sc, ec} end @@ -1577,7 +1577,7 @@ defmodule ElixirSense.Core.Compiler do when is_atom(name) do # transform protocol def to def with empty body {ast, state, env} = - expand_macro(meta, Kernel, :def, [call, {:__block__, [], []}], callback, state, env) + expand_macro(meta, Kernel, :def, [call, nil], callback, state, env) {ast, state, env} end @@ -1585,7 +1585,7 @@ defmodule ElixirSense.Core.Compiler do defp expand_macro(meta, Kernel, def_kind, [call], callback, state, env) when def_kind in [:def, :defp, :defmacro, :defmacrop, :defguard, :defguardp] do # transform guard and function head to def with empty body - expand_macro(meta, Kernel, def_kind, [call, {:__block__, [], []}], callback, state, env) + expand_macro(meta, Kernel, def_kind, [call, nil], callback, state, env) end defp expand_macro( @@ -1677,7 +1677,6 @@ defmodule ElixirSense.Core.Compiler do # TODO not sure vars scope is needed state = state |> new_vars_scope - # if there are other blocks besides do: wrap in try expr = case expr do nil -> @@ -1685,10 +1684,15 @@ defmodule ElixirSense.Core.Compiler do nil [do: do_block] -> + # do block only do_block _ -> if is_list(expr) and Keyword.has_key?(expr, :do) do + # do block with receive/catch/else/after + # wrap in try + # NOTE origin kind may be not correct here but origin is not used and + # elixir uses it only for error messages in elixir_clauses module {:try, [{:origin, def_kind} | meta], [expr]} else # elixir raises here @@ -2159,7 +2163,7 @@ defmodule ElixirSense.Core.Compiler do # TODO not sure new vars scope is actually needed sc = sc |> new_vars_scope - {e_expr, se, ee} = expand_for_do_block(meta, expr, sc, ec, maybe_reduce) + {e_expr, se, ee} = expand_for_do_block(expr, sc, ec, maybe_reduce) se = se @@ -2170,15 +2174,15 @@ defmodule ElixirSense.Core.Compiler do __MODULE__.Env.merge_vars(se, s, ee), e} end - defp expand_for_do_block(meta, [{:->, _, _} | _] = clauses, s, e, false) do + defp expand_for_do_block([{:->, _, _} | _] = clauses, s, e, false) do # elixir raises here for_without_reduce_bad_block # try to recover from error by emitting fake reduce - expand_for_do_block(meta, clauses, s, e, {:reduce, []}) + expand_for_do_block(clauses, s, e, {:reduce, []}) end - defp expand_for_do_block(_meta, expr, s, e, false), do: expand(expr, s, e) + defp expand_for_do_block(expr, s, e, false), do: expand(expr, s, e) - defp expand_for_do_block(meta, [{:->, _, _} | _] = clauses, s, e, {:reduce, _}) do + defp expand_for_do_block([{:->, _, _} | _] = clauses, s, e, {:reduce, _}) do transformer = fn {:->, clause_meta, [args, right]}, sa -> # elixir checks here that clause has exactly 1 arg by matching against {_, _, [[_], _]} @@ -2202,7 +2206,7 @@ defmodule ElixirSense.Core.Compiler do # no point in doing type inference here, we are only certain of the initial value of the accumulator {e_clause, s_acc, e_acc} = - __MODULE__.Clauses.clause(meta, :fn, &__MODULE__.Clauses.head/3, clause, s_reset, e) + __MODULE__.Clauses.clause(&__MODULE__.Clauses.head/3, clause, s_reset, e) {e_clause, __MODULE__.Env.merge_vars(s_acc, sa, e_acc)} end @@ -2211,16 +2215,16 @@ defmodule ElixirSense.Core.Compiler do {do_expr, sa, e} end - defp expand_for_do_block(meta, expr, s, e, {:reduce, _} = reduce) do + defp expand_for_do_block(expr, s, e, {:reduce, _} = reduce) do # elixir raises here for_with_reduce_bad_block case expr do [] -> # try to recover from error by emitting a fake clause - expand_for_do_block(meta, [{:->, meta, [[{:_, [], e.module}], :ok]}], s, e, reduce) + expand_for_do_block([{:->, [], [[{:_, [], e.module}], :ok]}], s, e, reduce) _ -> # try to recover from error by wrapping the expression in clause - expand_for_do_block(meta, [{:->, meta, [[expr], :ok]}], s, e, reduce) + expand_for_do_block([{:->, [], [[expr], :ok]}], s, e, reduce) end end @@ -2368,7 +2372,7 @@ defmodule ElixirSense.Core.Compiler do # opts # end - {e_opts, so, eo} = __MODULE__.Clauses.case(meta, e_expr, r_opts, se, ee) + {e_opts, so, eo} = __MODULE__.Clauses.case(e_expr, r_opts, se, ee) {{:case, meta, [e_expr, e_opts]}, so, eo} end @@ -2597,21 +2601,21 @@ defmodule ElixirSense.Core.Compiler do {e_expr, end_s, end_e} end - def clause(meta, kind, fun, {:->, clause_meta, [_, _]} = clause, s, e) + def clause(fun, {:->, clause_meta, [_, _]} = clause, s, e) when is_function(fun, 4) do - clause(meta, kind, fn x, sa, ea -> fun.(clause_meta, x, sa, ea) end, clause, s, e) + clause(fn x, sa, ea -> fun.(clause_meta, x, sa, ea) end, clause, s, e) end - def clause(_meta, _kind, fun, {:->, meta, [left, right]}, s, e) do + def clause(fun, {:->, meta, [left, right]}, s, e) do {e_left, sl, el} = fun.(left, s, e) {e_right, sr, er} = ElixirExpand.expand(right, sl, el) {{:->, meta, [e_left, e_right]}, sr, er} end - def clause(meta, kind, fun, expr, s, e) do + def clause(fun, expr, s, e) do # try to recover from error by wrapping the expression in clause # elixir raises here bad_or_missing_clauses - clause(meta, kind, fun, {:->, meta, [[expr], :ok]}, s, e) + clause(fun, {:->, [], [[expr], :ok]}, s, e) end def head([{:when, meta, [_ | _] = all}], s, e) do @@ -2660,19 +2664,19 @@ defmodule ElixirSense.Core.Compiler do @valid_case_opts [:do] - def case(meta, e_expr, [], s, e) do + def case(e_expr, [], s, e) do # elixir raises here missing_option # emit a fake do block - case(meta, e_expr, [do: []], s, e) + case(e_expr, [do: []], s, e) end - def case(_meta, _e_expr, opts, s, e) when not is_list(opts) do + def case(_e_expr, opts, s, e) when not is_list(opts) do # elixir raises here invalid_args # there may be cursor ElixirExpand.expand(opts, s, e) end - def case(meta, e_expr, opts, s, e) do + def case(e_expr, opts, s, e) do # expand invalid opts in case there's cursor {_ast, s, _e} = ElixirExpand.expand(opts |> Keyword.drop(@valid_case_opts), s, e) @@ -2682,16 +2686,14 @@ defmodule ElixirSense.Core.Compiler do {case_clauses, sa} = Enum.map_reduce(opts, s, fn x, sa -> - expand_case(meta, x, match_context, sa, e) + expand_case(x, match_context, sa, e) end) {case_clauses, sa, e} end - defp expand_case(meta, {:do, _} = do_clause, match_context, s, e) do + defp expand_case({:do, _} = do_clause, match_context, s, e) do expand_clauses( - meta, - :case, fn c, s, e -> case head(c, s, e) do {[h | _] = c, s, e} -> @@ -2716,19 +2718,19 @@ defmodule ElixirSense.Core.Compiler do @valid_cond_opts [:do] - def cond(meta, [], s, e) do + def cond([], s, e) do # elixir raises here missing_option # emit a fake do block - cond(meta, [do: []], s, e) + cond([do: []], s, e) end - def cond(_meta, opts, s, e) when not is_list(opts) do + def cond(opts, s, e) when not is_list(opts) do # elixir raises here invalid_args # there may be cursor ElixirExpand.expand(opts, s, e) end - def cond(meta, opts, s, e) do + def cond(opts, s, e) do # expand invalid opts in case there's cursor {_ast, s, _e} = ElixirExpand.expand(opts |> Keyword.drop(@valid_cond_opts), s, e) @@ -2736,33 +2738,33 @@ defmodule ElixirSense.Core.Compiler do {cond_clauses, sa} = Enum.map_reduce(opts, s, fn x, sa -> - expand_cond(meta, x, sa, e) + expand_cond(x, sa, e) end) {cond_clauses, sa, e} end - defp expand_cond(meta, {:do, _} = do_clause, s, e) do - expand_clauses(meta, :cond, &ElixirExpand.expand_args/3, do_clause, s, e) + defp expand_cond({:do, _} = do_clause, s, e) do + expand_clauses(&ElixirExpand.expand_args/3, do_clause, s, e) end # receive @valid_receive_opts [:do, :after] - def receive(meta, [], s, e) do + def receive([], s, e) do # elixir raises here missing_option # emit a fake do block - receive(meta, [do: []], s, e) + receive([do: []], s, e) end - def receive(_meta, opts, s, e) when not is_list(opts) do + def receive(opts, s, e) when not is_list(opts) do # elixir raises here invalid_args # there may be cursor ElixirExpand.expand(opts, s, e) end - def receive(meta, opts, s, e) do + def receive(opts, s, e) do # expand invalid opts in case there's cursor {_ast, s, _e} = ElixirExpand.expand(opts |> Keyword.drop(@valid_receive_opts), s, e) @@ -2770,41 +2772,41 @@ defmodule ElixirSense.Core.Compiler do {receive_clauses, sa} = Enum.map_reduce(opts, s, fn x, sa -> - expand_receive(meta, x, sa, e) + expand_receive(x, sa, e) end) {receive_clauses, sa, e} end - defp expand_receive(_meta, {:do, {:__block__, _, []}} = do_block, s, _e) do + defp expand_receive({:do, {:__block__, _, []}} = do_block, s, _e) do {do_block, s} end - defp expand_receive(meta, {:do, _} = do_clause, s, e) do + defp expand_receive({:do, _} = do_clause, s, e) do # no point in doing type inference here, we have no idea what message we may get - expand_clauses(meta, :receive, &head/3, do_clause, s, e) + expand_clauses(&head/3, do_clause, s, e) end - defp expand_receive(meta, {:after, [_ | _]} = after_clause, s, e) do - expand_clauses(meta, :receive, &ElixirExpand.expand_args/3, after_clause, s, e) + defp expand_receive({:after, [_ | _]} = after_clause, s, e) do + expand_clauses(&ElixirExpand.expand_args/3, after_clause, s, e) end - defp expand_receive(meta, {:after, expr}, s, e) when not is_list(expr) do + defp expand_receive({:after, expr}, s, e) when not is_list(expr) do # elixir raises here multiple_after_clauses_in_receive case expr do expr when not is_list(expr) -> # try to recover from error by wrapping the expression in list - expand_receive(meta, {:after, [expr]}, s, e) + expand_receive({:after, [expr]}, s, e) [first | discarded] -> # try to recover from error by taking first clause only # expand other in case there's cursor {_ast, s, _e} = ElixirExpand.expand(discarded, s, e) - expand_receive(meta, {:after, [first]}, s, e) + expand_receive({:after, [first]}, s, e) [] -> # try to recover from error by inserting a fake clause - expand_receive(meta, {:after, [{:->, meta, [[0], :ok]}]}, s, e) + expand_receive({:after, [{:->, [], [[0], :ok]}]}, s, e) end end @@ -2822,7 +2824,7 @@ defmodule ElixirSense.Core.Compiler do s0 = ElixirEnv.reset_vars(s) {e_exprs, {s1, e1}} = Enum.map_reduce(exprs, {s0, e}, &expand_with/2) {e_do, opts1, s2} = expand_with_do(meta, opts0, s, s1, e1) - {e_opts, _opts2, s3} = expand_with_else(meta, opts1, s2, e) + {e_opts, _opts2, s3} = expand_with_else(opts1, s2, e) {{:with, meta, e_exprs ++ [[{:do, e_do} | e_opts]]}, s3, e} end @@ -2863,7 +2865,7 @@ defmodule ElixirSense.Core.Compiler do {e_expr, rest_opts, ElixirEnv.merge_vars(s_acc, s, e_acc)} end - defp expand_with_else(meta, opts, s, e) do + defp expand_with_else(opts, s, e) do case Keyword.pop(opts, :else) do {nil, _} -> {[], opts, s} @@ -2872,7 +2874,7 @@ defmodule ElixirSense.Core.Compiler do pair = {:else, expr} # no point in doing type inference here, we have no idea what data we are matching against - {e_pair, se} = expand_clauses(meta, :with, &head/3, pair, s, e) + {e_pair, se} = expand_clauses(&head/3, pair, s, e) {[e_pair], rest_opts, se} end end @@ -2881,19 +2883,19 @@ defmodule ElixirSense.Core.Compiler do @valid_try_opts [:do, :rescue, :catch, :else, :after] - def try(meta, [], s, e) do + def try([], s, e) do # elixir raises here missing_option # emit a fake do block - try(meta, [do: []], s, e) + try([do: []], s, e) end - def try(_meta, opts, s, e) when not is_list(opts) do + def try(opts, s, e) when not is_list(opts) do # elixir raises here invalid_args # there may be cursor ElixirExpand.expand(opts, s, e) end - def try(meta, opts, s, e) do + def try(opts, s, e) do # expand invalid opts in case there's cursor {_ast, s, _e} = ElixirExpand.expand(opts |> Keyword.drop(@valid_try_opts), s, e) @@ -2901,39 +2903,39 @@ defmodule ElixirSense.Core.Compiler do {try_clauses, sa} = Enum.map_reduce(opts, s, fn x, sa -> - expand_try(meta, x, sa, e) + expand_try(x, sa, e) end) {try_clauses, sa, e} end - defp expand_try(_meta, {:do, expr}, s, e) do + defp expand_try({:do, expr}, s, e) do {e_expr, se, ee} = ElixirExpand.expand(expr, ElixirEnv.reset_vars(s), e) {{:do, e_expr}, ElixirEnv.merge_vars(se, s, ee)} end - defp expand_try(_meta, {:after, expr}, s, e) do + defp expand_try({:after, expr}, s, e) do {e_expr, se, ee} = ElixirExpand.expand(expr, ElixirEnv.reset_vars(s), e) {{:after, e_expr}, ElixirEnv.merge_vars(se, s, ee)} end - defp expand_try(meta, {:else, _} = else_clause, s, e) do + defp expand_try({:else, _} = else_clause, s, e) do # TODO we could try to infer type from last try block expression - expand_clauses(meta, :try, &head/3, else_clause, s, e) + expand_clauses(&head/3, else_clause, s, e) end - defp expand_try(meta, {:catch, _} = catch_clause, s, e) do - expand_clauses_with_stacktrace(meta, &expand_catch/4, catch_clause, s, e) + defp expand_try({:catch, _} = catch_clause, s, e) do + expand_clauses_with_stacktrace(&expand_catch/4, catch_clause, s, e) end - defp expand_try(meta, {:rescue, _} = rescue_clause, s, e) do - expand_clauses_with_stacktrace(meta, &expand_rescue/4, rescue_clause, s, e) + defp expand_try({:rescue, _} = rescue_clause, s, e) do + expand_clauses_with_stacktrace(&expand_rescue/4, rescue_clause, s, e) end - defp expand_clauses_with_stacktrace(meta, fun, clauses, s, e) do + defp expand_clauses_with_stacktrace(fun, clauses, s, e) do old_stacktrace = s.stacktrace ss = %{s | stacktrace: true} - {ret, se} = expand_clauses(meta, :try, fun, clauses, ss, e) + {ret, se} = expand_clauses(fun, clauses, ss, e) {ret, %{se | stacktrace: old_stacktrace}} end @@ -3071,15 +3073,10 @@ defmodule ElixirSense.Core.Compiler do end end - defp expand_clauses(meta, kind, fun, clauses, s, e) do - new_kind = origin(meta, kind) - expand_clauses_origin(meta, new_kind, fun, clauses, s, e) - end - - defp expand_clauses_origin(meta, kind, fun, {key, [_ | _] = clauses}, s, e) do + defp expand_clauses(fun, {key, [_ | _] = clauses}, s, e) do transformer = fn clause, sa -> {e_clause, s_acc, e_acc} = - clause(meta, {kind, key}, fun, clause, ElixirEnv.reset_vars(sa), e) + clause(fun, clause, ElixirEnv.reset_vars(sa), e) {e_clause, ElixirEnv.merge_vars(s_acc, sa, e_acc)} end @@ -3088,10 +3085,10 @@ defmodule ElixirSense.Core.Compiler do {{key, values}, se} end - defp expand_clauses_origin(meta, kind, fun, {key, expr}, s, e) do + defp expand_clauses(fun, {key, expr}, s, e) do # try to recover from error by wrapping the expression in a clauses list # elixir raises here bad_or_missing_clauses - expand_clauses_origin(meta, kind, fun, {key, [expr]}, s, e) + expand_clauses(fun, {key, [expr]}, s, e) end # helpers @@ -3106,10 +3103,6 @@ defmodule ElixirSense.Core.Compiler do defp sanitize_opts(opts, allowed) do Enum.flat_map(allowed, fn opt -> sanitize_opt(opts, opt) end) end - - defp origin(meta, default) do - Keyword.get(meta, :origin, default) - end end defmodule Bitstring do @@ -3555,7 +3548,7 @@ defmodule ElixirSense.Core.Compiler do # no point in doing type inference here, we have no idea what the fn will be called with {e_clause, s_acc, e_acc} = - ElixirClauses.clause(meta, :fn, &ElixirClauses.head/3, clause, s_reset, e) + ElixirClauses.clause(&ElixirClauses.head/3, clause, s_reset, e) {e_clause, ElixirEnv.merge_vars(s_acc, sa, e_acc)} end diff --git a/lib/elixir_sense/core/compiler/typespec.ex b/lib/elixir_sense/core/compiler/typespec.ex index 3c216f11..d0d6e57d 100644 --- a/lib/elixir_sense/core/compiler/typespec.ex +++ b/lib/elixir_sense/core/compiler/typespec.ex @@ -1,5 +1,4 @@ defmodule ElixirSense.Core.Compiler.Typespec do - alias ElixirSense.Core.Normalized.Macro.Env, as: NormalizedMacroEnv alias ElixirSense.Core.Compiler, as: ElixirExpand alias ElixirSense.Core.Compiler.Utils import ElixirSense.Core.State @@ -185,7 +184,7 @@ defmodule ElixirSense.Core.Compiler.Typespec do when is_atom(name) and is_atom(context) and name != :_ -> {add_var_write(state, {name, meta, context}), [name | var_names]} - other, acc -> + _other, acc -> # silently skip invalid typespec params acc end) @@ -257,9 +256,9 @@ defmodule ElixirSense.Core.Compiler.Typespec do end ## Handle maps and structs - defp typespec({:%{}, meta, fields} = map, vars, caller, state) do + defp typespec({:%{}, meta, fields}, vars, caller, state) do fun = fn - {{:required, meta2, [k]}, v}, state -> + {{:required, _meta2, [k]}, v}, state -> {arg1, state} = typespec(k, vars, caller, state) {arg2, state} = typespec(v, vars, caller, state) {{arg1, arg2}, state} @@ -284,7 +283,7 @@ defmodule ElixirSense.Core.Compiler.Typespec do {{:%{}, meta, fields |> Enum.filter(&(&1 != nil))}, state} end - defp typespec({:%, struct_meta, [name, {:%{}, meta, fields}]} = node, vars, caller, state) do + defp typespec({:%, struct_meta, [name, {:%{}, meta, fields}]}, vars, caller, state) do case ElixirExpand.Macro.expand(name, %{caller | function: {:__info__, 1}}) do module when is_atom(module) -> # TODO register alias/struct @@ -409,7 +408,7 @@ defmodule ElixirSense.Core.Compiler.Typespec do # Handle type operator defp typespec( - {:"::", meta, [{var_name, var_meta, context}, expr]} = ann_type, + {:"::", meta, [{var_name, var_meta, context}, expr]}, vars, caller, state @@ -434,7 +433,7 @@ defmodule ElixirSense.Core.Compiler.Typespec do # Macro.expand/2 on the remote does not expand module attributes (but expands # things like __MODULE__). defp typespec( - {{:., dot_meta, [{:@, attr_meta, [{attr, _, _}]}, name]}, meta, args} = orig, + {{:., dot_meta, [{:@, attr_meta, [{attr, _, _}]}, name]}, meta, args}, vars, caller, state @@ -459,7 +458,7 @@ defmodule ElixirSense.Core.Compiler.Typespec do end # Handle remote calls - defp typespec({{:., dot_meta, [remote, name]}, meta, args} = orig, vars, caller, state) do + defp typespec({{:., dot_meta, [remote, name]}, meta, args}, vars, caller, state) do remote = expand_remote(remote, caller) if remote == caller.module do @@ -491,7 +490,7 @@ defmodule ElixirSense.Core.Compiler.Typespec do end # Handle variables or local calls - defp typespec({name, meta, atom} = node, :disabled, caller, state) when is_atom(atom) do + defp typespec({name, meta, atom}, :disabled, _caller, state) when is_atom(atom) do {{name, meta, atom}, state} end @@ -532,7 +531,7 @@ defmodule ElixirSense.Core.Compiler.Typespec do {integer, state} end - defp typespec([], vars, caller, state) do + defp typespec([], _vars, _caller, state) do {[], state} end @@ -588,16 +587,6 @@ defmodule ElixirSense.Core.Compiler.Typespec do {nil, state} end - defp location(meta) do - line = Keyword.get(meta, :line, 0) - - if column = Keyword.get(meta, :column) do - {line, column} - else - line - end - end - # TODO trace alias? # TODO trace calls in expand defdelegate expand_remote(other, env), to: ElixirSense.Core.Compiler.Macro, as: :expand From f7cd3d1f720ba072b25fe0c06206cdb7137a898d Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 29 Aug 2024 20:31:39 +0200 Subject: [PATCH 174/235] module vars are not accessible in module callbacks --- lib/elixir_sense/core/compiler.ex | 133 ++++++++---------- .../metadata_builder/error_recovery_test.exs | 38 +++++ 2 files changed, 97 insertions(+), 74 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 1364d717..57c2769f 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1459,98 +1459,82 @@ defmodule ElixirSense.Core.Compiler do {expanded, _state, _env} = expand(alias, state, env) - # {expanded, with_alias} = - # case is_atom(expanded) do - # true -> - # {full, old, opts} = alias_defmodule(alias, expanded, env) - # # Expand the module considering the current environment/nesting - # meta = [defined: full] ++ alias_meta(alias) - # {full, {:require, meta, [old, opts]}} - - # false -> - # {expanded, nil} - # end - - # The env inside the block is discarded - {_result, state, env} = + {expanded, full, env} = if is_atom(expanded) do - # elixir emits a special require directive with :defined key set in meta - # require expand does alias, updates context_modules and runtime_modules - # we do it here instead {full, env} = alias_defmodule(alias, expanded, env) - env = %{env | context_modules: [full | env.context_modules]} + {expanded, full, env} + else + # elixir raises here + {:"Elixir.__Unknown__", :"Elixir.__Unknown__", env} + end - state = - case original_env do - %{function: nil} -> - state + # elixir emits a special require directive with :defined key set in meta + # require expand does alias, updates context_modules and runtime_modules + # we do it here instead - _ -> - %{state | runtime_modules: [full | state.runtime_modules]} - end + env = %{env | context_modules: [full | env.context_modules]} - range = extract_range(meta) + state = + case original_env do + %{function: nil} -> + state - module_functions = - case state.protocol do - nil -> [] - _ -> [{:__impl__, [:atom], :def}] - end + _ -> + %{state | runtime_modules: [full | state.runtime_modules]} + end - state = - state - |> add_module_to_index(full, range, []) - |> add_module - |> add_current_env_to_line(meta, %{env | module: full}) - |> add_module_functions(%{env | module: full}, module_functions, range) - |> new_vars_scope - |> new_attributes_scope + range = extract_range(meta) - # TODO magic with ElixirEnv instead of new_vars_scope? + module_functions = + case state.protocol do + nil -> [] + _ -> [{:__impl__, [:atom], :def}] + end - {state, _env} = maybe_add_protocol_behaviour(state, %{env | module: full}) + state = + state + |> add_module_to_index(full, range, []) + |> add_module + |> add_current_env_to_line(meta, %{env | module: full}) + |> add_module_functions(%{env | module: full}, module_functions, range) + |> new_vars_scope + |> new_attributes_scope - {result, state, e_env} = expand(block, state, %{env | module: full}) + # TODO magic with ElixirEnv instead of new_vars_scope? - before_compile = - for args <- Map.get(state.attribute_store, {full, :before_compile}, []) do - target = - case args do - {module, fun} -> [module, fun] - module -> [module, :__before_compile__] - end + {state, _env} = maybe_add_protocol_behaviour(state, %{env | module: full}) - {:__block__, [], - [ - {:require, [], [hd(target)]}, - {{:., [], target}, [], [e_env]} - ]} - end + {result, state, e_env} = expand(block, state, %{env | module: full}) - module_callbacks = {:__block__, [], before_compile} + {state, _e_env} = + for args <- Map.get(state.attribute_store, {full, :before_compile}, []) do + case args do + {module, fun} -> [module, fun] + module -> [module, :__before_compile__] + end + end + |> Enum.reduce({state, e_env}, fn target, {state, env} -> + env = %{env | versioned_vars: %{}, line: meta[:line]} + state = %{state | unused: 0, vars: {%{}, false}} + |> new_func_vars_scope() - {_result, state, _e_env} = expand(module_callbacks, state, e_env) + ast = {:__block__, [], + [ + {:require, [], [hd(target)]}, + {{:., [], target}, [], [env]} + ]} - state = - state - |> apply_optional_callbacks(%{env | module: full}) + {_result, state, env} = expand(ast, state, env) + {remove_func_vars_scope(state), env} + end) - {result, state, env} - else - raise "unable to expand module alias #{inspect(expanded)}" - # alias |> dbg - # keys = state |> Map.from_struct() |> Map.take([:vars, :unused]) - # keys |> dbg(limit: :infinity) - # block |> dbg - # # If we don't know the module name, do we still want to expand it here? - # # Perhaps it would be useful for dealing with local functions anyway? - # # But note that __MODULE__ will return nil. - - # # TODO - # expand(block, state, %{env | module: nil}) - end + state = + state + |> apply_optional_callbacks(%{env | module: full}) # restore vars from outer scope + # TODO magic with ElixirEnv instead of new_vars_scope? + # unused probably shouldnt be restored state = %{state | vars: vars, unused: unused} |> maybe_move_vars_to_outer_scope @@ -1644,6 +1628,7 @@ defmodule ElixirSense.Core.Compiler do } end + # TODO check if versioned_vars need to be set to %{} env_for_expand = %{env | function: {name, arity}} # based on :elixir_clauses.def diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index 90d6f481..05a4e090 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -2609,4 +2609,42 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert env.function == {:foo, 1} end end + + describe "defmodule" do + test "in defmodule" do + code = """ + defmodule\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.module == Abc + end + + test "in defmodule alias" do + code = """ + defmodule A\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.module == Abc + end + + test "in defmodule after do" do + code = """ + defmodule Abc, do: \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.module == Abc + end + + test "in defmodule invalid alias" do + code = """ + defmodule 123, do: \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.module == :"Elixir.__Unknown__" + end + end end From c697155e18c4bfeb0d2899c21989a16a036377c2 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 29 Aug 2024 21:44:00 +0200 Subject: [PATCH 175/235] simplify function var scope handling --- lib/elixir_sense/core/compiler.ex | 34 +++++++++++-------- lib/elixir_sense/core/compiler/typespec.ex | 26 ++++++-------- lib/elixir_sense/core/state.ex | 17 ++++++---- .../core/metadata_builder_test.exs | 17 +++++++--- 4 files changed, 52 insertions(+), 42 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 57c2769f..c5801718 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1459,13 +1459,12 @@ defmodule ElixirSense.Core.Compiler do {expanded, _state, _env} = expand(alias, state, env) - {expanded, full, env} = + {full, env} = if is_atom(expanded) do - {full, env} = alias_defmodule(alias, expanded, env) - {expanded, full, env} + alias_defmodule(alias, expanded, env) else # elixir raises here - {:"Elixir.__Unknown__", :"Elixir.__Unknown__", env} + {:"Elixir.__Unknown__", env} end # elixir emits a special require directive with :defined key set in meta @@ -1504,8 +1503,11 @@ defmodule ElixirSense.Core.Compiler do {state, _env} = maybe_add_protocol_behaviour(state, %{env | module: full}) - {result, state, e_env} = expand(block, state, %{env | module: full}) + {_result, state, e_env} = expand(block, state, %{env | module: full}) + # here we handle module callbacks. Only before_compile macro callbacks are expanded as they + # affect module body. Func before_compile callbacks are not executed. after_compile and after_verify + # are not executed as we do not preform a real compilation {state, _e_env} = for args <- Map.get(state.attribute_store, {full, :before_compile}, []) do case args do @@ -1514,10 +1516,14 @@ defmodule ElixirSense.Core.Compiler do end end |> Enum.reduce({state, e_env}, fn target, {state, env} -> + # module vars are not accessible in module callbacks env = %{env | versioned_vars: %{}, line: meta[:line]} - state = %{state | unused: 0, vars: {%{}, false}} - |> new_func_vars_scope() + state_orig = state + state = new_func_vars_scope(state) + # elixir dispatches callbacks by raw dispatch and eval_forms + # instead we expand a bock with require and possibly expand macros + # we do not attempt to exec function callbacks ast = {:__block__, [], [ {:require, [], [hd(target)]}, @@ -1525,7 +1531,7 @@ defmodule ElixirSense.Core.Compiler do ]} {_result, state, env} = expand(ast, state, env) - {remove_func_vars_scope(state), env} + {remove_func_vars_scope(state, state_orig), env} end) state = @@ -1583,7 +1589,7 @@ defmodule ElixirSense.Core.Compiler do ) when module != nil and def_kind in [:def, :defp, :defmacro, :defmacrop, :defguard, :defguardp] do - %{vars: vars, unused: unused} = state + state_orig = state unquoted_call = __MODULE__.Quote.has_unquotes(call) unquoted_expr = __MODULE__.Quote.has_unquotes(expr) @@ -1615,9 +1621,7 @@ defmodule ElixirSense.Core.Compiler do # module vars are not accessible in def body %{ state - | vars: {%{}, false}, - unused: 0, - caller: def_kind in [:defmacro, :defmacrop, :defguard, :defguardp] + | caller: def_kind in [:defmacro, :defmacrop, :defguard, :defguardp] } |> new_func_vars_scope() else @@ -1628,7 +1632,7 @@ defmodule ElixirSense.Core.Compiler do } end - # TODO check if versioned_vars need to be set to %{} + # no need to reset versioned_vars - we never update it env_for_expand = %{env | function: {name, arity}} # based on :elixir_clauses.def @@ -1690,14 +1694,14 @@ defmodule ElixirSense.Core.Compiler do # restore vars from outer scope state = - %{state | vars: vars, unused: unused, caller: false} + %{state | caller: false} |> maybe_move_vars_to_outer_scope |> remove_vars_scope state = unless has_unquotes do # restore module vars - remove_func_vars_scope(state) + remove_func_vars_scope(state, state_orig) else # no need to do anything state diff --git a/lib/elixir_sense/core/compiler/typespec.ex b/lib/elixir_sense/core/compiler/typespec.ex index d0d6e57d..d9bffa2f 100644 --- a/lib/elixir_sense/core/compiler/typespec.ex +++ b/lib/elixir_sense/core/compiler/typespec.ex @@ -32,16 +32,14 @@ defmodule ElixirSense.Core.Compiler.Typespec do def type_to_signature(_), do: {:__unknown__, []} def expand_spec(ast, state, env) do - # TODO not sure this is correct. Are module vars accessible? - state = - state - |> new_func_vars_scope + # unless there are unquotes module vars are not accessible + # TODO handle unquotes + state_orig = state + state = new_func_vars_scope(state) {ast, state, env} = do_expand_spec(ast, state, env) - state = - state - |> remove_func_vars_scope + state = remove_func_vars_scope(state, state_orig) {ast, state, env} end @@ -155,17 +153,13 @@ defmodule ElixirSense.Core.Compiler.Typespec do defp remove_default(other), do: other def expand_type(ast, state, env) do - state = - state - |> new_func_vars_scope + # unless there are unquotes module vars are not accessible + # TODO handle unquotes + state_orig = state - {ast, state, env} = do_expand_type(ast, state, env) + {ast, state, env} = do_expand_type(ast, new_func_vars_scope(state), env) - state = - state - |> remove_func_vars_scope - - {ast, state, env} + {ast, remove_func_vars_scope(state, state_orig), env} end defp do_expand_type({:"::", meta, [{name, name_meta, args}, definition]}, state, env) do diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index b9a0a7a1..2fbf7a81 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -325,11 +325,8 @@ defmodule ElixirSense.Core.State do current_scope_id = hd(state.scope_ids) # Macro.Env versioned_vars is not updated - # versioned_vars: macro_env.versioned_vars, + # elixir keeps current vars instate {versioned_vars, _} = state.vars - - # vars_info has both read and write vars - # filter to return only read [current_vars_info | _] = state.vars_info # here we filter vars to only return the ones with nil context to maintain macro hygiene @@ -811,7 +808,10 @@ defmodule ElixirSense.Core.State do state | scope_ids: [scope_id | state.scope_ids], scope_id_count: scope_id, - vars_info: [%{} | state.vars_info] + vars_info: [%{} | state.vars_info], + # elixir_ex entries + unused: 0, + vars: {%{}, false} } end @@ -828,12 +828,15 @@ defmodule ElixirSense.Core.State do } end - def remove_func_vars_scope(%__MODULE__{} = state) do + def remove_func_vars_scope(%__MODULE__{} = state, %{vars: vars, unused: unused}) do %__MODULE__{ state | scope_ids: tl(state.scope_ids), vars_info: tl(state.vars_info), - vars_info_per_scope_id: update_vars_info_per_scope_id(state) + vars_info_per_scope_id: update_vars_info_per_scope_id(state), + # restore elixir_ex fields + vars: vars, + unused: unused } end diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 09ad131f..c95e9926 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -2700,13 +2700,14 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] = vars end - test "vars defined inside a module" do + test "vars defined inside a module body" do state = """ defmodule MyModule do var_out1 = 1 def func do var_in = 1 + IO.puts "" end var_out2 = 1 IO.puts "" @@ -2714,15 +2715,23 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert Map.keys(state.lines_to_env[7].versioned_vars) == [ + assert Map.keys(state.lines_to_env[5].versioned_vars) == [ + {:var_in, nil} + ] + + assert [ + %VarInfo{name: :var_in, positions: [{4, 5}]} + ] = state |> get_line_vars(5) + + assert Map.keys(state.lines_to_env[8].versioned_vars) == [ {:var_out1, nil}, {:var_out2, nil} ] assert [ %VarInfo{name: :var_out1, positions: [{2, 3}], scope_id: scope_id}, - %VarInfo{name: :var_out2, positions: [{6, 3}], scope_id: scope_id} - ] = state |> get_line_vars(7) + %VarInfo{name: :var_out2, positions: [{7, 3}], scope_id: scope_id} + ] = state |> get_line_vars(8) end test "vars defined in a `for` comprehension" do From fd08d4634d6dfc6dde5181f67ea51088b3624ea0 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 29 Aug 2024 21:52:58 +0200 Subject: [PATCH 176/235] simplify var scopes --- lib/elixir_sense/core/compiler.ex | 14 -------------- test/elixir_sense/core/metadata_builder_test.exs | 8 ++++---- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index c5801718..63cf76ac 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -2150,15 +2150,8 @@ defmodule ElixirSense.Core.Compiler do {maybe_reduce, normalized_opts} = sanitize_for_options(e_opts, false, false, false, return, meta, e, []) - # TODO not sure new vars scope is actually needed - sc = sc |> new_vars_scope {e_expr, se, ee} = expand_for_do_block(expr, sc, ec, maybe_reduce) - se = - se - |> maybe_move_vars_to_outer_scope - |> remove_vars_scope - {{:for, meta, e_cases ++ [[{:do, e_expr} | normalized_opts]]}, __MODULE__.Env.merge_vars(se, s, ee), e} end @@ -2842,15 +2835,8 @@ defmodule ElixirSense.Core.Compiler do # we return empty expression expr = expr || [] - # TODO not sure new vars scope is needed - acc = acc |> new_vars_scope {e_expr, s_acc, e_acc} = ElixirExpand.expand(expr, acc, e) - s_acc = - s_acc - |> maybe_move_vars_to_outer_scope - |> remove_vars_scope - {e_expr, rest_opts, ElixirEnv.merge_vars(s_acc, s, e_acc)} end diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index c95e9926..0fa647b6 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -2767,7 +2767,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do %VarInfo{ name: :var_in, positions: [{5, 5}], - scope_id: scope_id_3 + scope_id: scope_id_2 }, %VarInfo{ name: :var_on, @@ -2785,7 +2785,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do scope_id: scope_id_1 } ] - when scope_id_2 > scope_id_1 and scope_id_3 > scope_id_2) = get_line_vars(state, 6) + when scope_id_2 > scope_id_1) = get_line_vars(state, 6) assert Map.keys(state.lines_to_env[9].versioned_vars) == [ {:var_out1, nil}, @@ -2831,7 +2831,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do %VarInfo{ name: :var_in, positions: [{5, 5}], - scope_id: scope_id_3 + scope_id: scope_id_2 }, %VarInfo{ name: :var_on, @@ -2849,7 +2849,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do scope_id: scope_id_1 } ] - when scope_id_2 > scope_id_1 and scope_id_3 > scope_id_2) = get_line_vars(state, 6) + when scope_id_2 > scope_id_1) = get_line_vars(state, 6) assert Map.keys(state.lines_to_env[9].versioned_vars) == [ {:var_out1, nil}, From 441de76d61a2b5a0a9fba42aa151a7802741597a Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 29 Aug 2024 21:56:44 +0200 Subject: [PATCH 177/235] refactor --- lib/elixir_sense/core/compiler.ex | 3 --- lib/elixir_sense/core/state.ex | 5 +++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 63cf76ac..a9d8f51c 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1543,7 +1543,6 @@ defmodule ElixirSense.Core.Compiler do # unused probably shouldnt be restored state = %{state | vars: vars, unused: unused} - |> maybe_move_vars_to_outer_scope |> remove_vars_scope |> remove_attributes_scope |> remove_module @@ -1695,7 +1694,6 @@ defmodule ElixirSense.Core.Compiler do # restore vars from outer scope state = %{state | caller: false} - |> maybe_move_vars_to_outer_scope |> remove_vars_scope state = @@ -2457,7 +2455,6 @@ defmodule ElixirSense.Core.Compiler do # dbg({read, write}) s = %{s | vars: {read, write}} - |> maybe_move_vars_to_outer_scope |> remove_vars_scope # dbg(s.vars_info) diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 2fbf7a81..f7e3ff25 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -820,6 +820,7 @@ defmodule ElixirSense.Core.State do end def remove_vars_scope(%__MODULE__{} = state) do + state = maybe_move_vars_to_outer_scope(state) %__MODULE__{ state | scope_ids: tl(state.scope_ids), @@ -1238,7 +1239,7 @@ defmodule ElixirSense.Core.State do def default_env, do: %ElixirSense.Core.State.Env{} - def maybe_move_vars_to_outer_scope( + defp maybe_move_vars_to_outer_scope( %__MODULE__{vars_info: [current_scope_vars, outer_scope_vars | other_scopes_vars]} = state ) do outer_scope_vars = @@ -1252,7 +1253,7 @@ defmodule ElixirSense.Core.State do %__MODULE__{state | vars_info: vars_info} end - def maybe_move_vars_to_outer_scope(state), do: state + defp maybe_move_vars_to_outer_scope(state), do: state @module_functions [ {:__info__, [:atom], :def}, From cc5e8a09441b42a028353e000e2f279d7f4a58e5 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 29 Aug 2024 22:46:25 +0200 Subject: [PATCH 178/235] refactor --- lib/elixir_sense/core/compiler.ex | 77 +++++++------------ lib/elixir_sense/core/metadata_builder.ex | 2 +- lib/elixir_sense/core/state.ex | 7 +- .../core/metadata_builder_test.exs | 16 ++-- 4 files changed, 39 insertions(+), 63 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index a9d8f51c..2679b218 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1454,7 +1454,7 @@ defmodule ElixirSense.Core.Compiler do state, env ) do - %{vars: vars, unused: unused} = state + state_orig = state original_env = env {expanded, _state, _env} = expand(alias, state, env) @@ -1534,16 +1534,10 @@ defmodule ElixirSense.Core.Compiler do {remove_func_vars_scope(state, state_orig), env} end) - state = - state - |> apply_optional_callbacks(%{env | module: full}) - # restore vars from outer scope - # TODO magic with ElixirEnv instead of new_vars_scope? - # unused probably shouldnt be restored - state = - %{state | vars: vars, unused: unused} - |> remove_vars_scope + state = state + |> apply_optional_callbacks(%{env | module: full}) + |> remove_vars_scope(state_orig) |> remove_attributes_scope |> remove_module @@ -1629,6 +1623,7 @@ defmodule ElixirSense.Core.Compiler do state | caller: def_kind in [:defmacro, :defmacrop, :defguard, :defguardp] } + |> new_vars_scope() end # no need to reset versioned_vars - we never update it @@ -1662,9 +1657,6 @@ defmodule ElixirSense.Core.Compiler do def_kind ) - # TODO not sure vars scope is needed - state = state |> new_vars_scope - expr = case expr do nil -> @@ -1694,15 +1686,14 @@ defmodule ElixirSense.Core.Compiler do # restore vars from outer scope state = %{state | caller: false} - |> remove_vars_scope state = unless has_unquotes do # restore module vars remove_func_vars_scope(state, state_orig) else - # no need to do anything - state + # remove scope + remove_vars_scope(state, state_orig) end # result of def expansion is fa tuple @@ -2138,7 +2129,7 @@ defmodule ElixirSense.Core.Compiler do {do_expr, do_opts} end - {e_opts, so, eo} = expand(opts, __MODULE__.Env.reset_vars(s), e) + {e_opts, so, eo} = expand(opts, new_vars_scope(s), e) {e_cases, sc, ec} = map_fold(&expand_for_generator/3, so, eo, cases) # elixir raises here for_generator_start on invalid start generator @@ -2148,10 +2139,10 @@ defmodule ElixirSense.Core.Compiler do {maybe_reduce, normalized_opts} = sanitize_for_options(e_opts, false, false, false, return, meta, e, []) - {e_expr, se, ee} = expand_for_do_block(expr, sc, ec, maybe_reduce) + {e_expr, se, _ee} = expand_for_do_block(expr, sc, ec, maybe_reduce) {{:for, meta, e_cases ++ [[{:do, e_expr} | normalized_opts]]}, - __MODULE__.Env.merge_vars(se, s, ee), e} + remove_vars_scope(se, s), e} end defp expand_for_do_block([{:->, _, _} | _] = clauses, s, e, false) do @@ -2182,13 +2173,13 @@ defmodule ElixirSense.Core.Compiler do {_ast, sa, _e} = expand(discarded_args, sa, e) clause = {:->, clause_meta, [args, right]} - s_reset = __MODULE__.Env.reset_vars(sa) + s_reset = new_vars_scope(sa) # no point in doing type inference here, we are only certain of the initial value of the accumulator - {e_clause, s_acc, e_acc} = + {e_clause, s_acc, _e_acc} = __MODULE__.Clauses.clause(&__MODULE__.Clauses.head/3, clause, s_reset, e) - {e_clause, __MODULE__.Env.merge_vars(s_acc, sa, e_acc)} + {e_clause, remove_vars_scope(s_acc, sa)} end {do_expr, sa} = Enum.map_reduce(clauses, s, transformer) @@ -2415,10 +2406,6 @@ defmodule ElixirSense.Core.Compiler do defmodule Env do alias ElixirSense.Core.Compiler.Utils, as: ElixirUtils - def reset_vars(s) do - s |> new_vars_scope - end - def reset_read(%{vars: {_, write}} = s, %{vars: {read, _}}) do %{s | vars: {read, write}} end @@ -2450,18 +2437,6 @@ defmodule ElixirSense.Core.Compiler do ) end - def merge_vars(s, %{vars: {read, write}}, _e) do - # dbg(s.vars_info) - # dbg({read, write}) - s = - %{s | vars: {read, write}} - |> remove_vars_scope - - # dbg(s.vars_info) - # dbg(s.vars_info_per_scope_id) - s - end - def calculate_span(meta, name) do case Keyword.fetch(meta, :column) do {:ok, column} -> @@ -2800,7 +2775,7 @@ defmodule ElixirSense.Core.Compiler do {_ast, s, _e} = ElixirExpand.expand(opts0 |> Keyword.drop(@valid_with_opts), s, e) opts0 = sanitize_opts(opts0, @valid_with_opts) - s0 = ElixirEnv.reset_vars(s) + s0 = new_vars_scope(s) {e_exprs, {s1, e1}} = Enum.map_reduce(exprs, {s0, e}, &expand_with/2) {e_do, opts1, s2} = expand_with_do(meta, opts0, s, s1, e1) {e_opts, _opts2, s3} = expand_with_else(opts1, s2, e) @@ -2832,9 +2807,9 @@ defmodule ElixirSense.Core.Compiler do # we return empty expression expr = expr || [] - {e_expr, s_acc, e_acc} = ElixirExpand.expand(expr, acc, e) + {e_expr, s_acc, _e_acc} = ElixirExpand.expand(expr, acc, e) - {e_expr, rest_opts, ElixirEnv.merge_vars(s_acc, s, e_acc)} + {e_expr, rest_opts, remove_vars_scope(s_acc, s)} end defp expand_with_else(opts, s, e) do @@ -2882,13 +2857,13 @@ defmodule ElixirSense.Core.Compiler do end defp expand_try({:do, expr}, s, e) do - {e_expr, se, ee} = ElixirExpand.expand(expr, ElixirEnv.reset_vars(s), e) - {{:do, e_expr}, ElixirEnv.merge_vars(se, s, ee)} + {e_expr, se, _ee} = ElixirExpand.expand(expr, new_vars_scope(s), e) + {{:do, e_expr}, remove_vars_scope(se, s)} end defp expand_try({:after, expr}, s, e) do - {e_expr, se, ee} = ElixirExpand.expand(expr, ElixirEnv.reset_vars(s), e) - {{:after, e_expr}, ElixirEnv.merge_vars(se, s, ee)} + {e_expr, se, _ee} = ElixirExpand.expand(expr, new_vars_scope(s), e) + {{:after, e_expr}, remove_vars_scope(se, s)} end defp expand_try({:else, _} = else_clause, s, e) do @@ -3047,10 +3022,10 @@ defmodule ElixirSense.Core.Compiler do defp expand_clauses(fun, {key, [_ | _] = clauses}, s, e) do transformer = fn clause, sa -> - {e_clause, s_acc, e_acc} = - clause(fun, clause, ElixirEnv.reset_vars(sa), e) + {e_clause, s_acc, _e_acc} = + clause(fun, clause, new_vars_scope(sa), e) - {e_clause, ElixirEnv.merge_vars(s_acc, sa, e_acc)} + {e_clause, remove_vars_scope(s_acc, sa)} end {values, se} = Enum.map_reduce(clauses, s, transformer) @@ -3516,13 +3491,13 @@ defmodule ElixirSense.Core.Compiler do transformer = fn {:->, _, [_left, _right]} = clause, sa -> # elixir raises defaults_in_args - s_reset = ElixirEnv.reset_vars(sa) + s_reset = new_vars_scope(sa) # no point in doing type inference here, we have no idea what the fn will be called with - {e_clause, s_acc, e_acc} = + {e_clause, s_acc, _e_acc} = ElixirClauses.clause(&ElixirClauses.head/3, clause, s_reset, e) - {e_clause, ElixirEnv.merge_vars(s_acc, sa, e_acc)} + {e_clause, remove_vars_scope(s_acc, sa)} end {e_clauses, se} = Enum.map_reduce(clauses, s, transformer) diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index bdc9b7b8..38ddec59 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -31,7 +31,7 @@ defmodule ElixirSense.Core.MetadataBuilder do state |> remove_attributes_scope - |> remove_vars_scope + |> remove_vars_scope(%{vars: {%{}, false}}) |> remove_module end diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index f7e3ff25..d9a4811c 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -819,16 +819,19 @@ defmodule ElixirSense.Core.State do %__MODULE__{state | attributes: [[] | state.attributes], scope_attributes: [[]]} end - def remove_vars_scope(%__MODULE__{} = state) do + def remove_vars_scope(%__MODULE__{} = state, %{vars: vars}) do state = maybe_move_vars_to_outer_scope(state) %__MODULE__{ state | scope_ids: tl(state.scope_ids), vars_info: tl(state.vars_info), - vars_info_per_scope_id: update_vars_info_per_scope_id(state) + vars_info_per_scope_id: update_vars_info_per_scope_id(state), + # restore elixir_ex fields + vars: vars } end + # TODO should we restore unused? def remove_func_vars_scope(%__MODULE__{} = state, %{vars: vars, unused: unused}) do %__MODULE__{ state diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 0fa647b6..da8b97ab 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -2660,16 +2660,15 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert ([ + assert [ %VarInfo{name: :_par5, positions: [{3, 57}], scope_id: scope_id_1}, %VarInfo{name: :par1, positions: [{3, 20}], scope_id: scope_id_1}, %VarInfo{name: :par2, positions: [{3, 33}], scope_id: scope_id_1}, %VarInfo{name: :par3, positions: [{3, 39}], scope_id: scope_id_1}, %VarInfo{name: :par4, positions: [{3, 51}], scope_id: scope_id_1}, - %VarInfo{name: :var_in1, positions: [{4, 5}], scope_id: scope_id_2}, - %VarInfo{name: :var_in2, positions: [{5, 5}], scope_id: scope_id_2} - ] - when scope_id_2 > scope_id_1) = state |> get_line_vars(6) + %VarInfo{name: :var_in1, positions: [{4, 5}], scope_id: scope_id_1}, + %VarInfo{name: :var_in2, positions: [{5, 5}], scope_id: scope_id_1} + ] = state |> get_line_vars(6) assert [ %VarInfo{name: :arg, positions: [{8, 14}, {8, 24}]} @@ -3449,7 +3448,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do {:x, nil} ] - assert ([ + assert [ %VarInfo{ name: :_my_other, positions: [{2, 24}], @@ -3458,7 +3457,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do %VarInfo{ name: :abc, positions: [{3, 6}], - scope_id: scope_id_2 + scope_id: scope_id_1 }, %VarInfo{ name: :my_var, @@ -3470,8 +3469,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do positions: [{2, 43}, {3, 14}], scope_id: scope_id_1 } - ] - when scope_id_2 > scope_id_1) = state |> get_line_vars(4) + ] = state |> get_line_vars(4) end end From 3731b6a3f688cb646dfa770e7fcf8181ee3fc891 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 29 Aug 2024 23:23:41 +0200 Subject: [PATCH 179/235] correctly handle var versioning --- lib/elixir_sense/core/compiler.ex | 7 +++--- lib/elixir_sense/core/metadata_builder.ex | 27 ++++++++++------------- lib/elixir_sense/core/state.ex | 17 +++++++++++--- 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 2679b218..e882b487 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1499,8 +1499,6 @@ defmodule ElixirSense.Core.Compiler do |> new_vars_scope |> new_attributes_scope - # TODO magic with ElixirEnv instead of new_vars_scope? - {state, _env} = maybe_add_protocol_behaviour(state, %{env | module: full}) {_result, state, e_env} = expand(block, state, %{env | module: full}) @@ -1535,9 +1533,10 @@ defmodule ElixirSense.Core.Compiler do end) # restore vars from outer scope + # restore version counter state = state |> apply_optional_callbacks(%{env | module: full}) - |> remove_vars_scope(state_orig) + |> remove_vars_scope(state_orig, true) |> remove_attributes_scope |> remove_module @@ -1633,6 +1632,8 @@ defmodule ElixirSense.Core.Compiler do {_e_args, state, env_for_expand} = expand_args(args, %{state | prematch: {%{}, 0, :none}}, %{env_for_expand | context: :match}) + # TODO expand defaults + {e_guard, state, env_for_expand} = __MODULE__.Clauses.guard( guards, diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index 38ddec59..b8d94ee4 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -14,24 +14,21 @@ defmodule ElixirSense.Core.MetadataBuilder do """ @spec build(Macro.t(), nil | {pos_integer, pos_integer}) :: State.t() def build(ast, cursor_position \\ nil) do - {_ast, state, _env} = - Compiler.expand( - ast, - %State{ - cursor_position: cursor_position, - prematch: - if Version.match?(System.version(), ">= 1.15.0-dev") do - Code.get_compiler_option(:on_undefined_variable) - else - :warn - end - }, - Compiler.env() - ) + state_orig = %State{ + cursor_position: cursor_position, + prematch: + if Version.match?(System.version(), ">= 1.15.0-dev") do + Code.get_compiler_option(:on_undefined_variable) + else + :warn + end + } + + {_ast, state, _env} =Compiler.expand(ast, state_orig, Compiler.env()) state |> remove_attributes_scope - |> remove_vars_scope(%{vars: {%{}, false}}) + |> remove_vars_scope(state_orig) |> remove_module end diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index d9a4811c..a593d33a 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -810,6 +810,7 @@ defmodule ElixirSense.Core.State do scope_id_count: scope_id, vars_info: [%{} | state.vars_info], # elixir_ex entries + # each def starts versioning from 0 unused: 0, vars: {%{}, false} } @@ -819,9 +820,9 @@ defmodule ElixirSense.Core.State do %__MODULE__{state | attributes: [[] | state.attributes], scope_attributes: [[]]} end - def remove_vars_scope(%__MODULE__{} = state, %{vars: vars}) do + def remove_vars_scope(%__MODULE__{} = state, %{vars: vars, unused: unused}, restore_version_counter \\ false) do state = maybe_move_vars_to_outer_scope(state) - %__MODULE__{ + state = %__MODULE__{ state | scope_ids: tl(state.scope_ids), vars_info: tl(state.vars_info), @@ -829,9 +830,18 @@ defmodule ElixirSense.Core.State do # restore elixir_ex fields vars: vars } + + if restore_version_counter do + # this is used by defmodule as module body does not affect outside versioning + %__MODULE__{ + state + | unused: unused + } + else + state + end end - # TODO should we restore unused? def remove_func_vars_scope(%__MODULE__{} = state, %{vars: vars, unused: unused}) do %__MODULE__{ state @@ -840,6 +850,7 @@ defmodule ElixirSense.Core.State do vars_info_per_scope_id: update_vars_info_per_scope_id(state), # restore elixir_ex fields vars: vars, + # restore versioning unused: unused } end From 51f3c44393c88427de5cff6f0f21d9cccb8b27a3 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 29 Aug 2024 23:24:04 +0200 Subject: [PATCH 180/235] format --- lib/elixir_sense/core/compiler.ex | 17 ++--- lib/elixir_sense/core/metadata_builder.ex | 2 +- lib/elixir_sense/core/state.ex | 12 +++- .../core/metadata_builder_test.exs | 68 +++++++++---------- 4 files changed, 53 insertions(+), 46 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index e882b487..c80b591d 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1522,11 +1522,12 @@ defmodule ElixirSense.Core.Compiler do # elixir dispatches callbacks by raw dispatch and eval_forms # instead we expand a bock with require and possibly expand macros # we do not attempt to exec function callbacks - ast = {:__block__, [], - [ - {:require, [], [hd(target)]}, - {{:., [], target}, [], [env]} - ]} + ast = + {:__block__, [], + [ + {:require, [], [hd(target)]}, + {{:., [], target}, [], [env]} + ]} {_result, state, env} = expand(ast, state, env) {remove_func_vars_scope(state, state_orig), env} @@ -1534,7 +1535,8 @@ defmodule ElixirSense.Core.Compiler do # restore vars from outer scope # restore version counter - state = state + state = + state |> apply_optional_callbacks(%{env | module: full}) |> remove_vars_scope(state_orig, true) |> remove_attributes_scope @@ -2142,8 +2144,7 @@ defmodule ElixirSense.Core.Compiler do {e_expr, se, _ee} = expand_for_do_block(expr, sc, ec, maybe_reduce) - {{:for, meta, e_cases ++ [[{:do, e_expr} | normalized_opts]]}, - remove_vars_scope(se, s), e} + {{:for, meta, e_cases ++ [[{:do, e_expr} | normalized_opts]]}, remove_vars_scope(se, s), e} end defp expand_for_do_block([{:->, _, _} | _] = clauses, s, e, false) do diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index b8d94ee4..694614f4 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -24,7 +24,7 @@ defmodule ElixirSense.Core.MetadataBuilder do end } - {_ast, state, _env} =Compiler.expand(ast, state_orig, Compiler.env()) + {_ast, state, _env} = Compiler.expand(ast, state_orig, Compiler.env()) state |> remove_attributes_scope diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index a593d33a..3a81b8c5 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -820,8 +820,13 @@ defmodule ElixirSense.Core.State do %__MODULE__{state | attributes: [[] | state.attributes], scope_attributes: [[]]} end - def remove_vars_scope(%__MODULE__{} = state, %{vars: vars, unused: unused}, restore_version_counter \\ false) do + def remove_vars_scope( + %__MODULE__{} = state, + %{vars: vars, unused: unused}, + restore_version_counter \\ false + ) do state = maybe_move_vars_to_outer_scope(state) + state = %__MODULE__{ state | scope_ids: tl(state.scope_ids), @@ -1254,8 +1259,9 @@ defmodule ElixirSense.Core.State do def default_env, do: %ElixirSense.Core.State.Env{} defp maybe_move_vars_to_outer_scope( - %__MODULE__{vars_info: [current_scope_vars, outer_scope_vars | other_scopes_vars]} = state - ) do + %__MODULE__{vars_info: [current_scope_vars, outer_scope_vars | other_scopes_vars]} = + state + ) do outer_scope_vars = for {key, _} <- outer_scope_vars, into: %{}, diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index da8b97ab..4a0b3221 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -2661,14 +2661,14 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert [ - %VarInfo{name: :_par5, positions: [{3, 57}], scope_id: scope_id_1}, - %VarInfo{name: :par1, positions: [{3, 20}], scope_id: scope_id_1}, - %VarInfo{name: :par2, positions: [{3, 33}], scope_id: scope_id_1}, - %VarInfo{name: :par3, positions: [{3, 39}], scope_id: scope_id_1}, - %VarInfo{name: :par4, positions: [{3, 51}], scope_id: scope_id_1}, - %VarInfo{name: :var_in1, positions: [{4, 5}], scope_id: scope_id_1}, - %VarInfo{name: :var_in2, positions: [{5, 5}], scope_id: scope_id_1} - ] = state |> get_line_vars(6) + %VarInfo{name: :_par5, positions: [{3, 57}], scope_id: scope_id_1}, + %VarInfo{name: :par1, positions: [{3, 20}], scope_id: scope_id_1}, + %VarInfo{name: :par2, positions: [{3, 33}], scope_id: scope_id_1}, + %VarInfo{name: :par3, positions: [{3, 39}], scope_id: scope_id_1}, + %VarInfo{name: :par4, positions: [{3, 51}], scope_id: scope_id_1}, + %VarInfo{name: :var_in1, positions: [{4, 5}], scope_id: scope_id_1}, + %VarInfo{name: :var_in2, positions: [{5, 5}], scope_id: scope_id_1} + ] = state |> get_line_vars(6) assert [ %VarInfo{name: :arg, positions: [{8, 14}, {8, 24}]} @@ -2715,12 +2715,12 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert Map.keys(state.lines_to_env[5].versioned_vars) == [ - {:var_in, nil} - ] + {:var_in, nil} + ] - assert [ - %VarInfo{name: :var_in, positions: [{4, 5}]} - ] = state |> get_line_vars(5) + assert [ + %VarInfo{name: :var_in, positions: [{4, 5}]} + ] = state |> get_line_vars(5) assert Map.keys(state.lines_to_env[8].versioned_vars) == [ {:var_out1, nil}, @@ -3449,27 +3449,27 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] assert [ - %VarInfo{ - name: :_my_other, - positions: [{2, 24}], - scope_id: scope_id_1 - }, - %VarInfo{ - name: :abc, - positions: [{3, 6}], - scope_id: scope_id_1 - }, - %VarInfo{ - name: :my_var, - positions: [{2, 13}], - scope_id: scope_id_1 - }, - %VarInfo{ - name: :x, - positions: [{2, 43}, {3, 14}], - scope_id: scope_id_1 - } - ] = state |> get_line_vars(4) + %VarInfo{ + name: :_my_other, + positions: [{2, 24}], + scope_id: scope_id_1 + }, + %VarInfo{ + name: :abc, + positions: [{3, 6}], + scope_id: scope_id_1 + }, + %VarInfo{ + name: :my_var, + positions: [{2, 13}], + scope_id: scope_id_1 + }, + %VarInfo{ + name: :x, + positions: [{2, 43}, {3, 14}], + scope_id: scope_id_1 + } + ] = state |> get_line_vars(4) end end From b4e3f71d5c7b280434605c06035c81ce8edb1c70 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 30 Aug 2024 19:50:51 +0200 Subject: [PATCH 181/235] correctly handle default args --- lib/elixir_sense/core/compiler.ex | 35 +++++++- .../core/metadata_builder_test.exs | 87 ++++++++++++------- 2 files changed, 86 insertions(+), 36 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index c80b591d..b26905f3 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1630,11 +1630,26 @@ defmodule ElixirSense.Core.Compiler do # no need to reset versioned_vars - we never update it env_for_expand = %{env | function: {name, arity}} + # expand defaults and pass args without defaults to expand_args + {args_no_defaults, args, state} = + expand_defaults(args, state, %{env_for_expand | context: nil}, [], []) + # based on :elixir_clauses.def - {_e_args, state, env_for_expand} = - expand_args(args, %{state | prematch: {%{}, 0, :none}}, %{env_for_expand | context: :match}) + {e_args_no_defaults, state, env_for_expand} = + expand_args(args_no_defaults, %{state | prematch: {%{}, 0, :none}}, %{ + env_for_expand + | context: :match + }) - # TODO expand defaults + args = + Enum.zip(args, e_args_no_defaults) + |> Enum.map(fn + {{:"\\\\", meta, [_, expanded_default]}, expanded_arg} -> + {:"\\\\", meta, [expanded_arg, expanded_default]} + + {_, expanded_arg} -> + expanded_arg + end) {e_guard, state, env_for_expand} = __MODULE__.Clauses.guard( @@ -1804,6 +1819,20 @@ defmodule ElixirSense.Core.Compiler do |> String.to_atom() end + defp expand_defaults([{:"\\\\", meta, [expr, default]} | args], s, e, acc_no_defaults, acc) do + {expanded_default, se, _} = expand(default, s, e) + + expand_defaults(args, se, e, [expr | acc_no_defaults], [ + {:"\\\\", meta, [expr, expanded_default]} | acc + ]) + end + + defp expand_defaults([arg | args], s, e, acc_no_defaults, acc), + do: expand_defaults(args, s, e, [arg | acc_no_defaults], [arg | acc]) + + defp expand_defaults([], s, _e, acc_no_defaults, acc), + do: {Enum.reverse(acc_no_defaults), Enum.reverse(acc), s} + # defmodule helpers # defmodule automatically defines aliases, we need to mirror this feature here. diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 4a0b3221..b598cbaf 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -5596,7 +5596,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do type: :defmodule }, {Proto.MyOtherStruct, :reverse, 1} => %ModFunInfo{ - params: [[{:term, [line: 6, column: 15], nil}]], + params: [[{:term, _, nil}]], type: :def }, {Proto.MyStruct, nil, nil} => %ModFunInfo{ @@ -5604,7 +5604,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do type: :defmodule }, {Proto.MyStruct, :reverse, 1} => %ModFunInfo{ - params: [[{:term, [line: 6, column: 15], nil}]], + params: [[{:term, _, nil}]], type: :def } } = state.mods_funs_to_positions @@ -5717,7 +5717,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert %{ {Reversible, :reverse, 1} => %ModFunInfo{ - params: [[{:term, [line: 2, column: 15], nil}]], + params: [[{:term, _, nil}]], positions: [{2, 3}], type: :def } @@ -5780,7 +5780,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do [nil], [1], [ - {:\\, [line: 2, column: 13], [{:a, [line: 2, column: 11], nil}, nil]} + {:\\, _, [{:a, _, nil}, nil]} ] ] } @@ -5801,10 +5801,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do {OuterModule, :abc, 4} => %ModFunInfo{ params: [ [ - {:a, [line: 2, column: 11], nil}, - {:\\, [line: 2, column: 16], [{:b, [line: 2, column: 14], nil}, nil]}, - {:c, [line: 2, column: 24], nil}, - {:\\, [line: 2, column: 29], [{:d, [line: 2, column: 27], nil}, [1]]} + {:a, _, nil}, + {:\\, _, [{:b, _, nil}, nil]}, + {:c, _, nil}, + {:\\, _, [{:d, _, nil}, [1]]} ] ] } @@ -6028,11 +6028,11 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert %{ {MyModule, :is_even, 1} => %{ - params: [[{:value, [line: 2, column: 20], nil}]], + params: [[{:value, _, nil}]], positions: [{2, 3}] }, {MyModule, :is_odd, 1} => %{ - params: [[{:value, [line: 3, column: 20], nil}]], + params: [[{:value, _, nil}]], positions: [{3, 3}] }, {MyModule, :useless, 0} => %{ @@ -6079,19 +6079,19 @@ defmodule ElixirSense.Core.MetadataBuilderTest do type: :defp }, {MyModuleWithFuns, :is_even, 1} => %ModFunInfo{ - params: [[{:value, [line: 16, column: 20], nil}]], + params: [[{:value, _, nil}]], type: :defguard }, {MyModuleWithFuns, :is_evenp, 1} => %ModFunInfo{ - params: [[{:value, [line: 17, column: 22], nil}]], + params: [[{:value, _, nil}]], type: :defguardp }, {MyModuleWithFuns, :macro1, 1} => %ModFunInfo{ - params: [[{:ast, [line: 10, column: 19], nil}]], + params: [[{:ast, _, nil}]], type: :defmacro }, {MyModuleWithFuns, :macro1p, 1} => %ModFunInfo{ - params: [[{:ast, [line: 13, column: 21], nil}]], + params: [[{:ast, _, nil}]], type: :defmacrop }, {MyModuleWithFuns, nil, nil} => %ModFunInfo{ @@ -6107,7 +6107,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do type: :defmodule }, {MyModuleWithFuns, :__info__, 1} => %ElixirSense.Core.State.ModFunInfo{ - params: [[{:atom, [line: 3, column: 1], nil}]], + params: [[{:atom, _, nil}]], type: :def }, {MyModuleWithFuns, :module_info, 0} => %ElixirSense.Core.State.ModFunInfo{ @@ -6115,11 +6115,11 @@ defmodule ElixirSense.Core.MetadataBuilderTest do type: :def }, {MyModuleWithFuns, :module_info, 1} => %ElixirSense.Core.State.ModFunInfo{ - params: [[{:atom, [line: 3, column: 1], nil}]], + params: [[{:atom, _, nil}]], type: :def }, {MyModuleWithFuns.Nested, :__info__, 1} => %ElixirSense.Core.State.ModFunInfo{ - params: [[{:atom, [line: 19, column: 3], nil}]], + params: [[{:atom, _, nil}]], type: :def }, {MyModuleWithFuns.Nested, :module_info, 0} => %ElixirSense.Core.State.ModFunInfo{ @@ -6127,11 +6127,11 @@ defmodule ElixirSense.Core.MetadataBuilderTest do type: :def }, {MyModuleWithFuns.Nested, :module_info, 1} => %ElixirSense.Core.State.ModFunInfo{ - params: [[{:atom, [line: 19, column: 3], nil}]], + params: [[{:atom, _, nil}]], type: :def }, {MyModuleWithoutFuns, :__info__, 1} => %ElixirSense.Core.State.ModFunInfo{ - params: [[{:atom, [line: 1, column: 1], nil}]], + params: [[{:atom, _, nil}]], type: :def }, {MyModuleWithoutFuns, :module_info, 0} => %ElixirSense.Core.State.ModFunInfo{ @@ -6139,7 +6139,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do type: :def }, {MyModuleWithoutFuns, :module_info, 1} => %ElixirSense.Core.State.ModFunInfo{ - params: [[{:atom, [line: 1, column: 1], nil}]], + params: [[{:atom, _, nil}]], type: :def } } = state.mods_funs_to_positions @@ -6963,6 +6963,27 @@ defmodule ElixirSense.Core.MetadataBuilderTest do end describe "calls" do + test "registers calls on default parameters" do + state = + """ + defmodule NyModule do + def func1(a, b \\\\ some(), c \\\\ Some.other()), do: :ok + end + """ + |> string_to_state + + assert [ + %CallInfo{ + arity: 0, + func: :other, + mod: Some, + position: {2, 39} + }, + %CallInfo{arity: 0, position: {2, 21}, func: :some, mod: nil}, + %CallInfo{arity: 2, position: {2, 3}, func: :def, mod: Kernel} + ] = state.calls[2] + end + test "registers calls with __MODULE__" do state = """ @@ -8314,14 +8335,14 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert %{ {My, :required, 1} => %ModFunInfo{ - params: [[{:var, [{:line, 2} | _], _}]], + params: [[{:var, _, _}]], positions: [{2, _}], target: nil, type: :defmacro, overridable: {true, ElixirSenseExample.OverridableFunctions} }, {My, :test, 2} => %ModFunInfo{ - params: [[{:x, [{:line, 2} | _], _}, {:y, [{:line, 2} | _], _}]], + params: [[{:x, _, _}, {:y, _, _}]], positions: [{2, _}], target: nil, type: :def, @@ -8348,7 +8369,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do overridable: {true, ElixirSenseExample.OverridableImplementation} }, {My, :bar, 1} => %ModFunInfo{ - params: [[{:var, [{:line, 2} | _], _}]], + params: [[{:var, _, _}]], positions: [{2, _}], target: nil, type: :defmacro, @@ -8375,8 +8396,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert %{ {My, :required, 1} => %ModFunInfo{ params: [ - [{:baz, [line: 8, column: 21], nil}], - [{:var, [{:line, 2} | _], _}] + [{:baz, _, nil}], + [{:var, _, _}] ], positions: [{8, 3}, {2, _}], target: nil, @@ -8385,8 +8406,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do }, {My, :test, 2} => %ModFunInfo{ params: [ - [{:a, [line: 4, column: 12], nil}, {:b, [line: 4, column: 15], nil}], - [{:x, [{:line, 2} | _], _}, {:y, [{:line, 2} | _], _}] + [{:a, _, nil}, {:b, _, nil}], + [{:x, _, _}, {:y, _, _}] ], positions: [{4, 3}, {2, _}], target: nil, @@ -8421,8 +8442,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do }, {My, :bar, 1} => %ModFunInfo{ params: [ - [{:baz, [line: 8, column: 16], nil}], - [{:var, [{:line, 2} | _], _}] + [{:baz, _, nil}], + [{:var, _, _}] ], positions: [{8, 3}, {2, _}], target: nil, @@ -8450,8 +8471,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert %{ {My, :required, 1} => %ModFunInfo{ params: [ - [{:baz, [line: 8, column: 22], nil}], - [{:var, [{:line, 2} | _], _}] + [{:baz, _, nil}], + [{:var, _, _}] ], positions: [{8, 3}, {2, _}], target: nil, @@ -8460,8 +8481,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do }, {My, :test, 2} => %ModFunInfo{ params: [ - [{:a, [line: 4, column: 13], nil}, {:b, [line: 4, column: 16], nil}], - [{:x, [{:line, 2} | _], _}, {:y, [{:line, 2} | _], _}] + [{:a, _, nil}, {:b, _, nil}], + [{:x, _, _}, {:y, _, _}] ], positions: [{4, 3}, {2, _}], target: nil, From c682ddb1d8cd46bd5336badf1a5465c792b7db49 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 30 Aug 2024 21:01:51 +0200 Subject: [PATCH 182/235] handle unexpected default operator --- lib/elixir_sense/core/compiler.ex | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index b26905f3..da23d375 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -86,12 +86,16 @@ defmodule ElixirSense.Core.Compiler do expand({:"__|__", meta, [left, right]}, s, e) end + defp do_expand({:"\\\\", meta, [left, right]}, s, e) do + # elixir doesn't match on naked default args operator + expand({:"__\\\\__", meta, [left, right]}, s, e) + end + # __block__ defp do_expand({:__block__, _meta, []}, s, e), do: {nil, s, e} defp do_expand({:__block__, _meta, [arg]}, s, e) do - # s = s |> add_current_env_to_line(Keyword.fetch!(meta, :line), e) expand(arg, s, e) end From 2e982176beeecbdc5aab92658cb8c21822bb49ac Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 30 Aug 2024 21:21:58 +0200 Subject: [PATCH 183/235] handle invalid number of args in for do block and catch --- lib/elixir_sense/core/compiler.ex | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index da23d375..056a90bd 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -2194,12 +2194,15 @@ defmodule ElixirSense.Core.Compiler do # elixir checks here that clause has exactly 1 arg by matching against {_, _, [[_], _]} # we drop excessive or generate a fake arg - # TODO this is invalid for guards {args, discarded_args} = case args do [] -> {[{:_, [], e.module}], []} + [{:when, meta, [head | rest]}] -> + [last | rest_reversed] = Enum.reverse(rest) + {[{:when, meta, [head, last]}], Enum.reverse(rest_reversed)} + [head | rest] -> {[head], rest} end @@ -2921,6 +2924,12 @@ defmodule ElixirSense.Core.Compiler do {ret, %{se | stacktrace: old_stacktrace}} end + defp expand_catch(meta, [{:when, when_meta, [a1, a2, a3, _ | _]}], s, e) do + # elixir raises here wrong_number_of_args_for_clause + # TODO expand dropped + expand_catch(meta, [{:when, when_meta, [a1, a2, a3]}], s, e) + end + defp expand_catch(_meta, args = [_], s, e) do # no point in doing type inference here, we have no idea what throw we caught head(args, s, e) @@ -2934,6 +2943,7 @@ defmodule ElixirSense.Core.Compiler do defp expand_catch(meta, [a1, a2 | _], s, e) do # attempt to recover from error by taking 2 first args # elixir raises here wrong_number_of_args_for_clause + # TODO expand dropped expand_catch(meta, [a1, a2], s, e) end @@ -2946,6 +2956,7 @@ defmodule ElixirSense.Core.Compiler do defp expand_rescue(meta, [a1 | _], s, e) do # try to recover from error by taking first argument only # elixir raises here wrong_number_of_args_for_clause + # TODO expand dropped expand_rescue(meta, [a1], s, e) end @@ -3517,7 +3528,6 @@ defmodule ElixirSense.Core.Compiler do defmodule Fn do alias ElixirSense.Core.Compiler, as: ElixirExpand - alias ElixirSense.Core.Compiler.Env, as: ElixirEnv alias ElixirSense.Core.Compiler.Clauses, as: ElixirClauses alias ElixirSense.Core.Compiler.Dispatch, as: ElixirDispatch alias ElixirSense.Core.Compiler.Utils, as: ElixirUtils From 3fb8ba55f86d2781954649f674902bca84e48691 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 1 Sep 2024 12:39:29 +0200 Subject: [PATCH 184/235] remove ast and macro expander --- lib/elixir_sense/core/ast.ex | 199 ------------------ lib/elixir_sense/core/compiler/typespec.ex | 7 +- lib/elixir_sense/core/macro_expander.ex | 79 ------- lib/elixir_sense/core/state.ex | 18 ++ test/elixir_sense/core/ast_test.exs | 36 ---- .../elixir_sense/core/macro_expander_test.exs | 66 ------ 6 files changed, 20 insertions(+), 385 deletions(-) delete mode 100644 lib/elixir_sense/core/ast.ex delete mode 100644 test/elixir_sense/core/ast_test.exs delete mode 100644 test/elixir_sense/core/macro_expander_test.exs diff --git a/lib/elixir_sense/core/ast.ex b/lib/elixir_sense/core/ast.ex deleted file mode 100644 index 3e9ba9d5..00000000 --- a/lib/elixir_sense/core/ast.ex +++ /dev/null @@ -1,199 +0,0 @@ -defmodule ElixirSense.Core.Ast do - @moduledoc """ - Abstract Syntax Tree support - """ - - # TODO the code in this module is broken and probably violates GPL license - # TODO replace - - @partials [ - :def, - :defp, - :defmodule, - :defprotocol, - :defimpl, - :defstruct, - :defexception, - :@, - :defmacro, - :defmacrop, - :defguard, - :defguardp, - :defdelegate, - :defoverridable, - :fn, - :__ENV__, - :__CALLER__, - :raise, - :throw, - :reraise, - :send, - :if, - :unless, - :with, - :case, - :cond, - :try, - :for, - :receive, - :in - ] - - @max_expand_count 30_000 - - def expand_partial(ast, env) do - {expanded_ast, _} = Macro.prewalk(ast, {env, 1}, &do_expand_partial/2) - expanded_ast - rescue - _e -> ast - catch - e -> e - end - - def expand_all(ast, env) do - try do - {expanded_ast, _} = Macro.prewalk(ast, {env, 1}, &do_expand_all/2) - expanded_ast - rescue - _e -> ast - catch - e -> e - end - end - - def set_module_for_env(env, module) do - Map.put(env, :module, module) - end - - def add_requires_to_env(env, modules) do - add_directive_modules_to_env(env, :require, modules) - end - - def add_imports_to_env(env, modules) do - add_directive_modules_to_env(env, :import, modules) - end - - defp add_directive_modules_to_env(env, directive, modules) do - directive_string = - modules - |> Enum.map(&format_module(directive, &1)) - |> Enum.filter(&(&1 != nil)) - |> Enum.join("; ") - - {new_env, _} = Code.eval_string("#{directive_string}; __ENV__", [], env) - new_env - end - - defp format_module(_directive, Elixir), do: nil - - defp format_module(directive, module) when is_atom(module) do - if match?({:module, _}, Code.ensure_compiled(module)) do - "#{directive} #{inspect(module)}" - end - end - - defp format_module(directive, {module, options}) when is_atom(module) do - if match?({:module, _}, Code.ensure_compiled(module)) do - formatted_options = - if options != [] do - ", " <> Macro.to_string(options) - else - "" - end - - "#{directive} #{inspect(module)}#{formatted_options}" - end - end - - defp do_expand_all(ast, acc) do - do_expand(ast, acc) - end - - defp do_expand_partial({name, _, _} = ast, acc) when name in @partials do - {ast, acc} - end - - defp do_expand_partial(ast, acc) do - do_expand(ast, acc) - end - - # TODO should we add imports here as well? - - defp do_expand({:require, _, _} = ast, {env, count}) do - # TODO is it ok to loose alias_tuples here? - {modules, _alias_tuples} = extract_directive_modules(:require, ast) - new_env = add_requires_to_env(env, modules) - {ast, {new_env, count}} - end - - defp do_expand(ast, acc) do - do_expand_with_fixes(ast, acc) - end - - # Fix inexpansible `use ExUnit.Case` - defp do_expand_with_fixes({:use, _, [{:__aliases__, _, [:ExUnit, :Case]} | _]}, acc) do - ast = - quote do - import ExUnit.Callbacks - import ExUnit.Assertions - import ExUnit.Case - import ExUnit.DocTest - end - - {ast, acc} - end - - defp do_expand_with_fixes(ast, {env, count}) do - if count > @max_expand_count do - throw({:expand_error, "Cannot expand recursive macro"}) - end - - try do - expanded_ast = Macro.expand(ast, env) - {expanded_ast, {env, count + 1}} - rescue - _e -> - {ast, {env, count + 1}} - end - end - - defp extract_directive_modules(directive, ast) do - case ast do - # multi notation - {^directive, _, [{{:., _, [{:__aliases__, _, prefix_atoms}, :{}]}, _, aliases}]} -> - list = - aliases - |> Enum.map(fn {:__aliases__, _, mods} -> - Module.concat(prefix_atoms ++ mods) - end) - - {list, []} - - # with options - {^directive, _, [module, opts]} when is_atom(module) -> - alias_tuples = - case opts |> Keyword.get(:as) do - nil -> [] - alias -> [{alias, module}] - end - - {[module], alias_tuples} - - # with options - {^directive, _, [{:__aliases__, _, module_parts}, _opts]} -> - {[module_parts |> Module.concat()], []} - - # without options - {^directive, _, [{:__aliases__, _, module_parts}]} -> - {[module_parts |> Module.concat()], []} - - # without options - {^directive, _, [module]} when is_atom(module) -> - {[module], []} - - {^directive, _, [{{:., _, [prefix, :{}]}, _, suffixes} | _]} when is_list(suffixes) -> - list = for suffix <- suffixes, do: Module.concat(prefix, suffix) - {list, []} - end - end -end diff --git a/lib/elixir_sense/core/compiler/typespec.ex b/lib/elixir_sense/core/compiler/typespec.ex index d9bffa2f..0f03a6a3 100644 --- a/lib/elixir_sense/core/compiler/typespec.ex +++ b/lib/elixir_sense/core/compiler/typespec.ex @@ -35,13 +35,10 @@ defmodule ElixirSense.Core.Compiler.Typespec do # unless there are unquotes module vars are not accessible # TODO handle unquotes state_orig = state - state = new_func_vars_scope(state) - {ast, state, env} = do_expand_spec(ast, state, env) + {ast, state, env} = do_expand_spec(ast, new_func_vars_scope(state), env) - state = remove_func_vars_scope(state, state_orig) - - {ast, state, env} + {ast, remove_func_vars_scope(state, state_orig), env} end defp do_expand_spec({:when, meta, [spec, guard]}, state, env) do diff --git a/lib/elixir_sense/core/macro_expander.ex b/lib/elixir_sense/core/macro_expander.ex index 59b32825..333089ed 100644 --- a/lib/elixir_sense/core/macro_expander.ex +++ b/lib/elixir_sense/core/macro_expander.ex @@ -7,83 +7,4 @@ defmodule ElixirSense.Core.MacroExpander do Keyword.merge(keyword, context: Elixir, import: Kernel) end) end - - def expand_use(ast, module, current_aliases, meta) do - env = %Macro.Env{ - module: module, - function: nil, - aliases: current_aliases, - macros: __ENV__.macros - } - - {use_expanded, _env} = Macro.prewalk(ast, env, &require_and_expand/2) - {use_expanded_with_meta, _meta} = Macro.prewalk(use_expanded, meta, &append_meta/2) - use_expanded_with_meta - end - - defp require_and_expand({:require, _, _} = ast, env) do - {env_after_require, _binding} = Code.eval_string("#{Macro.to_string(ast)}; __ENV__", [], env) - {ast, env_after_require} - end - - defp require_and_expand({:use, meta, arg}, env) do - use_directive_expanded = Macro.expand_once({:use, meta, arg}, env) - {use_directive_expanded, env} - end - - defp require_and_expand({{:., meta1, [module, :__using__]}, meta2, params}, env) - when is_atom(module) do - splitted = - Module.split(module) - |> Enum.map(&String.to_atom/1) - - module_expanded = Macro.expand_once({:__aliases__, [], splitted}, env) - ast_with_module_expanded = {{:., meta1, [module_expanded, :__using__]}, meta2, params} - ast_expanded = Macro.expand_once(ast_with_module_expanded, env) - - if ast_with_module_expanded != ast_expanded do - {{:__block__, [], [ast_expanded]}, env} - else - {[], env} - end - end - - defp require_and_expand(ast, env) do - {ast, env} - end - - defp append_meta({:defoverridable, ast_meta, args}, meta) when is_list(ast_meta) do - {{:defoverridable, Keyword.merge(ast_meta, meta), args}, meta} - end - - defp append_meta({:__aliases__, ast_meta, args}, meta) when is_list(ast_meta) do - new_args = - case ast_meta[:alias] do - false -> - args - - nil -> - args - - alias when is_atom(alias) -> - Module.split(alias) - |> Enum.map(&String.to_atom/1) - end - - {{:__aliases__, meta, new_args}, meta} - end - - defp append_meta({atom, ast_meta, args}, meta) when is_atom(atom) and is_list(ast_meta) do - new_args = - case args do - atom when is_atom(atom) -> nil - other -> other - end - - {{atom, meta, new_args}, meta} - end - - defp append_meta(other, meta) do - {other, meta} - end end diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 3a81b8c5..d50895e4 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -150,6 +150,24 @@ defmodule ElixirSense.Core.State do context: nil, typespec: nil, scope_id: nil + + def to_macro_env(%__MODULE__{} = env, file \\ "nofile", line \\ 1) do + # we omit lexical_tracker and tracers + %Macro.Env{ + line: line, + file: file, + context: env.context, + module: env.module, + function: env.function, + context_modules: env.context_modules, + macros: env.macros, + functions: env.functions, + requires: env.requires, + aliases: env.aliases, + macro_aliases: env.macro_aliases, + versioned_vars: env.versioned_vars + } + end end defmodule VarInfo do diff --git a/test/elixir_sense/core/ast_test.exs b/test/elixir_sense/core/ast_test.exs deleted file mode 100644 index 5a6eac35..00000000 --- a/test/elixir_sense/core/ast_test.exs +++ /dev/null @@ -1,36 +0,0 @@ -defmodule ElixirSense.Core.AstTest do - use ExUnit.Case, async: true - alias ElixirSense.Core.Ast - - defmodule ExpandRecursive do - defmacro my_macro do - quote do - abc = my_macro() - end - end - end - - test "expand_partial cannot expand recursive macros" do - import ExpandRecursive - - result = - quote do - my_macro() - end - |> Ast.expand_partial(__ENV__) - - assert result == {:expand_error, "Cannot expand recursive macro"} - end - - test "expand_all cannot expand recursive macros" do - import ExpandRecursive - - result = - quote do - my_macro() - end - |> Ast.expand_all(__ENV__) - - assert result == {:expand_error, "Cannot expand recursive macro"} - end -end diff --git a/test/elixir_sense/core/macro_expander_test.exs b/test/elixir_sense/core/macro_expander_test.exs deleted file mode 100644 index 22219b00..00000000 --- a/test/elixir_sense/core/macro_expander_test.exs +++ /dev/null @@ -1,66 +0,0 @@ -defmodule ElixirSense.Core.MacroExpanderTest do - use ExUnit.Case, async: true - - alias ElixirSense.Core.MacroExpander - - test "expand use" do - ast = - quote do - use ElixirSenseExample.OverridableFunctions - end - - expanded = - ast - |> MacroExpander.add_default_meta() - |> MacroExpander.expand_use(MyModule, [], line: 2, column: 1) - - assert Macro.to_string(expanded) =~ "defmacro required(var)" - end - - test "expand use with alias" do - ast = - quote do - use OverridableFunctions - end - - expanded = - ast - |> MacroExpander.add_default_meta() - |> MacroExpander.expand_use( - MyModule, - [{OverridableFunctions, ElixirSenseExample.OverridableFunctions}], - line: 2, - column: 1 - ) - - assert Macro.to_string(expanded) =~ "defmacro required(var)" - end - - test "expand use calling use" do - ast = - quote do - use ElixirSenseExample.Overridable.Using - end - - expanded = - ast - |> MacroExpander.add_default_meta() - |> MacroExpander.expand_use(MyModule, [], line: 2, column: 1) - - assert Macro.to_string(expanded) =~ "defmacro bar(var)" - end - - test "expand use when module does not define __using__ macro" do - ast = - quote do - use ElixirSenseExample.OverridableBehaviour - end - - expanded = - ast - |> MacroExpander.add_default_meta() - |> MacroExpander.expand_use(MyModule, [], line: 2, column: 1) - - assert Macro.to_string(expanded) =~ "require ElixirSenseExample.OverridableBehaviour" - end -end From 65737d7054f7aa3ec3a4b64a8aee069a3e724067 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sun, 1 Sep 2024 12:44:51 +0200 Subject: [PATCH 185/235] remove leftovers --- lib/elixir_sense/core/macro_expander.ex | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 lib/elixir_sense/core/macro_expander.ex diff --git a/lib/elixir_sense/core/macro_expander.ex b/lib/elixir_sense/core/macro_expander.ex deleted file mode 100644 index 333089ed..00000000 --- a/lib/elixir_sense/core/macro_expander.ex +++ /dev/null @@ -1,10 +0,0 @@ -# TODO replace this module -defmodule ElixirSense.Core.MacroExpander do - @moduledoc false - - def add_default_meta(expr) do - Macro.update_meta(expr, fn keyword -> - Keyword.merge(keyword, context: Elixir, import: Kernel) - end) - end -end From 8b1772e05ebf5b57d0b292b2b9e33d4ebbc14c50 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 11 Sep 2024 16:49:11 +0200 Subject: [PATCH 186/235] fix var rebinding in defs --- lib/elixir_sense/core/state.ex | 12 ++++++++- .../core/metadata_builder_test.exs | 25 ++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index d50895e4..04f5ca0d 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -878,6 +878,7 @@ defmodule ElixirSense.Core.State do } end + # TODO check if we can get rid of this function defp update_vars_info_per_scope_id(state) do [scope_id | _other_scope_ids] = state.scope_ids [current_scope_vars | _other_scope_vars] = state.vars_info @@ -885,7 +886,16 @@ defmodule ElixirSense.Core.State do for {scope_id, vars} <- state.vars_info_per_scope_id, into: %{} do updated_vars = for {key, var} <- vars, into: %{} do - {key, Map.get(current_scope_vars, key, var)} + updated_var = case Map.get(current_scope_vars, key) do + nil -> var + scope_var -> + if hd(scope_var.positions) == hd(var.positions) do + scope_var + else + var + end + end + {key, updated_var} end {scope_id, updated_vars} diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index b598cbaf..2b333c0f 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -185,6 +185,30 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } end + test "rebinding in defs" do + state = """ + defmodule MyModule do + def go(asd = 3, asd, x) do + :ok + end + + def go(asd = 3, [2, asd], y) do + :ok + end + end + """ + |> string_to_state + + assert %{ + {:x, 1} => %VarInfo{positions: [{2, 24}]}, + {:asd, 0} => %VarInfo{positions: [{2, 10}, {2, 19}]} + } = state.vars_info_per_scope_id[2] + assert %{ + {:y, 1} => %VarInfo{positions: [{6, 29}]}, + {:asd, 0} => %VarInfo{positions: [{6, 10}, {6, 23}]} + } = state.vars_info_per_scope_id[3] + end + test "binding in function call" do state = """ @@ -9074,7 +9098,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do env -> env.vars - # state.vars_info_per_scope_id[env.scope_id] end |> Enum.sort() end From 788985125222d02edfb01245252cda1f13f8d489 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 11 Sep 2024 17:08:39 +0200 Subject: [PATCH 187/235] add function needed by debug adapter --- lib/elixir_sense/core/state.ex | 48 +++++++++++++++---- .../core/metadata_builder_test.exs | 34 ++++++------- 2 files changed, 56 insertions(+), 26 deletions(-) diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 04f5ca0d..2032d0d7 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -168,6 +168,23 @@ defmodule ElixirSense.Core.State do versioned_vars: env.versioned_vars } end + + def update_from_macro_env(%__MODULE__{} = env, macro_env = %Macro.Env{}) do + # we omit lexical_tracker and tracers + %__MODULE__{ + env + | context: macro_env.context, + module: macro_env.module, + function: macro_env.function, + context_modules: macro_env.context_modules, + macros: macro_env.macros, + functions: macro_env.functions, + requires: macro_env.requires, + aliases: macro_env.aliases, + macro_aliases: macro_env.macro_aliases, + versioned_vars: macro_env.versioned_vars + } + end end defmodule VarInfo do @@ -343,8 +360,15 @@ defmodule ElixirSense.Core.State do current_scope_id = hd(state.scope_ids) # Macro.Env versioned_vars is not updated - # elixir keeps current vars instate - {versioned_vars, _} = state.vars + # elixir keeps current vars in state + # write vars are not really interesting (nor are write vars from upper write) + # only read vars are accessible + # NOTE definition/hover/references providers get all vars mapped to scope_id + # this means write vars are already closed + # completions provider do not need write vars at all + {read, _write} = state.vars + versioned_vars = read + [current_vars_info | _] = state.vars_info # here we filter vars to only return the ones with nil context to maintain macro hygiene @@ -886,15 +910,19 @@ defmodule ElixirSense.Core.State do for {scope_id, vars} <- state.vars_info_per_scope_id, into: %{} do updated_vars = for {key, var} <- vars, into: %{} do - updated_var = case Map.get(current_scope_vars, key) do - nil -> var - scope_var -> - if hd(scope_var.positions) == hd(var.positions) do - scope_var - else + updated_var = + case Map.get(current_scope_vars, key) do + nil -> var - end - end + + scope_var -> + if hd(scope_var.positions) == hd(var.positions) do + scope_var + else + var + end + end + {key, updated_var} end diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 2b333c0f..fab94694 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -186,27 +186,29 @@ defmodule ElixirSense.Core.MetadataBuilderTest do end test "rebinding in defs" do - state = """ - defmodule MyModule do - def go(asd = 3, asd, x) do - :ok - end + state = + """ + defmodule MyModule do + def go(asd = 3, asd, x) do + :ok + end - def go(asd = 3, [2, asd], y) do - :ok + def go(asd = 3, [2, asd], y) do + :ok + end end - end - """ - |> string_to_state + """ + |> string_to_state assert %{ - {:x, 1} => %VarInfo{positions: [{2, 24}]}, - {:asd, 0} => %VarInfo{positions: [{2, 10}, {2, 19}]} - } = state.vars_info_per_scope_id[2] + {:x, 1} => %VarInfo{positions: [{2, 24}]}, + {:asd, 0} => %VarInfo{positions: [{2, 10}, {2, 19}]} + } = state.vars_info_per_scope_id[2] + assert %{ - {:y, 1} => %VarInfo{positions: [{6, 29}]}, - {:asd, 0} => %VarInfo{positions: [{6, 10}, {6, 23}]} - } = state.vars_info_per_scope_id[3] + {:y, 1} => %VarInfo{positions: [{6, 29}]}, + {:asd, 0} => %VarInfo{positions: [{6, 10}, {6, 23}]} + } = state.vars_info_per_scope_id[3] end test "binding in function call" do From 29aa17762c70dd23d1be4b1c634e7fc2d575ae63 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 12 Sep 2024 06:57:32 +0200 Subject: [PATCH 188/235] move write var handling to state module --- lib/elixir_sense/core/compiler.ex | 85 +++++++------------------------ lib/elixir_sense/core/state.ex | 31 +++++++++++ 2 files changed, 49 insertions(+), 67 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 056a90bd..1552e144 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -629,9 +629,8 @@ defmodule ElixirSense.Core.Compiler do {{name, meta, kind}, s, e} _ -> - # elixir raises here undefined_var - span_meta = __MODULE__.Env.calculate_span(meta, name) - {{name, span_meta, kind}, s, e} + # elixir raises here undefined_var and attaches span meta + {{name, meta, kind}, s, e} end end end @@ -671,7 +670,7 @@ defmodule ElixirSense.Core.Compiler do {module, fun} end - expand_remote(ar, meta, af, meta, args, state, __MODULE__.Env.prepare_write(state), env) + expand_remote(ar, meta, af, meta, args, state, prepare_write(state), env) {:error, :not_found} -> expand_local(meta, fun, args, state, env) @@ -694,7 +693,7 @@ defmodule ElixirSense.Core.Compiler do when (is_tuple(module) or is_atom(module)) and is_atom(fun) and is_list(meta) and is_list(args) do # dbg({module, fun, args}) - {module, state_l, env} = expand(module, __MODULE__.Env.prepare_write(state), env) + {module, state_l, env} = expand(module, prepare_write(state), env) arity = length(args) if is_atom(module) do @@ -772,9 +771,9 @@ defmodule ElixirSense.Core.Compiler do defp do_expand(list, s, e) when is_list(list) do {e_args, {se, _}, ee} = - expand_list(list, &expand_arg/3, {__MODULE__.Env.prepare_write(s), s}, e, []) + expand_list(list, &expand_arg/3, {prepare_write(s), s}, e, []) - {e_args, __MODULE__.Env.close_write(se, s), ee} + {e_args, close_write(se, s), ee} end defp do_expand(function, s, e) when is_function(function) do @@ -1895,7 +1894,7 @@ defmodule ElixirSense.Core.Compiler do ) do {:ok, rewritten} -> s = - __MODULE__.Env.close_write(sa, s) + close_write(sa, s) |> add_call_to_line({receiver, right, length(e_args)}, meta) |> add_current_env_to_line(meta, e) @@ -1904,7 +1903,7 @@ defmodule ElixirSense.Core.Compiler do {:error, _error} -> # elixir raises here elixir_rewrite s = - __MODULE__.Env.close_write(sa, s) + close_write(sa, s) |> add_call_to_line({receiver, right, length(e_args)}, meta) |> add_current_env_to_line(meta, e) @@ -1918,7 +1917,7 @@ defmodule ElixirSense.Core.Compiler do {e_args, {sa, _}, ea} = map_fold(&expand_arg/3, {sl, s}, e, args) s = - __MODULE__.Env.close_write(sa, s) + close_write(sa, s) |> add_call_to_line({receiver, right, length(e_args)}, meta) |> add_current_env_to_line(meta, e) @@ -2239,7 +2238,7 @@ defmodule ElixirSense.Core.Compiler do defp expand_for_generator({:<-, meta, [left, right]}, s, e) do {e_right, sr, er} = expand(right, s, e) - sm = __MODULE__.Env.reset_read(sr, s) + sm = reset_read(sr, s) {[e_left], sl, el} = __MODULE__.Clauses.head([left], sm, er) match_context_r = TypeInference.type_of(e_right, e.context) @@ -2256,7 +2255,7 @@ defmodule ElixirSense.Core.Compiler do case __MODULE__.Utils.split_last(args) do {left_start, {:<-, op_meta, [left_end, right]}} -> {e_right, sr, er} = expand(right, s, e) - sm = __MODULE__.Env.reset_read(sr, s) + sm = reset_read(sr, s) {e_left, sl, el} = __MODULE__.Clauses.match( @@ -2391,7 +2390,7 @@ defmodule ElixirSense.Core.Compiler do end def expand_arg(arg, {acc, s}, e) do - {e_arg, s_acc, e_acc} = expand(arg, __MODULE__.Env.reset_read(acc, s), e) + {e_arg, s_acc, e_acc} = expand(arg, reset_read(acc, s), e) {e_arg, {s_acc, s}, e_acc} end @@ -2405,8 +2404,8 @@ defmodule ElixirSense.Core.Compiler do end def expand_args(args, s, e) do - {e_args, {sa, _}, ea} = map_fold(&expand_arg/3, {__MODULE__.Env.prepare_write(s), s}, e, args) - {e_args, __MODULE__.Env.close_write(sa, s), ea} + {e_args, {sa, _}, ea} = map_fold(&expand_arg/3, {prepare_write(s), s}, e, args) + {e_args, close_write(sa, s), ea} end @internals [{:behaviour_info, 1}, {:module_info, 1}, {:module_info, 0}] @@ -2441,52 +2440,6 @@ defmodule ElixirSense.Core.Compiler do end end - defmodule Env do - alias ElixirSense.Core.Compiler.Utils, as: ElixirUtils - - def reset_read(%{vars: {_, write}} = s, %{vars: {read, _}}) do - %{s | vars: {read, write}} - end - - def prepare_write(%{vars: {read, _}} = s) do - %{s | vars: {read, read}} - end - - def close_write(%{vars: {_read, write}} = s, %{vars: {_, false}}) do - %{s | vars: {write, false}} - end - - def close_write(%{vars: {_read, write}} = s, %{vars: {_, upper_write}}) do - %{s | vars: {write, merge_vars(upper_write, write)}} - end - - defp merge_vars(v, v), do: v - - defp merge_vars(v1, v2) do - :maps.fold( - fn k, m2, acc -> - case Map.fetch(acc, k) do - {:ok, m1} when m1 >= m2 -> acc - _ -> Map.put(acc, k, m2) - end - end, - v1, - v2 - ) - end - - def calculate_span(meta, name) do - case Keyword.fetch(meta, :column) do - {:ok, column} -> - span = {ElixirUtils.get_line(meta), column + String.length(Atom.to_string(name))} - [{:span, span} | meta] - - _ -> - meta - end - end - end - defmodule Utils do def split_last([]), do: {[], []} @@ -2549,7 +2502,6 @@ defmodule ElixirSense.Core.Compiler do defmodule Clauses do alias ElixirSense.Core.Compiler, as: ElixirExpand - alias ElixirSense.Core.Compiler.Env, as: ElixirEnv alias ElixirSense.Core.Compiler.Utils, as: ElixirUtils alias ElixirSense.Core.State alias ElixirSense.Core.TypeInference @@ -2823,7 +2775,7 @@ defmodule ElixirSense.Core.Compiler do defp expand_with({:<-, meta, [left, right]}, {s, e}) do {e_right, sr, er} = ElixirExpand.expand(right, s, e) - sm = ElixirEnv.reset_read(sr, s) + sm = reset_read(sr, s) {[e_left], sl, el} = head([left], sm, er) match_context_r = TypeInference.type_of(e_right, e.context) @@ -3100,7 +3052,6 @@ defmodule ElixirSense.Core.Compiler do defmodule Bitstring do alias ElixirSense.Core.Compiler, as: ElixirExpand - alias ElixirSense.Core.Compiler.Env, as: ElixirEnv alias ElixirSense.Core.Compiler.Utils, as: ElixirUtils defp expand_match(expr, {s, original_s}, e) do @@ -3119,12 +3070,12 @@ defmodule ElixirSense.Core.Compiler do {{:<<>>, [{:alignment, alignment} | meta], e_args}, sa, ea} _ -> - pair_s = {ElixirEnv.prepare_write(s), s} + pair_s = {prepare_write(s), s} {e_args, alignment, {sa, _}, ea} = expand(meta, &ElixirExpand.expand_arg/3, args, [], pair_s, e, 0, require_size) - {{:<<>>, [{:alignment, alignment} | meta], e_args}, ElixirEnv.close_write(sa, s), ea} + {{:<<>>, [{:alignment, alignment} | meta], e_args}, close_write(sa, s), ea} end end @@ -3451,7 +3402,7 @@ defmodule ElixirSense.Core.Compiler do end defp expand_spec_arg(expr, s, original_s, e) do - ElixirExpand.expand(expr, ElixirEnv.reset_read(s, original_s), e) + ElixirExpand.expand(expr, reset_read(s, original_s), e) end defp size_and_unit(type, size, unit) diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index 2032d0d7..d71f718d 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -931,6 +931,37 @@ defmodule ElixirSense.Core.State do |> Map.put(scope_id, current_scope_vars) end + def reset_read(%{vars: {_, write}} = s, %{vars: {read, _}}) do + %{s | vars: {read, write}} + end + + def prepare_write(%{vars: {read, _}} = s) do + %{s | vars: {read, read}} + end + + def close_write(%{vars: {_read, write}} = s, %{vars: {_, false}}) do + %{s | vars: {write, false}} + end + + def close_write(%{vars: {_read, write}} = s, %{vars: {_, upper_write}}) do + %{s | vars: {write, merge_vars(upper_write, write)}} + end + + defp merge_vars(v, v), do: v + + defp merge_vars(v1, v2) do + :maps.fold( + fn k, m2, acc -> + case Map.fetch(acc, k) do + {:ok, m1} when m1 >= m2 -> acc + _ -> Map.put(acc, k, m2) + end + end, + v1, + v2 + ) + end + def remove_attributes_scope(%__MODULE__{} = state) do attributes = tl(state.attributes) %__MODULE__{state | attributes: attributes, scope_attributes: attributes} From 968f4d5b572e08c4038e99939470c301e6c737fc Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 12 Sep 2024 21:35:34 +0200 Subject: [PATCH 189/235] apply fix from elixir --- lib/elixir_sense/core/compiler/macro.ex | 4 +- .../metadata_builder/error_recovery_test.exs | 42 +++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/lib/elixir_sense/core/compiler/macro.ex b/lib/elixir_sense/core/compiler/macro.ex index 657ea4f2..6c511b01 100644 --- a/lib/elixir_sense/core/compiler/macro.ex +++ b/lib/elixir_sense/core/compiler/macro.ex @@ -109,13 +109,13 @@ defmodule ElixirSense.Core.Compiler.Macro do defp do_expand_once({:__DIR__, _, atom}, env) when is_atom(atom), do: {:filename.dirname(env.file), true} - defp do_expand_once({:__ENV__, _, atom}, env) when is_atom(atom) do + defp do_expand_once({:__ENV__, _, atom}, env) when is_atom(atom) and env.context != :match do env = update_in(env.versioned_vars, &maybe_escape_map/1) {maybe_escape_map(env), true} end defp do_expand_once({{:., _, [{:__ENV__, _, atom}, field]}, _, []} = original, env) - when is_atom(atom) and is_atom(field) do + when is_atom(atom) and is_atom(field) and env.context != :match do if Map.has_key?(env, field) do {maybe_escape_map(Map.get(env, field)), true} else diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index 05a4e090..aca388ae 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -1732,6 +1732,48 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do end end + describe "__ENV__, __MODULE__, __CALLER__, __STACKTRACE__, __DIR__" do + test "__ENV__ in match" do + code = """ + __ENV__ = \ + """ + + assert get_cursor_env(code) + end + + test "__CALLER__ not in macro" do + code = """ + inspect(__CALLER__, \ + """ + + assert get_cursor_env(code) + end + + test "__STACKTRACE__ outside of catch/rescue" do + code = """ + inspect(__STACKTRACE__, \ + """ + + assert get_cursor_env(code) + end + + test "__MODULE__ outside of module" do + code = """ + inspect(__MODULE__, \ + """ + + assert get_cursor_env(code) + end + + test "__DIR__ when nofile" do + code = """ + inspect(__DIR__, \ + """ + + assert get_cursor_env(code) + end + end + describe "alias/import/require" do test "invalid alias expansion" do code = """ From 45e635660a9dd2933470d0608d588b96302591f9 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 12 Sep 2024 22:12:43 +0200 Subject: [PATCH 190/235] find cursor in multi alias --- lib/elixir_sense/core/compiler.ex | 2 + .../metadata_builder/error_recovery_test.exs | 38 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 1552e144..0ad1cbd5 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -2066,6 +2066,8 @@ defmodule ElixirSense.Core.Compiler do other, s, e -> # elixir raises here # expected_compile_time_module + # we search for cursor + {_, s, _} = expand(other, s, e) {other, s, e} end diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index aca388ae..34435205 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -1804,6 +1804,44 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert get_cursor_env(code) end + test "multi" do + code = """ + alias ElixirSenseExample\ + """ + + assert get_cursor_env(code) + + code = """ + alias ElixirSenseExample.\ + """ + + assert get_cursor_env(code) + + code = """ + alias ElixirSenseExample.{\ + """ + + assert get_cursor_env(code) + + code = """ + alias ElixirSenseExample.{S\ + """ + + assert get_cursor_env(code) + + code = """ + alias ElixirSenseExample.{Some, \ + """ + + assert get_cursor_env(code) + + code = """ + alias ElixirSenseExample.{Some, Mod\ + """ + + assert get_cursor_env(code) + end + test "invalid" do code = """ alias A.a\ From 5028b6ae58987cf439b17c8e1863191b6142638e Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 12 Sep 2024 22:30:31 +0200 Subject: [PATCH 191/235] handle cursor in def name --- lib/elixir_sense/core/compiler.ex | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 0ad1cbd5..457c9b6b 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1586,6 +1586,14 @@ defmodule ElixirSense.Core.Compiler do ) when module != nil and def_kind in [:def, :defp, :defmacro, :defmacrop, :defguard, :defguardp] do + + state = case call do + {:__cursor__, _, list} when is_list(list) -> + {_, state, _} = expand(call, state, %{env | function: {:__unknown__, 0}}) + state + _ -> state + end + state_orig = state unquoted_call = __MODULE__.Quote.has_unquotes(call) From c868a1847b41bbe91a8031a8441fbb83ff562b6e Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 13 Sep 2024 22:26:08 +0200 Subject: [PATCH 192/235] handle cursor in more cases --- lib/elixir_sense/core/compiler.ex | 28 ++++++++-- .../metadata_builder/error_recovery_test.exs | 52 +++++++++++++++++++ 2 files changed, 77 insertions(+), 3 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 457c9b6b..e9494603 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -857,6 +857,20 @@ defmodule ElixirSense.Core.Compiler do {[], state, env} end + defp expand_macro( + meta, + Kernel, + :@, + [{:__cursor__, _meta, list} = arg], + _callback, + state, + env + ) + when is_list(list) do + {arg, state, _env} = expand(arg, state, env) + {{:@, meta, [arg]}, state, env} + end + defp expand_macro( meta, Kernel, @@ -1181,8 +1195,8 @@ defmodule ElixirSense.Core.Compiler do [_] -> # @attribute(arg) - if env.function, do: raise("cannot set attribute @#{name} inside function/macro") - if name == :behavior, do: raise("@behavior attribute is not supported") + # elixir validates env.function is nil + # elixir forbids behavior name {true, expand_args(args, state, env)} args -> @@ -1593,7 +1607,7 @@ defmodule ElixirSense.Core.Compiler do state _ -> state end - + state_orig = state unquoted_call = __MODULE__.Quote.has_unquotes(call) @@ -1811,6 +1825,14 @@ defmodule ElixirSense.Core.Compiler do {{{:., meta, [module, fun]}, meta, args}, state, env} else ast -> + state = if __MODULE__.Utils.has_cursor?(args) and not __MODULE__.Utils.has_cursor?(ast) do + # in case there was cursor in the original args but it's not present in macro result + # expand a fake node + {_ast, state, _env} = expand({:__cursor__, [], []}, state, env) + state + else + state + end {ast, state, env} = expand(ast, state, env) {ast, state, env} end diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index 34435205..33bca0be 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -2727,4 +2727,56 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert env.module == :"Elixir.__Unknown__" end end + + describe "attribute" do + test "after @" do + code = """ + defmodule Abc do + @\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.module == Abc + end + + test "in name" do + code = """ + defmodule Abc do + @foo\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.module == Abc + end + + test "after name" do + code = """ + defmodule Abc do + @foo \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.module == Abc + end + + test "outside module" do + code = """ + @foo [\ + """ + + assert {_, env} = get_cursor_env(code) + assert env.module == Abc + end + + test "setting inside def" do + code = """ + defmodule Abc do + def go do + @foo \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.module == Abc + end + end end From b802c3db057d64bc84ad17740ea2723b440be41c Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 19 Sep 2024 00:05:17 +0200 Subject: [PATCH 193/235] add find_var --- lib/elixir_sense/core/compiler.ex | 32 ++++++++------ lib/elixir_sense/core/metadata.ex | 44 +++++++------------ .../providers/completion/suggestion.ex | 5 --- .../providers/definition/locator.ex | 1 - .../providers/references/locator.ex | 1 - 5 files changed, 33 insertions(+), 50 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index e9494603..4aa6d535 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1600,13 +1600,15 @@ defmodule ElixirSense.Core.Compiler do ) when module != nil and def_kind in [:def, :defp, :defmacro, :defmacrop, :defguard, :defguardp] do + state = + case call do + {:__cursor__, _, list} when is_list(list) -> + {_, state, _} = expand(call, state, %{env | function: {:__unknown__, 0}}) + state - state = case call do - {:__cursor__, _, list} when is_list(list) -> - {_, state, _} = expand(call, state, %{env | function: {:__unknown__, 0}}) - state - _ -> state - end + _ -> + state + end state_orig = state @@ -1825,14 +1827,16 @@ defmodule ElixirSense.Core.Compiler do {{{:., meta, [module, fun]}, meta, args}, state, env} else ast -> - state = if __MODULE__.Utils.has_cursor?(args) and not __MODULE__.Utils.has_cursor?(ast) do - # in case there was cursor in the original args but it's not present in macro result - # expand a fake node - {_ast, state, _env} = expand({:__cursor__, [], []}, state, env) - state - else - state - end + state = + if __MODULE__.Utils.has_cursor?(args) and not __MODULE__.Utils.has_cursor?(ast) do + # in case there was cursor in the original args but it's not present in macro result + # expand a fake node + {_ast, state, _env} = expand({:__cursor__, [], []}, state, env) + state + else + state + end + {ast, state, env} = expand(ast, state, env) {ast, state, env} end diff --git a/lib/elixir_sense/core/metadata.ex b/lib/elixir_sense/core/metadata.ex index ac3fab13..b59719e2 100644 --- a/lib/elixir_sense/core/metadata.ex +++ b/lib/elixir_sense/core/metadata.ex @@ -80,11 +80,8 @@ defmodule ElixirSense.Core.Metadata do {end_line, end_column} ]) - # IO.puts(metadata.source) - # dbg(needle) source_with_cursor = prefix <> "__cursor__(#{needle})" <> suffix - # IO.puts(source_with_cursor) - # dbg(metadata) + {prefix, source_with_cursor} nil -> @@ -98,8 +95,6 @@ defmodule ElixirSense.Core.Metadata do {prefix, source_with_cursor} end - # IO.puts(source_with_cursor) - {meta, cursor_env} = case Code.string_to_quoted(source_with_cursor, columns: true, token_metadata: true) do {:ok, ast} -> @@ -113,6 +108,7 @@ defmodule ElixirSense.Core.Metadata do if cursor_env != nil do {meta, cursor_env} else + # IO.puts(prefix <> "|") case NormalizedCode.Fragment.container_cursor_to_quoted(prefix, columns: true, token_metadata: true @@ -129,8 +125,7 @@ defmodule ElixirSense.Core.Metadata do cursor_env else case metadata.closest_env do - {pos, dist, env} -> - dbg({pos, dist}) + {_pos, _dist, env} -> env nil -> @@ -263,30 +258,21 @@ defmodule ElixirSense.Core.Metadata do |> Enum.min(fn -> nil end) end - def add_scope_vars( - %State.Env{} = env, + def find_var( %__MODULE__{vars_info_per_scope_id: vars_info_per_scope_id}, - {line, column}, - predicate \\ fn _ -> true end + variable, + version, + position ) do - scope_vars = vars_info_per_scope_id[env.scope_id] || %{} - env_vars_keys = env.vars |> Enum.map(&{&1.name, &1.version}) - - scope_vars_missing_in_env = - scope_vars - |> Enum.filter(fn {key, var} -> - key not in env_vars_keys and Enum.min(var.positions) <= {line, column} and - predicate.(var) + vars_info_per_scope_id + |> Enum.find_value(fn {_scope_id, vars} -> + vars + |> Enum.find_value(fn {{n, v}, info} -> + if n == variable and (v == version or version == :any) and position in info.positions do + info + end end) - |> Enum.map(fn {_, value} -> value end) - - env_vars = - for var <- env.vars do - key = {var.name, var.version} - Map.fetch!(scope_vars, key) - end - - %{env | vars: env_vars ++ scope_vars_missing_in_env} + end) end @spec at_module_body?(State.Env.t()) :: boolean() diff --git a/lib/elixir_sense/providers/completion/suggestion.ex b/lib/elixir_sense/providers/completion/suggestion.ex index 4a37d7de..20aeb2d5 100644 --- a/lib/elixir_sense/providers/completion/suggestion.ex +++ b/lib/elixir_sense/providers/completion/suggestion.ex @@ -122,11 +122,6 @@ defmodule ElixirSense.Providers.Completion.Suggestion do env = Metadata.get_env(metadata, {line, column}) - |> Metadata.add_scope_vars( - metadata, - {line, column}, - &(to_string(&1.name) != hint) - ) # if variable is rebound then in env there are many variables with the same name # find the one defined closest to cursor diff --git a/lib/elixir_sense/providers/definition/locator.ex b/lib/elixir_sense/providers/definition/locator.ex index f0a65938..46613c97 100644 --- a/lib/elixir_sense/providers/definition/locator.ex +++ b/lib/elixir_sense/providers/definition/locator.ex @@ -40,7 +40,6 @@ defmodule ElixirSense.Providers.Definition.Locator do env = Metadata.get_env(metadata, {line, column}) - |> Metadata.add_scope_vars(metadata, {line, column}) find( context, diff --git a/lib/elixir_sense/providers/references/locator.ex b/lib/elixir_sense/providers/references/locator.ex index 7dbf76e8..87a3f381 100644 --- a/lib/elixir_sense/providers/references/locator.ex +++ b/lib/elixir_sense/providers/references/locator.ex @@ -31,7 +31,6 @@ defmodule ElixirSense.Providers.References.Locator do module: module } = Metadata.get_env(metadata, {line, column}) - |> Metadata.add_scope_vars(metadata, {line, column}) # find last env of current module attributes = get_attributes(metadata, module) From 332ed4a41359f5d40bf33f67958faf05ad81137e Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 19 Sep 2024 19:17:29 +0200 Subject: [PATCH 194/235] don't rise on invalid attributes --- lib/elixir_sense/core/compiler.ex | 4 +++- .../metadata_builder/error_recovery_test.exs | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 4aa6d535..16ca8167 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1200,7 +1200,9 @@ defmodule ElixirSense.Core.Compiler do {true, expand_args(args, state, env)} args -> - raise "invalid @ call #{inspect(args)}" + # elixir raises "invalid @ call #{inspect(args)}" + {e_args, state, env} = expand_args(args, state, env) + {true, {[hd(e_args)], state, env}} end inferred_type = diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index 33bca0be..d89c7648 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -2778,5 +2778,24 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert {_, env} = get_cursor_env(code) assert env.module == Abc end + + test "invalid args" do + code = """ + defmodule Abc do + @ + def init(id) do + {:ok, + %Some.Mod{ + id: id, + events: [], + version: __cursor__() + }} + end + \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.module == Abc + end end end From b117caed9303d1d9936674ffdc37182f9248b53d Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 19 Sep 2024 22:13:22 +0200 Subject: [PATCH 195/235] better type inference in map guards --- lib/elixir_sense/core/type_inference/guard.ex | 37 ++++++++++++++-- .../core/type_inference/guard_test.exs | 44 ++++++++++++++++++- 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/lib/elixir_sense/core/type_inference/guard.ex b/lib/elixir_sense/core/type_inference/guard.ex index 9f6c4441..9867045c 100644 --- a/lib/elixir_sense/core/type_inference/guard.ex +++ b/lib/elixir_sense/core/type_inference/guard.ex @@ -77,11 +77,19 @@ defmodule ElixirSense.Core.TypeInference.Guard do end) end + # {{:., _, [target, key]}, _, []} + def type_information_from_guards({{:., _, [target, key]}, _, []}) when is_atom(key) do + case extract_var_type(target, {:map, [{key, {:atom, true}}], []}) do + nil -> %{} + {var, type} -> %{var => type} + end + end + # Standalone variable: func my_func(x) when x def type_information_from_guards({var, meta, context}) when is_atom(var) and is_atom(context) do case Keyword.fetch(meta, :version) do {:ok, version} -> - %{{var, version} => :boolean} + %{{var, version} => {:atom, true}} _ -> %{} @@ -94,8 +102,7 @@ defmodule ElixirSense.Core.TypeInference.Guard do {{:., _dot_meta, [:erlang, fun]}, _call_meta, params}, acc when is_atom(fun) and is_list(params) -> with {type, binding} <- guard_predicate_type(fun, params), - {var, meta, context} when is_atom(var) and is_atom(context) <- binding, - {:ok, version} <- Keyword.fetch(meta, :version) do + {{var, version}, type} <- extract_var_type(binding, type) do # If we found the predicate type, we can prematurely exit traversing the subtree {nil, Map.put(acc, {var, version}, type)} else @@ -115,6 +122,22 @@ defmodule ElixirSense.Core.TypeInference.Guard do acc end + defp extract_var_type({var, meta, context}, type) when is_atom(var) and is_atom(context) do + case Keyword.fetch(meta, :version) do + {:ok, version} -> + {{var, version}, type} + + _ -> + nil + end + end + + defp extract_var_type({{:., _, [target, key]}, _, []}, type) when is_atom(key) do + extract_var_type(target, {:map, [{key, type}], []}) + end + + defp extract_var_type(_, _), do: nil + # TODO div and rem only work on first arg defp guard_predicate_type(p, [first | _]) when p in [ @@ -221,11 +244,19 @@ defmodule ElixirSense.Core.TypeInference.Guard do {type_of(value), lhs} end + defp guard_predicate_type(p, [{{:., _, _}, _, []} = lhs, value]) when p in [:==, :===] do + {type_of(value), lhs} + end + defp guard_predicate_type(p, [value, {variable, _, context} = rhs]) when p in [:==, :===] and is_atom(variable) and is_atom(context) do guard_predicate_type(p, [rhs, value]) end + defp guard_predicate_type(p, [value, {{:., _, _}, _, []} = rhs]) when p in [:==, :===] do + guard_predicate_type(p, [rhs, value]) + end + defp guard_predicate_type(:is_map, [first | _]), do: {{:map, [], nil}, first} defp guard_predicate_type(:is_non_struct_map, [first | _]), do: {{:map, [], nil}, first} defp guard_predicate_type(:map_size, [first | _]), do: {{:map, [], nil}, first} diff --git a/test/elixir_sense/core/type_inference/guard_test.exs b/test/elixir_sense/core/type_inference/guard_test.exs index 2aaefd01..a89535ea 100644 --- a/test/elixir_sense/core/type_inference/guard_test.exs +++ b/test/elixir_sense/core/type_inference/guard_test.exs @@ -60,7 +60,7 @@ defmodule ElixirSense.Core.TypeInference.GuardTest do test "infers type from naked var" do guard_expr = quote(do: x) |> expand() result = Guard.type_information_from_guards(guard_expr) - assert result == %{{:x, 0} => :boolean} + assert result == %{{:x, 0} => {:atom, true}} end # 1. Simple guards @@ -270,7 +270,7 @@ defmodule ElixirSense.Core.TypeInference.GuardTest do guard = quote(do: is_number(x) or is_atom(x) or (is_nil(x) or x)) |> expand() result = Guard.type_information_from_guards(guard) - assert result == %{{:x, 0} => {:union, [:number, :atom, {:atom, nil}, :boolean]}} + assert result == %{{:x, 0} => {:union, [:number, :atom, {:atom, nil}, {:atom, true}]}} end test "handles nested when" do @@ -280,4 +280,44 @@ defmodule ElixirSense.Core.TypeInference.GuardTest do assert result == %{{:x, 0} => {:union, [:number, :binary]}} end end + + describe "guard on map field" do + test "naked" do + guard = quote(do: x.foo) |> expand() + + result = Guard.type_information_from_guards(guard) + assert result == %{{:x, 0} => {:map, [{:foo, {:atom, true}}], []}} + end + + test "naked nested" do + guard = quote(do: x.foo.bar) |> expand() + + result = Guard.type_information_from_guards(guard) + assert result == %{{:x, 0} => {:map, [{:foo, {:map, [{:bar, {:atom, true}}], []}}], []}} + end + + test "simple" do + guard = quote(do: is_atom(x.foo)) |> expand() + + result = Guard.type_information_from_guards(guard) + assert result == %{{:x, 0} => {:map, [{:foo, :atom}], []}} + end + + test "nested" do + guard = quote(do: is_atom(x.foo.bar.baz)) |> expand() + + result = Guard.type_information_from_guards(guard) + + assert result == %{ + {:x, 0} => {:map, [{:foo, {:map, [{:bar, {:map, [{:baz, :atom}], []}}], []}}], []} + } + end + + test "with operator" do + guard = quote(do: x.foo == 1) |> expand() + + result = Guard.type_information_from_guards(guard) + assert result == %{{:x, 0} => {:map, [{:foo, {:integer, 1}}], []}} + end + end end From 8d76dc2f4336b4aa6604e1a35b5ef565ebe97ea5 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 20 Sep 2024 21:21:15 +0200 Subject: [PATCH 196/235] a few tests fixed --- .../metadata_builder/error_recovery_test.exs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index d89c7648..a2b6e562 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -2620,7 +2620,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) - assert env.function == {:foo, 1} + assert env.function == {:__unknown__, 0} end test "in def after," do @@ -2665,7 +2665,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert env.context == :guard end - test "in def guard variale" do + test "in def guard variable" do code = """ defmodule Abc do def foo(some) when some\ @@ -2697,7 +2697,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) - assert env.module == Abc + assert env.module == nil end test "in defmodule alias" do @@ -2705,13 +2705,22 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do defmodule A\ """ + assert {_, env} = get_cursor_env(code) + assert env.module == nil + end + + test "in defmodule after do:" do + code = """ + defmodule Abc, do: \ + """ + assert {_, env} = get_cursor_env(code) assert env.module == Abc end test "in defmodule after do" do code = """ - defmodule Abc, do: \ + defmodule Abc do\ """ assert {_, env} = get_cursor_env(code) @@ -2765,7 +2774,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) - assert env.module == Abc + assert env.module == nil end test "setting inside def" do From 58becdfc7deb3225580c2f556484c140fe79302b Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 21 Sep 2024 21:58:55 +0200 Subject: [PATCH 197/235] fix parser tests --- test/elixir_sense/core/parser_test.exs | 258 +++++++++++-------------- 1 file changed, 110 insertions(+), 148 deletions(-) diff --git a/test/elixir_sense/core/parser_test.exs b/test/elixir_sense/core/parser_test.exs index 1c0de7e5..5381a743 100644 --- a/test/elixir_sense/core/parser_test.exs +++ b/test/elixir_sense/core/parser_test.exs @@ -2,8 +2,12 @@ defmodule ElixirSense.Core.ParserTest do use ExUnit.Case, async: true import ExUnit.CaptureIO - import ElixirSense.Core.Parser - alias ElixirSense.Core.{Metadata, State.Env, State.VarInfo} + alias ElixirSense.Core.{Metadata, State.Env, State.VarInfo, State.CallInfo, Parser} + + defp parse(source, cursor) do + metadata = Parser.parse_string(source, true, false, cursor) + {metadata, Metadata.get_cursor_env(metadata, cursor)} + end test "parse_string creates a Metadata struct" do source = """ @@ -13,12 +17,11 @@ defmodule ElixirSense.Core.ParserTest do end """ - assert %Metadata{ - error: nil, - mods_funs_to_positions: %{{MyModule, nil, nil} => %{positions: [{1, 1}]}}, - cursor_env: {_, %Env{functions: functions}}, - source: "defmodule MyModule" <> _ - } = parse_string(source, true, true, {3, 3}) + assert {%Metadata{ + error: nil, + mods_funs_to_positions: %{{MyModule, nil, nil} => %{positions: [{1, 1}]}}, + source: "defmodule MyModule" <> _ + }, %Env{functions: functions}} = parse(source, {3, 3}) assert Keyword.has_key?(functions, List) end @@ -31,10 +34,9 @@ defmodule ElixirSense.Core.ParserTest do end """ - assert %Metadata{ - error: {:error, :parse_error}, - cursor_env: {_, %Env{functions: functions3, module: MyModule}} - } = parse_string(source, true, true, {3, 10}) + assert {%Metadata{ + error: {:error, :parse_error} + }, %Env{functions: functions3, module: MyModule}} = parse(source, {3, 10}) assert Keyword.has_key?(functions3, List) end @@ -47,10 +49,9 @@ defmodule ElixirSense.Core.ParserTest do end """ - assert %Metadata{ - error: {:error, :parse_error}, - cursor_env: {_, %Env{functions: functions}} - } = parse_string(source, true, true, {3, 20}) + assert {%Metadata{ + error: {:error, :parse_error} + }, %Env{functions: functions}} = parse(source, {3, 20}) assert Keyword.has_key?(functions, List) end @@ -63,10 +64,9 @@ defmodule ElixirSense.Core.ParserTest do end """ - assert %Metadata{ - error: {:error, :parse_error}, - closest_env: {_, _, %Env{functions: functions}} - } = parse_string(source, true, true, {3, 8}) + assert {%Metadata{ + error: {:error, :parse_error} + }, %Env{functions: functions}} = parse(source, {3, 8}) assert Keyword.has_key?(functions, List) end @@ -79,10 +79,9 @@ defmodule ElixirSense.Core.ParserTest do end """ - assert %Metadata{ - error: {:error, :parse_error}, - closest_env: {_, _, %Env{functions: functions}} - } = parse_string(source, true, true, {3, 11}) + assert {%Metadata{ + error: {:error, :parse_error} + }, %Env{functions: functions}} = parse(source, {3, 11}) assert Keyword.has_key?(functions, List) end @@ -95,10 +94,9 @@ defmodule ElixirSense.Core.ParserTest do end """ - assert %Metadata{ - error: {:error, :parse_error}, - closest_env: {_, _, %Env{functions: functions}} - } = parse_string(source, true, true, {3, 12}) + assert {%Metadata{ + error: {:error, :parse_error} + }, %Env{functions: functions}} = parse(source, {3, 12}) assert Keyword.has_key?(functions, List) end @@ -111,13 +109,9 @@ defmodule ElixirSense.Core.ParserTest do end """ - assert %Metadata{ - error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{}, - 3 => %Env{functions: functions} - } - } = parse_string(source, true, true, {3, 10}) + assert {%Metadata{ + error: {:error, :parse_error} + }, %Env{functions: functions}} = parse(source, {3, 10}) assert Keyword.has_key?(functions, List) end @@ -130,13 +124,9 @@ defmodule ElixirSense.Core.ParserTest do end """ - assert %Metadata{ - error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{}, - 3 => %Env{functions: functions} - } - } = parse_string(source, true, true, {3, 10}) + assert {%Metadata{ + error: {:error, :parse_error} + }, %Env{functions: functions}} = parse(source, {3, 10}) assert Keyword.has_key?(functions, List) end @@ -149,10 +139,9 @@ defmodule ElixirSense.Core.ParserTest do end """ - assert %Metadata{ - error: {:error, :parse_error}, - closest_env: {_, _, %Env{functions: functions}} - } = parse_string(source, true, true, {3, 12}) + assert {%Metadata{ + error: {:error, :parse_error} + }, %Env{functions: functions}} = parse(source, {3, 12}) assert Keyword.has_key?(functions, List) end @@ -165,10 +154,9 @@ defmodule ElixirSense.Core.ParserTest do end """ - assert %Metadata{ - error: {:error, :parse_error}, - closest_env: {_, _, %Env{functions: functions}} - } = parse_string(source, true, true, {3, 12}) + assert {%Metadata{ + error: {:error, :parse_error} + }, %Env{functions: functions}} = parse(source, {3, 12}) assert Keyword.has_key?(functions, List) end @@ -181,10 +169,9 @@ defmodule ElixirSense.Core.ParserTest do end """ - assert %Metadata{ - error: {:error, :parse_error}, - closest_env: {_, _, %Env{functions: functions}} - } = parse_string(source, true, true, {3, 12}) + assert {%Metadata{ + error: {:error, :parse_error} + }, %Env{functions: functions}} = parse(source, {3, 12}) assert Keyword.has_key?(functions, List) end @@ -197,10 +184,9 @@ defmodule ElixirSense.Core.ParserTest do end """ - assert %Metadata{ - error: {:error, :parse_error}, - cursor_env: {_, %Env{functions: functions}} - } = parse_string(source, true, true, {3, 14}) + assert {%Metadata{ + error: {:error, :parse_error} + }, %Env{functions: functions}} = parse(source, {3, 14}) assert Keyword.has_key?(functions, List) end @@ -215,23 +201,13 @@ defmodule ElixirSense.Core.ParserTest do end """ - # assert capture_io(:stderr, fn -> - result = parse_string(source, true, true, {3, 23}) - # send(self(), {:result, result}) - # end) =~ "an expression is always required on the right side of ->" - - # assert_received {:result, result} - - assert %Metadata{ - error: {:error, :parse_error}, - closest_env: - {_, _, - %Env{ - vars: [ - %VarInfo{name: :x} - ] - }} - } = result + {_metadata, env} = parse(source, {3, 23}) + + assert %Env{ + vars: [ + %VarInfo{name: :x} + ] + } = env end test "parse_string with missing terminator \"end\" attempts to insert `end` at correct indentation" do @@ -240,10 +216,9 @@ defmodule ElixirSense.Core.ParserTest do """ - assert %Metadata{ - error: {:error, :parse_error}, - cursor_env: {_, %Env{module: MyModule}} - } = parse_string(source, true, true, {2, 3}) + assert {%Metadata{ + error: {:error, :parse_error} + }, %Env{module: MyModule}} = parse(source, {2, 3}) source = """ defmodule MyModule do @@ -252,40 +227,29 @@ defmodule ElixirSense.Core.ParserTest do end """ - assert %Metadata{ - error: {:error, :parse_error}, - closest_env: {_, _, %Env{module: MyModule}} - } = parse_string(source, true, true, {3, 1}) + assert {%Metadata{ + error: {:error, :parse_error} + }, %Env{module: MyModule}} = parse(source, {3, 1}) - assert %Metadata{ - error: {:error, :parse_error}, - cursor_env: {_, %Env{module: MyModule}} - } = parse_string(source, true, true, {2, 1}) + assert {%Metadata{ + error: {:error, :parse_error} + }, %Env{module: MyModule}} = parse(source, {2, 1}) source = """ defmodule MyModule do defmodule MyModule1 do + end """ - assert %Metadata{ - error: {:error, :parse_error}, - cursor_env: {_, %Env{module: MyModule}}, - lines_to_env: %{ - 1 => %Env{module: MyModule}, - 3 => %Env{module: MyModule.MyModule1} - } - } = parse_string(source, true, true, {2, 1}) - - assert %Metadata{ - error: {:error, :parse_error}, - closest_env: {_, _, %Env{module: MyModule}}, - lines_to_env: %{ - 1 => %Env{module: MyModule}, - 3 => %Env{module: MyModule.MyModule1} - } - } = parse_string(source, true, true, {3, 1}) + assert {%Metadata{ + error: {:error, :parse_error} + }, %Env{module: MyModule}} = parse(source, {2, 1}) + + assert {%Metadata{ + error: {:error, :parse_error} + }, %Env{module: MyModule.MyModule1}} = parse(source, {4, 5}) end test "parse_string with incomplete key for multiline keyword as argument" do @@ -299,10 +263,14 @@ defmodule ElixirSense.Core.ParserTest do end """ - capture_io(:stderr, fn -> - assert %Metadata{error: {:error, :parse_error}, cursor_env: {_, _}} = - parse_string(source, true, true, {5, 10}) - end) + assert {%Metadata{ + error: {:error, :parse_error}, + calls: %{ + 2 => [%CallInfo{func: :inspect}] + } + }, + %Env{module: MyModule}} = + parse(source, {5, 10}) end test "parse_string with missing value for multiline keyword as argument" do @@ -316,8 +284,14 @@ defmodule ElixirSense.Core.ParserTest do end """ - assert %Metadata{error: {:error, :parse_error}, cursor_env: {_, _}} = - parse_string(source, true, true, {5, 12}) + assert {%Metadata{ + error: {:error, :parse_error}, + calls: %{ + 2 => [%CallInfo{func: :inspect}] + } + }, + %Env{module: MyModule}} = + parse(source, {5, 12}) end @tag capture_log: true @@ -331,12 +305,11 @@ defmodule ElixirSense.Core.ParserTest do end """ - assert %Metadata{ - error: nil, - mods_funs_to_positions: %{{MyModule, nil, nil} => %{positions: [{1, 1}]}}, - cursor_env: {_, %Env{functions: functions}}, - source: "defmodule MyModule" <> _ - } = parse_string(source, true, true, {5, 3}) + assert {%Metadata{ + error: nil, + mods_funs_to_positions: %{{MyModule, nil, nil} => %{positions: [{1, 1}]}}, + source: "defmodule MyModule" <> _ + }, %Env{functions: functions}} = parse(source, {5, 3}) assert Keyword.has_key?(functions, List) end @@ -346,12 +319,9 @@ defmodule ElixirSense.Core.ParserTest do defmodule MyModule, do """ - assert %ElixirSense.Core.Metadata{ - error: {:error, :parse_error}, - lines_to_env: %{ - 1 => %Env{module: MyModule} - } - } = parse_string(source, true, true, {1, 23}) + assert {%ElixirSense.Core.Metadata{ + error: {:error, :parse_error} + }, %Env{module: MyModule}} = parse(source, {1, 23}) end test "parse_string with literal strings" do @@ -366,13 +336,10 @@ defmodule ElixirSense.Core.ParserTest do end ''' - assert %ElixirSense.Core.Metadata{ - cursor_env: - {_, - %ElixirSense.Core.State.Env{ - attributes: [%ElixirSense.Core.State.AttributeInfo{name: :my_attr}] - }} - } = parse_string(source, true, true, {6, 6}) + assert {%ElixirSense.Core.Metadata{}, + %ElixirSense.Core.State.Env{ + attributes: [%ElixirSense.Core.State.AttributeInfo{name: :my_attr}] + }} = parse(source, {6, 6}) end test "parse_string with literal strings in sigils" do @@ -387,15 +354,10 @@ defmodule ElixirSense.Core.ParserTest do end ''' - assert %ElixirSense.Core.Metadata{ - closest_env: { - _, - _, - %ElixirSense.Core.State.Env{ - vars: vars - } - } - } = parse_string(source, true, true, {5, 14}) + assert {%Metadata{}, + %Env{ + vars: vars + }} = parse(source, {5, 14}) assert [ %ElixirSense.Core.State.VarInfo{name: :x}, @@ -414,11 +376,11 @@ defmodule ElixirSense.Core.ParserTest do end """ - assert %ElixirSense.Core.Metadata{ - calls: %{ - 4 => [%{func: :foo}] - } - } = parse_string(source, true, true, {4, 7}) + assert {%ElixirSense.Core.Metadata{ + calls: %{ + 4 => [%CallInfo{func: :foo}] + } + }, %Env{function: {:func, 0}}} = parse(source, {4, 7}) end test "parse struct with missing terminator" do @@ -432,10 +394,10 @@ defmodule ElixirSense.Core.ParserTest do end """ - assert %ElixirSense.Core.Metadata{ - calls: %{ - 4 => [%{func: :foo}] - } - } = parse_string(source, true, true, {4, 8}) + assert {%ElixirSense.Core.Metadata{ + calls: %{ + 4 => [%{func: :foo}] + } + }, %Env{function: {:func, 0}}} = parse(source, {4, 8}) end end From 903f856bcbe9a2af2acb9dc52c270cbfa48d88e5 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 23 Sep 2024 21:48:00 +0200 Subject: [PATCH 198/235] resolve todo --- lib/elixir_sense/core/compiler/typespec.ex | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/elixir_sense/core/compiler/typespec.ex b/lib/elixir_sense/core/compiler/typespec.ex index 0f03a6a3..8b5c99f4 100644 --- a/lib/elixir_sense/core/compiler/typespec.ex +++ b/lib/elixir_sense/core/compiler/typespec.ex @@ -33,12 +33,17 @@ defmodule ElixirSense.Core.Compiler.Typespec do def expand_spec(ast, state, env) do # unless there are unquotes module vars are not accessible - # TODO handle unquotes state_orig = state - {ast, state, env} = do_expand_spec(ast, new_func_vars_scope(state), env) + unless ElixirExpand.Quote.has_unquotes(ast) do + {ast, state, env} = do_expand_spec(ast, new_func_vars_scope(state), env) - {ast, remove_func_vars_scope(state, state_orig), env} + {ast, remove_func_vars_scope(state, state_orig), env} + else + {ast, state, env} = do_expand_spec(ast, new_vars_scope(state), env) + + {ast, remove_vars_scope(state, state_orig), env} + end end defp do_expand_spec({:when, meta, [spec, guard]}, state, env) do @@ -151,12 +156,17 @@ defmodule ElixirSense.Core.Compiler.Typespec do def expand_type(ast, state, env) do # unless there are unquotes module vars are not accessible - # TODO handle unquotes state_orig = state - {ast, state, env} = do_expand_type(ast, new_func_vars_scope(state), env) + unless ElixirExpand.Quote.has_unquotes(ast) do + {ast, state, env} = do_expand_type(ast, new_func_vars_scope(state), env) - {ast, remove_func_vars_scope(state, state_orig), env} + {ast, remove_func_vars_scope(state, state_orig), env} + else + {ast, state, env} = do_expand_type(ast, new_vars_scope(state), env) + + {ast, remove_vars_scope(state, state_orig), env} + end end defp do_expand_type({:"::", meta, [{name, name_meta, args}, definition]}, state, env) do From 6d8c528d1b309758421bd1b2c41451ae354f8ff9 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 23 Sep 2024 21:57:12 +0200 Subject: [PATCH 199/235] resolve todos --- lib/elixir_sense/core/compiler.ex | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 16ca8167..e57411d5 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1095,8 +1095,6 @@ defmodule ElixirSense.Core.Compiler do cursor_after? = state.cursor_env != nil - # TODO elixir does Macro.escape with unquote: true - spec = TypeInfo.typespec_to_string(kind, expr) state = @@ -2136,7 +2134,7 @@ defmodule ElixirSense.Core.Compiler do hidden = Map.get(info.meta, :hidden, false) # def meta is not used anyway so let's pass empty meta = [] - # TODO count 1 hardcoded but that's probably OK + # we hardcode count to 1 count = 1 case hidden do @@ -4682,15 +4680,12 @@ defmodule ElixirSense.Core.Compiler do end defp do_rewrite(:guard, receiver, dot_meta, right, meta, e_args, s) do - :elixir_rewrite.guard_rewrite(receiver, dot_meta, right, meta, e_args, guard_context(s)) + # elixir uses guard context for error messages + :elixir_rewrite.guard_rewrite(receiver, dot_meta, right, meta, e_args, "guard") end defp do_rewrite(_, receiver, dot_meta, right, meta, e_args, _s) do {:ok, :elixir_rewrite.rewrite(receiver, dot_meta, right, meta, e_args)} end - - # TODO probably we can remove it/hardcode, used only for generating error message - defp guard_context(%{prematch: {_, _, {:bitsize, _}}}), do: "bitstring size specifier" - defp guard_context(_), do: "guard" end end From b4bab8dfd48fe748448eed68fcfb89720a08c0aa Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 23 Sep 2024 22:21:22 +0200 Subject: [PATCH 200/235] resolve todos --- .../core/metadata_builder_test.exs | 30 +++++++++++++++---- test/support/example_behaviour.ex | 1 - 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index fab94694..008f213e 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -990,8 +990,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert Map.keys(state.lines_to_env[7].versioned_vars) == [{:k, nil}, {:kv, nil}, {:v, nil}] # TODO we are not tracking usages in typespec - # TODO defdelegate - # TODO defguard 1.18 assert [ %VarInfo{name: :k, positions: [{3, 21}]}, %VarInfo{name: :kv, positions: [{2, 3}, {3, 13}]}, @@ -5674,7 +5672,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do generated: [true], specs: ["@callback without_spec(t(), term()) :: term()"] }, - # TODO there is raw unquote in spec + # there is raw unquote in spec... {Proto, :__protocol__, 1} => %ElixirSense.Core.State.SpecInfo{ kind: :spec, specs: [ @@ -6270,6 +6268,29 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] end + test "registers unknown for defdelegate with unquote fragments in call" do + state = + """ + defmodule MyModuleWithFuns do + kv = [foo: 1, bar: 2] + Enum.each(kv, fn {k, v} -> + defdelegate unquote(k)(), to: Foo + end) + end + """ + |> string_to_state + + assert Map.keys(state.mods_funs_to_positions) == [ + {MyModuleWithFuns, :__info__, 1}, + {MyModuleWithFuns, :__unknown__, 0}, + {MyModuleWithFuns, :module_info, 0}, + {MyModuleWithFuns, :module_info, 1}, + {MyModuleWithFuns, nil, nil} + ] + end + + # TODO test defguard with unquote fragment on 1.18 + test "registers builtin functions for protocols" do state = """ @@ -7948,7 +7969,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } = state.types end - # TODO check if variables are available in unquote test "store types as unknown when unquote fragments in call" do state = """ @@ -9082,8 +9102,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert %ModFunInfo{meta: %{overridable: true}} = state.mods_funs_to_positions[{User, :constant, 0}] end - - # TODO after_compile?, after_verify?, on_defined, on_load? end defp string_to_state(string) do diff --git a/test/support/example_behaviour.ex b/test/support/example_behaviour.ex index e6cb30e0..872ba851 100644 --- a/test/support/example_behaviour.ex +++ b/test/support/example_behaviour.ex @@ -73,7 +73,6 @@ defmodule ElixirSenseExample.ExampleBehaviour do """ end - # TODO: Remove this on Elixir v2.0 @before_compile UseWithCallbacks @doc false From 521bd24938cbc1f3f7b452a9763885832592fca0 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 23 Sep 2024 23:41:03 +0200 Subject: [PATCH 201/235] better support for variable tracking in unquote fragments --- lib/elixir_sense/core/compiler.ex | 42 +++++++++++++++---- lib/elixir_sense/core/compiler/typespec.ex | 26 +++++++++++- .../core/metadata_builder_test.exs | 35 ++++++++++++---- test/support/macro_hygiene.ex | 8 ++++ 4 files changed, 93 insertions(+), 18 deletions(-) create mode 100644 test/support/macro_hygiene.ex diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index e57411d5..8657e327 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -831,12 +831,19 @@ defmodule ElixirSense.Core.Compiler do funs |> List.wrap() |> Enum.reduce(state, fn fun, state -> - fun = + {fun, state} = if __MODULE__.Quote.has_unquotes(fun) do # dynamic defdelegate - replace unquote expression with fake call - {:__unknown__, [], []} + case fun do + {{:unquote, _, unquote_args}, meta, args} -> + {_, state, _} = expand(unquote_args, state, env) + {{:__unknown__, meta, args}, state} + + _ -> + {{:__unknown__, [], []}, state} + end else - fun + {fun, state} end {name, args, as, as_args} = Kernel.Utils.defdelegate_each(fun, opts) @@ -1625,13 +1632,30 @@ defmodule ElixirSense.Core.Compiler do # elixir raises here if def is invalid, we try to continue with unknown # especially, we return unknown for calls with unquote fragments - {name, _meta_1, args} = + {{name, _meta_1, args}, state} = case name_and_args do - {n, m, a} when is_atom(n) and is_atom(a) -> {n, m, []} - {n, m, a} when is_atom(n) and is_list(a) -> {n, m, a} - {_n, m, a} when is_atom(a) -> {:__unknown__, m, []} - {_n, m, a} when is_list(a) -> {:__unknown__, m, a} - _ -> {:__unknown__, [], []} + {n, m, a} when is_atom(n) and is_atom(a) -> + {{n, m, []}, state} + + {n, m, a} when is_atom(n) and is_list(a) -> + {{n, m, a}, state} + + {{:unquote, _, unquote_args}, m, a} when is_atom(a) -> + {_, state, _} = expand(unquote_args, state, env) + {{:__unknown__, m, []}, state} + + {{:unquote, _, unquote_args}, m, a} when is_list(a) -> + {_, state, _} = expand(unquote_args, state, env) + {{:__unknown__, m, a}, state} + + {_n, m, a} when is_atom(a) -> + {{:__unknown__, m, []}, state} + + {_n, m, a} when is_list(a) -> + {{:__unknown__, m, a}, state} + + _ -> + {{:__unknown__, [], []}, state} end arity = length(args) diff --git a/lib/elixir_sense/core/compiler/typespec.ex b/lib/elixir_sense/core/compiler/typespec.ex index 8b5c99f4..16b5715d 100644 --- a/lib/elixir_sense/core/compiler/typespec.ex +++ b/lib/elixir_sense/core/compiler/typespec.ex @@ -128,6 +128,18 @@ defmodule ElixirSense.Core.Compiler.Typespec do defp do_expand_spec(other, guard, guard_meta, state, env) do case other do + {:"::", meta, [{{:unquote, _, unquote_args}, meta1, call_args}, definition]} -> + # replace unquote fragment and try to expand args to find variables + {_, state, env} = ElixirExpand.expand(unquote_args, state, env) + + do_expand_spec( + {:"::", meta, [{:__unknown__, meta1, call_args}, definition]}, + guard, + guard_meta, + state, + env + ) + {name, meta, args} when is_atom(name) and name != :"::" -> # invalid or incomplete spec # try to wrap in :: expression @@ -169,7 +181,8 @@ defmodule ElixirSense.Core.Compiler.Typespec do end end - defp do_expand_type({:"::", meta, [{name, name_meta, args}, definition]}, state, env) do + defp do_expand_type({:"::", meta, [{name, name_meta, args}, definition]}, state, env) + when is_atom(name) and name != :"::" do args = if is_atom(args) do [] @@ -196,6 +209,11 @@ defmodule ElixirSense.Core.Compiler.Typespec do defp do_expand_type(other, state, env) do case other do + {:"::", meta, [{{:unquote, _, unquote_args}, meta1, call_args}, definition]} -> + # replace unquote fragment and try to expand args to find variables + {_, state, env} = ElixirExpand.expand(unquote_args, state, env) + do_expand_type({:"::", meta, [{:__unknown__, meta1, call_args}, definition]}, state, env) + {name, meta, args} when is_atom(name) and name != :"::" -> # invalid or incomplete type # try to wrap in :: expression @@ -514,6 +532,12 @@ defmodule ElixirSense.Core.Compiler.Typespec do {{name, meta, args}, state} end + # handle unquote fragment + defp typespec({:unquote, _, args}, _vars, caller, state) when is_list(args) do + {_, state, _env} = ElixirExpand.expand(args, state, caller) + {:__unknown__, state} + end + defp typespec({name, meta, args}, vars, caller, state) when is_atom(name) do {args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args) # elixir raises if type is not defined diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 008f213e..4d05813a 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -978,6 +978,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do kv = [foo: 1, bar: 2] |> IO.inspect Enum.each(kv, fn {k, v} -> @spec unquote(k)() :: unquote(v) + @type unquote(k)() :: unquote(v) + defdelegate unquote(k)(), to: Foo def unquote(k)() do unquote(v) record_env() @@ -989,12 +991,13 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert Map.keys(state.lines_to_env[7].versioned_vars) == [{:k, nil}, {:kv, nil}, {:v, nil}] - # TODO we are not tracking usages in typespec + # TODO should we handle unquote_slicing in arg list? + # TODO defquard on 1.18 assert [ - %VarInfo{name: :k, positions: [{3, 21}]}, + %VarInfo{name: :k, positions: [{3, 21}, {4, 19}, {5, 19}, {6, 25}, {7, 17}]}, %VarInfo{name: :kv, positions: [{2, 3}, {3, 13}]}, - %VarInfo{name: :v, positions: [{3, 24}, {6, 15}]} - ] = state |> get_line_vars(7) + %VarInfo{name: :v, positions: [{3, 24}, {4, 35}, {5, 35}, {8, 15}]} + ] = state |> get_line_vars(8) end test "in capture" do @@ -1186,6 +1189,22 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] = state |> get_line_vars(3) end + test "variables hygiene" do + state = + """ + defmodule MyModule do + import ElixirSenseExample.Math + def func do + squared(5) + IO.puts "" + end + end + """ + |> string_to_state + + assert [] == state |> get_line_vars(5) + end + test "variables are added to environment" do state = """ @@ -5678,7 +5697,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do specs: [ "@spec __protocol__(:impls) :: :not_consolidated | {:consolidated, list(module())}", "@spec __protocol__(:consolidated?) :: boolean()", - "@spec __protocol__(:functions) :: unquote(Protocol.__functions_spec__(@__functions__()))", + "@spec __protocol__(:functions) :: :__unknown__", "@spec __protocol__(:module) :: Proto" ] }, @@ -6221,7 +6240,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert %{ - {MyModuleWithFuns, :__unknown__, 0} => %ModFunInfo{ + {MyModuleWithFuns, :__unknown__, 1} => %ModFunInfo{ target: {List, :flatten}, type: :defdelegate } @@ -7964,7 +7983,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do positions: [{4, 5}], end_positions: _, generated: [false], - specs: ["@type foo() :: unquote(v())"] + specs: ["@type foo() :: :__unknown__"] } } = state.types end @@ -7985,7 +8004,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do {My, :__unknown__, 0} => %ElixirSense.Core.State.TypeInfo{ name: :__unknown__, args: [[]], - specs: ["@type unquote(k)() :: 123"], + specs: ["@type __unknown__() :: 123"], kind: :type, positions: [{4, 5}], end_positions: [{4, 30}], diff --git a/test/support/macro_hygiene.ex b/test/support/macro_hygiene.ex new file mode 100644 index 00000000..13a5b71b --- /dev/null +++ b/test/support/macro_hygiene.ex @@ -0,0 +1,8 @@ +defmodule ElixirSenseExample.Math do + defmacro squared(x) do + quote do + x = unquote(x) + x * x + end + end +end From c5c0437f6fff9a6c9f77e7211b825ffa4b15ba68 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 26 Sep 2024 22:16:40 +0200 Subject: [PATCH 202/235] handle unquote_slicing in args and correctly expand defaults in defdelegate --- lib/elixir_sense/core/compiler.ex | 83 +++++++++++++++++-- lib/elixir_sense/core/compiler/typespec.ex | 13 +-- .../core/metadata_builder_test.exs | 39 +++++++-- 3 files changed, 118 insertions(+), 17 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 8657e327..7c7aa41e 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -831,24 +831,61 @@ defmodule ElixirSense.Core.Compiler do funs |> List.wrap() |> Enum.reduce(state, fn fun, state -> - {fun, state} = + state_orig = state + + {fun, state, has_unquotes} = if __MODULE__.Quote.has_unquotes(fun) do + state = new_vars_scope(state) # dynamic defdelegate - replace unquote expression with fake call case fun do {{:unquote, _, unquote_args}, meta, args} -> {_, state, _} = expand(unquote_args, state, env) - {{:__unknown__, meta, args}, state} + {{:__unknown__, meta, args}, state, true} _ -> - {{:__unknown__, [], []}, state} + {fun, state, true} end else - {fun, state} + state = new_func_vars_scope(state) + {fun, state, false} end - {name, args, as, as_args} = Kernel.Utils.defdelegate_each(fun, opts) + {name, args, as, as_args} = __MODULE__.Utils.defdelegate_each(fun, opts) arity = length(args) + # no need to reset versioned_vars - we never update it + env_for_expand = %{env | function: {name, arity}} + + # expand defaults and pass args without defaults to expand_args + {args_no_defaults, args, state} = + expand_defaults(args, state, %{env_for_expand | context: nil}, [], []) + + # based on :elixir_clauses.def + {e_args_no_defaults, state, env_for_expand} = + expand_args(args_no_defaults, %{state | prematch: {%{}, 0, :none}}, %{ + env_for_expand + | context: :match + }) + + args = + Enum.zip(args, e_args_no_defaults) + |> Enum.map(fn + {{:"\\\\", meta, [_, expanded_default]}, expanded_arg} -> + {:"\\\\", meta, [expanded_arg, expanded_default]} + + {_, expanded_arg} -> + expanded_arg + end) + + state = + unless has_unquotes do + # restore module vars + remove_func_vars_scope(state, state_orig) + else + # remove scope + remove_vars_scope(state, state_orig) + end + state |> add_current_env_to_line(meta, %{env | context: nil, function: {name, arity}}) |> add_func_to_index( @@ -2558,6 +2595,42 @@ defmodule ElixirSense.Core.Compiler do result end + + def defdelegate_each(fun, opts) when is_list(opts) do + # TODO: Remove on v2.0 + append_first? = Keyword.get(opts, :append_first, false) + + {name, args} = + case fun do + {:when, _, [_left, right]} -> + raise ArgumentError, + "guards are not allowed in defdelegate/2, got: when #{Macro.to_string(right)}" + + _ -> + case Macro.decompose_call(fun) do + {_, _} = pair -> pair + _ -> raise ArgumentError, "invalid syntax in defdelegate #{Macro.to_string(fun)}" + end + end + + as = Keyword.get(opts, :as, name) + as_args = build_as_args(args, append_first?) + + {name, args, as, as_args} + end + + defp build_as_args(args, append_first?) do + as_args = :lists.map(&build_as_arg/1, args) + + case append_first? do + true -> tl(as_args) ++ [hd(as_args)] + false -> as_args + end + end + + # elixir validates arg + defp build_as_arg({:\\, _, [arg, _default_arg]}), do: arg + defp build_as_arg(arg), do: arg end defmodule Clauses do diff --git a/lib/elixir_sense/core/compiler/typespec.ex b/lib/elixir_sense/core/compiler/typespec.ex index 16b5715d..6e48a66a 100644 --- a/lib/elixir_sense/core/compiler/typespec.ex +++ b/lib/elixir_sense/core/compiler/typespec.ex @@ -526,18 +526,19 @@ defmodule ElixirSense.Core.Compiler.Typespec do end end + # handle unquote fragment + defp typespec({key, _, args}, _vars, caller, state) + when is_list(args) and key in [:unquote, :unquote_splicing] do + {_, state, _env} = ElixirExpand.expand(args, state, caller) + {:__unknown__, state} + end + # Handle local calls defp typespec({name, meta, args}, :disabled, caller, state) when is_atom(name) do {args, state} = :lists.mapfoldl(&typespec(&1, :disabled, caller, &2), state, args) {{name, meta, args}, state} end - # handle unquote fragment - defp typespec({:unquote, _, args}, _vars, caller, state) when is_list(args) do - {_, state, _env} = ElixirExpand.expand(args, state, caller) - {:__unknown__, state} - end - defp typespec({name, meta, args}, vars, caller, state) when is_atom(name) do {args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args) # elixir raises if type is not defined diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 4d05813a..2a90604d 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -985,11 +985,19 @@ defmodule ElixirSense.Core.MetadataBuilderTest do record_env() end end) + + keys = [{:foo, [], nil}, {:bar, [], nil}] + @spec foo_splicing(unquote_splicing(keys)) :: :ok + @type foo_splicing(unquote_splicing(keys)) :: :ok + defdelegate foo_splicing(unquote_splicing(keys)), to: Foo + def foo_splicing(unquote_splicing(keys)) do + record_env() + end end """ |> string_to_state - assert Map.keys(state.lines_to_env[7].versioned_vars) == [{:k, nil}, {:kv, nil}, {:v, nil}] + assert Map.keys(state.lines_to_env[9].versioned_vars) == [{:k, nil}, {:kv, nil}, {:v, nil}] # TODO should we handle unquote_slicing in arg list? # TODO defquard on 1.18 @@ -997,7 +1005,19 @@ defmodule ElixirSense.Core.MetadataBuilderTest do %VarInfo{name: :k, positions: [{3, 21}, {4, 19}, {5, 19}, {6, 25}, {7, 17}]}, %VarInfo{name: :kv, positions: [{2, 3}, {3, 13}]}, %VarInfo{name: :v, positions: [{3, 24}, {4, 35}, {5, 35}, {8, 15}]} - ] = state |> get_line_vars(8) + ] = state |> get_line_vars(9) + + assert Map.keys(state.lines_to_env[18].versioned_vars) == [keys: nil, kv: nil] + + # TODO should we handle unquote_slicing in arg list? + # TODO defquard on 1.18 + assert [ + %VarInfo{ + name: :keys, + positions: [{13, 3}, {14, 39}, {15, 39}, {16, 45}, {17, 37}] + }, + %VarInfo{name: :kv, positions: [{2, 3}, {3, 13}]} + ] = state |> get_line_vars(18) end test "in capture" do @@ -6197,34 +6217,41 @@ defmodule ElixirSense.Core.MetadataBuilderTest do defdelegate func_delegated_erlang(par), to: :erlang_module defdelegate func_delegated_as(par), to: __MODULE__.Sub, as: :my_func defdelegate func_delegated_alias(par), to: E + defdelegate func_delegated_defaults(par \\\\ 123), to: E end """ |> string_to_state assert %{ {MyModuleWithFuns, :func_delegated, 1} => %ModFunInfo{ - params: [[{:par, [line: 3, column: 30], nil}]], + params: [[{:par, _, nil}]], positions: [{3, 3}], target: {OtherModule, :func_delegated}, type: :defdelegate }, {MyModuleWithFuns, :func_delegated_alias, 1} => %ModFunInfo{ - params: [[{:par, [line: 6, column: 36], nil}]], + params: [[{:par, _, nil}]], positions: [{6, 3}], target: {Enum, :func_delegated_alias}, type: :defdelegate }, {MyModuleWithFuns, :func_delegated_as, 1} => %ModFunInfo{ - params: [[{:par, [line: 5, column: 33], nil}]], + params: [[{:par, _, nil}]], positions: [{5, 3}], target: {MyModuleWithFuns.Sub, :my_func}, type: :defdelegate }, {MyModuleWithFuns, :func_delegated_erlang, 1} => %ModFunInfo{ - params: [[{:par, [line: 4, column: 37], nil}]], + params: [[{:par, _, nil}]], positions: [{4, 3}], target: {:erlang_module, :func_delegated_erlang}, type: :defdelegate + }, + {MyModuleWithFuns, :func_delegated_defaults, 1} => %ModFunInfo{ + params: [[{:\\, _, [{:par, _, nil}, 123]}]], + positions: [{7, 3}], + target: {Enum, :func_delegated_defaults}, + type: :defdelegate } } = state.mods_funs_to_positions end From 6aca6560022f22d2ab0dc4de0724387b035f040f Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 26 Sep 2024 23:42:08 +0200 Subject: [PATCH 203/235] do not infer special forms as local calls --- lib/elixir_sense/core/type_inference.ex | 48 ++++++++++++++++ .../elixir_sense/core/type_inference_test.exs | 55 +++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/lib/elixir_sense/core/type_inference.ex b/lib/elixir_sense/core/type_inference.ex index 26a02804..e31a1fbc 100644 --- a/lib/elixir_sense/core/type_inference.ex +++ b/lib/elixir_sense/core/type_inference.ex @@ -167,6 +167,54 @@ defmodule ElixirSense.Core.TypeInference do {:list, list |> Enum.map(&type_of(&1, context))} end + # block expressions + def type_of({:__block__, _meta, exprs}, context) do + case List.last(exprs) do + nil -> nil + last_expr -> type_of(last_expr, context) + end + end + + # anonymous functions + def type_of({:fn, _meta, _clauses}, _context), do: nil + + # special forms + # for case/cond/with/receive/for/try we have no idea what the type is going to be + # we don't support binaries + # TODO guard? + # other are not worth handling + def type_of({form, _meta, _clauses}, _context) + when form in [ + :case, + :cond, + :try, + :receive, + :for, + :with, + :quote, + :unquote, + :unquote_splicing, + :import, + :alias, + :require, + :__aliases__, + :__cursor__, + :__DIR__, + :super, + :<<>>, + :"::" + ], + do: nil + + # __ENV__ is already expanded to map + def type_of({form, _meta, _clauses}, _context) when form in [:__CALLER__] do + {:struct, [], {:atom, Macro.Env}, nil} + end + + def type_of({:__STACKTRACE__, _meta, _clauses}, _context) do + {:list, nil} + end + # local call def type_of({var, _, args}, context) when is_atom(var) and is_list(args) do {:local_call, var, Enum.map(args, &type_of(&1, context))} diff --git a/test/elixir_sense/core/type_inference_test.exs b/test/elixir_sense/core/type_inference_test.exs index 5059395c..ca63badb 100644 --- a/test/elixir_sense/core/type_inference_test.exs +++ b/test/elixir_sense/core/type_inference_test.exs @@ -488,5 +488,60 @@ defmodule ElixirSense.Core.TypeInferenceTest do assert type_of("\"asd\"") == nil assert type_of("1.23") == nil end + + test "__STACKTRACE__ returns {:list, nil}" do + assert type_of("__STACKTRACE__") == {:list, nil} + end + + test "anonymous function returns nil" do + assert type_of("fn -> a end") == nil + assert type_of("fn x -> x + 1 end") == nil + assert type_of("fn x, y -> x * y end") == nil + end + end + + describe "block expressions" do + test "non-empty block returns type of last expression" do + assert type_of("(a = 1; b = 2; c = 3)") == {:integer, 3} + + assert type_of(""" + ( + a = 1 + b = 2 + c = 3 + ) + """) == {:integer, 3} + end + + test "empty block returns nil" do + assert type_of("( )") == nil + end + + test "__CALLER__ returns {:struct, [], {:atom, Macro.Env}, nil}" do + assert type_of("__CALLER__") == {:struct, [], {:atom, Macro.Env}, nil} + end + end + + describe "special forms" do + special_forms = [ + "case a do\n :ok -> 1\n :error -> 2\nend", + "cond do\n a -> 1\n b -> 2\nend", + "try do\n risky_operation()\nrescue\n e -> handle(e)\nend", + "receive do\n {:msg, msg} -> process(msg)\nend", + "for x <- list, do: x * 2", + "with {:ok, a} <- fetch_a(), {:ok, b} <- fetch_b(a), do: a + b", + "quote do: a + b", + "unquote(expr)", + "unquote_splicing(expr)", + "import Module", + "alias Module.SubModule", + "require Module" + ] + + for form <- special_forms do + test "special form: #{inspect(form)} returns nil" do + assert type_of(unquote(form)) == nil + end + end end end From 35b80caf6f7d556cf74eaa2920f576138fdd85c9 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 27 Sep 2024 21:55:11 +0200 Subject: [PATCH 204/235] address todos --- lib/elixir_sense/core/compiler.ex | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 7c7aa41e..90ee028e 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -3009,9 +3009,9 @@ defmodule ElixirSense.Core.Compiler do {ret, %{se | stacktrace: old_stacktrace}} end - defp expand_catch(meta, [{:when, when_meta, [a1, a2, a3, _ | _]}], s, e) do + defp expand_catch(meta, [{:when, when_meta, [a1, a2, a3, dh | dt]}], s, e) do # elixir raises here wrong_number_of_args_for_clause - # TODO expand dropped + {_, s, _} = ElixirExpand.expand([dh | dt], s, e) expand_catch(meta, [{:when, when_meta, [a1, a2, a3]}], s, e) end @@ -3025,10 +3025,10 @@ defmodule ElixirSense.Core.Compiler do head(args, s, e) end - defp expand_catch(meta, [a1, a2 | _], s, e) do + defp expand_catch(meta, [a1, a2 | d], s, e) do # attempt to recover from error by taking 2 first args # elixir raises here wrong_number_of_args_for_clause - # TODO expand dropped + {_, s, _} = ElixirExpand.expand(d, s, e) expand_catch(meta, [a1, a2], s, e) end @@ -3038,10 +3038,10 @@ defmodule ElixirSense.Core.Compiler do {[e_arg], sa, ea} end - defp expand_rescue(meta, [a1 | _], s, e) do + defp expand_rescue(meta, [a1 | d], s, e) do # try to recover from error by taking first argument only # elixir raises here wrong_number_of_args_for_clause - # TODO expand dropped + {_, s, _} = ElixirExpand.expand(d, s, e) expand_rescue(meta, [a1], s, e) end From 1bd3715877513b6d351a27d36a86abb20470decf Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 27 Sep 2024 22:18:04 +0200 Subject: [PATCH 205/235] address todos --- lib/elixir_sense/core/compiler.ex | 12 ++++++++++-- .../core/metadata_builder/error_recovery_test.exs | 9 +++++++++ test/elixir_sense/core/metadata_builder_test.exs | 4 +--- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 90ee028e..1beb5299 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1454,7 +1454,6 @@ defmodule ElixirSense.Core.Compiler do raise ArgumentError, "defimpl/3 expects a :for option when declared outside a module" end) - # TODO how to look for cursor in for? for = __MODULE__.Macro.expand_literals(for, %{ env @@ -1462,6 +1461,14 @@ defmodule ElixirSense.Core.Compiler do function: {:__impl__, 1} }) + {for, state} = + if is_atom(for) do + {for, state} + else + {_, state, _} = expand(for, state, env) + {:"Elixir.__UNKNOWN__", state} + end + {protocol, state, _env} = expand(name, state, env) impl = fn protocol, for, block, state, env -> @@ -1481,7 +1488,8 @@ defmodule ElixirSense.Core.Compiler do block = case opts do [] -> - raise ArgumentError, "defimpl expects a do-end block" + # elixir raises here + nil [do: block] -> block diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index a2b6e562..fe485472 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -2807,4 +2807,13 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert env.module == Abc end end + + test "defimpl for" do + code = """ + defimpl Enumerable, for: \ + """ + + assert {_, env} = get_cursor_env(code) + assert env.module == nil + end end diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 2a90604d..4dbcdec2 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -999,7 +999,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert Map.keys(state.lines_to_env[9].versioned_vars) == [{:k, nil}, {:kv, nil}, {:v, nil}] - # TODO should we handle unquote_slicing in arg list? # TODO defquard on 1.18 assert [ %VarInfo{name: :k, positions: [{3, 21}, {4, 19}, {5, 19}, {6, 25}, {7, 17}]}, @@ -1009,7 +1008,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert Map.keys(state.lines_to_env[18].versioned_vars) == [keys: nil, kv: nil] - # TODO should we handle unquote_slicing in arg list? # TODO defquard on 1.18 assert [ %VarInfo{ @@ -8034,7 +8032,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do specs: ["@type __unknown__() :: 123"], kind: :type, positions: [{4, 5}], - end_positions: [{4, 30}], + end_positions: [_], generated: [false], doc: "", meta: %{hidden: true} From ae2654b3fc64e8473773edf5171c0365fcd7e62c Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 27 Sep 2024 22:40:23 +0200 Subject: [PATCH 206/235] fix tests on < 1.16 --- lib/elixir_sense/core/compiler.ex | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 1beb5299..c9d9aa5e 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1943,8 +1943,16 @@ defmodule ElixirSense.Core.Compiler do # defmodule automatically defines aliases, we need to mirror this feature here. # defmodule Elixir.Alias - defp alias_defmodule({:__aliases__, _, [:"Elixir", _ | _]}, module, env), - do: {module, env} + if Version.match?(System.version(), "< 1.16.0-dev") do + # see https://github.com/elixir-lang/elixir/pull/12451#issuecomment-1461393633 + defp alias_defmodule({:__aliases__, meta, [:"Elixir", t] = x}, module, env) do + alias = String.to_atom("Elixir." <> Atom.to_string(t)) + {:ok, env} = NormalizedMacroEnv.define_alias(env, meta, alias, as: alias, trace: false) + {module, env} + end + end + + defp alias_defmodule({:__aliases__, _, [:"Elixir", _ | _] = x}, module, env), do: {module, env} # defmodule Alias in root defp alias_defmodule({:__aliases__, _, _}, module, %{module: nil} = env), From a235a24ad0ab0d1d2090ddfaefb2523422bcd96b Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 27 Sep 2024 22:50:48 +0200 Subject: [PATCH 207/235] exclude more tests on < 1.16 --- test/elixir_sense/core/binding_test.exs | 12 +- test/elixir_sense/core/compiler_test.exs | 138 ++++++++++-------- .../metadata_builder/error_recovery_test.exs | 16 +- .../core/metadata_builder_test.exs | 2 +- 4 files changed, 92 insertions(+), 76 deletions(-) diff --git a/test/elixir_sense/core/binding_test.exs b/test/elixir_sense/core/binding_test.exs index 8e20fce4..0d27e5c0 100644 --- a/test/elixir_sense/core/binding_test.exs +++ b/test/elixir_sense/core/binding_test.exs @@ -122,19 +122,11 @@ defmodule ElixirSense.Core.BindingTest do assert { :struct, [ - {:__struct__, {:atom, URI}}, - {:port, nil}, - {:scheme, nil}, - {:path, nil}, - {:host, nil}, - {:userinfo, nil}, - {:fragment, nil}, - {:query, nil}, - {:authority, nil} + {:__struct__, {:atom, URI}} | _ ], {:atom, URI}, nil - } == + } = Binding.expand( @env, { diff --git a/test/elixir_sense/core/compiler_test.exs b/test/elixir_sense/core/compiler_test.exs index b6adafe8..89f2648e 100644 --- a/test/elixir_sense/core/compiler_test.exs +++ b/test/elixir_sense/core/compiler_test.exs @@ -298,28 +298,34 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do assert_expansion("__ENV__.foo") end - test "expands quote literal" do - assert_expansion("quote do: 2") - assert_expansion("quote do: :foo") - assert_expansion("quote do: \"asd\"") - assert_expansion("quote do: []") - assert_expansion("quote do: [12]") - assert_expansion("quote do: [12, 34]") - assert_expansion("quote do: [12 | 34]") - assert_expansion("quote do: [12 | [34]]") - assert_expansion("quote do: {12}") - assert_expansion("quote do: {12, 34}") - assert_expansion("quote do: %{a: 12}") + if Version.match?(System.version(), ">= 1.16.0") do + test "expands quote literal" do + assert_expansion("quote do: 2") + assert_expansion("quote do: :foo") + assert_expansion("quote do: \"asd\"") + assert_expansion("quote do: []") + assert_expansion("quote do: [12]") + assert_expansion("quote do: [12, 34]") + assert_expansion("quote do: [12 | 34]") + assert_expansion("quote do: [12 | [34]]") + assert_expansion("quote do: {12}") + assert_expansion("quote do: {12, 34}") + assert_expansion("quote do: %{a: 12}") + end end - test "expands quote variable" do - assert_expansion("quote do: abc") + if Version.match?(System.version(), ">= 1.16.0") do + test "expands quote variable" do + assert_expansion("quote do: abc") + end end - test "expands quote quote" do - assert_expansion(""" - quote do: (quote do: 1) - """) + if Version.match?(System.version(), ">= 1.16.0") do + test "expands quote quote" do + assert_expansion(""" + quote do: (quote do: 1) + """) + end end test "expands quote block" do @@ -348,36 +354,44 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do """) end - test "expands quote unquote_splicing" do - assert_expansion(""" - a = [1, 2, 3] - quote do: (unquote_splicing(a)) - """) + if Version.match?(System.version(), ">= 1.16.0") do + test "expands quote unquote_splicing" do + assert_expansion(""" + a = [1, 2, 3] + quote do: (unquote_splicing(a)) + """) + end end - test "expands quote unquote_splicing in list" do - assert_expansion(""" - a = [1, 2, 3] - quote do: [unquote_splicing(a) | [1]] - """) + if Version.match?(System.version(), ">= 1.16.0") do + test "expands quote unquote_splicing in list" do + assert_expansion(""" + a = [1, 2, 3] + quote do: [unquote_splicing(a) | [1]] + """) - assert_expansion(""" - a = [1, 2, 3] - quote do: [1 | unquote_splicing(a)] - """) + assert_expansion(""" + a = [1, 2, 3] + quote do: [1 | unquote_splicing(a)] + """) + end end - test "expands quote alias" do - assert_expansion("quote do: Date") - assert_expansion("quote do: Elixir.Date") - assert_expansion("quote do: String.Chars") - assert_expansion("alias String.Chars; quote do: Chars") - assert_expansion("alias String.Chars; quote do: Chars.foo().A") + if Version.match?(System.version(), ">= 1.16.0") do + test "expands quote alias" do + assert_expansion("quote do: Date") + assert_expansion("quote do: Elixir.Date") + assert_expansion("quote do: String.Chars") + assert_expansion("alias String.Chars; quote do: Chars") + assert_expansion("alias String.Chars; quote do: Chars.foo().A") + end end - test "expands quote import" do - assert_expansion("quote do: inspect(1)") - assert_expansion("quote do: &inspect/1") + if Version.match?(System.version(), ">= 1.16.0") do + test "expands quote import" do + assert_expansion("quote do: inspect(1)") + assert_expansion("quote do: &inspect/1") + end end if Version.match?(System.version(), ">= 1.17.0") do @@ -393,12 +407,14 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do end end - test "expands quote with unquote false" do - assert_expansion(""" - quote unquote: false do - unquote("hello") + if Version.match?(System.version(), ">= 1.16.0") do + test "expands quote with unquote false" do + assert_expansion(""" + quote unquote: false do + unquote("hello") + end + """) end - """) end if Version.match?(System.version(), ">= 1.17.0") do @@ -409,22 +425,28 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do end end - test "expands quote with line" do - assert_expansion(""" - quote line: 123, do: bar(1, 2, 3) - """) + if Version.match?(System.version(), ">= 1.16.0") do + test "expands quote with line" do + assert_expansion(""" + quote line: 123, do: bar(1, 2, 3) + """) + end end - test "expands quote with location: keep" do - assert_expansion(""" - quote location: :keep, do: bar(1, 2, 3) - """) + if Version.match?(System.version(), ">= 1.16.0") do + test "expands quote with location: keep" do + assert_expansion(""" + quote location: :keep, do: bar(1, 2, 3) + """) + end end - test "expands quote with context" do - assert_expansion(""" - quote context: Foo, do: abc = 3 - """) + if Version.match?(System.version(), ">= 1.16.0") do + test "expands quote with context" do + assert_expansion(""" + quote context: Foo, do: abc = 3 + """) + end end test "expands &super" do diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index fe485472..d5e8280d 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -2141,14 +2141,16 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert env.typespec == {:foo, 0} end - test "in type after :: type with fun ( nex arg" do - code = """ - defmodule Abc do - @type foo :: (bar, \ - """ + if Version.match?(System.version(), ">= 1.16.0") do + test "in type after :: type with fun ( nex arg" do + code = """ + defmodule Abc do + @type foo :: (bar, \ + """ - assert {_, env} = get_cursor_env(code) - assert env.typespec == {:foo, 0} + assert {_, env} = get_cursor_env(code) + assert env.typespec == {:foo, 0} + end end test "in type after :: type with map empty" do diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 4dbcdec2..011b9065 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -8037,7 +8037,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do doc: "", meta: %{hidden: true} } - } == state.types + } = state.types end test "registers incomplete types" do From c3cd2dc8d2fc9e6a229fb86b7f47afdac9e64c67 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 27 Sep 2024 22:59:42 +0200 Subject: [PATCH 208/235] fix regression and invalid version check --- lib/elixir_sense/core/compiler.ex | 4 ++-- .../core/metadata_builder/error_recovery_test.exs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index c9d9aa5e..6fd3175b 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1462,11 +1462,11 @@ defmodule ElixirSense.Core.Compiler do }) {for, state} = - if is_atom(for) do + if is_atom(for) or (is_list(for) and Enum.all?(for, &is_atom/1)) do {for, state} else {_, state, _} = expand(for, state, env) - {:"Elixir.__UNKNOWN__", state} + {:"Elixir.__Unknown__", state} end {protocol, state, _env} = expand(name, state, env) diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index d5e8280d..7f25a5fb 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -2141,7 +2141,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert env.typespec == {:foo, 0} end - if Version.match?(System.version(), ">= 1.16.0") do + if Version.match?(System.version(), ">= 1.17.0") do test "in type after :: type with fun ( nex arg" do code = """ defmodule Abc do From 65f1798c86709dfc2529a37e2ddc4bc4092e6dc4 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 27 Sep 2024 23:24:06 +0200 Subject: [PATCH 209/235] exclude some tests on < 1.15 --- test/elixir_sense/core/compiler_test.exs | 90 ++++++++++++++---------- 1 file changed, 51 insertions(+), 39 deletions(-) diff --git a/test/elixir_sense/core/compiler_test.exs b/test/elixir_sense/core/compiler_test.exs index 89f2648e..e7503970 100644 --- a/test/elixir_sense/core/compiler_test.exs +++ b/test/elixir_sense/core/compiler_test.exs @@ -76,7 +76,13 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do end defp state_to_map(%State{} = state) do - Map.take(state, [:caller, :prematch, :stacktrace, :unused, :runtime_modules, :vars]) + res = Map.take(state, [:caller, :prematch, :stacktrace, :unused, :runtime_modules, :vars]) + + if Version.match?(System.version(), "< 1.15.0") do + res |> Map.put(:prematch, :warn) + else + res + end end defp expand(ast) do @@ -172,15 +178,17 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do assert_expansion("%x{} = %Date{year: 2024, month: 2, day: 18}") end - test "expands <<>>" do - assert_expansion("<<>>") - assert_expansion("<<1>>") - assert_expansion("<> = \"\"") - end + if Version.match?(System.version(), ">= 1.15.0") do + test "expands <<>>" do + assert_expansion("<<>>") + assert_expansion("<<1>>") + assert_expansion("<> = \"\"") + end - test "expands <<>> with modifier" do - assert_expansion("x = 1; y = 1; <>") - assert_expansion("x = 1; y = 1; <> = <<>>") + test "expands <<>> with modifier" do + assert_expansion("x = 1; y = 1; <>") + assert_expansion("x = 1; y = 1; <> = <<>>") + end end test "expands __block__" do @@ -557,20 +565,22 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do """) end - test "expands for with bitstring generator" do - assert_expansion(""" - for <> do - :ok + if Version.match?(System.version(), ">= 1.15.0") do + test "expands for with bitstring generator" do + assert_expansion(""" + for <> do + :ok + end + """) end - """) - end - test "expands for with reduce" do - assert_expansion(""" - for <>, x in ?a..?z, reduce: %{} do - acc -> acc + test "expands for with reduce" do + assert_expansion(""" + for <>, x in ?a..?z, reduce: %{} do + acc -> acc + end + """) end - """) end test "expands for in block" do @@ -683,29 +693,31 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do assert state_to_map(state) == elixir_ex_to_map(elixir_state) end - test "expands nullary call if_undefined: :warn" do - Code.put_compiler_option(:on_undefined_variable, :warn) - ast = {:self, [], nil} + if Version.match?(System.version(), ">= 1.15.0") do + test "expands nullary call if_undefined: :warn" do + Code.put_compiler_option(:on_undefined_variable, :warn) + ast = {:self, [], nil} - {expanded, state, env} = - Compiler.expand( - ast, - %State{ - prematch: Code.get_compiler_option(:on_undefined_variable) || :warn - }, - Compiler.env() - ) + {expanded, state, env} = + Compiler.expand( + ast, + %State{ + prematch: Code.get_compiler_option(:on_undefined_variable) || :warn + }, + Compiler.env() + ) - elixir_env = :elixir_env.new() + elixir_env = :elixir_env.new() - {elixir_expanded, elixir_state, elixir_env} = - :elixir_expand.expand(ast, :elixir_env.env_to_ex(elixir_env), elixir_env) + {elixir_expanded, elixir_state, elixir_env} = + :elixir_expand.expand(ast, :elixir_env.env_to_ex(elixir_env), elixir_env) - assert expanded == elixir_expanded - assert env == elixir_env - assert state_to_map(state) == elixir_ex_to_map(elixir_state) - after - Code.put_compiler_option(:on_undefined_variable, :raise) + assert expanded == elixir_expanded + assert env == elixir_env + assert state_to_map(state) == elixir_ex_to_map(elixir_state) + after + Code.put_compiler_option(:on_undefined_variable, :raise) + end end test "expands local call" do From fa78f3a09d28187f46e06c99582b4be362d1c9d7 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 27 Sep 2024 23:33:39 +0200 Subject: [PATCH 210/235] fix crash on < 1.14 --- lib/elixir_sense/core/compiler.ex | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 6fd3175b..c0824047 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -4792,9 +4792,15 @@ defmodule ElixirSense.Core.Compiler do :elixir_rewrite.match_rewrite(receiver, dot_meta, right, meta, e_args) end - defp do_rewrite(:guard, receiver, dot_meta, right, meta, e_args, s) do - # elixir uses guard context for error messages - :elixir_rewrite.guard_rewrite(receiver, dot_meta, right, meta, e_args, "guard") + if Version.match?(System.version(), "< 1.14.0") do + defp do_rewrite(:guard, receiver, dot_meta, right, meta, e_args, s) do + :elixir_rewrite.guard_rewrite(receiver, dot_meta, right, meta, e_args) + end + else + defp do_rewrite(:guard, receiver, dot_meta, right, meta, e_args, s) do + # elixir uses guard context for error messages + :elixir_rewrite.guard_rewrite(receiver, dot_meta, right, meta, e_args, "guard") + end end defp do_rewrite(_, receiver, dot_meta, right, meta, e_args, _s) do From 20003df00027db1ea2e189fef20dc7d31559be77 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 27 Sep 2024 23:46:05 +0200 Subject: [PATCH 211/235] exclude more tests --- test/elixir_sense/core/binding_test.exs | 6 +- .../metadata_builder/error_recovery_test.exs | 92 +++++++++++-------- .../core/metadata_builder_test.exs | 6 +- 3 files changed, 59 insertions(+), 45 deletions(-) diff --git a/test/elixir_sense/core/binding_test.exs b/test/elixir_sense/core/binding_test.exs index 0d27e5c0..84cac38a 100644 --- a/test/elixir_sense/core/binding_test.exs +++ b/test/elixir_sense/core/binding_test.exs @@ -165,13 +165,11 @@ defmodule ElixirSense.Core.BindingTest do assert { :struct, [ - {:__struct__, {:atom, ArgumentError}}, - {:message, nil}, - {:__exception__, {:atom, true}} + {:__struct__, {:atom, ArgumentError}} | _ ], {:atom, ArgumentError}, nil - } == + } = Binding.expand( @env, { diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index 7f25a5fb..f68dcf96 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -113,20 +113,24 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert Enum.any?(env.vars, &(&1.name == :x)) end - test "invalid number of args with when" do - code = """ - case nil do 0, z when not is_nil(z) -> \ - """ + if Version.match?(System.version(), "< 1.14.0") do + test "invalid number of args with when" do + code = """ + case nil do 0, z when not is_nil(z) -> \ + """ - assert get_cursor_env(code) + assert get_cursor_env(code) + end end - test "invalid number of args" do - code = """ - case nil do 0, z -> \ - """ + if Version.match?(System.version(), "< 1.14.0") do + test "invalid number of args" do + code = """ + case nil do 0, z -> \ + """ - assert get_cursor_env(code) + assert get_cursor_env(code) + end end end @@ -193,12 +197,14 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert Enum.any?(env.vars, &(&1.name == :x)) end - test "invalid number of args" do - code = """ - cond do 0, z -> \ - """ + if Version.match?(System.version(), "< 1.14.0") do + test "invalid number of args" do + code = """ + cond do 0, z -> \ + """ - assert get_cursor_env(code) + assert get_cursor_env(code) + end end end @@ -310,15 +316,17 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert Enum.any?(env.vars, &(&1.name == :x)) end - test "invalid number of args in after" do - code = """ - receive do - a -> :ok - after - 0, z -> \ - """ + if Version.match?(System.version(), ">= 1.15.0") do + test "invalid number of args in after" do + code = """ + receive do + a -> :ok + after + 0, z -> \ + """ - assert get_cursor_env(code) + assert get_cursor_env(code) + end end test "invalid number of clauses in after" do @@ -481,16 +489,18 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert Enum.any?(env.vars, &(&1.name == :x)) end - test "cursor in right side of catch clause 2 arg" do - code = """ - try do - bar() - catch - x, _ -> \ - """ + if Version.match?(System.version(), "< 1.14.0") do + test "cursor in right side of catch clause 2 arg" do + code = """ + try do + bar() + catch + x, _ -> \ + """ - assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, &(&1.name == :x)) + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + end end test "cursor in left side of else clause" do @@ -806,15 +816,17 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert Enum.any?(env.vars, &(&1.name == :y)) end - test "cursor in do block reduce right side of clause too many args" do - code = """ - for x <- [], reduce: %{} do - y, z -> \ - """ + if Version.match?(System.version(), "< 1.14.0") do + test "cursor in do block reduce right side of clause too many args" do + code = """ + for x <- [], reduce: %{} do + y, z -> \ + """ - assert {meta, env} = get_cursor_env(code) - assert Enum.any?(env.vars, &(&1.name == :x)) - assert Enum.any?(env.vars, &(&1.name == :y)) + assert {meta, env} = get_cursor_env(code) + assert Enum.any?(env.vars, &(&1.name == :x)) + assert Enum.any?(env.vars, &(&1.name == :y)) + end end test "cursor in do block reduce right side of clause too little args" do diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 011b9065..3bbf1c4f 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -8103,9 +8103,13 @@ defmodule ElixirSense.Core.MetadataBuilderTest do kind: :type, name: :t, specs: ["@type t() :: term()"], - doc: "All the types that implement this protocol" <> _ + doc: doc } } = state.types + + if Version.match?(System.version(), ">= 1.14.0") do + assert "All the types that implement this protocol" <> _ = doc + end end test "specs and callbacks" do From f0bad129cc282ab4bebba74d708b7c4f6bf74821 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 28 Sep 2024 13:05:52 +0200 Subject: [PATCH 212/235] exclude and fix tests on < 1.15 --- lib/elixir_sense/core/compiler.ex | 12 +++- lib/elixir_sense/core/normalized/macro/env.ex | 10 +++- test/elixir_sense/core/compiler_test.exs | 53 ++++++++++-------- .../metadata_builder/error_recovery_test.exs | 2 + .../core/metadata_builder_test.exs | 56 ++++++------------- test/elixir_sense/core/parser_test.exs | 13 +++++ 6 files changed, 80 insertions(+), 66 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index c0824047..5e8b5a48 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1747,10 +1747,16 @@ defmodule ElixirSense.Core.Compiler do expanded_arg end) + prematch = if Version.match?(System.version(), ">= 1.15.0-dev") do + Code.get_compiler_option(:on_undefined_variable) + else + :warn + end + {e_guard, state, env_for_expand} = __MODULE__.Clauses.guard( guards, - %{state | prematch: :raise}, + %{state | prematch: prematch}, %{env_for_expand | context: :guard} ) @@ -2521,7 +2527,11 @@ defmodule ElixirSense.Core.Compiler do {e_args, close_write(sa, s), ea} end + if Version.match?(System.version(), ">= 1.15.0-dev") do @internals [{:behaviour_info, 1}, {:module_info, 1}, {:module_info, 0}] + else + @internals [{:module_info, 1}, {:module_info, 0}] + end defp import_info_callback(module, state) do fn kind -> if Map.has_key?(state.mods_funs_to_positions, {module, nil, nil}) do diff --git a/lib/elixir_sense/core/normalized/macro/env.ex b/lib/elixir_sense/core/normalized/macro/env.ex index 595ac852..211b47c5 100644 --- a/lib/elixir_sense/core/normalized/macro/env.ex +++ b/lib/elixir_sense/core/normalized/macro/env.ex @@ -491,8 +491,14 @@ defmodule ElixirSense.Core.Normalized.Macro.Env do end) end - defp remove_internals(set) do - set -- [{:behaviour_info, 1}, {:module_info, 1}, {:module_info, 0}] + if Version.match?(System.version(), ">= 1.15.0-dev") do + defp remove_internals(set) do + set -- [{:behaviour_info, 1}, {:module_info, 1}, {:module_info, 0}] + end + else + defp remove_internals(set) do + set -- [{:module_info, 1}, {:module_info, 0}] + end end defp ensure_keyword_list([]) do diff --git a/test/elixir_sense/core/compiler_test.exs b/test/elixir_sense/core/compiler_test.exs index e7503970..d67e4a9c 100644 --- a/test/elixir_sense/core/compiler_test.exs +++ b/test/elixir_sense/core/compiler_test.exs @@ -76,26 +76,13 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do end defp state_to_map(%State{} = state) do - res = Map.take(state, [:caller, :prematch, :stacktrace, :unused, :runtime_modules, :vars]) - - if Version.match?(System.version(), "< 1.15.0") do - res |> Map.put(:prematch, :warn) - else - res - end + Map.take(state, [:caller, :prematch, :stacktrace, :unused, :runtime_modules, :vars]) end defp expand(ast) do Compiler.expand( ast, - %State{ - prematch: - if Version.match?(System.version(), ">= 1.15.0-dev") do - Code.get_compiler_option(:on_undefined_variable) - else - :warn - end - }, + state_with_prematch(), Compiler.env() ) end @@ -143,10 +130,21 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do {:ok, %{}} end + defp state_with_prematch do + %State{ + prematch: + if Version.match?(System.version(), ">= 1.15.0-dev") do + Code.get_compiler_option(:on_undefined_variable) + else + :warn + end + } + end + test "initial" do elixir_env = :elixir_env.new() assert Compiler.env() == elixir_env - assert state_to_map(%State{}) == elixir_ex_to_map(:elixir_env.env_to_ex(elixir_env)) + assert state_to_map(state_with_prematch()) == elixir_ex_to_map(:elixir_env.env_to_ex(elixir_env)) end describe "special forms" do @@ -227,7 +225,7 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do test "expands __MODULE__" do ast = {:__MODULE__, [], nil} - {expanded, state, env} = Compiler.expand(ast, %State{}, %{Compiler.env() | module: Foo}) + {expanded, state, env} = Compiler.expand(ast, state_with_prematch(), %{Compiler.env() | module: Foo}) elixir_env = %{:elixir_env.new() | module: Foo} {elixir_expanded, elixir_state, elixir_env} = @@ -242,7 +240,7 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do ast = {:__DIR__, [], nil} {expanded, state, env} = - Compiler.expand(ast, %State{}, %{Compiler.env() | file: __ENV__.file}) + Compiler.expand(ast, state_with_prematch(), %{Compiler.env() | file: __ENV__.file}) elixir_env = %{:elixir_env.new() | file: __ENV__.file} @@ -256,7 +254,7 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do test "expands __CALLER__" do ast = {:__CALLER__, [], nil} - {expanded, state, env} = Compiler.expand(ast, %State{caller: true}, Compiler.env()) + {expanded, state, env} = Compiler.expand(ast, %State{state_with_prematch() | caller: true}, Compiler.env()) elixir_env = :elixir_env.new() {elixir_expanded, elixir_state, elixir_env} = @@ -273,7 +271,7 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do test "expands __STACKTRACE__" do ast = {:__STACKTRACE__, [], nil} - {expanded, state, env} = Compiler.expand(ast, %State{stacktrace: true}, Compiler.env()) + {expanded, state, env} = Compiler.expand(ast, %State{state_with_prematch() | stacktrace: true}, Compiler.env()) elixir_env = :elixir_env.new() {elixir_expanded, elixir_state, elixir_env} = @@ -290,20 +288,27 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do test "expands __ENV__" do ast = {:__ENV__, [], nil} - {expanded, state, env} = Compiler.expand(ast, %State{}, Compiler.env()) + {expanded, state, env} = Compiler.expand(ast, state_with_prematch(), Compiler.env()) elixir_env = :elixir_env.new() {elixir_expanded, elixir_state, elixir_env} = :elixir_expand.expand(ast, :elixir_env.env_to_ex(elixir_env), elixir_env) - assert expanded == elixir_expanded + assert {:%{}, [], expanded_fields} = expanded + assert {:%{}, [], elixir_fields} = elixir_expanded + + assert Enum.sort(expanded_fields) == Enum.sort(elixir_fields) assert env == elixir_env assert state_to_map(state) == elixir_ex_to_map(elixir_state) end test "expands __ENV__.property" do assert_expansion("__ENV__.requires") - assert_expansion("__ENV__.foo") + if Version.match?(System.version(), ">= 1.15.0") do + # elixir 1.14 returns fields in different order + # we don't test that as the code is invalid anyway + assert_expansion("__ENV__.foo") + end end if Version.match?(System.version(), ">= 1.16.0") do @@ -682,7 +687,7 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do test "expands nullary call if_undefined: :apply" do ast = {:self, [if_undefined: :apply], nil} - {expanded, state, env} = Compiler.expand(ast, %State{}, Compiler.env()) + {expanded, state, env} = Compiler.expand(ast, state_with_prematch(), Compiler.env()) elixir_env = :elixir_env.new() {elixir_expanded, elixir_state, elixir_env} = diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index f68dcf96..4ecc239f 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -1,3 +1,4 @@ +if Version.match?(System.version(), ">= 1.15.0") do defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do use ExUnit.Case, async: true @@ -2831,3 +2832,4 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert env.module == nil end end +end diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 3bbf1c4f..ef4f286b 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -1672,7 +1672,11 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert [%VarInfo{type: {:atom, :my_var}}] = state |> get_line_vars(3) + if Version.match?(System.version(), "< 1.15.0") do + assert [%VarInfo{type: {:intersection, [{:atom, :my_var}, {:local_call, :x, []}]}}] = state |> get_line_vars(3) + else + assert [%VarInfo{type: {:atom, :my_var}}] = state |> get_line_vars(3) + end end test "variable binding simple case match context reverse order" do @@ -1685,7 +1689,11 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state + if Version.match?(System.version(), "< 1.15.0") do + assert [%VarInfo{type: {:intersection, [{:atom, :my_var}, {:local_call, :x, []}]}}] = state |> get_line_vars(3) + else assert [%VarInfo{type: {:atom, :my_var}}] = state |> get_line_vars(3) + end end test "variable binding simple case match context guard" do @@ -2147,13 +2155,19 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] = state |> get_line_vars(7) assert [ - %VarInfo{name: :var1, type: nil}, + %VarInfo{name: :var1, type: maybe_local_call}, %VarInfo{name: :var2, type: {:local_call, :now, []}}, %VarInfo{name: :var3, type: {:local_call, :now, [{:atom, :abc}]}}, %VarInfo{name: :var4, type: {:local_call, :now, [{:atom, :abc}]}}, %VarInfo{name: :var5, type: {:local_call, :now, [{:atom, :abc}, {:integer, 5}]}} ] = state |> get_line_vars(16) + if Version.match?(System.version(), "< 1.15.0") do + assert maybe_local_call == {:local_call, :now, []} + else + assert maybe_local_call == nil + end + assert [ %VarInfo{name: :abc, type: nil}, %VarInfo{name: :var1, type: {:call, {:variable, :var1, 0}, :abc, []}}, @@ -7403,7 +7417,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - if Version.match?(System.version(), ">= 1.15.0") do assert %{ 4 => [ %CallInfo{arity: 1, func: :func, position: {4, 11}, mod: {:attribute, :attr}}, @@ -7413,17 +7426,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do %CallInfo{arity: 1, func: :func, position: {5, 9}, mod: {:variable, :var, 0}} ] } = state.calls - else - assert %{ - 4 => [ - %CallInfo{arity: 1, func: :func, position: {4, 11}, mod: {:attribute, :attr}} - ], - 5 => [ - %CallInfo{arity: 0, func: :var, position: {5, 5}, mod: nil}, - %CallInfo{arity: 1, func: :func, position: {5, 9}, mod: {:variable, :var, 0}} - ] - } = state.calls - end end test "registers calls on attribute and var without args" do @@ -7439,7 +7441,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - if Version.match?(System.version(), ">= 1.15.0") do Enum.any?( state.calls[4], &match?( @@ -7455,17 +7456,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do &1 ) ) - else - assert %{ - 4 => [ - %CallInfo{arity: 0, func: :func, position: {4, 11}, mod: {:attribute, :attr}} - ], - 5 => [ - %CallInfo{arity: 0, func: :var, position: {5, 5}, mod: nil}, - %CallInfo{arity: 0, func: :func, position: {5, 9}, mod: {:variable, :var, 0}} - ] - } = state.calls - end end test "registers calls on attribute and var anonymous" do @@ -7481,7 +7471,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - if Version.match?(System.version(), ">= 1.15.0") do assert %{ 4 => [ %CallInfo{arity: 0, func: {:attribute, :attr}, position: {4, 11}, mod: nil}, @@ -7491,17 +7480,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do %CallInfo{arity: 0, func: {:variable, :var, 0}, position: {5, 9}, mod: nil} ] } = state.calls - else - assert %{ - 4 => [ - %CallInfo{arity: 0, func: {:attribute, :attr}, position: {4, 11}, mod: nil} - ], - 5 => [ - %CallInfo{arity: 0, func: :var, position: {5, 5}, mod: nil}, - %CallInfo{arity: 0, func: {:variable, :var, 0}, position: {5, 9}, mod: nil} - ] - } = state.calls - end end test "registers calls pipe with __MODULE__ operator no parens" do @@ -8107,7 +8085,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } } = state.types - if Version.match?(System.version(), ">= 1.14.0") do + if Version.match?(System.version(), ">= 1.15.0") do assert "All the types that implement this protocol" <> _ = doc end end diff --git a/test/elixir_sense/core/parser_test.exs b/test/elixir_sense/core/parser_test.exs index 5381a743..8deb5a03 100644 --- a/test/elixir_sense/core/parser_test.exs +++ b/test/elixir_sense/core/parser_test.exs @@ -113,7 +113,9 @@ defmodule ElixirSense.Core.ParserTest do error: {:error, :parse_error} }, %Env{functions: functions}} = parse(source, {3, 10}) + if Version.match?(System.version(), ">= 1.15.0") do assert Keyword.has_key?(functions, List) + end end test "parse_string with missing terminator \"\'\"" do @@ -128,7 +130,9 @@ defmodule ElixirSense.Core.ParserTest do error: {:error, :parse_error} }, %Env{functions: functions}} = parse(source, {3, 10}) + if Version.match?(System.version(), ">= 1.15.0") do assert Keyword.has_key?(functions, List) + end end test "parse_string with missing heredoc terminator" do @@ -203,11 +207,13 @@ defmodule ElixirSense.Core.ParserTest do {_metadata, env} = parse(source, {3, 23}) + if Version.match?(System.version(), ">= 1.15.0") do assert %Env{ vars: [ %VarInfo{name: :x} ] } = env + end end test "parse_string with missing terminator \"end\" attempts to insert `end` at correct indentation" do @@ -359,10 +365,17 @@ defmodule ElixirSense.Core.ParserTest do vars: vars }} = parse(source, {5, 14}) + if Version.match?(System.version(), "< 1.15.0") do + # container_cursor_to_quoted removes function body + assert [ + %ElixirSense.Core.State.VarInfo{name: :y} + ] = Enum.sort(vars) + else assert [ %ElixirSense.Core.State.VarInfo{name: :x}, %ElixirSense.Core.State.VarInfo{name: :y} ] = Enum.sort(vars) + end end test "parse struct" do From 534cb0f381a7574d70b7949f822fbaa2e444169c Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 28 Sep 2024 13:20:11 +0200 Subject: [PATCH 213/235] finer excludes --- test/elixir_sense/core/compiler_test.exs | 1516 ++++++++--------- .../metadata_builder/error_recovery_test.exs | 94 +- 2 files changed, 849 insertions(+), 761 deletions(-) diff --git a/test/elixir_sense/core/compiler_test.exs b/test/elixir_sense/core/compiler_test.exs index d67e4a9c..862dcde9 100644 --- a/test/elixir_sense/core/compiler_test.exs +++ b/test/elixir_sense/core/compiler_test.exs @@ -1,693 +1,716 @@ -if true or Version.match?(System.version(), ">= 1.17.0-dev") do - defmodule ElixirSense.Core.CompilerTest do - use ExUnit.Case, async: true - alias ElixirSense.Core.Compiler - alias ElixirSense.Core.State - require Record - - defp to_quoted!(ast, true), do: ast - - defp to_quoted!(string, false), - do: Code.string_to_quoted!(string, columns: true, token_metadata: true) - - if Version.match?(System.version(), ">= 1.17.0-dev") do - Record.defrecordp(:elixir_ex, - caller: false, - prematch: :raise, - stacktrace: false, - unused: {%{}, 0}, - runtime_modules: [], - vars: {%{}, false} - ) - - defp elixir_ex_to_map( - elixir_ex( - caller: caller, - prematch: prematch, - stacktrace: stacktrace, - unused: {_, unused}, - runtime_modules: runtime_modules, - vars: vars - ) - ) do - %{ - caller: caller, - prematch: prematch, - stacktrace: stacktrace, - unused: unused, - runtime_modules: runtime_modules, - vars: vars - } - end - else - Record.defrecordp(:elixir_ex, - caller: false, - prematch: %State{ - prematch: - if Version.match?(System.version(), ">= 1.15.0-dev") do - Code.get_compiler_option(:on_undefined_variable) - else - :warn - end - }, - stacktrace: false, - unused: {%{}, 0}, - vars: {%{}, false} - ) - - defp elixir_ex_to_map( - elixir_ex( - caller: caller, - prematch: prematch, - stacktrace: stacktrace, - unused: {_, unused}, - vars: vars - ) - ) do - %{ - caller: caller, - prematch: prematch, - stacktrace: stacktrace, - unused: unused, - runtime_modules: [], - vars: vars - } - end - end - - defp state_to_map(%State{} = state) do - Map.take(state, [:caller, :prematch, :stacktrace, :unused, :runtime_modules, :vars]) - end - - defp expand(ast) do - Compiler.expand( - ast, - state_with_prematch(), - Compiler.env() - ) - end - - defp elixir_expand(ast) do - env = :elixir_env.new() - :elixir_expand.expand(ast, :elixir_env.env_to_ex(env), env) - end - - defmacrop assert_expansion(code, ast \\ false) do - quote do - ast = to_quoted!(unquote(code), unquote(ast)) - {elixir_expanded, elixir_state, elixir_env} = elixir_expand(ast) - # dbg(elixir_expanded) - {expanded, state, env} = expand(ast) - # dbg(expanded) - - assert clean_capture_arg(expanded) == clean_capture_arg_elixir(elixir_expanded) - assert env == elixir_env - assert state_to_map(state) == elixir_ex_to_map(elixir_state) - end - end - - defmacrop assert_expansion_env(code, ast \\ false) do - quote do - ast = to_quoted!(unquote(code), unquote(ast)) - {elixir_expanded, elixir_state, elixir_env} = elixir_expand(ast) - # dbg(elixir_expanded) - # dbg(elixir_ex_to_map(elixir_state)) - {expanded, state, env} = expand(ast) - # dbg(expanded) - # dbg(state_to_map(state)) - - assert env == elixir_env - assert state_to_map(state) == elixir_ex_to_map(elixir_state) - end - end - - setup do - # Application.put_env(:elixir_sense, :compiler_rewrite, true) - on_exit(fn -> - Application.put_env(:elixir_sense, :compiler_rewrite, false) - end) - - {:ok, %{}} +defmodule ElixirSense.Core.CompilerTest do + use ExUnit.Case, async: true + alias ElixirSense.Core.Compiler + alias ElixirSense.Core.State + require Record + + defp to_quoted!(ast, true), do: ast + + defp to_quoted!(string, false), + do: Code.string_to_quoted!(string, columns: true, token_metadata: true) + + if Version.match?(System.version(), ">= 1.17.0-dev") do + Record.defrecordp(:elixir_ex, + caller: false, + prematch: :raise, + stacktrace: false, + unused: {%{}, 0}, + runtime_modules: [], + vars: {%{}, false} + ) + + defp elixir_ex_to_map( + elixir_ex( + caller: caller, + prematch: prematch, + stacktrace: stacktrace, + unused: {_, unused}, + runtime_modules: runtime_modules, + vars: vars + ) + ) do + %{ + caller: caller, + prematch: prematch, + stacktrace: stacktrace, + unused: unused, + runtime_modules: runtime_modules, + vars: vars + } end - - defp state_with_prematch do - %State{ + else + Record.defrecordp(:elixir_ex, + caller: false, + prematch: %State{ prematch: if Version.match?(System.version(), ">= 1.15.0-dev") do Code.get_compiler_option(:on_undefined_variable) else :warn end + }, + stacktrace: false, + unused: {%{}, 0}, + vars: {%{}, false} + ) + + defp elixir_ex_to_map( + elixir_ex( + caller: caller, + prematch: prematch, + stacktrace: stacktrace, + unused: {_, unused}, + vars: vars + ) + ) do + %{ + caller: caller, + prematch: prematch, + stacktrace: stacktrace, + unused: unused, + runtime_modules: [], + vars: vars } end + end - test "initial" do - elixir_env = :elixir_env.new() - assert Compiler.env() == elixir_env - assert state_to_map(state_with_prematch()) == elixir_ex_to_map(:elixir_env.env_to_ex(elixir_env)) - end + defp state_to_map(%State{} = state) do + Map.take(state, [:caller, :prematch, :stacktrace, :unused, :runtime_modules, :vars]) + end - describe "special forms" do - test "expands =" do - assert_expansion("1 = 1") - end + defp expand(ast) do + Compiler.expand( + ast, + state_with_prematch(), + Compiler.env() + ) + end - test "expands {}" do - assert_expansion("{}") - assert_expansion("{1, 2, 3}") - assert_expansion("{a, b} = {:ok, 1}") - end + defp elixir_expand(ast) do + env = :elixir_env.new() + :elixir_expand.expand(ast, :elixir_env.env_to_ex(env), env) + end - test "expands %{}" do - assert_expansion("%{1 => 2}") - assert_expansion("%{a: 3}") - assert_expansion("%{a: a} = %{}") - assert_expansion("%{1 => a} = %{}") - assert_expansion("%{%{a: 1} | a: 2}") - assert_expansion("%{%{\"a\" => 1} | \"a\" => 2}") - end + defmacrop assert_expansion(code, ast \\ false) do + quote do + ast = to_quoted!(unquote(code), unquote(ast)) + {elixir_expanded, elixir_state, elixir_env} = elixir_expand(ast) + # dbg(elixir_expanded) + {expanded, state, env} = expand(ast) + # dbg(expanded) + + assert clean_capture_arg(expanded) == clean_capture_arg_elixir(elixir_expanded) + assert env == elixir_env + assert state_to_map(state) == elixir_ex_to_map(elixir_state) + end + end - test "expands %" do - assert_expansion("%Date{year: 2024, month: 2, day: 18}") - assert_expansion("%Date{calendar: Calendar.ISO, year: 2024, month: 2, day: 18}") - assert_expansion("%{year: x} = %Date{year: 2024, month: 2, day: 18}") - assert_expansion("%Date{year: x} = %Date{year: 2024, month: 2, day: 18}") - assert_expansion("%Date{%Date{year: 2024, month: 2, day: 18} | day: 1}") - assert_expansion("%x{} = %Date{year: 2024, month: 2, day: 18}") - end + defmacrop assert_expansion_env(code, ast \\ false) do + quote do + ast = to_quoted!(unquote(code), unquote(ast)) + {elixir_expanded, elixir_state, elixir_env} = elixir_expand(ast) + # dbg(elixir_expanded) + # dbg(elixir_ex_to_map(elixir_state)) + {expanded, state, env} = expand(ast) + # dbg(expanded) + # dbg(state_to_map(state)) + + assert env == elixir_env + assert state_to_map(state) == elixir_ex_to_map(elixir_state) + end + end - if Version.match?(System.version(), ">= 1.15.0") do - test "expands <<>>" do - assert_expansion("<<>>") - assert_expansion("<<1>>") - assert_expansion("<> = \"\"") - end + setup do + # Application.put_env(:elixir_sense, :compiler_rewrite, true) + on_exit(fn -> + Application.put_env(:elixir_sense, :compiler_rewrite, false) + end) - test "expands <<>> with modifier" do - assert_expansion("x = 1; y = 1; <>") - assert_expansion("x = 1; y = 1; <> = <<>>") + {:ok, %{}} + end + + defp state_with_prematch do + %State{ + prematch: + if Version.match?(System.version(), ">= 1.15.0-dev") do + Code.get_compiler_option(:on_undefined_variable) + else + :warn end - end + } + end - test "expands __block__" do - assert_expansion({:__block__, [], []}, true) - assert_expansion({:__block__, [], [1]}, true) - assert_expansion({:__block__, [], [1, 2]}, true) - end + test "initial" do + elixir_env = :elixir_env.new() + assert Compiler.env() == elixir_env + assert state_to_map(state_with_prematch()) == elixir_ex_to_map(:elixir_env.env_to_ex(elixir_env)) + end - test "expands __aliases__" do - assert_expansion({:__aliases__, [], [:Asd, :Foo]}, true) - assert_expansion({:__block__, [], [:Asd]}, true) - assert_expansion({:__block__, [], [Elixir, :Asd]}, true) - end + describe "special forms" do + test "expands =" do + assert_expansion("1 = 1") + end - test "expands alias" do - assert_expansion("alias Foo") - assert_expansion("alias Foo.Bar") - assert_expansion("alias Foo.Bar, as: Baz") - end + test "expands {}" do + assert_expansion("{}") + assert_expansion("{1, 2, 3}") + assert_expansion("{a, b} = {:ok, 1}") + end - test "expands require" do - assert_expansion("require Code") - assert_expansion("require Code.Fragment") - assert_expansion("require Code.Fragment, as: Baz") - end + test "expands %{}" do + assert_expansion("%{1 => 2}") + assert_expansion("%{a: 3}") + assert_expansion("%{a: a} = %{}") + assert_expansion("%{1 => a} = %{}") + assert_expansion("%{%{a: 1} | a: 2}") + assert_expansion("%{%{\"a\" => 1} | \"a\" => 2}") + end - test "expands import" do - assert_expansion("import Code") - assert_expansion("import Code.Fragment") - assert_expansion("import Code.Fragment, only: :functions") + test "expands %" do + assert_expansion("%Date{year: 2024, month: 2, day: 18}") + assert_expansion("%Date{calendar: Calendar.ISO, year: 2024, month: 2, day: 18}") + assert_expansion("%{year: x} = %Date{year: 2024, month: 2, day: 18}") + assert_expansion("%Date{year: x} = %Date{year: 2024, month: 2, day: 18}") + assert_expansion("%Date{%Date{year: 2024, month: 2, day: 18} | day: 1}") + assert_expansion("%x{} = %Date{year: 2024, month: 2, day: 18}") + end + + if Version.match?(System.version(), ">= 1.15.0") do + test "expands <<>>" do + assert_expansion("<<>>") + assert_expansion("<<1>>") + assert_expansion("<> = \"\"") end - test "expands multi alias" do - assert_expansion("alias Foo.{Bar, Some.Other}") + test "expands <<>> with modifier" do + assert_expansion("x = 1; y = 1; <>") + assert_expansion("x = 1; y = 1; <> = <<>>") end + end - test "expands __MODULE__" do - ast = {:__MODULE__, [], nil} - {expanded, state, env} = Compiler.expand(ast, state_with_prematch(), %{Compiler.env() | module: Foo}) - elixir_env = %{:elixir_env.new() | module: Foo} + test "expands __block__" do + assert_expansion({:__block__, [], []}, true) + assert_expansion({:__block__, [], [1]}, true) + assert_expansion({:__block__, [], [1, 2]}, true) + end - {elixir_expanded, elixir_state, elixir_env} = - :elixir_expand.expand(ast, :elixir_env.env_to_ex(elixir_env), elixir_env) + test "expands __aliases__" do + assert_expansion({:__aliases__, [], [:Asd, :Foo]}, true) + assert_expansion({:__block__, [], [:Asd]}, true) + assert_expansion({:__block__, [], [Elixir, :Asd]}, true) + end - assert expanded == elixir_expanded - assert env == elixir_env - assert state_to_map(state) == elixir_ex_to_map(elixir_state) - end + test "expands alias" do + assert_expansion("alias Foo") + assert_expansion("alias Foo.Bar") + assert_expansion("alias Foo.Bar, as: Baz") + end - test "expands __DIR__" do - ast = {:__DIR__, [], nil} + test "expands require" do + assert_expansion("require Code") + assert_expansion("require Code.Fragment") + assert_expansion("require Code.Fragment, as: Baz") + end - {expanded, state, env} = - Compiler.expand(ast, state_with_prematch(), %{Compiler.env() | file: __ENV__.file}) + test "expands import" do + assert_expansion("import Code") + assert_expansion("import Code.Fragment") + assert_expansion("import Code.Fragment, only: :functions") + end - elixir_env = %{:elixir_env.new() | file: __ENV__.file} + test "expands multi alias" do + assert_expansion("alias Foo.{Bar, Some.Other}") + end - {elixir_expanded, elixir_state, elixir_env} = - :elixir_expand.expand(ast, :elixir_env.env_to_ex(elixir_env), elixir_env) + test "expands __MODULE__" do + ast = {:__MODULE__, [], nil} + {expanded, state, env} = Compiler.expand(ast, state_with_prematch(), %{Compiler.env() | module: Foo}) + elixir_env = %{:elixir_env.new() | module: Foo} - assert expanded == elixir_expanded - assert env == elixir_env - assert state_to_map(state) == elixir_ex_to_map(elixir_state) - end + {elixir_expanded, elixir_state, elixir_env} = + :elixir_expand.expand(ast, :elixir_env.env_to_ex(elixir_env), elixir_env) - test "expands __CALLER__" do - ast = {:__CALLER__, [], nil} - {expanded, state, env} = Compiler.expand(ast, %State{state_with_prematch() | caller: true}, Compiler.env()) - elixir_env = :elixir_env.new() + assert expanded == elixir_expanded + assert env == elixir_env + assert state_to_map(state) == elixir_ex_to_map(elixir_state) + end - {elixir_expanded, elixir_state, elixir_env} = - :elixir_expand.expand( - ast, - elixir_ex(:elixir_env.env_to_ex(elixir_env), caller: true), - elixir_env - ) + test "expands __DIR__" do + ast = {:__DIR__, [], nil} - assert expanded == elixir_expanded - assert env == elixir_env - assert state_to_map(state) == elixir_ex_to_map(elixir_state) - end + {expanded, state, env} = + Compiler.expand(ast, state_with_prematch(), %{Compiler.env() | file: __ENV__.file}) - test "expands __STACKTRACE__" do - ast = {:__STACKTRACE__, [], nil} - {expanded, state, env} = Compiler.expand(ast, %State{state_with_prematch() | stacktrace: true}, Compiler.env()) - elixir_env = :elixir_env.new() + elixir_env = %{:elixir_env.new() | file: __ENV__.file} - {elixir_expanded, elixir_state, elixir_env} = - :elixir_expand.expand( - ast, - elixir_ex(:elixir_env.env_to_ex(elixir_env), stacktrace: true), - elixir_env - ) + {elixir_expanded, elixir_state, elixir_env} = + :elixir_expand.expand(ast, :elixir_env.env_to_ex(elixir_env), elixir_env) - assert expanded == elixir_expanded - assert env == elixir_env - assert state_to_map(state) == elixir_ex_to_map(elixir_state) - end + assert expanded == elixir_expanded + assert env == elixir_env + assert state_to_map(state) == elixir_ex_to_map(elixir_state) + end - test "expands __ENV__" do - ast = {:__ENV__, [], nil} - {expanded, state, env} = Compiler.expand(ast, state_with_prematch(), Compiler.env()) - elixir_env = :elixir_env.new() + test "expands __CALLER__" do + ast = {:__CALLER__, [], nil} + {expanded, state, env} = Compiler.expand(ast, %State{state_with_prematch() | caller: true}, Compiler.env()) + elixir_env = :elixir_env.new() - {elixir_expanded, elixir_state, elixir_env} = - :elixir_expand.expand(ast, :elixir_env.env_to_ex(elixir_env), elixir_env) + {elixir_expanded, elixir_state, elixir_env} = + :elixir_expand.expand( + ast, + elixir_ex(:elixir_env.env_to_ex(elixir_env), caller: true), + elixir_env + ) - assert {:%{}, [], expanded_fields} = expanded - assert {:%{}, [], elixir_fields} = elixir_expanded + assert expanded == elixir_expanded + assert env == elixir_env + assert state_to_map(state) == elixir_ex_to_map(elixir_state) + end - assert Enum.sort(expanded_fields) == Enum.sort(elixir_fields) - assert env == elixir_env - assert state_to_map(state) == elixir_ex_to_map(elixir_state) - end + test "expands __STACKTRACE__" do + ast = {:__STACKTRACE__, [], nil} + {expanded, state, env} = Compiler.expand(ast, %State{state_with_prematch() | stacktrace: true}, Compiler.env()) + elixir_env = :elixir_env.new() - test "expands __ENV__.property" do - assert_expansion("__ENV__.requires") - if Version.match?(System.version(), ">= 1.15.0") do - # elixir 1.14 returns fields in different order - # we don't test that as the code is invalid anyway - assert_expansion("__ENV__.foo") - end - end + {elixir_expanded, elixir_state, elixir_env} = + :elixir_expand.expand( + ast, + elixir_ex(:elixir_env.env_to_ex(elixir_env), stacktrace: true), + elixir_env + ) - if Version.match?(System.version(), ">= 1.16.0") do - test "expands quote literal" do - assert_expansion("quote do: 2") - assert_expansion("quote do: :foo") - assert_expansion("quote do: \"asd\"") - assert_expansion("quote do: []") - assert_expansion("quote do: [12]") - assert_expansion("quote do: [12, 34]") - assert_expansion("quote do: [12 | 34]") - assert_expansion("quote do: [12 | [34]]") - assert_expansion("quote do: {12}") - assert_expansion("quote do: {12, 34}") - assert_expansion("quote do: %{a: 12}") - end + assert expanded == elixir_expanded + assert env == elixir_env + assert state_to_map(state) == elixir_ex_to_map(elixir_state) + end + + test "expands __ENV__" do + ast = {:__ENV__, [], nil} + {expanded, state, env} = Compiler.expand(ast, state_with_prematch(), Compiler.env()) + elixir_env = :elixir_env.new() + + {elixir_expanded, elixir_state, elixir_env} = + :elixir_expand.expand(ast, :elixir_env.env_to_ex(elixir_env), elixir_env) + + assert {:%{}, [], expanded_fields} = expanded + assert {:%{}, [], elixir_fields} = elixir_expanded + + assert Enum.sort(expanded_fields) == Enum.sort(elixir_fields) + assert env == elixir_env + assert state_to_map(state) == elixir_ex_to_map(elixir_state) + end + + test "expands __ENV__.property" do + assert_expansion("__ENV__.requires") + if Version.match?(System.version(), ">= 1.15.0") do + # elixir 1.14 returns fields in different order + # we don't test that as the code is invalid anyway + assert_expansion("__ENV__.foo") end + end - if Version.match?(System.version(), ">= 1.16.0") do - test "expands quote variable" do - assert_expansion("quote do: abc") - end + if Version.match?(System.version(), ">= 1.16.0") do + test "expands quote literal" do + assert_expansion("quote do: 2") + assert_expansion("quote do: :foo") + assert_expansion("quote do: \"asd\"") + assert_expansion("quote do: []") + assert_expansion("quote do: [12]") + assert_expansion("quote do: [12, 34]") + assert_expansion("quote do: [12 | 34]") + assert_expansion("quote do: [12 | [34]]") + assert_expansion("quote do: {12}") + assert_expansion("quote do: {12, 34}") + assert_expansion("quote do: %{a: 12}") end + end - if Version.match?(System.version(), ">= 1.16.0") do - test "expands quote quote" do - assert_expansion(""" - quote do: (quote do: 1) - """) - end + if Version.match?(System.version(), ">= 1.16.0") do + test "expands quote variable" do + assert_expansion("quote do: abc") end + end - test "expands quote block" do + if Version.match?(System.version(), ">= 1.16.0") do + test "expands quote quote" do assert_expansion(""" - quote do: () + quote do: (quote do: 1) """) end + end + + test "expands quote block" do + assert_expansion(""" + quote do: () + """) + end + + test "expands quote unquote" do + assert_expansion(""" + a = 1 + quote do: unquote(a) + """) + end + + test "expands quote unquote block" do + assert_expansion(""" + a = 1 + quote do: (unquote(a)) + """) + end + + test "expands quote unquote_splicing tuple" do + assert_expansion(""" + quote do: {unquote_splicing([1, 2]), unquote_splicing([2])} + """) + end - test "expands quote unquote" do + if Version.match?(System.version(), ">= 1.16.0") do + test "expands quote unquote_splicing" do assert_expansion(""" - a = 1 - quote do: unquote(a) + a = [1, 2, 3] + quote do: (unquote_splicing(a)) """) end + end - test "expands quote unquote block" do + if Version.match?(System.version(), ">= 1.16.0") do + test "expands quote unquote_splicing in list" do assert_expansion(""" - a = 1 - quote do: (unquote(a)) + a = [1, 2, 3] + quote do: [unquote_splicing(a) | [1]] """) - end - test "expands quote unquote_splicing tuple" do assert_expansion(""" - quote do: {unquote_splicing([1, 2]), unquote_splicing([2])} + a = [1, 2, 3] + quote do: [1 | unquote_splicing(a)] """) end + end - if Version.match?(System.version(), ">= 1.16.0") do - test "expands quote unquote_splicing" do - assert_expansion(""" - a = [1, 2, 3] - quote do: (unquote_splicing(a)) - """) - end + if Version.match?(System.version(), ">= 1.16.0") do + test "expands quote alias" do + assert_expansion("quote do: Date") + assert_expansion("quote do: Elixir.Date") + assert_expansion("quote do: String.Chars") + assert_expansion("alias String.Chars; quote do: Chars") + assert_expansion("alias String.Chars; quote do: Chars.foo().A") end + end - if Version.match?(System.version(), ">= 1.16.0") do - test "expands quote unquote_splicing in list" do - assert_expansion(""" - a = [1, 2, 3] - quote do: [unquote_splicing(a) | [1]] - """) + if Version.match?(System.version(), ">= 1.16.0") do + test "expands quote import" do + assert_expansion("quote do: inspect(1)") + assert_expansion("quote do: &inspect/1") + end + end - assert_expansion(""" - a = [1, 2, 3] - quote do: [1 | unquote_splicing(a)] - """) + if Version.match?(System.version(), ">= 1.17.0") do + test "expands quote with bind_quoted" do + assert_expansion(""" + kv = [a: 1] + quote bind_quoted: [kv: kv] do + Enum.each(kv, fn {k, v} -> + def unquote(k)(), do: unquote(v) + end) end + """) end + end - if Version.match?(System.version(), ">= 1.16.0") do - test "expands quote alias" do - assert_expansion("quote do: Date") - assert_expansion("quote do: Elixir.Date") - assert_expansion("quote do: String.Chars") - assert_expansion("alias String.Chars; quote do: Chars") - assert_expansion("alias String.Chars; quote do: Chars.foo().A") + if Version.match?(System.version(), ">= 1.16.0") do + test "expands quote with unquote false" do + assert_expansion(""" + quote unquote: false do + unquote("hello") end + """) end + end - if Version.match?(System.version(), ">= 1.16.0") do - test "expands quote import" do - assert_expansion("quote do: inspect(1)") - assert_expansion("quote do: &inspect/1") - end + if Version.match?(System.version(), ">= 1.17.0") do + test "expands quote with file" do + assert_expansion(""" + quote file: "some.ex", do: bar(1, 2, 3) + """) end + end - if Version.match?(System.version(), ">= 1.17.0") do - test "expands quote with bind_quoted" do - assert_expansion(""" - kv = [a: 1] - quote bind_quoted: [kv: kv] do - Enum.each(kv, fn {k, v} -> - def unquote(k)(), do: unquote(v) - end) - end - """) - end + if Version.match?(System.version(), ">= 1.16.0") do + test "expands quote with line" do + assert_expansion(""" + quote line: 123, do: bar(1, 2, 3) + """) end + end - if Version.match?(System.version(), ">= 1.16.0") do - test "expands quote with unquote false" do - assert_expansion(""" - quote unquote: false do - unquote("hello") - end - """) - end + if Version.match?(System.version(), ">= 1.16.0") do + test "expands quote with location: keep" do + assert_expansion(""" + quote location: :keep, do: bar(1, 2, 3) + """) end + end - if Version.match?(System.version(), ">= 1.17.0") do - test "expands quote with file" do - assert_expansion(""" - quote file: "some.ex", do: bar(1, 2, 3) - """) - end + if Version.match?(System.version(), ">= 1.16.0") do + test "expands quote with context" do + assert_expansion(""" + quote context: Foo, do: abc = 3 + """) end + end - if Version.match?(System.version(), ">= 1.16.0") do - test "expands quote with line" do - assert_expansion(""" - quote line: 123, do: bar(1, 2, 3) - """) + test "expands &super" do + assert_expansion_env(""" + defmodule Abc do + use ElixirSense.Core.CompilerTest.Overridable + + def foo(a) do + &super/1 end end + """) - if Version.match?(System.version(), ">= 1.16.0") do - test "expands quote with location: keep" do - assert_expansion(""" - quote location: :keep, do: bar(1, 2, 3) - """) + assert_expansion_env(""" + defmodule Abc do + use ElixirSense.Core.CompilerTest.Overridable + + def foo(a) do + &super(&1) end end + """) + end - if Version.match?(System.version(), ">= 1.16.0") do - test "expands quote with context" do - assert_expansion(""" - quote context: Foo, do: abc = 3 - """) - end + if Version.match?(System.version(), ">= 1.17.0") do + test "expands &" do + assert_expansion("& &1") + assert_expansion("&Enum.take(&1, 5)") + assert_expansion("&{&1, &2}") + assert_expansion("&[&1 | &2]") + assert_expansion("&inspect/1") + assert_expansion("&Enum.count/1") + assert_expansion("a = %{}; &a.b(&1)") + assert_expansion("&Enum.count(&1)") + assert_expansion("&inspect(&1)") + assert_expansion("&Enum.map(&2, &1)") + assert_expansion("&inspect([&2, &1])") end + end - test "expands &super" do - assert_expansion_env(""" - defmodule Abc do - use ElixirSense.Core.CompilerTest.Overridable - - def foo(a) do - &super/1 - end - end - """) + test "expands fn" do + assert_expansion("fn -> 1 end") + assert_expansion("fn a, b -> {a, b} end") - assert_expansion_env(""" - defmodule Abc do - use ElixirSense.Core.CompilerTest.Overridable - - def foo(a) do - &super(&1) - end - end - """) + assert_expansion(""" + fn + 1 -> 1 + a -> a end + """) + end - if Version.match?(System.version(), ">= 1.17.0") do - test "expands &" do - assert_expansion("& &1") - assert_expansion("&Enum.take(&1, 5)") - assert_expansion("&{&1, &2}") - assert_expansion("&[&1 | &2]") - assert_expansion("&inspect/1") - assert_expansion("&Enum.count/1") - assert_expansion("a = %{}; &a.b(&1)") - assert_expansion("&Enum.count(&1)") - assert_expansion("&inspect(&1)") - assert_expansion("&Enum.map(&2, &1)") - assert_expansion("&inspect([&2, &1])") - end + test "expands cond" do + assert_expansion(""" + cond do + nil -> 0 + true -> 1 end + """) + end - test "expands fn" do - assert_expansion("fn -> 1 end") - assert_expansion("fn a, b -> {a, b} end") + test "expands case" do + assert_expansion(""" + case 1 do + 0 -> 0 + 1 -> 1 + end + """) + end - assert_expansion(""" - fn - 1 -> 1 - a -> a - end - """) + test "expands try" do + assert_expansion(""" + try do + inspect(1) + rescue + e in ArgumentError -> + e + catch + {k, e} -> + {k, e} + else + _ -> :ok + after + IO.puts("") + end + """) + end + + test "expands receive" do + assert_expansion(""" + receive do + x -> x + after + 100 -> IO.puts("") end + """) + end - test "expands cond" do - assert_expansion(""" - cond do - nil -> 0 - true -> 1 - end - """) + test "expands for" do + assert_expansion(""" + for i <- [1, 2, 3] do + i end + """) + + assert_expansion(""" + for i <- [1, 2, 3], j <- [1, 2], true, into: %{}, do: {i, j} + """) + end - test "expands case" do + if Version.match?(System.version(), ">= 1.15.0") do + test "expands for with bitstring generator" do assert_expansion(""" - case 1 do - 0 -> 0 - 1 -> 1 + for <> do + :ok end """) end - test "expands try" do + test "expands for with reduce" do assert_expansion(""" - try do - inspect(1) - rescue - e in ArgumentError -> - e - catch - {k, e} -> - {k, e} - else - _ -> :ok - after - IO.puts("") + for <>, x in ?a..?z, reduce: %{} do + acc -> acc end """) end + end - test "expands receive" do - assert_expansion(""" - receive do - x -> x - after - 100 -> IO.puts("") - end - """) + test "expands for in block" do + assert_expansion(""" + for i <- [1, 2, 3] do + i end + :ok + """) - test "expands for" do - assert_expansion(""" - for i <- [1, 2, 3] do - i - end - """) + assert_expansion(""" + for i <- [1, 2, 3], uniq: true do + i + end + :ok + """) - assert_expansion(""" - for i <- [1, 2, 3], j <- [1, 2], true, into: %{}, do: {i, j} - """) + assert_expansion(""" + _ = for i <- [1, 2, 3] do + i end + :ok + """) + end - if Version.match?(System.version(), ">= 1.15.0") do - test "expands for with bitstring generator" do - assert_expansion(""" - for <> do - :ok - end - """) - end + test "expands with" do + assert_expansion(""" + with i <- :ok do + i + end + """) - test "expands for with reduce" do - assert_expansion(""" - for <>, x in ?a..?z, reduce: %{} do - acc -> acc - end - """) - end + assert_expansion(""" + with :ok <- :ok, j = 5 do + j + else + a -> a end + """) + end - test "expands for in block" do - assert_expansion(""" - for i <- [1, 2, 3] do - i - end - :ok - """) + defmodule Overridable do + defmacro __using__(_args) do + quote do + def foo(a) do + a + end - assert_expansion(""" - for i <- [1, 2, 3], uniq: true do - i - end - :ok - """) + defmacro bar(ast) do + ast + end - assert_expansion(""" - _ = for i <- [1, 2, 3] do - i + defoverridable foo: 1, bar: 1 end - :ok - """) end + end - test "expands with" do - assert_expansion(""" - with i <- :ok do - i + test "expands super" do + assert_expansion_env(""" + defmodule Abc do + use ElixirSense.Core.CompilerTest.Overridable + + def foo(a) do + super(a + 1) end - """) - assert_expansion(""" - with :ok <- :ok, j = 5 do - j - else - a -> a + defmacro bar(a) do + quote do + unquote(super(b)) - 1 + end end - """) end + """) + end - defmodule Overridable do - defmacro __using__(_args) do - quote do - def foo(a) do - a - end + test "expands underscored var write" do + assert_expansion("_ = 5") + end - defmacro bar(ast) do - ast - end + test "expands var write" do + assert_expansion("a = 5") + end - defoverridable foo: 1, bar: 1 - end - end - end + test "expands var read" do + assert_expansion("a = 5; a") + end - test "expands super" do - assert_expansion_env(""" - defmodule Abc do - use ElixirSense.Core.CompilerTest.Overridable - - def foo(a) do - super(a + 1) - end + test "expands var overwrite" do + assert_expansion("a = 5; a = 6") + end - defmacro bar(a) do - quote do - unquote(super(b)) - 1 - end - end - end - """) - end + test "expands var overwrite already overwritten" do + assert_expansion("[a, a] = [5, 5]") + end - test "expands underscored var write" do - assert_expansion("_ = 5") - end + test "expands var pin" do + assert_expansion("a = 5; ^a = 6") + end - test "expands var write" do - assert_expansion("a = 5") - end + test "expands nullary call if_undefined: :apply" do + ast = {:self, [if_undefined: :apply], nil} + {expanded, state, env} = Compiler.expand(ast, state_with_prematch(), Compiler.env()) + elixir_env = :elixir_env.new() - test "expands var read" do - assert_expansion("a = 5; a") - end + {elixir_expanded, elixir_state, elixir_env} = + :elixir_expand.expand(ast, :elixir_env.env_to_ex(elixir_env), elixir_env) - test "expands var overwrite" do - assert_expansion("a = 5; a = 6") - end + assert expanded == elixir_expanded + assert env == elixir_env + assert state_to_map(state) == elixir_ex_to_map(elixir_state) + end - test "expands var overwrite already overwritten" do - assert_expansion("[a, a] = [5, 5]") - end + if Version.match?(System.version(), ">= 1.15.0") do + test "expands nullary call if_undefined: :warn" do + Code.put_compiler_option(:on_undefined_variable, :warn) + ast = {:self, [], nil} - test "expands var pin" do - assert_expansion("a = 5; ^a = 6") - end + {expanded, state, env} = + Compiler.expand( + ast, + %State{ + prematch: Code.get_compiler_option(:on_undefined_variable) || :warn + }, + Compiler.env() + ) - test "expands nullary call if_undefined: :apply" do - ast = {:self, [if_undefined: :apply], nil} - {expanded, state, env} = Compiler.expand(ast, state_with_prematch(), Compiler.env()) elixir_env = :elixir_env.new() {elixir_expanded, elixir_state, elixir_env} = @@ -696,276 +719,251 @@ if true or Version.match?(System.version(), ">= 1.17.0-dev") do assert expanded == elixir_expanded assert env == elixir_env assert state_to_map(state) == elixir_ex_to_map(elixir_state) + after + Code.put_compiler_option(:on_undefined_variable, :raise) end + end - if Version.match?(System.version(), ">= 1.15.0") do - test "expands nullary call if_undefined: :warn" do - Code.put_compiler_option(:on_undefined_variable, :warn) - ast = {:self, [], nil} - - {expanded, state, env} = - Compiler.expand( - ast, - %State{ - prematch: Code.get_compiler_option(:on_undefined_variable) || :warn - }, - Compiler.env() - ) + test "expands local call" do + assert_expansion("get_in(%{}, [:bar])") + assert_expansion("length([])") + end - elixir_env = :elixir_env.new() + test "expands local operator call" do + assert_expansion("a = b = []; a ++ b") + end - {elixir_expanded, elixir_state, elixir_env} = - :elixir_expand.expand(ast, :elixir_env.env_to_ex(elixir_env), elixir_env) + test "expands local call macro" do + # TODO + # assert_expansion("if true, do: :ok") + assert_expansion("1 |> IO.inspect") + end - assert expanded == elixir_expanded - assert env == elixir_env - assert state_to_map(state) == elixir_ex_to_map(elixir_state) - after - Code.put_compiler_option(:on_undefined_variable, :raise) - end - end + test "expands remote call" do + assert_expansion("Kernel.get_in(%{}, [:bar])") + assert_expansion("Kernel.length([])") + assert_expansion("Some.fun().other()") + end - test "expands local call" do - assert_expansion("get_in(%{}, [:bar])") - assert_expansion("length([])") - end + test "expands remote call macro" do + assert_expansion("Kernel.|>(1, IO.inspect)") + end - test "expands local operator call" do - assert_expansion("a = b = []; a ++ b") - end + test "expands anonymous call" do + assert_expansion("foo = fn a -> a end; foo.(1)") + end - test "expands local call macro" do - # TODO - # assert_expansion("if true, do: :ok") - assert_expansion("1 |> IO.inspect") - end + test "expands 2-tuple" do + assert_expansion("{1, 2}") + assert_expansion("{a, b} = {1, 2}") + end - test "expands remote call" do - assert_expansion("Kernel.get_in(%{}, [:bar])") - assert_expansion("Kernel.length([])") - assert_expansion("Some.fun().other()") - end + test "expands list" do + assert_expansion("[]") + assert_expansion("[1, 2]") + assert_expansion("[1 | [2]]") + assert_expansion("[a | b] = [1, 2, 3]") + assert_expansion("[a] ++ [b] = [1, 2]") + end - test "expands remote call macro" do - assert_expansion("Kernel.|>(1, IO.inspect)") - end + test "expands function" do + assert_expansion(&inspect/1, true) + end - test "expands anonymous call" do - assert_expansion("foo = fn a -> a end; foo.(1)") - end + test "expands pid" do + assert_expansion(self(), true) + end - test "expands 2-tuple" do - assert_expansion("{1, 2}") - assert_expansion("{a, b} = {1, 2}") - end + test "expands number" do + assert_expansion(1, true) + assert_expansion(1.5, true) + end - test "expands list" do - assert_expansion("[]") - assert_expansion("[1, 2]") - assert_expansion("[1 | [2]]") - assert_expansion("[a | b] = [1, 2, 3]") - assert_expansion("[a] ++ [b] = [1, 2]") - end + test "expands atom" do + assert_expansion(true, true) + assert_expansion(:foo, true) + assert_expansion(Kernel, true) + end - test "expands function" do - assert_expansion(&inspect/1, true) - end + test "expands binary" do + assert_expansion("abc", true) + end + end - test "expands pid" do - assert_expansion(self(), true) + describe "Kernel macros" do + test "@" do + assert_expansion_env(""" + defmodule Abc do + @foo 1 + @foo end + """) + end - test "expands number" do - assert_expansion(1, true) - assert_expansion(1.5, true) - end + test "defmodule" do + assert_expansion_env("defmodule Abc, do: :ok") - test "expands atom" do - assert_expansion(true, true) - assert_expansion(:foo, true) - assert_expansion(Kernel, true) + assert_expansion_env(""" + defmodule Abc do + foo = 1 end + """) - test "expands binary" do - assert_expansion("abc", true) + assert_expansion_env(""" + defmodule Abc.Some do + foo = 1 end - end + """) - describe "Kernel macros" do - test "@" do - assert_expansion_env(""" - defmodule Abc do - @foo 1 - @foo - end - """) + assert_expansion_env(""" + defmodule Elixir.Abc.Some do + foo = 1 end + """) - test "defmodule" do - assert_expansion_env("defmodule Abc, do: :ok") - - assert_expansion_env(""" - defmodule Abc do + assert_expansion_env(""" + defmodule Abc.Some do + defmodule Child do foo = 1 end - """) - - assert_expansion_env(""" - defmodule Abc.Some do - foo = 1 - end - """) + end + """) - assert_expansion_env(""" - defmodule Elixir.Abc.Some do + assert_expansion_env(""" + defmodule Abc.Some do + defmodule Elixir.Child do foo = 1 end - """) + end + """) + end - assert_expansion_env(""" - defmodule Abc.Some do - defmodule Child do - foo = 1 + test "context local macro" do + # TODO this does not expand the macro + assert_expansion_env(""" + defmodule Abc do + defmacro foo(x) do + quote do + unquote(x) + 1 end end - """) - assert_expansion_env(""" - defmodule Abc.Some do - defmodule Elixir.Child do - foo = 1 - end + def go(z) do + foo(z) end - """) end + """) + end - test "context local macro" do - # TODO this does not expand the macro - assert_expansion_env(""" - defmodule Abc do - defmacro foo(x) do - quote do - unquote(x) + 1 - end - end - - def go(z) do - foo(z) + test "context remote macro" do + # TODO this does not expand the macro + assert_expansion_env(""" + defmodule Abc do + defmacro foo(x) do + quote do + unquote(x) + 1 end end - """) end - test "context remote macro" do - # TODO this does not expand the macro - assert_expansion_env(""" - defmodule Abc do - defmacro foo(x) do - quote do - unquote(x) + 1 - end - end - end - - defmodule Cde do - require Abc - def go(z) do - Abc.foo(z) - end + defmodule Cde do + require Abc + def go(z) do + Abc.foo(z) end - """) end + """) + end - test "def" do - ast = - Code.string_to_quoted( - """ - defmodule Abc do - def abc, do: :ok - end - """, - columns: true, - token_metadata: true - ) + test "def" do + ast = + Code.string_to_quoted( + """ + defmodule Abc do + def abc, do: :ok + end + """, + columns: true, + token_metadata: true + ) - {_expanded, _state, _env} = - Compiler.expand(ast, %State{}, %{Compiler.env() | module: Foo}) + {_expanded, _state, _env} = + Compiler.expand(ast, %State{}, %{Compiler.env() | module: Foo}) - # elixir_env = %{:elixir_env.new() | module: Foo} - # {elixir_expanded, _elixir_state, elixir_env} = :elixir_expand.expand(ast, :elixir_env.env_to_ex(elixir_env), elixir_env) + # elixir_env = %{:elixir_env.new() | module: Foo} + # {elixir_expanded, _elixir_state, elixir_env} = :elixir_expand.expand(ast, :elixir_env.env_to_ex(elixir_env), elixir_env) - # assert expanded == elixir_expanded - # assert env == elixir_env - end + # assert expanded == elixir_expanded + # assert env == elixir_env end + end - defmodule Foo do - defguard my(a) when is_integer(a) and a > 1 + defmodule Foo do + defguard my(a) when is_integer(a) and a > 1 - defmacro aaa(a) do - quote do - is_integer(unquote(a)) and unquote(a) > 1 - end + defmacro aaa(a) do + quote do + is_integer(unquote(a)) and unquote(a) > 1 end end + end - test "guard" do - code = """ - require ElixirSense.Core.CompilerTest.Foo, as: Foo - Foo.my(5) - """ - - assert_expansion(code) - end + test "guard" do + code = """ + require ElixirSense.Core.CompilerTest.Foo, as: Foo + Foo.my(5) + """ - test "macro" do - code = """ - require ElixirSense.Core.CompilerTest.Foo, as: Foo - Foo.aaa(5) - """ + assert_expansion(code) + end - assert_expansion(code) - end + test "macro" do + code = """ + require ElixirSense.Core.CompilerTest.Foo, as: Foo + Foo.aaa(5) + """ - defp clean_capture_arg(ast) do - {ast, _} = - Macro.prewalk(ast, nil, fn - {{:., dot_meta, target}, call_meta, args}, state -> - dot_meta = Keyword.delete(dot_meta, :column_correction) - {{{:., dot_meta, target}, call_meta, args}, state} + assert_expansion(code) + end - {atom, meta, nil} = node, state when is_atom(atom) -> - # elixir changes the name to capture and does different counter tracking - node = - with "&" <> int <- to_string(atom), {_, ""} <- Integer.parse(int) do - meta = Keyword.delete(meta, :counter) - {:capture, meta, nil} - else - _ -> node - end + defp clean_capture_arg(ast) do + {ast, _} = + Macro.prewalk(ast, nil, fn + {{:., dot_meta, target}, call_meta, args}, state -> + dot_meta = Keyword.delete(dot_meta, :column_correction) + {{{:., dot_meta, target}, call_meta, args}, state} + + {atom, meta, nil} = node, state when is_atom(atom) -> + # elixir changes the name to capture and does different counter tracking + node = + with "&" <> int <- to_string(atom), {_, ""} <- Integer.parse(int) do + meta = Keyword.delete(meta, :counter) + {:capture, meta, nil} + else + _ -> node + end - {node, state} + {node, state} - node, state -> - {node, state} - end) + node, state -> + {node, state} + end) - ast - end + ast + end - defp clean_capture_arg_elixir(ast) do - {ast, _} = - Macro.prewalk(ast, nil, fn - {:capture, meta, nil} = _node, state -> - # elixir changes the name to capture and does different counter tracking - meta = Keyword.delete(meta, :counter) - {{:capture, meta, nil}, state} + defp clean_capture_arg_elixir(ast) do + {ast, _} = + Macro.prewalk(ast, nil, fn + {:capture, meta, nil} = _node, state -> + # elixir changes the name to capture and does different counter tracking + meta = Keyword.delete(meta, :counter) + {{:capture, meta, nil}, state} - node, state -> - {node, state} - end) + node, state -> + {node, state} + end) - ast - end + ast end end diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index 4ecc239f..8e88f450 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -1,4 +1,3 @@ -if Version.match?(System.version(), ">= 1.15.0") do defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do use ExUnit.Case, async: true @@ -79,7 +78,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) + end end test "cursor in clause guard call" do @@ -99,7 +100,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) + end end test "cursor in clause right side after expressions" do @@ -111,7 +114,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) + end end if Version.match?(System.version(), "< 1.14.0") do @@ -173,7 +178,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) + end end test "cursor in clause right side" do @@ -183,7 +190,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) + end end test "cursor in clause right side after expressions" do @@ -195,7 +204,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) + end end if Version.match?(System.version(), "< 1.14.0") do @@ -268,7 +279,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) + end end test "cursor in clause left side guard call" do @@ -288,7 +301,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) + end end test "cursor in after clause left side" do @@ -422,7 +437,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) + end end test "cursor in left side of catch clause" do @@ -447,7 +464,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) + end end if Version.match?(System.version(), ">= 1.17.0") do @@ -487,7 +506,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) + end end if Version.match?(System.version(), "< 1.14.0") do @@ -526,7 +547,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) + end end test "cursor in right side of else clause" do @@ -538,7 +561,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) + end end test "cursor in after block" do @@ -582,7 +607,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) + end end test "cursor in match expressions - right side" do @@ -592,7 +619,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) + end end test "cursor in match expressions - right side next expression" do @@ -636,7 +665,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) + end end test "cursor in else clause right side" do @@ -648,7 +679,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) + end end end @@ -679,7 +712,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) + end end test "cursor in generator match expression right side" do @@ -689,7 +724,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) + end end test "cursor in generator match expressions bitstring" do @@ -708,7 +745,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) + end end test "cursor in generator match expression right side bitstring" do @@ -789,7 +828,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert {meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) + if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :y)) + end end if Version.match?(System.version(), ">= 1.17.0") do @@ -814,7 +855,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert {meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) + if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :y)) + end end if Version.match?(System.version(), "< 1.14.0") do @@ -848,7 +891,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert {meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) + if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :y)) + end end end @@ -863,7 +908,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) + end end test "default args in clause" do @@ -874,7 +921,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) + end end test "incomplete clause left side" do @@ -884,7 +933,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) + end end if Version.match?(System.version(), ">= 1.17.0") do @@ -907,7 +958,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) + end end end @@ -2051,7 +2104,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert env.typespec == {:__unknown__, 0} + end end test "in spec name" do @@ -2061,7 +2116,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert env.typespec == {:__unknown__, 0} + end end test "in type after ::" do @@ -2071,7 +2128,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert env.typespec == {:foo, 0} + end end test "in spec after ::" do @@ -2081,7 +2140,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert env.typespec == {:foo, 0} + end end test "in type after :: type" do @@ -2091,7 +2152,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert env.typespec == {:foo, 0} + end end test "in type after :: type with | empty" do @@ -2101,7 +2164,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert env.typespec == {:foo, 0} + end end test "in type after :: type with |" do @@ -2111,7 +2176,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert env.typespec == {:foo, 0} + end end test "in type after :: type with fun" do @@ -2243,7 +2310,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert env.typespec == {:foo, 0} + end end test "in type after :: remote type" do @@ -2253,7 +2322,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert env.typespec == {:foo, 0} + end end test "in type after :: type args" do @@ -2353,7 +2424,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert env.typespec == {:foo, 1} + end end test "in spec when after :" do @@ -2423,7 +2496,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert env.typespec == {:__required__, 1} + end end test "in type list" do @@ -2483,7 +2558,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert env.typespec == {:foo, 0} + end end test "in type bitstring" do @@ -2533,7 +2610,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert env.typespec == {:foo, 0} + end end test "in type struct {}" do @@ -2563,7 +2642,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert env.typespec == {:foo, 0} + end end test "type with underscored arg" do @@ -2573,7 +2654,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert env.typespec == {:foo, 1} + end end end @@ -2595,7 +2678,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert env.function == {:__unknown__, 0} + end end test "in def args" do @@ -2635,7 +2720,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert env.function == {:__unknown__, 0} + end end test "in def after," do @@ -2676,8 +2763,10 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert env.function == {:foo, 1} assert env.context == :guard + end end test "in def guard variable" do @@ -2687,8 +2776,10 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do assert env.function == {:foo, 1} assert env.context == :guard + end end test "in def after block" do @@ -2832,4 +2923,3 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert env.module == nil end end -end From 26d4e87cf462a1116a839706da9633461c207c12 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 28 Sep 2024 13:23:17 +0200 Subject: [PATCH 214/235] exclude tests on < 1.14 --- test/elixir_sense/core/compiler_test.exs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/elixir_sense/core/compiler_test.exs b/test/elixir_sense/core/compiler_test.exs index 862dcde9..5bc2c723 100644 --- a/test/elixir_sense/core/compiler_test.exs +++ b/test/elixir_sense/core/compiler_test.exs @@ -557,6 +557,7 @@ defmodule ElixirSense.Core.CompilerTest do """) end + if Version.match?(System.version(), ">= 1.14.0") do test "expands for" do assert_expansion(""" for i <- [1, 2, 3] do @@ -568,6 +569,7 @@ defmodule ElixirSense.Core.CompilerTest do for i <- [1, 2, 3], j <- [1, 2], true, into: %{}, do: {i, j} """) end + end if Version.match?(System.version(), ">= 1.15.0") do test "expands for with bitstring generator" do @@ -587,6 +589,7 @@ defmodule ElixirSense.Core.CompilerTest do end end + if Version.match?(System.version(), ">= 1.14.0") do test "expands for in block" do assert_expansion(""" for i <- [1, 2, 3] do @@ -609,6 +612,7 @@ defmodule ElixirSense.Core.CompilerTest do :ok """) end + end test "expands with" do assert_expansion(""" From 67b22e2ad349124aa7e44f733e82a3ecbd5d4dfb Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 30 Sep 2024 23:05:32 +0200 Subject: [PATCH 215/235] simplify current_env --- lib/elixir_sense/core/metadata.ex | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/elixir_sense/core/metadata.ex b/lib/elixir_sense/core/metadata.ex index b59719e2..2c58ca6c 100644 --- a/lib/elixir_sense/core/metadata.ex +++ b/lib/elixir_sense/core/metadata.ex @@ -66,21 +66,31 @@ defmodule ElixirSense.Core.Metadata do } end + def get_cursor_env( + metadata, + position, + surround \\ nil + ) + + def get_cursor_env( + %__MODULE__{cursor_env: cursor_env}, _, _) when cursor_env != nil do + cursor_env |> elem(1) + end + def get_cursor_env( %__MODULE__{} = metadata, {line, column}, - surround \\ nil + surround ) do {prefix, source_with_cursor} = case surround do {{begin_line, begin_column}, {end_line, end_column}} -> - [prefix, needle, suffix] = + [prefix, suffix] = ElixirSense.Core.Source.split_at(metadata.source, [ - {begin_line, begin_column}, - {end_line, end_column} + {begin_line, begin_column} ]) - source_with_cursor = prefix <> "__cursor__(#{needle})" <> suffix + source_with_cursor = prefix <> "__cursor__();" <> suffix {prefix, source_with_cursor} @@ -90,7 +100,7 @@ defmodule ElixirSense.Core.Metadata do {line, column} ]) - source_with_cursor = prefix <> "__cursor__()" <> suffix + source_with_cursor = prefix <> "__cursor__();" <> suffix {prefix, source_with_cursor} end From 986781b05e2606512e6c0f913a308685d1c9eda3 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 30 Sep 2024 23:15:01 +0200 Subject: [PATCH 216/235] format --- lib/elixir_sense/core/compiler.ex | 14 +- lib/elixir_sense/core/metadata.ex | 10 +- test/elixir_sense/core/compiler_test.exs | 112 ++++++++------ .../metadata_builder/error_recovery_test.exs | 142 ++++++++++++------ .../core/metadata_builder_test.exs | 86 +++++------ test/elixir_sense/core/parser_test.exs | 44 +++--- 6 files changed, 237 insertions(+), 171 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 5e8b5a48..41c1006c 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1747,11 +1747,12 @@ defmodule ElixirSense.Core.Compiler do expanded_arg end) - prematch = if Version.match?(System.version(), ">= 1.15.0-dev") do - Code.get_compiler_option(:on_undefined_variable) - else - :warn - end + prematch = + if Version.match?(System.version(), ">= 1.15.0-dev") do + Code.get_compiler_option(:on_undefined_variable) + else + :warn + end {e_guard, state, env_for_expand} = __MODULE__.Clauses.guard( @@ -2528,10 +2529,11 @@ defmodule ElixirSense.Core.Compiler do end if Version.match?(System.version(), ">= 1.15.0-dev") do - @internals [{:behaviour_info, 1}, {:module_info, 1}, {:module_info, 0}] + @internals [{:behaviour_info, 1}, {:module_info, 1}, {:module_info, 0}] else @internals [{:module_info, 1}, {:module_info, 0}] end + defp import_info_callback(module, state) do fn kind -> if Map.has_key?(state.mods_funs_to_positions, {module, nil, nil}) do diff --git a/lib/elixir_sense/core/metadata.ex b/lib/elixir_sense/core/metadata.ex index 2c58ca6c..60041b9c 100644 --- a/lib/elixir_sense/core/metadata.ex +++ b/lib/elixir_sense/core/metadata.ex @@ -73,9 +73,13 @@ defmodule ElixirSense.Core.Metadata do ) def get_cursor_env( - %__MODULE__{cursor_env: cursor_env}, _, _) when cursor_env != nil do - cursor_env |> elem(1) - end + %__MODULE__{cursor_env: cursor_env}, + _, + _ + ) + when cursor_env != nil do + cursor_env |> elem(1) + end def get_cursor_env( %__MODULE__{} = metadata, diff --git a/test/elixir_sense/core/compiler_test.exs b/test/elixir_sense/core/compiler_test.exs index 5bc2c723..21aae705 100644 --- a/test/elixir_sense/core/compiler_test.exs +++ b/test/elixir_sense/core/compiler_test.exs @@ -20,15 +20,15 @@ defmodule ElixirSense.Core.CompilerTest do ) defp elixir_ex_to_map( - elixir_ex( - caller: caller, - prematch: prematch, - stacktrace: stacktrace, - unused: {_, unused}, - runtime_modules: runtime_modules, - vars: vars - ) - ) do + elixir_ex( + caller: caller, + prematch: prematch, + stacktrace: stacktrace, + unused: {_, unused}, + runtime_modules: runtime_modules, + vars: vars + ) + ) do %{ caller: caller, prematch: prematch, @@ -55,14 +55,14 @@ defmodule ElixirSense.Core.CompilerTest do ) defp elixir_ex_to_map( - elixir_ex( - caller: caller, - prematch: prematch, - stacktrace: stacktrace, - unused: {_, unused}, - vars: vars - ) - ) do + elixir_ex( + caller: caller, + prematch: prematch, + stacktrace: stacktrace, + unused: {_, unused}, + vars: vars + ) + ) do %{ caller: caller, prematch: prematch, @@ -143,7 +143,9 @@ defmodule ElixirSense.Core.CompilerTest do test "initial" do elixir_env = :elixir_env.new() assert Compiler.env() == elixir_env - assert state_to_map(state_with_prematch()) == elixir_ex_to_map(:elixir_env.env_to_ex(elixir_env)) + + assert state_to_map(state_with_prematch()) == + elixir_ex_to_map(:elixir_env.env_to_ex(elixir_env)) end describe "special forms" do @@ -224,7 +226,10 @@ defmodule ElixirSense.Core.CompilerTest do test "expands __MODULE__" do ast = {:__MODULE__, [], nil} - {expanded, state, env} = Compiler.expand(ast, state_with_prematch(), %{Compiler.env() | module: Foo}) + + {expanded, state, env} = + Compiler.expand(ast, state_with_prematch(), %{Compiler.env() | module: Foo}) + elixir_env = %{:elixir_env.new() | module: Foo} {elixir_expanded, elixir_state, elixir_env} = @@ -253,7 +258,10 @@ defmodule ElixirSense.Core.CompilerTest do test "expands __CALLER__" do ast = {:__CALLER__, [], nil} - {expanded, state, env} = Compiler.expand(ast, %State{state_with_prematch() | caller: true}, Compiler.env()) + + {expanded, state, env} = + Compiler.expand(ast, %State{state_with_prematch() | caller: true}, Compiler.env()) + elixir_env = :elixir_env.new() {elixir_expanded, elixir_state, elixir_env} = @@ -270,7 +278,10 @@ defmodule ElixirSense.Core.CompilerTest do test "expands __STACKTRACE__" do ast = {:__STACKTRACE__, [], nil} - {expanded, state, env} = Compiler.expand(ast, %State{state_with_prematch() | stacktrace: true}, Compiler.env()) + + {expanded, state, env} = + Compiler.expand(ast, %State{state_with_prematch() | stacktrace: true}, Compiler.env()) + elixir_env = :elixir_env.new() {elixir_expanded, elixir_state, elixir_env} = @@ -303,6 +314,7 @@ defmodule ElixirSense.Core.CompilerTest do test "expands __ENV__.property" do assert_expansion("__ENV__.requires") + if Version.match?(System.version(), ">= 1.15.0") do # elixir 1.14 returns fields in different order # we don't test that as the code is invalid anyway @@ -558,17 +570,17 @@ defmodule ElixirSense.Core.CompilerTest do end if Version.match?(System.version(), ">= 1.14.0") do - test "expands for" do - assert_expansion(""" - for i <- [1, 2, 3] do - i - end - """) + test "expands for" do + assert_expansion(""" + for i <- [1, 2, 3] do + i + end + """) - assert_expansion(""" - for i <- [1, 2, 3], j <- [1, 2], true, into: %{}, do: {i, j} - """) - end + assert_expansion(""" + for i <- [1, 2, 3], j <- [1, 2], true, into: %{}, do: {i, j} + """) + end end if Version.match?(System.version(), ">= 1.15.0") do @@ -590,28 +602,28 @@ defmodule ElixirSense.Core.CompilerTest do end if Version.match?(System.version(), ">= 1.14.0") do - test "expands for in block" do - assert_expansion(""" - for i <- [1, 2, 3] do - i - end - :ok - """) + test "expands for in block" do + assert_expansion(""" + for i <- [1, 2, 3] do + i + end + :ok + """) - assert_expansion(""" - for i <- [1, 2, 3], uniq: true do - i - end - :ok - """) + assert_expansion(""" + for i <- [1, 2, 3], uniq: true do + i + end + :ok + """) - assert_expansion(""" - _ = for i <- [1, 2, 3] do - i + assert_expansion(""" + _ = for i <- [1, 2, 3] do + i + end + :ok + """) end - :ok - """) - end end test "expands with" do diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index 8e88f450..545f4041 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -78,8 +78,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert Enum.any?(env.vars, &(&1.name == :x)) + assert Enum.any?(env.vars, &(&1.name == :x)) end end @@ -100,8 +101,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert Enum.any?(env.vars, &(&1.name == :x)) + assert Enum.any?(env.vars, &(&1.name == :x)) end end @@ -114,8 +116,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert Enum.any?(env.vars, &(&1.name == :x)) + assert Enum.any?(env.vars, &(&1.name == :x)) end end @@ -178,8 +181,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert Enum.any?(env.vars, &(&1.name == :x)) + assert Enum.any?(env.vars, &(&1.name == :x)) end end @@ -190,8 +194,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert Enum.any?(env.vars, &(&1.name == :x)) + assert Enum.any?(env.vars, &(&1.name == :x)) end end @@ -204,8 +209,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert Enum.any?(env.vars, &(&1.name == :x)) + assert Enum.any?(env.vars, &(&1.name == :x)) end end @@ -279,8 +285,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert Enum.any?(env.vars, &(&1.name == :x)) + assert Enum.any?(env.vars, &(&1.name == :x)) end end @@ -301,8 +308,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert Enum.any?(env.vars, &(&1.name == :x)) + assert Enum.any?(env.vars, &(&1.name == :x)) end end @@ -437,8 +445,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert Enum.any?(env.vars, &(&1.name == :x)) + assert Enum.any?(env.vars, &(&1.name == :x)) end end @@ -464,8 +473,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert Enum.any?(env.vars, &(&1.name == :x)) + assert Enum.any?(env.vars, &(&1.name == :x)) end end @@ -506,8 +516,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert Enum.any?(env.vars, &(&1.name == :x)) + assert Enum.any?(env.vars, &(&1.name == :x)) end end @@ -547,8 +558,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert Enum.any?(env.vars, &(&1.name == :x)) + assert Enum.any?(env.vars, &(&1.name == :x)) end end @@ -561,8 +573,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert Enum.any?(env.vars, &(&1.name == :x)) + assert Enum.any?(env.vars, &(&1.name == :x)) end end @@ -607,8 +620,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert Enum.any?(env.vars, &(&1.name == :x)) + assert Enum.any?(env.vars, &(&1.name == :x)) end end @@ -619,8 +633,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert Enum.any?(env.vars, &(&1.name == :x)) + assert Enum.any?(env.vars, &(&1.name == :x)) end end @@ -665,8 +680,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert Enum.any?(env.vars, &(&1.name == :x)) + assert Enum.any?(env.vars, &(&1.name == :x)) end end @@ -679,8 +695,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert Enum.any?(env.vars, &(&1.name == :x)) + assert Enum.any?(env.vars, &(&1.name == :x)) end end end @@ -712,8 +729,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert Enum.any?(env.vars, &(&1.name == :x)) + assert Enum.any?(env.vars, &(&1.name == :x)) end end @@ -724,8 +742,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert Enum.any?(env.vars, &(&1.name == :x)) + assert Enum.any?(env.vars, &(&1.name == :x)) end end @@ -745,8 +764,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert Enum.any?(env.vars, &(&1.name == :x)) + assert Enum.any?(env.vars, &(&1.name == :x)) end end @@ -828,8 +848,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert {meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) + if Version.match?(System.version(), ">= 1.15.0") do - assert Enum.any?(env.vars, &(&1.name == :y)) + assert Enum.any?(env.vars, &(&1.name == :y)) end end @@ -855,8 +876,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert {meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) + if Version.match?(System.version(), ">= 1.15.0") do - assert Enum.any?(env.vars, &(&1.name == :y)) + assert Enum.any?(env.vars, &(&1.name == :y)) end end @@ -891,8 +913,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do assert {meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) + if Version.match?(System.version(), ">= 1.15.0") do - assert Enum.any?(env.vars, &(&1.name == :y)) + assert Enum.any?(env.vars, &(&1.name == :y)) end end end @@ -908,8 +931,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert Enum.any?(env.vars, &(&1.name == :x)) + assert Enum.any?(env.vars, &(&1.name == :x)) end end @@ -921,8 +945,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert Enum.any?(env.vars, &(&1.name == :x)) + assert Enum.any?(env.vars, &(&1.name == :x)) end end @@ -933,8 +958,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert Enum.any?(env.vars, &(&1.name == :x)) + assert Enum.any?(env.vars, &(&1.name == :x)) end end @@ -958,8 +984,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {meta, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert Enum.any?(env.vars, &(&1.name == :x)) + assert Enum.any?(env.vars, &(&1.name == :x)) end end end @@ -2104,8 +2131,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert env.typespec == {:__unknown__, 0} + assert env.typespec == {:__unknown__, 0} end end @@ -2116,8 +2144,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert env.typespec == {:__unknown__, 0} + assert env.typespec == {:__unknown__, 0} end end @@ -2128,8 +2157,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert env.typespec == {:foo, 0} + assert env.typespec == {:foo, 0} end end @@ -2140,8 +2170,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert env.typespec == {:foo, 0} + assert env.typespec == {:foo, 0} end end @@ -2152,8 +2183,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert env.typespec == {:foo, 0} + assert env.typespec == {:foo, 0} end end @@ -2164,8 +2196,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert env.typespec == {:foo, 0} + assert env.typespec == {:foo, 0} end end @@ -2176,8 +2209,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert env.typespec == {:foo, 0} + assert env.typespec == {:foo, 0} end end @@ -2310,8 +2344,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert env.typespec == {:foo, 0} + assert env.typespec == {:foo, 0} end end @@ -2322,8 +2357,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert env.typespec == {:foo, 0} + assert env.typespec == {:foo, 0} end end @@ -2424,8 +2460,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert env.typespec == {:foo, 1} + assert env.typespec == {:foo, 1} end end @@ -2496,8 +2533,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert env.typespec == {:__required__, 1} + assert env.typespec == {:__required__, 1} end end @@ -2558,8 +2596,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert env.typespec == {:foo, 0} + assert env.typespec == {:foo, 0} end end @@ -2610,8 +2649,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert env.typespec == {:foo, 0} + assert env.typespec == {:foo, 0} end end @@ -2642,8 +2682,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert env.typespec == {:foo, 0} + assert env.typespec == {:foo, 0} end end @@ -2654,8 +2695,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert env.typespec == {:foo, 1} + assert env.typespec == {:foo, 1} end end end @@ -2678,8 +2720,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert env.function == {:__unknown__, 0} + assert env.function == {:__unknown__, 0} end end @@ -2720,8 +2763,9 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert env.function == {:__unknown__, 0} + assert env.function == {:__unknown__, 0} end end @@ -2763,9 +2807,10 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert env.function == {:foo, 1} - assert env.context == :guard + assert env.function == {:foo, 1} + assert env.context == :guard end end @@ -2776,9 +2821,10 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do """ assert {_, env} = get_cursor_env(code) + if Version.match?(System.version(), ">= 1.15.0") do - assert env.function == {:foo, 1} - assert env.context == :guard + assert env.function == {:foo, 1} + assert env.context == :guard end end diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index ef4f286b..91afbb0c 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -1672,11 +1672,12 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - if Version.match?(System.version(), "< 1.15.0") do - assert [%VarInfo{type: {:intersection, [{:atom, :my_var}, {:local_call, :x, []}]}}] = state |> get_line_vars(3) - else - assert [%VarInfo{type: {:atom, :my_var}}] = state |> get_line_vars(3) - end + if Version.match?(System.version(), "< 1.15.0") do + assert [%VarInfo{type: {:intersection, [{:atom, :my_var}, {:local_call, :x, []}]}}] = + state |> get_line_vars(3) + else + assert [%VarInfo{type: {:atom, :my_var}}] = state |> get_line_vars(3) + end end test "variable binding simple case match context reverse order" do @@ -1690,9 +1691,10 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state if Version.match?(System.version(), "< 1.15.0") do - assert [%VarInfo{type: {:intersection, [{:atom, :my_var}, {:local_call, :x, []}]}}] = state |> get_line_vars(3) + assert [%VarInfo{type: {:intersection, [{:atom, :my_var}, {:local_call, :x, []}]}}] = + state |> get_line_vars(3) else - assert [%VarInfo{type: {:atom, :my_var}}] = state |> get_line_vars(3) + assert [%VarInfo{type: {:atom, :my_var}}] = state |> get_line_vars(3) end end @@ -2162,11 +2164,11 @@ defmodule ElixirSense.Core.MetadataBuilderTest do %VarInfo{name: :var5, type: {:local_call, :now, [{:atom, :abc}, {:integer, 5}]}} ] = state |> get_line_vars(16) - if Version.match?(System.version(), "< 1.15.0") do - assert maybe_local_call == {:local_call, :now, []} - else - assert maybe_local_call == nil - end + if Version.match?(System.version(), "< 1.15.0") do + assert maybe_local_call == {:local_call, :now, []} + else + assert maybe_local_call == nil + end assert [ %VarInfo{name: :abc, type: nil}, @@ -7417,15 +7419,15 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert %{ - 4 => [ - %CallInfo{arity: 1, func: :func, position: {4, 11}, mod: {:attribute, :attr}}, - _ - ], - 5 => [ - %CallInfo{arity: 1, func: :func, position: {5, 9}, mod: {:variable, :var, 0}} - ] - } = state.calls + assert %{ + 4 => [ + %CallInfo{arity: 1, func: :func, position: {4, 11}, mod: {:attribute, :attr}}, + _ + ], + 5 => [ + %CallInfo{arity: 1, func: :func, position: {5, 9}, mod: {:variable, :var, 0}} + ] + } = state.calls end test "registers calls on attribute and var without args" do @@ -7441,21 +7443,21 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - Enum.any?( - state.calls[4], - &match?( - %CallInfo{arity: 0, func: :func, position: {4, 11}, mod: {:attribute, :attr}}, - &1 - ) + Enum.any?( + state.calls[4], + &match?( + %CallInfo{arity: 0, func: :func, position: {4, 11}, mod: {:attribute, :attr}}, + &1 ) + ) - Enum.any?( - state.calls[4], - &match?( - %CallInfo{arity: 0, func: :func, position: {5, 9}, mod: {:variable, :var, 0}}, - &1 - ) + Enum.any?( + state.calls[4], + &match?( + %CallInfo{arity: 0, func: :func, position: {5, 9}, mod: {:variable, :var, 0}}, + &1 ) + ) end test "registers calls on attribute and var anonymous" do @@ -7471,15 +7473,15 @@ defmodule ElixirSense.Core.MetadataBuilderTest do """ |> string_to_state - assert %{ - 4 => [ - %CallInfo{arity: 0, func: {:attribute, :attr}, position: {4, 11}, mod: nil}, - _ - ], - 5 => [ - %CallInfo{arity: 0, func: {:variable, :var, 0}, position: {5, 9}, mod: nil} - ] - } = state.calls + assert %{ + 4 => [ + %CallInfo{arity: 0, func: {:attribute, :attr}, position: {4, 11}, mod: nil}, + _ + ], + 5 => [ + %CallInfo{arity: 0, func: {:variable, :var, 0}, position: {5, 9}, mod: nil} + ] + } = state.calls end test "registers calls pipe with __MODULE__ operator no parens" do diff --git a/test/elixir_sense/core/parser_test.exs b/test/elixir_sense/core/parser_test.exs index 8deb5a03..70cffa13 100644 --- a/test/elixir_sense/core/parser_test.exs +++ b/test/elixir_sense/core/parser_test.exs @@ -113,9 +113,9 @@ defmodule ElixirSense.Core.ParserTest do error: {:error, :parse_error} }, %Env{functions: functions}} = parse(source, {3, 10}) - if Version.match?(System.version(), ">= 1.15.0") do - assert Keyword.has_key?(functions, List) - end + if Version.match?(System.version(), ">= 1.15.0") do + assert Keyword.has_key?(functions, List) + end end test "parse_string with missing terminator \"\'\"" do @@ -130,9 +130,9 @@ defmodule ElixirSense.Core.ParserTest do error: {:error, :parse_error} }, %Env{functions: functions}} = parse(source, {3, 10}) - if Version.match?(System.version(), ">= 1.15.0") do - assert Keyword.has_key?(functions, List) - end + if Version.match?(System.version(), ">= 1.15.0") do + assert Keyword.has_key?(functions, List) + end end test "parse_string with missing heredoc terminator" do @@ -208,12 +208,12 @@ defmodule ElixirSense.Core.ParserTest do {_metadata, env} = parse(source, {3, 23}) if Version.match?(System.version(), ">= 1.15.0") do - assert %Env{ - vars: [ - %VarInfo{name: :x} - ] - } = env - end + assert %Env{ + vars: [ + %VarInfo{name: :x} + ] + } = env + end end test "parse_string with missing terminator \"end\" attempts to insert `end` at correct indentation" do @@ -365,16 +365,16 @@ defmodule ElixirSense.Core.ParserTest do vars: vars }} = parse(source, {5, 14}) - if Version.match?(System.version(), "< 1.15.0") do - # container_cursor_to_quoted removes function body - assert [ - %ElixirSense.Core.State.VarInfo{name: :y} - ] = Enum.sort(vars) - else - assert [ - %ElixirSense.Core.State.VarInfo{name: :x}, - %ElixirSense.Core.State.VarInfo{name: :y} - ] = Enum.sort(vars) + if Version.match?(System.version(), "< 1.15.0") do + # container_cursor_to_quoted removes function body + assert [ + %ElixirSense.Core.State.VarInfo{name: :y} + ] = Enum.sort(vars) + else + assert [ + %ElixirSense.Core.State.VarInfo{name: :x}, + %ElixirSense.Core.State.VarInfo{name: :y} + ] = Enum.sort(vars) end end From d64014597757d962dc15a4a63851d6c5f4f82898 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Mon, 30 Sep 2024 23:37:42 +0200 Subject: [PATCH 217/235] Revert "simplify current_env" This reverts commit 67b22e2ad349124aa7e44f733e82a3ecbd5d4dfb. # Conflicts: # lib/elixir_sense/core/metadata.ex --- lib/elixir_sense/core/metadata.ex | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/lib/elixir_sense/core/metadata.ex b/lib/elixir_sense/core/metadata.ex index 60041b9c..b59719e2 100644 --- a/lib/elixir_sense/core/metadata.ex +++ b/lib/elixir_sense/core/metadata.ex @@ -66,35 +66,21 @@ defmodule ElixirSense.Core.Metadata do } end - def get_cursor_env( - metadata, - position, - surround \\ nil - ) - - def get_cursor_env( - %__MODULE__{cursor_env: cursor_env}, - _, - _ - ) - when cursor_env != nil do - cursor_env |> elem(1) - end - def get_cursor_env( %__MODULE__{} = metadata, {line, column}, - surround + surround \\ nil ) do {prefix, source_with_cursor} = case surround do {{begin_line, begin_column}, {end_line, end_column}} -> - [prefix, suffix] = + [prefix, needle, suffix] = ElixirSense.Core.Source.split_at(metadata.source, [ - {begin_line, begin_column} + {begin_line, begin_column}, + {end_line, end_column} ]) - source_with_cursor = prefix <> "__cursor__();" <> suffix + source_with_cursor = prefix <> "__cursor__(#{needle})" <> suffix {prefix, source_with_cursor} @@ -104,7 +90,7 @@ defmodule ElixirSense.Core.Metadata do {line, column} ]) - source_with_cursor = prefix <> "__cursor__();" <> suffix + source_with_cursor = prefix <> "__cursor__()" <> suffix {prefix, source_with_cursor} end From 46546b2f7d82640bba9c89a65a14e1e4b7dd4b1f Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 1 Oct 2024 21:57:58 +0200 Subject: [PATCH 218/235] optimize boolean --- lib/elixir_sense/core/compiler.ex | 69 ++++++++++++++++++++---- test/elixir_sense/core/compiler_test.exs | 7 ++- 2 files changed, 62 insertions(+), 14 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 41c1006c..be4e5338 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -2489,21 +2489,67 @@ defmodule ElixirSense.Core.Compiler do defp expand_case(meta, expr, opts, s, e) do {e_expr, se, ee} = expand(expr, s, e) - r_opts = opts - # if proplists.get_value(:optimize_boolean, meta, false) do - # if ElixirUtils.returns_boolean(e_expr) do - # rewrite_case_clauses(opts) - # else - # generated_case_clauses(opts) - # end - # else - # opts - # end + r_opts = + if Keyword.get(meta, :optimize_boolean, false) do + if :elixir_utils.returns_boolean(e_expr) do + rewrite_case_clauses(opts) + else + generated_case_clauses(opts) + end + else + opts + end {e_opts, so, eo} = __MODULE__.Clauses.case(e_expr, r_opts, se, ee) {{:case, meta, [e_expr, e_opts]}, so, eo} end + def rewrite_case_clauses( + do: [ + {:->, false_meta, + [ + [{:when, _, [var, {{:., _, [Kernel, :in]}, _, [var, [false, nil]]}]}], + false_expr + ]}, + {:->, true_meta, + [ + [{:_, _, _}], + true_expr + ]} + ] + ) do + rewrite_case_clauses(false_meta, false_expr, true_meta, true_expr) + end + + def rewrite_case_clauses( + do: [ + {:->, false_meta, [[false], false_expr]}, + {:->, true_meta, [[true], true_expr]} | _ + ] + ) do + rewrite_case_clauses(false_meta, false_expr, true_meta, true_expr) + end + + def rewrite_case_clauses(other) do + generated_case_clauses(other) + end + + defp rewrite_case_clauses(false_meta, false_expr, true_meta, true_expr) do + [ + do: [ + {:->, __MODULE__.Utils.generated(false_meta), [[false], false_expr]}, + {:->, __MODULE__.Utils.generated(true_meta), [[true], true_expr]} + ] + ] + end + + defp generated_case_clauses(do: clauses) do + r_clauses = + for {:->, meta, args} <- clauses, do: {:->, __MODULE__.Utils.generated(meta), args} + + [do: r_clauses] + end + def expand_arg(arg, acc, e) when is_number(arg) or is_atom(arg) or is_binary(arg) or is_pid(arg) do {arg, acc, e} @@ -2566,6 +2612,9 @@ defmodule ElixirSense.Core.Compiler do end defmodule Utils do + def generated([{:generated, true} | _] = meta), do: meta + def generated(meta), do: [{:generated, true} | meta] + def split_last([]), do: {[], []} def split_last(list), do: split_last(list, []) diff --git a/test/elixir_sense/core/compiler_test.exs b/test/elixir_sense/core/compiler_test.exs index 21aae705..c639e446 100644 --- a/test/elixir_sense/core/compiler_test.exs +++ b/test/elixir_sense/core/compiler_test.exs @@ -750,8 +750,7 @@ defmodule ElixirSense.Core.CompilerTest do end test "expands local call macro" do - # TODO - # assert_expansion("if true, do: :ok") + assert_expansion("if true, do: :ok") assert_expansion("1 |> IO.inspect") end @@ -855,7 +854,7 @@ defmodule ElixirSense.Core.CompilerTest do end test "context local macro" do - # TODO this does not expand the macro + # this does not expand the macro assert_expansion_env(""" defmodule Abc do defmacro foo(x) do @@ -872,7 +871,7 @@ defmodule ElixirSense.Core.CompilerTest do end test "context remote macro" do - # TODO this does not expand the macro + # this does not expand the macro assert_expansion_env(""" defmodule Abc do defmacro foo(x) do From f91f258b052653ce4abe1f15a812066f6f8ff071 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 1 Oct 2024 22:16:10 +0200 Subject: [PATCH 219/235] prevent type narrowing from leaking to outer scope --- lib/elixir_sense/core/state.ex | 9 +++-- .../core/metadata_builder_test.exs | 36 ++++++------------- 2 files changed, 17 insertions(+), 28 deletions(-) diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index d71f718d..d33a1b5e 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -1351,9 +1351,12 @@ defmodule ElixirSense.Core.State do ) do outer_scope_vars = for {key, _} <- outer_scope_vars, - into: %{}, - # TODO merge type and positions? - do: {key, current_scope_vars[key]} + into: %{} do + # take type from outer scope as type narrowing in inner scope is not guaranteed to + # affect outer scope + type = outer_scope_vars[key].type + {key, %{current_scope_vars[key] | type: type}} + end vars_info = [current_scope_vars, outer_scope_vars | other_scopes_vars] diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 91afbb0c..21cf3d2a 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -3598,9 +3598,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert [%VarInfo{name: :x, type: :number}] = get_line_vars(state, 8) - # TODO this type should not leak outside clause - # assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 10) - assert [%VarInfo{name: :x, type: :number}] = + assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 10) end @@ -3649,10 +3647,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do assert [%VarInfo{name: :x, type: :number}] = get_line_vars(state, 10) - # TODO this type should not leak outside clause - # assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 12) - assert [%VarInfo{name: :x, type: :number}] = - get_line_vars(state, 12) + assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 12) end test "guards in with clauses" do @@ -3693,9 +3688,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do get_line_vars(state, 8) assert [%VarInfo{name: :x, type: :number}] = get_line_vars(state, 10) - # TODO this type should not leak outside clause - # assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 12) - assert [%VarInfo{name: :x, type: :number}] = get_line_vars(state, 12) + + assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 12) end test "guards in receive clauses" do @@ -3725,9 +3719,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] = get_line_vars(state, 6) assert [%VarInfo{name: :x, type: :number}] = get_line_vars(state, 8) - # TODO this type should not leak outside clause - # assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 10) - assert [%VarInfo{name: :x, type: :number}] = get_line_vars(state, 10) + + assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 10) end test "guards in for generator clauses" do @@ -3764,9 +3757,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do %VarInfo{name: :y, type: {:for_expression, {:variable, :a, 1}}} ] = get_line_vars(state, 5) - # TODO this type should not leak outside clause - # assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 7) - assert [%VarInfo{name: :x, type: :number}] = get_line_vars(state, 7) + assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 7) end test "guards in for aggregate clauses" do @@ -3808,9 +3799,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do %VarInfo{name: :x, type: :number} ] = get_line_vars(state, 10) - # TODO this type should not leak outside clause - # assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 12) - assert [%VarInfo{name: :x, type: :number}] = get_line_vars(state, 12) + assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 12) end test "guards in try clauses" do @@ -3850,9 +3839,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] = get_line_vars(state, 11) assert [%VarInfo{name: :x, type: :number}] = get_line_vars(state, 13) - # TODO this type should not leak outside clause - # assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 15) - assert [%VarInfo{name: :x, type: :number}] = get_line_vars(state, 15) + + assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 15) end test "guards in fn clauses" do @@ -3889,9 +3877,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] = get_line_vars(state, 8) assert [%VarInfo{name: :x, type: :number}] = get_line_vars(state, 10) - # TODO this type should not leak outside clause - # assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 12) - assert [%VarInfo{name: :x, type: :number}] = get_line_vars(state, 12) + assert [%VarInfo{name: :x, type: nil}] = get_line_vars(state, 12) end test "number guards" do From 057a5783c6c204b8969e0238430d4feeedd4c132 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 1 Oct 2024 22:27:46 +0200 Subject: [PATCH 220/235] fix warnings --- lib/elixir_sense/core/compiler.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index be4e5338..dc2c6f1f 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -861,7 +861,7 @@ defmodule ElixirSense.Core.Compiler do expand_defaults(args, state, %{env_for_expand | context: nil}, [], []) # based on :elixir_clauses.def - {e_args_no_defaults, state, env_for_expand} = + {e_args_no_defaults, state, _env_for_expand} = expand_args(args_no_defaults, %{state | prematch: {%{}, 0, :none}}, %{ env_for_expand | context: :match @@ -1959,7 +1959,7 @@ defmodule ElixirSense.Core.Compiler do end end - defp alias_defmodule({:__aliases__, _, [:"Elixir", _ | _] = x}, module, env), do: {module, env} + defp alias_defmodule({:__aliases__, _, [:"Elixir", _ | _]}, module, env), do: {module, env} # defmodule Alias in root defp alias_defmodule({:__aliases__, _, _}, module, %{module: nil} = env), @@ -4858,7 +4858,7 @@ defmodule ElixirSense.Core.Compiler do :elixir_rewrite.guard_rewrite(receiver, dot_meta, right, meta, e_args) end else - defp do_rewrite(:guard, receiver, dot_meta, right, meta, e_args, s) do + defp do_rewrite(:guard, receiver, dot_meta, right, meta, e_args, _s) do # elixir uses guard context for error messages :elixir_rewrite.guard_rewrite(receiver, dot_meta, right, meta, e_args, "guard") end From 749adcdbb4cd8b6b642d4e719feffddf312c5c69 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 1 Oct 2024 22:31:16 +0200 Subject: [PATCH 221/235] disable test --- test/elixir_sense/core/metadata_test.exs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/elixir_sense/core/metadata_test.exs b/test/elixir_sense/core/metadata_test.exs index 045b581b..11e2aee8 100644 --- a/test/elixir_sense/core/metadata_test.exs +++ b/test/elixir_sense/core/metadata_test.exs @@ -332,8 +332,9 @@ defmodule ElixirSense.Core.MetadataTest do env = Metadata.get_env(metadata, {49, 1}) assert env.module == Pr - assert env.function == nil - assert env.typespec == nil + # TODO this test should check cursor_env + # assert env.function == nil + # assert env.typespec == nil env = Metadata.get_env(metadata, {50, 3}) assert env.module == Pr @@ -351,12 +352,12 @@ defmodule ElixirSense.Core.MetadataTest do assert env.typespec == nil env = Metadata.get_env(metadata, {54, 3}) - assert env.module == Pr.String + assert env.module == Pr.List assert env.function == nil assert env.typespec == nil env = Metadata.get_env(metadata, {55, 3}) - assert env.module == Pr.String + assert env.module == Pr.List assert env.function == {:x, 1} assert env.typespec == nil end From 44a4987082d3c5e0ff9f706b168d4e61e051ef17 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 1 Oct 2024 22:58:19 +0200 Subject: [PATCH 222/235] reverse conditions --- .../core/metadata_builder/error_recovery_test.exs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index 545f4041..ffd47fb2 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -122,7 +122,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do end end - if Version.match?(System.version(), "< 1.14.0") do + if Version.match?(System.version(), ">= 1.14.0") do test "invalid number of args with when" do code = """ case nil do 0, z when not is_nil(z) -> \ @@ -132,7 +132,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do end end - if Version.match?(System.version(), "< 1.14.0") do + if Version.match?(System.version(), ">= 1.14.0") do test "invalid number of args" do code = """ case nil do 0, z -> \ @@ -215,7 +215,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do end end - if Version.match?(System.version(), "< 1.14.0") do + if Version.match?(System.version(), ">= 1.14.0") do test "invalid number of args" do code = """ cond do 0, z -> \ @@ -522,7 +522,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do end end - if Version.match?(System.version(), "< 1.14.0") do + if Version.match?(System.version(), ">= 1.14.0") do test "cursor in right side of catch clause 2 arg" do code = """ try do @@ -882,7 +882,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do end end - if Version.match?(System.version(), "< 1.14.0") do + if Version.match?(System.version(), ">= 1.14.0") do test "cursor in do block reduce right side of clause too many args" do code = """ for x <- [], reduce: %{} do From 1ea8683ec9198b59839b1819f1cfdf166c33929f Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 2 Oct 2024 23:35:12 +0200 Subject: [PATCH 223/235] capture TODOs addressed --- lib/elixir_sense/core/compiler.ex | 58 ++++++++++-------- .../core/metadata_builder_test.exs | 61 ++++++++++++++++++- 2 files changed, 92 insertions(+), 27 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index dc2c6f1f..6163d01c 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -3788,7 +3788,7 @@ defmodule ElixirSense.Core.Compiler do defp capture_import({atom, import_meta, args} = expr, s, e, sequential) do res = if sequential do - ElixirDispatch.import_function(import_meta, atom, length(args), e) + ElixirDispatch.import_function(import_meta, atom, length(args), s, e) else false end @@ -3808,7 +3808,14 @@ defmodule ElixirSense.Core.Compiler do {:remote, e_left, right, length(args)} _ when is_atom(e_left) -> - ElixirDispatch.require_function(require_meta, e_left, right, length(args), ee) + ElixirDispatch.require_function( + require_meta, + e_left, + right, + length(args), + s, + ee + ) _ -> false @@ -4515,24 +4522,22 @@ defmodule ElixirSense.Core.Compiler do imports end - def import_function(meta, name, arity, e) do + def import_function(meta, name, arity, s, e) do tuple = {name, arity} case find_import_by_name_arity(meta, tuple, [], e) do {:function, receiver} -> - # ElixirEnv.trace({:imported_function, meta, receiver, name, arity}, e) - # ElixirLocals.record_import(tuple, receiver, e.module, e.function) remote_function(meta, receiver, name, arity, e) {:macro, _receiver} -> false {:import, receiver} -> - require_function(meta, receiver, name, arity, e) + require_function(meta, receiver, name, arity, s, e) {:ambiguous, [first | _]} -> # elixir raises here, we return first matching - require_function(meta, first, name, arity, e) + require_function(meta, first, name, arity, s, e) false -> if Macro.special_form?(name, arity) do @@ -4540,27 +4545,26 @@ defmodule ElixirSense.Core.Compiler do else function = e.function - # TODO the condition has this at the end - # and ElixirDef.local_for(meta, name, arity, [:defmacro, :defmacrop], e) - if function != nil and function != tuple do + mfa = {e.module, name, arity} + + if function != nil and function != tuple and + Enum.any?(s.mods_funs_to_positions, fn {key, info} -> + key == mfa and State.ModFunInfo.get_category(info) == :macro + end) do false else - # ElixirEnv.trace({:local_function, meta, name, arity}, e) - # ElixirLocals.record_local(tuple, e.module, function, meta, false) - # TODO we may want to record {:local, name, arity} end end end end - def require_function(meta, receiver, name, arity, e) do + def require_function(meta, receiver, name, arity, s, e) do required = receiver in e.requires - if is_macro(name, arity, receiver, required) do + if is_macro(name, arity, receiver, required, s) do false else - # ElixirEnv.trace({:remote_function, meta, receiver, name, arity}, e) remote_function(meta, receiver, name, arity, e) end end @@ -4638,16 +4642,20 @@ defmodule ElixirSense.Core.Compiler do end end - defp is_macro(_name, _arity, _module, false), do: false + defp is_macro(_name, _arity, _module, false, _s), do: false - defp is_macro(name, arity, receiver, true) do - try do - # TODO is it OK for local requires? - macros = receiver.__info__(:macros) - {name, arity} in macros - rescue - _error -> false - end + defp is_macro(name, arity, receiver, true, s) do + mfa = {receiver, name, arity} + + Enum.any?(s.mods_funs_to_positions, fn {key, info} -> + key == mfa and State.ModFunInfo.get_category(info) == :macro + end) || + try do + macros = receiver.__info__(:macros) + {name, arity} in macros + rescue + _error -> false + end end end diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 21cf3d2a..fe21e133 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -7761,6 +7761,38 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } = state.calls end + test "registers calls capture required macro" do + state = + """ + defmodule Foo do + defmacro bar, do: :ok + end + + defmodule NyModule do + require Foo + require ElixirSenseExample.Math + def func do + &Foo.bar/0 + &ElixirSenseExample.Math.squared/1 + end + end + """ + |> string_to_state + + assert %{ + 9 => [%CallInfo{arity: 0, position: {9, 10}, func: :bar, mod: Foo}], + 10 => [ + _, + %CallInfo{ + arity: 1, + position: {10, 30}, + func: :squared, + mod: ElixirSenseExample.Math + } + ] + } = state.calls + end + test "registers calls capture expression external" do state = """ @@ -7809,21 +7841,46 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } = state.calls end + test "registers calls capture import" do + state = + """ + defmodule NyModule do + import Node + def func do + &list/0 + &binding/0 + end + end + """ + |> string_to_state + + assert %{ + 4 => [%CallInfo{arity: 0, func: :nodes, position: {4, 6}, mod: :erlang}], + 5 => [%CallInfo{arity: 0, func: :binding, position: {5, 6}, mod: Kernel}] + } = state.calls + end + test "registers calls capture operator local" do state = """ defmodule NyModule do + def foo, do: ok + defmacro bar, do: :ok def func do &func/1 &func/0 + &foo/0 + &bar/0 end end """ |> string_to_state assert %{ - 3 => [%CallInfo{arity: 1, func: :func, position: {3, 6}, mod: nil}], - 4 => [%CallInfo{arity: 0, func: :func, position: {4, 6}, mod: nil}] + 5 => [%CallInfo{arity: 1, func: :func, position: {5, 6}, mod: nil}], + 6 => [%CallInfo{arity: 0, func: :func, position: {6, 6}, mod: nil}], + 7 => [%CallInfo{arity: 0, func: :foo, position: {7, 6}, mod: nil}], + 8 => [%CallInfo{arity: 0, func: :bar, position: {8, 6}, mod: nil}] } = state.calls end From 4eda1d59b629d407f470a5894bd9fae0453f618a Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 4 Oct 2024 22:40:31 +0200 Subject: [PATCH 224/235] work on TODOs --- lib/elixir_sense/core/compiler.ex | 14 +++-- lib/elixir_sense/core/compiler/macro.ex | 6 +-- .../core/metadata_builder_test.exs | 52 +++++++++++++++++++ 3 files changed, 60 insertions(+), 12 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 6163d01c..95da202f 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -109,17 +109,14 @@ defmodule ElixirSense.Core.Compiler do defp do_expand({:__aliases__, meta, [head | tail] = list}, state, env) do case NormalizedMacroEnv.expand_alias(env, meta, list, trace: false) do {:alias, alias} -> - # TODO? - # A compiler may want to emit a :alias_reference trace in here. - # Elixir also warns on easy to confuse aliases, such as True/False/Nil. + # TODO track alias {alias, state, env} :error -> {head, state, env} = expand(head, state, env) if is_atom(head) do - # TODO? - # A compiler may want to emit a :alias_reference trace in here. + # TODO track alias {Module.concat([head | tail]), state, env} else # elixir raises here invalid_alias @@ -355,7 +352,7 @@ defmodule ElixirSense.Core.Compiler do unquote_opt = Keyword.get(e_opts, :unquote, default_unquote) generated = Keyword.get(e_opts, :generated, false) - # TODO this is a stub only + # alternative implementation # res = expand_quote(exprs, st, et) # res |> elem(0) |> IO.inspect # res @@ -2674,7 +2671,7 @@ defmodule ElixirSense.Core.Compiler do end def defdelegate_each(fun, opts) when is_list(opts) do - # TODO: Remove on v2.0 + # TODO Remove on elixir v2.0 append_first? = Keyword.get(opts, :append_first, false) {name, args} = @@ -4180,7 +4177,7 @@ defmodule ElixirSense.Core.Compiler do ) when is_atom(f) and is_integer(a) and is_atom(c) and is_list(meta) do new_meta = - case ElixirDispatch.find_import(meta, f, a, e) do + case ElixirDispatch.find_import(meta, f, a, e) |> dbg do false -> meta @@ -4491,6 +4488,7 @@ defmodule ElixirSense.Core.Compiler do case find_import_by_name_arity(meta, tuple, [], e) do {:function, receiver} -> # TODO trace call? + # TODO address when https://github.com/elixir-lang/elixir/issues/13878 is resolved # ElixirEnv.trace({:imported_function, meta, receiver, name, arity}, e) receiver diff --git a/lib/elixir_sense/core/compiler/macro.ex b/lib/elixir_sense/core/compiler/macro.ex index 6c511b01..d46f5a17 100644 --- a/lib/elixir_sense/core/compiler/macro.ex +++ b/lib/elixir_sense/core/compiler/macro.ex @@ -85,8 +85,7 @@ defmodule ElixirSense.Core.Compiler.Macro do defp do_expand_once({:__aliases__, meta, [head | tail] = list} = alias, env) do case NormalizedMacroEnv.expand_alias(env, meta, list, trace: false) do {:alias, alias} -> - # TODO? - # A compiler may want to emit a :alias_reference trace in here. + # TODO track alias {alias, true} :error -> @@ -94,8 +93,7 @@ defmodule ElixirSense.Core.Compiler.Macro do if is_atom(head) do receiver = Module.concat([head | tail]) - # TODO? - # A compiler may want to emit a :alias_reference trace in here. + # TODO track alias {receiver, true} else {alias, false} diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index fe21e133..cff0e92c 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -7793,6 +7793,58 @@ defmodule ElixirSense.Core.MetadataBuilderTest do } = state.calls end + # TODO reenable when https://github.com/elixir-lang/elixir/issues/13878 is resolved + # test "registers calls capture quoted" do + # state = + # """ + # defmodule MyModule do + # def aaa, do: :ok + # defmacro bbb, do: :ok + # defmacro foo do + # quote do + # aaa() + # &aaa/0 + # bbb() + # &bbb/0 + # inspect(1) + # &inspect/1 + # Node.list() + # &Node.list/0 + # end + # end + + # def go do + # foo() + # end + + # def bar do + # aaa() + # &aaa/0 + # bbb() + # &bbb/0 + # inspect(1) + # &inspect/1 + # Node.list() + # &Node.list/0 + # end + # end + # """ + # |> string_to_state + + # assert %{ + # 9 => [%CallInfo{arity: 0, position: {9, 10}, func: :bar, mod: Foo}], + # 10 => [ + # _, + # %CallInfo{ + # arity: 1, + # position: {10, 30}, + # func: :squared, + # mod: ElixirSenseExample.Math + # } + # ] + # } = state.calls + # end + test "registers calls capture expression external" do state = """ From a479232ad73adfb7b2eb699c665c786bcfccfdb4 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 5 Oct 2024 08:21:56 +0200 Subject: [PATCH 225/235] fix tests --- lib/elixir_sense/core/compiler.ex | 2 +- test/elixir_sense/core/introspection_test.exs | 52 ++++++++++++------- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 95da202f..ec454a93 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -4177,7 +4177,7 @@ defmodule ElixirSense.Core.Compiler do ) when is_atom(f) and is_integer(a) and is_atom(c) and is_list(meta) do new_meta = - case ElixirDispatch.find_import(meta, f, a, e) |> dbg do + case ElixirDispatch.find_import(meta, f, a, e) do false -> meta diff --git a/test/elixir_sense/core/introspection_test.exs b/test/elixir_sense/core/introspection_test.exs index 46a4c8a3..3fd890ad 100644 --- a/test/elixir_sense/core/introspection_test.exs +++ b/test/elixir_sense/core/introspection_test.exs @@ -55,7 +55,7 @@ defmodule ElixirSense.Core.IntrospectionTest do if System.otp_release() |> String.to_integer() >= 23 do if System.otp_release() |> String.to_integer() >= 27 do - assert "This function is" <> _ = summary + assert "Select the _callback mode_" <> _ = summary else assert "- CallbackMode = " <> _ = summary end @@ -146,25 +146,37 @@ defmodule ElixirSense.Core.IntrospectionTest do test "get_returns_from_callback (erlang specs)" do returns = get_returns_from_callback(:gen_fsm, :handle_event, 3) - assert returns == [ - %{ - description: "{:next_state, nextStateName, newStateData}", - snippet: "{:next_state, \"${1:nextStateName}$\", \"${2:newStateData}$\"}", - spec: "{:next_state, nextStateName :: atom(), newStateData :: term()}" - }, - %{ - description: "{:next_state, nextStateName, newStateData, timeout() | :hibernate}", - snippet: - "{:next_state, \"${1:nextStateName}$\", \"${2:newStateData}$\", \"${3:timeout() | :hibernate}$\"}", - spec: - "{:next_state, nextStateName :: atom(), newStateData :: term(), timeout() | :hibernate}" - }, - %{ - description: "{:stop, reason, newStateData}", - snippet: "{:stop, \"${1:reason}$\", \"${2:newStateData}$\"}", - spec: "{:stop, reason :: term(), newStateData :: term()}" - } - ] + if System.otp_release() |> String.to_integer() >= 27 do + assert returns == [ + %{ + description: "result", + snippet: "\"${1:result}$\"", + spec: + "result\nwhen event: term(),\n stateName: atom(),\n stateData: term(),\n result:\n {:next_state, nextStateName, newStateData}\n | {:next_state, nextStateName, newStateData, timeout}\n | {:next_state, nextStateName, newStateData, :hibernate}\n | {:stop, reason, newStateData},\n nextStateName: atom(),\n newStateData: term(),\n timeout: timeout(),\n reason: term()" + } + ] + else + assert returns == [ + %{ + description: "{:next_state, nextStateName, newStateData}", + snippet: "{:next_state, \"${1:nextStateName}$\", \"${2:newStateData}$\"}", + spec: "{:next_state, nextStateName :: atom(), newStateData :: term()}" + }, + %{ + description: + "{:next_state, nextStateName, newStateData, timeout() | :hibernate}", + snippet: + "{:next_state, \"${1:nextStateName}$\", \"${2:newStateData}$\", \"${3:timeout() | :hibernate}$\"}", + spec: + "{:next_state, nextStateName :: atom(), newStateData :: term(), timeout() | :hibernate}" + }, + %{ + description: "{:stop, reason, newStateData}", + snippet: "{:stop, \"${1:reason}$\", \"${2:newStateData}$\"}", + spec: "{:stop, reason :: term(), newStateData :: term()}" + } + ] + end end test "actual_mod_fun Elixir proxy" do From 7322fb4aa193ace1b2fdd0072ed955cc285ea460 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 5 Oct 2024 08:37:54 +0200 Subject: [PATCH 226/235] split modules --- lib/elixir_sense/core/compiler.ex | 2269 +---------------- lib/elixir_sense/core/compiler/bitstring.ex | 427 ++++ lib/elixir_sense/core/compiler/clauses.ex | 549 ++++ lib/elixir_sense/core/compiler/dispatch.ex | 179 ++ lib/elixir_sense/core/compiler/fn.ex | 248 ++ lib/elixir_sense/core/compiler/map.ex | 184 ++ lib/elixir_sense/core/compiler/quote.ex | 546 ++++ lib/elixir_sense/core/compiler/rewrite.ex | 32 + lib/elixir_sense/core/{ => compiler}/state.ex | 0 lib/elixir_sense/core/compiler/utils.ex | 98 + 10 files changed, 2264 insertions(+), 2268 deletions(-) create mode 100644 lib/elixir_sense/core/compiler/bitstring.ex create mode 100644 lib/elixir_sense/core/compiler/clauses.ex create mode 100644 lib/elixir_sense/core/compiler/dispatch.ex create mode 100644 lib/elixir_sense/core/compiler/fn.ex create mode 100644 lib/elixir_sense/core/compiler/map.ex create mode 100644 lib/elixir_sense/core/compiler/quote.ex create mode 100644 lib/elixir_sense/core/compiler/rewrite.ex rename lib/elixir_sense/core/{ => compiler}/state.ex (100%) create mode 100644 lib/elixir_sense/core/compiler/utils.ex diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index ec454a93..5ac8416c 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1,5 +1,5 @@ defmodule ElixirSense.Core.Compiler do - import ElixirSense.Core.State, except: [expand: 2, expand: 3, no_alias_expansion: 1] + import ElixirSense.Core.State alias ElixirSense.Core.State require Logger alias ElixirSense.Core.Introspection @@ -2607,2271 +2607,4 @@ defmodule ElixirSense.Core.Compiler do end end end - - defmodule Utils do - def generated([{:generated, true} | _] = meta), do: meta - def generated(meta), do: [{:generated, true} | meta] - - def split_last([]), do: {[], []} - - def split_last(list), do: split_last(list, []) - - defp split_last([h], acc), do: {Enum.reverse(acc), h} - - defp split_last([h | t], acc), do: split_last(t, [h | acc]) - - def split_opts(args) do - case split_last(args) do - {outer_cases, outer_opts} when is_list(outer_opts) -> - case split_last(outer_cases) do - {inner_cases, inner_opts} when is_list(inner_opts) -> - {inner_cases, inner_opts ++ outer_opts} - - _ -> - {outer_cases, outer_opts} - end - - _ -> - {args, []} - end - end - - def get_line(opts) when is_list(opts) do - case Keyword.fetch(opts, :line) do - {:ok, line} when is_integer(line) -> line - _ -> 0 - end - end - - def extract_guards({:when, _, [left, right]}), do: {left, extract_or_guards(right)} - def extract_guards(term), do: {term, []} - - def extract_or_guards({:when, _, [left, right]}), do: [left | extract_or_guards(right)] - def extract_or_guards(term), do: [term] - - def select_with_cursor(ast_list) do - Enum.find(ast_list, &has_cursor?/1) - end - - def has_cursor?(ast) do - # TODO rewrite to lazy prewalker - {_, result} = - Macro.prewalk(ast, false, fn - _node, true -> - {nil, true} - - {:__cursor__, _, list}, _state when is_list(list) -> - {nil, true} - - node, false -> - {node, false} - end) - - result - end - - def defdelegate_each(fun, opts) when is_list(opts) do - # TODO Remove on elixir v2.0 - append_first? = Keyword.get(opts, :append_first, false) - - {name, args} = - case fun do - {:when, _, [_left, right]} -> - raise ArgumentError, - "guards are not allowed in defdelegate/2, got: when #{Macro.to_string(right)}" - - _ -> - case Macro.decompose_call(fun) do - {_, _} = pair -> pair - _ -> raise ArgumentError, "invalid syntax in defdelegate #{Macro.to_string(fun)}" - end - end - - as = Keyword.get(opts, :as, name) - as_args = build_as_args(args, append_first?) - - {name, args, as, as_args} - end - - defp build_as_args(args, append_first?) do - as_args = :lists.map(&build_as_arg/1, args) - - case append_first? do - true -> tl(as_args) ++ [hd(as_args)] - false -> as_args - end - end - - # elixir validates arg - defp build_as_arg({:\\, _, [arg, _default_arg]}), do: arg - defp build_as_arg(arg), do: arg - end - - defmodule Clauses do - alias ElixirSense.Core.Compiler, as: ElixirExpand - alias ElixirSense.Core.Compiler.Utils, as: ElixirUtils - alias ElixirSense.Core.State - alias ElixirSense.Core.TypeInference - - def match(fun, expr, after_s, _before_s, %{context: :match} = e) do - fun.(expr, after_s, e) - end - - def match(fun, expr, after_s, before_s, e) do - %{vars: current, unused: unused} = after_s - %{vars: {read, _write}, prematch: prematch} = before_s - - call_s = %{ - before_s - | prematch: {read, unused, :none}, - unused: unused, - vars: current, - calls: after_s.calls, - lines_to_env: after_s.lines_to_env, - vars_info: after_s.vars_info, - cursor_env: after_s.cursor_env, - closest_env: after_s.closest_env - } - - call_e = Map.put(e, :context, :match) - {e_expr, %{vars: new_current, unused: new_unused} = s_expr, ee} = fun.(expr, call_s, call_e) - - end_s = %{ - after_s - | prematch: prematch, - unused: new_unused, - vars: new_current, - calls: s_expr.calls, - lines_to_env: s_expr.lines_to_env, - vars_info: s_expr.vars_info, - cursor_env: s_expr.cursor_env, - closest_env: s_expr.closest_env - } - - end_e = Map.put(ee, :context, Map.get(e, :context)) - {e_expr, end_s, end_e} - end - - def clause(fun, {:->, clause_meta, [_, _]} = clause, s, e) - when is_function(fun, 4) do - clause(fn x, sa, ea -> fun.(clause_meta, x, sa, ea) end, clause, s, e) - end - - def clause(fun, {:->, meta, [left, right]}, s, e) do - {e_left, sl, el} = fun.(left, s, e) - {e_right, sr, er} = ElixirExpand.expand(right, sl, el) - {{:->, meta, [e_left, e_right]}, sr, er} - end - - def clause(fun, expr, s, e) do - # try to recover from error by wrapping the expression in clause - # elixir raises here bad_or_missing_clauses - clause(fun, {:->, [], [[expr], :ok]}, s, e) - end - - def head([{:when, meta, [_ | _] = all}], s, e) do - {args, guard} = ElixirUtils.split_last(all) - prematch = s.prematch - - {{e_args, e_guard}, sg, eg} = - match( - fn _ok, sm, em -> - {e_args, sa, ea} = ElixirExpand.expand_args(args, sm, em) - - {e_guard, sg, eg} = - guard(guard, %{sa | prematch: prematch}, %{ea | context: :guard}) - - type_info = Guard.type_information_from_guards(e_guard) - - sg = merge_inferred_types(sg, type_info) - - {{e_args, e_guard}, sg, eg} - end, - :ok, - s, - s, - e - ) - - {[{:when, meta, e_args ++ [e_guard]}], sg, eg} - end - - def head(args, s, e) do - match(&ElixirExpand.expand_args/3, args, s, s, e) - end - - def guard({:when, meta, [left, right]}, s, e) do - {e_left, sl, el} = guard(left, s, e) - {e_right, sr, er} = guard(right, sl, el) - {{:when, meta, [e_left, e_right]}, sr, er} - end - - def guard(guard, s, e) do - {e_guard, sg, eg} = ElixirExpand.expand(guard, s, e) - {e_guard, sg, eg} - end - - # case - - @valid_case_opts [:do] - - def case(e_expr, [], s, e) do - # elixir raises here missing_option - # emit a fake do block - case(e_expr, [do: []], s, e) - end - - def case(_e_expr, opts, s, e) when not is_list(opts) do - # elixir raises here invalid_args - # there may be cursor - ElixirExpand.expand(opts, s, e) - end - - def case(e_expr, opts, s, e) do - # expand invalid opts in case there's cursor - {_ast, s, _e} = ElixirExpand.expand(opts |> Keyword.drop(@valid_case_opts), s, e) - - opts = sanitize_opts(opts, @valid_case_opts) - - match_context = TypeInference.type_of(e_expr, e.context) - - {case_clauses, sa} = - Enum.map_reduce(opts, s, fn x, sa -> - expand_case(x, match_context, sa, e) - end) - - {case_clauses, sa, e} - end - - defp expand_case({:do, _} = do_clause, match_context, s, e) do - expand_clauses( - fn c, s, e -> - case head(c, s, e) do - {[h | _] = c, s, e} -> - clause_vars_with_inferred_types = - TypeInference.find_typed_vars(h, match_context, :match) - - s = State.merge_inferred_types(s, clause_vars_with_inferred_types) - - {c, s, e} - - other -> - other - end - end, - do_clause, - s, - e - ) - end - - # cond - - @valid_cond_opts [:do] - - def cond([], s, e) do - # elixir raises here missing_option - # emit a fake do block - cond([do: []], s, e) - end - - def cond(opts, s, e) when not is_list(opts) do - # elixir raises here invalid_args - # there may be cursor - ElixirExpand.expand(opts, s, e) - end - - def cond(opts, s, e) do - # expand invalid opts in case there's cursor - {_ast, s, _e} = ElixirExpand.expand(opts |> Keyword.drop(@valid_cond_opts), s, e) - - opts = sanitize_opts(opts, @valid_cond_opts) - - {cond_clauses, sa} = - Enum.map_reduce(opts, s, fn x, sa -> - expand_cond(x, sa, e) - end) - - {cond_clauses, sa, e} - end - - defp expand_cond({:do, _} = do_clause, s, e) do - expand_clauses(&ElixirExpand.expand_args/3, do_clause, s, e) - end - - # receive - - @valid_receive_opts [:do, :after] - - def receive([], s, e) do - # elixir raises here missing_option - # emit a fake do block - receive([do: []], s, e) - end - - def receive(opts, s, e) when not is_list(opts) do - # elixir raises here invalid_args - # there may be cursor - ElixirExpand.expand(opts, s, e) - end - - def receive(opts, s, e) do - # expand invalid opts in case there's cursor - {_ast, s, _e} = ElixirExpand.expand(opts |> Keyword.drop(@valid_receive_opts), s, e) - - opts = sanitize_opts(opts, @valid_receive_opts) - - {receive_clauses, sa} = - Enum.map_reduce(opts, s, fn x, sa -> - expand_receive(x, sa, e) - end) - - {receive_clauses, sa, e} - end - - defp expand_receive({:do, {:__block__, _, []}} = do_block, s, _e) do - {do_block, s} - end - - defp expand_receive({:do, _} = do_clause, s, e) do - # no point in doing type inference here, we have no idea what message we may get - expand_clauses(&head/3, do_clause, s, e) - end - - defp expand_receive({:after, [_ | _]} = after_clause, s, e) do - expand_clauses(&ElixirExpand.expand_args/3, after_clause, s, e) - end - - defp expand_receive({:after, expr}, s, e) when not is_list(expr) do - # elixir raises here multiple_after_clauses_in_receive - case expr do - expr when not is_list(expr) -> - # try to recover from error by wrapping the expression in list - expand_receive({:after, [expr]}, s, e) - - [first | discarded] -> - # try to recover from error by taking first clause only - # expand other in case there's cursor - {_ast, s, _e} = ElixirExpand.expand(discarded, s, e) - expand_receive({:after, [first]}, s, e) - - [] -> - # try to recover from error by inserting a fake clause - expand_receive({:after, [{:->, [], [[0], :ok]}]}, s, e) - end - end - - # with - - @valid_with_opts [:do, :else] - - def with(meta, args, s, e) do - {exprs, opts0} = ElixirUtils.split_opts(args) - - # expand invalid opts in case there's cursor - {_ast, s, _e} = ElixirExpand.expand(opts0 |> Keyword.drop(@valid_with_opts), s, e) - - opts0 = sanitize_opts(opts0, @valid_with_opts) - s0 = new_vars_scope(s) - {e_exprs, {s1, e1}} = Enum.map_reduce(exprs, {s0, e}, &expand_with/2) - {e_do, opts1, s2} = expand_with_do(meta, opts0, s, s1, e1) - {e_opts, _opts2, s3} = expand_with_else(opts1, s2, e) - - {{:with, meta, e_exprs ++ [[{:do, e_do} | e_opts]]}, s3, e} - end - - defp expand_with({:<-, meta, [left, right]}, {s, e}) do - {e_right, sr, er} = ElixirExpand.expand(right, s, e) - sm = reset_read(sr, s) - {[e_left], sl, el} = head([left], sm, er) - - match_context_r = TypeInference.type_of(e_right, e.context) - vars_l_with_inferred_types = TypeInference.find_typed_vars(e_left, match_context_r, :match) - - sl = State.merge_inferred_types(sl, vars_l_with_inferred_types) - - {{:<-, meta, [e_left, e_right]}, {sl, el}} - end - - defp expand_with(expr, {s, e}) do - {e_expr, se, ee} = ElixirExpand.expand(expr, s, e) - {e_expr, {se, ee}} - end - - defp expand_with_do(_meta, opts, s, acc, e) do - {expr, rest_opts} = Keyword.pop(opts, :do) - # elixir raises here missing_option - # we return empty expression - expr = expr || [] - - {e_expr, s_acc, _e_acc} = ElixirExpand.expand(expr, acc, e) - - {e_expr, rest_opts, remove_vars_scope(s_acc, s)} - end - - defp expand_with_else(opts, s, e) do - case Keyword.pop(opts, :else) do - {nil, _} -> - {[], opts, s} - - {expr, rest_opts} -> - pair = {:else, expr} - - # no point in doing type inference here, we have no idea what data we are matching against - {e_pair, se} = expand_clauses(&head/3, pair, s, e) - {[e_pair], rest_opts, se} - end - end - - # try - - @valid_try_opts [:do, :rescue, :catch, :else, :after] - - def try([], s, e) do - # elixir raises here missing_option - # emit a fake do block - try([do: []], s, e) - end - - def try(opts, s, e) when not is_list(opts) do - # elixir raises here invalid_args - # there may be cursor - ElixirExpand.expand(opts, s, e) - end - - def try(opts, s, e) do - # expand invalid opts in case there's cursor - {_ast, s, _e} = ElixirExpand.expand(opts |> Keyword.drop(@valid_try_opts), s, e) - - opts = sanitize_opts(opts, @valid_try_opts) - - {try_clauses, sa} = - Enum.map_reduce(opts, s, fn x, sa -> - expand_try(x, sa, e) - end) - - {try_clauses, sa, e} - end - - defp expand_try({:do, expr}, s, e) do - {e_expr, se, _ee} = ElixirExpand.expand(expr, new_vars_scope(s), e) - {{:do, e_expr}, remove_vars_scope(se, s)} - end - - defp expand_try({:after, expr}, s, e) do - {e_expr, se, _ee} = ElixirExpand.expand(expr, new_vars_scope(s), e) - {{:after, e_expr}, remove_vars_scope(se, s)} - end - - defp expand_try({:else, _} = else_clause, s, e) do - # TODO we could try to infer type from last try block expression - expand_clauses(&head/3, else_clause, s, e) - end - - defp expand_try({:catch, _} = catch_clause, s, e) do - expand_clauses_with_stacktrace(&expand_catch/4, catch_clause, s, e) - end - - defp expand_try({:rescue, _} = rescue_clause, s, e) do - expand_clauses_with_stacktrace(&expand_rescue/4, rescue_clause, s, e) - end - - defp expand_clauses_with_stacktrace(fun, clauses, s, e) do - old_stacktrace = s.stacktrace - ss = %{s | stacktrace: true} - {ret, se} = expand_clauses(fun, clauses, ss, e) - {ret, %{se | stacktrace: old_stacktrace}} - end - - defp expand_catch(meta, [{:when, when_meta, [a1, a2, a3, dh | dt]}], s, e) do - # elixir raises here wrong_number_of_args_for_clause - {_, s, _} = ElixirExpand.expand([dh | dt], s, e) - expand_catch(meta, [{:when, when_meta, [a1, a2, a3]}], s, e) - end - - defp expand_catch(_meta, args = [_], s, e) do - # no point in doing type inference here, we have no idea what throw we caught - head(args, s, e) - end - - defp expand_catch(_meta, args = [_, _], s, e) do - # TODO is it worth to infer type of the first arg? :error | :exit | :throw | {:EXIT, pid()} - head(args, s, e) - end - - defp expand_catch(meta, [a1, a2 | d], s, e) do - # attempt to recover from error by taking 2 first args - # elixir raises here wrong_number_of_args_for_clause - {_, s, _} = ElixirExpand.expand(d, s, e) - expand_catch(meta, [a1, a2], s, e) - end - - defp expand_rescue(_meta, [arg], s, e) do - # elixir is strict here and raises invalid_rescue_clause on invalid args - {e_arg, sa, ea} = expand_rescue(arg, s, e) - {[e_arg], sa, ea} - end - - defp expand_rescue(meta, [a1 | d], s, e) do - # try to recover from error by taking first argument only - # elixir raises here wrong_number_of_args_for_clause - {_, s, _} = ElixirExpand.expand(d, s, e) - expand_rescue(meta, [a1], s, e) - end - - # rescue var - defp expand_rescue({name, _, atom} = var, s, e) when is_atom(name) and is_atom(atom) do - {e_left, sl, el} = match(&ElixirExpand.expand/3, var, s, s, e) - - match_context = {:struct, [], {:atom, Exception}, nil} - - vars_with_inferred_types = TypeInference.find_typed_vars(e_left, match_context, :match) - sl = State.merge_inferred_types(sl, vars_with_inferred_types) - - {e_left, sl, el} - end - - # rescue Alias => _ in [Alias] - defp expand_rescue({:__aliases__, _, [_ | _]} = alias, s, e) do - expand_rescue({:in, [], [{:_, [], e.module}, alias]}, s, e) - end - - # rescue var in _ - defp expand_rescue( - {:in, _, [{name, _, var_context} = var, {:_, _, underscore_context}]}, - s, - e - ) - when is_atom(name) and is_atom(var_context) and is_atom(underscore_context) do - {e_left, sl, el} = match(&ElixirExpand.expand/3, var, s, s, e) - - match_context = {:struct, [], {:atom, Exception}, nil} - - vars_with_inferred_types = TypeInference.find_typed_vars(e_left, match_context, :match) - sl = State.merge_inferred_types(sl, vars_with_inferred_types) - - {e_left, sl, el} - end - - # rescue var in (list() or atom()) - defp expand_rescue({:in, meta, [left, right]}, s, e) do - {e_left, sl, el} = match(&ElixirExpand.expand/3, left, s, s, e) - {e_right, sr, er} = ElixirExpand.expand(right, sl, el) - - case e_left do - {name, _, atom} when is_atom(name) and is_atom(atom) -> - normalized = normalize_rescue(e_right, e) - - match_context = - for exception <- normalized, reduce: nil do - nil -> {:struct, [], {:atom, exception}, nil} - other -> {:union, [other, {:struct, [], {:atom, exception}, nil}]} - end - - match_context = - if match_context == nil do - {:struct, [], {:atom, Exception}, nil} - else - match_context - end - - vars_with_inferred_types = TypeInference.find_typed_vars(e_left, match_context, :match) - sr = State.merge_inferred_types(sr, vars_with_inferred_types) - - {{:in, meta, [e_left, normalized]}, sr, er} - - _ -> - # elixir rejects this case, we normalize to underscore - {{:in, meta, [{:_, [], e.module}, normalize_rescue(e_right, e)]}, sr, er} - end - end - - # rescue expr() => rescue expanded_expr() - defp expand_rescue({_, meta, _} = arg, s, e) do - # TODO how to check for cursor here? - case ElixirExpand.Macro.expand_once(arg, %{e | line: ElixirUtils.get_line(meta)}) do - ^arg -> - # elixir rejects this case - # try to recover from error by generating fake expression - expand_rescue({:in, meta, [arg, {:_, [], e.module}]}, s, e) - - new_arg -> - expand_rescue(new_arg, s, e) - end - end - - # rescue list() or atom() => _ in (list() or atom()) - defp expand_rescue(arg, s, e) do - expand_rescue({:in, [], [{:_, [], e.module}, arg]}, s, e) - end - - defp normalize_rescue(atom, _e) when is_atom(atom) do - [atom] - end - - defp normalize_rescue(other, e) do - # elixir is strict here, we reject invalid nodes - res = - if is_list(other) do - Enum.filter(other, &is_atom/1) - else - [] - end - - if res == [] do - [{:_, [], e.module}] - else - res - end - end - - defp expand_clauses(fun, {key, [_ | _] = clauses}, s, e) do - transformer = fn clause, sa -> - {e_clause, s_acc, _e_acc} = - clause(fun, clause, new_vars_scope(sa), e) - - {e_clause, remove_vars_scope(s_acc, sa)} - end - - {values, se} = Enum.map_reduce(clauses, s, transformer) - {{key, values}, se} - end - - defp expand_clauses(fun, {key, expr}, s, e) do - # try to recover from error by wrapping the expression in a clauses list - # elixir raises here bad_or_missing_clauses - expand_clauses(fun, {key, [expr]}, s, e) - end - - # helpers - - defp sanitize_opt(opts, opt) do - case Keyword.fetch(opts, opt) do - :error -> [] - {:ok, value} -> [{opt, value}] - end - end - - defp sanitize_opts(opts, allowed) do - Enum.flat_map(allowed, fn opt -> sanitize_opt(opts, opt) end) - end - end - - defmodule Bitstring do - alias ElixirSense.Core.Compiler, as: ElixirExpand - alias ElixirSense.Core.Compiler.Utils, as: ElixirUtils - - defp expand_match(expr, {s, original_s}, e) do - {e_expr, se, ee} = ElixirExpand.expand(expr, s, e) - {e_expr, {se, original_s}, ee} - end - - def expand(meta, args, s, e, require_size) do - case Map.get(e, :context) do - :match -> - {e_args, alignment, {sa, _}, ea} = - expand(meta, &expand_match/3, args, [], {s, s}, e, 0, require_size) - - # elixir validates if there is no nested match - - {{:<<>>, [{:alignment, alignment} | meta], e_args}, sa, ea} - - _ -> - pair_s = {prepare_write(s), s} - - {e_args, alignment, {sa, _}, ea} = - expand(meta, &ElixirExpand.expand_arg/3, args, [], pair_s, e, 0, require_size) - - {{:<<>>, [{:alignment, alignment} | meta], e_args}, close_write(sa, s), ea} - end - end - - def expand(_bitstr_meta, _fun, [], acc, s, e, alignment, _require_size) do - {Enum.reverse(acc), alignment, s, e} - end - - def expand( - bitstr_meta, - fun, - [{:"::", meta, [left, right]} | t], - acc, - s, - e, - alignment, - require_size - ) do - {e_left, {sl, original_s}, el} = expand_expr(meta, left, fun, s, e) - - match_or_require_size = require_size or is_match_size(t, el) - e_type = expr_type(e_left) - - expect_size = - case e_left do - _ when not match_or_require_size -> :optional - {:^, _, [{_, _, _}]} -> {:infer, e_left} - _ -> :required - end - - {e_right, e_alignment, ss, es} = - expand_specs(e_type, meta, right, sl, original_s, el, expect_size) - - e_acc = concat_or_prepend_bitstring(meta, e_left, e_right, acc) - - expand( - bitstr_meta, - fun, - t, - e_acc, - {ss, original_s}, - es, - alignment(alignment, e_alignment), - require_size - ) - end - - def expand(bitstr_meta, fun, [h | t], acc, s, e, alignment, require_size) do - meta = extract_meta(h, bitstr_meta) - {e_left, {ss, original_s}, es} = expand_expr(meta, h, fun, s, e) - - e_type = expr_type(e_left) - e_right = infer_spec(e_type, meta) - - inferred_meta = [{:inferred_bitstring_spec, true} | meta] - - e_acc = - concat_or_prepend_bitstring( - inferred_meta, - e_left, - e_right, - acc - ) - - expand(meta, fun, t, e_acc, {ss, original_s}, es, alignment, require_size) - end - - defp expand_expr( - _meta, - {{:., _, [mod, :to_string]}, _, [arg]} = ast, - fun, - s, - %{context: context} = e - ) - when context != nil and (mod == Kernel or mod == String.Chars) do - case fun.(arg, s, e) do - {ebin, se, ee} when is_binary(ebin) -> {ebin, se, ee} - _ -> fun.(ast, s, e) - end - end - - defp expand_expr(_meta, component, fun, s, e) do - case fun.(component, s, e) do - {e_component, s, e} when is_list(e_component) or is_atom(e_component) -> - # elixir raises here invalid_literal - # try to recover from error by replacing it with "" - {"", s, e} - - expanded -> - expanded - end - end - - defp expand_specs(expr_type, meta, info, s, original_s, e, expect_size) do - default = - %{size: :default, unit: :default, sign: :default, type: :default, endianness: :default} - - {specs, ss, es} = - expand_each_spec(meta, unpack_specs(info, []), default, s, original_s, e) - - merged_type = type(expr_type, specs.type) - - # elixir validates if unsized binary is not on the end - - size_and_unit = size_and_unit(expr_type, specs.size, specs.unit) - alignment = compute_alignment(merged_type, specs.size, specs.unit) - - maybe_inferred_size = - case {expect_size, merged_type, size_and_unit} do - {{:infer, pinned_var}, :binary, []} -> - [{:size, meta, [{{:., meta, [:erlang, :byte_size]}, meta, [pinned_var]}]}] - - {{:infer, pinned_var}, :bitstring, []} -> - [{:size, meta, [{{:., meta, [:erlang, :bit_size]}, meta, [pinned_var]}]}] - - _ -> - size_and_unit - end - - [h | t] = - build_spec( - specs.size, - specs.unit, - merged_type, - specs.endianness, - specs.sign, - maybe_inferred_size - ) - - {Enum.reduce(t, h, fn i, acc -> {:-, meta, [acc, i]} end), alignment, ss, es} - end - - defp type(:default, :default), do: :integer - defp type(expr_type, :default), do: expr_type - - defp type(:binary, type) when type in [:binary, :bitstring, :utf8, :utf16, :utf32], - do: type - - defp type(:bitstring, type) when type in [:binary, :bitstring], do: type - - defp type(:integer, type) when type in [:integer, :float, :utf8, :utf16, :utf32], - do: type - - defp type(:float, :float), do: :float - defp type(:default, type), do: type - - defp type(_other, _type) do - # elixir raises here bittype_mismatch - type(:default, :default) - end - - defp expand_each_spec(meta, [{:__cursor__, _, args} = h | t], map, s, original_s, e) - when is_list(args) do - {h, s, e} = ElixirExpand.expand(h, s, e) - - args = - case h do - nil -> t - h -> [h | t] - end - - expand_each_spec(meta, args, map, s, original_s, e) - end - - defp expand_each_spec(meta, [{expr, meta_e, args} = h | t], map, s, original_s, e) - when is_atom(expr) do - case validate_spec(expr, args) do - {key, arg} -> - {value, se, ee} = expand_spec_arg(arg, s, original_s, e) - # elixir validates spec arg here - # elixir raises bittype_mismatch in some cases - expand_each_spec(meta, t, Map.put(map, key, value), se, original_s, ee) - - :none -> - ha = - if args == nil do - {expr, meta_e, []} - else - h - end - - # TODO how to check for cursor here? - case ElixirExpand.Macro.expand(ha, Map.put(e, :line, ElixirUtils.get_line(meta))) do - ^ha -> - # elixir raises here undefined_bittype - # we omit the spec - expand_each_spec(meta, t, map, s, original_s, e) - - new_types -> - expand_each_spec(meta, unpack_specs(new_types, []) ++ t, map, s, original_s, e) - end - end - end - - defp expand_each_spec(meta, [_expr | tail], map, s, original_s, e) do - # elixir raises undefined_bittype - # we skip it - expand_each_spec(meta, tail, map, s, original_s, e) - end - - defp expand_each_spec(_meta, [], map, s, _original_s, e), do: {map, s, e} - - defp compute_alignment(_, size, unit) when is_integer(size) and is_integer(unit), - do: rem(size * unit, 8) - - defp compute_alignment(:default, size, unit), do: compute_alignment(:integer, size, unit) - defp compute_alignment(:integer, :default, unit), do: compute_alignment(:integer, 8, unit) - defp compute_alignment(:integer, size, :default), do: compute_alignment(:integer, size, 1) - defp compute_alignment(:bitstring, size, :default), do: compute_alignment(:bitstring, size, 1) - defp compute_alignment(:binary, size, :default), do: compute_alignment(:binary, size, 8) - defp compute_alignment(:binary, _, _), do: 0 - defp compute_alignment(:float, _, _), do: 0 - defp compute_alignment(:utf32, _, _), do: 0 - defp compute_alignment(:utf16, _, _), do: 0 - defp compute_alignment(:utf8, _, _), do: 0 - defp compute_alignment(_, _, _), do: :unknown - - defp alignment(left, right) when is_integer(left) and is_integer(right) do - rem(left + right, 8) - end - - defp alignment(_, _), do: :unknown - - defp extract_meta({_, meta, _}, _), do: meta - defp extract_meta(_, meta), do: meta - - defp infer_spec(:bitstring, meta), do: {:bitstring, meta, nil} - defp infer_spec(:binary, meta), do: {:binary, meta, nil} - defp infer_spec(:float, meta), do: {:float, meta, nil} - defp infer_spec(:integer, meta), do: {:integer, meta, nil} - defp infer_spec(:default, meta), do: {:integer, meta, nil} - - defp expr_type(integer) when is_integer(integer), do: :integer - defp expr_type(float) when is_float(float), do: :float - defp expr_type(binary) when is_binary(binary), do: :binary - defp expr_type({:<<>>, _, _}), do: :bitstring - defp expr_type(_), do: :default - - defp concat_or_prepend_bitstring(_meta, {:<<>>, _, []}, _e_right, acc), - do: acc - - defp concat_or_prepend_bitstring( - meta, - {:<<>>, parts_meta, parts} = e_left, - e_right, - acc - ) do - # elixir raises unsized_binary in some cases - - case e_right do - {:binary, _, nil} -> - {alignment, alignment} = Keyword.fetch!(parts_meta, :alignment) - - if is_integer(alignment) do - # elixir raises unaligned_binary if alignment != 0 - Enum.reverse(parts, acc) - else - [{:"::", meta, [e_left, e_right]} | acc] - end - - {:bitstring, _, nil} -> - Enum.reverse(parts, acc) - end - end - - defp concat_or_prepend_bitstring(meta, e_left, e_right, acc) do - [{:"::", meta, [e_left, e_right]} | acc] - end - - defp unpack_specs({:-, _, [h, t]}, acc), do: unpack_specs(h, unpack_specs(t, acc)) - - defp unpack_specs({:*, _, [{:_, _, atom}, unit]}, acc) when is_atom(atom), - do: [{:unit, [], [unit]} | acc] - - defp unpack_specs({:*, _, [size, unit]}, acc), - do: [{:size, [], [size]}, {:unit, [], [unit]} | acc] - - defp unpack_specs(size, acc) when is_integer(size), do: [{:size, [], [size]} | acc] - - defp unpack_specs({expr, meta, args}, acc) when is_atom(expr) do - list_args = - cond do - is_atom(args) -> nil - is_list(args) -> args - true -> args - end - - [{expr, meta, list_args} | acc] - end - - defp unpack_specs(other, acc), do: [other | acc] - - defp validate_spec(spec, []), do: validate_spec(spec, nil) - defp validate_spec(:big, nil), do: {:endianness, :big} - defp validate_spec(:little, nil), do: {:endianness, :little} - defp validate_spec(:native, nil), do: {:endianness, :native} - defp validate_spec(:size, [size]), do: {:size, size} - defp validate_spec(:unit, [unit]), do: {:unit, unit} - defp validate_spec(:integer, nil), do: {:type, :integer} - defp validate_spec(:float, nil), do: {:type, :float} - defp validate_spec(:binary, nil), do: {:type, :binary} - defp validate_spec(:bytes, nil), do: {:type, :binary} - defp validate_spec(:bitstring, nil), do: {:type, :bitstring} - defp validate_spec(:bits, nil), do: {:type, :bitstring} - defp validate_spec(:utf8, nil), do: {:type, :utf8} - defp validate_spec(:utf16, nil), do: {:type, :utf16} - defp validate_spec(:utf32, nil), do: {:type, :utf32} - defp validate_spec(:signed, nil), do: {:sign, :signed} - defp validate_spec(:unsigned, nil), do: {:sign, :unsigned} - defp validate_spec(_, _), do: :none - - defp expand_spec_arg(expr, s, _original_s, e) when is_atom(expr) or is_integer(expr) do - {expr, s, e} - end - - defp expand_spec_arg(expr, s, original_s, %{context: :match} = e) do - %{prematch: {pre_read, pre_counter, _} = old_pre} = s - %{vars: {original_read, _}} = original_s - new_pre = {pre_read, pre_counter, {:bitsize, original_read}} - - {e_expr, se, ee} = - ElixirExpand.expand(expr, %{s | prematch: new_pre}, %{e | context: :guard}) - - {e_expr, %{se | prematch: old_pre}, %{ee | context: :match}} - end - - defp expand_spec_arg(expr, s, original_s, e) do - ElixirExpand.expand(expr, reset_read(s, original_s), e) - end - - defp size_and_unit(type, size, unit) - when type in [:bitstring, :binary] and (size != :default or unit != :default) do - # elixir raises here bittype_literal_bitstring or bittype_literal_string - # we don't care - size_and_unit(type, :default, :default) - end - - defp size_and_unit(_expr_type, size, unit) do - add_arg(:unit, unit, add_arg(:size, size, [])) - end - - defp build_spec(_size, _unit, type, endianness, _sign, spec) - when type in [:utf8, :utf16, :utf32] do - # elixir raises bittype_signed if signed - # elixir raises bittype_utf if size specified - # we don't care - - add_spec(type, add_spec(endianness, spec)) - end - - defp build_spec(_size, _unit, type, _endianness, _sign, spec) - when type in [:binary, :bitstring] do - # elixir raises bittype_signed if signed - # elixir raises bittype_mismatch if bitstring unit != 1 or default - # we don't care - - add_spec(type, spec) - end - - defp build_spec(size, unit, type, endianness, sign, spec) - when type in [:integer, :float] do - number_size = number_size(size, unit) - - cond do - type == :float and is_integer(number_size) -> - if valid_float_size(number_size) do - add_spec(type, add_spec(endianness, add_spec(sign, spec))) - else - # elixir raises here bittype_float_size - # we fall back to 64 - build_spec(64, :default, type, endianness, sign, spec) - end - - size == :default and unit != :default -> - # elixir raises here bittype_unit - # we fall back to default - build_spec(size, :default, type, endianness, sign, spec) - - true -> - add_spec(type, add_spec(endianness, add_spec(sign, spec))) - end - end - - defp add_spec(:default, spec), do: spec - defp add_spec(key, spec), do: [{key, [], nil} | spec] - - defp number_size(size, :default) when is_integer(size), do: size - defp number_size(size, unit) when is_integer(size), do: size * unit - defp number_size(size, _), do: size - - defp valid_float_size(16), do: true - defp valid_float_size(32), do: true - defp valid_float_size(64), do: true - defp valid_float_size(_), do: false - - defp add_arg(_key, :default, spec), do: spec - defp add_arg(key, arg, spec), do: [{key, [], [arg]} | spec] - - defp is_match_size([_ | _], %{context: :match}), do: true - defp is_match_size(_, _), do: false - end - - defmodule Fn do - alias ElixirSense.Core.Compiler, as: ElixirExpand - alias ElixirSense.Core.Compiler.Clauses, as: ElixirClauses - alias ElixirSense.Core.Compiler.Dispatch, as: ElixirDispatch - alias ElixirSense.Core.Compiler.Utils, as: ElixirUtils - - def expand(meta, clauses, s, e) when is_list(clauses) do - transformer = fn - {:->, _, [_left, _right]} = clause, sa -> - # elixir raises defaults_in_args - s_reset = new_vars_scope(sa) - - # no point in doing type inference here, we have no idea what the fn will be called with - {e_clause, s_acc, _e_acc} = - ElixirClauses.clause(&ElixirClauses.head/3, clause, s_reset, e) - - {e_clause, remove_vars_scope(s_acc, sa)} - end - - {e_clauses, se} = Enum.map_reduce(clauses, s, transformer) - - {{:fn, meta, e_clauses}, se, e} - end - - # Capture - - def capture(meta, {:/, _, [{{:., _, [_m, f]} = dot, require_meta, []}, a]}, s, e) - when is_atom(f) and is_integer(a) do - args = args_from_arity(meta, a) - - capture_require({dot, require_meta, args}, s, e, true) - end - - def capture(meta, {:/, _, [{f, import_meta, c}, a]}, s, e) - when is_atom(f) and is_integer(a) and is_atom(c) do - args = args_from_arity(meta, a) - capture_import({f, import_meta, args}, s, e, true) - end - - def capture(_meta, {{:., _, [_, fun]}, _, args} = expr, s, e) - when is_atom(fun) and is_list(args) do - capture_require(expr, s, e, is_sequential_and_not_empty(args)) - end - - def capture(meta, {{:., _, [_]}, _, args} = expr, s, e) when is_list(args) do - capture_expr(meta, expr, s, e, false) - end - - def capture(meta, {:__block__, _, [expr]}, s, e) do - capture(meta, expr, s, e) - end - - def capture(meta, {:__block__, _, expr}, s, e) do - # elixir raises block_expr_in_capture - # try to recover from error - expr = - case expr do - [] -> - {:"&1", meta, e.module} - - list -> - ElixirUtils.select_with_cursor(list) || hd(list) - end - - capture(meta, expr, s, e) - end - - def capture(_meta, {atom, _, args} = expr, s, e) when is_atom(atom) and is_list(args) do - capture_import(expr, s, e, is_sequential_and_not_empty(args)) - end - - def capture(meta, {left, right}, s, e) do - capture(meta, {:{}, meta, [left, right]}, s, e) - end - - def capture(meta, list, s, e) when is_list(list) do - capture_expr(meta, list, s, e, is_sequential_and_not_empty(list)) - end - - def capture(meta, integer, s, e) when is_integer(integer) do - # elixir raises here capture_arg_outside_of_capture - # emit fake capture - capture(meta, [{:&, meta, [1]}], s, e) - end - - def capture(meta, arg, s, e) do - # elixir raises invalid_args_for_capture - # we try to transform the capture to local fun capture - case arg do - {var, _, context} when is_atom(var) and is_atom(context) -> - capture(meta, {:/, meta, [arg, 0]}, s, e) - - _ -> - # try to wrap it in list - capture(meta, [arg], s, e) - end - end - - defp capture_import({atom, import_meta, args} = expr, s, e, sequential) do - res = - if sequential do - ElixirDispatch.import_function(import_meta, atom, length(args), s, e) - else - false - end - - handle_capture(res, import_meta, import_meta, expr, s, e, sequential) - end - - defp capture_require({{:., dot_meta, [left, right]}, require_meta, args}, s, e, sequential) do - case escape(left, []) do - {esc_left, []} -> - {e_left, se, ee} = ElixirExpand.expand(esc_left, s, e) - - res = - if sequential do - case e_left do - {name, _, context} when is_atom(name) and is_atom(context) -> - {:remote, e_left, right, length(args)} - - _ when is_atom(e_left) -> - ElixirDispatch.require_function( - require_meta, - e_left, - right, - length(args), - s, - ee - ) - - _ -> - false - end - else - false - end - - dot = {{:., dot_meta, [e_left, right]}, require_meta, args} - handle_capture(res, require_meta, dot_meta, dot, se, ee, sequential) - - {esc_left, escaped} -> - dot = {{:., dot_meta, [esc_left, right]}, require_meta, args} - capture_expr(require_meta, dot, s, e, escaped, sequential) - end - end - - defp handle_capture(false, meta, _dot_meta, expr, s, e, sequential) do - capture_expr(meta, expr, s, e, sequential) - end - - defp handle_capture(local_or_remote, meta, dot_meta, _expr, s, e, _sequential) do - {local_or_remote, meta, dot_meta, s, e} - end - - defp capture_expr(meta, expr, s, e, sequential) do - capture_expr(meta, expr, s, e, [], sequential) - end - - defp capture_expr(meta, expr, s, e, escaped, sequential) do - case escape(expr, escaped) do - {e_expr, []} when not sequential -> - # elixir raises here invalid_args_for_capture - # we emit fn without args - fn_expr = {:fn, meta, [{:->, meta, [[], e_expr]}]} - {:expand, fn_expr, s, e} - - {e_expr, e_dict} -> - # elixir raises capture_arg_without_predecessor here - # if argument vars are not consecutive - e_vars = Enum.map(e_dict, &elem(&1, 1)) - fn_expr = {:fn, meta, [{:->, meta, [e_vars, e_expr]}]} - {:expand, fn_expr, s, e} - end - end - - defp escape({:&, meta, [pos]}, dict) when is_integer(pos) and pos > 0 do - # This might pollute user space but is unlikely because variables - # named :"&1" are not valid syntax. - var = {:"&#{pos}", meta, nil} - {var, :orddict.store(pos, var, dict)} - - case :orddict.find(pos, dict) do - {:ok, var} -> - {var, dict} - - :error -> - # elixir uses here elixir_module:next_counter(?key(E, module)) - # but we are not compiling and do not need to keep count in module scope - # elixir 1.17 also renames the var to `capture` - next = System.unique_integer() - var = {:"&#{pos}", [{:counter, next} | meta], nil} - {var, :orddict.store(pos, var, dict)} - end - end - - defp escape({:&, meta, [pos]}, dict) when is_integer(pos) do - # elixir raises here invalid_arity_for_capture - # we substitute arg number - escape({:&, meta, [1]}, dict) - end - - defp escape({:&, _meta, args}, dict) do - # elixir raises here nested_capture - # try to recover from error by dropping & - escape(args, dict) - end - - defp escape({left, meta, right}, dict0) do - {t_left, dict1} = escape(left, dict0) - {t_right, dict2} = escape(right, dict1) - {{t_left, meta, t_right}, dict2} - end - - defp escape({left, right}, dict0) do - {t_left, dict1} = escape(left, dict0) - {t_right, dict2} = escape(right, dict1) - {{t_left, t_right}, dict2} - end - - defp escape(list, dict) when is_list(list) do - Enum.map_reduce(list, dict, fn x, acc -> escape(x, acc) end) - end - - defp escape(other, dict) do - {other, dict} - end - - defp args_from_arity(_meta, 0), do: [] - - defp args_from_arity(meta, a) when is_integer(a) and a >= 1 and a <= 255 do - for x <- 1..a do - {:&, meta, [x]} - end - end - - defp args_from_arity(_meta, _a) do - # elixir raises invalid_arity_for_capture - [] - end - - defp is_sequential_and_not_empty([]), do: false - defp is_sequential_and_not_empty(list), do: is_sequential(list, 1) - - defp is_sequential([{:&, _, [int]} | t], int), do: is_sequential(t, int + 1) - defp is_sequential([], _int), do: true - defp is_sequential(_, _int), do: false - end - - defmodule Quote do - alias ElixirSense.Core.Compiler.Dispatch, as: ElixirDispatch - - defstruct line: false, - file: nil, - context: nil, - op: :none, - aliases_hygiene: nil, - imports_hygiene: nil, - unquote: true, - generated: false - - def fun_to_quoted(function) do - {:module, module} = :erlang.fun_info(function, :module) - {:name, name} = :erlang.fun_info(function, :name) - {:arity, arity} = :erlang.fun_info(function, :arity) - - {:&, [], [{:/, [], [{{:., [], [module, name]}, [{:no_parens, true}], []}, arity]}]} - end - - def has_unquotes(ast), do: has_unquotes(ast, 0) - - def has_unquotes({:quote, _, [child]}, quote_level) do - has_unquotes(child, quote_level + 1) - end - - def has_unquotes({:quote, _, [quote_opts, child]}, quote_level) do - case disables_unquote(quote_opts) do - true -> false - _ -> has_unquotes(child, quote_level + 1) - end - end - - def has_unquotes({unquote, _, [child]}, quote_level) - when unquote in [:unquote, :unquote_splicing] do - case quote_level do - 0 -> true - _ -> has_unquotes(child, quote_level - 1) - end - end - - def has_unquotes({{:., _, [_, :unquote]}, _, [_]}, _), do: true - def has_unquotes({var, _, ctx}, _) when is_atom(var) and is_atom(ctx), do: false - - def has_unquotes({name, _, args}, quote_level) when is_list(args) do - has_unquotes(name) or Enum.any?(args, fn child -> has_unquotes(child, quote_level) end) - end - - def has_unquotes({left, right}, quote_level) do - has_unquotes(left, quote_level) or has_unquotes(right, quote_level) - end - - def has_unquotes(list, quote_level) when is_list(list) do - Enum.any?(list, fn child -> has_unquotes(child, quote_level) end) - end - - def has_unquotes(_other, _), do: false - - defp disables_unquote([{:unquote, false} | _]), do: true - defp disables_unquote([{:bind_quoted, _} | _]), do: true - defp disables_unquote([_h | t]), do: disables_unquote(t) - defp disables_unquote(_), do: false - - def build(meta, line, file, context, unquote, generated, e) do - acc0 = [] - - {v_line, acc1} = validate_compile(meta, :line, line, acc0) - {v_file, acc2} = validate_compile(meta, :file, file, acc1) - {v_context, acc3} = validate_compile(meta, :context, context, acc2) - - unquote = validate_runtime(:unquote, unquote) - generated = validate_runtime(:generated, generated) - - q = %__MODULE__{ - op: :add_context, - aliases_hygiene: e, - imports_hygiene: e, - line: v_line, - file: v_file, - unquote: unquote, - context: v_context, - generated: generated - } - - {q, v_context, acc3} - end - - defp validate_compile(_meta, :line, value, acc) when is_boolean(value) do - {value, acc} - end - - defp validate_compile(_meta, :file, nil, acc) do - {nil, acc} - end - - defp validate_compile(meta, key, value, acc) do - case is_valid(key, value) do - true -> - {value, acc} - - false -> - var = {key, meta, __MODULE__} - call = {{:., meta, [__MODULE__, :validate_runtime]}, meta, [key, value]} - {var, [{:=, meta, [var, call]} | acc]} - end - end - - defp validate_runtime(key, value) do - case is_valid(key, value) do - true -> - value - - false -> - # elixir raises here invalid runtime value for option - default(key) - end - end - - defp is_valid(:line, line), do: is_integer(line) - defp is_valid(:file, file), do: is_binary(file) - defp is_valid(:context, context), do: is_atom(context) and context != nil - defp is_valid(:generated, generated), do: is_boolean(generated) - defp is_valid(:unquote, unquote), do: is_boolean(unquote) - defp default(:unquote), do: true - defp default(:generated), do: false - - def escape(expr, op, unquote) do - do_quote( - expr, - %__MODULE__{ - line: true, - file: nil, - op: op, - unquote: unquote - } - ) - end - - def quote({:unquote_splicing, _, [_]} = expr, %__MODULE__{unquote: true} = q) do - # elixir raises here unquote_splicing only works inside arguments and block contexts - # try to recover from error by wrapping it in block - __MODULE__.quote({:__block__, [], [expr]}, q) - end - - def quote(expr, q) do - do_quote(expr, q) - end - - # quote/unquote - - defp do_quote({:quote, meta, [arg]}, q) when is_list(meta) do - t_arg = do_quote(arg, %__MODULE__{q | unquote: false}) - - new_meta = - case q do - %__MODULE__{op: :add_context, context: context} -> - keystore(:context, meta, context) - - _ -> - meta - end - - {:{}, [], [:quote, meta(new_meta, q), [t_arg]]} - end - - defp do_quote({:quote, meta, [opts, arg]}, q) when is_list(meta) do - t_opts = do_quote(opts, q) - t_arg = do_quote(arg, %__MODULE__{q | unquote: false}) - - new_meta = - case q do - %__MODULE__{op: :add_context, context: context} -> - keystore(:context, meta, context) - - _ -> - meta - end - - {:{}, [], [:quote, meta(new_meta, q), [t_opts, t_arg]]} - end - - defp do_quote({:unquote, meta, [expr]}, %__MODULE__{unquote: true}) when is_list(meta), - do: expr - - # Aliases - - defp do_quote({:__aliases__, meta, [h | t] = list}, %__MODULE__{aliases_hygiene: e = %{}} = q) - when is_atom(h) and h != Elixir and is_list(meta) do - annotation = - case NormalizedMacroEnv.expand_alias(e, meta, list, trace: false) do - {:alias, atom} -> atom - :error -> false - end - - alias_meta = keystore(:alias, Keyword.delete(meta, :counter), annotation) - do_quote_tuple(:__aliases__, alias_meta, [h | t], q) - end - - # Vars - - defp do_quote({name, meta, nil}, %__MODULE__{op: :add_context} = q) - when is_atom(name) and is_list(meta) do - import_meta = - case q.imports_hygiene do - nil -> meta - e -> import_meta(meta, name, 0, q, e) - end - - {:{}, [], [name, meta(import_meta, q), q.context]} - end - - # cursor - - defp do_quote( - {:__cursor__, meta, args}, - %__MODULE__{unquote: _} - ) - when is_list(args) do - # emit cursor as is regardless of unquote - {:__cursor__, meta, args} - end - - # Unquote - - defp do_quote( - {{{:., meta, [left, :unquote]}, _, [expr]}, _, args}, - %__MODULE__{unquote: true} = q - ) - when is_list(meta) do - do_quote_call(left, meta, expr, args, q) - end - - defp do_quote({{:., meta, [left, :unquote]}, _, [expr]}, %__MODULE__{unquote: true} = q) - when is_list(meta) do - do_quote_call(left, meta, expr, nil, q) - end - - # Imports - - defp do_quote( - {:&, meta, [{:/, _, [{f, _, c}, a]}] = args}, - %__MODULE__{imports_hygiene: e = %{}} = q - ) - when is_atom(f) and is_integer(a) and is_atom(c) and is_list(meta) do - new_meta = - case ElixirDispatch.find_import(meta, f, a, e) do - false -> - meta - - receiver -> - keystore(:context, keystore(:imports, meta, [{a, receiver}]), q.context) - end - - do_quote_tuple(:&, new_meta, args, q) - end - - defp do_quote({name, meta, args_or_context}, %__MODULE__{imports_hygiene: e = %{}} = q) - when is_atom(name) and is_list(meta) and - (is_list(args_or_context) or is_atom(args_or_context)) do - arity = - case args_or_context do - args when is_list(args) -> length(args) - context when is_atom(context) -> 0 - end - - import_meta = import_meta(meta, name, arity, q, e) - annotated = annotate({name, import_meta, args_or_context}, q.context) - do_quote_tuple(annotated, q) - end - - # Two-element tuples - - defp do_quote({left, right}, %__MODULE__{unquote: true} = q) - when is_tuple(left) and elem(left, 0) == :unquote_splicing and - is_tuple(right) and elem(right, 0) == :unquote_splicing do - do_quote({:{}, [], [left, right]}, q) - end - - defp do_quote({left, right}, q) do - t_left = do_quote(left, q) - t_right = do_quote(right, q) - {t_left, t_right} - end - - # Everything else - - defp do_quote(other, q = %{op: op}) when op != :add_context do - do_escape(other, q) - end - - defp do_quote({_, _, _} = tuple, q) do - annotated = annotate(tuple, q.context) - do_quote_tuple(annotated, q) - end - - defp do_quote([], _), do: [] - - defp do_quote([h | t], %__MODULE__{unquote: false} = q) do - head_quoted = do_quote(h, q) - do_quote_simple_list(t, head_quoted, q) - end - - defp do_quote([h | t], q) do - do_quote_tail(:lists.reverse(t, [h]), q) - end - - defp do_quote(other, _), do: other - - defp import_meta(meta, name, arity, q, e) do - case Keyword.get(meta, :imports, false) == false && - ElixirDispatch.find_imports(meta, name, e) do - [_ | _] = imports -> - keystore(:imports, keystore(:context, meta, q.context), imports) - - _ -> - case arity == 1 && Keyword.fetch(meta, :ambiguous_op) do - {:ok, nil} -> - keystore(:ambiguous_op, meta, q.context) - - _ -> - meta - end - end - end - - defp do_quote_call(left, meta, expr, args, q) do - all = [left, {:unquote, meta, [expr]}, args, q.context] - tall = Enum.map(all, fn x -> do_quote(x, q) end) - {{:., meta, [:elixir_quote, :dot]}, meta, [meta(meta, q) | tall]} - end - - defp do_quote_tuple({left, meta, right}, q) do - do_quote_tuple(left, meta, right, q) - end - - defp do_quote_tuple(left, meta, right, q) do - t_left = do_quote(left, q) - t_right = do_quote(right, q) - {:{}, [], [t_left, meta(meta, q), t_right]} - end - - defp do_quote_simple_list([], prev, _), do: [prev] - - defp do_quote_simple_list([h | t], prev, q) do - [prev | do_quote_simple_list(t, do_quote(h, q), q)] - end - - defp do_quote_simple_list(other, prev, q) do - [{:|, [], [prev, do_quote(other, q)]}] - end - - defp do_quote_tail( - [{:|, meta, [{:unquote_splicing, _, [left]}, right]} | t], - %__MODULE__{unquote: true} = q - ) do - tt = do_quote_splice(t, q, [], []) - tr = do_quote(right, q) - do_runtime_list(meta, :tail_list, [left, tr, tt]) - end - - defp do_quote_tail(list, q) do - do_quote_splice(list, q, [], []) - end - - defp do_quote_splice( - [{:unquote_splicing, meta, [expr]} | t], - %__MODULE__{unquote: true} = q, - buffer, - acc - ) do - runtime = do_runtime_list(meta, :list, [expr, do_list_concat(buffer, acc)]) - do_quote_splice(t, q, [], runtime) - end - - defp do_quote_splice([h | t], q, buffer, acc) do - th = do_quote(h, q) - do_quote_splice(t, q, [th | buffer], acc) - end - - defp do_quote_splice([], _q, buffer, acc) do - do_list_concat(buffer, acc) - end - - defp do_list_concat(left, []), do: left - defp do_list_concat([], right), do: right - - defp do_list_concat(left, right) do - {{:., [], [:erlang, :++]}, [], [left, right]} - end - - defp do_runtime_list(meta, fun, args) do - {{:., meta, [:elixir_quote, fun]}, meta, args} - end - - defp meta(meta, q) do - generated(keep(Keyword.delete(meta, :column), q), q) - end - - defp generated(meta, %__MODULE__{generated: true}), do: [{:generated, true} | meta] - defp generated(meta, %__MODULE__{generated: false}), do: meta - - defp keep(meta, %__MODULE__{file: nil, line: line}) do - line(meta, line) - end - - defp keep(meta, %__MODULE__{file: file, line: true}) do - case Keyword.pop(meta, :line) do - {nil, _} -> - [{:keep, {file, 0}} | meta] - - {line, meta_no_line} -> - [{:keep, {file, line}} | meta_no_line] - end - end - - defp keep(meta, %__MODULE__{file: file, line: false}) do - [{:keep, {file, 0}} | Keyword.delete(meta, :line)] - end - - defp keep(meta, %__MODULE__{file: file, line: line}) do - [{:keep, {file, line}} | Keyword.delete(meta, :line)] - end - - defp line(meta, true), do: meta - - defp line(meta, false) do - Keyword.delete(meta, :line) - end - - defp line(meta, line) do - keystore(:line, meta, line) - end - - defguardp defs(kind) when kind in [:def, :defp, :defmacro, :defmacrop, :@] - defguardp lexical(kind) when kind in [:import, :alias, :require] - - defp annotate({def, meta, [h | t]}, context) when defs(def) do - {def, meta, [annotate_def(h, context) | t]} - end - - defp annotate({{:., _, [_, def]} = target, meta, [h | t]}, context) when defs(def) do - {target, meta, [annotate_def(h, context) | t]} - end - - defp annotate({lexical, meta, [_ | _] = args}, context) when lexical(lexical) do - new_meta = keystore(:context, Keyword.delete(meta, :counter), context) - {lexical, new_meta, args} - end - - defp annotate(tree, _context), do: tree - - defp annotate_def({:when, meta, [left, right]}, context) do - {:when, meta, [annotate_def(left, context), right]} - end - - defp annotate_def({fun, meta, args}, context) do - {fun, keystore(:context, meta, context), args} - end - - defp annotate_def(other, _context), do: other - - defp do_escape({left, meta, right}, q = %{op: :prune_metadata}) when is_list(meta) do - tm = for {k, v} <- meta, k == :no_parens or k == :line, do: {k, v} - tl = do_quote(left, q) - tr = do_quote(right, q) - {:{}, [], [tl, tm, tr]} - end - - defp do_escape(tuple, q) when is_tuple(tuple) do - tt = do_quote(Tuple.to_list(tuple), q) - {:{}, [], tt} - end - - defp do_escape(bitstring, _) when is_bitstring(bitstring) do - case Bitwise.band(bit_size(bitstring), 7) do - 0 -> - bitstring - - size -> - <> = bitstring - - {:<<>>, [], - [{:"::", [], [bits, {size, [], [size]}]}, {:"::", [], [bytes, {:binary, [], nil}]}]} - end - end - - defp do_escape(map, q) when is_map(map) do - tt = do_quote(Enum.sort(Map.to_list(map)), q) - {:%{}, [], tt} - end - - defp do_escape([], _), do: [] - - defp do_escape([h | t], %__MODULE__{unquote: false} = q) do - do_quote_simple_list(t, do_quote(h, q), q) - end - - defp do_escape([h | t], q) do - # The improper case is inefficient, but improper lists are rare. - try do - l = Enum.reverse(t, [h]) - do_quote_tail(l, q) - catch - _ -> - {l, r} = reverse_improper(t, [h]) - tl = do_quote_splice(l, q, [], []) - tr = do_quote(r, q) - update_last(tl, fn x -> {:|, [], [x, tr]} end) - end - end - - defp do_escape(other, _) when is_number(other) or is_pid(other) or is_atom(other), - do: other - - defp do_escape(fun, _) when is_function(fun) do - case {Function.info(fun, :env), Function.info(fun, :type)} do - {{:env, []}, {:type, :external}} -> - fun_to_quoted(fun) - - _ -> - # elixir raises here ArgumentError - nil - end - end - - defp do_escape(_other, _) do - # elixir raises here ArgumentError - nil - end - - defp reverse_improper([h | t], acc), do: reverse_improper(t, [h | acc]) - defp reverse_improper([], acc), do: acc - defp reverse_improper(t, acc), do: {acc, t} - defp update_last([], _), do: [] - defp update_last([h], f), do: [f.(h)] - defp update_last([h | t], f), do: [h | update_last(t, f)] - - defp keystore(_key, meta, value) when value == nil do - meta - end - - defp keystore(key, meta, value) do - :lists.keystore(key, 1, meta, {key, value}) - end - end - - defmodule Dispatch do - alias ElixirSense.Core.Compiler.Rewrite, as: ElixirRewrite - import :ordsets, only: [is_element: 2] - - def find_import(meta, name, arity, e) do - tuple = {name, arity} - - case find_import_by_name_arity(meta, tuple, [], e) do - {:function, receiver} -> - # TODO trace call? - # TODO address when https://github.com/elixir-lang/elixir/issues/13878 is resolved - # ElixirEnv.trace({:imported_function, meta, receiver, name, arity}, e) - receiver - - {:macro, receiver} -> - # TODO trace call? - # ElixirEnv.trace({:imported_macro, meta, receiver, name, arity}, e) - receiver - - {:ambiguous, [head | _]} -> - # elixir raises here, we choose first one - # TODO trace call? - head - - _ -> - false - end - end - - def find_imports(meta, name, e) do - funs = e.functions - macs = e.macros - - acc0 = %{} - acc1 = find_imports_by_name(funs, acc0, name, meta, e) - acc2 = find_imports_by_name(macs, acc1, name, meta, e) - - imports = acc2 |> Map.to_list() |> Enum.sort() - # trace_import_quoted(imports, meta, name, e) - imports - end - - def import_function(meta, name, arity, s, e) do - tuple = {name, arity} - - case find_import_by_name_arity(meta, tuple, [], e) do - {:function, receiver} -> - remote_function(meta, receiver, name, arity, e) - - {:macro, _receiver} -> - false - - {:import, receiver} -> - require_function(meta, receiver, name, arity, s, e) - - {:ambiguous, [first | _]} -> - # elixir raises here, we return first matching - require_function(meta, first, name, arity, s, e) - - false -> - if Macro.special_form?(name, arity) do - false - else - function = e.function - - mfa = {e.module, name, arity} - - if function != nil and function != tuple and - Enum.any?(s.mods_funs_to_positions, fn {key, info} -> - key == mfa and State.ModFunInfo.get_category(info) == :macro - end) do - false - else - {:local, name, arity} - end - end - end - end - - def require_function(meta, receiver, name, arity, s, e) do - required = receiver in e.requires - - if is_macro(name, arity, receiver, required, s) do - false - else - remote_function(meta, receiver, name, arity, e) - end - end - - defp remote_function(_meta, receiver, name, arity, _e) do - case ElixirRewrite.inline(receiver, name, arity) do - {ar, an} -> {:remote, ar, an, arity} - false -> {:remote, receiver, name, arity} - end - end - - def find_imports_by_name([{mod, imports} | mod_imports], acc, name, meta, e) do - new_acc = find_imports_by_name(name, imports, acc, mod, meta, e) - find_imports_by_name(mod_imports, new_acc, name, meta, e) - end - - def find_imports_by_name([], acc, _name, _meta, _e), do: acc - - def find_imports_by_name(name, [{name, arity} | imports], acc, mod, meta, e) do - case Map.get(acc, arity) do - nil -> - find_imports_by_name(name, imports, Map.put(acc, arity, mod), mod, meta, e) - - _other_mod -> - # elixir raises here ambiguous_call - find_imports_by_name(name, imports, acc, mod, meta, e) - end - end - - def find_imports_by_name(name, [{import_name, _} | imports], acc, mod, meta, e) - when name > import_name do - find_imports_by_name(name, imports, acc, mod, meta, e) - end - - def find_imports_by_name(_name, _imports, acc, _mod, _meta, _e), do: acc - - defp find_import_by_name_arity(meta, {_name, arity} = tuple, extra, e) do - case is_import(meta, arity) do - {:import, _} = import_res -> - import_res - - false -> - funs = e.functions - macs = extra ++ e.macros - fun_match = find_import_by_name_arity(tuple, funs) - mac_match = find_import_by_name_arity(tuple, macs) - - case {fun_match, mac_match} do - {[], [receiver]} -> - {:macro, receiver} - - {[receiver], []} -> - {:function, receiver} - - {[], []} -> - false - - _ -> - {:ambiguous, fun_match ++ mac_match} - end - end - end - - defp find_import_by_name_arity(tuple, list) do - for {receiver, set} <- list, is_element(tuple, set), do: receiver - end - - defp is_import(meta, arity) do - with {:ok, imports = [_ | _]} <- Keyword.fetch(meta, :imports), - {:ok, _} <- Keyword.fetch(meta, :context), - {_arity, receiver} <- :lists.keyfind(arity, 1, imports) do - {:import, receiver} - else - _ -> false - end - end - - defp is_macro(_name, _arity, _module, false, _s), do: false - - defp is_macro(name, arity, receiver, true, s) do - mfa = {receiver, name, arity} - - Enum.any?(s.mods_funs_to_positions, fn {key, info} -> - key == mfa and State.ModFunInfo.get_category(info) == :macro - end) || - try do - macros = receiver.__info__(:macros) - {name, arity} in macros - rescue - _error -> false - end - end - end - - defmodule Map do - alias ElixirSense.Core.Compiler, as: ElixirExpand - - def expand_struct(meta, left, {:%{}, map_meta, map_args}, s, %{context: context} = e) do - clean_map_args = clean_struct_key_from_map_args(map_args) - - {[e_left, e_right], se, ee} = - ElixirExpand.expand_args([left, {:%{}, map_meta, clean_map_args}], s, e) - - case validate_struct(e_left, context) do - true when is_atom(e_left) -> - # TODO register alias/struct - case extract_struct_assocs(e_right) do - {:expand, map_meta, assocs} when context != :match -> - assoc_keys = Enum.map(assocs, fn {k, _} -> k end) - struct = load_struct(e_left, [assocs], se, ee) - keys = [:__struct__ | assoc_keys] - without_keys = Elixir.Map.drop(struct, keys) - - struct_assocs = - ElixirExpand.Macro.escape(Enum.sort(Elixir.Map.to_list(without_keys))) - - {{:%, meta, [e_left, {:%{}, map_meta, struct_assocs ++ assocs}]}, se, ee} - - {_, _, _assocs} -> - # elixir validates assocs against struct keys - # we don't need to validate keys - {{:%, meta, [e_left, e_right]}, se, ee} - end - - _ -> - # elixir raises invalid_struct_name if validate_struct returns false - {{:%, meta, [e_left, e_right]}, se, ee} - end - end - - def expand_struct(meta, left, right, s, e) do - # elixir raises here non_map_after_struct - # try to recover from error by wrapping the expression in map - expand_struct(meta, left, wrap_in_fake_map(right), s, e) - end - - defp wrap_in_fake_map(right) do - map_args = - case right do - list when is_list(list) -> - if Keyword.keyword?(list) do - list - else - [__fake_key__: list] - end - - _ -> - [__fake_key__: right] - end - - {:%{}, [], map_args} - end - - def expand_map(meta, [{:|, update_meta, [left, right]}], s, e) do - # elixir raises update_syntax_in_wrong_context if e.context is not nil - {[e_left | e_right], se, ee} = ElixirExpand.expand_args([left | right], s, e) - e_right = sanitize_kv(e_right, e) - {{:%{}, meta, [{:|, update_meta, [e_left, e_right]}]}, se, ee} - end - - def expand_map(meta, args, s, e) do - {e_args, se, ee} = ElixirExpand.expand_args(args, s, e) - e_args = sanitize_kv(e_args, e) - {{:%{}, meta, e_args}, se, ee} - end - - defp clean_struct_key_from_map_args([{:|, pipe_meta, [left, map_assocs]}]) do - [{:|, pipe_meta, [left, delete_struct_key(map_assocs)]}] - end - - defp clean_struct_key_from_map_args(map_assocs) do - delete_struct_key(map_assocs) - end - - defp sanitize_kv(kv, %{context: context}) do - Enum.filter(kv, fn - {k, _v} -> - if context == :match do - validate_match_key(k) - else - true - end - - _ -> - false - end) - end - - defp validate_match_key({name, _, context}) - when is_atom(name) and is_atom(context) do - # elixir raises here invalid_variable_in_map_key_match - false - end - - defp validate_match_key({:"::", _, [left, _]}) do - validate_match_key(left) - end - - defp validate_match_key({:^, _, [{name, _, context}]}) - when is_atom(name) and is_atom(context), - do: true - - defp validate_match_key({:%{}, _, [_ | _]}), do: true - - defp validate_match_key({left, _, right}) do - validate_match_key(left) and validate_match_key(right) - end - - defp validate_match_key({left, right}) do - validate_match_key(left) and validate_match_key(right) - end - - defp validate_match_key(list) when is_list(list) do - Enum.all?(list, &validate_match_key/1) - end - - defp validate_match_key(_), do: true - - defp validate_struct({:^, _, [{var, _, ctx}]}, :match) when is_atom(var) and is_atom(ctx), - do: true - - defp validate_struct({var, _meta, ctx}, :match) when is_atom(var) and is_atom(ctx), do: true - defp validate_struct(atom, _) when is_atom(atom), do: true - defp validate_struct(_, _), do: false - - defp sanitize_assocs(list) do - Enum.filter(list, &match?({k, _} when is_atom(k), &1)) - end - - defp extract_struct_assocs({:%{}, meta, [{:|, _, [_, assocs]}]}) do - {:update, meta, delete_struct_key(sanitize_assocs(assocs))} - end - - defp extract_struct_assocs({:%{}, meta, assocs}) do - {:expand, meta, delete_struct_key(sanitize_assocs(assocs))} - end - - defp extract_struct_assocs(right) do - # elixir raises here non_map_after_struct - # try to recover from error by wrapping the expression in map - extract_struct_assocs(wrap_in_fake_map(right)) - end - - defp delete_struct_key(assocs) do - Keyword.delete(assocs, :__struct__) - end - - def load_struct(name, assocs, s, _e) do - case s.structs[name] do - nil -> - try do - apply(name, :__struct__, assocs) - else - %{:__struct__ => ^name} = struct -> - struct - - _ -> - # recover from invalid return value - [__struct__: name] |> merge_assocs(assocs) - rescue - _ -> - # recover from error by building the fake struct - [__struct__: name] |> merge_assocs(assocs) - end - - info -> - info.fields |> merge_assocs(assocs) - end - end - - defp merge_assocs(fields, []) do - fields |> Elixir.Map.new() - end - - defp merge_assocs(fields, [assocs]) do - fields |> Keyword.merge(assocs) |> Elixir.Map.new() - end - end - - defmodule Rewrite do - def inline(module, fun, arity) do - :elixir_rewrite.inline(module, fun, arity) - end - - def rewrite(context, receiver, dot_meta, right, meta, e_args, s) do - do_rewrite(context, receiver, dot_meta, right, meta, e_args, s) - end - - defp do_rewrite(_, :erlang, _, :+, _, [arg], _s) when is_number(arg), do: {:ok, arg} - - defp do_rewrite(_, :erlang, _, :-, _, [arg], _s) when is_number(arg), do: {:ok, -arg} - - defp do_rewrite(:match, receiver, dot_meta, right, meta, e_args, _s) do - :elixir_rewrite.match_rewrite(receiver, dot_meta, right, meta, e_args) - end - - if Version.match?(System.version(), "< 1.14.0") do - defp do_rewrite(:guard, receiver, dot_meta, right, meta, e_args, s) do - :elixir_rewrite.guard_rewrite(receiver, dot_meta, right, meta, e_args) - end - else - defp do_rewrite(:guard, receiver, dot_meta, right, meta, e_args, _s) do - # elixir uses guard context for error messages - :elixir_rewrite.guard_rewrite(receiver, dot_meta, right, meta, e_args, "guard") - end - end - - defp do_rewrite(_, receiver, dot_meta, right, meta, e_args, _s) do - {:ok, :elixir_rewrite.rewrite(receiver, dot_meta, right, meta, e_args)} - end - end end diff --git a/lib/elixir_sense/core/compiler/bitstring.ex b/lib/elixir_sense/core/compiler/bitstring.ex new file mode 100644 index 00000000..9ba106ea --- /dev/null +++ b/lib/elixir_sense/core/compiler/bitstring.ex @@ -0,0 +1,427 @@ +defmodule ElixirSense.Core.Compiler.Bitstring do + alias ElixirSense.Core.Compiler, as: ElixirExpand + alias ElixirSense.Core.Compiler.Utils, as: ElixirUtils + alias ElixirSense.Core.State + + defp expand_match(expr, {s, original_s}, e) do + {e_expr, se, ee} = ElixirExpand.expand(expr, s, e) + {e_expr, {se, original_s}, ee} + end + + def expand(meta, args, s, e, require_size) do + case Map.get(e, :context) do + :match -> + {e_args, alignment, {sa, _}, ea} = + expand(meta, &expand_match/3, args, [], {s, s}, e, 0, require_size) + + # elixir validates if there is no nested match + + {{:<<>>, [{:alignment, alignment} | meta], e_args}, sa, ea} + + _ -> + pair_s = {State.prepare_write(s), s} + + {e_args, alignment, {sa, _}, ea} = + expand(meta, &ElixirExpand.expand_arg/3, args, [], pair_s, e, 0, require_size) + + {{:<<>>, [{:alignment, alignment} | meta], e_args}, State.close_write(sa, s), ea} + end + end + + def expand(_bitstr_meta, _fun, [], acc, s, e, alignment, _require_size) do + {Enum.reverse(acc), alignment, s, e} + end + + def expand( + bitstr_meta, + fun, + [{:"::", meta, [left, right]} | t], + acc, + s, + e, + alignment, + require_size + ) do + {e_left, {sl, original_s}, el} = expand_expr(meta, left, fun, s, e) + + match_or_require_size = require_size or is_match_size(t, el) + e_type = expr_type(e_left) + + expect_size = + case e_left do + _ when not match_or_require_size -> :optional + {:^, _, [{_, _, _}]} -> {:infer, e_left} + _ -> :required + end + + {e_right, e_alignment, ss, es} = + expand_specs(e_type, meta, right, sl, original_s, el, expect_size) + + e_acc = concat_or_prepend_bitstring(meta, e_left, e_right, acc) + + expand( + bitstr_meta, + fun, + t, + e_acc, + {ss, original_s}, + es, + alignment(alignment, e_alignment), + require_size + ) + end + + def expand(bitstr_meta, fun, [h | t], acc, s, e, alignment, require_size) do + meta = extract_meta(h, bitstr_meta) + {e_left, {ss, original_s}, es} = expand_expr(meta, h, fun, s, e) + + e_type = expr_type(e_left) + e_right = infer_spec(e_type, meta) + + inferred_meta = [{:inferred_bitstring_spec, true} | meta] + + e_acc = + concat_or_prepend_bitstring( + inferred_meta, + e_left, + e_right, + acc + ) + + expand(meta, fun, t, e_acc, {ss, original_s}, es, alignment, require_size) + end + + defp expand_expr( + _meta, + {{:., _, [mod, :to_string]}, _, [arg]} = ast, + fun, + s, + %{context: context} = e + ) + when context != nil and (mod == Kernel or mod == String.Chars) do + case fun.(arg, s, e) do + {ebin, se, ee} when is_binary(ebin) -> {ebin, se, ee} + _ -> fun.(ast, s, e) + end + end + + defp expand_expr(_meta, component, fun, s, e) do + case fun.(component, s, e) do + {e_component, s, e} when is_list(e_component) or is_atom(e_component) -> + # elixir raises here invalid_literal + # try to recover from error by replacing it with "" + {"", s, e} + + expanded -> + expanded + end + end + + defp expand_specs(expr_type, meta, info, s, original_s, e, expect_size) do + default = + %{size: :default, unit: :default, sign: :default, type: :default, endianness: :default} + + {specs, ss, es} = + expand_each_spec(meta, unpack_specs(info, []), default, s, original_s, e) + + merged_type = type(expr_type, specs.type) + + # elixir validates if unsized binary is not on the end + + size_and_unit = size_and_unit(expr_type, specs.size, specs.unit) + alignment = compute_alignment(merged_type, specs.size, specs.unit) + + maybe_inferred_size = + case {expect_size, merged_type, size_and_unit} do + {{:infer, pinned_var}, :binary, []} -> + [{:size, meta, [{{:., meta, [:erlang, :byte_size]}, meta, [pinned_var]}]}] + + {{:infer, pinned_var}, :bitstring, []} -> + [{:size, meta, [{{:., meta, [:erlang, :bit_size]}, meta, [pinned_var]}]}] + + _ -> + size_and_unit + end + + [h | t] = + build_spec( + specs.size, + specs.unit, + merged_type, + specs.endianness, + specs.sign, + maybe_inferred_size + ) + + {Enum.reduce(t, h, fn i, acc -> {:-, meta, [acc, i]} end), alignment, ss, es} + end + + defp type(:default, :default), do: :integer + defp type(expr_type, :default), do: expr_type + + defp type(:binary, type) when type in [:binary, :bitstring, :utf8, :utf16, :utf32], + do: type + + defp type(:bitstring, type) when type in [:binary, :bitstring], do: type + + defp type(:integer, type) when type in [:integer, :float, :utf8, :utf16, :utf32], + do: type + + defp type(:float, :float), do: :float + defp type(:default, type), do: type + + defp type(_other, _type) do + # elixir raises here bittype_mismatch + type(:default, :default) + end + + defp expand_each_spec(meta, [{:__cursor__, _, args} = h | t], map, s, original_s, e) + when is_list(args) do + {h, s, e} = ElixirExpand.expand(h, s, e) + + args = + case h do + nil -> t + h -> [h | t] + end + + expand_each_spec(meta, args, map, s, original_s, e) + end + + defp expand_each_spec(meta, [{expr, meta_e, args} = h | t], map, s, original_s, e) + when is_atom(expr) do + case validate_spec(expr, args) do + {key, arg} -> + {value, se, ee} = expand_spec_arg(arg, s, original_s, e) + # elixir validates spec arg here + # elixir raises bittype_mismatch in some cases + expand_each_spec(meta, t, Map.put(map, key, value), se, original_s, ee) + + :none -> + ha = + if args == nil do + {expr, meta_e, []} + else + h + end + + # TODO how to check for cursor here? + case ElixirExpand.Macro.expand(ha, Map.put(e, :line, ElixirUtils.get_line(meta))) do + ^ha -> + # elixir raises here undefined_bittype + # we omit the spec + expand_each_spec(meta, t, map, s, original_s, e) + + new_types -> + expand_each_spec(meta, unpack_specs(new_types, []) ++ t, map, s, original_s, e) + end + end + end + + defp expand_each_spec(meta, [_expr | tail], map, s, original_s, e) do + # elixir raises undefined_bittype + # we skip it + expand_each_spec(meta, tail, map, s, original_s, e) + end + + defp expand_each_spec(_meta, [], map, s, _original_s, e), do: {map, s, e} + + defp compute_alignment(_, size, unit) when is_integer(size) and is_integer(unit), + do: rem(size * unit, 8) + + defp compute_alignment(:default, size, unit), do: compute_alignment(:integer, size, unit) + defp compute_alignment(:integer, :default, unit), do: compute_alignment(:integer, 8, unit) + defp compute_alignment(:integer, size, :default), do: compute_alignment(:integer, size, 1) + defp compute_alignment(:bitstring, size, :default), do: compute_alignment(:bitstring, size, 1) + defp compute_alignment(:binary, size, :default), do: compute_alignment(:binary, size, 8) + defp compute_alignment(:binary, _, _), do: 0 + defp compute_alignment(:float, _, _), do: 0 + defp compute_alignment(:utf32, _, _), do: 0 + defp compute_alignment(:utf16, _, _), do: 0 + defp compute_alignment(:utf8, _, _), do: 0 + defp compute_alignment(_, _, _), do: :unknown + + defp alignment(left, right) when is_integer(left) and is_integer(right) do + rem(left + right, 8) + end + + defp alignment(_, _), do: :unknown + + defp extract_meta({_, meta, _}, _), do: meta + defp extract_meta(_, meta), do: meta + + defp infer_spec(:bitstring, meta), do: {:bitstring, meta, nil} + defp infer_spec(:binary, meta), do: {:binary, meta, nil} + defp infer_spec(:float, meta), do: {:float, meta, nil} + defp infer_spec(:integer, meta), do: {:integer, meta, nil} + defp infer_spec(:default, meta), do: {:integer, meta, nil} + + defp expr_type(integer) when is_integer(integer), do: :integer + defp expr_type(float) when is_float(float), do: :float + defp expr_type(binary) when is_binary(binary), do: :binary + defp expr_type({:<<>>, _, _}), do: :bitstring + defp expr_type(_), do: :default + + defp concat_or_prepend_bitstring(_meta, {:<<>>, _, []}, _e_right, acc), + do: acc + + defp concat_or_prepend_bitstring( + meta, + {:<<>>, parts_meta, parts} = e_left, + e_right, + acc + ) do + # elixir raises unsized_binary in some cases + + case e_right do + {:binary, _, nil} -> + {alignment, alignment} = Keyword.fetch!(parts_meta, :alignment) + + if is_integer(alignment) do + # elixir raises unaligned_binary if alignment != 0 + Enum.reverse(parts, acc) + else + [{:"::", meta, [e_left, e_right]} | acc] + end + + {:bitstring, _, nil} -> + Enum.reverse(parts, acc) + end + end + + defp concat_or_prepend_bitstring(meta, e_left, e_right, acc) do + [{:"::", meta, [e_left, e_right]} | acc] + end + + defp unpack_specs({:-, _, [h, t]}, acc), do: unpack_specs(h, unpack_specs(t, acc)) + + defp unpack_specs({:*, _, [{:_, _, atom}, unit]}, acc) when is_atom(atom), + do: [{:unit, [], [unit]} | acc] + + defp unpack_specs({:*, _, [size, unit]}, acc), + do: [{:size, [], [size]}, {:unit, [], [unit]} | acc] + + defp unpack_specs(size, acc) when is_integer(size), do: [{:size, [], [size]} | acc] + + defp unpack_specs({expr, meta, args}, acc) when is_atom(expr) do + list_args = + cond do + is_atom(args) -> nil + is_list(args) -> args + true -> args + end + + [{expr, meta, list_args} | acc] + end + + defp unpack_specs(other, acc), do: [other | acc] + + defp validate_spec(spec, []), do: validate_spec(spec, nil) + defp validate_spec(:big, nil), do: {:endianness, :big} + defp validate_spec(:little, nil), do: {:endianness, :little} + defp validate_spec(:native, nil), do: {:endianness, :native} + defp validate_spec(:size, [size]), do: {:size, size} + defp validate_spec(:unit, [unit]), do: {:unit, unit} + defp validate_spec(:integer, nil), do: {:type, :integer} + defp validate_spec(:float, nil), do: {:type, :float} + defp validate_spec(:binary, nil), do: {:type, :binary} + defp validate_spec(:bytes, nil), do: {:type, :binary} + defp validate_spec(:bitstring, nil), do: {:type, :bitstring} + defp validate_spec(:bits, nil), do: {:type, :bitstring} + defp validate_spec(:utf8, nil), do: {:type, :utf8} + defp validate_spec(:utf16, nil), do: {:type, :utf16} + defp validate_spec(:utf32, nil), do: {:type, :utf32} + defp validate_spec(:signed, nil), do: {:sign, :signed} + defp validate_spec(:unsigned, nil), do: {:sign, :unsigned} + defp validate_spec(_, _), do: :none + + defp expand_spec_arg(expr, s, _original_s, e) when is_atom(expr) or is_integer(expr) do + {expr, s, e} + end + + defp expand_spec_arg(expr, s, original_s, %{context: :match} = e) do + %{prematch: {pre_read, pre_counter, _} = old_pre} = s + %{vars: {original_read, _}} = original_s + new_pre = {pre_read, pre_counter, {:bitsize, original_read}} + + {e_expr, se, ee} = + ElixirExpand.expand(expr, %{s | prematch: new_pre}, %{e | context: :guard}) + + {e_expr, %{se | prematch: old_pre}, %{ee | context: :match}} + end + + defp expand_spec_arg(expr, s, original_s, e) do + ElixirExpand.expand(expr, State.reset_read(s, original_s), e) + end + + defp size_and_unit(type, size, unit) + when type in [:bitstring, :binary] and (size != :default or unit != :default) do + # elixir raises here bittype_literal_bitstring or bittype_literal_string + # we don't care + size_and_unit(type, :default, :default) + end + + defp size_and_unit(_expr_type, size, unit) do + add_arg(:unit, unit, add_arg(:size, size, [])) + end + + defp build_spec(_size, _unit, type, endianness, _sign, spec) + when type in [:utf8, :utf16, :utf32] do + # elixir raises bittype_signed if signed + # elixir raises bittype_utf if size specified + # we don't care + + add_spec(type, add_spec(endianness, spec)) + end + + defp build_spec(_size, _unit, type, _endianness, _sign, spec) + when type in [:binary, :bitstring] do + # elixir raises bittype_signed if signed + # elixir raises bittype_mismatch if bitstring unit != 1 or default + # we don't care + + add_spec(type, spec) + end + + defp build_spec(size, unit, type, endianness, sign, spec) + when type in [:integer, :float] do + number_size = number_size(size, unit) + + cond do + type == :float and is_integer(number_size) -> + if valid_float_size(number_size) do + add_spec(type, add_spec(endianness, add_spec(sign, spec))) + else + # elixir raises here bittype_float_size + # we fall back to 64 + build_spec(64, :default, type, endianness, sign, spec) + end + + size == :default and unit != :default -> + # elixir raises here bittype_unit + # we fall back to default + build_spec(size, :default, type, endianness, sign, spec) + + true -> + add_spec(type, add_spec(endianness, add_spec(sign, spec))) + end + end + + defp add_spec(:default, spec), do: spec + defp add_spec(key, spec), do: [{key, [], nil} | spec] + + defp number_size(size, :default) when is_integer(size), do: size + defp number_size(size, unit) when is_integer(size), do: size * unit + defp number_size(size, _), do: size + + defp valid_float_size(16), do: true + defp valid_float_size(32), do: true + defp valid_float_size(64), do: true + defp valid_float_size(_), do: false + + defp add_arg(_key, :default, spec), do: spec + defp add_arg(key, arg, spec), do: [{key, [], [arg]} | spec] + + defp is_match_size([_ | _], %{context: :match}), do: true + defp is_match_size(_, _), do: false +end diff --git a/lib/elixir_sense/core/compiler/clauses.ex b/lib/elixir_sense/core/compiler/clauses.ex new file mode 100644 index 00000000..db1008b7 --- /dev/null +++ b/lib/elixir_sense/core/compiler/clauses.ex @@ -0,0 +1,549 @@ +defmodule ElixirSense.Core.Compiler.Clauses do + alias ElixirSense.Core.Compiler, as: ElixirExpand + alias ElixirSense.Core.Compiler.Utils, as: ElixirUtils + alias ElixirSense.Core.State + alias ElixirSense.Core.TypeInference + + def match(fun, expr, after_s, _before_s, %{context: :match} = e) do + fun.(expr, after_s, e) + end + + def match(fun, expr, after_s, before_s, e) do + %{vars: current, unused: unused} = after_s + %{vars: {read, _write}, prematch: prematch} = before_s + + call_s = %{ + before_s + | prematch: {read, unused, :none}, + unused: unused, + vars: current, + calls: after_s.calls, + lines_to_env: after_s.lines_to_env, + vars_info: after_s.vars_info, + cursor_env: after_s.cursor_env, + closest_env: after_s.closest_env + } + + call_e = Map.put(e, :context, :match) + {e_expr, %{vars: new_current, unused: new_unused} = s_expr, ee} = fun.(expr, call_s, call_e) + + end_s = %{ + after_s + | prematch: prematch, + unused: new_unused, + vars: new_current, + calls: s_expr.calls, + lines_to_env: s_expr.lines_to_env, + vars_info: s_expr.vars_info, + cursor_env: s_expr.cursor_env, + closest_env: s_expr.closest_env + } + + end_e = Map.put(ee, :context, Map.get(e, :context)) + {e_expr, end_s, end_e} + end + + def clause(fun, {:->, clause_meta, [_, _]} = clause, s, e) + when is_function(fun, 4) do + clause(fn x, sa, ea -> fun.(clause_meta, x, sa, ea) end, clause, s, e) + end + + def clause(fun, {:->, meta, [left, right]}, s, e) do + {e_left, sl, el} = fun.(left, s, e) + {e_right, sr, er} = ElixirExpand.expand(right, sl, el) + {{:->, meta, [e_left, e_right]}, sr, er} + end + + def clause(fun, expr, s, e) do + # try to recover from error by wrapping the expression in clause + # elixir raises here bad_or_missing_clauses + clause(fun, {:->, [], [[expr], :ok]}, s, e) + end + + def head([{:when, meta, [_ | _] = all}], s, e) do + {args, guard} = ElixirUtils.split_last(all) + prematch = s.prematch + + {{e_args, e_guard}, sg, eg} = + match( + fn _ok, sm, em -> + {e_args, sa, ea} = ElixirExpand.expand_args(args, sm, em) + + {e_guard, sg, eg} = + guard(guard, %{sa | prematch: prematch}, %{ea | context: :guard}) + + type_info = TypeInference.Guard.type_information_from_guards(e_guard) + + sg = State.merge_inferred_types(sg, type_info) + + {{e_args, e_guard}, sg, eg} + end, + :ok, + s, + s, + e + ) + + {[{:when, meta, e_args ++ [e_guard]}], sg, eg} + end + + def head(args, s, e) do + match(&ElixirExpand.expand_args/3, args, s, s, e) + end + + def guard({:when, meta, [left, right]}, s, e) do + {e_left, sl, el} = guard(left, s, e) + {e_right, sr, er} = guard(right, sl, el) + {{:when, meta, [e_left, e_right]}, sr, er} + end + + def guard(guard, s, e) do + {e_guard, sg, eg} = ElixirExpand.expand(guard, s, e) + {e_guard, sg, eg} + end + + # case + + @valid_case_opts [:do] + + def case(e_expr, [], s, e) do + # elixir raises here missing_option + # emit a fake do block + case(e_expr, [do: []], s, e) + end + + def case(_e_expr, opts, s, e) when not is_list(opts) do + # elixir raises here invalid_args + # there may be cursor + ElixirExpand.expand(opts, s, e) + end + + def case(e_expr, opts, s, e) do + # expand invalid opts in case there's cursor + {_ast, s, _e} = ElixirExpand.expand(opts |> Keyword.drop(@valid_case_opts), s, e) + + opts = sanitize_opts(opts, @valid_case_opts) + + match_context = TypeInference.type_of(e_expr, e.context) + + {case_clauses, sa} = + Enum.map_reduce(opts, s, fn x, sa -> + expand_case(x, match_context, sa, e) + end) + + {case_clauses, sa, e} + end + + defp expand_case({:do, _} = do_clause, match_context, s, e) do + expand_clauses( + fn c, s, e -> + case head(c, s, e) do + {[h | _] = c, s, e} -> + clause_vars_with_inferred_types = + TypeInference.find_typed_vars(h, match_context, :match) + + s = State.merge_inferred_types(s, clause_vars_with_inferred_types) + + {c, s, e} + + other -> + other + end + end, + do_clause, + s, + e + ) + end + + # cond + + @valid_cond_opts [:do] + + def cond([], s, e) do + # elixir raises here missing_option + # emit a fake do block + cond([do: []], s, e) + end + + def cond(opts, s, e) when not is_list(opts) do + # elixir raises here invalid_args + # there may be cursor + ElixirExpand.expand(opts, s, e) + end + + def cond(opts, s, e) do + # expand invalid opts in case there's cursor + {_ast, s, _e} = ElixirExpand.expand(opts |> Keyword.drop(@valid_cond_opts), s, e) + + opts = sanitize_opts(opts, @valid_cond_opts) + + {cond_clauses, sa} = + Enum.map_reduce(opts, s, fn x, sa -> + expand_cond(x, sa, e) + end) + + {cond_clauses, sa, e} + end + + defp expand_cond({:do, _} = do_clause, s, e) do + expand_clauses(&ElixirExpand.expand_args/3, do_clause, s, e) + end + + # receive + + @valid_receive_opts [:do, :after] + + def receive([], s, e) do + # elixir raises here missing_option + # emit a fake do block + receive([do: []], s, e) + end + + def receive(opts, s, e) when not is_list(opts) do + # elixir raises here invalid_args + # there may be cursor + ElixirExpand.expand(opts, s, e) + end + + def receive(opts, s, e) do + # expand invalid opts in case there's cursor + {_ast, s, _e} = ElixirExpand.expand(opts |> Keyword.drop(@valid_receive_opts), s, e) + + opts = sanitize_opts(opts, @valid_receive_opts) + + {receive_clauses, sa} = + Enum.map_reduce(opts, s, fn x, sa -> + expand_receive(x, sa, e) + end) + + {receive_clauses, sa, e} + end + + defp expand_receive({:do, {:__block__, _, []}} = do_block, s, _e) do + {do_block, s} + end + + defp expand_receive({:do, _} = do_clause, s, e) do + # no point in doing type inference here, we have no idea what message we may get + expand_clauses(&head/3, do_clause, s, e) + end + + defp expand_receive({:after, [_ | _]} = after_clause, s, e) do + expand_clauses(&ElixirExpand.expand_args/3, after_clause, s, e) + end + + defp expand_receive({:after, expr}, s, e) when not is_list(expr) do + # elixir raises here multiple_after_clauses_in_receive + case expr do + expr when not is_list(expr) -> + # try to recover from error by wrapping the expression in list + expand_receive({:after, [expr]}, s, e) + + [first | discarded] -> + # try to recover from error by taking first clause only + # expand other in case there's cursor + {_ast, s, _e} = ElixirExpand.expand(discarded, s, e) + expand_receive({:after, [first]}, s, e) + + [] -> + # try to recover from error by inserting a fake clause + expand_receive({:after, [{:->, [], [[0], :ok]}]}, s, e) + end + end + + # with + + @valid_with_opts [:do, :else] + + def with(meta, args, s, e) do + {exprs, opts0} = ElixirUtils.split_opts(args) + + # expand invalid opts in case there's cursor + {_ast, s, _e} = ElixirExpand.expand(opts0 |> Keyword.drop(@valid_with_opts), s, e) + + opts0 = sanitize_opts(opts0, @valid_with_opts) + s0 = State.new_vars_scope(s) + {e_exprs, {s1, e1}} = Enum.map_reduce(exprs, {s0, e}, &expand_with/2) + {e_do, opts1, s2} = expand_with_do(meta, opts0, s, s1, e1) + {e_opts, _opts2, s3} = expand_with_else(opts1, s2, e) + + {{:with, meta, e_exprs ++ [[{:do, e_do} | e_opts]]}, s3, e} + end + + defp expand_with({:<-, meta, [left, right]}, {s, e}) do + {e_right, sr, er} = ElixirExpand.expand(right, s, e) + sm = State.reset_read(sr, s) + {[e_left], sl, el} = head([left], sm, er) + + match_context_r = TypeInference.type_of(e_right, e.context) + vars_l_with_inferred_types = TypeInference.find_typed_vars(e_left, match_context_r, :match) + + sl = State.merge_inferred_types(sl, vars_l_with_inferred_types) + + {{:<-, meta, [e_left, e_right]}, {sl, el}} + end + + defp expand_with(expr, {s, e}) do + {e_expr, se, ee} = ElixirExpand.expand(expr, s, e) + {e_expr, {se, ee}} + end + + defp expand_with_do(_meta, opts, s, acc, e) do + {expr, rest_opts} = Keyword.pop(opts, :do) + # elixir raises here missing_option + # we return empty expression + expr = expr || [] + + {e_expr, s_acc, _e_acc} = ElixirExpand.expand(expr, acc, e) + + {e_expr, rest_opts, State.remove_vars_scope(s_acc, s)} + end + + defp expand_with_else(opts, s, e) do + case Keyword.pop(opts, :else) do + {nil, _} -> + {[], opts, s} + + {expr, rest_opts} -> + pair = {:else, expr} + + # no point in doing type inference here, we have no idea what data we are matching against + {e_pair, se} = expand_clauses(&head/3, pair, s, e) + {[e_pair], rest_opts, se} + end + end + + # try + + @valid_try_opts [:do, :rescue, :catch, :else, :after] + + def try([], s, e) do + # elixir raises here missing_option + # emit a fake do block + try([do: []], s, e) + end + + def try(opts, s, e) when not is_list(opts) do + # elixir raises here invalid_args + # there may be cursor + ElixirExpand.expand(opts, s, e) + end + + def try(opts, s, e) do + # expand invalid opts in case there's cursor + {_ast, s, _e} = ElixirExpand.expand(opts |> Keyword.drop(@valid_try_opts), s, e) + + opts = sanitize_opts(opts, @valid_try_opts) + + {try_clauses, sa} = + Enum.map_reduce(opts, s, fn x, sa -> + expand_try(x, sa, e) + end) + + {try_clauses, sa, e} + end + + defp expand_try({:do, expr}, s, e) do + {e_expr, se, _ee} = ElixirExpand.expand(expr, State.new_vars_scope(s), e) + {{:do, e_expr}, State.remove_vars_scope(se, s)} + end + + defp expand_try({:after, expr}, s, e) do + {e_expr, se, _ee} = ElixirExpand.expand(expr, State.new_vars_scope(s), e) + {{:after, e_expr}, State.remove_vars_scope(se, s)} + end + + defp expand_try({:else, _} = else_clause, s, e) do + # TODO we could try to infer type from last try block expression + expand_clauses(&head/3, else_clause, s, e) + end + + defp expand_try({:catch, _} = catch_clause, s, e) do + expand_clauses_with_stacktrace(&expand_catch/4, catch_clause, s, e) + end + + defp expand_try({:rescue, _} = rescue_clause, s, e) do + expand_clauses_with_stacktrace(&expand_rescue/4, rescue_clause, s, e) + end + + defp expand_clauses_with_stacktrace(fun, clauses, s, e) do + old_stacktrace = s.stacktrace + ss = %{s | stacktrace: true} + {ret, se} = expand_clauses(fun, clauses, ss, e) + {ret, %{se | stacktrace: old_stacktrace}} + end + + defp expand_catch(meta, [{:when, when_meta, [a1, a2, a3, dh | dt]}], s, e) do + # elixir raises here wrong_number_of_args_for_clause + {_, s, _} = ElixirExpand.expand([dh | dt], s, e) + expand_catch(meta, [{:when, when_meta, [a1, a2, a3]}], s, e) + end + + defp expand_catch(_meta, args = [_], s, e) do + # no point in doing type inference here, we have no idea what throw we caught + head(args, s, e) + end + + defp expand_catch(_meta, args = [_, _], s, e) do + # TODO is it worth to infer type of the first arg? :error | :exit | :throw | {:EXIT, pid()} + head(args, s, e) + end + + defp expand_catch(meta, [a1, a2 | d], s, e) do + # attempt to recover from error by taking 2 first args + # elixir raises here wrong_number_of_args_for_clause + {_, s, _} = ElixirExpand.expand(d, s, e) + expand_catch(meta, [a1, a2], s, e) + end + + defp expand_rescue(_meta, [arg], s, e) do + # elixir is strict here and raises invalid_rescue_clause on invalid args + {e_arg, sa, ea} = expand_rescue(arg, s, e) + {[e_arg], sa, ea} + end + + defp expand_rescue(meta, [a1 | d], s, e) do + # try to recover from error by taking first argument only + # elixir raises here wrong_number_of_args_for_clause + {_, s, _} = ElixirExpand.expand(d, s, e) + expand_rescue(meta, [a1], s, e) + end + + # rescue var + defp expand_rescue({name, _, atom} = var, s, e) when is_atom(name) and is_atom(atom) do + {e_left, sl, el} = match(&ElixirExpand.expand/3, var, s, s, e) + + match_context = {:struct, [], {:atom, Exception}, nil} + + vars_with_inferred_types = TypeInference.find_typed_vars(e_left, match_context, :match) + sl = State.merge_inferred_types(sl, vars_with_inferred_types) + + {e_left, sl, el} + end + + # rescue Alias => _ in [Alias] + defp expand_rescue({:__aliases__, _, [_ | _]} = alias, s, e) do + expand_rescue({:in, [], [{:_, [], e.module}, alias]}, s, e) + end + + # rescue var in _ + defp expand_rescue( + {:in, _, [{name, _, var_context} = var, {:_, _, underscore_context}]}, + s, + e + ) + when is_atom(name) and is_atom(var_context) and is_atom(underscore_context) do + {e_left, sl, el} = match(&ElixirExpand.expand/3, var, s, s, e) + + match_context = {:struct, [], {:atom, Exception}, nil} + + vars_with_inferred_types = TypeInference.find_typed_vars(e_left, match_context, :match) + sl = State.merge_inferred_types(sl, vars_with_inferred_types) + + {e_left, sl, el} + end + + # rescue var in (list() or atom()) + defp expand_rescue({:in, meta, [left, right]}, s, e) do + {e_left, sl, el} = match(&ElixirExpand.expand/3, left, s, s, e) + {e_right, sr, er} = ElixirExpand.expand(right, sl, el) + + case e_left do + {name, _, atom} when is_atom(name) and is_atom(atom) -> + normalized = normalize_rescue(e_right, e) + + match_context = + for exception <- normalized, reduce: nil do + nil -> {:struct, [], {:atom, exception}, nil} + other -> {:union, [other, {:struct, [], {:atom, exception}, nil}]} + end + + match_context = + if match_context == nil do + {:struct, [], {:atom, Exception}, nil} + else + match_context + end + + vars_with_inferred_types = TypeInference.find_typed_vars(e_left, match_context, :match) + sr = State.merge_inferred_types(sr, vars_with_inferred_types) + + {{:in, meta, [e_left, normalized]}, sr, er} + + _ -> + # elixir rejects this case, we normalize to underscore + {{:in, meta, [{:_, [], e.module}, normalize_rescue(e_right, e)]}, sr, er} + end + end + + # rescue expr() => rescue expanded_expr() + defp expand_rescue({_, meta, _} = arg, s, e) do + # TODO how to check for cursor here? + case ElixirExpand.Macro.expand_once(arg, %{e | line: ElixirUtils.get_line(meta)}) do + ^arg -> + # elixir rejects this case + # try to recover from error by generating fake expression + expand_rescue({:in, meta, [arg, {:_, [], e.module}]}, s, e) + + new_arg -> + expand_rescue(new_arg, s, e) + end + end + + # rescue list() or atom() => _ in (list() or atom()) + defp expand_rescue(arg, s, e) do + expand_rescue({:in, [], [{:_, [], e.module}, arg]}, s, e) + end + + defp normalize_rescue(atom, _e) when is_atom(atom) do + [atom] + end + + defp normalize_rescue(other, e) do + # elixir is strict here, we reject invalid nodes + res = + if is_list(other) do + Enum.filter(other, &is_atom/1) + else + [] + end + + if res == [] do + [{:_, [], e.module}] + else + res + end + end + + defp expand_clauses(fun, {key, [_ | _] = clauses}, s, e) do + transformer = fn clause, sa -> + {e_clause, s_acc, _e_acc} = + clause(fun, clause, State.new_vars_scope(sa), e) + + {e_clause, State.remove_vars_scope(s_acc, sa)} + end + + {values, se} = Enum.map_reduce(clauses, s, transformer) + {{key, values}, se} + end + + defp expand_clauses(fun, {key, expr}, s, e) do + # try to recover from error by wrapping the expression in a clauses list + # elixir raises here bad_or_missing_clauses + expand_clauses(fun, {key, [expr]}, s, e) + end + + # helpers + + defp sanitize_opt(opts, opt) do + case Keyword.fetch(opts, opt) do + :error -> [] + {:ok, value} -> [{opt, value}] + end + end + + defp sanitize_opts(opts, allowed) do + Enum.flat_map(allowed, fn opt -> sanitize_opt(opts, opt) end) + end +end diff --git a/lib/elixir_sense/core/compiler/dispatch.ex b/lib/elixir_sense/core/compiler/dispatch.ex new file mode 100644 index 00000000..7f967659 --- /dev/null +++ b/lib/elixir_sense/core/compiler/dispatch.ex @@ -0,0 +1,179 @@ +defmodule ElixirSense.Core.Compiler.Dispatch do + alias ElixirSense.Core.Compiler.Rewrite, as: ElixirRewrite + alias ElixirSense.Core.State + import :ordsets, only: [is_element: 2] + + def find_import(meta, name, arity, e) do + tuple = {name, arity} + + case find_import_by_name_arity(meta, tuple, [], e) do + {:function, receiver} -> + # TODO trace call? + # TODO address when https://github.com/elixir-lang/elixir/issues/13878 is resolved + # ElixirEnv.trace({:imported_function, meta, receiver, name, arity}, e) + receiver + + {:macro, receiver} -> + # TODO trace call? + # ElixirEnv.trace({:imported_macro, meta, receiver, name, arity}, e) + receiver + + {:ambiguous, [head | _]} -> + # elixir raises here, we choose first one + # TODO trace call? + head + + _ -> + false + end + end + + def find_imports(meta, name, e) do + funs = e.functions + macs = e.macros + + acc0 = %{} + acc1 = find_imports_by_name(funs, acc0, name, meta, e) + acc2 = find_imports_by_name(macs, acc1, name, meta, e) + + imports = acc2 |> Map.to_list() |> Enum.sort() + # trace_import_quoted(imports, meta, name, e) + imports + end + + def import_function(meta, name, arity, s, e) do + tuple = {name, arity} + + case find_import_by_name_arity(meta, tuple, [], e) do + {:function, receiver} -> + remote_function(meta, receiver, name, arity, e) + + {:macro, _receiver} -> + false + + {:import, receiver} -> + require_function(meta, receiver, name, arity, s, e) + + {:ambiguous, [first | _]} -> + # elixir raises here, we return first matching + require_function(meta, first, name, arity, s, e) + + false -> + if Macro.special_form?(name, arity) do + false + else + function = e.function + + mfa = {e.module, name, arity} + + if function != nil and function != tuple and + Enum.any?(s.mods_funs_to_positions, fn {key, info} -> + key == mfa and State.ModFunInfo.get_category(info) == :macro + end) do + false + else + {:local, name, arity} + end + end + end + end + + def require_function(meta, receiver, name, arity, s, e) do + required = receiver in e.requires + + if is_macro(name, arity, receiver, required, s) do + false + else + remote_function(meta, receiver, name, arity, e) + end + end + + defp remote_function(_meta, receiver, name, arity, _e) do + case ElixirRewrite.inline(receiver, name, arity) do + {ar, an} -> {:remote, ar, an, arity} + false -> {:remote, receiver, name, arity} + end + end + + def find_imports_by_name([{mod, imports} | mod_imports], acc, name, meta, e) do + new_acc = find_imports_by_name(name, imports, acc, mod, meta, e) + find_imports_by_name(mod_imports, new_acc, name, meta, e) + end + + def find_imports_by_name([], acc, _name, _meta, _e), do: acc + + def find_imports_by_name(name, [{name, arity} | imports], acc, mod, meta, e) do + case Map.get(acc, arity) do + nil -> + find_imports_by_name(name, imports, Map.put(acc, arity, mod), mod, meta, e) + + _other_mod -> + # elixir raises here ambiguous_call + find_imports_by_name(name, imports, acc, mod, meta, e) + end + end + + def find_imports_by_name(name, [{import_name, _} | imports], acc, mod, meta, e) + when name > import_name do + find_imports_by_name(name, imports, acc, mod, meta, e) + end + + def find_imports_by_name(_name, _imports, acc, _mod, _meta, _e), do: acc + + defp find_import_by_name_arity(meta, {_name, arity} = tuple, extra, e) do + case is_import(meta, arity) do + {:import, _} = import_res -> + import_res + + false -> + funs = e.functions + macs = extra ++ e.macros + fun_match = find_import_by_name_arity(tuple, funs) + mac_match = find_import_by_name_arity(tuple, macs) + + case {fun_match, mac_match} do + {[], [receiver]} -> + {:macro, receiver} + + {[receiver], []} -> + {:function, receiver} + + {[], []} -> + false + + _ -> + {:ambiguous, fun_match ++ mac_match} + end + end + end + + defp find_import_by_name_arity(tuple, list) do + for {receiver, set} <- list, is_element(tuple, set), do: receiver + end + + defp is_import(meta, arity) do + with {:ok, imports = [_ | _]} <- Keyword.fetch(meta, :imports), + {:ok, _} <- Keyword.fetch(meta, :context), + {_arity, receiver} <- :lists.keyfind(arity, 1, imports) do + {:import, receiver} + else + _ -> false + end + end + + defp is_macro(_name, _arity, _module, false, _s), do: false + + defp is_macro(name, arity, receiver, true, s) do + mfa = {receiver, name, arity} + + Enum.any?(s.mods_funs_to_positions, fn {key, info} -> + key == mfa and State.ModFunInfo.get_category(info) == :macro + end) || + try do + macros = receiver.__info__(:macros) + {name, arity} in macros + rescue + _error -> false + end + end +end diff --git a/lib/elixir_sense/core/compiler/fn.ex b/lib/elixir_sense/core/compiler/fn.ex new file mode 100644 index 00000000..e420dc3d --- /dev/null +++ b/lib/elixir_sense/core/compiler/fn.ex @@ -0,0 +1,248 @@ +defmodule ElixirSense.Core.Compiler.Fn do + alias ElixirSense.Core.Compiler, as: ElixirExpand + alias ElixirSense.Core.Compiler.Clauses, as: ElixirClauses + alias ElixirSense.Core.Compiler.Dispatch, as: ElixirDispatch + alias ElixirSense.Core.Compiler.Utils, as: ElixirUtils + alias ElixirSense.Core.State + + def expand(meta, clauses, s, e) when is_list(clauses) do + transformer = fn + {:->, _, [_left, _right]} = clause, sa -> + # elixir raises defaults_in_args + s_reset = State.new_vars_scope(sa) + + # no point in doing type inference here, we have no idea what the fn will be called with + {e_clause, s_acc, _e_acc} = + ElixirClauses.clause(&ElixirClauses.head/3, clause, s_reset, e) + + {e_clause, State.remove_vars_scope(s_acc, sa)} + end + + {e_clauses, se} = Enum.map_reduce(clauses, s, transformer) + + {{:fn, meta, e_clauses}, se, e} + end + + # Capture + + def capture(meta, {:/, _, [{{:., _, [_m, f]} = dot, require_meta, []}, a]}, s, e) + when is_atom(f) and is_integer(a) do + args = args_from_arity(meta, a) + + capture_require({dot, require_meta, args}, s, e, true) + end + + def capture(meta, {:/, _, [{f, import_meta, c}, a]}, s, e) + when is_atom(f) and is_integer(a) and is_atom(c) do + args = args_from_arity(meta, a) + capture_import({f, import_meta, args}, s, e, true) + end + + def capture(_meta, {{:., _, [_, fun]}, _, args} = expr, s, e) + when is_atom(fun) and is_list(args) do + capture_require(expr, s, e, is_sequential_and_not_empty(args)) + end + + def capture(meta, {{:., _, [_]}, _, args} = expr, s, e) when is_list(args) do + capture_expr(meta, expr, s, e, false) + end + + def capture(meta, {:__block__, _, [expr]}, s, e) do + capture(meta, expr, s, e) + end + + def capture(meta, {:__block__, _, expr}, s, e) do + # elixir raises block_expr_in_capture + # try to recover from error + expr = + case expr do + [] -> + {:"&1", meta, e.module} + + list -> + ElixirUtils.select_with_cursor(list) || hd(list) + end + + capture(meta, expr, s, e) + end + + def capture(_meta, {atom, _, args} = expr, s, e) when is_atom(atom) and is_list(args) do + capture_import(expr, s, e, is_sequential_and_not_empty(args)) + end + + def capture(meta, {left, right}, s, e) do + capture(meta, {:{}, meta, [left, right]}, s, e) + end + + def capture(meta, list, s, e) when is_list(list) do + capture_expr(meta, list, s, e, is_sequential_and_not_empty(list)) + end + + def capture(meta, integer, s, e) when is_integer(integer) do + # elixir raises here capture_arg_outside_of_capture + # emit fake capture + capture(meta, [{:&, meta, [1]}], s, e) + end + + def capture(meta, arg, s, e) do + # elixir raises invalid_args_for_capture + # we try to transform the capture to local fun capture + case arg do + {var, _, context} when is_atom(var) and is_atom(context) -> + capture(meta, {:/, meta, [arg, 0]}, s, e) + + _ -> + # try to wrap it in list + capture(meta, [arg], s, e) + end + end + + defp capture_import({atom, import_meta, args} = expr, s, e, sequential) do + res = + if sequential do + ElixirDispatch.import_function(import_meta, atom, length(args), s, e) + else + false + end + + handle_capture(res, import_meta, import_meta, expr, s, e, sequential) + end + + defp capture_require({{:., dot_meta, [left, right]}, require_meta, args}, s, e, sequential) do + case escape(left, []) do + {esc_left, []} -> + {e_left, se, ee} = ElixirExpand.expand(esc_left, s, e) + + res = + if sequential do + case e_left do + {name, _, context} when is_atom(name) and is_atom(context) -> + {:remote, e_left, right, length(args)} + + _ when is_atom(e_left) -> + ElixirDispatch.require_function( + require_meta, + e_left, + right, + length(args), + s, + ee + ) + + _ -> + false + end + else + false + end + + dot = {{:., dot_meta, [e_left, right]}, require_meta, args} + handle_capture(res, require_meta, dot_meta, dot, se, ee, sequential) + + {esc_left, escaped} -> + dot = {{:., dot_meta, [esc_left, right]}, require_meta, args} + capture_expr(require_meta, dot, s, e, escaped, sequential) + end + end + + defp handle_capture(false, meta, _dot_meta, expr, s, e, sequential) do + capture_expr(meta, expr, s, e, sequential) + end + + defp handle_capture(local_or_remote, meta, dot_meta, _expr, s, e, _sequential) do + {local_or_remote, meta, dot_meta, s, e} + end + + defp capture_expr(meta, expr, s, e, sequential) do + capture_expr(meta, expr, s, e, [], sequential) + end + + defp capture_expr(meta, expr, s, e, escaped, sequential) do + case escape(expr, escaped) do + {e_expr, []} when not sequential -> + # elixir raises here invalid_args_for_capture + # we emit fn without args + fn_expr = {:fn, meta, [{:->, meta, [[], e_expr]}]} + {:expand, fn_expr, s, e} + + {e_expr, e_dict} -> + # elixir raises capture_arg_without_predecessor here + # if argument vars are not consecutive + e_vars = Enum.map(e_dict, &elem(&1, 1)) + fn_expr = {:fn, meta, [{:->, meta, [e_vars, e_expr]}]} + {:expand, fn_expr, s, e} + end + end + + defp escape({:&, meta, [pos]}, dict) when is_integer(pos) and pos > 0 do + # This might pollute user space but is unlikely because variables + # named :"&1" are not valid syntax. + var = {:"&#{pos}", meta, nil} + {var, :orddict.store(pos, var, dict)} + + case :orddict.find(pos, dict) do + {:ok, var} -> + {var, dict} + + :error -> + # elixir uses here elixir_module:next_counter(?key(E, module)) + # but we are not compiling and do not need to keep count in module scope + # elixir 1.17 also renames the var to `capture` + next = System.unique_integer() + var = {:"&#{pos}", [{:counter, next} | meta], nil} + {var, :orddict.store(pos, var, dict)} + end + end + + defp escape({:&, meta, [pos]}, dict) when is_integer(pos) do + # elixir raises here invalid_arity_for_capture + # we substitute arg number + escape({:&, meta, [1]}, dict) + end + + defp escape({:&, _meta, args}, dict) do + # elixir raises here nested_capture + # try to recover from error by dropping & + escape(args, dict) + end + + defp escape({left, meta, right}, dict0) do + {t_left, dict1} = escape(left, dict0) + {t_right, dict2} = escape(right, dict1) + {{t_left, meta, t_right}, dict2} + end + + defp escape({left, right}, dict0) do + {t_left, dict1} = escape(left, dict0) + {t_right, dict2} = escape(right, dict1) + {{t_left, t_right}, dict2} + end + + defp escape(list, dict) when is_list(list) do + Enum.map_reduce(list, dict, fn x, acc -> escape(x, acc) end) + end + + defp escape(other, dict) do + {other, dict} + end + + defp args_from_arity(_meta, 0), do: [] + + defp args_from_arity(meta, a) when is_integer(a) and a >= 1 and a <= 255 do + for x <- 1..a do + {:&, meta, [x]} + end + end + + defp args_from_arity(_meta, _a) do + # elixir raises invalid_arity_for_capture + [] + end + + defp is_sequential_and_not_empty([]), do: false + defp is_sequential_and_not_empty(list), do: is_sequential(list, 1) + + defp is_sequential([{:&, _, [int]} | t], int), do: is_sequential(t, int + 1) + defp is_sequential([], _int), do: true + defp is_sequential(_, _int), do: false +end diff --git a/lib/elixir_sense/core/compiler/map.ex b/lib/elixir_sense/core/compiler/map.ex new file mode 100644 index 00000000..cb6666b7 --- /dev/null +++ b/lib/elixir_sense/core/compiler/map.ex @@ -0,0 +1,184 @@ +defmodule ElixirSense.Core.Compiler.Map do + alias ElixirSense.Core.Compiler, as: ElixirExpand + + def expand_struct(meta, left, {:%{}, map_meta, map_args}, s, %{context: context} = e) do + clean_map_args = clean_struct_key_from_map_args(map_args) + + {[e_left, e_right], se, ee} = + ElixirExpand.expand_args([left, {:%{}, map_meta, clean_map_args}], s, e) + + case validate_struct(e_left, context) do + true when is_atom(e_left) -> + # TODO register alias/struct + case extract_struct_assocs(e_right) do + {:expand, map_meta, assocs} when context != :match -> + assoc_keys = Enum.map(assocs, fn {k, _} -> k end) + struct = load_struct(e_left, [assocs], se, ee) + keys = [:__struct__ | assoc_keys] + without_keys = Elixir.Map.drop(struct, keys) + + struct_assocs = + ElixirExpand.Macro.escape(Enum.sort(Elixir.Map.to_list(without_keys))) + + {{:%, meta, [e_left, {:%{}, map_meta, struct_assocs ++ assocs}]}, se, ee} + + {_, _, _assocs} -> + # elixir validates assocs against struct keys + # we don't need to validate keys + {{:%, meta, [e_left, e_right]}, se, ee} + end + + _ -> + # elixir raises invalid_struct_name if validate_struct returns false + {{:%, meta, [e_left, e_right]}, se, ee} + end + end + + def expand_struct(meta, left, right, s, e) do + # elixir raises here non_map_after_struct + # try to recover from error by wrapping the expression in map + expand_struct(meta, left, wrap_in_fake_map(right), s, e) + end + + defp wrap_in_fake_map(right) do + map_args = + case right do + list when is_list(list) -> + if Keyword.keyword?(list) do + list + else + [__fake_key__: list] + end + + _ -> + [__fake_key__: right] + end + + {:%{}, [], map_args} + end + + def expand_map(meta, [{:|, update_meta, [left, right]}], s, e) do + # elixir raises update_syntax_in_wrong_context if e.context is not nil + {[e_left | e_right], se, ee} = ElixirExpand.expand_args([left | right], s, e) + e_right = sanitize_kv(e_right, e) + {{:%{}, meta, [{:|, update_meta, [e_left, e_right]}]}, se, ee} + end + + def expand_map(meta, args, s, e) do + {e_args, se, ee} = ElixirExpand.expand_args(args, s, e) + e_args = sanitize_kv(e_args, e) + {{:%{}, meta, e_args}, se, ee} + end + + defp clean_struct_key_from_map_args([{:|, pipe_meta, [left, map_assocs]}]) do + [{:|, pipe_meta, [left, delete_struct_key(map_assocs)]}] + end + + defp clean_struct_key_from_map_args(map_assocs) do + delete_struct_key(map_assocs) + end + + defp sanitize_kv(kv, %{context: context}) do + Enum.filter(kv, fn + {k, _v} -> + if context == :match do + validate_match_key(k) + else + true + end + + _ -> + false + end) + end + + defp validate_match_key({name, _, context}) + when is_atom(name) and is_atom(context) do + # elixir raises here invalid_variable_in_map_key_match + false + end + + defp validate_match_key({:"::", _, [left, _]}) do + validate_match_key(left) + end + + defp validate_match_key({:^, _, [{name, _, context}]}) + when is_atom(name) and is_atom(context), + do: true + + defp validate_match_key({:%{}, _, [_ | _]}), do: true + + defp validate_match_key({left, _, right}) do + validate_match_key(left) and validate_match_key(right) + end + + defp validate_match_key({left, right}) do + validate_match_key(left) and validate_match_key(right) + end + + defp validate_match_key(list) when is_list(list) do + Enum.all?(list, &validate_match_key/1) + end + + defp validate_match_key(_), do: true + + defp validate_struct({:^, _, [{var, _, ctx}]}, :match) when is_atom(var) and is_atom(ctx), + do: true + + defp validate_struct({var, _meta, ctx}, :match) when is_atom(var) and is_atom(ctx), do: true + defp validate_struct(atom, _) when is_atom(atom), do: true + defp validate_struct(_, _), do: false + + defp sanitize_assocs(list) do + Enum.filter(list, &match?({k, _} when is_atom(k), &1)) + end + + defp extract_struct_assocs({:%{}, meta, [{:|, _, [_, assocs]}]}) do + {:update, meta, delete_struct_key(sanitize_assocs(assocs))} + end + + defp extract_struct_assocs({:%{}, meta, assocs}) do + {:expand, meta, delete_struct_key(sanitize_assocs(assocs))} + end + + defp extract_struct_assocs(right) do + # elixir raises here non_map_after_struct + # try to recover from error by wrapping the expression in map + extract_struct_assocs(wrap_in_fake_map(right)) + end + + defp delete_struct_key(assocs) do + Keyword.delete(assocs, :__struct__) + end + + def load_struct(name, assocs, s, _e) do + case s.structs[name] do + nil -> + try do + apply(name, :__struct__, assocs) + else + %{:__struct__ => ^name} = struct -> + struct + + _ -> + # recover from invalid return value + [__struct__: name] |> merge_assocs(assocs) + rescue + _ -> + # recover from error by building the fake struct + [__struct__: name] |> merge_assocs(assocs) + end + + info -> + info.fields |> merge_assocs(assocs) + end + end + + defp merge_assocs(fields, []) do + fields |> Elixir.Map.new() + end + + defp merge_assocs(fields, [assocs]) do + fields |> Keyword.merge(assocs) |> Elixir.Map.new() + end +end diff --git a/lib/elixir_sense/core/compiler/quote.ex b/lib/elixir_sense/core/compiler/quote.ex new file mode 100644 index 00000000..869f5fa4 --- /dev/null +++ b/lib/elixir_sense/core/compiler/quote.ex @@ -0,0 +1,546 @@ +defmodule ElixirSense.Core.Compiler.Quote do + alias ElixirSense.Core.Compiler.Dispatch, as: ElixirDispatch + alias ElixirSense.Core.Normalized.Macro.Env, as: NormalizedMacroEnv + + defstruct line: false, + file: nil, + context: nil, + op: :none, + aliases_hygiene: nil, + imports_hygiene: nil, + unquote: true, + generated: false + + def fun_to_quoted(function) do + {:module, module} = :erlang.fun_info(function, :module) + {:name, name} = :erlang.fun_info(function, :name) + {:arity, arity} = :erlang.fun_info(function, :arity) + + {:&, [], [{:/, [], [{{:., [], [module, name]}, [{:no_parens, true}], []}, arity]}]} + end + + def has_unquotes(ast), do: has_unquotes(ast, 0) + + def has_unquotes({:quote, _, [child]}, quote_level) do + has_unquotes(child, quote_level + 1) + end + + def has_unquotes({:quote, _, [quote_opts, child]}, quote_level) do + case disables_unquote(quote_opts) do + true -> false + _ -> has_unquotes(child, quote_level + 1) + end + end + + def has_unquotes({unquote, _, [child]}, quote_level) + when unquote in [:unquote, :unquote_splicing] do + case quote_level do + 0 -> true + _ -> has_unquotes(child, quote_level - 1) + end + end + + def has_unquotes({{:., _, [_, :unquote]}, _, [_]}, _), do: true + def has_unquotes({var, _, ctx}, _) when is_atom(var) and is_atom(ctx), do: false + + def has_unquotes({name, _, args}, quote_level) when is_list(args) do + has_unquotes(name) or Enum.any?(args, fn child -> has_unquotes(child, quote_level) end) + end + + def has_unquotes({left, right}, quote_level) do + has_unquotes(left, quote_level) or has_unquotes(right, quote_level) + end + + def has_unquotes(list, quote_level) when is_list(list) do + Enum.any?(list, fn child -> has_unquotes(child, quote_level) end) + end + + def has_unquotes(_other, _), do: false + + defp disables_unquote([{:unquote, false} | _]), do: true + defp disables_unquote([{:bind_quoted, _} | _]), do: true + defp disables_unquote([_h | t]), do: disables_unquote(t) + defp disables_unquote(_), do: false + + def build(meta, line, file, context, unquote, generated, e) do + acc0 = [] + + {v_line, acc1} = validate_compile(meta, :line, line, acc0) + {v_file, acc2} = validate_compile(meta, :file, file, acc1) + {v_context, acc3} = validate_compile(meta, :context, context, acc2) + + unquote = validate_runtime(:unquote, unquote) + generated = validate_runtime(:generated, generated) + + q = %__MODULE__{ + op: :add_context, + aliases_hygiene: e, + imports_hygiene: e, + line: v_line, + file: v_file, + unquote: unquote, + context: v_context, + generated: generated + } + + {q, v_context, acc3} + end + + defp validate_compile(_meta, :line, value, acc) when is_boolean(value) do + {value, acc} + end + + defp validate_compile(_meta, :file, nil, acc) do + {nil, acc} + end + + defp validate_compile(meta, key, value, acc) do + case is_valid(key, value) do + true -> + {value, acc} + + false -> + var = {key, meta, __MODULE__} + call = {{:., meta, [__MODULE__, :validate_runtime]}, meta, [key, value]} + {var, [{:=, meta, [var, call]} | acc]} + end + end + + defp validate_runtime(key, value) do + case is_valid(key, value) do + true -> + value + + false -> + # elixir raises here invalid runtime value for option + default(key) + end + end + + defp is_valid(:line, line), do: is_integer(line) + defp is_valid(:file, file), do: is_binary(file) + defp is_valid(:context, context), do: is_atom(context) and context != nil + defp is_valid(:generated, generated), do: is_boolean(generated) + defp is_valid(:unquote, unquote), do: is_boolean(unquote) + defp default(:unquote), do: true + defp default(:generated), do: false + + def escape(expr, op, unquote) do + do_quote( + expr, + %__MODULE__{ + line: true, + file: nil, + op: op, + unquote: unquote + } + ) + end + + def quote({:unquote_splicing, _, [_]} = expr, %__MODULE__{unquote: true} = q) do + # elixir raises here unquote_splicing only works inside arguments and block contexts + # try to recover from error by wrapping it in block + __MODULE__.quote({:__block__, [], [expr]}, q) + end + + def quote(expr, q) do + do_quote(expr, q) + end + + # quote/unquote + + defp do_quote({:quote, meta, [arg]}, q) when is_list(meta) do + t_arg = do_quote(arg, %__MODULE__{q | unquote: false}) + + new_meta = + case q do + %__MODULE__{op: :add_context, context: context} -> + keystore(:context, meta, context) + + _ -> + meta + end + + {:{}, [], [:quote, meta(new_meta, q), [t_arg]]} + end + + defp do_quote({:quote, meta, [opts, arg]}, q) when is_list(meta) do + t_opts = do_quote(opts, q) + t_arg = do_quote(arg, %__MODULE__{q | unquote: false}) + + new_meta = + case q do + %__MODULE__{op: :add_context, context: context} -> + keystore(:context, meta, context) + + _ -> + meta + end + + {:{}, [], [:quote, meta(new_meta, q), [t_opts, t_arg]]} + end + + defp do_quote({:unquote, meta, [expr]}, %__MODULE__{unquote: true}) when is_list(meta), + do: expr + + # Aliases + + defp do_quote({:__aliases__, meta, [h | t] = list}, %__MODULE__{aliases_hygiene: e = %{}} = q) + when is_atom(h) and h != Elixir and is_list(meta) do + annotation = + case NormalizedMacroEnv.expand_alias(e, meta, list, trace: false) do + {:alias, atom} -> atom + :error -> false + end + + alias_meta = keystore(:alias, Keyword.delete(meta, :counter), annotation) + do_quote_tuple(:__aliases__, alias_meta, [h | t], q) + end + + # Vars + + defp do_quote({name, meta, nil}, %__MODULE__{op: :add_context} = q) + when is_atom(name) and is_list(meta) do + import_meta = + case q.imports_hygiene do + nil -> meta + e -> import_meta(meta, name, 0, q, e) + end + + {:{}, [], [name, meta(import_meta, q), q.context]} + end + + # cursor + + defp do_quote( + {:__cursor__, meta, args}, + %__MODULE__{unquote: _} + ) + when is_list(args) do + # emit cursor as is regardless of unquote + {:__cursor__, meta, args} + end + + # Unquote + + defp do_quote( + {{{:., meta, [left, :unquote]}, _, [expr]}, _, args}, + %__MODULE__{unquote: true} = q + ) + when is_list(meta) do + do_quote_call(left, meta, expr, args, q) + end + + defp do_quote({{:., meta, [left, :unquote]}, _, [expr]}, %__MODULE__{unquote: true} = q) + when is_list(meta) do + do_quote_call(left, meta, expr, nil, q) + end + + # Imports + + defp do_quote( + {:&, meta, [{:/, _, [{f, _, c}, a]}] = args}, + %__MODULE__{imports_hygiene: e = %{}} = q + ) + when is_atom(f) and is_integer(a) and is_atom(c) and is_list(meta) do + new_meta = + case ElixirDispatch.find_import(meta, f, a, e) do + false -> + meta + + receiver -> + keystore(:context, keystore(:imports, meta, [{a, receiver}]), q.context) + end + + do_quote_tuple(:&, new_meta, args, q) + end + + defp do_quote({name, meta, args_or_context}, %__MODULE__{imports_hygiene: e = %{}} = q) + when is_atom(name) and is_list(meta) and + (is_list(args_or_context) or is_atom(args_or_context)) do + arity = + case args_or_context do + args when is_list(args) -> length(args) + context when is_atom(context) -> 0 + end + + import_meta = import_meta(meta, name, arity, q, e) + annotated = annotate({name, import_meta, args_or_context}, q.context) + do_quote_tuple(annotated, q) + end + + # Two-element tuples + + defp do_quote({left, right}, %__MODULE__{unquote: true} = q) + when is_tuple(left) and elem(left, 0) == :unquote_splicing and + is_tuple(right) and elem(right, 0) == :unquote_splicing do + do_quote({:{}, [], [left, right]}, q) + end + + defp do_quote({left, right}, q) do + t_left = do_quote(left, q) + t_right = do_quote(right, q) + {t_left, t_right} + end + + # Everything else + + defp do_quote(other, q = %{op: op}) when op != :add_context do + do_escape(other, q) + end + + defp do_quote({_, _, _} = tuple, q) do + annotated = annotate(tuple, q.context) + do_quote_tuple(annotated, q) + end + + defp do_quote([], _), do: [] + + defp do_quote([h | t], %__MODULE__{unquote: false} = q) do + head_quoted = do_quote(h, q) + do_quote_simple_list(t, head_quoted, q) + end + + defp do_quote([h | t], q) do + do_quote_tail(:lists.reverse(t, [h]), q) + end + + defp do_quote(other, _), do: other + + defp import_meta(meta, name, arity, q, e) do + case Keyword.get(meta, :imports, false) == false && + ElixirDispatch.find_imports(meta, name, e) do + [_ | _] = imports -> + keystore(:imports, keystore(:context, meta, q.context), imports) + + _ -> + case arity == 1 && Keyword.fetch(meta, :ambiguous_op) do + {:ok, nil} -> + keystore(:ambiguous_op, meta, q.context) + + _ -> + meta + end + end + end + + defp do_quote_call(left, meta, expr, args, q) do + all = [left, {:unquote, meta, [expr]}, args, q.context] + tall = Enum.map(all, fn x -> do_quote(x, q) end) + {{:., meta, [:elixir_quote, :dot]}, meta, [meta(meta, q) | tall]} + end + + defp do_quote_tuple({left, meta, right}, q) do + do_quote_tuple(left, meta, right, q) + end + + defp do_quote_tuple(left, meta, right, q) do + t_left = do_quote(left, q) + t_right = do_quote(right, q) + {:{}, [], [t_left, meta(meta, q), t_right]} + end + + defp do_quote_simple_list([], prev, _), do: [prev] + + defp do_quote_simple_list([h | t], prev, q) do + [prev | do_quote_simple_list(t, do_quote(h, q), q)] + end + + defp do_quote_simple_list(other, prev, q) do + [{:|, [], [prev, do_quote(other, q)]}] + end + + defp do_quote_tail( + [{:|, meta, [{:unquote_splicing, _, [left]}, right]} | t], + %__MODULE__{unquote: true} = q + ) do + tt = do_quote_splice(t, q, [], []) + tr = do_quote(right, q) + do_runtime_list(meta, :tail_list, [left, tr, tt]) + end + + defp do_quote_tail(list, q) do + do_quote_splice(list, q, [], []) + end + + defp do_quote_splice( + [{:unquote_splicing, meta, [expr]} | t], + %__MODULE__{unquote: true} = q, + buffer, + acc + ) do + runtime = do_runtime_list(meta, :list, [expr, do_list_concat(buffer, acc)]) + do_quote_splice(t, q, [], runtime) + end + + defp do_quote_splice([h | t], q, buffer, acc) do + th = do_quote(h, q) + do_quote_splice(t, q, [th | buffer], acc) + end + + defp do_quote_splice([], _q, buffer, acc) do + do_list_concat(buffer, acc) + end + + defp do_list_concat(left, []), do: left + defp do_list_concat([], right), do: right + + defp do_list_concat(left, right) do + {{:., [], [:erlang, :++]}, [], [left, right]} + end + + defp do_runtime_list(meta, fun, args) do + {{:., meta, [:elixir_quote, fun]}, meta, args} + end + + defp meta(meta, q) do + generated(keep(Keyword.delete(meta, :column), q), q) + end + + defp generated(meta, %__MODULE__{generated: true}), do: [{:generated, true} | meta] + defp generated(meta, %__MODULE__{generated: false}), do: meta + + defp keep(meta, %__MODULE__{file: nil, line: line}) do + line(meta, line) + end + + defp keep(meta, %__MODULE__{file: file, line: true}) do + case Keyword.pop(meta, :line) do + {nil, _} -> + [{:keep, {file, 0}} | meta] + + {line, meta_no_line} -> + [{:keep, {file, line}} | meta_no_line] + end + end + + defp keep(meta, %__MODULE__{file: file, line: false}) do + [{:keep, {file, 0}} | Keyword.delete(meta, :line)] + end + + defp keep(meta, %__MODULE__{file: file, line: line}) do + [{:keep, {file, line}} | Keyword.delete(meta, :line)] + end + + defp line(meta, true), do: meta + + defp line(meta, false) do + Keyword.delete(meta, :line) + end + + defp line(meta, line) do + keystore(:line, meta, line) + end + + defguardp defs(kind) when kind in [:def, :defp, :defmacro, :defmacrop, :@] + defguardp lexical(kind) when kind in [:import, :alias, :require] + + defp annotate({def, meta, [h | t]}, context) when defs(def) do + {def, meta, [annotate_def(h, context) | t]} + end + + defp annotate({{:., _, [_, def]} = target, meta, [h | t]}, context) when defs(def) do + {target, meta, [annotate_def(h, context) | t]} + end + + defp annotate({lexical, meta, [_ | _] = args}, context) when lexical(lexical) do + new_meta = keystore(:context, Keyword.delete(meta, :counter), context) + {lexical, new_meta, args} + end + + defp annotate(tree, _context), do: tree + + defp annotate_def({:when, meta, [left, right]}, context) do + {:when, meta, [annotate_def(left, context), right]} + end + + defp annotate_def({fun, meta, args}, context) do + {fun, keystore(:context, meta, context), args} + end + + defp annotate_def(other, _context), do: other + + defp do_escape({left, meta, right}, q = %{op: :prune_metadata}) when is_list(meta) do + tm = for {k, v} <- meta, k == :no_parens or k == :line, do: {k, v} + tl = do_quote(left, q) + tr = do_quote(right, q) + {:{}, [], [tl, tm, tr]} + end + + defp do_escape(tuple, q) when is_tuple(tuple) do + tt = do_quote(Tuple.to_list(tuple), q) + {:{}, [], tt} + end + + defp do_escape(bitstring, _) when is_bitstring(bitstring) do + case Bitwise.band(bit_size(bitstring), 7) do + 0 -> + bitstring + + size -> + <> = bitstring + + {:<<>>, [], + [{:"::", [], [bits, {size, [], [size]}]}, {:"::", [], [bytes, {:binary, [], nil}]}]} + end + end + + defp do_escape(map, q) when is_map(map) do + tt = do_quote(Enum.sort(Map.to_list(map)), q) + {:%{}, [], tt} + end + + defp do_escape([], _), do: [] + + defp do_escape([h | t], %__MODULE__{unquote: false} = q) do + do_quote_simple_list(t, do_quote(h, q), q) + end + + defp do_escape([h | t], q) do + # The improper case is inefficient, but improper lists are rare. + try do + l = Enum.reverse(t, [h]) + do_quote_tail(l, q) + catch + _ -> + {l, r} = reverse_improper(t, [h]) + tl = do_quote_splice(l, q, [], []) + tr = do_quote(r, q) + update_last(tl, fn x -> {:|, [], [x, tr]} end) + end + end + + defp do_escape(other, _) when is_number(other) or is_pid(other) or is_atom(other), + do: other + + defp do_escape(fun, _) when is_function(fun) do + case {Function.info(fun, :env), Function.info(fun, :type)} do + {{:env, []}, {:type, :external}} -> + fun_to_quoted(fun) + + _ -> + # elixir raises here ArgumentError + nil + end + end + + defp do_escape(_other, _) do + # elixir raises here ArgumentError + nil + end + + defp reverse_improper([h | t], acc), do: reverse_improper(t, [h | acc]) + defp reverse_improper([], acc), do: acc + defp reverse_improper(t, acc), do: {acc, t} + defp update_last([], _), do: [] + defp update_last([h], f), do: [f.(h)] + defp update_last([h | t], f), do: [h | update_last(t, f)] + + defp keystore(_key, meta, value) when value == nil do + meta + end + + defp keystore(key, meta, value) do + :lists.keystore(key, 1, meta, {key, value}) + end +end diff --git a/lib/elixir_sense/core/compiler/rewrite.ex b/lib/elixir_sense/core/compiler/rewrite.ex new file mode 100644 index 00000000..d3053b86 --- /dev/null +++ b/lib/elixir_sense/core/compiler/rewrite.ex @@ -0,0 +1,32 @@ +defmodule ElixirSense.Core.Compiler.Rewrite do + def inline(module, fun, arity) do + :elixir_rewrite.inline(module, fun, arity) + end + + def rewrite(context, receiver, dot_meta, right, meta, e_args, s) do + do_rewrite(context, receiver, dot_meta, right, meta, e_args, s) + end + + defp do_rewrite(_, :erlang, _, :+, _, [arg], _s) when is_number(arg), do: {:ok, arg} + + defp do_rewrite(_, :erlang, _, :-, _, [arg], _s) when is_number(arg), do: {:ok, -arg} + + defp do_rewrite(:match, receiver, dot_meta, right, meta, e_args, _s) do + :elixir_rewrite.match_rewrite(receiver, dot_meta, right, meta, e_args) + end + + if Version.match?(System.version(), "< 1.14.0") do + defp do_rewrite(:guard, receiver, dot_meta, right, meta, e_args, s) do + :elixir_rewrite.guard_rewrite(receiver, dot_meta, right, meta, e_args) + end + else + defp do_rewrite(:guard, receiver, dot_meta, right, meta, e_args, _s) do + # elixir uses guard context for error messages + :elixir_rewrite.guard_rewrite(receiver, dot_meta, right, meta, e_args, "guard") + end + end + + defp do_rewrite(_, receiver, dot_meta, right, meta, e_args, _s) do + {:ok, :elixir_rewrite.rewrite(receiver, dot_meta, right, meta, e_args)} + end +end diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/compiler/state.ex similarity index 100% rename from lib/elixir_sense/core/state.ex rename to lib/elixir_sense/core/compiler/state.ex diff --git a/lib/elixir_sense/core/compiler/utils.ex b/lib/elixir_sense/core/compiler/utils.ex new file mode 100644 index 00000000..6cc07090 --- /dev/null +++ b/lib/elixir_sense/core/compiler/utils.ex @@ -0,0 +1,98 @@ +defmodule ElixirSense.Core.Compiler.Utils do + def generated([{:generated, true} | _] = meta), do: meta + def generated(meta), do: [{:generated, true} | meta] + + def split_last([]), do: {[], []} + + def split_last(list), do: split_last(list, []) + + defp split_last([h], acc), do: {Enum.reverse(acc), h} + + defp split_last([h | t], acc), do: split_last(t, [h | acc]) + + def split_opts(args) do + case split_last(args) do + {outer_cases, outer_opts} when is_list(outer_opts) -> + case split_last(outer_cases) do + {inner_cases, inner_opts} when is_list(inner_opts) -> + {inner_cases, inner_opts ++ outer_opts} + + _ -> + {outer_cases, outer_opts} + end + + _ -> + {args, []} + end + end + + def get_line(opts) when is_list(opts) do + case Keyword.fetch(opts, :line) do + {:ok, line} when is_integer(line) -> line + _ -> 0 + end + end + + def extract_guards({:when, _, [left, right]}), do: {left, extract_or_guards(right)} + def extract_guards(term), do: {term, []} + + def extract_or_guards({:when, _, [left, right]}), do: [left | extract_or_guards(right)] + def extract_or_guards(term), do: [term] + + def select_with_cursor(ast_list) do + Enum.find(ast_list, &has_cursor?/1) + end + + def has_cursor?(ast) do + # TODO rewrite to lazy prewalker + {_, result} = + Macro.prewalk(ast, false, fn + _node, true -> + {nil, true} + + {:__cursor__, _, list}, _state when is_list(list) -> + {nil, true} + + node, false -> + {node, false} + end) + + result + end + + def defdelegate_each(fun, opts) when is_list(opts) do + # TODO Remove on elixir v2.0 + append_first? = Keyword.get(opts, :append_first, false) + + {name, args} = + case fun do + {:when, _, [_left, right]} -> + raise ArgumentError, + "guards are not allowed in defdelegate/2, got: when #{Macro.to_string(right)}" + + _ -> + case Macro.decompose_call(fun) do + {_, _} = pair -> pair + _ -> raise ArgumentError, "invalid syntax in defdelegate #{Macro.to_string(fun)}" + end + end + + as = Keyword.get(opts, :as, name) + as_args = build_as_args(args, append_first?) + + {name, args, as, as_args} + end + + defp build_as_args(args, append_first?) do + as_args = :lists.map(&build_as_arg/1, args) + + case append_first? do + true -> tl(as_args) ++ [hd(as_args)] + false -> as_args + end + end + + # elixir validates arg + defp build_as_arg({:\\, _, [arg, _default_arg]}), do: arg + defp build_as_arg(arg), do: arg +end From d0d33d5e6f5a3c713832ff423043e88487ccf38d Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 5 Oct 2024 08:43:43 +0200 Subject: [PATCH 227/235] fix small issues --- lib/elixir_sense/core/compiler.ex | 2 +- lib/elixir_sense/core/compiler/state.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 5ac8416c..bec54bc5 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1096,7 +1096,7 @@ defmodule ElixirSense.Core.Compiler do } end - :error -> + _other -> acc end end) diff --git a/lib/elixir_sense/core/compiler/state.ex b/lib/elixir_sense/core/compiler/state.ex index d33a1b5e..b6d286a6 100644 --- a/lib/elixir_sense/core/compiler/state.ex +++ b/lib/elixir_sense/core/compiler/state.ex @@ -28,7 +28,7 @@ defmodule ElixirSense.Core.State do } @type vars_info_per_scope_id_t :: %{ optional(scope_id_t) => [ - %{optional({atom(), non_neg_integer()}) => ElixirSense.Core.State.VerInfo.t()} + %{optional({atom(), non_neg_integer()}) => ElixirSense.Core.State.VarInfo.t()} ] } @type structs_t :: %{optional(module) => ElixirSense.Core.State.StructInfo.t()} From c085d6e9a0aee49c50abe63ced192d832291eac7 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 5 Oct 2024 08:56:02 +0200 Subject: [PATCH 228/235] split out infos from state --- lib/elixir_sense/core/compiler/state.ex | 257 +----------------- lib/elixir_sense/core/state/attribute_info.ex | 11 + lib/elixir_sense/core/state/call_info.ex | 15 + lib/elixir_sense/core/state/env.ex | 77 ++++++ lib/elixir_sense/core/state/mod_fun_info.ex | 56 ++++ lib/elixir_sense/core/state/spec_info.ex | 25 ++ lib/elixir_sense/core/state/struct_info.ex | 11 + lib/elixir_sense/core/state/type_info.ex | 25 ++ lib/elixir_sense/core/state/var_info.ex | 18 ++ 9 files changed, 250 insertions(+), 245 deletions(-) create mode 100644 lib/elixir_sense/core/state/attribute_info.ex create mode 100644 lib/elixir_sense/core/state/call_info.ex create mode 100644 lib/elixir_sense/core/state/env.ex create mode 100644 lib/elixir_sense/core/state/mod_fun_info.ex create mode 100644 lib/elixir_sense/core/state/spec_info.ex create mode 100644 lib/elixir_sense/core/state/struct_info.ex create mode 100644 lib/elixir_sense/core/state/type_info.ex create mode 100644 lib/elixir_sense/core/state/var_info.ex diff --git a/lib/elixir_sense/core/compiler/state.ex b/lib/elixir_sense/core/compiler/state.ex index b6d286a6..e567b9a9 100644 --- a/lib/elixir_sense/core/compiler/state.ex +++ b/lib/elixir_sense/core/compiler/state.ex @@ -3,8 +3,19 @@ defmodule ElixirSense.Core.State do Core State """ - alias ElixirSense.Core.Introspection alias ElixirSense.Core.BuiltinFunctions + alias ElixirSense.Core.State.Env + + alias ElixirSense.Core.State.{ + CallInfo, + StructInfo, + ModFunInfo, + SpecInfo, + TypeInfo, + VarInfo, + AttributeInfo + } + require Logger @type fun_arity :: {atom, non_neg_integer} @@ -109,250 +120,6 @@ defmodule ElixirSense.Core.State do attribute_store: %{}, cursor_position: nil - defmodule Env do - @moduledoc """ - Line environment - """ - - @type t :: %Env{ - functions: [{module, [{atom, arity}]}], - macros: [{module, [{atom, arity}]}], - requires: list(module), - aliases: list(ElixirSense.Core.State.alias_t()), - macro_aliases: [{module, {term, module}}], - context: nil | :match | :guard, - module: nil | module, - function: nil | {atom, arity}, - protocol: nil | ElixirSense.Core.State.protocol_t(), - versioned_vars: %{optional({atom, atom}) => non_neg_integer}, - vars: list(ElixirSense.Core.State.VarInfo.t()), - attributes: list(ElixirSense.Core.State.AttributeInfo.t()), - behaviours: list(module), - context_modules: list(module), - typespec: nil | {atom, arity}, - scope_id: nil | ElixirSense.Core.State.scope_id_t() - } - defstruct functions: [], - macros: [], - requires: [], - aliases: [], - macro_aliases: [], - # NOTE for protocol implementation this will be the first variant - module: nil, - function: nil, - # NOTE for protocol implementation this will be the first variant - protocol: nil, - versioned_vars: %{}, - vars: [], - attributes: [], - behaviours: [], - context_modules: [], - context: nil, - typespec: nil, - scope_id: nil - - def to_macro_env(%__MODULE__{} = env, file \\ "nofile", line \\ 1) do - # we omit lexical_tracker and tracers - %Macro.Env{ - line: line, - file: file, - context: env.context, - module: env.module, - function: env.function, - context_modules: env.context_modules, - macros: env.macros, - functions: env.functions, - requires: env.requires, - aliases: env.aliases, - macro_aliases: env.macro_aliases, - versioned_vars: env.versioned_vars - } - end - - def update_from_macro_env(%__MODULE__{} = env, macro_env = %Macro.Env{}) do - # we omit lexical_tracker and tracers - %__MODULE__{ - env - | context: macro_env.context, - module: macro_env.module, - function: macro_env.function, - context_modules: macro_env.context_modules, - macros: macro_env.macros, - functions: macro_env.functions, - requires: macro_env.requires, - aliases: macro_env.aliases, - macro_aliases: macro_env.macro_aliases, - versioned_vars: macro_env.versioned_vars - } - end - end - - defmodule VarInfo do - @moduledoc """ - Variable info - """ - - @type t :: %VarInfo{ - name: atom, - positions: list(ElixirSense.Core.State.position_t()), - scope_id: nil | ElixirSense.Core.State.scope_id_t(), - version: non_neg_integer(), - type: ElixirSense.Core.State.var_type() - } - defstruct name: nil, - positions: [], - scope_id: nil, - version: 0, - type: nil - end - - defmodule TypeInfo do - @moduledoc """ - Type definition info - """ - @type t :: %TypeInfo{ - name: atom, - args: list(list(String.t())), - specs: [String.t()], - kind: :type | :typep | :opaque, - positions: [ElixirSense.Core.State.position_t()], - end_positions: [ElixirSense.Core.State.position_t() | nil], - doc: String.t(), - meta: map(), - generated: list(boolean) - } - defstruct name: nil, - args: [], - specs: [], - kind: :type, - positions: [], - end_positions: [], - generated: [], - doc: "", - meta: %{} - end - - defmodule SpecInfo do - @moduledoc """ - Type definition info - """ - @type t :: %SpecInfo{ - name: atom, - args: list(list(String.t())), - specs: [String.t()], - kind: :spec | :callback | :macrocallback, - positions: [ElixirSense.Core.State.position_t()], - end_positions: [ElixirSense.Core.State.position_t() | nil], - doc: String.t(), - meta: map(), - generated: list(boolean) - } - defstruct name: nil, - args: [], - specs: [], - kind: :spec, - positions: [], - end_positions: [], - generated: [], - doc: "", - meta: %{} - end - - defmodule StructInfo do - @moduledoc """ - Structure definition info - """ - @type field_t :: {atom, any} - @type t :: %StructInfo{ - type: :defstruct | :defexception, - fields: list(field_t) - } - defstruct type: :defstruct, fields: [] - end - - defmodule AttributeInfo do - @moduledoc """ - Variable info - """ - @type t :: %AttributeInfo{ - name: atom, - positions: list(ElixirSense.Core.State.position_t()), - type: ElixirSense.Core.State.var_type() - } - defstruct name: nil, positions: [], type: nil - end - - defmodule CallInfo do - @moduledoc """ - Function call info - """ - @type t :: %CallInfo{ - arity: non_neg_integer, - position: ElixirSense.Core.State.position_t(), - func: atom, - mod: module | {:attribute, atom} - } - defstruct arity: 0, - position: {1, 1}, - func: nil, - mod: Elixir - end - - defmodule ModFunInfo do - @moduledoc """ - Module or function info - """ - - @type t :: %ModFunInfo{ - params: list(list(term)), - positions: list(ElixirSense.Core.State.position_t()), - end_positions: list(ElixirSense.Core.State.position_t() | nil), - target: nil | {module, atom}, - overridable: false | {true, module}, - generated: list(boolean), - doc: String.t(), - meta: map(), - # TODO defmodule defprotocol defimpl? - type: - :def - | :defp - | :defmacro - | :defmacrop - | :defdelegate - | :defguard - | :defguardp - | :defmodule - } - - defstruct params: [], - positions: [], - end_positions: [], - target: nil, - type: nil, - generated: [], - overridable: false, - doc: "", - meta: %{} - - def get_arities(%ModFunInfo{params: params_variants}) do - params_variants - |> Enum.map(fn params -> - {length(params), Introspection.count_defaults(params)} - end) - end - - def get_category(%ModFunInfo{type: type}) - when type in [:defmacro, :defmacrop, :defguard, :defguardp], - do: :macro - - def get_category(%ModFunInfo{type: type}) when type in [:def, :defp, :defdelegate], - do: :function - - def get_category(%ModFunInfo{}), do: :module - - def private?(%ModFunInfo{type: type}), do: type in [:defp, :defmacrop, :defguardp] - end - defp get_current_env(%__MODULE__{} = state, macro_env) do current_attributes = state |> get_current_attributes() current_behaviours = state.behaviours |> Map.get(macro_env.module, []) diff --git a/lib/elixir_sense/core/state/attribute_info.ex b/lib/elixir_sense/core/state/attribute_info.ex new file mode 100644 index 00000000..9a12e2bb --- /dev/null +++ b/lib/elixir_sense/core/state/attribute_info.ex @@ -0,0 +1,11 @@ +defmodule ElixirSense.Core.State.AttributeInfo do + @moduledoc """ + Variable info + """ + @type t :: %ElixirSense.Core.State.AttributeInfo{ + name: atom, + positions: list(ElixirSense.Core.State.position_t()), + type: ElixirSense.Core.State.var_type() + } + defstruct name: nil, positions: [], type: nil +end diff --git a/lib/elixir_sense/core/state/call_info.ex b/lib/elixir_sense/core/state/call_info.ex new file mode 100644 index 00000000..c0fdfa0c --- /dev/null +++ b/lib/elixir_sense/core/state/call_info.ex @@ -0,0 +1,15 @@ +defmodule ElixirSense.Core.State.CallInfo do + @moduledoc """ + Function call info + """ + @type t :: %ElixirSense.Core.State.CallInfo{ + arity: non_neg_integer, + position: ElixirSense.Core.State.position_t(), + func: atom, + mod: module | {:attribute, atom} + } + defstruct arity: 0, + position: {1, 1}, + func: nil, + mod: Elixir +end diff --git a/lib/elixir_sense/core/state/env.ex b/lib/elixir_sense/core/state/env.ex new file mode 100644 index 00000000..f66447c7 --- /dev/null +++ b/lib/elixir_sense/core/state/env.ex @@ -0,0 +1,77 @@ +defmodule ElixirSense.Core.State.Env do + @moduledoc """ + Line environment + """ + + @type t :: %ElixirSense.Core.State.Env{ + functions: [{module, [{atom, arity}]}], + macros: [{module, [{atom, arity}]}], + requires: list(module), + aliases: list(ElixirSense.Core.State.alias_t()), + macro_aliases: [{module, {term, module}}], + context: nil | :match | :guard, + module: nil | module, + function: nil | {atom, arity}, + protocol: nil | ElixirSense.Core.State.protocol_t(), + versioned_vars: %{optional({atom, atom}) => non_neg_integer}, + vars: list(ElixirSense.Core.State.VarInfo.t()), + attributes: list(ElixirSense.Core.State.AttributeInfo.t()), + behaviours: list(module), + context_modules: list(module), + typespec: nil | {atom, arity}, + scope_id: nil | ElixirSense.Core.State.scope_id_t() + } + defstruct functions: [], + macros: [], + requires: [], + aliases: [], + macro_aliases: [], + # NOTE for protocol implementation this will be the first variant + module: nil, + function: nil, + # NOTE for protocol implementation this will be the first variant + protocol: nil, + versioned_vars: %{}, + vars: [], + attributes: [], + behaviours: [], + context_modules: [], + context: nil, + typespec: nil, + scope_id: nil + + def to_macro_env(%__MODULE__{} = env, file \\ "nofile", line \\ 1) do + # we omit lexical_tracker and tracers + %Macro.Env{ + line: line, + file: file, + context: env.context, + module: env.module, + function: env.function, + context_modules: env.context_modules, + macros: env.macros, + functions: env.functions, + requires: env.requires, + aliases: env.aliases, + macro_aliases: env.macro_aliases, + versioned_vars: env.versioned_vars + } + end + + def update_from_macro_env(%__MODULE__{} = env, macro_env = %Macro.Env{}) do + # we omit lexical_tracker and tracers + %__MODULE__{ + env + | context: macro_env.context, + module: macro_env.module, + function: macro_env.function, + context_modules: macro_env.context_modules, + macros: macro_env.macros, + functions: macro_env.functions, + requires: macro_env.requires, + aliases: macro_env.aliases, + macro_aliases: macro_env.macro_aliases, + versioned_vars: macro_env.versioned_vars + } + end +end diff --git a/lib/elixir_sense/core/state/mod_fun_info.ex b/lib/elixir_sense/core/state/mod_fun_info.ex new file mode 100644 index 00000000..7e1f8f19 --- /dev/null +++ b/lib/elixir_sense/core/state/mod_fun_info.ex @@ -0,0 +1,56 @@ +defmodule ElixirSense.Core.State.ModFunInfo do + @moduledoc """ + Module or function info + """ + alias ElixirSense.Core.State.ModFunInfo + alias ElixirSense.Core.Introspection + + @type t :: %ElixirSense.Core.State.ModFunInfo{ + params: list(list(term)), + positions: list(ElixirSense.Core.State.position_t()), + end_positions: list(ElixirSense.Core.State.position_t() | nil), + target: nil | {module, atom}, + overridable: false | {true, module}, + generated: list(boolean), + doc: String.t(), + meta: map(), + # TODO defmodule defprotocol defimpl? + type: + :def + | :defp + | :defmacro + | :defmacrop + | :defdelegate + | :defguard + | :defguardp + | :defmodule + } + + defstruct params: [], + positions: [], + end_positions: [], + target: nil, + type: nil, + generated: [], + overridable: false, + doc: "", + meta: %{} + + def get_arities(%ModFunInfo{params: params_variants}) do + params_variants + |> Enum.map(fn params -> + {length(params), Introspection.count_defaults(params)} + end) + end + + def get_category(%ModFunInfo{type: type}) + when type in [:defmacro, :defmacrop, :defguard, :defguardp], + do: :macro + + def get_category(%ModFunInfo{type: type}) when type in [:def, :defp, :defdelegate], + do: :function + + def get_category(%ModFunInfo{}), do: :module + + def private?(%ModFunInfo{type: type}), do: type in [:defp, :defmacrop, :defguardp] +end diff --git a/lib/elixir_sense/core/state/spec_info.ex b/lib/elixir_sense/core/state/spec_info.ex new file mode 100644 index 00000000..a80be660 --- /dev/null +++ b/lib/elixir_sense/core/state/spec_info.ex @@ -0,0 +1,25 @@ +defmodule ElixirSense.Core.State.SpecInfo do + @moduledoc """ + Type definition info + """ + @type t :: %ElixirSense.Core.State.SpecInfo{ + name: atom, + args: list(list(String.t())), + specs: [String.t()], + kind: :spec | :callback | :macrocallback, + positions: [ElixirSense.Core.State.position_t()], + end_positions: [ElixirSense.Core.State.position_t() | nil], + doc: String.t(), + meta: map(), + generated: list(boolean) + } + defstruct name: nil, + args: [], + specs: [], + kind: :spec, + positions: [], + end_positions: [], + generated: [], + doc: "", + meta: %{} +end diff --git a/lib/elixir_sense/core/state/struct_info.ex b/lib/elixir_sense/core/state/struct_info.ex new file mode 100644 index 00000000..f1f46632 --- /dev/null +++ b/lib/elixir_sense/core/state/struct_info.ex @@ -0,0 +1,11 @@ +defmodule ElixirSense.Core.State.StructInfo do + @moduledoc """ + Structure definition info + """ + @type field_t :: {atom, any} + @type t :: %ElixirSense.Core.State.StructInfo{ + type: :defstruct | :defexception, + fields: list(field_t) + } + defstruct type: :defstruct, fields: [] +end diff --git a/lib/elixir_sense/core/state/type_info.ex b/lib/elixir_sense/core/state/type_info.ex new file mode 100644 index 00000000..4052a581 --- /dev/null +++ b/lib/elixir_sense/core/state/type_info.ex @@ -0,0 +1,25 @@ +defmodule ElixirSense.Core.State.TypeInfo do + @moduledoc """ + Type definition info + """ + @type t :: %ElixirSense.Core.State.TypeInfo{ + name: atom, + args: list(list(String.t())), + specs: [String.t()], + kind: :type | :typep | :opaque, + positions: [ElixirSense.Core.State.position_t()], + end_positions: [ElixirSense.Core.State.position_t() | nil], + doc: String.t(), + meta: map(), + generated: list(boolean) + } + defstruct name: nil, + args: [], + specs: [], + kind: :type, + positions: [], + end_positions: [], + generated: [], + doc: "", + meta: %{} +end diff --git a/lib/elixir_sense/core/state/var_info.ex b/lib/elixir_sense/core/state/var_info.ex new file mode 100644 index 00000000..c7eba2be --- /dev/null +++ b/lib/elixir_sense/core/state/var_info.ex @@ -0,0 +1,18 @@ +defmodule ElixirSense.Core.State.VarInfo do + @moduledoc """ + Variable info + """ + + @type t :: %ElixirSense.Core.State.VarInfo{ + name: atom, + positions: list(ElixirSense.Core.State.position_t()), + scope_id: nil | ElixirSense.Core.State.scope_id_t(), + version: non_neg_integer(), + type: ElixirSense.Core.State.var_type() + } + defstruct name: nil, + positions: [], + scope_id: nil, + version: 0, + type: nil +end From 1a04eaa763aa9c4a1e6e48b4d71b0aa1efcb7936 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 5 Oct 2024 09:23:15 +0200 Subject: [PATCH 229/235] separate compiler state --- lib/elixir_sense/core/compiler.ex | 269 +++++++++--------- lib/elixir_sense/core/compiler/bitstring.ex | 2 +- lib/elixir_sense/core/compiler/clauses.ex | 2 +- lib/elixir_sense/core/compiler/dispatch.ex | 6 +- lib/elixir_sense/core/compiler/fn.ex | 2 +- lib/elixir_sense/core/compiler/state.ex | 12 +- lib/elixir_sense/core/compiler/typespec.ex | 34 +-- lib/elixir_sense/core/metadata.ex | 7 +- lib/elixir_sense/core/metadata_builder.ex | 28 +- .../core/compiler/typespec_test.exs | 2 +- test/elixir_sense/core/compiler_test.exs | 2 +- test/elixir_sense/core/parser_test.exs | 1 - 12 files changed, 185 insertions(+), 182 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index bec54bc5..a826a950 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -1,12 +1,12 @@ defmodule ElixirSense.Core.Compiler do - import ElixirSense.Core.State - alias ElixirSense.Core.State + alias ElixirSense.Core.Compiler.State require Logger alias ElixirSense.Core.Introspection alias ElixirSense.Core.TypeInfo alias ElixirSense.Core.TypeInference alias ElixirSense.Core.TypeInference.Guard alias ElixirSense.Core.Normalized.Macro.Env, as: NormalizedMacroEnv + alias ElixirSense.Core.State.ModFunInfo @env :elixir_env.new() def env, do: @env @@ -17,8 +17,8 @@ defmodule ElixirSense.Core.Compiler do case ast do {_, meta, _} when is_list(meta) -> state - |> add_current_env_to_line(meta, env) - |> update_closest_env(meta, env) + |> State.add_current_env_to_line(meta, env) + |> State.update_closest_env(meta, env) # state _ -> @@ -47,7 +47,7 @@ defmodule ElixirSense.Core.Compiler do vars_with_inferred_types = TypeInference.find_typed_vars(e_expr, nil, el.context) - sl = merge_inferred_types(sl, vars_with_inferred_types) + sl = State.merge_inferred_types(sl, vars_with_inferred_types) {e_expr, sl, el} end @@ -148,8 +148,8 @@ defmodule ElixirSense.Core.Compiler do defp do_expand({:alias, meta, [arg, opts]}, state, env) do state = state - |> add_first_alias_positions(env, meta) - |> add_current_env_to_line(meta, env) + |> State.add_first_alias_positions(env, meta) + |> State.add_current_env_to_line(meta, env) # no need to call expand_without_aliases_report - we never report {arg, state, env} = expand(arg, state, env) @@ -173,7 +173,7 @@ defmodule ElixirSense.Core.Compiler do defp do_expand({:require, meta, [arg, opts]}, state, env) do state = state - |> add_current_env_to_line(meta, env) + |> State.add_current_env_to_line(meta, env) # no need to call expand_without_aliases_report - we never report {arg, state, env} = expand(arg, state, env) @@ -204,7 +204,7 @@ defmodule ElixirSense.Core.Compiler do defp do_expand({:import, meta, [arg, opts]}, state, env) do state = state - |> add_current_env_to_line(meta, env) + |> State.add_current_env_to_line(meta, env) # no need to call expand_without_aliases_report - we never report {arg, state, env} = expand(arg, state, env) @@ -235,34 +235,34 @@ defmodule ElixirSense.Core.Compiler do # Compilation environment macros defp do_expand({:__MODULE__, meta, ctx}, state, env) when is_atom(ctx) do - state = add_current_env_to_line(state, meta, env) + state = State.add_current_env_to_line(state, meta, env) {env.module, state, env} end defp do_expand({:__DIR__, meta, ctx}, state, env) when is_atom(ctx) do - state = add_current_env_to_line(state, meta, env) + state = State.add_current_env_to_line(state, meta, env) {Path.dirname(env.file), state, env} end defp do_expand({:__CALLER__, meta, ctx} = caller, state, env) when is_atom(ctx) do # elixir checks if context is not match and if caller is allowed - state = add_current_env_to_line(state, meta, env) + state = State.add_current_env_to_line(state, meta, env) {caller, state, env} end defp do_expand({:__STACKTRACE__, meta, ctx} = stacktrace, state, env) when is_atom(ctx) do # elixir checks if context is not match and if stacktrace is allowed - state = add_current_env_to_line(state, meta, env) + state = State.add_current_env_to_line(state, meta, env) {stacktrace, state, env} end defp do_expand({:__ENV__, meta, ctx}, state, env) when is_atom(ctx) do # elixir checks if context is not match - state = add_current_env_to_line(state, meta, env) + state = State.add_current_env_to_line(state, meta, env) {escape_map(escape_env_entries(meta, state, env)), state, env} end @@ -270,7 +270,7 @@ defmodule ElixirSense.Core.Compiler do defp do_expand({{:., dot_meta, [{:__ENV__, meta, atom}, field]}, call_meta, []}, s, e) when is_atom(atom) and is_atom(field) do # elixir checks if context is not match - s = add_current_env_to_line(s, call_meta, e) + s = State.add_current_env_to_line(s, call_meta, e) env = escape_env_entries(meta, s, e) @@ -287,7 +287,7 @@ defmodule ElixirSense.Core.Compiler do # elixir raises here unquote_outside_quote # we may have cursor there {arg, s, e} = expand(arg, s, e) - s = s |> add_current_env_to_line(meta, e) + s = s |> State.add_current_env_to_line(meta, e) {{unquote_call, meta, [arg]}, s, e} end @@ -307,7 +307,7 @@ defmodule ElixirSense.Core.Compiler do # elixir raises here invalid_args # we may have cursor there {arg, s, e} = expand(arg, s, e) - s = s |> add_current_env_to_line(meta, e) + s = s |> State.add_current_env_to_line(meta, e) {{:quote, meta, [arg]}, s, e} end @@ -364,7 +364,7 @@ defmodule ElixirSense.Core.Compiler do quoted = __MODULE__.Quote.quote(exprs, q) {e_quoted, es, eq} = expand(quoted, sc, ec) - es = es |> add_current_env_to_line(meta, eq) + es = es |> State.add_current_env_to_line(meta, eq) e_binding = for {k, v} <- binding do @@ -411,8 +411,8 @@ defmodule ElixirSense.Core.Compiler do {kind, name, _} when kind in [:def, :defp] -> s = s - |> add_call_to_line({nil, name, arity}, super_meta) - |> add_current_env_to_line(super_meta, e) + |> State.add_call_to_line({nil, name, arity}, super_meta) + |> State.add_current_env_to_line(super_meta, e) {{:&, meta, [{:/, arity_meta, [{name, super_meta, context}, arity]}]}, s, e} @@ -463,7 +463,7 @@ defmodule ElixirSense.Core.Compiler do s = unless s.cursor_env do s - |> add_cursor_env(meta, e) + |> State.add_cursor_env(meta, e) else s end @@ -488,8 +488,8 @@ defmodule ElixirSense.Core.Compiler do sa = sa - |> add_call_to_line({nil, name, arity}, meta) - |> add_current_env_to_line(meta, ea) + |> State.add_call_to_line({nil, name, arity}, meta) + |> State.add_current_env_to_line(meta, ea) {{:super, [{:super, {kind, name}} | meta], e_args}, sa, ea} @@ -510,7 +510,7 @@ defmodule ElixirSense.Core.Compiler do case expand(arg, no_match_s, %{e | context: nil}) do {{name, _var_meta, kind} = var, %{unused: unused}, _} when is_atom(name) and is_atom(kind) -> - s = add_var_read(s, var) + s = State.add_var_read(s, var) {{:^, meta, [var]}, %{s | unused: unused}, e} {arg, s, _e} -> @@ -546,7 +546,7 @@ defmodule ElixirSense.Core.Compiler do %{^pair => var_version} when var_version >= prematch_version -> var = {name, [{:version, var_version} | meta], kind} # it's a write but for simplicity treat it as read - s = add_var_read(s, var) + s = State.add_var_read(s, var) {var, %{s | unused: version}, e} # Variable is being overridden now @@ -554,7 +554,7 @@ defmodule ElixirSense.Core.Compiler do new_read = Map.put(read, pair, version) new_write = if write != false, do: Map.put(write, pair, version), else: write var = {name, [{:version, version} | meta], kind} - s = add_var_write(s, var) + s = State.add_var_write(s, var) {var, %{s | vars: {new_read, new_write}, unused: version + 1}, e} # Variable defined for the first time @@ -562,7 +562,7 @@ defmodule ElixirSense.Core.Compiler do new_read = Map.put(read, pair, version) new_write = if write != false, do: Map.put(write, pair, version), else: write var = {name, [{:version, version} | meta], kind} - s = add_var_write(s, var) + s = State.add_var_write(s, var) {var, %{s | vars: {new_read, new_write}, unused: version + 1}, e} end end @@ -602,7 +602,7 @@ defmodule ElixirSense.Core.Compiler do case result do {:ok, pair_version} -> var = {name, [{:version, pair_version} | meta], kind} - s = add_var_read(s, var) + s = State.add_var_read(s, var) {var, s, e} error -> @@ -652,8 +652,8 @@ defmodule ElixirSense.Core.Compiler do # :elixir_env.env_to_ex(env) possibly losing some details. Jose Valim is convinced this is not a problem state = state - |> add_call_to_line({module, fun, length(args)}, meta) - |> add_current_env_to_line(meta, env) + |> State.add_call_to_line({module, fun, length(args)}, meta) + |> State.add_current_env_to_line(meta, env) expand_macro(meta, module, fun, args, callback, state, env) @@ -667,7 +667,7 @@ defmodule ElixirSense.Core.Compiler do {module, fun} end - expand_remote(ar, meta, af, meta, args, state, prepare_write(state), env) + expand_remote(ar, meta, af, meta, args, state, State.prepare_write(state), env) {:error, :not_found} -> expand_local(meta, fun, args, state, env) @@ -690,7 +690,7 @@ defmodule ElixirSense.Core.Compiler do when (is_tuple(module) or is_atom(module)) and is_atom(fun) and is_list(meta) and is_list(args) do # dbg({module, fun, args}) - {module, state_l, env} = expand(module, prepare_write(state), env) + {module, state_l, env} = expand(module, State.prepare_write(state), env) arity = length(args) if is_atom(module) do @@ -708,8 +708,8 @@ defmodule ElixirSense.Core.Compiler do # :elixir_env.env_to_ex(env) possibly losing some details. Jose Valim is convinced this is not a problem state = state - |> add_call_to_line({module, fun, length(args)}, meta) - |> add_current_env_to_line(meta, env) + |> State.add_call_to_line({module, fun, length(args)}, meta) + |> State.add_current_env_to_line(meta, env) expand_macro(meta, module, fun, args, callback, state, env) @@ -735,8 +735,8 @@ defmodule ElixirSense.Core.Compiler do sa = sa - |> add_call_to_line({nil, e_expr, length(e_args)}, dot_meta) - |> add_current_env_to_line(meta, e) + |> State.add_call_to_line({nil, e_expr, length(e_args)}, dot_meta) + |> State.add_current_env_to_line(meta, e) {{{:., dot_meta, [e_expr]}, meta, e_args}, sa, ea} end @@ -768,9 +768,9 @@ defmodule ElixirSense.Core.Compiler do defp do_expand(list, s, e) when is_list(list) do {e_args, {se, _}, ee} = - expand_list(list, &expand_arg/3, {prepare_write(s), s}, e, []) + expand_list(list, &expand_arg/3, {State.prepare_write(s), s}, e, []) - {e_args, close_write(se, s), ee} + {e_args, State.close_write(se, s), ee} end defp do_expand(function, s, e) when is_function(function) do @@ -832,7 +832,7 @@ defmodule ElixirSense.Core.Compiler do {fun, state, has_unquotes} = if __MODULE__.Quote.has_unquotes(fun) do - state = new_vars_scope(state) + state = State.new_vars_scope(state) # dynamic defdelegate - replace unquote expression with fake call case fun do {{:unquote, _, unquote_args}, meta, args} -> @@ -843,7 +843,7 @@ defmodule ElixirSense.Core.Compiler do {fun, state, true} end else - state = new_func_vars_scope(state) + state = State.new_func_vars_scope(state) {fun, state, false} end @@ -877,19 +877,19 @@ defmodule ElixirSense.Core.Compiler do state = unless has_unquotes do # restore module vars - remove_func_vars_scope(state, state_orig) + State.remove_func_vars_scope(state, state_orig) else # remove scope - remove_vars_scope(state, state_orig) + State.remove_vars_scope(state, state_orig) end state - |> add_current_env_to_line(meta, %{env | context: nil, function: {name, arity}}) - |> add_func_to_index( + |> State.add_current_env_to_line(meta, %{env | context: nil, function: {name, arity}}) + |> State.add_func_to_index( env, name, args, - extract_range(meta), + State.extract_range(meta), :defdelegate, target: {target, as, length(as_args)} ) @@ -924,10 +924,10 @@ defmodule ElixirSense.Core.Compiler do when module != nil do state = state - |> add_current_env_to_line(meta, env) + |> State.add_current_env_to_line(meta, env) {arg, state, env} = expand(arg, state, env) - add_behaviour(arg, state, env) + State.add_behaviour(arg, state, env) end defp expand_macro( @@ -942,17 +942,17 @@ defmodule ElixirSense.Core.Compiler do when module != nil do state = state - |> add_current_env_to_line(meta, env) + |> State.add_current_env_to_line(meta, env) {arg, state, env} = expand(arg, state, env) state = state - |> add_moduledoc_positions( + |> State.add_moduledoc_positions( env, meta ) - |> register_doc(env, :moduledoc, arg) + |> State.register_doc(env, :moduledoc, arg) {{:@, meta, [{:moduledoc, doc_meta, [arg]}]}, state, env} end @@ -969,13 +969,13 @@ defmodule ElixirSense.Core.Compiler do when doc in [:doc, :typedoc] and module != nil do state = state - |> add_current_env_to_line(meta, env) + |> State.add_current_env_to_line(meta, env) {arg, state, env} = expand(arg, state, env) state = state - |> register_doc(env, doc, arg) + |> State.register_doc(env, doc, arg) {{:@, meta, [{doc, doc_meta, [arg]}]}, state, env} end @@ -992,14 +992,14 @@ defmodule ElixirSense.Core.Compiler do when module != nil do state = state - |> add_current_env_to_line(meta, env) + |> State.add_current_env_to_line(meta, env) {arg, state, env} = expand(arg, state, env) # impl adds sets :hidden by default state = state - |> register_doc(env, :doc, :impl) + |> State.register_doc(env, :doc, :impl) {{:@, meta, [{:impl, doc_meta, [arg]}]}, state, env} end @@ -1016,13 +1016,13 @@ defmodule ElixirSense.Core.Compiler do when module != nil do state = state - |> add_current_env_to_line(meta, env) + |> State.add_current_env_to_line(meta, env) {arg, state, env} = expand(arg, state, env) state = state - |> register_optional_callbacks(arg) + |> State.register_optional_callbacks(arg) {{:@, meta, [{:optional_callbacks, doc_meta, [arg]}]}, state, env} end @@ -1039,13 +1039,13 @@ defmodule ElixirSense.Core.Compiler do when module != nil do state = state - |> add_current_env_to_line(meta, env) + |> State.add_current_env_to_line(meta, env) {arg, state, env} = expand(arg, state, env) state = state - |> register_doc(env, :doc, deprecated: arg) + |> State.register_doc(env, :doc, deprecated: arg) {{:@, meta, [{:deprecated, doc_meta, [arg]}]}, state, env} end @@ -1079,7 +1079,7 @@ defmodule ElixirSense.Core.Compiler do nil -> # implementation for: Any not detected (is in other file etc.) acc - |> add_module_to_index(mod, extract_range(meta), generated: true) + |> State.add_module_to_index(mod, State.extract_range(meta), generated: true) _any_mods_funs -> # copy implementation for: Any @@ -1140,10 +1140,10 @@ defmodule ElixirSense.Core.Compiler do state = state - |> add_type(env, name, type_args, spec, kind, extract_range(attr_meta)) - |> with_typespec({name, length(type_args)}) - |> add_current_env_to_line(attr_meta, env) - |> with_typespec(nil) + |> State.add_type(env, name, type_args, spec, kind, State.extract_range(attr_meta)) + |> State.with_typespec({name, length(type_args)}) + |> State.add_current_env_to_line(attr_meta, env) + |> State.with_typespec(nil) state = if not cursor_before? and cursor_after? do @@ -1174,12 +1174,12 @@ defmodule ElixirSense.Core.Compiler do cursor_after? = state.cursor_env != nil spec = TypeInfo.typespec_to_string(kind, expr) - range = extract_range(attr_meta) + range = State.extract_range(attr_meta) state = if kind in [:callback, :macrocallback] do state - |> add_func_to_index( + |> State.add_func_to_index( env, :behaviour_info, [{:atom, attr_meta, nil}], @@ -1195,10 +1195,10 @@ defmodule ElixirSense.Core.Compiler do state = state - |> add_spec(env, name, type_args, spec, kind, range) - |> with_typespec({name, length(type_args)}) - |> add_current_env_to_line(attr_meta, env) - |> with_typespec(nil) + |> State.add_spec(env, name, type_args, spec, kind, range) + |> State.with_typespec({name, length(type_args)}) + |> State.add_current_env_to_line(attr_meta, env) + |> State.with_typespec(nil) state = if not cursor_before? and cursor_after? do @@ -1252,8 +1252,8 @@ defmodule ElixirSense.Core.Compiler do state = state - |> add_attribute(env, name, meta, e_args, inferred_type, is_definition) - |> add_current_env_to_line(meta, env) + |> State.add_attribute(env, name, meta, e_args, inferred_type, is_definition) + |> State.add_current_env_to_line(meta, env) {{:@, meta, [{name, name_meta, e_args}]}, state, env} end @@ -1272,7 +1272,7 @@ defmodule ElixirSense.Core.Compiler do case arg do keyword when is_list(keyword) -> - {nil, make_overridable(state, env, keyword, meta[:context]), env} + {nil, State.make_overridable(state, env, keyword, meta[:context]), env} behaviour_module when is_atom(behaviour_module) -> if Code.ensure_loaded?(behaviour_module) and @@ -1281,7 +1281,7 @@ defmodule ElixirSense.Core.Compiler do behaviour_module.behaviour_info(:callbacks) |> Enum.map(&Introspection.drop_macro_prefix/1) - {nil, make_overridable(state, env, keyword, meta[:context]), env} + {nil, State.make_overridable(state, env, keyword, meta[:context]), env} else {nil, state, env} end @@ -1333,7 +1333,7 @@ defmodule ElixirSense.Core.Compiler do state = state - |> add_struct_or_exception(env, type, fields, extract_range(meta)) + |> State.add_struct_or_exception(env, type, fields, State.extract_range(meta)) {{type, meta, [fields]}, state, env} end @@ -1348,7 +1348,7 @@ defmodule ElixirSense.Core.Compiler do env = %{module: module} ) when call in [:defrecord, :defrecordp] and module != nil do - range = extract_range(meta) + range = State.extract_range(meta) {[name, _fields] = args, state, env} = expand(args, state, env) type = @@ -1361,7 +1361,7 @@ defmodule ElixirSense.Core.Compiler do state = state - |> add_func_to_index( + |> State.add_func_to_index( env, name, [{:\\, [], [{:args, [], nil}, []]}], @@ -1369,7 +1369,7 @@ defmodule ElixirSense.Core.Compiler do type, options ) - |> add_func_to_index( + |> State.add_func_to_index( env, name, [{:record, [], nil}, {:args, [], nil}], @@ -1377,7 +1377,7 @@ defmodule ElixirSense.Core.Compiler do type, options ) - |> add_current_env_to_line(meta, env) + |> State.add_current_env_to_line(meta, env) {{{:., meta, [Record, call]}, meta, args}, state, env} end @@ -1401,15 +1401,15 @@ defmodule ElixirSense.Core.Compiler do # generate callbacks as macro expansion currently fails state = state - |> add_func_to_index( + |> State.add_func_to_index( %{env | module: module}, :behaviour_info, [:atom], - extract_range(meta), + State.extract_range(meta), :def, generated: true ) - |> generate_protocol_callbacks(%{env | module: module}) + |> State.generate_protocol_callbacks(%{env | module: module}) {ast, state, env} end @@ -1548,7 +1548,7 @@ defmodule ElixirSense.Core.Compiler do %{state | runtime_modules: [full | state.runtime_modules]} end - range = extract_range(meta) + range = State.extract_range(meta) module_functions = case state.protocol do @@ -1558,14 +1558,14 @@ defmodule ElixirSense.Core.Compiler do state = state - |> add_module_to_index(full, range, []) - |> add_module - |> add_current_env_to_line(meta, %{env | module: full}) - |> add_module_functions(%{env | module: full}, module_functions, range) - |> new_vars_scope - |> new_attributes_scope + |> State.add_module_to_index(full, range, []) + |> State.add_module() + |> State.add_current_env_to_line(meta, %{env | module: full}) + |> State.add_module_functions(%{env | module: full}, module_functions, range) + |> State.new_vars_scope() + |> State.new_attributes_scope() - {state, _env} = maybe_add_protocol_behaviour(state, %{env | module: full}) + {state, _env} = State.maybe_add_protocol_behaviour(state, %{env | module: full}) {_result, state, e_env} = expand(block, state, %{env | module: full}) @@ -1583,7 +1583,7 @@ defmodule ElixirSense.Core.Compiler do # module vars are not accessible in module callbacks env = %{env | versioned_vars: %{}, line: meta[:line]} state_orig = state - state = new_func_vars_scope(state) + state = State.new_func_vars_scope(state) # elixir dispatches callbacks by raw dispatch and eval_forms # instead we expand a bock with require and possibly expand macros @@ -1596,17 +1596,17 @@ defmodule ElixirSense.Core.Compiler do ]} {_result, state, env} = expand(ast, state, env) - {remove_func_vars_scope(state, state_orig), env} + {State.remove_func_vars_scope(state, state_orig), env} end) # restore vars from outer scope # restore version counter state = state - |> apply_optional_callbacks(%{env | module: full}) - |> remove_vars_scope(state_orig, true) - |> remove_attributes_scope - |> remove_module + |> State.apply_optional_callbacks(%{env | module: full}) + |> State.remove_vars_scope(state_orig, true) + |> State.remove_attributes_scope() + |> State.remove_module() # in elixir the result of defmodule expansion is # require (a module atom) and :elixir_module.compile dot call in block @@ -1710,14 +1710,14 @@ defmodule ElixirSense.Core.Compiler do state | caller: def_kind in [:defmacro, :defmacrop, :defguard, :defguardp] } - |> new_func_vars_scope() + |> State.new_func_vars_scope() else # make module variables accessible if there are unquote fragments in def body %{ state | caller: def_kind in [:defmacro, :defmacrop, :defguard, :defguardp] } - |> new_vars_scope() + |> State.new_vars_scope() end # no need to reset versioned_vars - we never update it @@ -1760,18 +1760,18 @@ defmodule ElixirSense.Core.Compiler do type_info = Guard.type_information_from_guards(e_guard) - state = merge_inferred_types(state, type_info) + state = State.merge_inferred_types(state, type_info) env_for_expand = %{env_for_expand | context: nil} state = state - |> add_current_env_to_line(meta, env_for_expand) - |> add_func_to_index( + |> State.add_current_env_to_line(meta, env_for_expand) + |> State.add_func_to_index( env, name, args, - extract_range(meta), + State.extract_range(meta), def_kind ) @@ -1808,10 +1808,10 @@ defmodule ElixirSense.Core.Compiler do state = unless has_unquotes do # restore module vars - remove_func_vars_scope(state, state_orig) + State.remove_func_vars_scope(state, state_orig) else # remove scope - remove_vars_scope(state, state_orig) + State.remove_vars_scope(state, state_orig) end # result of def expansion is fa tuple @@ -1990,8 +1990,8 @@ defmodule ElixirSense.Core.Compiler do sl = sl - |> add_call_to_line({receiver, right, length(args)}, meta) - |> add_current_env_to_line(meta, e) + |> State.add_call_to_line({receiver, right, length(args)}, meta) + |> State.add_current_env_to_line(meta, e) {{{:., dot_meta, [receiver, right]}, meta, []}, sl, e} else @@ -2009,18 +2009,18 @@ defmodule ElixirSense.Core.Compiler do ) do {:ok, rewritten} -> s = - close_write(sa, s) - |> add_call_to_line({receiver, right, length(e_args)}, meta) - |> add_current_env_to_line(meta, e) + State.close_write(sa, s) + |> State.add_call_to_line({receiver, right, length(e_args)}, meta) + |> State.add_current_env_to_line(meta, e) {rewritten, s, ea} {:error, _error} -> # elixir raises here elixir_rewrite s = - close_write(sa, s) - |> add_call_to_line({receiver, right, length(e_args)}, meta) - |> add_current_env_to_line(meta, e) + State.close_write(sa, s) + |> State.add_call_to_line({receiver, right, length(e_args)}, meta) + |> State.add_current_env_to_line(meta, e) {{{:., dot_meta, [receiver, right]}, attached_meta, e_args}, s, ea} end @@ -2032,9 +2032,9 @@ defmodule ElixirSense.Core.Compiler do {e_args, {sa, _}, ea} = map_fold(&expand_arg/3, {sl, s}, e, args) s = - close_write(sa, s) - |> add_call_to_line({receiver, right, length(e_args)}, meta) - |> add_current_env_to_line(meta, e) + State.close_write(sa, s) + |> State.add_call_to_line({receiver, right, length(e_args)}, meta) + |> State.add_current_env_to_line(meta, e) {{{:., dot_meta, [receiver, right]}, meta, e_args}, s, ea} end @@ -2076,8 +2076,8 @@ defmodule ElixirSense.Core.Compiler do state = state - |> add_call_to_line({nil, fun, length(args)}, meta) - |> add_current_env_to_line(meta, env) + |> State.add_call_to_line({nil, fun, length(args)}, meta) + |> State.add_current_env_to_line(meta, env) {args, state, env} = expand_args(args, state, env) {{fun, meta, args}, state, env} @@ -2122,7 +2122,7 @@ defmodule ElixirSense.Core.Compiler do defp expand_block([], acc, _meta, s, e), do: {Enum.reverse(acc), s, e} defp expand_block([h], acc, meta, s, e) do - # s = s |> add_current_env_to_line(meta, e) + # s = s |> State.add_current_env_to_line(meta, e) {eh, se, ee} = expand(h, s, e) expand_block([], [eh | acc], meta, se, ee) end @@ -2140,7 +2140,7 @@ defmodule ElixirSense.Core.Compiler do end defp expand_block([h | t], acc, meta, s, e) do - # s = s |> add_current_env_to_line(meta, e) + # s = s |> State.add_current_env_to_line(meta, e) {eh, se, ee} = expand(h, s, e) expand_block(t, [eh | acc], meta, se, ee) end @@ -2203,7 +2203,7 @@ defmodule ElixirSense.Core.Compiler do state.mods_funs_to_positions case state.mods_funs_to_positions[{module, name, arity}] do - %State.ModFunInfo{overridable: {true, _}} = info -> + %ModFunInfo{overridable: {true, _}} = info -> kind = case info.type do :defdelegate -> :def @@ -2247,8 +2247,8 @@ defmodule ElixirSense.Core.Compiler do se = se - |> add_call_to_line({remote, fun, arity}, attached_meta) - |> add_current_env_to_line(attached_meta, ee) + |> State.add_call_to_line({remote, fun, arity}, attached_meta) + |> State.add_current_env_to_line(attached_meta, ee) {{:&, meta, [{:/, [], [{{:., dot_meta, [remote, fun]}, attached_meta, []}, arity]}]}, se, ee} @@ -2258,8 +2258,8 @@ defmodule ElixirSense.Core.Compiler do se = se - |> add_call_to_line({nil, fun, arity}, local_meta) - |> add_current_env_to_line(local_meta, ee) + |> State.add_call_to_line({nil, fun, arity}, local_meta) + |> State.add_current_env_to_line(local_meta, ee) {{:&, meta, [{:/, [], [{fun, local_meta, nil}, arity]}]}, se, ee} @@ -2281,7 +2281,7 @@ defmodule ElixirSense.Core.Compiler do {do_expr, do_opts} end - {e_opts, so, eo} = expand(opts, new_vars_scope(s), e) + {e_opts, so, eo} = expand(opts, State.new_vars_scope(s), e) {e_cases, sc, ec} = map_fold(&expand_for_generator/3, so, eo, cases) # elixir raises here for_generator_start on invalid start generator @@ -2293,7 +2293,8 @@ defmodule ElixirSense.Core.Compiler do {e_expr, se, _ee} = expand_for_do_block(expr, sc, ec, maybe_reduce) - {{:for, meta, e_cases ++ [[{:do, e_expr} | normalized_opts]]}, remove_vars_scope(se, s), e} + {{:for, meta, e_cases ++ [[{:do, e_expr} | normalized_opts]]}, State.remove_vars_scope(se, s), + e} end defp expand_for_do_block([{:->, _, _} | _] = clauses, s, e, false) do @@ -2327,13 +2328,13 @@ defmodule ElixirSense.Core.Compiler do {_ast, sa, _e} = expand(discarded_args, sa, e) clause = {:->, clause_meta, [args, right]} - s_reset = new_vars_scope(sa) + s_reset = State.new_vars_scope(sa) # no point in doing type inference here, we are only certain of the initial value of the accumulator {e_clause, s_acc, _e_acc} = __MODULE__.Clauses.clause(&__MODULE__.Clauses.head/3, clause, s_reset, e) - {e_clause, remove_vars_scope(s_acc, sa)} + {e_clause, State.remove_vars_scope(s_acc, sa)} end {do_expr, sa} = Enum.map_reduce(clauses, s, transformer) @@ -2355,7 +2356,7 @@ defmodule ElixirSense.Core.Compiler do defp expand_for_generator({:<-, meta, [left, right]}, s, e) do {e_right, sr, er} = expand(right, s, e) - sm = reset_read(sr, s) + sm = State.reset_read(sr, s) {[e_left], sl, el} = __MODULE__.Clauses.head([left], sm, er) match_context_r = TypeInference.type_of(e_right, e.context) @@ -2372,7 +2373,7 @@ defmodule ElixirSense.Core.Compiler do case __MODULE__.Utils.split_last(args) do {left_start, {:<-, op_meta, [left_end, right]}} -> {e_right, sr, er} = expand(right, s, e) - sm = reset_read(sr, s) + sm = State.reset_read(sr, s) {e_left, sl, el} = __MODULE__.Clauses.match( @@ -2553,7 +2554,7 @@ defmodule ElixirSense.Core.Compiler do end def expand_arg(arg, {acc, s}, e) do - {e_arg, s_acc, e_acc} = expand(arg, reset_read(acc, s), e) + {e_arg, s_acc, e_acc} = expand(arg, State.reset_read(acc, s), e) {e_arg, {s_acc, s}, e_acc} end @@ -2567,8 +2568,8 @@ defmodule ElixirSense.Core.Compiler do end def expand_args(args, s, e) do - {e_args, {sa, _}, ea} = map_fold(&expand_arg/3, {prepare_write(s), s}, e, args) - {e_args, close_write(sa, s), ea} + {e_args, {sa, _}, ea} = map_fold(&expand_arg/3, {State.prepare_write(s), s}, e, args) + {e_args, State.close_write(sa, s), ea} end if Version.match?(System.version(), ">= 1.15.0-dev") do @@ -2584,8 +2585,8 @@ defmodule ElixirSense.Core.Compiler do for {{^module, fun, arity}, info} when fun != nil <- state.mods_funs_to_positions, {fun, arity} not in @internals, - State.ModFunInfo.get_category(info) == category, - not State.ModFunInfo.private?(info) do + ModFunInfo.get_category(info) == category, + not ModFunInfo.private?(info) do {fun, arity} end else diff --git a/lib/elixir_sense/core/compiler/bitstring.ex b/lib/elixir_sense/core/compiler/bitstring.ex index 9ba106ea..36f50da3 100644 --- a/lib/elixir_sense/core/compiler/bitstring.ex +++ b/lib/elixir_sense/core/compiler/bitstring.ex @@ -1,7 +1,7 @@ defmodule ElixirSense.Core.Compiler.Bitstring do alias ElixirSense.Core.Compiler, as: ElixirExpand alias ElixirSense.Core.Compiler.Utils, as: ElixirUtils - alias ElixirSense.Core.State + alias ElixirSense.Core.Compiler.State defp expand_match(expr, {s, original_s}, e) do {e_expr, se, ee} = ElixirExpand.expand(expr, s, e) diff --git a/lib/elixir_sense/core/compiler/clauses.ex b/lib/elixir_sense/core/compiler/clauses.ex index db1008b7..c78d53f5 100644 --- a/lib/elixir_sense/core/compiler/clauses.ex +++ b/lib/elixir_sense/core/compiler/clauses.ex @@ -1,7 +1,7 @@ defmodule ElixirSense.Core.Compiler.Clauses do alias ElixirSense.Core.Compiler, as: ElixirExpand alias ElixirSense.Core.Compiler.Utils, as: ElixirUtils - alias ElixirSense.Core.State + alias ElixirSense.Core.Compiler.State alias ElixirSense.Core.TypeInference def match(fun, expr, after_s, _before_s, %{context: :match} = e) do diff --git a/lib/elixir_sense/core/compiler/dispatch.ex b/lib/elixir_sense/core/compiler/dispatch.ex index 7f967659..7a92791d 100644 --- a/lib/elixir_sense/core/compiler/dispatch.ex +++ b/lib/elixir_sense/core/compiler/dispatch.ex @@ -1,6 +1,6 @@ defmodule ElixirSense.Core.Compiler.Dispatch do alias ElixirSense.Core.Compiler.Rewrite, as: ElixirRewrite - alias ElixirSense.Core.State + alias ElixirSense.Core.State.ModFunInfo import :ordsets, only: [is_element: 2] def find_import(meta, name, arity, e) do @@ -68,7 +68,7 @@ defmodule ElixirSense.Core.Compiler.Dispatch do if function != nil and function != tuple and Enum.any?(s.mods_funs_to_positions, fn {key, info} -> - key == mfa and State.ModFunInfo.get_category(info) == :macro + key == mfa and ModFunInfo.get_category(info) == :macro end) do false else @@ -167,7 +167,7 @@ defmodule ElixirSense.Core.Compiler.Dispatch do mfa = {receiver, name, arity} Enum.any?(s.mods_funs_to_positions, fn {key, info} -> - key == mfa and State.ModFunInfo.get_category(info) == :macro + key == mfa and ModFunInfo.get_category(info) == :macro end) || try do macros = receiver.__info__(:macros) diff --git a/lib/elixir_sense/core/compiler/fn.ex b/lib/elixir_sense/core/compiler/fn.ex index e420dc3d..8bd65f97 100644 --- a/lib/elixir_sense/core/compiler/fn.ex +++ b/lib/elixir_sense/core/compiler/fn.ex @@ -3,7 +3,7 @@ defmodule ElixirSense.Core.Compiler.Fn do alias ElixirSense.Core.Compiler.Clauses, as: ElixirClauses alias ElixirSense.Core.Compiler.Dispatch, as: ElixirDispatch alias ElixirSense.Core.Compiler.Utils, as: ElixirUtils - alias ElixirSense.Core.State + alias ElixirSense.Core.Compiler.State def expand(meta, clauses, s, e) when is_list(clauses) do transformer = fn diff --git a/lib/elixir_sense/core/compiler/state.ex b/lib/elixir_sense/core/compiler/state.ex index e567b9a9..bc2ace42 100644 --- a/lib/elixir_sense/core/compiler/state.ex +++ b/lib/elixir_sense/core/compiler/state.ex @@ -1,8 +1,4 @@ -defmodule ElixirSense.Core.State do - @moduledoc """ - Core State - """ - +defmodule ElixirSense.Core.Compiler.State do alias ElixirSense.Core.BuiltinFunctions alias ElixirSense.Core.State.Env @@ -46,7 +42,7 @@ defmodule ElixirSense.Core.State do @type protocol_t :: {module, nonempty_list(module)} @type var_type :: nil | {:atom, atom} | {:map, keyword} | {:struct, keyword, module} - @type t :: %ElixirSense.Core.State{ + @type t :: %__MODULE__{ attributes: list(list(ElixirSense.Core.State.AttributeInfo.t())), scope_attributes: list(list(atom)), behaviours: %{optional(module) => [module]}, @@ -120,7 +116,7 @@ defmodule ElixirSense.Core.State do attribute_store: %{}, cursor_position: nil - defp get_current_env(%__MODULE__{} = state, macro_env) do + def get_current_env(%__MODULE__{} = state, macro_env) do current_attributes = state |> get_current_attributes() current_behaviours = state.behaviours |> Map.get(macro_env.module, []) @@ -1110,8 +1106,6 @@ defmodule ElixirSense.Core.State do end end - def default_env, do: %ElixirSense.Core.State.Env{} - defp maybe_move_vars_to_outer_scope( %__MODULE__{vars_info: [current_scope_vars, outer_scope_vars | other_scopes_vars]} = state diff --git a/lib/elixir_sense/core/compiler/typespec.ex b/lib/elixir_sense/core/compiler/typespec.ex index 6e48a66a..5870c59e 100644 --- a/lib/elixir_sense/core/compiler/typespec.ex +++ b/lib/elixir_sense/core/compiler/typespec.ex @@ -1,7 +1,7 @@ defmodule ElixirSense.Core.Compiler.Typespec do alias ElixirSense.Core.Compiler, as: ElixirExpand alias ElixirSense.Core.Compiler.Utils - import ElixirSense.Core.State + alias ElixirSense.Core.Compiler.State def spec_to_signature({:when, _, [spec, _]}), do: type_to_signature(spec) def spec_to_signature(other), do: type_to_signature(other) @@ -36,13 +36,13 @@ defmodule ElixirSense.Core.Compiler.Typespec do state_orig = state unless ElixirExpand.Quote.has_unquotes(ast) do - {ast, state, env} = do_expand_spec(ast, new_func_vars_scope(state), env) + {ast, state, env} = do_expand_spec(ast, State.new_func_vars_scope(state), env) - {ast, remove_func_vars_scope(state, state_orig), env} + {ast, State.remove_func_vars_scope(state, state_orig), env} else - {ast, state, env} = do_expand_spec(ast, new_vars_scope(state), env) + {ast, state, env} = do_expand_spec(ast, State.new_vars_scope(state), env) - {ast, remove_vars_scope(state, state_orig), env} + {ast, State.remove_vars_scope(state, state_orig), env} end end @@ -87,7 +87,7 @@ defmodule ElixirSense.Core.Compiler.Typespec do Enum.reduce(guard, {state, []}, fn {name, _val}, {state, var_names} when is_atom(name) -> # guard is a keyword list so we don't have exact meta on keys - {add_var_write(state, {name, guard_meta, nil}), [name | var_names]} + {State.add_var_write(state, {name, guard_meta, nil}), [name | var_names]} _, acc -> # invalid entry @@ -171,13 +171,13 @@ defmodule ElixirSense.Core.Compiler.Typespec do state_orig = state unless ElixirExpand.Quote.has_unquotes(ast) do - {ast, state, env} = do_expand_type(ast, new_func_vars_scope(state), env) + {ast, state, env} = do_expand_type(ast, State.new_func_vars_scope(state), env) - {ast, remove_func_vars_scope(state, state_orig), env} + {ast, State.remove_func_vars_scope(state, state_orig), env} else - {ast, state, env} = do_expand_type(ast, new_vars_scope(state), env) + {ast, state, env} = do_expand_type(ast, State.new_vars_scope(state), env) - {ast, remove_vars_scope(state, state_orig), env} + {ast, State.remove_vars_scope(state, state_orig), env} end end @@ -196,7 +196,7 @@ defmodule ElixirSense.Core.Compiler.Typespec do Enum.reduce(args, {state, []}, fn {name, meta, context}, {state, var_names} when is_atom(name) and is_atom(context) and name != :_ -> - {add_var_write(state, {name, meta, context}), [name | var_names]} + {State.add_var_write(state, {name, meta, context}), [name | var_names]} _other, acc -> # silently skip invalid typespec params @@ -245,7 +245,7 @@ defmodule ElixirSense.Core.Compiler.Typespec do state = unless state.cursor_env do state - |> add_cursor_env(meta, caller) + |> State.add_cursor_env(meta, caller) else state end @@ -460,8 +460,8 @@ defmodule ElixirSense.Core.Compiler.Typespec do # TODO Module.get_attribute(caller.module, attr) state = state - |> add_attribute(caller, attr, attr_meta, nil, nil, false) - |> add_call_to_line({Kernel, :@, 0}, attr_meta) + |> State.add_attribute(caller, attr, attr_meta, nil, nil, false) + |> State.add_call_to_line({Kernel, :@, 0}, attr_meta) case Map.get(state.attribute_store, {caller.module, attr}) do remote when is_atom(remote) and remote != nil -> @@ -519,7 +519,7 @@ defmodule ElixirSense.Core.Compiler.Typespec do defp typespec({name, meta, atom} = node, vars, caller, state) when is_atom(atom) do if :lists.member(name, vars) do - state = add_var_read(state, node) + state = State.add_var_read(state, node) {{name, meta, atom}, state} else typespec({name, meta, []}, vars, caller, state) @@ -543,7 +543,7 @@ defmodule ElixirSense.Core.Compiler.Typespec do {args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args) # elixir raises if type is not defined - state = add_call_to_line(state, {nil, name, length(args)}, meta) + state = State.add_call_to_line(state, {nil, name, length(args)}, meta) {{name, meta, args}, state} end @@ -619,7 +619,7 @@ defmodule ElixirSense.Core.Compiler.Typespec do defp remote_type({{:., dot_meta, [remote_spec, name_spec]}, meta, args}, vars, caller, state) do {args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args) - state = add_call_to_line(state, {remote_spec, name_spec, length(args)}, meta) + state = State.add_call_to_line(state, {remote_spec, name_spec, length(args)}, meta) {{{:., dot_meta, [remote_spec, name_spec]}, meta, args}, state} end diff --git a/lib/elixir_sense/core/metadata.ex b/lib/elixir_sense/core/metadata.ex index b59719e2..cf87e5d3 100644 --- a/lib/elixir_sense/core/metadata.ex +++ b/lib/elixir_sense/core/metadata.ex @@ -8,6 +8,7 @@ defmodule ElixirSense.Core.Metadata do alias ElixirSense.Core.Normalized.Code, as: NormalizedCode alias ElixirSense.Core.State alias ElixirSense.Core.BuiltinFunctions + alias ElixirSense.Core.MetadataBuilder @type t :: %ElixirSense.Core.Metadata{ source: String.t(), @@ -98,7 +99,7 @@ defmodule ElixirSense.Core.Metadata do {meta, cursor_env} = case Code.string_to_quoted(source_with_cursor, columns: true, token_metadata: true) do {:ok, ast} -> - ElixirSense.Core.MetadataBuilder.build(ast).cursor_env || {[], nil} + MetadataBuilder.build(ast).cursor_env || {[], nil} _ -> {[], nil} @@ -114,7 +115,7 @@ defmodule ElixirSense.Core.Metadata do token_metadata: true ) do {:ok, ast} -> - ElixirSense.Core.MetadataBuilder.build(ast).cursor_env || {[], nil} + MetadataBuilder.build(ast).cursor_env || {[], nil} _ -> {[], nil} @@ -219,7 +220,7 @@ defmodule ElixirSense.Core.Metadata do end, &>=/2, fn -> - {line, State.default_env()} + {line, MetadataBuilder.default_env({line, column})} end ) |> elem(1) diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index 694614f4..101434e2 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -3,9 +3,7 @@ defmodule ElixirSense.Core.MetadataBuilder do This module is responsible for building/retrieving environment information from an AST. """ - import ElixirSense.Core.State - - alias ElixirSense.Core.State + alias ElixirSense.Core.Compiler.State alias ElixirSense.Core.Compiler @doc """ @@ -14,7 +12,18 @@ defmodule ElixirSense.Core.MetadataBuilder do """ @spec build(Macro.t(), nil | {pos_integer, pos_integer}) :: State.t() def build(ast, cursor_position \\ nil) do - state_orig = %State{ + state_initial = initial_state(cursor_position) + + {_ast, state, _env} = Compiler.expand(ast, state_initial, Compiler.env()) + + state + |> State.remove_attributes_scope() + |> State.remove_vars_scope(state_initial) + |> State.remove_module() + end + + def initial_state(cursor_position) do + %State{ cursor_position: cursor_position, prematch: if Version.match?(System.version(), ">= 1.15.0-dev") do @@ -23,13 +32,12 @@ defmodule ElixirSense.Core.MetadataBuilder do :warn end } + end - {_ast, state, _env} = Compiler.expand(ast, state_orig, Compiler.env()) - - state - |> remove_attributes_scope - |> remove_vars_scope(state_orig) - |> remove_module + def default_env(cursor_position \\ nil) do + macro_env = Compiler.env() + state_initial = initial_state(cursor_position) + State.get_current_env(state_initial, macro_env) end # defp post_string_literal(ast, _state, _line, str) do diff --git a/test/elixir_sense/core/compiler/typespec_test.exs b/test/elixir_sense/core/compiler/typespec_test.exs index 5e4ecdca..976ad462 100644 --- a/test/elixir_sense/core/compiler/typespec_test.exs +++ b/test/elixir_sense/core/compiler/typespec_test.exs @@ -2,7 +2,7 @@ defmodule ElixirSense.Core.Compiler.TypespecTest do use ExUnit.Case, async: true alias ElixirSense.Core.Compiler.Typespec alias ElixirSense.Core.Compiler - alias ElixirSense.Core.State + alias ElixirSense.Core.Compiler.State alias ElixirSense.Core.Normalized.Macro.Env, as: NormalizedMacroEnv defp default_state, diff --git a/test/elixir_sense/core/compiler_test.exs b/test/elixir_sense/core/compiler_test.exs index c639e446..c66a4e0b 100644 --- a/test/elixir_sense/core/compiler_test.exs +++ b/test/elixir_sense/core/compiler_test.exs @@ -1,7 +1,7 @@ defmodule ElixirSense.Core.CompilerTest do use ExUnit.Case, async: true alias ElixirSense.Core.Compiler - alias ElixirSense.Core.State + alias ElixirSense.Core.Compiler.State require Record defp to_quoted!(ast, true), do: ast diff --git a/test/elixir_sense/core/parser_test.exs b/test/elixir_sense/core/parser_test.exs index 70cffa13..def33dc0 100644 --- a/test/elixir_sense/core/parser_test.exs +++ b/test/elixir_sense/core/parser_test.exs @@ -1,7 +1,6 @@ defmodule ElixirSense.Core.ParserTest do use ExUnit.Case, async: true - import ExUnit.CaptureIO alias ElixirSense.Core.{Metadata, State.Env, State.VarInfo, State.CallInfo, Parser} defp parse(source, cursor) do From 4348d7fb576f464fc89e63a8e1a3cea7561c9a6c Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 5 Oct 2024 09:28:14 +0200 Subject: [PATCH 230/235] fix warnings --- .../metadata_builder/error_recovery_test.exs | 146 +++++++++--------- 1 file changed, 73 insertions(+), 73 deletions(-) diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index ffd47fb2..5592118f 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -30,7 +30,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, _env} = get_cursor_env(code) end test "no arg 2" do @@ -39,7 +39,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, _env} = get_cursor_env(code) end test "cursor in argument 1" do @@ -47,7 +47,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do case [], \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, _env} = get_cursor_env(code) end test "cursor in argument 2" do @@ -56,7 +56,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do case \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end @@ -66,7 +66,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do [x, \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, _env} = get_cursor_env(code) # this test fails # assert Enum.any?(env.vars, &(&1.name == :x)) end @@ -77,7 +77,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do x when \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) @@ -90,7 +90,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do x when is_atom(\ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end @@ -100,7 +100,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do x -> \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) @@ -115,7 +115,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) @@ -150,7 +150,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, _env} = get_cursor_env(code) end test "cursor in arg" do @@ -159,7 +159,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do cond \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end @@ -170,7 +170,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end @@ -180,7 +180,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do (x = foo(); \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) @@ -193,7 +193,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do x = foo() -> \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) @@ -208,7 +208,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) @@ -233,7 +233,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, _env} = get_cursor_env(code) end test "cursor in arg" do @@ -242,7 +242,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do receive \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end @@ -253,7 +253,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end @@ -264,7 +264,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do {^\ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end @@ -274,7 +274,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do {:msg, x, \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end @@ -284,7 +284,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do {:msg, x} when \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) @@ -297,7 +297,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do {:msg, x} when is_atom(\ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end @@ -307,7 +307,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do {:msg, x} -> \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) @@ -323,7 +323,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end @@ -336,7 +336,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do 0 -> \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end @@ -373,7 +373,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, _env} = get_cursor_env(code) end test "cursor in arg" do @@ -382,7 +382,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do try \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end @@ -393,7 +393,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end @@ -406,7 +406,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end @@ -419,7 +419,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do bar() in [\ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end @@ -432,7 +432,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do e in [\ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end @@ -444,7 +444,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do x in [Error] -> \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) @@ -460,7 +460,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end @@ -472,7 +472,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do x when \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) @@ -488,7 +488,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do x, \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end end @@ -502,7 +502,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do x, _ when \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end end @@ -515,7 +515,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do x -> \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) @@ -531,7 +531,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do x, _ -> \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end end @@ -545,7 +545,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end @@ -557,7 +557,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do x when \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) @@ -572,7 +572,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do x -> \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) @@ -588,7 +588,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end end @@ -600,7 +600,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do with [], \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end @@ -610,7 +610,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do with \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end @@ -619,7 +619,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do with x when \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) @@ -632,7 +632,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do with 1 <- \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) @@ -644,7 +644,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do with x <- foo(), \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end @@ -654,7 +654,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end @@ -667,7 +667,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end @@ -679,7 +679,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do x when \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) @@ -694,7 +694,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do x -> \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) @@ -709,7 +709,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do for [], \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end @@ -719,7 +719,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do for \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end @@ -728,7 +728,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do for x when \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) @@ -741,7 +741,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do for a <- \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) @@ -754,7 +754,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do for <<\ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end @@ -763,7 +763,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do for <= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) @@ -776,7 +776,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do for <= 1.15.0") do @@ -861,7 +861,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do y, \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) # this test fails # assert Enum.any?(env.vars, &(&1.name == :y)) @@ -874,7 +874,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do y -> \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) if Version.match?(System.version(), ">= 1.15.0") do @@ -889,7 +889,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do y, z -> \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) assert Enum.any?(env.vars, &(&1.name == :y)) end @@ -901,7 +901,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do -> \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end @@ -911,7 +911,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do y -> \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) if Version.match?(System.version(), ">= 1.15.0") do @@ -971,7 +971,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do x when \ """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) assert Enum.any?(env.vars, &(&1.name == :x)) end end @@ -983,7 +983,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do end """ - assert {meta, env} = get_cursor_env(code) + assert {_meta, env} = get_cursor_env(code) if Version.match?(System.version(), ">= 1.15.0") do assert Enum.any?(env.vars, &(&1.name == :x)) @@ -1400,7 +1400,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do < Date: Sat, 5 Oct 2024 09:33:21 +0200 Subject: [PATCH 231/235] rename --- lib/elixir_sense/core/compiler/bitstring.ex | 16 ++--- lib/elixir_sense/core/compiler/clauses.ex | 66 ++++++++++----------- lib/elixir_sense/core/compiler/dispatch.ex | 4 +- lib/elixir_sense/core/compiler/fn.ex | 18 +++--- lib/elixir_sense/core/compiler/map.ex | 10 ++-- lib/elixir_sense/core/compiler/quote.ex | 6 +- lib/elixir_sense/core/compiler/typespec.ex | 18 +++--- 7 files changed, 69 insertions(+), 69 deletions(-) diff --git a/lib/elixir_sense/core/compiler/bitstring.ex b/lib/elixir_sense/core/compiler/bitstring.ex index 36f50da3..80a03e19 100644 --- a/lib/elixir_sense/core/compiler/bitstring.ex +++ b/lib/elixir_sense/core/compiler/bitstring.ex @@ -1,10 +1,10 @@ defmodule ElixirSense.Core.Compiler.Bitstring do - alias ElixirSense.Core.Compiler, as: ElixirExpand - alias ElixirSense.Core.Compiler.Utils, as: ElixirUtils + alias ElixirSense.Core.Compiler + alias ElixirSense.Core.Compiler.Utils alias ElixirSense.Core.Compiler.State defp expand_match(expr, {s, original_s}, e) do - {e_expr, se, ee} = ElixirExpand.expand(expr, s, e) + {e_expr, se, ee} = Compiler.expand(expr, s, e) {e_expr, {se, original_s}, ee} end @@ -22,7 +22,7 @@ defmodule ElixirSense.Core.Compiler.Bitstring do pair_s = {State.prepare_write(s), s} {e_args, alignment, {sa, _}, ea} = - expand(meta, &ElixirExpand.expand_arg/3, args, [], pair_s, e, 0, require_size) + expand(meta, &Compiler.expand_arg/3, args, [], pair_s, e, 0, require_size) {{:<<>>, [{:alignment, alignment} | meta], e_args}, State.close_write(sa, s), ea} end @@ -177,7 +177,7 @@ defmodule ElixirSense.Core.Compiler.Bitstring do defp expand_each_spec(meta, [{:__cursor__, _, args} = h | t], map, s, original_s, e) when is_list(args) do - {h, s, e} = ElixirExpand.expand(h, s, e) + {h, s, e} = Compiler.expand(h, s, e) args = case h do @@ -206,7 +206,7 @@ defmodule ElixirSense.Core.Compiler.Bitstring do end # TODO how to check for cursor here? - case ElixirExpand.Macro.expand(ha, Map.put(e, :line, ElixirUtils.get_line(meta))) do + case Compiler.Macro.expand(ha, Map.put(e, :line, Utils.get_line(meta))) do ^ha -> # elixir raises here undefined_bittype # we omit the spec @@ -345,13 +345,13 @@ defmodule ElixirSense.Core.Compiler.Bitstring do new_pre = {pre_read, pre_counter, {:bitsize, original_read}} {e_expr, se, ee} = - ElixirExpand.expand(expr, %{s | prematch: new_pre}, %{e | context: :guard}) + Compiler.expand(expr, %{s | prematch: new_pre}, %{e | context: :guard}) {e_expr, %{se | prematch: old_pre}, %{ee | context: :match}} end defp expand_spec_arg(expr, s, original_s, e) do - ElixirExpand.expand(expr, State.reset_read(s, original_s), e) + Compiler.expand(expr, State.reset_read(s, original_s), e) end defp size_and_unit(type, size, unit) diff --git a/lib/elixir_sense/core/compiler/clauses.ex b/lib/elixir_sense/core/compiler/clauses.ex index c78d53f5..1ef8eee4 100644 --- a/lib/elixir_sense/core/compiler/clauses.ex +++ b/lib/elixir_sense/core/compiler/clauses.ex @@ -1,6 +1,6 @@ defmodule ElixirSense.Core.Compiler.Clauses do - alias ElixirSense.Core.Compiler, as: ElixirExpand - alias ElixirSense.Core.Compiler.Utils, as: ElixirUtils + alias ElixirSense.Core.Compiler + alias ElixirSense.Core.Compiler.Utils alias ElixirSense.Core.Compiler.State alias ElixirSense.Core.TypeInference @@ -50,7 +50,7 @@ defmodule ElixirSense.Core.Compiler.Clauses do def clause(fun, {:->, meta, [left, right]}, s, e) do {e_left, sl, el} = fun.(left, s, e) - {e_right, sr, er} = ElixirExpand.expand(right, sl, el) + {e_right, sr, er} = Compiler.expand(right, sl, el) {{:->, meta, [e_left, e_right]}, sr, er} end @@ -61,13 +61,13 @@ defmodule ElixirSense.Core.Compiler.Clauses do end def head([{:when, meta, [_ | _] = all}], s, e) do - {args, guard} = ElixirUtils.split_last(all) + {args, guard} = Utils.split_last(all) prematch = s.prematch {{e_args, e_guard}, sg, eg} = match( fn _ok, sm, em -> - {e_args, sa, ea} = ElixirExpand.expand_args(args, sm, em) + {e_args, sa, ea} = Compiler.expand_args(args, sm, em) {e_guard, sg, eg} = guard(guard, %{sa | prematch: prematch}, %{ea | context: :guard}) @@ -88,7 +88,7 @@ defmodule ElixirSense.Core.Compiler.Clauses do end def head(args, s, e) do - match(&ElixirExpand.expand_args/3, args, s, s, e) + match(&Compiler.expand_args/3, args, s, s, e) end def guard({:when, meta, [left, right]}, s, e) do @@ -98,7 +98,7 @@ defmodule ElixirSense.Core.Compiler.Clauses do end def guard(guard, s, e) do - {e_guard, sg, eg} = ElixirExpand.expand(guard, s, e) + {e_guard, sg, eg} = Compiler.expand(guard, s, e) {e_guard, sg, eg} end @@ -115,12 +115,12 @@ defmodule ElixirSense.Core.Compiler.Clauses do def case(_e_expr, opts, s, e) when not is_list(opts) do # elixir raises here invalid_args # there may be cursor - ElixirExpand.expand(opts, s, e) + Compiler.expand(opts, s, e) end def case(e_expr, opts, s, e) do # expand invalid opts in case there's cursor - {_ast, s, _e} = ElixirExpand.expand(opts |> Keyword.drop(@valid_case_opts), s, e) + {_ast, s, _e} = Compiler.expand(opts |> Keyword.drop(@valid_case_opts), s, e) opts = sanitize_opts(opts, @valid_case_opts) @@ -169,12 +169,12 @@ defmodule ElixirSense.Core.Compiler.Clauses do def cond(opts, s, e) when not is_list(opts) do # elixir raises here invalid_args # there may be cursor - ElixirExpand.expand(opts, s, e) + Compiler.expand(opts, s, e) end def cond(opts, s, e) do # expand invalid opts in case there's cursor - {_ast, s, _e} = ElixirExpand.expand(opts |> Keyword.drop(@valid_cond_opts), s, e) + {_ast, s, _e} = Compiler.expand(opts |> Keyword.drop(@valid_cond_opts), s, e) opts = sanitize_opts(opts, @valid_cond_opts) @@ -187,7 +187,7 @@ defmodule ElixirSense.Core.Compiler.Clauses do end defp expand_cond({:do, _} = do_clause, s, e) do - expand_clauses(&ElixirExpand.expand_args/3, do_clause, s, e) + expand_clauses(&Compiler.expand_args/3, do_clause, s, e) end # receive @@ -203,12 +203,12 @@ defmodule ElixirSense.Core.Compiler.Clauses do def receive(opts, s, e) when not is_list(opts) do # elixir raises here invalid_args # there may be cursor - ElixirExpand.expand(opts, s, e) + Compiler.expand(opts, s, e) end def receive(opts, s, e) do # expand invalid opts in case there's cursor - {_ast, s, _e} = ElixirExpand.expand(opts |> Keyword.drop(@valid_receive_opts), s, e) + {_ast, s, _e} = Compiler.expand(opts |> Keyword.drop(@valid_receive_opts), s, e) opts = sanitize_opts(opts, @valid_receive_opts) @@ -230,7 +230,7 @@ defmodule ElixirSense.Core.Compiler.Clauses do end defp expand_receive({:after, [_ | _]} = after_clause, s, e) do - expand_clauses(&ElixirExpand.expand_args/3, after_clause, s, e) + expand_clauses(&Compiler.expand_args/3, after_clause, s, e) end defp expand_receive({:after, expr}, s, e) when not is_list(expr) do @@ -243,7 +243,7 @@ defmodule ElixirSense.Core.Compiler.Clauses do [first | discarded] -> # try to recover from error by taking first clause only # expand other in case there's cursor - {_ast, s, _e} = ElixirExpand.expand(discarded, s, e) + {_ast, s, _e} = Compiler.expand(discarded, s, e) expand_receive({:after, [first]}, s, e) [] -> @@ -257,10 +257,10 @@ defmodule ElixirSense.Core.Compiler.Clauses do @valid_with_opts [:do, :else] def with(meta, args, s, e) do - {exprs, opts0} = ElixirUtils.split_opts(args) + {exprs, opts0} = Utils.split_opts(args) # expand invalid opts in case there's cursor - {_ast, s, _e} = ElixirExpand.expand(opts0 |> Keyword.drop(@valid_with_opts), s, e) + {_ast, s, _e} = Compiler.expand(opts0 |> Keyword.drop(@valid_with_opts), s, e) opts0 = sanitize_opts(opts0, @valid_with_opts) s0 = State.new_vars_scope(s) @@ -272,7 +272,7 @@ defmodule ElixirSense.Core.Compiler.Clauses do end defp expand_with({:<-, meta, [left, right]}, {s, e}) do - {e_right, sr, er} = ElixirExpand.expand(right, s, e) + {e_right, sr, er} = Compiler.expand(right, s, e) sm = State.reset_read(sr, s) {[e_left], sl, el} = head([left], sm, er) @@ -285,7 +285,7 @@ defmodule ElixirSense.Core.Compiler.Clauses do end defp expand_with(expr, {s, e}) do - {e_expr, se, ee} = ElixirExpand.expand(expr, s, e) + {e_expr, se, ee} = Compiler.expand(expr, s, e) {e_expr, {se, ee}} end @@ -295,7 +295,7 @@ defmodule ElixirSense.Core.Compiler.Clauses do # we return empty expression expr = expr || [] - {e_expr, s_acc, _e_acc} = ElixirExpand.expand(expr, acc, e) + {e_expr, s_acc, _e_acc} = Compiler.expand(expr, acc, e) {e_expr, rest_opts, State.remove_vars_scope(s_acc, s)} end @@ -327,12 +327,12 @@ defmodule ElixirSense.Core.Compiler.Clauses do def try(opts, s, e) when not is_list(opts) do # elixir raises here invalid_args # there may be cursor - ElixirExpand.expand(opts, s, e) + Compiler.expand(opts, s, e) end def try(opts, s, e) do # expand invalid opts in case there's cursor - {_ast, s, _e} = ElixirExpand.expand(opts |> Keyword.drop(@valid_try_opts), s, e) + {_ast, s, _e} = Compiler.expand(opts |> Keyword.drop(@valid_try_opts), s, e) opts = sanitize_opts(opts, @valid_try_opts) @@ -345,12 +345,12 @@ defmodule ElixirSense.Core.Compiler.Clauses do end defp expand_try({:do, expr}, s, e) do - {e_expr, se, _ee} = ElixirExpand.expand(expr, State.new_vars_scope(s), e) + {e_expr, se, _ee} = Compiler.expand(expr, State.new_vars_scope(s), e) {{:do, e_expr}, State.remove_vars_scope(se, s)} end defp expand_try({:after, expr}, s, e) do - {e_expr, se, _ee} = ElixirExpand.expand(expr, State.new_vars_scope(s), e) + {e_expr, se, _ee} = Compiler.expand(expr, State.new_vars_scope(s), e) {{:after, e_expr}, State.remove_vars_scope(se, s)} end @@ -376,7 +376,7 @@ defmodule ElixirSense.Core.Compiler.Clauses do defp expand_catch(meta, [{:when, when_meta, [a1, a2, a3, dh | dt]}], s, e) do # elixir raises here wrong_number_of_args_for_clause - {_, s, _} = ElixirExpand.expand([dh | dt], s, e) + {_, s, _} = Compiler.expand([dh | dt], s, e) expand_catch(meta, [{:when, when_meta, [a1, a2, a3]}], s, e) end @@ -393,7 +393,7 @@ defmodule ElixirSense.Core.Compiler.Clauses do defp expand_catch(meta, [a1, a2 | d], s, e) do # attempt to recover from error by taking 2 first args # elixir raises here wrong_number_of_args_for_clause - {_, s, _} = ElixirExpand.expand(d, s, e) + {_, s, _} = Compiler.expand(d, s, e) expand_catch(meta, [a1, a2], s, e) end @@ -406,13 +406,13 @@ defmodule ElixirSense.Core.Compiler.Clauses do defp expand_rescue(meta, [a1 | d], s, e) do # try to recover from error by taking first argument only # elixir raises here wrong_number_of_args_for_clause - {_, s, _} = ElixirExpand.expand(d, s, e) + {_, s, _} = Compiler.expand(d, s, e) expand_rescue(meta, [a1], s, e) end # rescue var defp expand_rescue({name, _, atom} = var, s, e) when is_atom(name) and is_atom(atom) do - {e_left, sl, el} = match(&ElixirExpand.expand/3, var, s, s, e) + {e_left, sl, el} = match(&Compiler.expand/3, var, s, s, e) match_context = {:struct, [], {:atom, Exception}, nil} @@ -434,7 +434,7 @@ defmodule ElixirSense.Core.Compiler.Clauses do e ) when is_atom(name) and is_atom(var_context) and is_atom(underscore_context) do - {e_left, sl, el} = match(&ElixirExpand.expand/3, var, s, s, e) + {e_left, sl, el} = match(&Compiler.expand/3, var, s, s, e) match_context = {:struct, [], {:atom, Exception}, nil} @@ -446,8 +446,8 @@ defmodule ElixirSense.Core.Compiler.Clauses do # rescue var in (list() or atom()) defp expand_rescue({:in, meta, [left, right]}, s, e) do - {e_left, sl, el} = match(&ElixirExpand.expand/3, left, s, s, e) - {e_right, sr, er} = ElixirExpand.expand(right, sl, el) + {e_left, sl, el} = match(&Compiler.expand/3, left, s, s, e) + {e_right, sr, er} = Compiler.expand(right, sl, el) case e_left do {name, _, atom} when is_atom(name) and is_atom(atom) -> @@ -480,7 +480,7 @@ defmodule ElixirSense.Core.Compiler.Clauses do # rescue expr() => rescue expanded_expr() defp expand_rescue({_, meta, _} = arg, s, e) do # TODO how to check for cursor here? - case ElixirExpand.Macro.expand_once(arg, %{e | line: ElixirUtils.get_line(meta)}) do + case Compiler.Macro.expand_once(arg, %{e | line: Utils.get_line(meta)}) do ^arg -> # elixir rejects this case # try to recover from error by generating fake expression diff --git a/lib/elixir_sense/core/compiler/dispatch.ex b/lib/elixir_sense/core/compiler/dispatch.ex index 7a92791d..0a32a2b4 100644 --- a/lib/elixir_sense/core/compiler/dispatch.ex +++ b/lib/elixir_sense/core/compiler/dispatch.ex @@ -1,5 +1,5 @@ defmodule ElixirSense.Core.Compiler.Dispatch do - alias ElixirSense.Core.Compiler.Rewrite, as: ElixirRewrite + alias ElixirSense.Core.Compiler.Rewrite alias ElixirSense.Core.State.ModFunInfo import :ordsets, only: [is_element: 2] @@ -89,7 +89,7 @@ defmodule ElixirSense.Core.Compiler.Dispatch do end defp remote_function(_meta, receiver, name, arity, _e) do - case ElixirRewrite.inline(receiver, name, arity) do + case Rewrite.inline(receiver, name, arity) do {ar, an} -> {:remote, ar, an, arity} false -> {:remote, receiver, name, arity} end diff --git a/lib/elixir_sense/core/compiler/fn.ex b/lib/elixir_sense/core/compiler/fn.ex index 8bd65f97..82268789 100644 --- a/lib/elixir_sense/core/compiler/fn.ex +++ b/lib/elixir_sense/core/compiler/fn.ex @@ -1,8 +1,8 @@ defmodule ElixirSense.Core.Compiler.Fn do - alias ElixirSense.Core.Compiler, as: ElixirExpand - alias ElixirSense.Core.Compiler.Clauses, as: ElixirClauses - alias ElixirSense.Core.Compiler.Dispatch, as: ElixirDispatch - alias ElixirSense.Core.Compiler.Utils, as: ElixirUtils + alias ElixirSense.Core.Compiler + alias ElixirSense.Core.Compiler.Clauses + alias ElixirSense.Core.Compiler.Dispatch + alias ElixirSense.Core.Compiler.Utils alias ElixirSense.Core.Compiler.State def expand(meta, clauses, s, e) when is_list(clauses) do @@ -13,7 +13,7 @@ defmodule ElixirSense.Core.Compiler.Fn do # no point in doing type inference here, we have no idea what the fn will be called with {e_clause, s_acc, _e_acc} = - ElixirClauses.clause(&ElixirClauses.head/3, clause, s_reset, e) + Clauses.clause(&Clauses.head/3, clause, s_reset, e) {e_clause, State.remove_vars_scope(s_acc, sa)} end @@ -60,7 +60,7 @@ defmodule ElixirSense.Core.Compiler.Fn do {:"&1", meta, e.module} list -> - ElixirUtils.select_with_cursor(list) || hd(list) + Utils.select_with_cursor(list) || hd(list) end capture(meta, expr, s, e) @@ -100,7 +100,7 @@ defmodule ElixirSense.Core.Compiler.Fn do defp capture_import({atom, import_meta, args} = expr, s, e, sequential) do res = if sequential do - ElixirDispatch.import_function(import_meta, atom, length(args), s, e) + Dispatch.import_function(import_meta, atom, length(args), s, e) else false end @@ -111,7 +111,7 @@ defmodule ElixirSense.Core.Compiler.Fn do defp capture_require({{:., dot_meta, [left, right]}, require_meta, args}, s, e, sequential) do case escape(left, []) do {esc_left, []} -> - {e_left, se, ee} = ElixirExpand.expand(esc_left, s, e) + {e_left, se, ee} = Compiler.expand(esc_left, s, e) res = if sequential do @@ -120,7 +120,7 @@ defmodule ElixirSense.Core.Compiler.Fn do {:remote, e_left, right, length(args)} _ when is_atom(e_left) -> - ElixirDispatch.require_function( + Dispatch.require_function( require_meta, e_left, right, diff --git a/lib/elixir_sense/core/compiler/map.ex b/lib/elixir_sense/core/compiler/map.ex index cb6666b7..eb717ef5 100644 --- a/lib/elixir_sense/core/compiler/map.ex +++ b/lib/elixir_sense/core/compiler/map.ex @@ -1,11 +1,11 @@ defmodule ElixirSense.Core.Compiler.Map do - alias ElixirSense.Core.Compiler, as: ElixirExpand + alias ElixirSense.Core.Compiler def expand_struct(meta, left, {:%{}, map_meta, map_args}, s, %{context: context} = e) do clean_map_args = clean_struct_key_from_map_args(map_args) {[e_left, e_right], se, ee} = - ElixirExpand.expand_args([left, {:%{}, map_meta, clean_map_args}], s, e) + Compiler.expand_args([left, {:%{}, map_meta, clean_map_args}], s, e) case validate_struct(e_left, context) do true when is_atom(e_left) -> @@ -18,7 +18,7 @@ defmodule ElixirSense.Core.Compiler.Map do without_keys = Elixir.Map.drop(struct, keys) struct_assocs = - ElixirExpand.Macro.escape(Enum.sort(Elixir.Map.to_list(without_keys))) + Compiler.Macro.escape(Enum.sort(Elixir.Map.to_list(without_keys))) {{:%, meta, [e_left, {:%{}, map_meta, struct_assocs ++ assocs}]}, se, ee} @@ -59,13 +59,13 @@ defmodule ElixirSense.Core.Compiler.Map do def expand_map(meta, [{:|, update_meta, [left, right]}], s, e) do # elixir raises update_syntax_in_wrong_context if e.context is not nil - {[e_left | e_right], se, ee} = ElixirExpand.expand_args([left | right], s, e) + {[e_left | e_right], se, ee} = Compiler.expand_args([left | right], s, e) e_right = sanitize_kv(e_right, e) {{:%{}, meta, [{:|, update_meta, [e_left, e_right]}]}, se, ee} end def expand_map(meta, args, s, e) do - {e_args, se, ee} = ElixirExpand.expand_args(args, s, e) + {e_args, se, ee} = Compiler.expand_args(args, s, e) e_args = sanitize_kv(e_args, e) {{:%{}, meta, e_args}, se, ee} end diff --git a/lib/elixir_sense/core/compiler/quote.ex b/lib/elixir_sense/core/compiler/quote.ex index 869f5fa4..ca2dda74 100644 --- a/lib/elixir_sense/core/compiler/quote.ex +++ b/lib/elixir_sense/core/compiler/quote.ex @@ -1,5 +1,5 @@ defmodule ElixirSense.Core.Compiler.Quote do - alias ElixirSense.Core.Compiler.Dispatch, as: ElixirDispatch + alias ElixirSense.Core.Compiler.Dispatch alias ElixirSense.Core.Normalized.Macro.Env, as: NormalizedMacroEnv defstruct line: false, @@ -244,7 +244,7 @@ defmodule ElixirSense.Core.Compiler.Quote do ) when is_atom(f) and is_integer(a) and is_atom(c) and is_list(meta) do new_meta = - case ElixirDispatch.find_import(meta, f, a, e) do + case Dispatch.find_import(meta, f, a, e) do false -> meta @@ -309,7 +309,7 @@ defmodule ElixirSense.Core.Compiler.Quote do defp import_meta(meta, name, arity, q, e) do case Keyword.get(meta, :imports, false) == false && - ElixirDispatch.find_imports(meta, name, e) do + Dispatch.find_imports(meta, name, e) do [_ | _] = imports -> keystore(:imports, keystore(:context, meta, q.context), imports) diff --git a/lib/elixir_sense/core/compiler/typespec.ex b/lib/elixir_sense/core/compiler/typespec.ex index 5870c59e..bf7de478 100644 --- a/lib/elixir_sense/core/compiler/typespec.ex +++ b/lib/elixir_sense/core/compiler/typespec.ex @@ -1,5 +1,5 @@ defmodule ElixirSense.Core.Compiler.Typespec do - alias ElixirSense.Core.Compiler, as: ElixirExpand + alias ElixirSense.Core.Compiler alias ElixirSense.Core.Compiler.Utils alias ElixirSense.Core.Compiler.State def spec_to_signature({:when, _, [spec, _]}), do: type_to_signature(spec) @@ -35,7 +35,7 @@ defmodule ElixirSense.Core.Compiler.Typespec do # unless there are unquotes module vars are not accessible state_orig = state - unless ElixirExpand.Quote.has_unquotes(ast) do + unless Compiler.Quote.has_unquotes(ast) do {ast, state, env} = do_expand_spec(ast, State.new_func_vars_scope(state), env) {ast, State.remove_func_vars_scope(state, state_orig), env} @@ -130,7 +130,7 @@ defmodule ElixirSense.Core.Compiler.Typespec do case other do {:"::", meta, [{{:unquote, _, unquote_args}, meta1, call_args}, definition]} -> # replace unquote fragment and try to expand args to find variables - {_, state, env} = ElixirExpand.expand(unquote_args, state, env) + {_, state, env} = Compiler.expand(unquote_args, state, env) do_expand_spec( {:"::", meta, [{:__unknown__, meta1, call_args}, definition]}, @@ -170,7 +170,7 @@ defmodule ElixirSense.Core.Compiler.Typespec do # unless there are unquotes module vars are not accessible state_orig = state - unless ElixirExpand.Quote.has_unquotes(ast) do + unless Compiler.Quote.has_unquotes(ast) do {ast, state, env} = do_expand_type(ast, State.new_func_vars_scope(state), env) {ast, State.remove_func_vars_scope(state, state_orig), env} @@ -211,7 +211,7 @@ defmodule ElixirSense.Core.Compiler.Typespec do case other do {:"::", meta, [{{:unquote, _, unquote_args}, meta1, call_args}, definition]} -> # replace unquote fragment and try to expand args to find variables - {_, state, env} = ElixirExpand.expand(unquote_args, state, env) + {_, state, env} = Compiler.expand(unquote_args, state, env) do_expand_type({:"::", meta, [{:__unknown__, meta1, call_args}, definition]}, state, env) {name, meta, args} when is_atom(name) and name != :"::" -> @@ -303,11 +303,11 @@ defmodule ElixirSense.Core.Compiler.Typespec do end defp typespec({:%, struct_meta, [name, {:%{}, meta, fields}]}, vars, caller, state) do - case ElixirExpand.Macro.expand(name, %{caller | function: {:__info__, 1}}) do + case Compiler.Macro.expand(name, %{caller | function: {:__info__, 1}}) do module when is_atom(module) -> # TODO register alias/struct struct = - ElixirExpand.Map.load_struct(module, [], state, caller) + Compiler.Map.load_struct(module, [], state, caller) |> Map.delete(:__struct__) |> Map.to_list() @@ -363,7 +363,7 @@ defmodule ElixirSense.Core.Compiler.Typespec do when is_atom(tag) and is_list(field_specs) do # We cannot set a function name to avoid tracking # as a compile time dependency because for records it actually is one. - case ElixirExpand.Macro.expand({tag, [], [{:{}, [], []}]}, caller) do + case Compiler.Macro.expand({tag, [], [{:{}, [], []}]}, caller) do {_, _, [name, fields | _]} when is_list(fields) -> types = :lists.map( @@ -529,7 +529,7 @@ defmodule ElixirSense.Core.Compiler.Typespec do # handle unquote fragment defp typespec({key, _, args}, _vars, caller, state) when is_list(args) and key in [:unquote, :unquote_splicing] do - {_, state, _env} = ElixirExpand.expand(args, state, caller) + {_, state, _env} = Compiler.expand(args, state, caller) {:__unknown__, state} end From 4ef2bc0403b8d31f80c06cd6164b104d480f6c86 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 5 Oct 2024 09:47:38 +0200 Subject: [PATCH 232/235] fix invalid API usage --- lib/elixir_sense/core/compiler/bitstring.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir_sense/core/compiler/bitstring.ex b/lib/elixir_sense/core/compiler/bitstring.ex index 80a03e19..e63ac6f0 100644 --- a/lib/elixir_sense/core/compiler/bitstring.ex +++ b/lib/elixir_sense/core/compiler/bitstring.ex @@ -275,7 +275,7 @@ defmodule ElixirSense.Core.Compiler.Bitstring do case e_right do {:binary, _, nil} -> - {alignment, alignment} = Keyword.fetch!(parts_meta, :alignment) + alignment = Keyword.fetch!(parts_meta, :alignment) if is_integer(alignment) do # elixir raises unaligned_binary if alignment != 0 From 5f2a694b38865cbfd8d5539cac9c35fddd2f0696 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 5 Oct 2024 11:17:53 +0200 Subject: [PATCH 233/235] exclude test --- test/elixir_sense/core/parser_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/elixir_sense/core/parser_test.exs b/test/elixir_sense/core/parser_test.exs index def33dc0..7077732f 100644 --- a/test/elixir_sense/core/parser_test.exs +++ b/test/elixir_sense/core/parser_test.exs @@ -206,7 +206,7 @@ defmodule ElixirSense.Core.ParserTest do {_metadata, env} = parse(source, {3, 23}) - if Version.match?(System.version(), ">= 1.15.0") do + if Version.match?(System.version(), ">= 1.17.0") do assert %Env{ vars: [ %VarInfo{name: :x} From ef46958947d69b15bd008d54507276dfa77fc33b Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 5 Oct 2024 11:23:35 +0200 Subject: [PATCH 234/235] exclude tests --- .../core/metadata_builder/error_recovery_test.exs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs index 5592118f..7081b110 100644 --- a/test/elixir_sense/core/metadata_builder/error_recovery_test.exs +++ b/test/elixir_sense/core/metadata_builder/error_recovery_test.exs @@ -122,7 +122,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do end end - if Version.match?(System.version(), ">= 1.14.0") do + if Version.match?(System.version(), ">= 1.15.0") do test "invalid number of args with when" do code = """ case nil do 0, z when not is_nil(z) -> \ @@ -132,7 +132,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do end end - if Version.match?(System.version(), ">= 1.14.0") do + if Version.match?(System.version(), ">= 1.15.0") do test "invalid number of args" do code = """ case nil do 0, z -> \ @@ -215,7 +215,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do end end - if Version.match?(System.version(), ">= 1.14.0") do + if Version.match?(System.version(), ">= 1.15.0") do test "invalid number of args" do code = """ cond do 0, z -> \ @@ -522,7 +522,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do end end - if Version.match?(System.version(), ">= 1.14.0") do + if Version.match?(System.version(), ">= 1.15.0") do test "cursor in right side of catch clause 2 arg" do code = """ try do @@ -882,7 +882,7 @@ defmodule ElixirSense.Core.MetadataBuilder.ErrorRecoveryTest do end end - if Version.match?(System.version(), ">= 1.14.0") do + if Version.match?(System.version(), ">= 1.15.0") do test "cursor in do block reduce right side of clause too many args" do code = """ for x <- [], reduce: %{} do From f2b6172e9f7d8da2a1039888e355cc7b7757c2b5 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 5 Oct 2024 11:40:47 +0200 Subject: [PATCH 235/235] hack for < 1.15 --- lib/elixir_sense/core/metadata.ex | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/elixir_sense/core/metadata.ex b/lib/elixir_sense/core/metadata.ex index cf87e5d3..be55e721 100644 --- a/lib/elixir_sense/core/metadata.ex +++ b/lib/elixir_sense/core/metadata.ex @@ -67,10 +67,24 @@ defmodule ElixirSense.Core.Metadata do } end + def get_cursor_env( + metadata, + position, + surround \\ nil + ) + + if Version.match?(System.version(), "< 1.15.0") do + # return early if cursor env already found by parser replacing line + # this helps on < 1.15 and braks tests on later versions + def get_cursor_env(%__MODULE__{cursor_env: {_, env}}, _position, _surround) do + env + end + end + def get_cursor_env( %__MODULE__{} = metadata, {line, column}, - surround \\ nil + surround ) do {prefix, source_with_cursor} = case surround do