-
Notifications
You must be signed in to change notification settings - Fork 172
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
Support Hover, Completion, Go to definition for Class Variables #2944
base: main
Are you sure you want to change the base?
Changes from 10 commits
a53b2fe
d7b3b31
37ba38e
823ff6e
c6583c0
fd97ea1
f48298a
80a7595
28f2c68
358972d
6afa177
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -80,6 +80,11 @@ def initialize(index, dispatcher, parse_result, uri, collect_comments: false) | |
:on_instance_variable_or_write_node_enter, | ||
:on_instance_variable_target_node_enter, | ||
:on_alias_method_node_enter, | ||
:on_class_variable_and_write_node_enter, | ||
:on_class_variable_operator_write_node_enter, | ||
:on_class_variable_or_write_node_enter, | ||
:on_class_variable_target_node_enter, | ||
:on_class_variable_write_node_enter, | ||
) | ||
end | ||
|
||
|
@@ -434,6 +439,31 @@ def on_alias_method_node_enter(node) | |
) | ||
end | ||
|
||
sig { params(node: Prism::ClassVariableAndWriteNode).void } | ||
def on_class_variable_and_write_node_enter(node) | ||
handle_class_variable(node, node.name_loc) | ||
end | ||
|
||
sig { params(node: Prism::ClassVariableOperatorWriteNode).void } | ||
def on_class_variable_operator_write_node_enter(node) | ||
handle_class_variable(node, node.name_loc) | ||
end | ||
|
||
sig { params(node: Prism::ClassVariableOrWriteNode).void } | ||
def on_class_variable_or_write_node_enter(node) | ||
handle_class_variable(node, node.name_loc) | ||
end | ||
|
||
sig { params(node: Prism::ClassVariableTargetNode).void } | ||
def on_class_variable_target_node_enter(node) | ||
handle_class_variable(node, node.location) | ||
end | ||
|
||
sig { params(node: Prism::ClassVariableWriteNode).void } | ||
def on_class_variable_write_node_enter(node) | ||
handle_class_variable(node, node.name_loc) | ||
end | ||
|
||
sig do | ||
params( | ||
name: String, | ||
|
@@ -552,6 +582,40 @@ def handle_global_variable(node, loc) | |
)) | ||
end | ||
|
||
sig do | ||
params( | ||
node: T.any( | ||
Prism::ClassVariableAndWriteNode, | ||
Prism::ClassVariableOperatorWriteNode, | ||
Prism::ClassVariableOrWriteNode, | ||
Prism::ClassVariableTargetNode, | ||
Prism::ClassVariableWriteNode, | ||
), | ||
loc: Prism::Location, | ||
).void | ||
end | ||
def handle_class_variable(node, loc) | ||
name = node.name.to_s | ||
return if name == "@@" | ||
|
||
comments = collect_comments(node) | ||
|
||
owner = @owner_stack.last | ||
|
||
# set the class variable's owner to the attached context when defined within a singleton scope. | ||
if owner.is_a?(Entry::SingletonClass) | ||
owner = @owner_stack.reverse.find { |entry| !entry.name.include?("<Class:") } | ||
end | ||
|
||
@index.add(Entry::ClassVariable.new( | ||
name, | ||
@uri, | ||
Location.from_prism_location(loc, @code_units_cache), | ||
comments, | ||
owner, | ||
)) | ||
Comment on lines
+612
to
+618
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe we actually need to add the class variables to both the attached and the first level singleton class. For example: # Inside singleton context
class Foo
class << self
@@hello = 123
end
end
Foo.class_variables # => [:@@hello]
Foo.singleton_class.class_variables # => [:@@hello]
Foo.singleton_class.singleton_class.class_variables # => []
# Outside singleton context
class Bar
@@hello = 123
end
Bar.class_variables # => [:@@hello]
Bar.singleton_class.class_variables # => [:@@hello]
Bar.singleton_class.singleton_class.class_variables # => [] So it seems that we need to always add to both. Can you please add a test for that? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, should we push two entries: one for the singleton class and another for the base class? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, correct. That will make the features properly find the variable both in a singleton context or in the attached class context, since it is available for both. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have updated the logic to add class variable entries to both the singleton class and the base class. def handle_class_variable(node, loc)
name = node.name.to_s
return if name == "@@"
comments = collect_comments(node)
owner_stack = @owner_stack.reverse
owner = owner_stack.shift
@index.add(Entry::ClassVariable.new(
name,
@uri,
Location.from_prism_location(loc, @code_units_cache),
comments,
owner,
))
case owner
when Entry::SingletonClass
owner_stack.each do |owner|
@index.add(Entry::ClassVariable.new(
name,
@uri,
Location.from_prism_location(loc, @code_units_cache),
comments,
owner,
))
break unless owner.is_a?(Entry::SingletonClass)
end
when Entry::Class
@index.add(Entry::ClassVariable.new(
name,
@uri,
Location.from_prism_location(loc, @code_units_cache),
comments,
@index.existing_or_new_singleton_class(owner.name),
))
end
end If the owner is a class node, I add a class variable entry to both its class and its singleton class. If the node is a singleton class, I reverse the owner stack and continue adding the class variable to the owners in the stack, stopping once I reach the second owner that is not a singleton class. Bubbling up works as expected, but I am uncertain how to implement bubbling in. Screencast.from.15-12-24.12.23.09.AM.IST.webmThere was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand why we need to go through the owner stack in reverse. Can we not do something like this? owner = current_owner
return unless owner
if owner.is_a?(Entry::SingletonClass)
# If we're inside a singleton context of a class or module, add the
# variable to the attached namespace
@index.add(Entry::ClassVariable.new(
name,
@uri,
Location.from_prism_location(loc, @code_units_cache),
comments,
@index.attached_class(owner.name),
))
else
# If we're inside a class or a module, add the variable to the singleton
# class of that namespace
@index.add(Entry::ClassVariable.new(
name,
@uri,
Location.from_prism_location(loc, @code_units_cache),
comments,
@index.existing_or_new_singleton_class(owner.name),
))
end And then you essentially need to implement def attached_class(name)
# Build the attached name excluding the last portion, which for singletons
# is always `<Class:Foo>`
parts = name.split("::")
attached_name = parts[...-1].join("::")
# Return the existing namespaces for the attached name
self[attached_name]&.grep(Entry::Namespace)
end There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not entirely confident about the singleton context and feel a bit confused regarding how our DeclarartionListener and TypeInferrer interpret it. Consider the example below. class Foo
# The declaration listener interprets the owner here as "Foo".
# The type inferrer interprets the owner here as "Foo::<Class:Foo>".
@@aa = 1
class << self
# The declaration listener interprets the owner here as "Foo::<Class:Foo>".
# The type inferrer interprets the owner here as "Foo::<Class:Foo>::<Class:<Class:Foo>>".
@@ab = 2
def bar
# The declaration listener interprets the owner here as "Foo::<Class:Foo>".
# The type inferrer interprets the owner here as "Foo::<Class:Foo>".
@@ac = 3
end
end
def foobar
# The declaration listener interprets the owner here as "Foo".
# The type inferrer interprets the owner here as "Foo".
@@ad = 5
end
end I reversed the owner stack based on the owner names inferred by the TypeInferrer. For instance, at the line where @@ab is defined, the owner stack would appear as [Foo, Foo::Class:Foo, Foo::Class:Foo::<Class:Class:Foo>]. Given that class variables are shared across these contexts, I reversed the stack and added a class variable entry for @@ab to each owner until encountering a class entry, using an each loop in a do-while manner. However, after reviewing your comments, I debugged the DeclarationListener and found that its inferred context differs from the TypeInferrer's inferred context. This discrepancy might be the source of confusion. This discrepancy has left me wondering, am I missing something or misunderstanding how these contexts works? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand the confusion now. The changes in the type inferrer are assuming that class variables will behave exactly like instance variables, which is likely the source of the issue. I think we may be able to do this:
class Foo
# The inferred `self` here is `Foo` for class variables
@@var = 123
class << self
# The inferred `self` here is also `Foo` for class variables
@@other = 123
end
end I think that should lead to all of the features working as expected and it will simplify the declaration listener. Let me know if you find that this is not the right way. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have made the changes as per your suggestion. |
||
end | ||
|
||
sig do | ||
params( | ||
node: T.any( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
# typed: true | ||
# frozen_string_literal: true | ||
|
||
require_relative "test_case" | ||
|
||
module RubyIndexer | ||
class ClassVariableTest < TestCase | ||
def test_class_variable_and_write | ||
index(<<~RUBY) | ||
class Foo | ||
@@bar &&= 1 | ||
end | ||
RUBY | ||
|
||
assert_entry("@@bar", Entry::ClassVariable, "/fake/path/foo.rb:1-2:1-7") | ||
|
||
entry = T.must(@index["@@bar"]&.first) | ||
owner = T.must(entry.owner) | ||
assert_instance_of(Entry::Class, owner) | ||
assert_equal("Foo", owner.name) | ||
end | ||
|
||
def test_class_variable_operator_write | ||
index(<<~RUBY) | ||
class Foo | ||
@@bar += 1 | ||
end | ||
RUBY | ||
|
||
assert_entry("@@bar", Entry::ClassVariable, "/fake/path/foo.rb:1-2:1-7") | ||
end | ||
|
||
def test_class_variable_or_write | ||
index(<<~RUBY) | ||
class Foo | ||
@@bar ||= 1 | ||
end | ||
RUBY | ||
|
||
assert_entry("@@bar", Entry::ClassVariable, "/fake/path/foo.rb:1-2:1-7") | ||
end | ||
|
||
def test_class_variable_target_node | ||
index(<<~RUBY) | ||
class Foo | ||
@@foo, @@bar = 1 | ||
end | ||
RUBY | ||
|
||
assert_entry("@@foo", Entry::ClassVariable, "/fake/path/foo.rb:1-2:1-7") | ||
assert_entry("@@bar", Entry::ClassVariable, "/fake/path/foo.rb:1-9:1-14") | ||
|
||
entry = T.must(@index["@@foo"]&.first) | ||
owner = T.must(entry.owner) | ||
assert_instance_of(Entry::Class, owner) | ||
assert_equal("Foo", owner.name) | ||
|
||
entry = T.must(@index["@@bar"]&.first) | ||
owner = T.must(entry.owner) | ||
assert_instance_of(Entry::Class, owner) | ||
assert_equal("Foo", owner.name) | ||
end | ||
|
||
def test_class_variable_write | ||
index(<<~RUBY) | ||
class Foo | ||
@@bar = 1 | ||
end | ||
RUBY | ||
|
||
assert_entry("@@bar", Entry::ClassVariable, "/fake/path/foo.rb:1-2:1-7") | ||
end | ||
|
||
def test_empty_name_class_variable | ||
index(<<~RUBY) | ||
module Foo | ||
@@ = 1 | ||
end | ||
RUBY | ||
|
||
refute_entry("@@") | ||
end | ||
|
||
def test_top_level_class_variable | ||
index(<<~RUBY) | ||
@foo = 123 | ||
RUBY | ||
|
||
entry = T.must(@index["@foo"]&.first) | ||
assert_nil(entry.owner) | ||
end | ||
|
||
def test_class_variable_inside_self_method | ||
index(<<~RUBY) | ||
class Foo | ||
def self.bar | ||
@@bar = 123 | ||
end | ||
end | ||
RUBY | ||
|
||
entry = T.must(@index["@@bar"]&.first) | ||
owner = T.must(entry.owner) | ||
assert_instance_of(Entry::Class, owner) | ||
assert_equal("Foo", owner.name) | ||
end | ||
|
||
def test_class_variable_inside_singleton_class | ||
index(<<~RUBY) | ||
class Foo | ||
class << self | ||
@@bar = 123 | ||
end | ||
end | ||
RUBY | ||
|
||
entry = T.must(@index["@@bar"]&.first) | ||
owner = T.must(entry.owner) | ||
assert_instance_of(Entry::Class, owner) | ||
assert_equal("Foo", owner.name) | ||
end | ||
|
||
def test_class_variable_in_singleton_class_method | ||
index(<<~RUBY) | ||
class Foo | ||
class << self | ||
def self.bar | ||
@@bar = 123 | ||
end | ||
end | ||
end | ||
RUBY | ||
|
||
entry = T.must(@index["@@bar"]&.first) | ||
owner = T.must(entry.owner) | ||
assert_instance_of(Entry::Class, owner) | ||
assert_equal("Foo", owner.name) | ||
end | ||
end | ||
end |
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.
Let's add a comment to explain that this isn't valid Ruby, but the code could be temporarily in this state.