diff --git a/lib/elixir_sense.ex b/lib/elixir_sense.ex index e224ace2..03751881 100644 --- a/lib/elixir_sense.ex +++ b/lib/elixir_sense.ex @@ -13,6 +13,7 @@ defmodule ElixirSense do alias ElixirSense.Core.Parser alias ElixirSense.Core.Source alias ElixirSense.Core.State + alias ElixirSense.Core.State.VarInfo alias ElixirSense.Location alias ElixirSense.Providers.Definition alias ElixirSense.Providers.Docs @@ -93,11 +94,11 @@ defmodule ElixirSense do """ @spec definition(String.t(), pos_integer, pos_integer) :: Location.t() | nil def definition(code, line, column) do - case Source.subject(code, line, column) do + case Source.subject_with_position(code, line, column) do nil -> nil - subject -> + {subject, {line, col}} -> buffer_file_metadata = Parser.parse_string(code, true, true, line) env = Metadata.get_env(buffer_file_metadata, line) @@ -112,6 +113,7 @@ defmodule ElixirSense do Definition.find( subject, line, + col, env, buffer_file_metadata.mods_funs_to_positions, calls, @@ -411,23 +413,34 @@ defmodule ElixirSense do env = %State.Env{ - scope_id: scope_id, - module: module + module: module, + vars: vars } = Metadata.get_env(buffer_file_metadata, line) # find last env of current module attributes = get_attributes(buffer_file_metadata.lines_to_env, module) - # in (h|l)?eex templates vars_info_per_scope_id[scope_id] is nil + # one line can contain variables from many scopes vars = - if buffer_file_metadata.vars_info_per_scope_id[scope_id], - do: buffer_file_metadata.vars_info_per_scope_id[scope_id] |> Map.values(), - else: %{} + case Enum.find(vars, fn %VarInfo{positions: positions} -> {line, col} in positions end) do + %VarInfo{scope_id: scope_id} -> + # in (h|l)?eex templates vars_info_per_scope_id[scope_id] is nil + if buffer_file_metadata.vars_info_per_scope_id[scope_id] do + buffer_file_metadata.vars_info_per_scope_id[scope_id] + else + [] + end + + nil -> + [] + end arity = Metadata.get_call_arity(buffer_file_metadata, line, col) References.find( subject, + line, + col, arity, env, vars, diff --git a/lib/elixir_sense/core/metadata_builder.ex b/lib/elixir_sense/core/metadata_builder.ex index 6dd24b09..808f9e01 100644 --- a/lib/elixir_sense/core/metadata_builder.ex +++ b/lib/elixir_sense/core/metadata_builder.ex @@ -207,6 +207,11 @@ defmodule ElixirSense.Core.MetadataBuilder do defp pre_func({type, _, _} = ast, state, %{line: line, col: col}, name, params, options \\ []) when is_atom(name) do + vars = + state + |> find_vars(params) + |> merge_same_name_vars() + state |> new_named_func(name, length(params || [])) |> add_func_to_index(name, params || [], {line, col}, type, options) @@ -214,7 +219,7 @@ defmodule ElixirSense.Core.MetadataBuilder do |> new_import_scope |> new_require_scope |> new_func_vars_scope - |> add_vars(find_vars(state, params), true) + |> add_vars(vars, true) |> add_current_env_to_line(line) |> result(ast) end @@ -256,6 +261,7 @@ defmodule ElixirSense.Core.MetadataBuilder do end state + |> maybe_move_vars_to_outer_scope |> remove_vars_scope |> result(ast) end @@ -292,17 +298,23 @@ defmodule ElixirSense.Core.MetadataBuilder do |> remove_alias_scope |> remove_import_scope |> remove_require_scope + |> maybe_move_vars_to_outer_scope |> remove_vars_scope |> result(ast) end defp pre_clause({_clause, [line: line, column: _column], _} = ast, state, lhs) do + vars = + state + |> find_vars(lhs, Enum.at(state.binding_context, 0)) + |> merge_same_name_vars() + state |> new_alias_scope |> new_import_scope |> new_require_scope |> new_vars_scope - |> add_vars(find_vars(state, lhs, Enum.at(state.binding_context, 0)), true) + |> add_vars(vars, true) |> add_current_env_to_line(line) |> result(ast) end @@ -312,6 +324,7 @@ defmodule ElixirSense.Core.MetadataBuilder do |> remove_alias_scope |> remove_import_scope |> remove_require_scope + |> maybe_move_vars_to_outer_scope |> remove_vars_scope |> result(ast) end @@ -474,8 +487,15 @@ defmodule ElixirSense.Core.MetadataBuilder do pre_func(ast_without_params, state, %{line: line, col: column}, name, params, options) end - defp pre({def_name, meta, [{:when, _, [head | _]}, body]}, state) when def_name in @defs do - pre({def_name, meta, [head, body]}, state) + # function head with guards + defp pre( + {def_name, meta, + [{:when, _, [{name, [line: line, column: column] = meta2, params}, guards]}, body]}, + state + ) + when def_name in @defs do + ast_without_params = {def_name, meta, [{name, add_no_call(meta2), []}, guards, body]} + pre_func(ast_without_params, state, %{line: line, col: column}, name, params) end defp pre( @@ -795,6 +815,10 @@ defmodule ElixirSense.Core.MetadataBuilder 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 @@ -803,55 +827,37 @@ defmodule ElixirSense.Core.MetadataBuilder do pre_clause({:->, meta, [:_, rhs]}, state, lhs) end - defp pre({atom, [line: line, column: _column] = meta, [lhs, rhs]}, state) + defp pre({atom, [line: _line, column: _column] = meta, [lhs, rhs]}, state) when atom in [:=, :<-] do - match_context_r = get_binding_type(state, 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_vars(state, 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) - - vars_l ++ nested_vars - - _ -> - vars_l - end - - state - |> add_vars(vars, true) - |> add_current_env_to_line(line) - |> result({:=, meta, [:_, rhs]}) + result(state, {atom, meta, [lhs, rhs]}) end defp pre({var_or_call, [line: line, column: column], nil} = ast, state) when is_atom(var_or_call) and var_or_call != :__MODULE__ do if Enum.any?(get_current_vars(state), &(&1.name == var_or_call)) do - state - |> add_vars(find_vars(state, ast), false) + vars = + state + |> find_vars(ast) + |> merge_same_name_vars() + + add_vars(state, vars, false) else # pre Elixir 1.4 local call syntax # TODO remove on Elixir 2.0 - state - |> add_call_to_line({nil, var_or_call, 0}, {line, column}) - |> add_current_env_to_line(line) + add_call_to_line(state, {nil, var_or_call, 0}, {line, column}) end + |> add_current_env_to_line(line) |> result(ast) end defp pre({:when, meta, [lhs, rhs]}, state) do + vars = + state + |> find_vars(lhs) + |> merge_same_name_vars() + state - |> add_vars(find_vars(state, lhs), true) + |> add_vars(vars, true) |> result({:when, meta, [:_, rhs]}) end @@ -1223,6 +1229,14 @@ defmodule ElixirSense.Core.MetadataBuilder do post_func(ast, state) end + defp post( + {def_name, [line: _line, column: _column], [{name, _, _params}, _guards, _]} = ast, + state + ) + when def_name in @defs and is_atom(name) do + post_func(ast, state) + end + defp post({def_name, _, _} = ast, state) when def_name in @defs do {ast, state} end @@ -1254,6 +1268,44 @@ defmodule ElixirSense.Core.MetadataBuilder do post_clause(ast, state) end + defp post({atom, [line: line, column: _column], [lhs, rhs]} = ast, state) + when atom in [:=, :<-] do + match_context_r = get_binding_type(state, 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_vars(state, 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) + + vars_l ++ nested_vars + + _ -> + 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) + |> result(ast) + end + # String literal defp post({_, [no_call: true, line: line, column: _column], [str]} = ast, state) when is_binary(str) do diff --git a/lib/elixir_sense/core/source.ex b/lib/elixir_sense/core/source.ex index 59d66418..dbea75a9 100644 --- a/lib/elixir_sense/core/source.ex +++ b/lib/elixir_sense/core/source.ex @@ -395,7 +395,7 @@ defmodule ElixirSense.Core.Source do end defp find_subject(grapheme, rest, line, col, %{pos_found: false, line: line, col: col} = acc) do - find_subject(grapheme, rest, line, col, %{acc | pos_found: true}) + find_subject(grapheme, rest, line, col, %{acc | pos: {line, col - 1}, pos_found: true}) end defp find_subject("." = grapheme, rest, _line, _col, %{pos_found: false} = acc) do diff --git a/lib/elixir_sense/core/state.ex b/lib/elixir_sense/core/state.ex index b16b1d8d..49fb7ee4 100644 --- a/lib/elixir_sense/core/state.ex +++ b/lib/elixir_sense/core/state.ex @@ -303,10 +303,34 @@ defmodule ElixirSense.Core.State do end def add_current_env_to_line(%__MODULE__{} = state, line) when is_integer(line) do - env = get_current_env(state) + 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 + 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, [line: line, column: column], @@ -387,6 +411,26 @@ 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, type, fields) do structs = get_current_module_variants(state) @@ -412,7 +456,10 @@ defmodule ElixirSense.Core.State do end def get_current_vars(%__MODULE__{} = state) do - state.scope_vars |> List.flatten() |> reduce_vars() |> Map.values() + state.scope_vars + |> List.flatten() + |> reduce_vars() + |> Enum.flat_map(fn {_var, scopes} -> scopes end) end def get_current_vars_refs(%__MODULE__{} = state) do @@ -691,7 +738,15 @@ defmodule ElixirSense.Core.State do end def new_func_vars_scope(%__MODULE__{} = state) do - %__MODULE__{state | vars: [[] | state.vars], scope_vars: [[]]} + scope_id = state.scope_id_count + 1 + + %__MODULE__{ + state + | scope_ids: [scope_id | state.scope_ids], + scope_id_count: scope_id, + vars: [[] | state.vars], + scope_vars: [[]] + } end def new_attributes_scope(%__MODULE__{} = state) do @@ -703,24 +758,89 @@ defmodule ElixirSense.Core.State do end def remove_vars_scope(%__MODULE__{} = state) do - [current_scope_vars | other_scope_vars] = state.scope_vars - [scope_id | other_scope_ids] = state.scope_ids - - vars_info_per_scope_id = - state.vars_info_per_scope_id |> Map.put(scope_id, reduce_vars(current_scope_vars)) - %__MODULE__{ state - | scope_ids: other_scope_ids, + | scope_ids: tl(state.scope_ids), vars: tl(state.vars), - scope_vars: other_scope_vars, - vars_info_per_scope_id: vars_info_per_scope_id + scope_vars: tl(state.scope_vars), + vars_info_per_scope_id: update_vars_info_per_scope_id(state) } end def remove_func_vars_scope(%__MODULE__{} = state) do - vars = tl(state.vars) - %__MODULE__{state | vars: vars, scope_vars: vars} + %__MODULE__{ + state + | scope_ids: tl(state.scope_ids), + vars: tl(state.vars), + scope_vars: tl(state.vars), + vars_info_per_scope_id: update_vars_info_per_scope_id(state) + } + end + + defp update_vars_info_per_scope_id(state) do + [scope_id | _other_scope_ids] = state.scope_ids + + [current_scope_vars | other_scope_vars] = state.scope_vars + + 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) + + 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) end def remove_attributes_scope(%__MODULE__{} = state) do @@ -1029,44 +1149,81 @@ defmodule ElixirSense.Core.State do vars |> Enum.reduce(state, fn var, state -> add_var(state, var, is_definition) end) end - defp reduce_vars(vars) do - Enum.reduce(vars, %{}, fn %VarInfo{name: var, positions: [position]} = el, acc -> + # 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 + + 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: [current_scope_vars | other_vars], + scope_vars: [current_scope_vars | tl(state.scope_vars)] + } + + {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 - var_info = %VarInfo{is_definition: false} -> + %VarInfo{} = var_info -> type = - if position in var_info.positions do + 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 ++ [position]) |> Enum.uniq() |> Enum.sort(), - type: type - } - - var_info = %VarInfo{is_definition: true} -> - type = - if position in var_info.positions do - merge_type(el.type, var_info.type) - else - var_info.type - end - %VarInfo{ var_info - | positions: (var_info.positions ++ [position]) |> Enum.uniq() |> Enum.sort(), + | 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 @@ -1106,4 +1263,56 @@ defmodule ElixirSense.Core.State do current_aliases = current_aliases(state) Introspection.expand_alias(Module.concat(module), current_aliases) end + + def maybe_move_vars_to_outer_scope(%__MODULE__{} = state) do + scope_vars = move_references_to_outer_scope(state.scope_vars) + vars = move_references_to_outer_scope(state.vars) + + %__MODULE__{state | vars: vars, scope_vars: 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) + + [current_scope_vars, vars_to_move ++ outer_scope_vars | other_scopes_vars] + end end diff --git a/lib/elixir_sense/providers/definition.ex b/lib/elixir_sense/providers/definition.ex index 3c171536..0e3cc5cc 100644 --- a/lib/elixir_sense/providers/definition.ex +++ b/lib/elixir_sense/providers/definition.ex @@ -21,6 +21,7 @@ defmodule ElixirSense.Providers.Definition do @spec find( String.t(), pos_integer, + pos_integer, State.Env.t(), State.mods_funs_to_positions_t(), list(State.CallInfo.t()), @@ -29,6 +30,7 @@ defmodule ElixirSense.Providers.Definition do def find( subject, line, + column, %State.Env{ aliases: aliases, module: module, @@ -45,17 +47,27 @@ defmodule ElixirSense.Providers.Definition do current_module: module } - var_info = - unless subject_is_call?(subject, calls) do - vars |> Enum.find(fn %VarInfo{name: name} -> to_string(name) == subject end) + vars_info = + if subject_is_call?(subject, calls) do + [] + else + Enum.filter(vars, fn %VarInfo{name: name} -> to_string(name) == subject end) end attribute_info = find_attribute(subject, attributes) cond do - var_info != nil -> - %VarInfo{positions: [{line, column} | _]} = var_info - %Location{type: :variable, file: nil, line: line, column: column} + vars_info != [] -> + {definition_line, definition_column} = + vars_info + |> Enum.find(vars_info, fn %VarInfo{positions: positions} -> + {line, column} in positions + end) + |> then(fn %VarInfo{positions: positions} -> positions end) + |> Enum.sort() + |> List.first() + + %Location{type: :variable, file: nil, line: definition_line, column: definition_column} attribute_info != nil -> %State.AttributeInfo{positions: [{line, column} | _]} = attribute_info diff --git a/lib/elixir_sense/providers/references.ex b/lib/elixir_sense/providers/references.ex index b6091ae3..d58c8874 100644 --- a/lib/elixir_sense/providers/references.ex +++ b/lib/elixir_sense/providers/references.ex @@ -27,6 +27,8 @@ defmodule ElixirSense.Providers.References do @spec find( String.t(), non_neg_integer, + non_neg_integer, + non_neg_integer, State.Env.t(), [VarInfo.t()], [AttributeInfo.t()], @@ -35,6 +37,8 @@ defmodule ElixirSense.Providers.References do ) :: [ElixirSense.Providers.References.reference_info()] def find( subject, + line, + column, arity, %State.Env{ imports: imports, @@ -51,7 +55,10 @@ defmodule ElixirSense.Providers.References do }, trace ) do - var_info = vars |> Enum.find(fn %VarInfo{name: name} -> to_string(name) == subject end) + var_info = + Enum.find(vars, fn %VarInfo{name: name, positions: positions} -> + to_string(name) == subject and {line, column} in positions + end) attribute_info = case subject do diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 1b768498..8dc731bb 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -472,8 +472,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert [ - %VarInfo{name: :var_in1, positions: [{4, 5}], scope_id: 3}, - %VarInfo{name: :var_in2, positions: [{5, 5}], 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) end @@ -504,16 +504,40 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state 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(8) - assert [%VarInfo{type: {:atom, List}}] = state |> get_line_vars(10) - assert [%VarInfo{type: {:atom, Enum}}] = state |> get_line_vars(12) - assert [%VarInfo{type: {:atom, Map}}] = state |> get_line_vars(14) - assert [%VarInfo{type: {:atom, Atom}}] = state |> get_line_vars(16) + + assert [%VarInfo{type: {:atom, String}}, %VarInfo{type: {:atom, Map}}] = + state |> get_line_vars(6) + + assert [%VarInfo{type: {:atom, String}}, %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} + ] = 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} + ] = state |> get_line_vars(12) + + assert [%VarInfo{type: {:atom, String}}, %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{name: :var, type: {:atom, Atom}} + %VarInfo{type: {:atom, String}}, + %VarInfo{type: {:atom, Map}}, + %VarInfo{type: {:atom, Atom}} ] = state |> get_line_vars(18) end @@ -579,7 +603,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] = state |> get_line_vars(16) assert [ - %VarInfo{name: :var1, type: {:call, {:variable, :var1}, :abc, []}}, + %VarInfo{name: :var1, type: nil, scope_id: 7}, + %VarInfo{name: :var1, type: {:call, {:variable, :var1}, :abc, []}, scope_id: 8}, %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) @@ -610,19 +635,31 @@ 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}, 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}} + ] = state |> get_line_vars(6) - assert [%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}} + ] = 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}], 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) - assert %VarInfo{type: {:map, [asd: {:integer, 2}, zxc: {:integer, 5}], {:variable, :var}}} = - state |> get_line_vars(12) |> Enum.find(&(&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.find(&(&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 test "struct binding" do @@ -659,13 +696,22 @@ defmodule ElixirSense.Core.MetadataBuilderTest do 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.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}} + } + ] = 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.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}} + }, + %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, :z}, {:variable, :asd}]}}, @@ -861,14 +907,14 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert [ - %VarInfo{name: :var_arg, positions: [{3, 12}], scope_id: 2}, - %VarInfo{name: :var_in1, positions: [{4, 5}], scope_id: 3}, - %VarInfo{name: :var_in2, positions: [{5, 5}], scope_id: 3} + %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: 4}, - %VarInfo{name: :var_arg, positions: [{3, 12}], scope_id: 2} + %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 @@ -889,55 +935,19 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert [ - %VarInfo{name: :par1, positions: [{3, 20}], scope_id: 2}, - %VarInfo{name: :par2, positions: [{3, 33}], scope_id: 2}, - %VarInfo{name: :par3, positions: [{3, 39}], scope_id: 2}, - %VarInfo{name: :par4, positions: [{3, 51}], scope_id: 2}, - %VarInfo{name: :var_in1, positions: [{4, 5}], scope_id: 3}, - %VarInfo{name: :var_in2, positions: [{5, 5}], 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}], scope_id: 2} + %VarInfo{name: :arg, positions: [{8, 14}, {8, 24}], scope_id: 5} ] = state |> get_line_vars(8) end - test "guards do not define vars" do - state = - """ - defmodule MyModule do - def func1(a) when is_integer(b) do - IO.puts("") - end - def func2(a) when is_integer(b) or is_list(c) do - IO.puts("") - end - def func3(a) when is_integer(b) when is_list(c) do - IO.puts("") - end - - case x do - y when is_integer(z) -> - IO.puts("") - end - - with x when is_integer(y) <- z do - IO.puts("") - end - - def func3(a) when is_integer(b) - end - """ - |> string_to_state - - assert [%VarInfo{name: :a, positions: [{2, 13}], scope_id: 2}] = state |> get_line_vars(3) - assert [%VarInfo{name: :a, positions: [{5, 13}], scope_id: 2}] = state |> get_line_vars(6) - assert [%VarInfo{name: :a, positions: [{8, 13}], scope_id: 2}] = state |> get_line_vars(9) - assert [%VarInfo{name: :y, positions: [{13, 5}], scope_id: 7}] = state |> get_line_vars(14) - assert [%VarInfo{name: :x, positions: [{17, 8}], scope_id: 8}] = state |> get_line_vars(18) - assert [%VarInfo{name: :a, positions: [{21, 13}], scope_id: 2}] = state |> get_line_vars(21) - end - test "rebinding vars" do state = """ @@ -956,7 +966,9 @@ defmodule ElixirSense.Core.MetadataBuilderTest do vars = state |> get_line_vars(6) assert [ - %VarInfo{name: :var1, positions: [{3, 19}, {3, 37}, {4, 5}, {5, 5}], scope_id: 3} + %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 @@ -1391,7 +1403,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do ] = get_line_vars(state, 8) assert [ - %VarInfo{name: :func_var, positions: [{10, 7}], scope_id: 5} + %VarInfo{name: :func_var, positions: [{10, 7}], scope_id: 6} ] = get_line_vars(state, 11) assert [ @@ -1423,9 +1435,9 @@ defmodule ElixirSense.Core.MetadataBuilderTest do |> string_to_state assert [ - %VarInfo{is_definition: true, name: :abc, positions: [{3, 6}], scope_id: 3}, - %VarInfo{is_definition: true, name: :my_var, positions: [{2, 13}], scope_id: 2}, - %VarInfo{is_definition: true, name: :x, positions: [{2, 43}, {3, 14}], scope_id: 2} + %VarInfo{is_definition: true, name: :abc, positions: [{3, 6}], scope_id: 4}, + %VarInfo{is_definition: true, name: :my_var, positions: [{2, 13}], scope_id: 3}, + %VarInfo{is_definition: true, name: :x, positions: [{2, 43}, {3, 14}], scope_id: 3} ] = state |> get_line_vars(4) end diff --git a/test/elixir_sense/definition_test.exs b/test/elixir_sense/definition_test.exs index 99a5b1e7..86b23d23 100644 --- a/test/elixir_sense/definition_test.exs +++ b/test/elixir_sense/definition_test.exs @@ -161,7 +161,7 @@ defmodule ElixirSense.Providers.DefinitionTest do buffer = """ defmodule MyModule do def main, do: my_func("a", "b") - # ^ + # ^ def my_func, do: "not this one" def my_func(a, b), do: a <> b end @@ -408,7 +408,7 @@ defmodule ElixirSense.Providers.DefinitionTest do assert ElixirSense.definition(buffer, 6, 13) == %Location{ type: :variable, file: nil, - line: 3, + line: 5, column: 5 } @@ -424,7 +424,7 @@ defmodule ElixirSense.Providers.DefinitionTest do buffer = """ defmodule MyModule do def func do - var1 = + var1 = 1 end end @@ -518,6 +518,167 @@ defmodule ElixirSense.Providers.DefinitionTest do } end + test "find definition for a redefined variable" do + buffer = """ + defmodule MyModule do + def my_fun(var) do + var = 1 + var + + var + end + end + """ + + # `var` defined in the function header + assert ElixirSense.definition(buffer, 3, 15) == %Location{ + type: :variable, + file: nil, + line: 2, + column: 14 + } + + # `var` redefined in the function body + assert ElixirSense.definition(buffer, 5, 5) == %Location{ + type: :variable, + file: nil, + line: 3, + column: 5 + } + end + + test "find definition of a variable in a guard" do + buffer = """ + defmodule MyModule do + def my_fun(var) when is_atom(var) do + case var do + var when var > 0 -> var + end + + Enum.map([1, 2], fn x when x > 0 -> x end) + end + end + """ + + assert ElixirSense.definition(buffer, 2, 32) == %Location{ + type: :variable, + file: nil, + line: 2, + column: 14 + } + + assert ElixirSense.definition(buffer, 4, 16) == %Location{ + type: :variable, + file: nil, + line: 4, + column: 7 + } + + assert ElixirSense.definition(buffer, 7, 32) == %Location{ + type: :variable, + file: nil, + line: 7, + column: 25 + } + end + + test "find definition of variables when variable is a function parameter" do + buffer = """ + defmodule MyModule do + def my_fun([h | t]) do + sum = h + my_fun(t) + + if h > sum do + h + sum + else + h = my_fun(t) + sum + h + end + end + end + """ + + # `h` from the function header + assert ElixirSense.definition(buffer, 3, 11) == %Location{ + type: :variable, + file: nil, + line: 2, + column: 15 + } + + assert ElixirSense.definition(buffer, 6, 7) == %Location{ + type: :variable, + file: nil, + line: 2, + column: 15 + } + + # `h` from the if-else scope + assert ElixirSense.definition(buffer, 9, 7) == %Location{ + type: :variable, + file: nil, + line: 8, + column: 7 + } + + # `t` + assert ElixirSense.definition(buffer, 8, 18) == %Location{ + type: :variable, + file: nil, + line: 2, + column: 19 + } + + # `sum` + assert ElixirSense.definition(buffer, 8, 23) == %Location{ + type: :variable, + file: nil, + line: 3, + column: 5 + } + end + + test "find definition of variables from the scope of an anonymous function" do + buffer = """ + defmodule MyModule do + def my_fun(x, y) do + x = Enum.map(x, fn x -> x + y end) + end + end + """ + + # `x` from the `my_fun` function header + assert ElixirSense.definition(buffer, 3, 18) == %Location{ + type: :variable, + file: nil, + line: 2, + column: 14 + } + + # `y` from the `my_fun` function header + assert ElixirSense.definition(buffer, 3, 33) == %Location{ + type: :variable, + file: nil, + line: 2, + column: 17 + } + + # `x` from the anonymous function + assert ElixirSense.definition(buffer, 3, 29) == %Location{ + type: :variable, + file: nil, + line: 3, + column: 24 + } + + # redefined `x` + assert ElixirSense.definition(buffer, 3, 5) == %Location{ + type: :variable, + file: nil, + line: 3, + column: 5 + } + end + test "find definition of attributes" do buffer = """ defmodule MyModule do diff --git a/test/elixir_sense/references_test.exs b/test/elixir_sense/references_test.exs index 23b98f12..636ccf83 100644 --- a/test/elixir_sense/references_test.exs +++ b/test/elixir_sense/references_test.exs @@ -729,7 +729,6 @@ defmodule ElixirSense.Providers.ReferencesTest do references = ElixirSense.references(buffer, 6, 13, trace) assert references == [ - %{uri: nil, range: %{start: %{line: 3, column: 5}, end: %{line: 3, column: 9}}}, %{uri: nil, range: %{start: %{line: 5, column: 5}, end: %{line: 5, column: 9}}}, %{uri: nil, range: %{start: %{line: 6, column: 13}, end: %{line: 6, column: 17}}} ] @@ -737,9 +736,7 @@ defmodule ElixirSense.Providers.ReferencesTest do references = ElixirSense.references(buffer, 3, 6, trace) assert references == [ - %{uri: nil, range: %{start: %{line: 3, column: 5}, end: %{line: 3, column: 9}}}, - %{uri: nil, range: %{start: %{line: 5, column: 5}, end: %{line: 5, column: 9}}}, - %{uri: nil, range: %{start: %{line: 6, column: 13}, end: %{line: 6, column: 17}}} + %{uri: nil, range: %{start: %{line: 3, column: 5}, end: %{line: 3, column: 9}}} ] end @@ -747,7 +744,7 @@ defmodule ElixirSense.Providers.ReferencesTest do buffer = """ defmodule MyModule do def func do - var1 = + var1 = 1 var1 end @@ -762,6 +759,177 @@ defmodule ElixirSense.Providers.ReferencesTest do ] end + test "find references for a redefined variable", %{trace: trace} do + buffer = """ + defmodule MyModule do + def my_fun(var) do + var = 1 + var + + var + end + end + """ + + # `var` defined in the function header + expected_references = [ + %{uri: nil, range: %{start: %{line: 2, column: 14}, end: %{line: 2, column: 17}}}, + %{uri: nil, range: %{start: %{line: 3, column: 15}, end: %{line: 3, column: 18}}} + ] + + assert ElixirSense.references(buffer, 2, 14, trace) == expected_references + assert ElixirSense.references(buffer, 3, 15, trace) == expected_references + + # `var` redefined in the function body + expected_references = [ + %{uri: nil, range: %{start: %{line: 3, column: 5}, end: %{line: 3, column: 8}}}, + %{uri: nil, range: %{start: %{line: 5, column: 5}, end: %{line: 5, column: 8}}} + ] + + assert ElixirSense.references(buffer, 3, 5, trace) == expected_references + assert ElixirSense.references(buffer, 5, 5, trace) == expected_references + end + + test "find references for a variable in a guard", %{trace: trace} do + buffer = """ + defmodule MyModule do + def my_fun(var) when is_atom(var) do + case var do + var when var > 0 -> var + end + + Enum.map([1, 2], fn x when x > 0 -> x end) + end + end + """ + + # `var` defined in the function header + expected_references = [ + %{uri: nil, range: %{start: %{line: 2, column: 14}, end: %{line: 2, column: 17}}}, + %{uri: nil, range: %{start: %{line: 2, column: 32}, end: %{line: 2, column: 35}}}, + %{uri: nil, range: %{start: %{line: 3, column: 10}, end: %{line: 3, column: 13}}} + ] + + assert ElixirSense.references(buffer, 2, 14, trace) == expected_references + assert ElixirSense.references(buffer, 2, 32, trace) == expected_references + assert ElixirSense.references(buffer, 3, 10, trace) == expected_references + + # `var` defined in the case clause + expected_references = [ + %{uri: nil, range: %{start: %{line: 4, column: 7}, end: %{line: 4, column: 10}}}, + %{uri: nil, range: %{start: %{line: 4, column: 16}, end: %{line: 4, column: 19}}}, + %{uri: nil, range: %{start: %{line: 4, column: 27}, end: %{line: 4, column: 30}}} + ] + + assert ElixirSense.references(buffer, 4, 7, trace) == expected_references + assert ElixirSense.references(buffer, 4, 16, trace) == expected_references + assert ElixirSense.references(buffer, 4, 27, trace) == expected_references + + # `x` + expected_references = [ + %{uri: nil, range: %{start: %{line: 7, column: 25}, end: %{line: 7, column: 26}}}, + %{uri: nil, range: %{start: %{line: 7, column: 32}, end: %{line: 7, column: 33}}}, + %{uri: nil, range: %{start: %{line: 7, column: 41}, end: %{line: 7, column: 42}}} + ] + + assert ElixirSense.references(buffer, 7, 25, trace) == expected_references + assert ElixirSense.references(buffer, 7, 32, trace) == expected_references + assert ElixirSense.references(buffer, 7, 41, trace) == expected_references + end + + test "find references for variable in inner scopes", %{trace: trace} do + buffer = """ + defmodule MyModule do + def my_fun([h | t]) do + sum = h + my_fun(t) + + if h > sum do + h + sum + else + h = my_fun(t) + sum + h + end + end + end + """ + + # `h` from the function header + expected_references = [ + %{uri: nil, range: %{start: %{line: 2, column: 15}, end: %{line: 2, column: 16}}}, + %{uri: nil, range: %{start: %{line: 3, column: 11}, end: %{line: 3, column: 12}}}, + %{uri: nil, range: %{start: %{line: 5, column: 8}, end: %{line: 5, column: 9}}}, + %{uri: nil, range: %{start: %{line: 6, column: 7}, end: %{line: 6, column: 8}}} + ] + + Enum.each([{2, 15}, {3, 11}, {5, 8}, {6, 7}], fn {line, column} -> + assert ElixirSense.references(buffer, line, column, trace) == expected_references + end) + + # `h` from the if-else scope + expected_references = [ + %{uri: nil, range: %{start: %{line: 8, column: 7}, end: %{line: 8, column: 8}}}, + %{uri: nil, range: %{start: %{line: 9, column: 7}, end: %{line: 9, column: 8}}} + ] + + assert ElixirSense.references(buffer, 8, 7, trace) == expected_references + assert ElixirSense.references(buffer, 9, 7, trace) == expected_references + + # `sum` + expected_references = [ + %{uri: nil, range: %{start: %{line: 3, column: 5}, end: %{line: 3, column: 8}}}, + %{uri: nil, range: %{start: %{line: 5, column: 12}, end: %{line: 5, column: 15}}}, + %{uri: nil, range: %{start: %{line: 6, column: 11}, end: %{line: 6, column: 14}}}, + %{uri: nil, range: %{start: %{line: 8, column: 23}, end: %{line: 8, column: 26}}} + ] + + Enum.each([{3, 5}, {5, 12}, {6, 11}, {8, 23}], fn {line, column} -> + assert ElixirSense.references(buffer, line, column, trace) == expected_references + end) + end + + test "find references for variable from the scope of an anonymous function", %{trace: trace} do + buffer = """ + defmodule MyModule do + def my_fun(x, y) do + x = Enum.map(x, fn x -> x + y end) + end + end + """ + + # `x` from the `my_fun` function header + expected_references = [ + %{uri: nil, range: %{start: %{line: 2, column: 14}, end: %{line: 2, column: 15}}}, + %{uri: nil, range: %{start: %{line: 3, column: 18}, end: %{line: 3, column: 19}}} + ] + + assert ElixirSense.references(buffer, 2, 14, trace) == expected_references + assert ElixirSense.references(buffer, 3, 18, trace) == expected_references + + # `y` from the `my_fun` function header + expected_references = [ + %{uri: nil, range: %{start: %{line: 2, column: 17}, end: %{line: 2, column: 18}}}, + %{uri: nil, range: %{start: %{line: 3, column: 33}, end: %{line: 3, column: 34}}} + ] + + assert ElixirSense.references(buffer, 2, 17, trace) == expected_references + assert ElixirSense.references(buffer, 3, 33, trace) == expected_references + + # `x` from the anonymous function + expected_references = [ + %{uri: nil, range: %{start: %{line: 3, column: 24}, end: %{line: 3, column: 25}}}, + %{uri: nil, range: %{start: %{line: 3, column: 29}, end: %{line: 3, column: 30}}} + ] + + assert ElixirSense.references(buffer, 3, 24, trace) == expected_references + assert ElixirSense.references(buffer, 3, 29, trace) == expected_references + + # redefined `x` + expected_references = [ + %{uri: nil, range: %{start: %{line: 3, column: 5}, end: %{line: 3, column: 6}}} + ] + + assert ElixirSense.references(buffer, 3, 5, trace) == expected_references + end + test "find references of attributes", %{trace: trace} do buffer = """ defmodule MyModule do diff --git a/test/elixir_sense/suggestions_test.exs b/test/elixir_sense/suggestions_test.exs index 222f89a6..2dfbf9e6 100644 --- a/test/elixir_sense/suggestions_test.exs +++ b/test/elixir_sense/suggestions_test.exs @@ -1295,10 +1295,7 @@ defmodule ElixirSense.SuggestionsTest do assert_received {:result, list} - assert list == [ - %{name: "arg", type: :variable}, - %{name: "my", type: :variable} - ] + assert list == [%{name: "arg", type: :variable}] end test "lists params in fn's not finished" do