-
Notifications
You must be signed in to change notification settings - Fork 41
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
Fix variable references #177
Conversation
Excellent work @sheldak. I think you nailed most of the issues here. I once tried to fix #124 but got lost in the amount of things that were not correctly handled.
Yes, IIRC I had a similar idea back then
I didn't initially see that as necessary but you are right. SSA
Nice catch |
%{name: "arg", type: :variable}, | ||
%{name: "my", type: :variable} | ||
] | ||
assert list == [%{name: "arg", type: :variable}] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice
] = state |> get_line_vars(8) | ||
end | ||
|
||
test "guards do not define vars" do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added that test in 1037edf but looking at it now it was a workaround instead of a fix
@@ -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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
@sheldak Great work, I didn't find any issues. I hope you are still interested in fixing the pin issue |
Thanks, that's great. Yes, but it will probably take me a few weeks since I can only work part-time on this. |
I've made an attempt to fix the reference provider (Issue #124). I've described part of the changes below since they needed to be applied in several places. After that, the provider should cover many cases that are handled incorrectly in the current implementation.
1. A new scope for a function
State.new_func_vars_scope
had a different behavior thanState.new_vars_scope
- the first one didn't increase the scope counter what I've changed. Previously all variables from all function headers were in the same scope. Now, it is changed and every function creates two new scopes:do ... end
differently for the functions; this part was already there)2. Preserving variables when leaving a scope
I created a new function
State.maybe_move_vars_to_outer_scope
. It is called in these three places:MetadataBuilder.post_scope_keyword
- after processing:for
,:fn
and:with
keywordsMetadataBuilder.post_block_keyword
- after processing:do
,:else
,:rescue
,:catch
and:after
keywordsMetadataBuilder.post_clause
- after processing->
so whenever a scope is deleted and it may be needed to move part of the variables to the outer scope to extract all the references later. The function takes all variables in the scope we are deleting, filters out the ones that were defined inside the scope, and puts the rest of them into the outer scope.
3. Vars info per scope id
Calculating
vars_info_per_scope_id
field in theState
struct needed to be changed to keep all variables that are not only defined, but also available in the particular scope. Thus, it was needed to consider all outer scopes during the update ofvars_info_per_scope_id
field.4. Redefining variables
Variables defined in different scopes must be treated as different ones. E.g.
To make it more consistent I decided to extend the above to treat every reassignment as creating a new variable. E.g.
Also, if we consider renaming based on references, we may obtain a tool more powerful than using just regex.
This required differentiating between variables with the same names so in many places variables started to be differentiated by their positions. This propagated to
ElixirSense.Providers.References.find
andElixirSense.Providers.Definition.find
functions - finding proper definitions and references required checking the position of the variable as well. That led to extending the parameters of these functions by a line and a column.5. Assignments
Since we can have many variables with the same name, I rely on their order in
vars
andscope_vars
fields in theState
struct. The variables are "reduced" by merging the ones with the same name until getting one withis_definition: true
. Then, the next occurrence of a variable with the same name will be treated as a separate variable. Reduction is done from the newest to the oldest occurrences of variables. However, in the case of the assignments (:=
and:<-
), we want to add a new definition of the variable after processing the statement. Let's consider an example:We don't want to treat the first
x
in the second line as the definition of the secondx
in that line. Thus, I moved processing the left side of assignments to thepost
function.6. Guards
Previously guards were omitted in the AST traversal so variables in them were not tracked. I've changed that.
TODO
The pin operator (
^
) is not taken into consideration so variables can be wrongly marked as definitions. E.g.variable
b
will be treated as redefined in the case clause, but in fact, it is the same unchanged variable as the one defined in the function header. I suggest fixing it in a follow-up PR.I'm ready to work more on that in case I omitted or broke something, so I'm waiting for the feedback.