Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix variable references #177

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 21 additions & 8 deletions lib/elixir_sense.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope that's easily switchable to Code.Fragment when it's time to merge #170

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If Code.Fragment returns the same line and column, it should be fine

nil ->
nil

subject ->
{subject, {line, col}} ->
buffer_file_metadata = Parser.parse_string(code, true, true, line)

env = Metadata.get_env(buffer_file_metadata, line)
Expand All @@ -112,6 +113,7 @@ defmodule ElixirSense do
Definition.find(
subject,
line,
col,
env,
buffer_file_metadata.mods_funs_to_positions,
calls,
Expand Down Expand Up @@ -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,
Expand Down
128 changes: 90 additions & 38 deletions lib/elixir_sense/core/metadata_builder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -207,14 +207,19 @@ 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)
|> new_alias_scope
|> 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
Expand Down Expand Up @@ -256,6 +261,7 @@ defmodule ElixirSense.Core.MetadataBuilder do
end

state
|> maybe_move_vars_to_outer_scope
|> remove_vars_scope
|> result(ast)
end
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/elixir_sense/core/source.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading